Questo codelab fa parte del corso Kotlin Bootcamp for Programmers. Per ottenere il massimo valore da questo corso, ti consigliamo di seguire le codelab in sequenza. A seconda delle tue conoscenze, potresti riuscire a leggere rapidamente alcune sezioni. Questo corso è rivolto ai programmatori che conoscono un linguaggio orientato agli oggetti e vogliono imparare Kotlin.
Introduzione
In questo codelab vengono presentate classi, funzioni e metodi generici e il loro funzionamento in Kotlin.
Anziché creare una singola app di esempio, le lezioni di questo corso sono progettate per sviluppare le tue conoscenze, ma sono semi-indipendenti l'una dall'altra, in modo che tu possa scorrere rapidamente le sezioni che conosci. Per collegarli, molti esempi utilizzano un tema acquatico. Se vuoi scoprire tutta la storia dell'acquario, dai un'occhiata al corso Kotlin Bootcamp for Programmers di Udacity.
Cosa devi già sapere
- La sintassi di funzioni, classi e metodi Kotlin
- Come creare una nuova classe in IntelliJ IDEA ed eseguire un programma
Obiettivi didattici
- Come utilizzare classi, metodi e funzioni generici
In questo lab proverai a:
- Creare una classe generica e aggiungere vincoli
- Crea tipi
ineout - Creare funzioni generiche, metodi e funzioni di estensione
Introduzione ai generici
Kotlin, come molti linguaggi di programmazione, ha tipi generici. Un tipo generico ti consente di rendere generica una classe e quindi molto più flessibile.
Immagina di implementare una classe MyList che contiene un elenco di elementi. Senza i generici, dovresti implementare una nuova versione di MyList per ogni tipo: una per Double, una per String e una per Fish. Con i generici, puoi rendere l'elenco generico, in modo che possa contenere qualsiasi tipo di oggetto. È come rendere il tipo un carattere jolly che si adatta a molti tipi.
Per definire un tipo generico, inserisci T tra parentesi angolari <T> dopo il nome della classe. Puoi utilizzare un'altra lettera o un nome più lungo, ma la convenzione per un tipo generico è T.
class MyList<T> {
fun get(pos: Int): T {
TODO("implement")
}
fun addItem(item: T) {}
}Puoi fare riferimento a T come se fosse un tipo normale. Il tipo restituito per get() è T e il parametro per addItem() è di tipo T. Naturalmente, gli elenchi generici sono molto utili, quindi la classe List è integrata in Kotlin.
Passaggio 1: crea una gerarchia dei tipi
In questo passaggio crei alcune classi da utilizzare nel passaggio successivo. La creazione di sottoclassi è stata trattata in un codelab precedente, ma ecco un breve riepilogo.
- Per mantenere l'esempio pulito, crea un nuovo pacchetto in src e chiamalo
generics. - Nel pacchetto generics, crea un nuovo file
Aquarium.kt. In questo modo puoi ridefinire gli elementi utilizzando gli stessi nomi senza conflitti, quindi il resto del codice di questo codelab viene inserito in questo file. - Crea una gerarchia di tipi di approvvigionamento idrico. Inizia rendendo
WaterSupplyuna classeopen, in modo che possa essere suddivisa in sottoclassi. - Aggiungi un parametro booleano
var,needsProcessing. In questo modo viene creata automaticamente una proprietà modificabile, insieme a un getter e un setter. - Crea una sottoclasse
TapWaterche estendaWaterSupplye passatrueperneedsProcessing, perché l'acqua del rubinetto contiene additivi dannosi per i pesci. - In
TapWater, definisci una funzione chiamataaddChemicalCleaners()che impostaneedsProcessingsufalsedopo aver pulito l'acqua. La proprietàneedsProcessingpuò essere impostata daTapWaterperché èpublicper impostazione predefinita e accessibile alle sottoclassi. Ecco il codice completato.
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}- Crea altre due sottoclassi di
WaterSupply, denominateFishStoreWatereLakeWater.FishStoreWaternon richiede l'elaborazione, maLakeWaterdeve essere filtrato con il metodofilter(). Dopo il filtraggio, non è necessario elaborarlo di nuovo, quindi infilter()impostaneedsProcessing = false.
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}Se hai bisogno di ulteriori informazioni, rivedi la lezione precedente sull'ereditarietà in Kotlin.
Passaggio 2: crea una classe generica
In questo passaggio modifichi la classe Aquarium per supportare diversi tipi di approvvigionamento idrico.
- In Aquarium.kt, definisci una classe
Aquarium, con<T>tra parentesi dopo il nome della classe. - Aggiungi una proprietà immutabile
waterSupplydi tipoTaAquarium.
class Aquarium<T>(val waterSupply: T)- Scrivi una funzione denominata
genericsExample(). Non fa parte di una classe, quindi può essere inserito nel livello superiore del file, come la funzionemain()o le definizioni di classe. Nella funzione, crea unAquariume trasmetti unWaterSupply. Poiché il parametrowaterSupplyè generico, devi specificare il tipo tra parentesi angolari<>.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
}- In
genericsExample()il tuo codice può accedere all'waterSupplydell'acquario. Poiché è di tipoTapWater, puoi chiamareaddChemicalCleaners()senza alcun cast di tipo.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}- Quando crei l'oggetto
Aquarium, puoi rimuovere le parentesi angolari e ciò che è contenuto al loro interno perché Kotlin ha l'inferenza del tipo. Quindi non c'è motivo di direTapWaterdue volte quando crei l'istanza. Il tipo può essere dedotto dall'argomento diAquarium; verrà comunque creato unAquariumdi tipoTapWater.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}- Per vedere cosa sta succedendo, stampa
needsProcessingprima e dopo aver chiamatoaddChemicalCleaners(). Di seguito è riportata la funzione completata.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
aquarium.waterSupply.addChemicalCleaners()
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}- Aggiungi una funzione
main()per chiamaregenericsExample(), quindi esegui il programma e osserva il risultato.
fun main() {
genericsExample()
}⇒ water needs processing: true water needs processing: false
Passaggio 3: specifica ulteriormente
Generico significa che puoi superare quasi tutto e a volte questo è un problema. In questo passaggio rendi la classe Aquarium più specifica su cosa puoi inserirvi.
- In
genericsExample(), crea unAquarium, passando una stringa perwaterSupply, quindi stampa la proprietàwaterSupplydell'acquario.
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}- Esegui il programma e osserva il risultato.
⇒ string
Il risultato è la stringa che hai passato, perché Aquarium non impone limitazioni a T.. È possibile passare qualsiasi tipo, incluso String.
- In
genericsExample(), crea un altroAquarium, passandonullperwaterSupply. SewaterSupplyè null, stampa"waterSupply is null".
fun genericsExample() {
val aquarium3 = Aquarium(null)
if (aquarium3.waterSupply == null) {
println("waterSupply is null")
}
}- Esegui il programma e osserva il risultato.
⇒ waterSupply is null
Perché puoi passare null quando crei un Aquarium? Ciò è possibile perché per impostazione predefinita T indica il tipo Any? nullable, il tipo in cima alla gerarchia dei tipi. Di seguito è riportato l'equivalente di ciò che hai digitato in precedenza.
class Aquarium<T: Any?>(val waterSupply: T)- Per non consentire il passaggio di
null, creaTdi tipoAnyin modo esplicito rimuovendo?dopoAny.
class Aquarium<T: Any>(val waterSupply: T)In questo contesto, Any è chiamato vincolo generico. Ciò significa che per T può essere passato qualsiasi tipo, purché non sia null.
- Quello che vuoi davvero è assicurarti che solo un
WaterSupply(o una delle sue sottoclassi) possa essere passato perT. SostituisciAnyconWaterSupplyper definire un vincolo generico più specifico.
class Aquarium<T: WaterSupply>(val waterSupply: T)Passaggio 4: aggiungi altre verifiche
In questo passaggio scoprirai la funzione check() per assicurarti che il codice si comporti come previsto. La funzione check() è una funzione di libreria standard in Kotlin. Funge da asserzione e genera un IllegalStateException se il relativo argomento restituisce false.
- Aggiungi un metodo
addWater()alla classeAquariumper aggiungere acqua, con uncheck()che assicura di non doverla trattare prima.
class Aquarium<T: WaterSupply>(val waterSupply: T) {
fun addWater() {
check(!waterSupply.needsProcessing) { "water supply needs processing first" }
println("adding water from $waterSupply")
}
}In questo caso, se needsProcessing è true, check() genererà un'eccezione.
- In
genericsExample(), aggiungi il codice per creare unAquariumconLakeWater, poi aggiungi un po' d'acqua.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}- Esegui il programma e riceverai un'eccezione perché l'acqua deve essere filtrata prima.
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first
at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)- Aggiungi una chiamata per filtrare l'acqua prima di aggiungerla a
Aquarium. Ora, quando esegui il programma, non viene generata alcuna eccezione.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.waterSupply.filter()
aquarium4.addWater()
}⇒ adding water from generics.LakeWater@880ec60
Quanto sopra illustra le nozioni di base dei generici. Le seguenti attività riguardano più aspetti, ma il concetto importante è come dichiarare e utilizzare una classe generica con un vincolo generico.
In questa attività, scoprirai i tipi in e out con i generici. Un tipo in è un tipo che può essere passato solo a una classe, non restituito. Un tipo out è un tipo che può essere restituito solo da una classe.
Guarda la classe Aquarium e vedrai che il tipo generico viene restituito solo quando si ottiene la proprietà waterSupply. Non esistono metodi che accettano un valore di tipo T come parametro (tranne per la definizione nel costruttore). Kotlin ti consente di definire tipi out esattamente per questo caso e può dedurre informazioni aggiuntive su dove è sicuro utilizzare i tipi. Analogamente, puoi definire i tipi in per i tipi generici che vengono passati solo ai metodi, non restituiti. In questo modo, Kotlin può eseguire controlli aggiuntivi per la sicurezza del codice.
I tipi in e out sono direttive per il sistema di tipi di Kotlin. Spiegare l'intero sistema di tipi non rientra nell'ambito di questo bootcamp (è piuttosto complesso). Tuttavia, il compilatore segnalerà i tipi che non sono contrassegnati in modo appropriato con in e out, quindi devi conoscerli.
Passaggio 1: definisci un tipo di uscita
- Nella classe
Aquarium, modificaT: WaterSupplyin modo che sia di tipoout.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}- Nello stesso file, al di fuori della classe, dichiara una funzione
addItemTo()che prevede unAquariumdiWaterSupply.
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")- Chiama
addItemTo()dagenericsExample()ed esegui il programma.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}⇒ item added
Kotlin può garantire che addItemTo() non esegua operazioni non sicure per il tipo con il generico WaterSupply, perché è dichiarato come tipo out.
- Se rimuovi la parola chiave
out, il compilatore restituirà un errore quando chiamiaddItemTo(), perché Kotlin non può garantire che non stai facendo nulla di non sicuro con il tipo.
Passaggio 2: definisci un tipo di ingresso
Il tipo in è simile al tipo out, ma per i tipi generici che vengono passati solo alle funzioni, non restituiti. Se provi a restituire un tipo in, riceverai un errore del compilatore. In questo esempio definirai un tipo in come parte di un'interfaccia.
- In Aquarium.kt, definisci un'interfaccia
Cleanerche accetta unTgenerico vincolato aWaterSupply. Poiché viene utilizzato solo come argomento perclean(), puoi impostarlo come parametroin.
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}- Per utilizzare l'interfaccia
Cleaner, crea una classeTapWaterCleanerche implementiCleanerper la pulizia diTapWateraggiungendo prodotti chimici.
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}- Nella classe
Aquarium, aggiornaaddWater()per prelevare unCleanerdi tipoTe pulisci l'acqua prima di aggiungerla.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
fun addWater(cleaner: Cleaner<T>) {
if (waterSupply.needsProcessing) {
cleaner.clean(waterSupply)
}
println("water added")
}
}- Aggiorna il codice di esempio
genericsExample()per creare unTapWaterCleaner, unAquariumconTapWater, quindi aggiungi un po' d'acqua utilizzando il detergente. Utilizzerà il detergente quando necessario.
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}Kotlin utilizzerà le informazioni sui tipi in e out per assicurarsi che il codice utilizzi i generici in modo sicuro. Out e in sono facili da ricordare: i tipi out possono essere passati verso l'esterno come valori restituiti, mentre i tipi in possono essere passati verso l'interno come argomenti.

