Inline funkciók Kotlinban

1. Áttekintés

Kotlinban a funkciók első osztályú állampolgárok, így a funkciókat átadhatjuk vagy visszaküldhetjük ugyanúgy, mint más normál típusokat. Ezeknek a funkcióknak a futás közbeni ábrázolása azonban néha néhány korlátozást vagy teljesítménybonyodalmat okozhat.

Ebben az oktatóanyagban először két, látszólag nem kapcsolódó kérdést fogunk felsorolni a lambdákkal és a generikus gyógyszerekkel kapcsolatban, majd miután bemutattuk Beépített funkciók, meglátjuk, hogyan tudják kezelni mindkét problémát, kezdjük tehát!

2. Baj a Paradicsomban

2.1. A Lambdas rezsije Kotlinban

A funkciók egyik előnye, hogy első osztályú polgárok vagyunk Kotlinban, hogy egyfajta viselkedést átadhatunk más funkcióknak. A lambda funkciók átadásával tömörebb és elegánsabb módon fejezhetjük ki szándékainkat, de ez csak a történet egy része.

A lambdas sötét oldalának felfedezéséhez feltaláljuk újra a kereket úgy, hogy deklarálunk egy kiterjesztési funkciót szűrő gyűjtemények:

fun Gyűjtemény.szűrő (állítmány: (T) -> Boolean): Gyűjtemény = // Kihagyva

Most nézzük meg, hogyan áll össze a fenti függvény a Java-ba. Összpontosítson a állítmány paraméterként átadott függvény:

nyilvános statikus végleges Gyűjtemény szűrő (Gyűjtemény, kotlin.jvm.functions.Function1);

Figyelje meg, hogy a állítmány kezelése a Funkció1 felület?

Most, ha ezt Kotlinban hívjuk:

sampleCollection.filter {it == 1}

A következőkhöz hasonlót állítunk elő a lambda kód beburkolásához:

szűrő (sampleCollection, új Function1 () {@Orride public Boolean invoke (Integer param) {return param == 1;}});

Minden alkalommal, amikor magasabb rendű funkciót deklarálunk, legalább egy ilyen speciális példányt Funkció* típusok jönnek létre.

Miért teszi ezt Kotlin mondjuk használat helyett megidézett dinamikus mint hogy a Java 8 hogyan működik a lambdákkal? Egyszerűen fogalmazva: Kotlin a Java 6 kompatibilitást igényli, és megidézett dinamikus csak Java 7-ig érhető el.

De ezzel még nincs vége. Mint sejthetnénk, nem elég csak egy példány létrehozása.

A Kotlin lambdába zárt művelet tényleges végrehajtása érdekében a magasabb rendű függvény - szűrő ebben az esetben - meg kell hívnia a megnevezett speciális módszert hivatkozni az új példányon. Az eredmény többletköltség az extra hívás miatt.

Tehát csak összefoglalva, amikor egy lambdát adunk át egy függvénynek, a következő történik a motorháztető alatt:

  1. Legalább egy speciális típusú példány létrejön és tárolódik a kupacban
  2. Mindig történik egy extra módszerhívás

Még egy példány allokáció és még egy virtuális módszer hívás nem tűnik olyan rossznak, igaz?

2.2. Zárások

Mint korábban láttuk, amikor egy lambdát átadunk egy függvénynek, egy függvény típusú példány jön létre, hasonlóan a névtelen belső osztályok Java-ban.

Akárcsak az utóbbinál, egy lambda kifejezés hozzáférhet annak bezárás, vagyis a külső hatókörben deklarált változók. Amikor a lambda rögzít egy változót a bezárásakor, Kotlin eltárolja a változót a befogó lambda kóddal együtt.

Az extra memória-allokációk még rosszabbak lesznek, ha a lambda változót rögzít: A JVM minden hívásnál létrehoz egy függvénytípus példányt. A nem befogó lambdák esetében csak egy példány lesz, a szingli, azon függvénytípusok közül.

