Kotlin From Scratch Avancerade funktioner

Kotlin är ett funktionellt språk, och det betyder att funktionerna är främre och centrala. Språket är fullt med funktioner för att göra kodningsfunktionerna enkla och uttrycksfulla. I det här inlägget lär du dig information om förlängningsfunktioner, högre orderfunktioner, stängningar och inlinefunktioner i Kotlin.

I den föregående artikeln lärde du dig om högsta funktioner, lambda-uttryck, anonyma funktioner, lokala funktioner, infixfunktioner och slutligen medlemsfunktioner i Kotlin. I den här handledningen fortsätter vi att lära oss mer om funktioner i Kotlin genom att gräva i:

  • förlängningsfunktioner 
  • högre orderfunktioner
  • förslutningar
  • inline-funktioner

1. Förlängningsfunktioner 

Skulle det inte vara trevligt om Sträng skriv in Java hade en metod för att kapitalisera första bokstaven i a Sträng-tycka om ucfirst () i PHP? Vi kan kalla den här metoden upperCaseFirstLetter ()

För att uppnå detta kan du skapa en Sträng underklass som sträcker sig Sträng skriv i Java. Men kom ihåg att Sträng klass i Java är final-vilket innebär att du inte kan förlänga det. En möjlig lösning för Kotlin skulle vara att skapa hjälparfunktioner eller funktioner på toppnivå, men det här kanske inte är idealiskt eftersom vi då inte kunde använda IDE auto-complete-funktionen för att se listan över metoder som finns tillgängliga för Sträng typ. Det som verkligen skulle vara trevligt skulle vara att på något sätt lägga till en funktion i en klass utan att behöva ärva från den klassen.

Jo, Kotlin har oss täckt med ännu en fantastisk funktion: förlängningsfunktioner. Dessa ger oss möjligheten att utöka en klass med ny funktionalitet utan att behöva ärva från den klassen. Med andra ord behöver vi inte skapa en ny undertyp eller ändra originaltypen. 

En förlängningsfunktion deklareras utanför klassen som den vill förlänga. Med andra ord är det också en toppnivåfunktion (om du vill ha en uppfriskning på toppnivåfunktionerna i Kotlin, besök handledningen Mer kul med funktioner i denna serie). 

Tillsammans med förlängningsfunktioner stöder Kotlin också förlängningsegenskaper. I det här inlägget kommer vi att diskutera förlängningsfunktioner och vi väntar tills ett framtida inlägg för att diskutera förlängningsegenskaper tillsammans med klasser i Kotlin. 

Skapa en förlängningsfunktion

Som du kan se i koden nedan definierade vi en toppnivåfunktion som vanligt för oss att deklarera en förlängningsfunktion. Denna förlängningsfunktion finns i ett paket som heter com.chike.kotlin.strings

För att skapa en förlängningsfunktion måste du prefixa namnet på den klass som du utökar före funktionsnamnet. Klassnamnet eller typen som förlängningen definieras kallas för mottagartyp, och den mottagarobjekt är klassens instans eller värde som förlängningsfunktionen heter.

paketet com.chike.kotlin.strings fun String.upperCaseFirstLetter (): String returnera detta.substring (0, 1) .toUpperCase (). plus (this.substring (1))

Observera att detta nyckelord inuti funktionskroppen refererar till mottagarobjektet eller förekomsten. 

Ringa en förlängningsfunktion

När du har skapat din förlängningsfunktion måste du först importera utvidgningsfunktionen till andra paket eller filer som ska användas i den filen eller paketet. Då ringer funktionen är detsamma som att ringa någon annan metod för mottagarklassen.

paketet com.chike.kotlin.packagex importera com.chike.kotlin.strings.upperCaseFirstLetter print ("chike" .upperCaseFirstLetter ()) // "Chike"

I exemplet ovan mottagartyp är klass Sträng, och den mottagarobjekt är "Chike". Om du använder en IDE som IntelliJ IDEA som har IntelliSense-funktionen, ser du din nya tilläggsfunktion som föreslås bland listan över andra funktioner i en Sträng typ. 