Se vuoi approfondire il tipo di problemi che risolvono i tipi in entrata e in uscita, la documentazione li tratta in modo approfondito.
In questa attività imparerai a conoscere le funzioni generiche e quando utilizzarle. In genere, creare una funzione generica è una buona idea quando la funzione accetta un argomento di una classe con un tipo generico.
Passaggio 1: crea una funzione generica
- In generics/Aquarium.kt, crea una funzione
isWaterClean()che accetta unAquarium. Devi specificare il tipo generico del parametro. Un'opzione è utilizzareWaterSupply.
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}Tuttavia, ciò significa che Aquarium deve avere un parametro di tipo out per poter essere chiamato. A volte out o in sono troppo restrittivi perché devi utilizzare un tipo sia per l'input che per l'output. Puoi rimuovere il requisito out rendendo generica la funzione.
- Per rendere generica la funzione, inserisci le parentesi angolari dopo la parola chiave
funcon un tipo genericoTe eventuali vincoli, in questo casoWaterSupply. ModificaAquariumin modo che sia vincolato daTanziché daWaterSupply.
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}T è un parametro di tipo per isWaterClean() che viene utilizzato per specificare il tipo generico di acquario. Questo pattern è molto comune ed è una buona idea dedicare un po' di tempo per analizzarlo.
- Chiama la funzione
isWaterClean()specificando il tipo tra parentesi angolari subito dopo il nome della funzione e prima delle parentesi.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean<TapWater>(aquarium)
}- A causa dell'inferenza del tipo dall'argomento
aquarium, il tipo non è necessario, quindi rimuovilo. Esegui il programma e osserva l'output.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean(aquarium)
}⇒ aquarium water is clean: false
Passaggio 2: crea un metodo generico con un tipo reificato
Puoi utilizzare funzioni generiche anche per i metodi, persino nelle classi che hanno il proprio tipo generico. In questo passaggio, aggiungi un metodo generico a Aquarium che controlla se ha un tipo di WaterSupply.
- Nella classe
Aquarium, dichiara un metodo,hasWaterSupplyOfType(), che accetta un parametro genericoR(Tè già utilizzato) vincolato aWaterSupplye restituiscetruesewaterSupplyè di tipoR. È simile alla funzione dichiarata in precedenza, ma all'interno della classeAquarium.
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R- Nota che l'ultimo
Rè sottolineato in rosso. Tieni il puntatore sopra l'icona per vedere qual è l'errore.
- Per eseguire un controllo
is, devi indicare a Kotlin che il tipo è reified, ovvero reale, e può essere utilizzato nella funzione. Per farlo, inserisciinlinedavanti alla parola chiavefunereifieddavanti al tipo genericoR.
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is RUna volta che un tipo è stato reso concreto, puoi utilizzarlo come un tipo normale, perché è un tipo reale dopo l'inlining. Ciò significa che puoi eseguire controlli is utilizzando il tipo.
Se non utilizzi reified qui, il tipo non sarà "reale" abbastanza da consentire a Kotlin di eseguire i controlli is. Questo perché i tipi non reificati sono disponibili solo in fase di compilazione e non possono essere utilizzati in fase di runtime dal programma. Questo aspetto viene trattato in modo più approfondito nella sezione successiva.
- Pass
TapWatercome tipo. Come per le funzioni generiche, chiama i metodi generici utilizzando le parentesi angolari con il tipo dopo il nome della funzione. Esegui il programma e osserva il risultato.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>()) // true
}⇒ true
Passaggio 3: crea le funzioni di estensione
Puoi utilizzare i tipi concreti anche per le funzioni regolari e di estensione.
- Al di fuori della classe
Aquarium, definisci una funzione di estensione suWaterSupplychiamataisOfType()che controlla seWaterSupplypassato è di un tipo specifico, ad esempioTapWater.
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T- Chiama la funzione di estensione proprio come un metodo.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}⇒ true
Con queste funzioni di estensione, non importa il tipo di Aquarium (Aquarium o TowerTank o qualche altra sottoclasse), purché sia un Aquarium. L'utilizzo della sintassi di proiezione con asterisco è un modo pratico per specificare una serie di corrispondenze. Quando utilizzi una proiezione a stella, Kotlin si assicura che tu non faccia nulla di pericoloso.
- Per utilizzare una proiezione stellare, inserisci
<*>dopoAquarium. SpostahasWaterSupplyOfType()in modo che sia una funzione di estensione, perché non fa parte dell'API di base diAquarium.
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R- Modifica la chiamata in
hasWaterSupplyOfType()ed esegui il programma.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>())
}⇒ true
Nell'esempio precedente, dovevi contrassegnare il tipo generico come reified e rendere la funzione inline, perché Kotlin deve conoscerli in fase di runtime, non solo in fase di compilazione.
Tutti i tipi generici vengono utilizzati solo in fase di compilazione da Kotlin. In questo modo, il compilatore può assicurarsi che tu stia facendo tutto in sicurezza. In fase di runtime tutti i tipi generici vengono cancellati, da cui il messaggio di errore precedente relativo al controllo di un tipo cancellato.
Il compilatore può creare codice corretto senza conservare i tipi generici fino al runtime. Tuttavia, a volte fai qualcosa, come i controlli is sui tipi generici, che il compilatore non può supportare. Per questo motivo, Kotlin ha aggiunto i tipi reified, o reali.
Puoi scoprire di più sui tipi reificati e sull'eliminazione dei tipi nella documentazione di Kotlin.
Questa lezione si è concentrata sui generici, che sono importanti per rendere il codice più flessibile e più facile da riutilizzare.
- Crea classi generiche per rendere il codice più flessibile.
- Aggiungi vincoli generici per limitare i tipi utilizzati con i generici.
- Utilizza i tipi
ineoutcon i generici per fornire un controllo dei tipi migliore per limitare i tipi passati o restituiti dalle classi. - Crea funzioni e metodi generici per lavorare con tipi generici. Ad esempio:
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... } - Utilizza funzioni di estensione generiche per aggiungere funzionalità non di base a una classe.
- A volte i tipi concreti sono necessari a causa dell'eliminazione dei tipi. I tipi concreti, a differenza di quelli generici, vengono mantenuti durante l'esecuzione.
- Utilizza la funzione
check()per verificare che il codice venga eseguito come previsto. Ad esempio:check(!waterSupply.needsProcessing) { "water supply needs processing first" }
Documentazione di Kotlin
Se vuoi maggiori informazioni su un argomento di questo corso o se hai difficoltà, https://kotlinlang.org è il punto di partenza migliore.
- Generics
- Vincoli generici
- Proiezioni stellari
- Tipi
Ineout - Parametri concreti
- Cancellazione del tipo
- Funzione
check()
Tutorial di Kotlin
Il sito web https://try.kotlinlang.org include tutorial dettagliati chiamati Kotlin Koans, un interprete basato sul web e una serie completa di documentazione di riferimento con esempi.
Corso Udacity
Per visualizzare il corso Udacity su questo argomento, consulta Kotlin Bootcamp for Programmers.
IntelliJ IDEA
La documentazione di IntelliJ IDEA è disponibile sul sito web di JetBrains.
Questa sezione elenca i possibili compiti a casa per gli studenti che seguono questo codelab nell'ambito di un corso guidato da un insegnante. Spetta all'insegnante:
- Assegna i compiti, se richiesto.
- Comunica agli studenti come inviare i compiti.
- Valuta i compiti a casa.
Gli insegnanti possono utilizzare questi suggerimenti nella misura che ritengono opportuna e sono liberi di assegnare qualsiasi altro compito a casa che ritengono appropriato.
Se stai seguendo questo codelab in autonomia, sentiti libero di utilizzare questi compiti per casa per mettere alla prova le tue conoscenze.
Rispondi a queste domande
Domanda 1
Quale delle seguenti è la convenzione per la denominazione di un tipo generico?
▢ <Gen>
▢ <Generic>
▢ <T>
▢ <X>
Domanda 2
Una limitazione dei tipi consentiti per un tipo generico è chiamata:
▢ una limitazione generica
▢ un vincolo generico
▢ disambiguazione
▢ un limite di tipo generico
Domanda 3
Per reificazione si intende:
▢ È stato calcolato l'impatto dell'esecuzione reale di un oggetto.
▢ È stato impostato un indice di voci con limitazioni per il corso.
▢ Il parametro di tipo generico è stato trasformato in un tipo reale.
▢ È stato attivato un indicatore di errore remoto.
Passa alla lezione successiva:
Per una panoramica del corso, inclusi i link ad altri codelab, vedi "Kotlin Bootcamp for Programmers: Welcome to the course" (Kotlin Bootcamp per programmatori: benvenuto al corso).