Hogy vagyunk ilyen biztosak ebben? Találjuk ki újra egy kereket úgy, hogy deklarálunk egy függvényt egy függvény alkalmazására az egyes gyűjteményelemeken:

szórakoztató Collection.each (blokk: (T) -> Egység) {for (e ebben a blokkban) (e)}

Bármilyen butának is tűnik, itt minden gyűjtőelemet véletlenszámmal megszorzunk:

fun main () {val számok = listOf (1, 2, 3, 4, 5) val véletlenszerű = véletlenszerű () számok. minden {println (véletlenszerű * it)} // a véletlen változó rögzítése}

És ha egy pillantást vetünk a bytecode-ra a segítségével javap:

>> javap -c MainKt public final class MainKt {public static final void main (); Kód: // kihagyva 51: új # 29 // class MainKt $ main $ 1 54: dup 55: fload_1 56: invokespecial # 33 // Method MainKt $ main $ 1. "" :( F) V 59: checkcast # 35 // class kotlin / jvm / functions / Function1 62: invokestatic # 41 // Method CollectionsKt.each: (Ljava / util / Collection; Lkotlin / jvm / functions / Function1;) V 65: return

Ezután az 51-es indexből észrevehetjük, hogy a JVM új példányt hoz létre MainKt $ main $1 belső osztály minden meghíváshoz. Az 56-os index azt is mutatja, hogy Kotlin hogyan rögzíti a véletlen változót. Ez azt jelenti, hogy minden elfogott változó konstruktor argumentumként kerül továbbításra, ezáltal memóriaterhet generálva.

2.3. Típus Törlés

Ami a JVM generikumait illeti, kezdetben soha nem volt paradicsom! Egyébként Kotlin futás közben törli az általános típusú információkat. Vagyis egy általános osztály egy példánya futás közben nem őrzi meg a típusparamétereit.

Például, amikor néhány gyűjteményt deklarál, mint Lista vagy Lista, minden, ami futás közben van, csak nyers Listas. Úgy tűnik, ez nem kapcsolódik az előző kérdésekhez, ahogy ígértük, de meglátjuk, hogy az inline függvények hogyan jelentik a közös megoldást mindkét problémára.

3. Beépített funkciók

3.1. A lambdas rezsijének eltávolítása

A lambdas használata esetén az extra memória-allokációk és az extra virtuális metódus-hívás bizonyos futásidejű általános költségeket jelentenek. Tehát, ha ugyanazt a kódot közvetlenül futtatnánk, a lambdas használata helyett a megvalósításunk hatékonyabb lenne.

Választanunk kell az absztrakció és a hatékonyság között?

Mint kiderült, inline funkciókkal Kotlinban mindkettő megvan! Megírhatjuk kedves és elegáns lambdáinkat, és a fordító előállítja nekünk a beágyazott és közvetlen kódot. Csak annyit kell tennünk, hogy beteszünk egy Sorban Rajta:

inline fun Collection.each (blokk: (T) -> Egység) {for (e ebben a blokkban) (e) blokk}

Inline függvények használatakor a fordító beágyazza a függvénytestet. Vagyis a testet közvetlenül olyan helyekre helyettesíti, ahol a függvény meghívásra kerül. Alapértelmezés szerint a fordító beágyazza a kódot mind a függvény, mind a hozzá átadott lambdas számára.

Például a fordító lefordítja:

val számok = listOf (1, 2, 3, 4, 5) számok. minden {println (it)}

Ilyesmire:

val számok = listOf (1, 2, 3, 4, 5) a (szám számokban) println (szám)

Inline függvények használata esetén nincs extra objektum-allokáció és nincs további virtuális módszer-hívás.

Az inline függvényeket azonban nem szabad túlzásba vinni, különösen a hosszú függvényeknél, mivel a beillesztés miatt a generált kód eléggé növekedhet.

3.2. Nincs Inline

Alapértelmezés szerint az összes inline függvénynek átadott lambda is be van vonva. Azonban néhány lambdát a -val jelölhetünk noinline kulcsszó, hogy kizárják őket a beillesztésből:

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

3.3. Inline Reification

Mint korábban láttuk, Kotlin futás közben törli az általános típusú információkat, de az inline függvényeknél elkerülhetjük ezt a korlátozást. Vagyis a fordító vissza tudja igazítani az inline függvények általános típusinformációit.

Csak annyit kell tennünk, hogy a type paramétert a újraszerződött kulcsszó:

inline fun Any.isA (): Boolean = ez T

Nélkül Sorban és újraszerződött, a egy függvény nem fordítható le, ahogy azt Kotlin Generics cikkünkben alaposan elmagyarázzuk.

3.4. Nem helyi visszatérések

Kotlinban, használhatjuk a Visszatérés kifejezés (más néven minősíthetetlen Visszatérés) csak egy megnevezett vagy anonim függvényből való kilépéshez:

fun namedFunction (): Int {return 42} fun anonymous (): () -> Int {// anonymous function return fun (): Int {return 42}}

Mindkét példában a Visszatérés kifejezés azért érvényes, mert a függvények vagy meg vannak nevezve, vagy névtelenek.

Azonban, nem használhatunk minősítés nélküli Visszatérés kifejezések a lambda kifejezésből való kilépéshez. Ennek jobb megértése érdekében találjunk ki még egy kereket:

szórakozás List.eachIndexed (f: (Int, T) -> Egység) {for (i indexekben) {f (i, ez [i])}}

Ez a függvény végrehajtja az adott kódblokkot (függvény f) minden elemen, megadva a szekvenciális indexet az elemmel. Használjuk ezt a függvényt egy másik függvény megírásához:

fun List.indexOf (x: T): Int {mindenIndexed {index, érték -> if (érték == x) {return index}} return -1}

Ennek a függvénynek állítólag meg kell keresnie az adott elemet a fogadó listán, és vissza kell adnia a megtalált elem indexét vagy -1-et. Azonban, mivel nem tudunk kilépni a lambdából minősítés nélkül Visszatérés kifejezéseket, a függvény nem is fordítja le:

Kotlin: a „visszatérés” itt nem megengedett

Ennek a korlátozásnak a megoldásaként megtehetjük Sorban a mindegyikTeljes funkció:

inline fun List.eachIndexed (f: (Int, T) -> Egység) {for (i indexekben) {f (i, ez [i])}}

Akkor valóban használhatjuk a indexe funkció:

val talált = számok.indexOf (5)

Az inline függvények csupán a forráskód összetevői, és futás közben nem jelentkeznek. Ebből kifolyólag, a beágyazott lambdáról való visszatérés egyenértékű a bezáró funkcióból való visszatéréssel.

4. Korlátozások

Általában, csak akkor tudjuk beilleszteni a függvényeket lambda paraméterekkel, ha a lambdát vagy közvetlenül meghívjuk, vagy átadjuk egy másik inline függvénynek. Ellenkező esetben a fordító megakadályozza a fordítás hibáját.

Vessünk egy pillantást például a cserélje ki funkció a Kotlin standard könyvtárban:

inline fun CharSequence.replace (regex: Regex, noinline transzformáció: (MatchResult) -> CharSequence): String = regex.replace (this, transzformáció) // átadás normál függvényre

A fenti részlet átmegy a lambdán, átalakulni, normál funkcióra, cserélje ki, ezért a noinline.

5. Következtetés

Ebben a cikkben a lambda teljesítményével és a típusok törlésével kapcsolatos kérdésekkel foglalkozunk Kotlinban. Ezután az inline függvények bevezetése után láttuk, hogy ezek hogyan tudják megoldani mindkét kérdést.

Azonban meg kell próbálnunk nem túlzásba vinni az ilyen típusú funkciókat, különösen akkor, ha a függvénytest túl nagy, mivel a generált bytecode méret növekedhet, és útközben elveszíthetünk néhány JVM-optimalizálást is.

Szokás szerint az összes példa elérhető a GitHubon.