Java Interoperabilitet

Observera att bakom kulisserna skapar Kotlin en statisk metod. Denna statiska metodens första argument är mottagarobjektet. Så det är enkelt för Java-röstare att ringa denna statiska metod och sedan överföra mottagarobjektet som ett argument. 

Till exempel, om vår förlängningsfunktion deklarerades i a StringUtils.kt fil, skulle Kotlin-kompilatorn skapa en Java-klass StringUtilsKt med en statisk metod upperCaseFirstLetter ()

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

Det betyder att Java-röstare enkelt kan ringa metoden genom att referera till sin genererade klass, precis som för någon annan statisk metod. 

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

Kom ihåg att denna Java-interoperabilitet liknar hur högnivåfunktioner fungerar i Kotlin, som vi diskuterade i posten Fler kul med funktioner!

Förlängningsfunktioner vs. Medlemsfunktioner

Observera att förlängningsfunktioner inte kan åsidosätta funktioner som redan deklarerats i en klass eller ett gränssnitt som är känt som medlemsfunktioner (om du vill ha en uppdatering av medlemsfunktionerna i Kotlin, ta en titt på den tidigare handledningen i denna serie). Så om du har definierat en förlängningsfunktion med exakt samma funktionssignatur - samma funktionsnamn och samma antal, typer och ordning med argument, oberoende av returtyp - kommer Kotlin-kompilatorn inte att göra det. I samlingsprocessen söker Kotlin-kompilatorn först en match i de medlemsfunktioner som definieras i förekomsttypen eller i superklasserna när en funktion påkallas. Om det finns en match är den medlemsfunktionen den som åberopas eller är bunden. Om det inte finns någon match, kommer kompilatorn att anropa någon förlängningsfunktion av den typen. 

Så kortfattat: medlemsfunktionerna vinner alltid. 

Låt oss se ett praktiskt exempel.

