Kotlin From Scratch Avanserte funksjoner

Kotlin er et funksjonelt språk, og det betyr at funksjonene er foran og midt. Språket er fullpakket med funksjoner for å gjøre kodingsfunksjonene enkle og uttrykksfulle. I dette innlegget vil du lære om utvidelsesfunksjoner, høyere rekkefølgefunksjoner, nedleggelser og inline-funksjoner i Kotlin.

I forrige artikkel lærte du om toppnivåfunksjoner, lambda-uttrykk, anonyme funksjoner, lokale funksjoner, infixfunksjoner og til slutt medlemsfunksjoner i Kotlin. I denne opplæringen fortsetter vi å lære mer om funksjoner i Kotlin ved å grave inn i:

  • utvidelsesfunksjoner 
  • høyere rekkefølge funksjoner
  • nedleggelser
  • inline funksjoner

1. Forlengelsesfunksjoner 

Ville det ikke vært fint om string skriv inn Java hadde en metode for å kapitalisere det første bokstaven i en string-som ucfirst () i PHP? Vi kunne kalle denne metoden upperCaseFirstLetter ()

For å innse dette, kan du opprette en string underklasse som strekker seg string skriv inn Java. Men husk at string Klassen i Java er endelig - noe som betyr at du ikke kan utvide den. En mulig løsning for Kotlin ville være å skape hjelpefunksjoner eller toppnivåfunksjoner, men dette er kanskje ikke ideelt fordi vi da ikke kunne bruke IDE auto-complete-funksjonen for å se listen over metoder som er tilgjengelige for string type. Det som ville være veldig fint, ville være å på en eller annen måte legge til en funksjon i en klasse uten å arve fra den klassen.

Vel, Kotlin har oss dekket med enda en fantastisk funksjon: utvidelsesfunksjoner. Disse gir oss muligheten til å forlenge en klasse med ny funksjonalitet uten å arve fra den klassen. Med andre ord trenger vi ikke å opprette en ny undertype eller endre original type. 

En utvidelsesfunksjon er deklarert utenfor klassen den ønsker å utvide. Med andre ord er det også en toppnivåfunksjon (hvis du vil ha en oppfriskning på toppnivåfunksjoner i Kotlin, kan du gå til Mer Fun With Functions-opplæringen i denne serien). 

Sammen med utvidelsesfunksjoner støtter Kotlin også utvidelsesegenskaper. I dette innlegget drøfter vi utvidelsesfunksjoner, og vi venter til et fremtidig innlegg for å diskutere utvidelsesegenskaper sammen med klasser i Kotlin. 

Opprette en utvidelsesfunksjon

Som du kan se i koden under, definerte vi en toppnivåfunksjon som vanlig for oss å erklære en utvidelsesfunksjon. Denne utvidelsesfunksjonen er inne i en pakke som heter com.chike.kotlin.strings

For å opprette en utvidelsesfunksjon må du prefikse navnet på den klassen du strekker seg for, for funksjonsnavnet. Klassenavnet eller typen som forlengelsen er definert kalles mottaker type, og mottaker objekt er klassens forekomst eller verdi som forlengelsesfunksjonen kalles på.

pakke com.chike.kotlin.strings moro String.upperCaseFirstLetter (): String return this.substring (0, 1) .toUpperCase (). pluss (this.substring (1))

Legg merke til at dette Søkeord inne i funksjonskroppen refererer til mottakerobjektet eller forekomsten. 

Kaller en forlengelsesfunksjon

Etter å ha opprettet utvidelsesfunksjonen, må du først importere utvidelsesfunksjonen til andre pakker eller filer som skal brukes i den filen eller pakken. Deretter kaller funksjonen det samme som å kalle en hvilken som helst annen metode for mottakerens type klasse.

pakke com.chike.kotlin.packagex importere com.chike.kotlin.strings.upperCaseFirstLetter print ("chike" .upperCaseFirstLetter ()) // "Chike"

I eksemplet ovenfor, er mottaker type er klasse string, og mottaker objekt er "Chike". Hvis du bruker en IDE som IntelliJ IDEA som har IntelliSense-funksjonen, vil du se din nye utvidelsesfunksjon foreslått blant listen over andre funksjoner i en string type. 

Java interoperabilitet

