Questo codelab fa parte del Kotlin Bootcamp for Programs. Otterrai il massimo valore da questo corso se lavori in sequenza nei codelab. A seconda delle tue conoscenze, potresti essere in grado di scorrere alcune sezioni. Questo corso è rivolto ai programmatori che conoscono una lingua orientata agli oggetti e vogliono imparare il kotlin.
Introduzione
In questo codelab ti verranno illustrati corsi, funzioni e metodi generici e come funzionano in Kotlin.
Anziché creare un'unica app di esempio, le lezioni di questo corso sono state concepite per sviluppare le tue conoscenze, ma sono semi-indipendenti gli uni dagli altri per esplorare le sezioni che conosci. Per collegare tutti questi esempi, molti sono basati su un acquario. E se vuoi vedere l'intera storia dell'acquario, dai un'occhiata al Bootcamp Kotlin per programmatoriCorso Udacity.
Informazioni importanti
- La sintassi delle funzioni, delle classi e dei metodi di 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:
- Crea una classe generica e aggiungi vincoli
- Crea tipi
in
eout
- Creare funzioni, metodi ed estensioni generici
Introduzione ai termini generici
Kotlin, come molti linguaggi di programmazione, ha tipi generici. Il tipo generico consente di rendere un corso generico e, di conseguenza, di renderlo molto più flessibile.
Immagina di implementare una classe MyList
che contiene un elenco di elementi. Senza generiche, dovresti implementare una nuova versione di MyList
per ogni tipo: una per Double
, una per String
, una per Fish
. Con i generici, puoi rendere generico l'elenco, in modo che possa contenere qualsiasi tipo di oggetto. È simile a un tipo di 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 di valore restituito per get()
è T
, mentre 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 creerai alcuni corsi da utilizzare al suo interno. La sottoclasse è stata trattata in un codelab precedente, ma ecco una breve revisione.
- Per mantenere l'esempio disponibile, 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, in modo che il resto del codice per questo codelab venga inserito in questo file. - Crea una gerarchia di tipi di approvvigionamento dell'acqua. Inizia rendendo
WaterSupply
una classeopen
, in modo che possa essere sottoclasse. - Aggiungi un parametro booleano
var
,needsProcessing
. In questo modo viene automaticamente creata una proprietà modificabile, insieme a un getter e un setter. - Creare una sottoclasse
TapWater
che includaWaterSupply
e superaretrue
perneedsProcessing
, poiché l'acqua del rubinetto contiene additivi non adatti al pesce. - In
TapWater
, definisci una funzione chiamataaddChemicalCleaners()
che impostaneedsProcessing
sufalse
dopo la pulizia dell'acqua. La proprietàneedsProcessing
può essere impostata daTapWater
perché èpublic
per impostazione predefinita ed è accessibile alle classi secondarie. 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
, denominateFishStoreWater
eLakeWater
.FishStoreWater
non richiede l'elaborazione, ma il filtroLakeWater
deve essere filtrato con il metodofilter()
. Non è necessario elaborare nuovamente la pagina dopo l'applicazione di filtri, quindi sceglineedsProcessing = false
infilter()
.
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}
Se hai bisogno di ulteriori informazioni, consulta la lezione precedente sull'ereditarietà in Kotlin.
Passaggio 2: crea una classe generica
In questo passaggio modificherai la classe Aquarium
in modo che supporti diversi tipi di acqua.
- In Aquarium.kt, definisci una classe
Aquarium
, con<T>
tra parentesi dopo il nome della classe. - Aggiungi una proprietà immutabile
waterSupply
di tipoT
aAquarium
.
class Aquarium<T>(val waterSupply: T)
- Scrivi una funzione chiamata
genericsExample()
. Non fa parte di un corso, pertanto può essere posizionato al livello superiore del file, ad esempio la funzionemain()
o le definizioni del corso. Nella funzione, inserisci unAquarium
e 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'acquario diwaterSupply
. Poiché è di tipoTapWater
, puoi chiamareaddChemicalCleaners()
senza trasmettere alcun tipo.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- Quando crei l'oggetto
Aquarium
, puoi rimuovere le parentesi angolari e le relative vie tra loro, perché Kotlin utilizza l'inferenza tipo. Pertanto non c'è motivo di ripetereTapWater
due volte quando crei l'istanza. Il tipo può essere dedotto dall'argomentoAquarium
; renderà comunqueAquarium
di tipoTapWater
.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- Per sapere che cosa sta succedendo, stampa
needsProcessing
prima e dopo aver chiamato il numeroaddChemicalCleaners()
. 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: rendilo più specifico
Generico, significa che non si è riscontrata alcuna eventualità e a volte questo è un problema. In questo passaggio rendi il corso Aquarium
più specifico riguardo agli argomenti che puoi inserire.
- In
genericsExample()
, crea un elementoAquarium
, passando una stringa per l'attributowaterSupply
, quindi stampa la proprietàwaterSupply
dell'acquario.
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}
- Esegui il programma osserva il risultato.
⇒ string
Il risultato è la stringa che hai passato, perché Aquarium
non applica limitazioni su T.
Qualsiasi tipo, incluso String
, può essere trasmesso.
- In
genericsExample()
, crea un altroAquarium
, superandonull
perwaterSupply
. 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 superare null
durante la creazione di un Aquarium
? Ciò è possibile perché, per impostazione predefinita, T
sta per "null" di tipo Any?
, ovvero il tipo all'inizio della gerarchia di tipi. Quanto segue equivale a ciò che hai digitato in precedenza.
class Aquarium<T: Any?>(val waterSupply: T)
- Per non consentire il passaggio di
null
, rendiT
di tipoAny
esplicitamente, rimuovendo?
dopoAny
.
class Aquarium<T: Any>(val waterSupply: T)
In questo contesto, Any
è chiamato vincolo generico. Ciò significa che qualsiasi tipo può essere trasmesso per T
a condizione che non sia null
.
- Assicurati soltanto che gli elementi
WaterSupply
(o una delle sue sottoclassi) possano essere trasmessi perT
. SostituisciAny
conWaterSupply
per definire un vincolo generico più specifico.
class Aquarium<T: WaterSupply>(val waterSupply: T)
Passaggio 4: aggiungi altro controllo
In questo passaggio viene illustrata la funzione check()
per assicurarsi che il codice funzioni come previsto. La funzione check()
è una funzione standard della libreria in Kotlin. Funge da asserzione e restituisce un elemento IllegalStateException
se l'argomento corrispondente è false
.
- Aggiungi un metodo
addWater()
alla classeAquarium
per aggiungere l'acqua, con uncheck()
che ti aiuti a non elaborarlo 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 del codice per creare unAquarium
conLakeWater
, poi aggiungi un po' di acqua.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}
- Esegui il programma e riceverai un'eccezione, perché l'acqua deve essere prima filtrata.
⇒ 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 tuo programma, non vengono fatte eccezioni.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.waterSupply.filter()
aquarium4.addWater()
}
⇒ adding water from generics.LakeWater@880ec60
La tabella precedente illustra i concetti di base degli annunci generici. Le attività descritte di seguito descrivono più a fondo, ma il concetto importante è come dichiarare e utilizzare una classe generica con un vincolo generico.
In questa attività vengono descritti i tipi di entrata e uscita con i generici. Un tipo in
è un tipo che può essere passato solo a un corso, non restituito. Un tipo out
è un tipo che può essere restituito solo da un corso.
Guardando la classe Aquarium
e vedrai che il tipo generico viene restituito solo quando ottieni la proprietà waterSupply
. Non ci sono metodi che utilizzano un valore di tipo T
come parametro, ad eccezione della definizione nel costruttore. Kotlin ti consente di definire out
tipi esattamente per questo caso e può dedurre ulteriori informazioni sulla posizione sicura di tali tipi. Allo stesso modo, puoi definire in
tipi per i tipi generici che vengono trasmessi solo in metodi, non restituiti. In questo modo Kotlin può effettuare controlli aggiuntivi per garantire la sicurezza del codice.
I tipi in
e out
sono istruzioni per il sistema di tipo Kotlin. Spiegando l'intero tipo di sistema non rientra nell'ambito di questo bootcamp (è piuttosto coinvolto); tuttavia, il compilatore segnalerà i tipi che non sono contrassegnati come in
e out
in modo appropriato, quindi devi conoscerli.
Passaggio 1: definisci un tipo di out
- Nel corso
Aquarium
, impostaT: WaterSupply
come tipoout
.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}
- Nello stesso file, all'esterno della classe, dichiara una funzione
addItemTo()
che prevede unAquarium
diWaterSupply
.
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 faccia nulla di sicuro con il tipo generico WaterSupply
, perché è stato dichiarato di tipo out
.
- Se rimuovi la parola chiave
out
, il compilatore restituirà un errore durante la chiamata aaddItemTo()
, perché Kotlin non può garantire che non stia facendo nulla di sicuro con questo tipo.
Passaggio 2: definisci un tipo
Il tipo in
è simile al tipo out
, ma per i tipi generici che vengono trasmessi solo in funzioni non vengono restituiti. Se tenti di restituire un tipo in
, riceverai un errore di compilazione. In questo esempio, definirai un tipo in
come parte di un'interfaccia.
- In Aquarium.kt, definisci un'interfaccia
Cleaner
che utilizzi unT
generico limitato 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 di
Cleaner
, crea una classeTapWaterCleaner
che implementiCleaner
per la pulizia diTapWater
aggiungendo prodotti chimici.
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}
- Nel corso
Aquarium
, aggiornaaddWater()
in modo che sia presente unCleaner
di tipoT
e pulisci l'acqua prima di aggiungerlo.
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
, unAquarium
conTapWater
e poi aggiungi un po' di acqua utilizzando il detergente. Utilizzerà il detergente in base alle necessità.
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}
Kotlin utilizzerà le informazioni del tipo in
e out
per assicurarsi che il codice utilizzi in modo sicuro i codici generici. Out
e in
sono facili da ricordare: i tipi out
possono essere trasmessi verso l'esterno come valori di ritorno e i tipi in
possono essere trasmessi verso l'interno come argomenti.
Se vuoi esaminare più a fondo il tipo di problemi relativi ai tipi e ai tipi di risoluzione, la documentazione li esamina in dettaglio.
In questa attività scoprirai le funzioni generiche e quando utilizzarle. In genere, creare una funzione generica è una buona idea quando questa funzione include argomenti di una classe di tipo generico.
Passaggio 1: realizza una funzione generica
- In generics/Aquarium.kt, crea una funzione
isWaterClean()
che richieda unAquarium
. Devi specificare il tipo generico del parametro; una delle opzioni consiste nell'utilizzareWaterSupply
.
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}
Tuttavia, questo significa che l'elemento Aquarium
deve avere un parametro di tipo out
per essere chiamato. A volte out
o in
sono troppo restrittivi, in quanto è necessario utilizzare un tipo sia per l'input che per l'output. Puoi rimuovere il requisito out
rendendo la funzione generica.
- Per rendere la funzione generica, inserisci le parentesi angolari dopo la parola chiave
fun
con un tipo genericoT
ed eventuali vincoli, in questo casoWaterSupply
. ModificaAquarium
in modo che sia vincolato daT
anziché daWaterSupply
.
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}
T
è un parametro di tipo isWaterClean()
per utilizzare il tipo generico dell'acquario. Questo schema è molto comune ed è una buona idea dedicare un po' di tempo a questo punto.
- Richiama 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 unificato
Puoi utilizzare le funzioni generiche anche per i metodi, anche nelle classi che dispongono di un tipo generico. In questo passaggio, devi aggiungere un metodo generico a Aquarium
per verificare se è di tipo WaterSupply
.
- Nella classe
Aquarium
, dichiara un metodohasWaterSupplyOfType()
che accetta un parametro genericoR
(T
è già utilizzato) vincolato aWaterSupply
e restituiscetrue
sewaterSupply
è di tipoR
. È la funzione che hai dichiarato 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 di esso per vedere qual è l'errore. - Per eseguire un controllo
is
, devi dire a Kotlin che il tipo è reificato o reale e può essere utilizzato nella funzione. A questo scopo, inserisciinline
davanti alla parola chiavefun
ereified
davanti al tipo genericoR
.
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
Una volta unificato un tipo, puoi utilizzarlo come un tipo normale, perché è un tipo reale dopo l'incorporamento. Ciò significa che puoi effettuare controlli is
utilizzando il tipo.
Se non utilizzi reified
qui, il tipo non può essere "reale", sufficiente per consentire a Kotlin di consentire i controlli is
. Questo perché i tipi non unificati sono disponibili solo al momento della compilazione e non possono essere utilizzati in fase di esecuzione dal tuo programma. Ne parliamo più avanti nella sezione successiva.
- Passa il tipo
TapWater
. Come per le funzioni generiche, richiama metodi generici utilizzando 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: configura le funzioni di estensione
Puoi utilizzare i tipi unificati anche per le funzioni normali e per le estensioni.
- Al di fuori della classe
Aquarium
, definisci una funzione estensione suWaterSupply
chiamataisOfType()
che verifichi se il valoreWaterSupply
superato è di un tipo specifico, ad esempioTapWater
.
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T
- Chiama la funzione estensione come se fosse un metodo.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}
⇒ true
Con queste funzioni di estensione, non ha importanza di che tipo sia Aquarium
(Aquarium
, TowerTank
o qualche altra sottoclasse), purché sia una Aquarium
. La sintassi di star-projection è un metodo pratico per specificare una serie di corrispondenze. Inoltre, se utilizzi una proiezione di stelle, Kotlin si assicurerà di non fare nulla di pericoloso.
- Per utilizzare una proiezione di stelle, inserisci
<*>
dopoAquarium
. SpostahasWaterSupplyOfType()
come funzione di estensione, perché non fa parte dell'API principale diAquarium
.
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R
- Cambia la chiamata a
hasWaterSupplyOfType()
ed esegui il programma.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>())
}
⇒ true
Nell'esempio precedente, hai dovuto contrassegnare il tipo generico come reified
e rendere la funzione inline
, perché Kotlin ha bisogno di conoscerle al momento dell'esecuzione, non solo dell'ora di compilazione.
Tutti i tipi generici vengono utilizzati solo in fase di compilazione da Kotlin. Ciò consente al compilatore di eseguire tutte le operazioni in sicurezza. Per impostazione predefinita, tutti i tipi generici vengono cancellati, pertanto viene visualizzato il precedente messaggio di errore relativo alla verifica di un tipo cancellato.
Il compilatore può creare il codice corretto senza conservare i tipi generici fino al tempo di esecuzione. Tuttavia, significa che a volte esegui operazioni quali il controllo dei tipi generici da parte di is
, che non sono supportate dal compilatore. Ecco perché Kotlin ha aggiunto tipi uniformi o reali.
Puoi leggere ulteriori informazioni sui tipi unificati e sulla cancellazione dei tipi nella documentazione di Kotlin.
Questa lezione è incentrata sugli annunci generici, che sono importanti per rendere il codice più flessibile e 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
in
eout
con parole chiave generiche per consentire un controllo migliore sui tipi di corsi o di corsi restituiti. - Crea funzioni e metodi generici per lavorare con tipi generici. Ad esempio:
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... }
- Puoi utilizzare funzioni di estensione generiche per aggiungere funzionalità non principali a una classe.
- A volte i tipi unificati sono necessari a causa della cancellazione dei tipi. I tipi unificati, a differenza dei tipi generici, rimangono in esecuzione.
- Utilizza la funzione
check()
per verificare che il codice funzioni come previsto. Ad esempio:check(!waterSupply.needsProcessing) { "water supply needs processing first" }
Documentazione di Kotlin
Se vuoi saperne di più su qualsiasi argomento del corso o se ti blocchi, https://kotlinlang.org è il punto di partenza migliore.
- Generici
- Vincoli generici
- Proiezioni speciali
- Tipi di
In
eout
- Parametri unificati
- Cancellazione con tipo
- Funzione
check()
Tutorial su Kotlin
Il sito web https://try.kotlinlang.org include tutorial avanzati 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 il Kotlin Bootcamp for Programrs.
IDA IntelliJ
La documentazione relativa a IntelliJ IDEA è disponibile sul sito web di JetBrains.
In questa sezione sono elencati i possibili compiti per gli studenti che lavorano attraverso questo codelab nell'ambito di un corso tenuto da un insegnante. Spetta all'insegnante fare quanto segue:
- Assegna i compiti, se necessario.
- Comunica agli studenti come inviare compiti.
- Valuta i compiti.
Gli insegnanti possono utilizzare i suggerimenti solo quanto e come vogliono e dovrebbero assegnare i compiti che ritengono appropriati.
Se stai lavorando da solo a questo codelab, puoi utilizzare questi compiti per mettere alla prova le tue conoscenze.
Rispondi a queste domande
Domanda 1
Quale delle seguenti convenzioni prevede la denominazione di un tipo generico?
▢ <Gen>
▢ <Generic>
▢ <T>
▢ <X>
Domanda 2
Una limitazione sui tipi consentiti per un tipo generico è chiamata:
▢ una restrizione generica
▢ un vincolo generico
▢, disambiguazione
▢ un tipo generico di limite
Domanda 3
Unificato significa:
▢ È stato calcolato l'impatto reale di esecuzione di un oggetto.
▢ È stato impostato un indice delle voci con restrizioni per il corso.
▢ Il parametro del tipo generico è stato trasformato in un tipo reale.
▢ Si è 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 Programs: Welcome to the Course."