class Student fun printResult () println ("Skriv ut studentresultat") roligt expel () println ("Expelling student from school") roligt Student.printResult () println ("Extension function printResult ()")  roligt Student.expel (orsak: String) println ("Expelling student from School. Reason: \" $ reason \ "")

I koden ovan definierade vi en typ som heter Studerande med två medlemsfunktioner: printResult () och utvisa(). Vi definierade då två förlängningsfunktioner som har samma namn som medlemsfunktionerna. 

Låt oss ringa printResult () funktionen och se resultatet. 

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

Som du kan se var funktionen som åberopades eller bundet var medlemsfunktionen och inte förlängningsfunktionen med samma funktionssignatur (även om IntelliJ IDEA fortfarande skulle ge dig en ledtråd om det).

Men ringer medlemsfunktionen utvisa() och förlängningsfunktionen utvisa (anledning: String) kommer att producera olika resultat eftersom funktionssignaturerna är olika. 

student.expel () // Expelling student från school student.expel ("stal money") // Expelling student från skolan. Anledning: "stal pengar"

Medlemsförlängningsfunktioner

Du kommer att förklara en förlängningsfunktion som en högsta funktion mesteparten av tiden, men notera att du också kan deklarera dem som medlemsfunktioner. 

klass ClassB  klass ClassA kul ClassB.exFunction () print (toString ()) // samtal ClassB toString () roligt callExFunction (classB: ClassB) classB.exFunction () // kalla tilläggsfunktionen

I koden ovan förklarade vi en förlängningsfunktion exFunction () av ClassB skriv in i en annan klass KlassA. De avsändarmottagare är förekomsten av klassen där förlängningen förklaras, och förekomsten av mottagartypen av förlängningsmetoden kallas förlängningsmottagare. När det finns en namnkonflikt eller skuggning mellan avsändarmottagaren och förlängningsmottagaren, notera att kompilatorn väljer förlängningsmottagaren. 

Så i koden exemplet ovan, den förlängningsmottagare är en förekomst av ClassB-så det betyder det att stränga() Metoden är av typen ClassB när den kallas inuti förlängningsfunktionen exFunction (). För oss att åberopa att stränga() metod för avsändarmottagare KlassA i stället måste vi använda en kvalificerad detta:

// ... kul ClassB.extFunction () print ([email protected] ()) // nu kallar ClassA toString () metod // ... 

2. Högbeställningsfunktioner 

En högre orderfunktion är bara en funktion som tar en annan funktion (eller lambda-uttryck) som en parameter, returnerar en funktion, eller gör båda. De sista() samlingsfunktionen är ett exempel på en högre orderfunktion från standardbiblioteket. 

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

Här passerade vi en lambda till sista funktion för att fungera som ett predikat för att söka inom en delmängd av element. Nu dyker vi in ​​på att skapa våra egna högre orderfunktioner i Kotlin. 

Skapa en högre orderfunktion

Titta på funktionen circleOperation () nedan har den två parametrar. Den första, radie, accepterar en dubbel och den andra, op, är en funktion som accepterar en dubbel som input och returnerar också en dubbel som utgång-vi kan säga mer kortfattat att den andra parametern är "en funktion från dubbel till dubbel". 

Observera att op Funktionsparametertyper för funktionen är inslagna inom parentes (), och utgångstypen separeras med en pil. Funktionen circleOperation () är ett typiskt exempel på en högre orderfunktion som accepterar en funktion som en parameter.

roligt calCircumference (Radius: Double) = (2 * Math.PI) * Radiokul calArea (Radius: Dubbel): Dubbel = (Math.PI) * Math.pow (radie, 2,0) rolig cirkelOperation (radie: Dubbel, upp: (Dubbel) -> Dubbel): Dubbel valresultat = upp (radius) returresultat

Inbjuder till en högre orderfunktion

Vid invigningen av detta circleOperation () funktion, passerar vi en annan funktion, calArea (), till den. (Observera att om metodens signatur för den överförda funktionen inte överensstämmer med vad högorderfunktionen deklarerar, kommer funktionsanropet inte att kompileras.) 

För att passera calArea () fungera som en parameter till circleOperation (), vi måste prefixa det med :: och släppa bort () konsoler.

Skriv ut (circleOperation (3.0, :: calArea)) // 28.274333882308138 print (circleOperation (3.0, calArea)) // kommer inte kompilera utskrift (circleOperation (3.0, calArea ())) // kommer inte kompilera utskrift (circleOperation 6,7, :: calCircumference)) // 42.09734155810323

Använda högre orderfunktioner kan klokt göra vår kod lättare att läsa och mer förståelig. 

Lambdas och högre orderfunktioner

Vi kan också vidarebefordra en lambda (eller funktional bokstavlig) till en högre orderfunktion direkt när funktionen aktiveras: 

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

Kom ihåg att vi för att undvika att namngivna argumentet uttryckligen kan använda Det argumentnamn som automatiskt genereras för oss endast om lambda har ett argument. (Om du vill ha en uppfriskning på lambda i Kotlin, besök tutorialet Mer kul med funktioner i den här serien).

Returnerar en funktion

Kom ihåg att förutom att acceptera en funktion som en parameter kan högre orderfunktioner också returnera en funktion till uppringare. 

rolig multiplikator (faktor: dubbel): (dubbel) -> dubbel = tal -> nummer * faktor

här multiplikator() funktionen returnerar en funktion som tillämpar den givna faktorn på ett tal som passerat in i den. Denna returnerade funktion är en lambda (eller funktionell bokstavlig) från dubbla till dubbla (vilket betyder att ingångsparametern för den returnerade funktionen är en dubbel typ och utgångsresultatet är också en dubbel typ).  

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

För att testa detta, passerade vi i en faktor två och tilldelade den returnerade funktionen till variabeldubblaren. Vi kan åberopa detta som en normal funktion, och det värde som vi passerar in kommer att fördubblas.

3. Stängningar

En stängning är en funktion som har tillgång till variabler och parametrar som definieras i ett yttre räckvidd. 

roligt printFilteredNamesByLength (längd: Int) valnamn = arrayListOf ("Adam", "Andrew", "Chike", "Kechi") valfilterResult = names.filter it.length == length println (filterResult) printFilteredNamesByLength 5) // [Chike, Kechi]

