kiedy używać funkcji inline w Kotlinie?

108

Wiem, że funkcja wbudowana może poprawić wydajność i spowodować wzrost generowanego kodu, ale nie jestem pewien, kiedy należy jej użyć.

lock(l) { foo() }

Zamiast tworzyć obiekt funkcji dla parametru i generować wywołanie, kompilator może wyemitować następujący kod. ( Źródło )

l.lock()
try {
  foo()
}
finally {
  l.unlock()
}

ale odkryłem, że nie ma obiektu funkcji utworzonego przez kotlin dla funkcji nieliniowej. czemu?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
    lock.lock();
    try {
        block();
    } finally {
        lock.unlock();
    }
}
holi-java
źródło
7
Istnieją dwa główne przypadki użycia, jeden dotyczy pewnych typów funkcji wyższego rzędu, a drugi to parametry typu reifikowanego. Dokumentacja funkcji inline obejmuje: kotlinlang.org/docs/reference/inline-functions.html
zsmb13
2
@ zsmb13 dzięki, sir. ale nie rozumiem tego: „Zamiast tworzyć obiekt funkcji dla parametru i generować wywołanie, kompilator mógłby wyemitować następujący kod”
holi-java
2
nie rozumiem tego przykładu tbh.
filthy_wizard,
Dlaczego mówisz, że inlinefunkcja może poprawić wydajność i że byłaby zauważalna przez użytkownika?
IgorGanapolsky

Odpowiedzi:

285

Powiedzmy, że tworzysz funkcję wyższego rzędu, która przyjmuje lambdę typu () -> Unit(bez parametrów, bez wartości zwracanej) i wykonuje ją w następujący sposób:

fun nonInlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

W języku Java przełoży się to na coś takiego (uproszczone!):

public void nonInlined(Function block) {
    System.out.println("before");
    block.invoke();
    System.out.println("after");
}

A kiedy dzwonisz z Kotlina ...

nonInlined {
    println("do something here")
}

Pod maską Functionzostanie utworzona tutaj instancja will, która otacza kod wewnątrz lambdy (znowu jest to uproszczone):

nonInlined(new Function() {
    @Override
    public void invoke() {
        System.out.println("do something here");
    }
});

Więc w zasadzie wywołanie tej funkcji i przekazanie do niej lambdy zawsze tworzy instancję Functionobiektu.


Z drugiej strony, jeśli używasz inlinesłowa kluczowego:

inline fun inlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

Kiedy nazywasz to w ten sposób:

inlined {
    println("do something here")
}

Żadna Functioninstancja nie zostanie utworzona, zamiast tego kod wokół wywołania blockfunkcji wbudowanej zostanie skopiowany do witryny wywołania, więc w kodzie bajtowym otrzymasz coś takiego:

System.out.println("before");
System.out.println("do something here");
System.out.println("after");

W takim przypadku nie są tworzone żadne nowe instancje.

zsmb13
źródło
21
Jaka jest zaleta posiadania opakowania obiektu Function w pierwszej kolejności? tj. - dlaczego nie wszystko jest wstawione?
Arturs Vancans
14
W ten sposób możesz również dowolnie przekazywać funkcje jako parametry, przechowywać je w zmiennych itp.
zsmb13
2
Możesz, a jeśli robisz z nimi skomplikowane rzeczy, w końcu będziesz chciał dowiedzieć się o słowach kluczowych noinlinei crossinline- zobacz dokumentację .
zsmb13
2
Dokumenty podają powód, dla którego nie chcesz domyślnie wstawiać w tekście
CorayThan
2
Ale nie odpowiedziałeś na pytanie „Kiedy używać”. Właśnie wyjaśniłeś, jak to jest używane. Odpowiedź na to pytanie znajduje się w dokumentacji i wskazuje, że jeśli masz funkcję z małą ilością kodu i funkcja jest często używana, użycie funkcji wbudowanej może zwiększyć wydajność.
AndroidDev
45

