Inline-funktioner i Kotlin

1. Oversigt

I Kotlin er funktioner førsteklasses borgere, så vi kan videregive funktioner rundt eller returnere dem ligesom andre normale typer. Imidlertid kan repræsentationen af ​​disse funktioner under kørsel nogle gange medføre nogle få begrænsninger eller ydeevnekomplikationer.

I denne vejledning skal vi først opregne to tilsyneladende ikke-relaterede problemer om lambdas og generiske lægemidler, og derefter efter introduktion Inline-funktioner, vi får se, hvordan de kan tackle begge disse bekymringer, så lad os komme i gang!

2. Problemer i paradis

2.1. Overhead of Lambdas i Kotlin

En af fordelene ved funktioner, der er førsteklasses borgere i Kotlin, er, at vi kan videregive et stykke adfærd til andre funktioner. Ved at passere fungerer som lambdas kan vi udtrykke vores intentioner på en mere kortfattet og elegant måde, men det er kun en del af historien.

For at udforske den mørke side af lambdas, lad os genopfinde hjulet ved at erklære en udvidelsesfunktion til filter samlinger:

sjov Collection.filter (predikat: (T) -> Boolean): Collection = // udeladt

Lad os nu se, hvordan funktionen ovenfor kompileres til Java. Fokuser på predikat funktion, der sendes som parameter:

offentlig statisk endelig indsamlingsfilter (Collection, kotlin.jvm.functions.Function1);

Læg mærke til, hvordan predikat håndteres ved hjælp af Funktion 1 interface?

Hvis vi kalder dette i Kotlin nu:

sampleCollection.filter {it == 1}

Noget svarende til det følgende vil blive produceret for at indpakke lambdakoden:

filter (sampleCollection, new Function1 () {@ Override public Boolean invoke (Integer param) {return param == 1;}});

Hver gang vi erklærer en højere ordensfunktion, mindst én forekomst af disse specielle Fungere* typer oprettes.

Hvorfor gør Kotlin dette i stedet for f.eks. At bruge påkaldt dynamisk ligesom hvordan Java 8 gør med lambdas? Kort sagt går Kotlin til Java 6-kompatibilitet, og påkaldt dynamisk er ikke tilgængelig før Java 7.

Men dette er ikke slutningen på det. Som vi måske gætter, er det ikke nok at oprette en forekomst af en type.

For faktisk at udføre operationen indkapslet i en Kotlin lambda, er den højere ordens funktion - filter i dette tilfælde - skal kalde den specielle metode, der er navngivet påberåbe sig på den nye instans. Resultatet er mere overhead på grund af det ekstra opkald.

Så bare for at opsummere, når vi passerer en lambda til en funktion, sker følgende under emhætten:

  1. Mindst en forekomst af en særlig type oprettes og gemmes i bunken
  2. En ekstra metodeopkald vil altid ske

Endnu en instanseallokering og endnu en virtuel metodeopkald virker ikke så slemt, ikke?

2.2. Lukninger

Som vi så tidligere, når vi sender en lambda til en funktion, oprettes der en forekomst af en funktionstype svarende til anonyme indre klasser i Java.

Ligesom med sidstnævnte, et lambda-udtryk kan få adgang til dets lukningvariabler, der er erklæret i det ydre omfang. Når en lambda fanger en variabel fra dens lukning, gemmer Kotlin variablen sammen med den fangende lambdakode.

De ekstra hukommelsesallokeringer bliver endnu værre, når en lambda fanger en variabel: JVM opretter en funktionstypeforekomst på hver påkaldelse. For ikke-fangende lambdas vil der kun være én forekomst, a singletonaf disse funktionstyper.

Hvordan er vi så sikre på dette? Lad os genopfinde et andet hjul ved at erklære en funktion til at anvende en funktion på hvert indsamlingselement:

sjov Collection.each (blok: (T) -> Enhed) {for (e i denne) blok (e)}

Så fjollet som det kan lyde, her multiplicerer vi hvert indsamlingselement med et tilfældigt tal:

sjov hoved () {valnumre = listOf (1, 2, 3, 4, 5) val tilfældig = tilfældig () tal. hver {println (tilfældig * it)} // optagelse af den tilfældige variabel}

Og hvis vi kigger ind i bytekoden ved hjælp af javap:

>> javap -c MainKt offentlig endelig klasse MainKt {offentlig statisk endelig ugyldig main (); Kode: // udeladt 51: ny # 29 // klasse MainKt $ main $ 1 54: dup 55: fload_1 56: invokespecial # 33 // Metode MainKt $ main $ 1. "" :( F) V 59: checkcast # 35 // klasse kotlin / jvm / funktioner / Function1 62: invokestatic # 41 // Method CollectionsKt.each: (Ljava / util / Collection; Lkotlin / jvm / functions / Function1;) V 65: return

Så kan vi se fra indeks 51, som JVM opretter en ny forekomst af MainKt $ main $1 indre klasse for hver påkaldelse. Indeks 56 viser også, hvordan Kotlin registrerer den tilfældige variabel. Dette betyder, at hver fanget variabel sendes som konstruktørargumenter og således genererer en hukommelsesoverhead.

2.3. Skriv sletning

Når det kommer til generiske stoffer på JVM, har det aldrig været et paradis til at begynde med! Under alle omstændigheder sletter Kotlin de generiske typeoplysninger ved kørsel. Det er, en forekomst af en generisk klasse bevarer ikke dens typeparametre under kørsel.

For eksempel når man erklærer et par samlinger som Liste eller Liste, alt, hvad vi har ved kørsel, er bare rå Listes. Dette synes ikke at være relateret til de tidligere problemer, som lovet, men vi får se, hvordan integrerede funktioner er den fælles løsning på begge problemer.

3. Integrerede funktioner

3.1. Fjernelse af Lambdas overhead

Når du bruger lambdas, introducerer den ekstra hukommelsesallokering og den ekstra virtuelle metodeopkald noget runtime-overhead. Så hvis vi udførte den samme kode direkte i stedet for at bruge lambdas, ville vores implementering være mere effektiv.

Skal vi vælge mellem abstraktion og effektivitet?

Som det viser sig, med inline-funktioner i Kotlin kan vi have begge dele! Vi kan skrive vores pæne og elegante lambdas, og compileren genererer den indføjede og direkte kode for os. Alt hvad vi skal gøre er at sætte en inline på det:

inline sjov Collection.each (blok: (T) -> Enhed) {for (e i denne) blok (e)}

Når du bruger inline-funktioner, integrerer compileren funktionselementet. Det vil sige, det erstatter kroppen direkte til steder, hvor funktionen bliver kaldt. Som standard indsætter compileren koden for både selve funktionen og lambdas, der er sendt til den.

For eksempel oversætter compileren:

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

Til noget som:

valnumre = listOf (1, 2, 3, 4, 5) til (nummer i tal) println (nummer)

Når du bruger integrerede funktioner, er der ingen ekstra objektallokering og ingen ekstra virtuelle metodeopkald.

Vi bør dog ikke overforbruge de integrerede funktioner, især for lange funktioner, da indlejringen kan få den genererede kode til at vokse en hel del.

3.2. Ingen indbygget

Som standard vil alle lambdas, der sendes til en inline-funktion, også være inline. Vi kan dog markere nogle af lambdas med noinline nøgleord for at udelukke dem fra inlining:

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

3.3. Inline Reification

Som vi så tidligere, sletter Kotlin informationen om generisk type ved kørsel, men for inline-funktioner kan vi undgå denne begrænsning. Det vil sige, at kompilatoren kan genoprette generisk typeinformation til inline-funktioner.

Alt, hvad vi skal gøre er at markere typeparameteren med reified nøgleord:

inline sjov Any.isA (): Boolsk = dette er T

Uden inline og reified, det er en funktion ville ikke kompilere, som vi grundigt forklarer i vores Kotlin Generics-artikel.

3.4. Ikke-lokale returneringer

I Kotlin, vi kan bruge Vend tilbage udtryk (også kendt som ukvalificeret Vend tilbage) kun for at afslutte en navngivet funktion eller en anonym funktion:

sjovt navngivetFunktion (): Int {return 42} sjov anonym (): () -> Int {// anonym funktion returner sjov (): Int {return 42}}

I begge eksempler er Vend tilbage udtryk er gyldigt, fordi funktionerne enten er navngivne eller anonyme.

Imidlertid, vi kan ikke bruge ukvalificeret Vend tilbage udtryk for at forlade et lambda-udtryk. For bedre at forstå dette, lad os genopfinde endnu et hjul:

sjov List.eachIndexed (f: (Int, T) -> Enhed) {for (i i indekser) {f (i, dette [i])}}

Denne funktion udfører den givne kode kode (funktion f) på hvert element, hvilket giver det sekventielle indeks med elementet. Lad os bruge denne funktion til at skrive en anden funktion:

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

Denne funktion skal søge i det givne element på modtagerlisten og returnere indekset for det fundne element eller -1. Imidlertid, da vi ikke kan komme ud af en lambda med ukvalificeret Vend tilbage udtryk, kompilerer funktionen ikke engang:

Kotlin: 'retur' er ikke tilladt her

Som en løsning på denne begrænsning kan vi inline det hver indekseret fungere:

inline sjov List.eachIndexed (f: (Int, T) -> Enhed) {for (i i indekser) {f (i, dette [i])}}

Så kan vi faktisk bruge indeks af fungere:

val fundet = numbers.indexOf (5)

Inline-funktioner er kun artefakter af kildekoden og manifesterer sig ikke ved kørsel. Derfor, retur fra en indrammet lambda svarer til retur fra den indesluttende funktion.

4. Begrænsninger

Generelt, vi kan kun integrere funktioner med lambda-parametre, hvis lambda enten kaldes direkte eller sendes til en anden inline-funktion. Ellers forhindrer compileren inline med en compiler-fejl.

Lad os for eksempel se på erstatte funktion i Kotlin standardbibliotek:

inline sjov CharSequence.replace (regex: Regex, noinline transform: (MatchResult) -> CharSequence): String = regex.replace (this, transform) // videregive til en normal funktion

Stykket ovenfor passerer lambda, transformere, til en normal funktion, erstatte, derfor noinline.

5. Konklusion

I denne artikel dykker vi ind i problemer med lambda-ydeevne og type sletning i Kotlin. Derefter, efter introduktion af integrerede funktioner, så vi, hvordan disse kan løse begge problemer.

Vi bør dog forsøge ikke at overforbruge disse typer funktioner, især når funktionskroppen er for stor, da den genererede bytecode-størrelse kan vokse, og vi også kan miste et par JVM-optimeringer undervejs.

Som sædvanligt er alle eksemplerne tilgængelige på GitHub.


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