Legg merke til at bak kulissene, vil Kotlin opprette en statisk metode. Denne statiske metoden første argumentet er mottakerobjektet. Så det er enkelt for Java-oppringere å ringe denne statiske metoden og deretter sende mottakerobjektet som et argument. 

Hvis for eksempel vår utvidelsesfunksjon ble deklarert i a StringUtils.kt fil, ville Kotlin-kompilatoren opprette en Java-klasse StringUtilsKt med en statisk metode upperCaseFirstLetter ()

/ * Java * / pakke com.chike.kotlin.strings offentlig klasse StringUtilsKt offentlig statisk String upperCaseFirstLetter (String str) return str.substring (0, 1) .toUpperCase () + str.substring (1); 

Dette betyr at Java-oppringere bare kan kalle metoden ved å referere til den genererte klassen, akkurat som for enhver annen statisk metode. 

/ * Java * / print (StringUtilsKt.upperCaseFirstLetter ("chike")); // "Chike"

Husk at denne Java-interopmekanismen ligner hvordan toppnivåfunksjoner fungerer i Kotlin, som vi diskuterte i Mer Fun With Functions-innlegget!

Forlengelsesfunksjoner vs. Medlemsfunksjoner

Legg merke til at utvidelsesfunksjoner ikke kan overstyre funksjoner som allerede er deklarert i en klasse eller et grensesnitt som er kjent som medlemsfunksjoner (hvis du vil ha en oppfriskning på medlemsfunksjoner i Kotlin, ta en titt på den forrige opplæringen i denne serien). Så, hvis du har definert en utvidelsesfunksjon med nøyaktig samme funksjons signatur - det samme funksjonsnavnet og samme nummer, typer og rekkefølgen av argumenter, uavhengig av returtype - Kotlin-kompilatoren vil ikke påkalle den. I samarbeidsprosessen, når en funksjon er påkalt, vil Kotlin-kompilatoren først se etter en kamp i medlemsfunksjonene som er definert i forekomststypen eller i superklassene. Hvis det er en kamp, ​​er den medlemsfunksjonen den som er påkalt eller bundet. Hvis det ikke er noen kamp, ​​vil kompilatoren påkalle en utvidelsesfunksjon av den typen. 

Så i sammendrag: medlemsfunksjoner vinner alltid. 

La oss se et praktisk eksempel.