Dodam: kiedy nie używaćinline :

  1. Jeśli masz prostą funkcję, która nie akceptuje innych funkcji jako argumentu, nie ma sensu ich wstawiać. IntelliJ ostrzeże Cię:

    Oczekiwany wpływ na wydajność wstawiania „…” jest nieznaczny. Inlining działa najlepiej w przypadku funkcji o parametrach typów funkcjonalnych

  2. Nawet jeśli masz funkcję „z parametrami typów funkcjonalnych”, możesz napotkać kompilator, który poinformuje Cię, że wstawianie nie działa. Rozważmy ten przykład:

     inline fun calculateNoInline(param: Int, operation: IntMapper): Int {
         val o = operation //compiler does not like this
         return o(param)
     }

    Ten kod nie skompiluje się, powodując błąd:

    Niedozwolone użycie parametru wbudowanego „operacja” w „...”. Dodaj modyfikator „noinline” do deklaracji parametru.

    Powodem jest to, że kompilator nie może wstawić tego kodu, zwłaszcza operationparametru. Jeśli operationnie jest opakowany w obiekt (co byłoby wynikiem zastosowania inline), jak w ogóle można go przypisać do zmiennej? W tym przypadku kompilator sugeruje przedstawienie argumentu noinline. Posiadanie inlinefunkcji z pojedynczą noinlinefunkcją nie ma sensu, nie rób tego. Jeśli jednak istnieje wiele parametrów typów funkcjonalnych, rozważ wstawienie niektórych z nich, jeśli jest to wymagane.

Oto kilka sugerowanych zasad:

  • Państwo może inline gdy wszystkie parametry funkcjonalne typu nazywane są bezpośrednio lub przekazywane do innych inline funkcji
  • Państwo powinno inline kiedy ^ jest.
  • Nie można wstawiać, gdy parametr funkcji jest przypisywany do zmiennej wewnątrz funkcji
  • Państwo powinno rozważyć inline, jeżeli co najmniej jedna z twoich funkcjonalnych parametrów typu mogą być wstawiane, wykorzystania noinlinedla innych.
  • Nie powinieneś wstawiać ogromnych funkcji, pomyśl o wygenerowanym kodzie bajtowym. Zostanie skopiowany do wszystkich miejsc, z których wywoływana jest funkcja.
  • Innym przypadkiem użycia są reifiedparametry typu, które wymagają użycia inline. Przeczytaj tutaj .
s1m0nw1
źródło
5
z technicznego punktu widzenia nadal można by wbudować funkcje, które nie przyjmują wyrażeń lambda, prawda? .. tutaj zaletą jest to, że w tym przypadku unika się narzutu wywołań funkcji .. język taki jak Scala na to pozwala .. nie wiem, dlaczego Kotlin zabrania tego typu wbudowanych ing
rogue-one
4
@ rogue-one Kotlin nie zabrania tym razem inliningu. Autorzy języka twierdzą po prostu, że korzyści związane z wydajnością mogą być nieznaczne. Małe metody prawdopodobnie zostaną już wbudowane przez JVM podczas optymalizacji JIT, zwłaszcza jeśli są wykonywane często. Innym przypadkiem, w którym inlinemoże być szkodliwy, jest wielokrotne wywoływanie parametru funkcjonalnego w funkcji wbudowanej, na przykład w różnych gałęziach warunkowych. Właśnie natknąłem się na przypadek, w którym cały kod bajtowy dla argumentów funkcjonalnych był z tego powodu kopiowany.
Mike Hill
Kiedy powinniśmy używać crossinlineparametrów?
IgorGanapolsky
5

Najważniejszym przypadkiem, gdy używamy modyfikatora inline, jest definiowanie funkcji użytkowych z funkcjami parametrycznymi. Gromadzenie lub przetwarzanie ciąg (jak filter, maplub joinToString) lub funkcje tylko jednostkowe są doskonałym przykładem.

Dlatego modyfikator wbudowany jest głównie ważną optymalizacją dla programistów bibliotek. Powinni wiedzieć, jak to działa i jakie są jego ulepszenia i koszty. Powinniśmy używać inline modyfikatora w naszych projektach, kiedy definiujemy własne funkcje util z parametrami typu funkcji.

Jeśli nie mamy parametru typu funkcji, parametru typu reified i nie potrzebujemy nielokalnego zwrotu, najprawdopodobniej nie powinniśmy używać modyfikatora inline. Dlatego będziemy mieć ostrzeżenie na temat Android Studio lub IDEA IntelliJ.

Ponadto występuje problem z rozmiarem kodu. Wbudowanie dużej funkcji może znacznie zwiększyć rozmiar kodu bajtowego, ponieważ jest on kopiowany do każdej strony wywołania. W takich przypadkach można refaktoryzować funkcję i wyodrębniać kod do zwykłych funkcji.

0xAliHn
źródło
Co to jest zwrot nielokalny ?
IgorGanapolsky
5

Funkcje wyższego rzędu są bardzo pomocne i mogą naprawdę poprawić reusabilitykod. Jednak jednym z największych problemów związanych z ich używaniem jest wydajność. Wyrażenia lambda są kompilowane do klas (często klas anonimowych), a tworzenie obiektów w Javie to ciężka operacja. Nadal możemy efektywnie korzystać z funkcji wyższego rzędu, zachowując wszystkie korzyści, dzięki wbudowaniu funkcji.

tu pojawia się funkcja inline w obraz

Gdy funkcja jest oznaczona jako inline, podczas kompilacji kodu kompilator zastąpi wszystkie wywołania funkcji rzeczywistą treścią funkcji. Ponadto wyrażenia lambda podane jako argumenty są zastępowane ich rzeczywistą treścią. Nie będą traktowane jako funkcje, ale jako rzeczywisty kod.

W skrócie: - Inline -> zamiast być wywoływane, są zastępowane przez kod funkcji w czasie kompilacji ...

W Kotlinie użycie funkcji jako parametru innej funkcji (tzw. Funkcji wyższego rzędu) wydaje się bardziej naturalne niż w Javie.

Używanie lambd ma jednak pewne wady. Ponieważ są to klasy anonimowe (a zatem obiekty), potrzebują pamięci (a nawet mogą zwiększyć ogólną liczbę metod Twojej aplikacji). Aby tego uniknąć, możemy wprowadzić nasze metody.

fun notInlined(getString: () -> String?) = println(getString())

inline fun inlined(getString: () -> String?) = println(getString())

Z powyższego przykładu : - Te dwie funkcje robią dokładnie to samo - wypisują wynik funkcji getString. Jedna jest wstawiona, a druga nie.

Jeśli sprawdzisz zdekompilowany kod java, zobaczysz, że metody są całkowicie identyczne. Dzieje się tak, ponieważ słowo kluczowe inline jest instrukcją dla kompilatora, aby skopiować kod do witryny wywołania.

Jeśli jednak przekazujemy dowolny typ funkcji do innej funkcji, jak poniżej:

//Compile time error… Illegal usage of inline function type ftOne...
 inline fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
 }

Aby rozwiązać ten problem, możemy przepisać naszą funkcję jak poniżej:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

Załóżmy, że mamy funkcję wyższego rzędu, jak poniżej:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

W tym przypadku kompilator powie nam, abyśmy nie używali słowa kluczowego inline, gdy jest tylko jeden parametr lambda i przekazujemy go do innej funkcji. Możemy więc przepisać powyższą funkcję jak poniżej:

fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
}

Uwaga : -Musieliśmy również usunąć słowo kluczowe noinline, ponieważ może być używane tylko do funkcji wbudowanych!

Załóżmy, że mamy taką funkcję ->

fun intercept() {
    // ...
    val start = SystemClock.elapsedRealtime()
    val result = doSomethingWeWantToMeasure()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    // ...}

Działa to dobrze, ale logika funkcji jest zanieczyszczona kodem pomiarowym, co utrudnia kolegom pracę nad tym, co się dzieje. :)

Oto jak funkcja wbudowana może pomóc w tym kodzie:

 fun intercept() {
    // ...
    val result = measure { doSomethingWeWantToMeasure() }
    // ...
    }
 }

 inline fun <T> measure(action: () -> T) {
   val start = SystemClock.elapsedRealtime()
   val result = action()
   val duration = SystemClock.elapsedRealtime() - start
   log(duration)
   return result
 }

Teraz mogę skoncentrować się na czytaniu, co jest głównym celem funkcji intercept () bez pomijania linii kodu pomiarowego. Korzystamy również z możliwości ponownego wykorzystania tego kodu w innych miejscach, w których chcemy

inline pozwala na wywołanie funkcji z argumentem lambda w zamknięciu ({...}) zamiast przekazywania miary podobnej do lambda (myLamda)

Kiedy jest to przydatne?

Słowo kluczowe inline jest przydatne w przypadku funkcji, które akceptują inne funkcje lub wyrażenia lambda jako argumenty.

Bez słowa kluczowego inline w funkcji argument lambda tej funkcji jest konwertowany w czasie kompilacji na wystąpienie interfejsu funkcji za pomocą jednej metody o nazwie invoke (), a kod w wyrażeniu lambda jest wykonywany przez wywołanie metody invoke () w tej instancji funkcji wewnątrz treści funkcji.

W przypadku słowa kluczowego wbudowanego w funkcji ta konwersja podczas kompilacji nigdy nie występuje. Zamiast tego treść funkcji wbudowanej zostaje wstawiona w miejscu wywołania, a jej kod jest wykonywany bez narzutu związanego z tworzeniem instancji funkcji.

