Kotlin è un linguaggio funzionale e ciò significa che le funzioni sono frontali e centrali. Il linguaggio è ricco di funzioni per rendere le funzioni di codifica semplici ed espressive. In questo post, scoprirai le funzioni di estensione, le funzioni di ordine superiore, le chiusure e le funzioni inline in Kotlin.
Nell'articolo precedente, hai imparato a conoscere le funzioni di primo livello, le espressioni lambda, le funzioni anonime, le funzioni locali, le funzioni infissi e infine le funzioni membro in Kotlin. In questo tutorial, continueremo a saperne di più sulle funzioni in Kotlin scavando in:
Non sarebbe bello se il Stringa
digitare Java aveva un metodo per capitalizzare la prima lettera in a Stringa
-piace ucfirst ()
in PHP? Potremmo chiamare questo metodo upperCaseFirstLetter ()
.
Per realizzare questo, potresti creare un Stringa
sottoclasse che estende il Stringa
digitare in Java. Ma ricorda che il Stringa
la classe in Java è finale, il che significa che non è possibile estenderla. Una possibile soluzione per Kotlin sarebbe quella di creare funzioni di supporto o funzioni di primo livello, ma questo potrebbe non essere l'ideale perché non potremmo quindi utilizzare la funzione di completamento automatico IDE per visualizzare l'elenco dei metodi disponibili per Stringa
genere. Quello che sarebbe davvero bello sarebbe in qualche modo aggiungere una funzione a una classe senza dover ereditare da quella classe.
Bene, Kotlin ci ha coperto con un'altra caratteristica eccezionale: funzioni di estensione. Questi ci danno la possibilità di estendere una classe con nuove funzionalità senza dover ereditare da quella classe. In altre parole, non è necessario creare un nuovo sottotipo o modificare il tipo originale.
Una funzione di estensione è dichiarata al di fuori della classe che vuole estendere. In altre parole, è anche una funzione di primo livello (se vuoi un aggiornamento sulle funzioni di primo livello in Kotlin, visita il tutorial More Fun With Functions di questa serie).
Insieme alle funzioni di estensione, Kotlin supporta anche proprietà di estensione. In questo post, discuteremo delle funzioni di estensione e aspetteremo un post futuro per discutere le proprietà dell'estensione insieme alle classi in Kotlin.
Come puoi vedere nel codice qui sotto, abbiamo definito una funzione di primo livello come normale per noi dichiarare una funzione di estensione. Questa funzione di estensione è all'interno di un pacchetto chiamato com.chike.kotlin.strings
.
Per creare una funzione di estensione, devi anteporre il nome della classe che stai estendendo prima del nome della funzione. Il nome della classe o il tipo su cui è definita l'estensione è chiamato tipo di ricevitore, e il oggetto ricevitore è l'istanza della classe o il valore su cui viene chiamata la funzione di estensione.
pacchetto com.chike.kotlin.strings fun String.upperCaseFirstLetter (): String return this.substring (0, 1) .toUpperCase (). plus (this.substring (1))
Si noti che il Questo
la parola chiave all'interno del corpo della funzione fa riferimento all'oggetto o istanza del destinatario.
Dopo aver creato la funzione di estensione, dovrai prima importare la funzione di estensione in altri pacchetti o file da utilizzare in quel file o pacchetto. Quindi, chiamare la funzione equivale a chiamare qualsiasi altro metodo della classe del tipo di ricevitore.
pacchetto com.chike.kotlin.packagex import com.chike.kotlin.strings.upperCaseFirstLetter print ("chike" .upperCaseFirstLetter ()) // "Chike"
Nell'esempio sopra, il tipo di ricevitore è di classe Stringa
, e il oggetto ricevitore è "Chike"
. Se utilizzi un IDE come IntelliJ IDEA con la funzione IntelliSense, vedresti la tua nuova funzione di estensione suggerita nell'elenco di altre funzioni in un Stringa
genere.
Nota che dietro le quinte, Kotlin creerà un metodo statico. Il primo argomento di questo metodo statico è l'oggetto destinatario. Quindi è facile per i chiamanti Java chiamare questo metodo statico e quindi passare l'oggetto ricevente come argomento.
Ad esempio, se la nostra funzione di estensione è stata dichiarata in a StringUtils.kt file, il compilatore Kotlin creerebbe una classe Java StringUtilsKt
con un metodo statico upperCaseFirstLetter ()
.
/ * Java * / pacchetto com.chike.kotlin.strings public class StringUtilsKt public static String upperCaseFirstLetter (String str) return str.substring (0, 1) .toUpperCase () + str.substring (1);
Ciò significa che i chiamanti Java possono semplicemente chiamare il metodo facendo riferimento alla sua classe generata, proprio come per qualsiasi altro metodo statico.
/ * Java * / print (StringUtilsKt.upperCaseFirstLetter ("chike")); // "Chike"
Ricorda che questo meccanismo di interoperabilità Java è simile a come funzionano le funzioni di primo livello in Kotlin, come abbiamo discusso nel post More Fun With Functions!
Nota che le funzioni di estensione non possono sovrascrivere funzioni già dichiarate in una classe o in un'interfaccia, note come funzioni membro (se vuoi un aggiornamento sulle funzioni membro in Kotlin, dai un'occhiata al precedente tutorial di questa serie). Quindi, se hai definito una funzione di estensione con la stessa identica funzione, lo stesso nome di funzione e lo stesso numero, tipi e ordine degli argomenti, indipendentemente dal tipo di ritorno, il compilatore di Kotlin non la invocherà. Nel processo di compilazione, quando viene invocata una funzione, il compilatore Kotlin cercherà innanzitutto una corrispondenza nelle funzioni membro definite nel tipo di istanza o nelle sue superclassi. Se c'è una corrispondenza, quella funzione membro è quella invocata o vincolata. Se non c'è corrispondenza, il compilatore invocherà qualsiasi funzione di estensione di quel tipo.
Quindi in sintesi: le funzioni dei membri vincono sempre.
Vediamo un esempio pratico.
class Student fun printResult () println ("Stampa risultato studente") fun expel () println ("Espellere studente da scuola") divertente Student.printResult () println ("Extension function printResult ()") fun Student.expel (reason: String) println ("Expelling student from School. Motivo: \" $ reason \ ""
Nel codice sopra, abbiamo definito un tipo chiamato Alunno
con due funzioni membro: printResult ()
e espellere()
. Abbiamo quindi definito due funzioni di estensione con gli stessi nomi delle funzioni membro.
Chiamiamo il printResult ()
funzione e vedere il risultato.
val student = Student () student.printResult () // Stampa risultato studente
Come puoi vedere, la funzione invocata o vincolata era la funzione membro e non la funzione di estensione con la stessa firma di funzione (sebbene IntelliJ IDEA ti desse comunque un suggerimento).
Tuttavia, chiamando la funzione membro espellere()
e la funzione di estensione espellere (motivo: stringa)
produrrà risultati diversi perché le firme delle funzioni sono diverse.
student.expel () // Espellere lo studente dalla scuola student.expel ("rubare denaro") // Espellere lo studente dalla scuola. Motivo: "rubato denaro"
La maggior parte delle volte dichiarerai una funzione di estensione come funzione di primo livello, ma tieni presente che puoi anche dichiararle come funzioni membro.
class ClassB classe ClassA fun ClassB.exFunction () print (toString ()) // chiama ClassB toString () fun callExFunction (classB: ClassB) classB.exFunction () // chiama la funzione di estensione
Nel codice sopra, abbiamo dichiarato una funzione di estensione exFunction ()
di ClassB
digita dentro un'altra classe Classe A
. Il Ricevitore di spedizione è l'istanza della classe in cui viene dichiarata l'estensione e l'istanza del tipo di destinatario del metodo di estensione è chiamata ricevitore di estensione. Quando c'è un conflitto di nome o ombreggiamento tra il ricevitore di invio e il ricevitore di estensione, notare che il compilatore sceglie il ricevitore di estensione.
Quindi nell'esempio di codice sopra, il ricevitore di estensione è un'istanza di ClassB
-quindi significa accordare()
il metodo è di tipo ClassB
quando chiamato all'interno della funzione di estensione exFunction ()
. Per noi invocare il accordare()
metodo del Ricevitore di spedizione Classe A
invece, abbiamo bisogno di usare un qualificato Questo
:
// ... fun ClassB.extFunction () print ([email protected] ()) // ora chiama il metodo ClassA toString () // ...
Una funzione di ordine superiore è solo una funzione che prende un'altra funzione (o espressione lambda) come parametro, restituisce una funzione o fa entrambe le cose. Il scorso()
la funzione di raccolta è un esempio di una funzione di ordine superiore della libreria standard.
val stringList: List= listOf ("in", "the", "club") print (stringList.last it.length == 3) // "the"
Qui abbiamo passato un lambda al scorso
funzione per fungere da predicato per cercare all'interno di un sottoinsieme di elementi. Ora ci tufferemo nella creazione delle nostre funzioni di ordine superiore in Kotlin.
Guardando la funzione circleOperation ()
sotto, ha due parametri. Il primo, raggio
, accetta un doppio e il secondo, operazione
, è una funzione che accetta un double come input e restituisce anche un double come output - possiamo dire in modo succinto che il secondo parametro è "una funzione da doppio a doppio".
Osservare che il operazione
i tipi di parametri della funzione per la funzione sono racchiusi tra parentesi ()
, e il tipo di output è separato da una freccia. La funzione circleOperation ()
è un tipico esempio di una funzione di ordine superiore che accetta una funzione come parametro.
fun calCircumference (raggio: Double) = (2 * Math.PI) * raggio fun calArea (raggio: Double): Double = (Math.PI) * Math.pow (raggio, 2.0) fun circleOperation (raggio: Double, op: (Double) -> Double): Double val result = op (raggio) risultato di ritorno
Nell'invocazione di questo circleOperation ()
funzione, passiamo un'altra funzione, calArea ()
, ad esso. (Si noti che se la firma del metodo della funzione passata non corrisponde a ciò che la funzione di ordine superiore dichiara, la chiamata alla funzione non verrà compilata).
Per passare il calArea ()
funzione come parametro per circleOperation ()
, abbiamo bisogno di prefisso con ::
e ometti il ()
parentesi.
print (circleOperation (3.0, :: calArea)) // 28.274333882308138 print (circleOperation (3.0, calArea)) // non compilerà print (circleOperation (3.0, calArea ())) // non compilerà print (circleOperation ( 6.7, :: calCircumference)) // 42.09734155810323
Usare saggiamente le funzioni di ordine superiore può rendere il nostro codice più facile da leggere e più comprensibile.
Possiamo anche passare un lambda (o una funzione letterale) a una funzione di ordine superiore direttamente quando si richiama la funzione:
circleOperation (5.3, (2 * Math.PI) * it)
Ricorda, per evitare di nominare esplicitamente l'argomento, possiamo usare il esso
argomento nome generato automaticamente per noi solo se il lambda ha un argomento. (Se vuoi un aggiornamento su lambda in Kotlin, visita il tutorial More Fun With Functions di questa serie).
Ricordare che oltre ad accettare una funzione come parametro, le funzioni di ordine superiore possono anche restituire una funzione ai chiamanti.
divertente moltiplicatore (fattore: doppio): (doppio) -> doppio = numero -> numero * fattore
qui il moltiplicatore()
la funzione restituirà una funzione che applica il fattore specificato a qualsiasi numero passato in esso. Questa funzione restituita è una lambda (o funzione letterale) dal doppio al doppio (il che significa che il parametro di input della funzione restituita è di tipo double e il risultato di output è anche un tipo double).
val doubler = moltiplicatore (2) print (doubler (5.6)) // 11.2
Per verificarlo, abbiamo passato un fattore due e assegnato la funzione restituita al duplicatore di variabili. Possiamo invocare questo come una normale funzione, e qualsiasi valore che passeremo sarà raddoppiato.
Una chiusura è una funzione che ha accesso a variabili e parametri definiti in un ambito esterno.
fun printFilteredNamesByLength (length: Int) val names = arrayListOf ("Adam", "Andrew", "Chike", "Kechi") val filterResult = names.filter it.length == length println (filterResult) printFilteredNamesByLength ( 5) // [Chike, Kechi]
Nel codice sopra, il lambda passò al filtro()
la funzione di raccolta usa il parametro lunghezza
della funzione esterna printFilteredNamesByLength ()
. Si noti che questo parametro è definito al di fuori dell'ambito della lambda, ma che il lambda è ancora in grado di accedere a lunghezza
. Questo meccanismo è un esempio di chiusura nella programmazione funzionale.
In More Fun With Functions, ho detto che il compilatore Kotlin crea una classe anonima nelle versioni precedenti di Java dietro le quinte quando creava espressioni lambda.
Sfortunatamente, questo meccanismo introduce un sovraccarico perché una classe anonima viene creata sotto il cofano ogni volta che creiamo un lambda. Inoltre, un lambda che usa il parametro di funzione esterno o la variabile locale con una chiusura aggiunge il proprio overhead di allocazione di memoria perché un nuovo oggetto viene allocato all'heap con ogni richiamo.
Per evitare questi costi generali, il team di Kotlin ci ha fornito il in linea
modificatore per le funzioni. Una funzione di ordine superiore con il in linea
il modificatore verrà sottolineato durante la compilazione del codice. In altre parole, il compilatore copia il lambda (o la funzione letterale) e anche il corpo della funzione di ordine superiore e li incolla sul sito di chiamata.
Diamo un'occhiata ad un esempio pratico.
fun circleOperation (raggio: Double, op: (Double) -> Double) println ("Radius is $ radius") val result = op (radius) println ("Il risultato è $ result") fun main (args: Array) circleOperation (5.3, (2 * Math.PI) * it)
Nel codice sopra, abbiamo una funzione di ordine superiore circleOperation ()
quello non ha il in linea
modificatore. Vediamo ora il bytecode di Kotlin generato quando compiliamo e decompiliamo il codice e poi lo confrontiamo con uno che ha il in linea
modificatore.
public final class InlineFunctionKt public static final void circleOperation (double radius, @NotNull Function1 op) Intrinsics.checkParameterIsNotNull (op, "op"); String var3 = "Raggio è" + raggio; System.out.println (VAR3); double result = ((Number) op.invoke (radius)). doubleValue (); String var5 = "Il risultato è" + risultato; System.out.println (VAR5); public static final void main (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); circleOperation (5.3D, (Function1) null.INSTANCE);
Nel bytecode Java generato sopra, puoi vedere che il compilatore ha chiamato la funzione circleOperation ()
dentro il principale()
metodo.
Specifichiamo ora la funzione di ordine superiore come in linea
invece, e vedi anche il bytecode generato.
inline fun circleOperation (raggio: Double, op: (Double) -> Double) println ("Radius is $ radius") val result = op (radius) println ("Il risultato è $ result") fun main (args: schieramento) circleOperation (5.3, (2 * Math.PI) * it)
Per rendere una funzione di ordine superiore in linea, dobbiamo inserire il in linea
modificatore prima del divertimento
parola chiave, proprio come abbiamo fatto nel codice sopra. Controlliamo anche il bytecode generato per questa funzione inline.
public static final void circleOperation (double radius, @NotNull Function1 op) Intrinsics.checkParameterIsNotNull (op, "op"); String var4 = "Raggio è" + raggio; System.out.println (var4); double result = ((Number) op.invoke (radius)). doubleValue (); String var6 = "Il risultato è" + risultato; System.out.println (var6); public static final void main (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); doppio raggio $ iv = 5.3D; String var3 = "Raggio è" + raggio $ iv; System.out.println (VAR3); doppio risultato $ iv = 6.283185307179586D * raggio $ iv; String var9 = "Il risultato è" + result $ iv; System.out.println (var9);
Guardando il bytecode generato per la funzione inline all'interno di principale()
funzione, è possibile osservare che invece di chiamare il circleOperation ()
funzione, ora ha copiato il file circleOperation ()
corpo della funzione compreso il corpo lambda e incollato al suo sito di chiamata.
Con questo meccanismo, il nostro codice è stato ottimizzato in modo significativo, non più creazione di classi anonime o allocazioni di memoria extra. Ma sappi che avremmo un bytecode più ampio dietro le quinte di prima. Per questo motivo, si consiglia vivamente di incorporare solo funzioni di ordine superiore più piccole che accettano lambda come parametri.
Molte delle funzioni di ordine superiore della libreria standard in Kotlin hanno il modificatore in linea. Ad esempio, se si dà un'occhiata alle funzioni di operazione di raccolta filtro()
e primo()
, vedrai che hanno il in linea
modificatore e sono anche di piccole dimensioni.
divertimento pubblico in lineaiterable .filter (predicato: (T) -> Boolean): List return filterTo (ArrayList (), predicato) divertimento pubblico in linea iterable .primo (predicato: (T) -> Boolean): T for (elemento in questo) if (predicate (elemento)) return elemento lancia NoSuchElementException ("Collection non contiene elementi corrispondenti al predicato.")
Ricordati di non incorporare le normali funzioni che non accettano un lambda come parametro! Verranno compilati, ma non ci sarebbe alcun miglioramento significativo delle prestazioni (IntelliJ IDEA potrebbe anche dare un suggerimento a riguardo).
noinline
ModificatoreSe hai più di due parametri lambda in una funzione, hai la possibilità di decidere quale lambda non usare in linea usando il noinline
modificatore sul parametro. Questa funzionalità è utile specialmente per un parametro lambda che richiede molto codice. In altre parole, il compilatore Kotlin non copierà e incollerà quel lambda dove viene chiamato, ma creerà invece una classe anonima dietro la scena.
divertimento in linea myFunc (op: (Double) -> Double, noinline op2: (Int) -> Int) // esegui operazioni
Qui, abbiamo inserito il noinline
modificatore al secondo parametro lambda. Si noti che questo modificatore è valido solo se la funzione ha il in linea
modificatore.
Si noti che quando un'eccezione viene lanciata all'interno di una funzione inline, lo stack di chiamata al metodo nello stack trace è diverso da una normale funzione senza il in linea
modificatore. Ciò è dovuto al meccanismo di copia e incolla utilizzato dal compilatore per le funzioni inline. La cosa interessante è che IntelliJ IDEA ci aiuta a navigare facilmente nello stack di chiamata del metodo nello stack trace per una funzione inline. Vediamo un esempio.
divertimento in linea myFunc (op: (Double) -> Double) throw Exception ("message 123") fun main (args: Array) myFunc (4.5)
Nel codice sopra, un'eccezione viene lanciata deliberatamente all'interno della funzione inline myFunc ()
. Vediamo ora la traccia dello stack all'interno di IntelliJ IDEA quando viene eseguito il codice. Guardando lo screenshot qui sotto, puoi vedere che ci sono due opzioni di navigazione da scegliere: il corpo della funzione Inline o il sito di chiamata della funzione inline. La scelta del primo ci porterà al punto in cui l'eccezione è stata lanciata nel corpo della funzione, mentre il secondo ci porterà al punto in cui il metodo è stato chiamato.
Se la funzione non era in linea, la nostra traccia di stack sarebbe come quella con cui potresti già familiarizzare:
In questo tutorial, hai imparato ancora più cose che puoi fare con le funzioni di Kotlin. Abbiamo coperto:
Nel prossimo tutorial della serie Kotlin From Scratch, scaveremo nella programmazione orientata agli oggetti e inizieremo ad imparare come funzionano le classi in Kotlin. A presto!
Per saperne di più sulla lingua di Kotlin, ti consiglio di visitare la documentazione di Kotlin. Oppure guarda alcuni dei nostri altri post di sviluppo di app per Android qui su Envato Tuts+!