I koden ovan gick lambda till filtrera() insamlingsfunktionen använder parametern längd av den yttre funktionen printFilteredNamesByLength (). Observera att denna parameter är definierad utanför lambdas omfattning, men att lambda fortfarande kan komma åt längd. Denna mekanism är ett exempel på nedläggning i funktionell programmering.

4. Inline-funktioner

I mer kul med funktioner nämnde jag att Kotlin-kompilatorn skapar en anonym klass i tidigare versioner av Java bakom kulisserna när man skapar lambda-uttryck. 

Tyvärr introducerar denna mekanism överhuvudtaget eftersom en anonym klass skapas under huven varje gång vi skapar en lambda. En lambda som använder den yttre funktionsparametern eller den lokala variabeln med en förslutning lägger till sin egen minnesallokering, eftersom ett nytt objekt tilldelas högen med varje påkallelse. 

Jämförelse av inlinefunktioner med normala funktioner

För att förhindra dessa omkostnader gav Kotlin-teamet oss i kö modifierare för funktioner. En högre orderfunktion med i kö modifieraren kommer att inline under kodkompilering. Med andra ord kommer kompilatorn att kopiera lambda (eller funktionen bokstavlig) och även den högre orderfunktionskroppen och klistra in dem på samtalssidan. 

Låt oss titta på ett praktiskt exempel. 

roligt cirkelOperation (rad: Dubbel, upp: (Dubbel) -> Dubbel) println ("Radius är $ radie") valresultat = op (radius) println ("Resultatet är $ resultat") roligt huvud (args: Array) circleOperation (5.3, (2 * Math.PI) * it) 

I koden ovan har vi en högre orderfunktion circleOperation () det har inte den i kö modifieringsmedel. Låt oss nu se Kotlin-bytekoden som genereras när vi sammanställer och dekompilerar koden och jämför sedan den med en som har i kö modifieringsmedel. 

offentlig slutlig klass InlineFunctionKt offentlig statisk slutgilt cirkelOperation (dubbelradie, @NotNull Function1 op) Intrinsics.checkParameterIsNotNull (op, "op"); String var3 = "Radius är" + radien; System.out.println (var3); dubbelresultat = ((Nummer) op.invoke (radie)). doubleValue (); String var5 = "Resultatet är" + resultat; System.out.println (var5);  offentliga statiska slutgiltiga huvud (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); circleOperation (5.3D, (Function1) null.INSTANCE); 

I den genererade Java bytecode ovan kan du se att kompilatorn kallat funktionen circleOperation () inuti main () metod.

Låt oss nu ange högre orderfunktionen som i kö istället, och även se den genererade bytekoden.

inline roligt cirkelOperation (rad: Dubbel, upp: (Dubbel) -> Dubbel) println ("Radius är $ radien") valresultat = op (radius) println ("Resultatet är $ resultat") roligt huvud Array) circleOperation (5.3, (2 * Math.PI) * it)

För att göra en högre orderfunktion inline måste vi infoga i kö modifierare före roligt nyckelord, precis som vi gjorde i koden ovan. Låt oss också kontrollera bytekoden som genereras för denna inline-funktion. 

offentliga statiska slutgiltiga cirkeloperation (dubbelradie, @NotNull Function1 op) Intrinsics.checkParameterIsNotNull (op, "op"); String var4 = "Radius är" + radien; System.out.println (var4); dubbelresultat = ((Nummer) op.invoke (radie)). doubleValue (); String var6 = "Resultatet är" + resultat; System.out.println (var6);  offentliga statiska slutgiltiga huvud (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); dubbelradie $ iv = 5,3D; String var3 = "Radius är" + radien $ iv; System.out.println (var3); dubbelresultat $ iv = 6.283185307179586D * radie $ iv; String var9 = "Resultatet är" + resultat $ iv; System.out.println (var9); 