Hmmm? Przykład w Androidzie ->

Powiedzmy, że mamy funkcję w klasie routera aktywności, która uruchamia działanie i stosuje pewne dodatki

fun startActivity(context: Context,
              activity: Class<*>,
              applyExtras: (intent: Intent) -> Unit) {
  val intent = Intent(context, activity)
  applyExtras(intent)
  context.startActivity(intent)
  }

Ta funkcja tworzy intencję, stosuje pewne dodatki, wywołując argument funkcji applyExtras i rozpoczyna działanie.

Jeśli spojrzymy na skompilowany kod bajtowy i zdekompilujemy go na Javę, wygląda to mniej więcej tak:

void startActivity(Context context,
               Class activity,
               Function1 applyExtras) {
  Intent intent = new Intent(context, activity);
  applyExtras.invoke(intent);
  context.startActivity(intent);
  }

Powiedzmy, że nazywamy to z poziomu odbiornika kliknięć w działaniu:

override fun onClick(v: View) {
router.startActivity(this, SomeActivity::class.java) { intent ->
intent.putExtra("key1", "value1")
intent.putExtra("key2", 5)
}
 }

Zdekompilowany kod bajtowy tego odbiornika kliknięć wyglądałby wtedy następująco:

@Override void onClick(View v) {
router.startActivity(this, SomeActivity.class, new Function1() {
@Override void invoke(Intent intent) {
  intent.putExtra("key1", "value1");
  intent.putExtra("key2", 5);
}
 }
}

Nowa instancja funkcji Function1 jest tworzona za każdym razem, gdy jest uruchamiany odbiornik kliknięć. To działa dobrze, ale nie jest idealne!

Teraz po prostu dodajmy inline do naszej metody routera aktywności:

inline fun startActivity(context: Context,
                     activity: Class<*>,
                     applyExtras: (intent: Intent) -> Unit) {
 val intent = Intent(context, activity)
 applyExtras(intent)
 context.startActivity(intent)
 }

Bez zmiany naszego kodu odbiornika kliknięć możemy teraz uniknąć tworzenia tej instancji Function1. Odpowiednik Java kodu nasłuchiwania kliknięć wyglądałby teraz mniej więcej tak:

@Override void onClick(View v) {
Intent intent = new Intent(context, SomeActivity.class);
intent.putExtra("key1", "value1");
intent.putExtra("key2", 5);
context.startActivity(intent);
}

Otóż ​​to.. :)

Umieszczanie funkcji w tekście oznacza po prostu skopiowanie jej treści i wklejenie w miejscu wywołania funkcji. Dzieje się to w czasie kompilacji.

Wini
źródło
Nie widzę korzyści z używania inline functions. Czy możesz podać przykład użycia ze świata rzeczywistego (tj. W prawdziwym projekcie, nad którym pracowałeś)?
IgorGanapolsky
1
@IgorGanapolsky dodany z przykładem Androida Zobacz
Wini
3

Jednym prostym przypadkiem, w którym możesz chcieć, jest utworzenie funkcji util, która przyjmuje blok zawieszenia. Rozważ to.

fun timer(block: () -> Unit) {
    // stuff
    block()
    //stuff
}

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

    // This is an error
    timer { asyncLogic() }
}

W takim przypadku nasz zegar nie zaakceptuje funkcji wstrzymania. Aby go rozwiązać, możesz ulec pokusie, aby go również zawiesić

suspend fun timer(block: suspend () -> Unit) {
    // stuff
    block()
    // stuff
}

Ale wtedy można go używać tylko z samych programów / funkcji wstrzymania. Następnie utworzysz wersję asynchroniczną i nie-asynchroniczną tych narzędzi. Problem znika, jeśli umieścisz go w linii.

inline fun timer(block: () -> Unit) {
    // stuff
    block()
    // stuff
}

fun main() {
    // timer can be used from anywhere now
    timer { logic() }

    launch {
        timer { asyncLogic() }
    }
}

Oto plac zabaw kotlin ze stanem błędu. Ustaw licznik czasu w linii, aby go rozwiązać.

Anthony Naddeo
źródło
Twój fragment kotlin plac zabaw nie używa inline fun. Czy możesz wyjaśnić?
IgorGanapolsky
@IgorGanapolsky Sprawdź ponownie ostatnią część mojego postu. Powiem ci, który z nich powinien być wbudowany, i mam przykład w odpowiedzi powyżej.
Anthony Naddeo