Bottcamp Kotlin per programmatori 5.2: generiche

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 e out
  • 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.

  1. Per mantenere l'esempio disponibile, crea un nuovo pacchetto in src e chiamalo generics.
  2. 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.
  3. Crea una gerarchia di tipi di approvvigionamento dell'acqua. Inizia rendendo WaterSupply una classe open, in modo che possa essere sottoclasse.
  4. Aggiungi un parametro booleano var, needsProcessing. In questo modo viene automaticamente creata una proprietà modificabile, insieme a un getter e un setter.
  5. Creare una sottoclasse TapWater che includa WaterSupply e superare true per needsProcessing, poiché l'acqua del rubinetto contiene additivi non adatti al pesce.
  6. In TapWater, definisci una funzione chiamata addChemicalCleaners() che imposta needsProcessing su false dopo la pulizia dell'acqua. La proprietà needsProcessing può essere impostata da TapWater 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
   }
}
  1. Crea altre due sottoclassi di WaterSupply, denominate FishStoreWater e LakeWater. FishStoreWater non richiede l'elaborazione, ma il filtro LakeWater deve essere filtrato con il metodo filter(). Non è necessario elaborare nuovamente la pagina dopo l'applicazione di filtri, quindi scegli needsProcessing = false in filter().
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.

  1. In Aquarium.kt, definisci una classe Aquarium, con <T> tra parentesi dopo il nome della classe.
  2. Aggiungi una proprietà immutabile waterSupply di tipo T a Aquarium.
class Aquarium<T>(val waterSupply: T)
  1. Scrivi una funzione chiamata genericsExample(). Non fa parte di un corso, pertanto può essere posizionato al livello superiore del file, ad esempio la funzione main() o le definizioni del corso. Nella funzione, inserisci un Aquarium e trasmetti un WaterSupply. Poiché il parametro waterSupply è generico, devi specificare il tipo tra parentesi angolari <>.
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
}
  1. In genericsExample() il tuo codice può accedere all'acquario di waterSupply. Poiché è di tipo TapWater, puoi chiamare addChemicalCleaners() senza trasmettere alcun tipo.
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
    aquarium.waterSupply.addChemicalCleaners()
}
  1. 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 ripetere TapWater due volte quando crei l'istanza. Il tipo può essere dedotto dall'argomento Aquarium; renderà comunque Aquarium di tipo TapWater.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    aquarium.waterSupply.addChemicalCleaners()
}
  1. Per sapere che cosa sta succedendo, stampa needsProcessing prima e dopo aver chiamato il numero addChemicalCleaners(). 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}")
}
  1. Aggiungi una funzione main() per chiamare genericsExample(), 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.

  1. In genericsExample(), crea un elemento Aquarium, passando una stringa per l'attributo waterSupply, quindi stampa la proprietà waterSupply dell'acquario.
fun genericsExample() {
    val aquarium2 = Aquarium("string")
    println(aquarium2.waterSupply)
}
  1. 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.

  1. In genericsExample(), crea un altro Aquarium, superando null per waterSupply. Se waterSupply è null, stampa "waterSupply is null".
fun genericsExample() {
    val aquarium3 = Aquarium(null)
    if (aquarium3.waterSupply == null) {
        println("waterSupply is null")
    }
}
  1. 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)
  1. Per non consentire il passaggio di null, rendi T di tipo Any esplicitamente, rimuovendo ? dopo Any.
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.

  1. Assicurati soltanto che gli elementi WaterSupply (o una delle sue sottoclassi) possano essere trasmessi per T. Sostituisci Any con WaterSupply 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.

  1. Aggiungi un metodo addWater() alla classe Aquarium per aggiungere l'acqua, con un check() 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.

  1. In genericsExample(), aggiungi del codice per creare un Aquarium con LakeWater, poi aggiungi un po' di acqua.
fun genericsExample() {
    val aquarium4 = Aquarium(LakeWater())
    aquarium4.addWater()
}
  1. 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)
  1. 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

  1. Nel corso Aquarium, imposta T: WaterSupply come tipo out.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
    ...
}
  1. Nello stesso file, all'esterno della classe, dichiara una funzione addItemTo() che prevede un Aquarium di WaterSupply.
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")
  1. Chiama addItemTo() da genericsExample() 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.

  1. Se rimuovi la parola chiave out, il compilatore restituirà un errore durante la chiamata a addItemTo(), 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.

  1. In Aquarium.kt, definisci un'interfaccia Cleaner che utilizzi un T generico limitato a WaterSupply. Poiché viene utilizzato solo come argomento per clean(), puoi impostarlo come parametro in.
interface Cleaner<in T: WaterSupply> {
    fun clean(waterSupply: T)
}
  1. Per utilizzare l'interfaccia di Cleaner, crea una classe TapWaterCleaner che implementi Cleaner per la pulizia di TapWater aggiungendo prodotti chimici.
class TapWaterCleaner : Cleaner<TapWater> {
    override fun clean(waterSupply: TapWater) =   waterSupply.addChemicalCleaners()
}
  1. Nel corso Aquarium, aggiorna addWater() in modo che sia presente un Cleaner di tipo T 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")
    }
}
  1. Aggiorna il codice di esempio genericsExample() per creare un TapWaterCleaner, un Aquarium con TapWater 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

  1. In generics/Aquarium.kt, crea una funzione isWaterClean() che richieda un Aquarium. Devi specificare il tipo generico del parametro; una delle opzioni consiste nell'utilizzare WaterSupply.
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.

  1. Per rendere la funzione generica, inserisci le parentesi angolari dopo la parola chiave fun con un tipo generico T ed eventuali vincoli, in questo caso WaterSupply. Modifica Aquarium in modo che sia vincolato da T anziché da WaterSupply.
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.

  1. 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)
}
  1. 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.

  1. Nella classe Aquarium, dichiara un metodo hasWaterSupplyOfType() che accetta un parametro generico R (T è già utilizzato) vincolato a WaterSupply e restituisce true se waterSupply è di tipo R. È la funzione che hai dichiarato in precedenza, ma all'interno della classe Aquarium.
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
  1. Nota che l'ultimo R è sottolineato in rosso. Tieni il puntatore sopra di esso per vedere qual è l'errore.
  2. Per eseguire un controllo is, devi dire a Kotlin che il tipo è reificato o reale e può essere utilizzato nella funzione. A questo scopo, inserisci inline davanti alla parola chiave fun e reified davanti al tipo generico R.
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.

  1. 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.

  1. Al di fuori della classe Aquarium, definisci una funzione estensione su WaterSupply chiamata isOfType() che verifichi se il valore WaterSupply superato è di un tipo specifico, ad esempio TapWater.
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T
  1. 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.

  1. Per utilizzare una proiezione di stelle, inserisci <*> dopo Aquarium. Sposta hasWaterSupplyOfType() come funzione di estensione, perché non fa parte dell'API principale di Aquarium.
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R
  1. 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 e out 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.

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: 6. Manipolazione funzionale

Per una panoramica del corso, inclusi i link ad altri codelab, vedi "Kotlin Bootcamp for Programs: Welcome to the Course."