Le funzioni di estensione (extension functions) sono una delle differenze chiave tra Kotlin e Java, apparse per la prima volta con le prime versioni del linguaggio ispirate a C#. Permettono di aggiungere nuovi metodi a classi esistenti senza necessità di ereditare o modificare la classe originale. Questo aumenta drasticamente l'espressività e la concisione del codice.
In Java e in molti altri linguaggi, non è possibile aggiungere metodi a una classe esistente (ad esempio, String o List) senza utilizzare l'ereditarietà o il pattern Decorator. Kotlin risolve questa mancanza tramite il meccanismo delle funzioni di estensione.
Molte classi della libreria (ad esempio, String, Int, List) non possono essere modificate direttamente. Spesso sono necessari metodi aggiuntivi per rendere il codice più leggibile ed evitare la duplicazione della logica. Tuttavia, le funzioni di estensione non modificano la classe reale: vengono create staticamente e all'interno non hanno accesso ai membri private/protected.
In Kotlin, la dichiarazione di una funzione di estensione appare così:
fun String.lastChar(): Char = this[this.length - 1]
Ora è possibile chiamare questo metodo su qualsiasi String:
"Kotlin".lastChar() // restituirà 'n'
Un funzione di estensione può essere sovrascritta in una sottoclasse?
No. Le funzioni di estensione non sono membri della classe, non possono essere sovrascritte tramite override: la loro staticità si manifesta anche nei sottotipi — il compilatore sceglie la funzione in base al tipo di espressione, non al reale tipo di oggetto.
open class Base class Derived : Base() fun Base.foo() = "base" fun Derived.foo() = "derived" fun printFoo(b: Base) { println(b.foo()) } val d = Derived() printFoo(d) // stamperà "base"
Le funzioni di estensione possono cambiare lo stato dell'oggetto?
Le funzioni di estensione non possono accedere allo stato private/protected, tuttavia se il tipo è mutabile (ad esempio, List o una classe personalizzata), possono modificare lo stato esterno disponibile tramite metodi pubblici.
fun MutableList<Int>.addTwice(elem: Int) { add(elem) add(elem) }
Le funzioni di estensione confliggono con i membri della classe con lo stesso nome?
Se il nome dell'estensione coincide con il nome di un vero metodo, la priorità va al membro della classe. L'estensione verrà chiamata solo se i membri sono assenti o non visibili.
class Foo { fun bar() = "member" } fun Foo.bar() = "extension" val f = Foo() println(f.bar()) // "member"
Un developer estende la classe String standard con una funzione che imposta un formato specifico per il progetto, e la utilizza ovunque — nel frattempo, altri sviluppatori non si rendono conto che non è un'operazione standard della classe String.
Pro:
Contro:
Estensione di una classe standard con un metodo utilitario, che è chiaramente localizzato nel pacchetto utils e ben documentato. L'uso è limitato a specifiche sezioni di codice chiaramente definite.
Pro:
Contro: