Inline funkcije u Kotlinu

1. Pregled

U Kotlinu su funkcije prvorazredni građani, pa ih možemo prosljeđivati ​​ili vraćati poput ostalih normalnih tipova. Međutim, predstavljanje ovih funkcija tijekom izvođenja ponekad može uzrokovati nekoliko ograničenja ili komplikacija u izvedbi.

U ovom uputstvu prvo ćemo nabrojiti dva naizgled nepovezana problema o lambdama i generičkim lijekovima, a zatim, nakon uvođenja Inline funkcije, vidjet ćemo kako će se riješiti obje te zabrinutosti, pa krenimo!

2. Nevolje u raju

2.1. Nadzemni lambdas u Kotlinu

Jedna od pogodnosti funkcija prvorazrednih građana u Kotlinu jest ta da ponašanje možemo prenijeti na druge funkcije. Prenošenje funkcija kao lambda dopušta nam da svoje namjere izrazimo sažetije i elegantnije, ali to je samo jedan dio priče.

Da bismo istražili tamnu stranu lambda, izumimo kotačić tako što ćemo proglasiti funkciju produženja filtar zbirke:

fun Collection.filter (predikat: (T) -> Boolean): Collection = // Izostavljeno

Sada, da vidimo kako se gornja funkcija kompajlira u Javu. Usredotočite se na predikat funkcija koja se prosljeđuje kao parametar:

javni statički završni filtar zbirke (Collection, kotlin.jvm.functions.Function1);

Primijetite kako predikat rješava se pomoću Funkcija1 sučelje?

Ako ovo zovemo u Kotlinu:

sampleCollection.filter {it == 1}

Za umotavanje lambda koda izradit će se nešto slično sljedećem:

filter (sampleCollection, nova Funkcija1 () {@Preuzmi javni logički poziv (Integer param) {return param == 1;}});

Svaki put kada proglasimo funkciju višeg reda, barem jedan primjerak tih posebnih Funkcija* stvorit će se vrste.

Zašto Kotlin to čini umjesto, recimo, koristeći invokedynamic poput Java 8-e s lambdama? Jednostavno rečeno, Kotlin se zalaže za kompatibilnost s Java 6 i invokedynamic nije dostupan do Java 7.

Ali tu nije kraj. Kao što bismo mogli pretpostaviti, samo stvaranje instance tipa nije dovoljno.

Da bi se zapravo izvršila operacija kapsulirana u Kotlinovom lambda-u, funkcija višeg reda - filtar u ovom slučaju - morat će nazvati posebnu metodu imenovanu prizivati na novoj instanci. Rezultat je veći zbog dodatnih poziva.

Dakle, samo da rezimiramo, kad prenosimo lambda-u funkciju, ispod haube se događa sljedeće:

  1. Barem jedan primjerak posebnog tipa kreira se i pohranjuje u hrpu
  2. Uvijek će se dogoditi dodatni poziv metode

Još jedna dodjela instance i još jedan poziv virtualne metode ne čini se tako loše, zar ne?

2.2. Zatvaranja

Kao što smo vidjeli ranije, kada proslijedimo lambda-u funkciju, stvorit će se instanca tipa funkcije, slično kao anonimne unutarnje klase na Javi.

Baš kao i kod potonjeg, lambda izraz može pristupiti svom zatvaranje, odnosno varijable deklarirane u vanjskom opsegu. Kad lambda uhvati varijablu iz njezina zatvaranja, Kotlin sprema varijablu zajedno s hvatanjem lambda koda.

Dodjela dodatne memorije postaje još gora kada lambda uhvati varijablu: JVM stvara instancu tipa funkcije na svakom pozivu. Za nehvatanje lambda, postojat će samo jedan primjerak, a jednokrevetna, tih tipova funkcija.

Kako smo tako sigurni u ovo? Izmislimo još jedan kotačić deklarirajući funkciju koja će primijeniti funkciju na svaki element kolekcije:

zabavna kolekcija.svaki (blok: (T) -> Jedinica) {za (e u ovom) blok (e)}

Koliko god glupo zvučalo, ovdje ćemo pomnožiti svaki element kolekcije slučajnim brojem:

fun main () {val numbers = listOf (1, 2, 3, 4, 5) val random = random () numbers.each {println (random * it)} // hvatanje slučajne varijable}

A ako zavirimo u bytecode pomoću javap:

>> javap -c MainKt javna završna klasa MainKt {javna statička konačna void main (); Šifra: // Izostavljeno 51: novo # 29 // klasa MainKt $ main $ 1 54: dup 55: fload_1 56: invokespecial # 33 // Metoda MainKt $ main $ 1. "" :( F) V 59: checkcast # 35 // klasa kotlin / jvm / functions / Function1 62: invokestatic # 41 // Method CollectionsKt.each: (Ljava / util / Collection; Lkotlin / jvm / functions / Function1;) V 65: return

Tada iz indeksa 51 možemo uočiti da JVM stvara novu instancu MainKt $ glavni $1 unutarnja klasa za svaki poziv. Također, indeks 56 pokazuje kako Kotlin bilježi slučajnu varijablu. To znači da će se svaka zarobljena varijabla proslijediti kao argumenti konstruktora, stvarajući tako memorijske troškove.

2.3. Tip Brisanje

Što se tiče generičkih lijekova na JVM-u, za početak nikad nije bio raj! U svakom slučaju, Kotlin tijekom izvođenja briše informacije o generičkom tipu. To je, instanca generičke klase ne čuva svoje parametre tipa za vrijeme izvođenja.

Primjerice, prilikom deklariranja nekoliko zbirki poput Popis ili Popis, sve što imamo u vrijeme izvođenja je samo sirovo Popiss. Čini se da ovo nije povezano s prethodnim izdanjima, kao što je i obećano, ali vidjet ćemo kako su ugrađene funkcije zajedničko rješenje za oba problema.

3. Inline funkcije

3.1. Uklanjanje gornjih dijelova Lambda

Kada se koriste lambda-e, dodatna dodjela memorije i dodatni virtualni poziv metode dovode do nekih dodatnih troškova za vrijeme izvođenja. Dakle, ako bismo isti kôd izvršavali izravno, umjesto da koristimo lambde, naša bi implementacija bila učinkovitija.

Moramo li birati između apstrakcije i učinkovitosti?

Kao što se ispostavilo, s ugrađenim funkcijama u Kotlinu možemo imati oboje! Možemo napisati naše lijepe i elegantne lambde, a kompajler generira ubačeni i izravni kod za nas. Sve što trebamo učiniti je staviti u redu na tome:

inline fun Collection.each (blok: (T) -> Unit) {for (e u ovom) block (e)}

Kada se koristi ugrađene funkcije, prevodilac ugrađuje tijelo funkcije. Odnosno, supstituira tijelo izravno na mjesta na kojima se funkcija poziva. Prema zadanim postavkama, prevodilac ugrađuje kôd i za samu funkciju i za lambde koje su joj proslijeđene.

Na primjer, kompajler prevodi:

val brojevi = listOf (1, 2, 3, 4, 5) brojeva.each {println (it)}

Na nešto poput:

brojevi val = listaOf (1, 2, 3, 4, 5) za (broj u brojevima) println (broj)

Kada koristite ugrađene funkcije, nema dodatne alokacije objekata i nema dodatnih poziva virtualne metode.

Međutim, ne bismo trebali pretjerivati ​​s ugrađenim funkcijama, posebno za duge funkcije, jer ugrađivanje može generiranom kodu poprilično narasti.

3.2. Nema ugrađenog

Prema zadanim postavkama, sve lambde proslijeđene u ugrađenu funkciju također bi bile ugrađene. Međutim, neke lambde možemo označiti znakom noinline ključna riječ za njihovo isključivanje iz ugradnje:

inline fun foo (inlined: () -> Unit, noinline notInlined: () -> Unit) {...}

3.3. Inline reifikacija

Kao što smo vidjeli ranije, Kotlin tijekom izvođenja briše informacije o generičkom tipu, ali za ugrađene funkcije možemo izbjeći ovo ograničenje. Odnosno, kompajler može reificirati informacije o generičkom tipu za ugrađene funkcije.

Sve što moramo učiniti je označiti parametar tipa s reificirano ključna riječ:

inline zabava Any.isA (): Boolean = ovo je T

Bez u redu i reificirano, je funkcija se ne bi kompajlirala, kao što temeljito objašnjavamo u našem članku Kotlin Generics.

3.4. Nelokalni povrati

U Kotlinu, možemo koristiti povratak izraz (poznat i kao nekvalificirani povratak) samo za izlazak iz imenovane ili anonimne funkcije:

zabava namedFunction (): Int {return 42} zabava anonimna (): () -> Int {// anonimna funkcija return fun (): Int {return 42}}

U oba primjera, povratak izraz je valjan jer su funkcije imenovane ili anonimne.

Međutim, ne možemo se koristiti nekvalificiranim povratak izrazi za izlazak iz lambda izraza. Da bismo ovo bolje razumjeli, izumimo još jedan kotačić:

zabavna lista.eachIndexed (f: (Int, T) -> Unit) {for (i u indeksima) {f (i, this [i])}}

Ova funkcija izvodi zadani blok koda (funkcija f) na svakom elementu, pružajući sekvencijalni indeks s elementom. Upotrijebimo ovu funkciju za pisanje druge funkcije:

zabavno List.indexOf (x: T): Int {eachIndexed {index, value -> if (value == x) {return index}} return -1}

Ova bi funkcija trebala pretraživati ​​zadani element na popisu primatelja i vratiti indeks pronađenog elementa ili -1. Međutim, budući da ne možemo izaći iz lambde s nekvalificiranim povratak izraza, funkcija se neće ni kompajlirati:

Kotlin: 'povratak' ovdje nije dopušten

Kao rješenje ovog ograničenja možemo u redu the svakiIndeksiran funkcija:

inline fun List.eachIndexed (f: (Int, T) -> Unit) {for (i u indeksima) {f (i, this [i])}}

Tada zapravo možemo koristiti indexOf funkcija:

val pronađen = numbers.indexOf (5)

Inline funkcije su samo artefakti izvornog koda i ne manifestiraju se tijekom izvođenja. Stoga, povratak iz umetnute lambde ekvivalentan je povratku iz funkcije zatvaranja.

4. Ograničenja

Općenito, možemo ugrađivati ​​funkcije s lambda parametrima samo ako je lambda ili pozvana izravno ili proslijeđena drugoj inline funkciji. Inače, kompajler sprečava umetanje s pogreškom kompajlera.

Na primjer, pogledajmo zamijeniti funkcija u standardnoj knjižnici Kotlin:

ubačena zabava CharSequence.replace (regex: Regex, noinline transform: (MatchResult) -> CharSequence): String = regex.replace (this, transform) // prelazak u normalnu funkciju

Isječak gore prolazi pored lambde, transformirati, na normalnu funkciju, zamijeniti, dakle noinline.

5. Zaključak

U ovom smo članku zaglibili u probleme s izvedbom lambda i brisanjem tipa u Kotlinu. Zatim smo, nakon uvođenja ugrađenih funkcija, vidjeli kako se one mogu riješiti oba problema.

Međutim, trebali bismo pokušati ne pretjerivati ​​s ovim vrstama funkcija, posebno kada je tijelo funkcije preveliko jer generirana veličina bajt koda može rasti, a usput možemo izgubiti i nekoliko JVM optimizacija.

Kao i obično, svi su primjeri dostupni na GitHubu.


$config[zx-auto] not found$config[zx-overlay] not found