Titta på den genererade bytekoden för inlinefunktionen inuti main () funktion, kan du observera det istället för att ringa circleOperation () funktionen har den nu kopierat circleOperation () funktionskropp inklusive lambda-kroppen och klistrade in det på dess samtalställe.

Med denna mekanism har vår kod blivit väsentligt optimerad, inte längre skapande av anonyma klasser eller extra minnesallokeringar. Men var mycket medveten om att vi skulle ha en större bytekod bakom kulisserna än tidigare. Av denna anledning rekommenderas det att endast inline mindre högre orderfunktioner som accepterar lambda som parametrar. 

Många av standardbibliotekets högre orderfunktioner i Kotlin har inline modifieraren. Till exempel, om du tar en titt på insamlingsoperationsfunktionerna filtrera() och först(), du ser att de har i kö modifierare och är också små i storlek. 

allmän inline kul  Iterable.filter (predikat: (T) -> Boolean): Lista return filterTo (ArrayList(), predikat) public inline fun  Iterable.första (predikat: (T) -> booleskt): T för (element i detta) om (predikat (element)) returneringselement kasta NoSuchElementException ("Samling innehåller inget element som matchar predikatet.")

Kom ihåg att inte inline normala funktioner som inte accepterar en lambda som parameter! De kommer att kompilera, men det skulle inte finnas någon signifikant prestationsförbättring (IntelliJ IDEA skulle till och med ge en antydan om detta).  

De noinline modifier

Om du har mer än två lambda parametrar i en funktion har du möjlighet att bestämma vilken lambda som inte ska inline med hjälp av noinline modifierare på parametern. Denna funktionalitet är speciellt användbar för en lambda-parameter som kommer att innehålla mycket kod. Med andra ord kommer Kotlin-kompilatorn inte kopiera och klistra in den lambda där den heter men istället kommer den att skapa en anonym klass bakom scenen.  

inline fun myFunc (op: (Dubbel) -> Double, noinline op2: (Int) -> Int) // utföra operationer

Här satte vi in noinline modifierare till den andra lambda-parametern. Observera att denna modifierare endast är giltig om funktionen har i kö modifieringsmedel.

Stapelspår i inlinefunktioner

Observera att när ett undantag kastas inuti en inline-funktion, är metallsamlingsstacken i stapelspåret annorlunda än en normal funktion utan i kö modifieringsmedel. Detta beror på kopia och klistra mekanismen som används av kompilatorn för inline-funktioner. Den snygga saken är att IntelliJ IDEA hjälper oss att enkelt navigera i metodkallstapeln i stapelspåret för en inline-funktion. Låt oss se ett exempel.

inline fun myFunc (upp: (Dubbel) -> Dubbel) kasta Undantag ("meddelande 123") rolig huvud (args: Array) myFunc (4,5)

I koden ovan kastas ett undantag medvetet inuti inline-funktionen myFunc (). Låt oss nu se stapelspåret inom IntelliJ IDEA när koden körs. Titta på skärmdumpen nedan kan du se att vi får två navigeringsalternativ att välja: Inline-funktionskroppen eller inline-funktionens samtalställe. Att välja den tidigare kommer att ta oss till den punkt som undantaget kastades i funktionskroppen, medan den senare kommer att ta oss till den punkt som metoden heter.

Om funktionen inte var en inline, skulle vår stackspår vara den som du kanske redan är bekant med:

Slutsats

I denna handledning lärde du dig ännu fler saker du kan göra med funktioner i Kotlin. Vi täckte:

  • förlängningsfunktioner
  • högre orderfunktioner
  • förslutningar
  • inline-funktioner

I nästa handledning i Kotlin From Scratch-serien gräver vi oss in i objektorienterad programmering och börjar lära oss hur klasserna fungerar i Kotlin. Ses snart!

För att lära mig mer om Kotlins språket rekommenderar jag att du besöker Kotlins dokumentation. Eller kolla in några av våra andra Android App-utvecklingsposter här på Envato Tuts+!