klasse student morsom utskriftResultat () println ("Skriv ut studentresultat") morsomt utelukkende () println ("Utvende student fra skolen") morsom Student.printResult () println ("Utvidelsesfunksjon printResult ()")  morsom Student.expel (grunn: String) println ("Utvide student fra skolen. Årsak: \" $ grunn \ "")

I koden ovenfor definerte vi en type som ble kalt Student med to medlemsfunksjoner: printResult () og utvise(). Vi definerte da to utvidelsesfunksjoner som har samme navn som medlemsfunksjonene. 

La oss ringe printResult () funksjon og se resultatet. 

val student = Student () student.printResult () // Skrive ut studentresultat

Som du kan se, var funksjonen som ble påkalt eller bundet, medlemsfunksjonen og ikke utvidelsesfunksjonen med samme funksjons signatur (selv om IntelliJ IDEA fortsatt gir deg et hint om det).

Men ringer medlemsfunksjonen utvise() og utvidelsesfunksjonen utvise (grunn: streng) vil produsere forskjellige resultater fordi funksjonstegnene er forskjellige. 

student.expel () // Expelling student fra school student.expel ("stjal penger") // Expelling student from School. Årsak: "stjal penger"

Medlemsutvidelsesfunksjoner

Du vil deklarere en utvidelsesfunksjon som en toppfunksjon mesteparten av tiden, men merk at du også kan deklarere dem som medlemsfunksjoner. 

klasse ClassB  klasse ClassA morsom ClassB.exFunction () print (toString ()) // kall ClassB toString () morsom callExFunction (klasseB: ClassB) classB.exFunction () // ring utvidelsesfunksjonen

I koden ovenfor erklærte vi en utvidelsesfunksjon exFunction () av ClassB skriv inn i en annen klasse Classa. De forsendelsesmottaker er forekomsten av klassen der forlengelsen er erklært, og forekomsten av mottaker typen av forlengelsesmetoden kalles forlengelsesmottaker. Når det er en navnekonflikt eller skygge mellom sendemottakeren og forlengelsesmottakeren, merk at kompilatoren velger forlengelsesmottakeren. 

Så i koden eksempelet ovenfor, er forlengelsesmottaker er en forekomst av ClassB-så det betyr det toString () Metoden er av typen ClassB når det kalles inne i forlengelsesfunksjonen exFunction (). For oss å påberope seg toString () metode av forsendelsesmottaker Classa I stedet må vi bruke en kvalifisert dette:

// ... morsom ClassB.extFunction () print ([email protected] ()) // nå kaller ClassA toString () metode // ... 

2. Ordrer med høyere ordre 

En høyereordningsfunksjon er bare en funksjon som tar en annen funksjon (eller lambda-uttrykk) som en parameter, returnerer en funksjon, eller gjør begge deler. De siste() samlingsfunksjon er et eksempel på en høyereordningsfunksjon fra standardbiblioteket. 

val strengliste: Liste = listOf ("in", "the", "club") skriv ut (stringList.last it.length == 3) // "the"

Her passerte vi en lambda til siste fungere som et predikat for å søke i en delmengde av elementer. Nå vil vi dykke inn i å skape våre egne høyereordningsfunksjoner i Kotlin. 

Opprette en høyere ordrefunksjon

Ser på funksjonen circleOperation () under, den har to parametere. Den første, radius, aksepterer en dobbel, og den andre, op, er en funksjon som aksepterer en dobbel som input og returnerer også en dobbel som utgang-vi kan si mer kortfattet at den andre parameteren er "en funksjon fra dobbel til dobbel". 

Vær oppmerksom på at op Funksjonsparametertyper for funksjonen er innpakket i parentes (), og utgangstypen er skilt med en pil. Funksjonen circleOperation () er et typisk eksempel på en høyereordningsfunksjon som aksepterer en funksjon som en parameter.

morsomt calCircumference (radius: Dobbel) = (2 * Math.PI) * radius morsom calArea (radius: Dobbel): Dobbel = (Math.PI) * Math.pow (radius, 2.0) morsom sirkeloperasjon (radius: Dobbel, (Dobbel) -> Dobbel): Dobbel valresultat = opp (radius) returresultat

Innkalling av en høyere ordrefunksjon

I påkallingen av dette circleOperation () funksjon, sender vi en annen funksjon, calArea (), til det. (Merk at hvis metodesignaturen til den bestått funksjonen ikke samsvarer med hva høyereordens funksjonen erklærer, vil funksamtalen ikke kompilere.) 

For å passere calArea () fungere som en parameter til circleOperation (), vi må prefikse det med :: og utelate () braketter.

print (circleOperation (3.0, calArea)) // 28.274333882308138 print (circleOperation (3.0, calArea)) // vil ikke kompilere utskrift (circleOperation (3.0, calArea ())) // vil ikke kompilere utskrift (circleOperation 6,7, :: calCircumference)) // 42.09734155810323

Ved å bruke høyere ordensfunksjoner kan vi gjøre koden enklere å lese og forståeligere. 

Lambdas og høyereordningsfunksjoner

Vi kan også sende en lambda (eller funksjonsbokstavelig) til en høyereordningsfunksjon direkte når man påberoper funksjonen: 

circleOperation (5.3, (2 * Math.PI) * it)

Husk, for å unngå å nevne argumentet eksplisitt, kan vi bruke den Argumentnavn automatisk generert for oss bare hvis lambda har ett argument. (Hvis du vil ha en oppfriskning på lambda i Kotlin, kan du gå til Mer Fun With Functions-opplæringen i denne serien).

Returnerer en funksjon

Husk at i tillegg til å akseptere en funksjon som parameter, kan høyereordens funksjoner også returnere en funksjon til innringere. 

morsom multiplikator (faktor: Dobbel): (Dobbel) -> Dobbel = tall -> tall * faktor

her multiplikator () funksjonen returnerer en funksjon som gjelder den oppgitte faktoren til et hvilket som helst tall som er sendt inn i den. Denne returnerte funksjonen er en lambda (eller funksjonsbokstavelig) fra dobbel til dobbel (betyr at inngangsparametrene for den returnerte funksjonen er en dobbel type, og utgangssultatet er også en dobbel type).  

val doubler = multiplikator (2) print (dobler (5.6)) // 11.2

For å teste dette ut, passerte vi en faktor på to og tildelte den returnerte funksjonen til variabeldobleren. Vi kan påberope dette som en vanlig funksjon, og hvilken verdi vi passerer inn i, vil bli doblet.

3. Lukk

En lukking er en funksjon som har tilgang til variabler og parametere som er definert i et ytre omfang. 

morsom utskriftFilteredNamesByLength (lengde: Int) valnavn = arrayListOf ("Adam", "Andrew", "Chike", "Kechi") val filterResult = names.filter it.length == length println (filterResult) printFilteredNamesByLength 5) // [Chike, Kechi]

