Kotlin is een functionele taal en dat betekent dat functies vooraan staan. De taal zit boordevol functies om codeerfuncties eenvoudig en expressief te maken. In dit bericht leert u over uitbreidingsfuncties, hogere orde functies, sluitingen en inline functies in Kotlin.
In het vorige artikel leerde je over functies op het hoogste niveau, lambda-expressies, anonieme functies, lokale functies, infix-functies en uiteindelijk lidfuncties in Kotlin. In deze tutorial zullen we meer leren over functies in Kotlin door in te graven in:
Zou het niet leuk zijn als het Draad
type in Java had een methode om de eerste letter in a te kapitaliseren Draad
-net zoals ucfirst ()
in PHP? We zouden deze methode kunnen noemen upperCaseFirstLetter ()
.
Om dit te realiseren, zou je een Draad
subklasse die het Draad
typ in Java. Maar vergeet niet dat het Draad
klasse in Java is definitief - wat betekent dat je het niet kunt uitbreiden. Een mogelijke oplossing voor Kotlin is om helperfuncties of functies op het hoogste niveau te maken, maar dit is misschien niet ideaal omdat we dan geen gebruik konden maken van de IDE-functie voor automatisch aanvullen om de lijst met beschikbare methoden voor de Draad
type. Wat wel heel leuk zou zijn, zou zijn om op de een of andere manier een functie aan een klas toe te voegen zonder van die klasse te hoeven erven.
Nou, Kotlin heeft ons bedekt met nog een andere geweldige functie: uitbreidingsfuncties. Deze geven ons de mogelijkheid om een klasse uit te breiden met nieuwe functionaliteit zonder van die klasse te hoeven erven. Met andere woorden, we hoeven geen nieuw subtype te maken of het originele type te wijzigen.
Een uitbreidingsfunctie wordt verklaard buiten de klas die hij wil uitbreiden. Met andere woorden, het is ook een topfunctie (als je een opfriscursus op topniveau wilt in Kotlin, ga dan naar de tutorial Fun Fun Functions in deze reeks).
Samen met uitbreidingsfuncties ondersteunt Kotlin ook uitbreidingseigenschappen. In dit bericht zullen uitbreidingsfuncties worden besproken en we zullen wachten tot een volgende post om eigenschappen van uitbreidingen te bespreken, samen met klassen in Kotlin.
Zoals je in de onderstaande code kunt zien, hebben we een functie op het hoogste niveau gedefinieerd die normaal is voor ons om een uitbreidingsfunctie te declareren. Deze uitbreidingsfunctie bevindt zich in een pakket met de naam com.chike.kotlin.strings
.
Als u een uitbreidingsfunctie wilt maken, moet u de naam van de klasse die u verlengt, voorafgaan aan de functienaam. De klassenaam of het type waarop de extensie is gedefinieerd, wordt de ontvanger type, en de ontvanger object is de klasse-instantie of waarde waarop de uitbreidingsfunctie wordt aangeroepen.
pakket com.chike.kotlin.strings fun String.upperCaseFirstLetter (): String retourneer this.substring (0, 1) .toUpperCase (). plus (this.substring (1))
Merk op dat de deze
trefwoord in de functie body verwijst naar het object van de ontvanger of instantie.
Nadat u uw uitbreidingsfunctie hebt gemaakt, moet u eerst de uitbreidingsfunctie importeren in andere pakketten of bestanden die in dat bestand of pakket worden gebruikt. Het aanroepen van de functie is dan precies hetzelfde als het aanroepen van een andere methode van de receiver type klasse.
pakket com.chike.kotlin.packagex import com.chike.kotlin.strings.upperCaseFirstLetter print ("chike" .upperCaseFirstLetter ()) // "Chike"
In het bovenstaande voorbeeld, de ontvanger type is klasse Draad
, en de ontvanger object is "Chike"
. Als u een IDE gebruikt, zoals IntelliJ IDEA met de IntelliSense-functie, wordt uw nieuwe uitbreidingsfunctie voorgesteld in de lijst met andere functies in een Draad
type.
Merk op dat Kotlin achter de schermen een statische methode zal creëren. Het eerste argument van deze statische methode is het ontvangerobject. Het is dus gemakkelijk voor Java-bellers om deze statische methode aan te roepen en vervolgens het ontvangerobject als een argument door te geven.
Als onze uitbreidingsfunctie bijvoorbeeld is gedeclareerd in a StringUtils.kt bestand, zou de Kotlin-compiler een Java-klasse maken StringUtilsKt
met een statische methode upperCaseFirstLetter ()
.
/ * Java * / pakket com.chike.kotlin.strings public class StringUtilsKt public static String upperCaseFirstLetter (String str) return str.substring (0, 1) .toUpperCase () + str.substring (1);
Dit betekent dat Java-bellers de methode gewoon kunnen aanroepen door naar de gegenereerde klasse te verwijzen, net als voor elke andere statische methode.
/ * Java * / print (StringUtilsKt.upperCaseFirstLetter ("chike")); // "Chike"
Houd er rekening mee dat dit Java-interop-mechanisme vergelijkbaar is met hoe topfuncties werken in Kotlin, zoals we in het artikel Meer plezier met functies hebben besproken!
Merk op dat extensiefuncties geen functies kunnen overschrijven die al zijn gedeclareerd in een klasse of interface, ook wel lidfuncties worden genoemd (als u een opfriscursus wilt naar lidfuncties in Kotlin, bekijk dan de vorige zelfstudie in deze reeks). Dus, als u een uitbreidingsfunctie hebt gedefinieerd met exact dezelfde functiehandtekening - dezelfde functienaam en hetzelfde nummer, typen en volgorde van argumenten, ongeacht het retourneertype - zal de Kotlin-compiler het niet oproepen. In het proces van compilatie, wanneer een functie wordt aangeroepen, zoekt de Kotlin-compiler eerst naar een overeenkomst in de ledfuncties die zijn gedefinieerd in het subsysteemtype of in de superklasse. Als er een overeenkomst is, is die ledenfunctie degene die wordt aangeroepen of gebonden. Als er geen overeenkomst is, roept de compiler een willekeurige uitbreidingsfunctie van dat type aan.
Dus kort samengevat: lidfuncties winnen altijd.
Laten we een praktisch voorbeeld bekijken.
klas Student fun printResult () println ("Print student results") fun expel () println ("Expelling student from school") fun Student.printResult () println ("Uitbreiding functie printResult ()") fun Student.expel (reden: String) println ("Expelling student from School. Reden: \" $ reason \ ""
In de bovenstaande code hebben we een type gedefinieerd Student
met twee ledenfuncties: printResult ()
en verdrijven ()
. Vervolgens hebben we twee uitbreidingsfuncties gedefinieerd die dezelfde namen hebben als de ledfuncties.
Laten we het printResult ()
functie en zie het resultaat.
val student = Student () student.printResult () // Studentresultaten afdrukken
Zoals u kunt zien, was de functie die werd aangeroepen of gekoppeld de ledenfunctie en niet de uitbreidingsfunctie met dezelfde functiesignatuur (hoewel IntelliJ IDEA u nog steeds een hint zou geven).
De ledenfunctie echter wel aanroepen verdrijven ()
en de uitbreidingsfunctie uitwerpen (reden: String)
zal verschillende resultaten produceren omdat de functiesignaturen anders zijn.
student.expel () // Student uit school uitsluiten student.expel ("geld stelen") // student uit school verdrijven. Reden: "geld gestolen"
U zult het grootste deel van de tijd een uitbreidingsfunctie als een functie op het hoogste niveau declareren, maar houd er rekening mee dat u ze ook als lidfuncties kunt declareren.
class ClassB class ClassA fun ClassB.exFunction () print (toString ()) // call ClassB toString () fun callExFunction (classB: ClassB) classB.exFunction () // roep de uitbreidingsfunctie op
In de bovenstaande code hebben we een uitbreidingsfunctie opgegeven exFunction ()
van ClassB
typ in een andere klas Klasse A, eerste klasse
. De verzend ontvanger is het exemplaar van de klasse waarin de extensie is gedeclareerd en de instantie van het type ontvanger van de uitbreidingsmethode wordt de uitbreiding ontvanger. Wanneer er een naamconflict of schaduw tussen de verzendontvanger en de uitbreidingsontvanger is, merk dan op dat de compiler de uitbreidingsontvanger kiest.
Dus in het bovenstaande codevoorbeeld, de uitbreiding ontvanger is een instantie van ClassB
-dus het betekent het toString ()
methode is van het type ClassB
wanneer aangeroepen binnen de uitbreidingsfunctie exFunction ()
. Voor ons om het toString ()
methode van de verzend ontvanger Klasse A, eerste klasse
in plaats daarvan moeten we een gekwalificeerd persoon gebruiken deze
:
// ... leuk ClassB.extFunction () print ([email protected] ()) // roept nu ClassA toString () methode // ...
Een functie van hogere orde is slechts een functie die een andere functie (of lambda-uitdrukking) als parameter gebruikt, een functie retourneert of beide. De laatste()
verzamelfunctie is een voorbeeld van een functie van een hogere orde uit de standaardbibliotheek.
val stringList: lijst= listOf ("in", "the", "club") print (stringList.last it.length == 3) // "the"
Hier passeerden we een lambda naar de laatste
functie om als een predikaat te dienen om binnen een subset van elementen te zoeken. We duiken nu in het creëren van onze eigen hogere orde functies in Kotlin.
Kijkend naar de functie circleOperation ()
hieronder heeft het twee parameters. De eerste, radius
, accepteert een dubbele en de tweede, op
, is een functie die een dubbel als invoer accepteert en ook een dubbel als uitvoer teruggeeft - we kunnen meer beknopt zeggen dat de tweede parameter "een functie van dubbel naar dubbel" is.
Merk op dat de op
functieparametertypen voor de functie zijn tussen haakjes ingepakt ()
, en het uitvoertype wordt gescheiden door een pijl. De functie circleOperation ()
is een typisch voorbeeld van een hogere orde-functie die een functie als een parameter accepteert.
fun calCircumference (radius: Double) = (2 * Math.PI) * radius fun calArea (radius: Double): Double = (Math.PI) * Math.pow (radius, 2.0) leuke cirkelBediening (straal: dubbel, op: (Double) -> Double): Double val result = op (radius) retourresultaat
In de aanroeping hiervan circleOperation ()
functie, geven we een andere functie door, calArea ()
, aan het. (Merk op dat als de methodesignatuur van de gepasseerde functie niet overeenkomt met wat de hogere-orde-functie declareert, de functie-aanroep niet compileert.)
Om de calArea ()
functie als een parameter voor circleOperation ()
, we moeten het prefixen met ::
en weglaten van de ()
beugels.
print (circleOperation (3.0, :: calArea)) // 28.274333882308138 print (circleOperation (3.0, calArea)) // compileert niet afdrukken (circleOperation (3.0, calArea ())) // compileert niet afdrukken (circleOperation ( 6.7, :: calCircumference)) // 42.09734155810323
Met verstandig gebruik van functies van een hogere rangorde kan onze code leesbaarder en begrijpelijker worden.
We kunnen ook een lambda (of functie letterlijk) direct doorgeven aan een hogere-orde functie wanneer de functie wordt aangeroepen:
circleOperation (5.3, (2 * Math.PI) * it)
Vergeet niet dat we, om te voorkomen dat we het argument expliciet moeten noemen, de het
argumentnaam automatisch gegenereerd voor ons alleen als de lambda één argument heeft. (Als je een opfriscursus op lambda in Kotlin wilt, bezoek dan de tutorial Fun Fun Functions in deze reeks).
Vergeet niet dat, naast het accepteren van een functie als een parameter, hogere-orde functies ook een functie kunnen teruggeven aan bellers.
leuke vermenigvuldiger (factor: dubbel): (dubbel) -> dubbel = nummer -> getal * factor
Hier de multiplier ()
functie retourneert een functie die de gegeven factor toepast op een willekeurig getal dat erin wordt ingevoerd. Deze geretourneerde functie is een lambda (of functie letterlijk) van dubbel naar dubbel (wat betekent dat de invoerparameter van de geretourneerde functie van het dubbele type is en het uitvoerresultaat ook van het dubbele type is).
val doubler = vermenigvuldiger (2) print (doubler (5.6)) // 11.2
Om dit uit te testen, hebben we een factor twee doorgegeven en de geretourneerde functie toegewezen aan de variabele verdubbelaar. We kunnen dit als een normale functie oproepen, en welke waarde we ook doorgeven, zal worden verdubbeld.
Een afsluiting is een functie die toegang heeft tot variabelen en parameters die in een buitenste bereik zijn gedefinieerd.
fun printFilteredNamesByLength (length: Int) val names = arrayListOf ("Adam", "Andrew", "Chike", "Kechi") val filterResult = names.filter it.length == length println (filterResult) printFilteredNamesByLength ( 5) // [Chike, Kechi]
In de code hierboven, de lambda doorgegeven aan de filter()
verzamelfunctie gebruikt de parameter lengte
van de uiterlijke functie printFilteredNamesByLength ()
. Merk op dat deze parameter buiten het bereik van de lambda is gedefinieerd, maar dat de lambda nog steeds toegang heeft tot de lengte
. Dit mechanisme is een voorbeeld van afsluiting bij functioneel programmeren.
In More Fun With Functions zei ik dat de Kotlin-compiler achter de schermen een anonieme klasse in oudere versies van Java creëert bij het maken van lambda-expressies.
Helaas introduceert dit mechanisme overhead, omdat elke keer als we een lambda creëren een anonieme klasse onder de motorkap wordt gecreëerd. Ook voegt een lambda die de parameter van de buitenfunctie of de lokale variabele met een afsluiting gebruikt zijn eigen overhead voor geheugenallocatie toe, omdat bij elke aanroep een nieuw object aan de heap wordt toegewezen.
Om deze overheadkosten te voorkomen, heeft het Kotlin-team ons voorzien van de in lijn
modifier voor functies. Een hogere orde functie met de in lijn
modifier wordt gestippeld tijdens de codecompilatie. Met andere woorden, de compiler kopieert de lambda (of functie letterlijk) en ook de hogere-orde functie-instantie en plakt ze op de oproepsite.
Laten we een praktisch voorbeeld bekijken.
leuke cirkelBediening (straal: Dubbel, op: (Dubbel) -> Dubbel) println ("Radius is $ radius") val result = op (radius) println ("Het resultaat is $ resultaat") fun main (args: Array) circleOperation (5.3, (2 * Math.PI) * it)
In de bovenstaande code hebben we een hogere-orde-functie circleOperation ()
dat heeft niet de in lijn
modifier. Laten we nu de bytecode van Kotlin zien die wordt gegenereerd wanneer we de code compileren en decompileren en deze vervolgens vergelijken met een code die de code bevat. in lijn
wijziger.
openbare eindklasse InlineFunctionKt public static final leegte circleOperation (dubbele straal, @NotNull Function1 op) Intrinsics.checkParameterIsNotNull (op, "op"); String var3 = "Radius is" + straal; System.out.println (var3); double result = ((Number) op.invoke (radius)). doubleValue (); String var5 = "Het resultaat is" + resultaat; System.out.println (var5); public static final void main (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); circleOperation (5.3D, (Function1) null.INSTANCE);
In de gegenereerde Java-bytecode hierboven kunt u zien dat de compiler de functie heeft genoemd circleOperation ()
binnen in de hoofd()
methode.
Laten we nu de functie van de hogere orde als specificeren in lijn
in plaats daarvan en zie ook de gegenereerde bytecode.
inline fun circle Bediening (straal: Dubbel, op: (Dubbel) -> Dubbel) println ("Radius is $ radius") val result = op (radius) println ("Het resultaat is $ resultaat") fun main (args: reeks) circleOperation (5.3, (2 * Math.PI) * it)
Om een hogere-orde functie inline te maken, moeten we de in lijn
modifier voor de pret
zoekwoord, net als in de bovenstaande code. Laten we ook de bytecode controleren die voor deze inline-functie is gegenereerd.
public static final leegte circleOperation (dubbele straal, @NotNull Function1 op) Intrinsics.checkParameterIsNotNull (op, "op"); String var4 = "Radius is" + straal; System.out.println (Var4); double result = ((Number) op.invoke (radius)). doubleValue (); String var6 = "Het resultaat is" + resultaat; System.out.println (var6); public static final void main (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); dubbele straal $ iv = 5,3D; String var3 = "Radius is" + straal $ iv; System.out.println (var3); dubbel resultaat $ iv = 6.283185307179586D * straal $ iv; String var9 = "Het resultaat is" + resultaat $ iv; System.out.println (var9);
Kijken naar de gegenereerde bytecode voor de inline-functie in de hoofd()
functie, je kunt dat waarnemen in plaats van de circleOperation ()
functie, het heeft nu het circleOperation ()
functioneringslichaam inclusief de lambda-body en geplakt op de call-site.
Met dit mechanisme is onze code aanzienlijk geoptimaliseerd - niet meer het maken van anonieme klassen of extra geheugentoewijzingen. Maar wees u ervan bewust dat we achter de schermen een grotere bytecode zouden hebben dan voorheen. Om deze reden wordt het ten zeerste aanbevolen alleen kleinere functies van hogere orde in te schakelen die lambda als parameters accepteren.
Veel van de hogere orde functies van de standaard bibliotheek in Kotlin hebben de inline modifier. Bijvoorbeeld als u een kijkje neemt bij de functies van de verzamelingbewerking filter()
en eerste()
, je zult zien dat ze de hebben in lijn
modifier en zijn ook klein van formaat.
openbare inline plezierIterable .filter (predicate: (T) -> Boolean): List return filterTo (ArrayList (), predicate) public inline fun Iterable .first (predicate: (T) -> Boolean): T for (element in this) if (predicate (element)) return element throw NoSuchElementException ("Verzameling bevat geen element dat overeenkomt met het predikaat.")
Vergeet niet om normale functies inline te gebruiken die geen lambda als parameter accepteren! Ze zullen compileren, maar er is geen significante prestatieverbetering (IntelliJ IDEA zou hier zelfs een hint over geven).
noinline
wijzigerAls je meer dan twee lambda-parameters hebt in een functie, heb je de mogelijkheid om te beslissen welke lambda niet inline zal gebruiken met de noinline
modifier op de parameter. Deze functionaliteit is vooral nuttig voor een lambda-parameter die veel code in zich opneemt. Met andere woorden, de Kotlin-compiler kopieert en plakt die lambda niet waar deze wordt genoemd, maar maakt in plaats daarvan een anonieme klasse achter de schermen.
inline fun myFunc (op: (Double) -> Double, noinline op2: (Int) -> Int) // voer operaties uit
Hier hebben we de noinline
modifier voor de tweede lambda-parameter. Merk op dat deze modifier alleen geldig is als de functie de in lijn
wijziger.
Merk op dat wanneer een uitzondering in een inline-functie wordt gegooid, de methode-oproepstapel in de stacktracering verschilt van een normale functie zonder de in lijn
modifier. Dit komt door het kopieer- en plakmechanisme dat door de compiler wordt gebruikt voor inline-functies. Het leuke is dat IntelliJ IDEA ons helpt om gemakkelijk de methode-call stack in de stacktracering te navigeren voor een inline-functie. Laten we een voorbeeld bekijken.
inline fun myFunc (op: (Double) -> Double) throw Exception ("message 123") fun main (args: Array) myFunc (4.5)
In de bovenstaande code wordt een uitzondering bewust in de inline-functie gegooid myFunc ()
. Laten we nu de stack volgen binnen IntelliJ IDEA wanneer de code wordt uitgevoerd. Als u naar de onderstaande schermafbeelding kijkt, ziet u dat we twee navigatie-opties kunnen kiezen: de inline-functie-instantie of de inline-functie-oproepsite. Het kiezen van het eerste zal ons zover brengen dat de uitzondering in het functielichaam is gegooid, terwijl het laatste ons naar het punt zal brengen dat de methode werd genoemd.
Als de functie geen inline-functie was, zou onze stacktracering eruit zien als degene die u misschien al kent:
In deze zelfstudie hebt u nog meer dingen geleerd die u kunt doen met functies in Kotlin. We hebben betrekking op:
In de volgende tutorial in de Kotlin From Scratch-serie gaan we op zoek naar objectgeoriënteerd programmeren en beginnen we te leren hoe lessen werken in Kotlin. Tot ziens!
Voor meer informatie over de Kotlin-taal, raad ik aan de Kotlin-documentatie te bezoeken. Of bekijk enkele van onze andere Android-apps voor app-ontwikkeling hier op Envato Tuts+!