Funkcje inline (funkcje „w linii”, funkcje otwarte) są konceptem znanym już z C++, potem C (w standardzie C99). Jednak potem zaskakująco rzadko o nich słychać. Aż tu nagle, powracają w wielkim stylu w Kotlinie. Okazuje się bowiem, iż – po małym tuningu – są one świetnym narzędziem zwiększającym wydajność JVM’owej aplikacji. Ich nadużywanie ma jednak swoją cenę. Ale zacznijmy od początku…
Czym są Funkcje Inline?
Funkcja inline różni się od zwykłej funkcji tym, iż w miejscu jej wywołania, kompilator nie umieszcza do niej wskaźnika (jak to zwykle bywa), ale „wkleja” całą jej zawartość. Mówiąc kolokwialnie: kompilator robi copy-paste całej zawartości funkcji we wszystkich miejscach, gdzie jest ona wywoływana.
Plusem takiego rozwiązania jest nieznaczne przyspieszenie działania programu. Minusem jest wzrost rozmiaru pliku wykonywalnego. Bo jeżeli funkcja jest wywoływana w pięciu miejscach, to jej zawartość zostanie skopiowana pięć razy.
W Kotlinie, taką funkcję tworzymy przez poprzedzenie definicji funkcji modyfikatorem inline, np.:
// Definicja inline fun add(a: Int, b: Int): Int { return a+b } // Użycie val sum = add(10, 20)Co się dzieje „pod spodem”?
Żeby zobaczyć jak to naprawdę działa, zrobimy mały eksperyment: podejrzymy sobie byte-kod Kotlina, a potem zdekompilujemy go do Javy. Użyjemy poniższej klasy testowej:
class InlineTest { inline fun add(a: Int, b: Int): Int { return a+b } fun test() { val sum = add(10, 20) //(1) } }Żeby mieć punkt odniesienia, najpierw usuwamy z powyższego przykładu słówko inline. Następnie otwieramy IntelliJ, i wchodzimy w: Tools -> Kotlin -> Show Kotlin Bytecode. Gdy zaznaczymy linię (1), naszym oczom ukażą się magiczne runy:
ALOAD 0 BIPUSH 10 BIPUSH 20 INVOKEVIRTUAL com/geekydevs/myapplication/InlineTest.add (II)I ISTORE 1…które po dekompilacji do Javy wyglądają tak:
// Java int sum = this.add(10, 20);Gdy jednak z powrotem wstawimy modyfikator inline, byte-kod będzie wyglądał tak:
ALOAD 0 ASTORE 2 BIPUSH 10 ISTORE 3 BIPUSH 20 ISTORE 4…co w Javie wygląda tak:
// Java byte a = 10; byte b = 20; int sum = a+b;Czyli widać, iż to działa
Jakie nam dają korzyści?
No właśnie. Tak szczerze, jakie to nam daje korzyści? Przecież, przy dzisiejszej mocy obliczeniowej, koszt wywołania funkcji jest adekwatnie żaden.
Podręcznikowa definicja mówi, aby używać inline dla małych, często wywoływanych funkcji, i jako przykład pokazywana jest zawsze prosta funkcja wywoływana miliony razy w pętli – wtedy faktycznie widać jakieś przyspieszenie. Ale jak to się ma do rzeczywistości? Potrzebujemy bardziej praktycznych przykładów.
Na szczęście Kotlin ma w zanadrzu kilka trików
Funkcje Inline vs Lambdy
Sytuacja nabiera kolorów, gdy parametrem Twojej funkcji jest lambda.
Pamiętajmy, iż Kotlina obowiązują standardowe ograniczenia JVM. Czyli każda lambda jest obiektem – najczęściej klasą anonimową. Dodatkowo, lambdy mają dostęp do wszystkich zmiennych, które są w ich zasięgu – czyli tzw. „domknięcie” (ang. closure). Te wszystkie rzeczy trzeba przecież zaalokować. A to już może mieć spory wpływ na wydajność.
I tutaj znów z pomocą przychodzą funkcje inline. Bowiem wszystkie lambdy będące parametrami takiej funkcji, będą również umieszczone „w linii” (ang. inlined).
Rozpatrzmy to na przykładzie funkcji let() zawartej w bibliotece standardowej Kotlina. Wygląda ona tak:
// Definicja inline fun <T, R> T.let(block: (T) -> R): R = block(this) // Przykładowe użycie user?.let { println("User is not null") println("He's name is ${it.name}") }Gdyby nie była ona oznaczona jako „inline”, to lambda zostałaby skompilowana do klasy anonimowej (lub zagnieżdżonej), i ogólnie byłoby sporo nadmiarowych obiektów – czyli negatywny wpływ na wydajność. Jednak jako iż jest inline, to zdekompilowany byte-kod wygląda tak:
// Java String var6 = "User is not null"; System.out.println(var6); var6 = "He\'s name is " + user.getName(); System.out.println(var6);Genialny wynalazek, prawda?
noinline
Jeśli, z jakichś powodów, nie chcemy aby nasza lambda była „w linii” (np. chcemy jej użyć asynchronicznie), zawsze możemy ją oznaczyć modyfikatorem noinline.
Funkcje Inline vs Typy Generyczne
Druga interesująca implikacja inline w Kotlinie jest związana z typami generycznymi.
Mianowicie, funkcje inline mogą tutaj korzystać z tzw. reified generics (w wolnym tłumaczeniu: „skonkretyzowane typy generyczne”). Dają nam one możliwość wyłuskania Klasy wewnątrz funkcji, bez konieczności przekazywania obiektu Class w parametrze (co zwykle robimy w Javie).
Tutaj fajnym przykładem jest np. rejestracja bean’ów w Springu 5.0:
// Java context.registerBean(Foo.class); context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class)) ); // Kotlin context.registerBean<Foo>() context.registerBean { Bar(it.getBean<Foo>()) }Albo startowanie Aktywności w Androidzie w bibliotece Anko:
// Java startActivity(new Intent(this, SecondActivity.class)) // Kotlin + Anko startActivity<SecondActivity>()Nie wnikając za bardzo w szczegóły samego Anko, definicja funkcji startActivity() wygląda tak:
inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any>) { AnkoInternals.internalStartActivity(this, T::class.java, params) }Pomińmy nieistotne w tej chwili rzeczy: parametry vararg, oraz fakt iż funkcja ta jest rozszerzeniem. Ważne jest, iż typ generyczny poprzedzony jest słówkiem reified, co z kolei pozwala nam na użycie T::class.java, i uwalnia nas od konieczności przekazywania clazz: Class<T> w parametrze funkcji.
Podsumowanie
W mojej opinii, funkcje inline są nadspodziewanie przydatnym narzędziem na JVM. Z początku wydają się niepozorne, wręcz nieprzydatne, jednak pozwalają nam pisać bardziej czytelny kod, bez negatywnych skutków dla wydajności aplikacji.
A co Ty o tym sądzisz? Może znasz jakieś interesujące zastosowania inline, których tutaj nie poruszyłem? Podziel się swoją opinią w komentarzu