I koden ovenfor gikk lambda til filter() samlingsfunksjon bruker parameteren lengde av ytre funksjon printFilteredNamesByLength (). Merk at denne parameteren er definert utenfor omfanget av lambda, men at lambdaen fortsatt kan få tilgang til lengde. Denne mekanismen er et eksempel på nedleggelse i funksjonell programmering.

4. Inline Funksjoner

I morsommere med funksjoner nevnte jeg at Kotlin-kompilatoren lager en anonym klasse i tidligere versjoner av Java bak kulissene når man lager lambda-uttrykk. 

Dessverre innfører denne mekanismen overhead fordi en anonym klasse er opprettet under hetten hver gang vi lager en lambda. Også, en lambda som bruker den ytre funksjonsparameteren eller den lokale variabelen med en lukking, legger til sin egen minnefordeling overhead fordi et nytt objekt er allokert til bunken med hvert anrop. 

Sammenligning av inline-funksjoner med normale funksjoner

For å forhindre disse overheadene ga Kotlin-teamet oss med på linje modifikator for funksjoner. En høyere rekkefølge funksjon med på linje modifikator vil bli innstilt under kodekompilering. Med andre ord, vil kompilatoren kopiere lambdaen (eller funksjonen bokstavelig) og også den høyere rekkefølgefunksjonen og lim inn dem på anropssiden. 

La oss se på et praktisk eksempel. 

morsom sirkeloperasjon (radius: Dobbel, op: (Dobbel) -> Dobbel) println ("Radius er $ radius") Valresultat = Op (radius) println ("Resultatet er $ resultat") Fun main) circleOperation (5.3, (2 * Math.PI) * it) 

I koden ovenfor har vi en høyere ordrefunksjon circleOperation () det har ikke den på linje modifier. La oss nå se Kotlin bytecode generert når vi kompilerer og dekompilerer koden, og sammenligner den med en som har på linje modifier. 

offentlig sluttklass InlineFunctionKt offentlig statisk endelig ugyldig sirkelOperasjon (dobbel radius, @NotNull funksjon1 op) Intrinsics.checkParameterIsNotNull (op, "op"); String var3 = "Radius er" + radius; System.out.println (var3); dobbeltresultat = ((tall) op.invoke (radius)). doubleValue (); String var5 = "Resultatet er" + resultat; System.out.println (var5);  offentlig statisk endelig tomgang hoved (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); circleOperation (5.3D, (Function1) null.INSTANCE); 

I den genererte Java bytecode ovenfor kan du se at kompilatoren kalte funksjonen circleOperation () inne i hoved() metode.

La oss nå spesifisere funksjonen for høyere rekkefølge som på linje i stedet, og også se generert bytekode.

Inline Fun CircleOperation (radius: Double, op: (Dobbel) -> Dobbel) println ("Radius er $ radius") valresultat = op (radius) println ("Resultatet er $ resultat") Array) circleOperation (5.3, (2 * Math.PI) * it)

For å gjøre en høyere ordrefunksjon inline, må vi sette inn på linje modifikator før moro søkeord, akkurat som vi gjorde i koden ovenfor. La oss også sjekke bytekoden som er generert for denne inline-funksjonen. 

offentlig statisk endelig ugyldig circleOperation (dobbel radius, @NotNull Function1 op) Intrinsics.checkParameterIsNotNull (op, "op"); String var4 = "Radius er" + radius; System.out.println (var4); dobbeltresultat = ((tall) op.invoke (radius)). doubleValue (); String var6 = "Resultatet er" + resultat; System.out.println (var6);  offentlig statisk endelig tomgang hoved (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); dobbel radius $ iv = 5,3D; String var3 = "Radius er" + radius $ iv; System.out.println (var3); dobbeltresultat $ iv = 6.283185307179586D * radius $ iv; String var9 = "Resultatet er" + resultat $ iv; System.out.println (var9); 

Ser på den genererte bytekoden for inlinefunksjonen inne i hoved() funksjon, kan du observere det i stedet for å ringe circleOperation () funksjonen, har den nå kopiert circleOperation () funksjon kroppen inkludert lambda kroppen og klistret det på sin samtale.

Med denne mekanismen har vår kode blitt vesentlig optimalisert - ikke mer opprettelse av anonyme klasser eller ekstra minneallokeringer. Men vær veldig oppmerksom på at vi ville ha en større bytekode bak kulissene enn før. Av denne grunn anbefales det å bare legge inn mindre høyere rekkefølgefunksjoner som aksepterer lambda som parametere. 

Mange av standardbibliotekets høyere rekkefølgefunksjoner i Kotlin har inline modifier. For eksempel, hvis du tar en titt på samlingsoperasjonsfunksjonene filter() og først(), du vil se at de har på linje modifikator og er også små i størrelse. 

offentlig inline moro  Iterable.filter (predikat: (T) -> boolsk): Liste returfilterTo (ArrayList(), predikat) offentlig inline moro  Iterable.først (predikat: (T) -> boolsk): T for (element i dette) hvis (predikat (element)) returelementkast NoSuchElementException ("Samling inneholder ikke noe element som samsvarer med predikatet.")

Husk å ikke legge inn normale funksjoner som ikke godtar en lambda som parameter! De vil kompilere, men det ville ikke være noen signifikant ytelsesforbedring (IntelliJ IDEA vil selv gi et hint om dette).  

De noinline modifier

Hvis du har mer enn to lambda-parametre i en funksjon, har du muligheten til å bestemme hvilken lambda du ikke skal innføre ved hjelp av noinline modifikator på parameteren. Denne funksjonaliteten er nyttig spesielt for en lambda-parameter som vil ta mye kode. Kotlin-kompilatoren vil med andre ord ikke kopiere og lime inn den lambda der den heter, men i stedet vil lage en anonym klasse bak scenen.  

inline fun myFunc (op: (Dobbel) -> Dobbel, noinline op2: (Int) -> Int) // utføre operasjoner

Her satt vi inn noinline modifikator til den andre lambda-parameteren. Merk at denne modifikatoren bare er gyldig hvis funksjonen har på linje modifier.

Stack Trace i Inline Funksjoner

Vær oppmerksom på at når et unntak kastes inne i en inline-funksjon, er metallsamlingsstakken i stablingssporet forskjellig fra en normal funksjon uten på linje modifier. Dette skyldes kopi og limmekanismen som brukes av kompilatoren for inline-funksjoner. Den kule tingen er at IntelliJ IDEA hjelper oss med å enkelt navigere i metodekallestakken i stablingssporet for en inline-funksjon. La oss se et eksempel.

inline fun myFunc (op: (Dobbel) -> Dobbel) kaste Unntak ("melding 123") morsom hoved (args: Array) myFunc (4.5)

I koden ovenfor blir et unntak kastet bevisst inne i inline-funksjonen myfunc (). La oss nå se stakken spore i IntelliJ IDEA når koden kjøres. Ser på skjermbildet under, kan du se at vi får to navigasjonsalternativer å velge: Inline-funksjonen eller Inline-funksjonen. Å velge den tidligere vil ta oss til det punktet unntaket ble kastet i funksjonsorganet, mens sistnevnte vil ta oss til det punktet metoden ble kalt.

Hvis funksjonen ikke var en inline, ville vår stakkespor være som den du allerede er kjent med:

Konklusjon

I denne opplæringen lærte du enda flere ting du kan gjøre med funksjoner i Kotlin. Vi dekket:

  • utvidelsesfunksjoner
  • høyere rekkefølge funksjoner
  • nedleggelser
  • inline funksjoner

I neste veiledning i Kotlin From Scratch-serien, tar vi del i objektorientert programmering og begynner å lære hvordan klasser fungerer i Kotlin. Ser deg snart!

For å lære mer om Kotlin-språket, anbefaler jeg at du besøker Kotlin-dokumentasjonen. Eller sjekk ut noen av våre andre Android-apputviklingsposter her på Envato Tuts+!