Perché i generics
Ti imbatti presto in tipi che dovrebbero funzionare con più di un tipo di elemento. A una "coppia" di due valori non importa se i valori sono interi, stringhe o una shape definita dall'utente. I generics ti permettono di scrivere il tipo una volta e di istanziarlo con i tipi di elemento di cui ha bisogno chi lo usa.
L'alternativa, cioè scrivere IntPair, StringPair, BytePair e così via, diventa noiosa in fretta e non si compone. In Zero i generics sono lo strumento standard per questo lavoro.
Funzioni generiche
Dichiara i parametri di tipo tra parentesi angolari, tra il nome della funzione e l'elenco dei parametri:
fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
return Pair { left: left, right: right }
}
T e U sono tipi segnaposto: è chi chiama a decidere cosa sono.
Nelle chiamate di solito non serve scriverli; il compilatore li deduce dai tipi degli argomenti:
let pair = makePair(40, 2_u8)
Qui T viene dedotto come i32 (il tipo predefinito per un letterale intero senza suffisso) e U come u8 (dal suffisso _u8). Il binding risultante pair ha tipo Pair<i32, u8>.
Se l'inferenza scegliesse i tipi sbagliati, per esempio perché i letterali sono ambigui, puoi fissare i parametri nel punto di chiamata:
let pair = makePair<u8, u8>(1, 2)
(La sintassi esatta della chiamata con parentesi angolari potrebbe variare tra le versioni di Zero; controlla la documentazione attuale per la forma precisa. Il comportamento basato prima di tutto sull'inferenza è la parte stabile.)
Shape generiche
Le shape ricevono parametri di tipo allo stesso modo:
shape Pair<T, U> {
left: T,
right: U,
}
Il tipo di ogni campo può menzionare i parametri. Le istanze li fissano:
let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words: Pair<String, String> = Pair { left: "hi", right: "there" }
Un esempio completo che combina una shape generica e una funzione generica: clicca su Esegui per vedere l'inferenza all'opera:
Una dichiarazione di shape generica, una funzione generica, un punto di chiamata fortemente tipizzato. Niente codice ripetitivo come IntBytePair.
Alias di tipo
Quando lo stesso tipo parametrizzato ricorre spesso, dagli un nome con type:
type BytePair = Pair<u8, u8>
Ora BytePair è intercambiabile con Pair<u8, u8> ovunque tu possa scrivere un tipo:
Un alias è solo una questione di nomi: non crea un tipo distinto. Una funzione che riceve un BytePair accetterà tranquillamente un valore di tipo Pair<u8, u8> (e viceversa).
Generics nella libreria standard
Lo stesso meccanismo alimenta gran parte della libreria standard. Eccone alcuni che vedrai nel codice Zero reale:
Maybe<T>: un valore opzionale, che contiene unToppure niente.Span<T>: una vista presa in prestito su valoriT.Span<u8>è la vista classica su un buffer di byte.ref<T>emutref<T>: tipi riferimento espliciti per i casi in cui devi condividere dati senza copiarli.
Non devi impararli tutti in una volta. Il senso dei generics è che la stessa struttura funziona con qualsiasi tipo di elemento tu abbia a disposizione.
Quando i generics convengono (e quando no)
Usa i generics quando ti accorgi di scrivere due volte la stessa funzione o shape con tipi di elemento diversi. Usa un tipo concreto quando:
- La logica della funzione ha senso solo per un tipo specifico (un parser per
String, per esempio). - Vuoi che il tipo compaia nei messaggi di errore, così il debug è più semplice.
- Le caratteristiche prestazionali dipendono da una dimensione specifica in memoria.
I generics hanno un costo reale: binari più grandi (ogni istanziazione genera nuovo codice) e tempi di compilazione leggermente più lunghi. Per la maggior parte del codice applicativo quel costo è trascurabile, ma è bene saperlo quando scrivi codice compatto in stile embedded, dove le dimensioni del binario contano.
Una nota sui vincoli
Alcuni sistemi di generics permettono di vincolare un parametro di tipo ("T deve supportare ==", "T deve implementare Iterator"). In Zero pre-1.0 la gestione dei vincoli è ancora in evoluzione: gli esempi del repository ufficiale usano i generics nella loro forma semplice, senza limiti elaborati. Man mano che il linguaggio si assesta, aspettati che la sintassi dei vincoli arrivi in una forma piccola e regolare, coerente con il resto del linguaggio. Per ora scrivi generics che funzionino con ogni T che gli passi davvero e lascia che sia il compilatore a dirti quando un'operazione non è supportata.
Prossimo passo: enum
I generics ti permettono di parametrizzare sui tipi. Il prossimo mattone sta all'estremo opposto: gli enum, il tipo enumerazione semplice di Zero per i casi in cui le varianti non portano dati aggiuntivi.
Domande frequenti
Come funzionano i generics in Zero?
Dichiara i parametri di tipo tra parentesi angolari dopo il nome della funzione o della shape: fun makePair<T, U>(left: T, right: U) -> Pair<T, U> o shape Pair<T, U> { left: T, right: U }. Chi chiama può fissare i parametri esplicitamente (Pair<i32, u8>) o lasciare che il compilatore li deduca dagli argomenti della chiamata.
Le shape possono essere generiche in Zero?
Sì. Una shape può ricevere parametri di tipo con la stessa sintassi a parentesi angolari usata per le funzioni: shape Pair<T, U> { left: T, right: U }. Ogni campo può usare i parametri nel proprio tipo. Le istanze si ottengono scrivendo il tipo parametrizzato, per esempio Pair<i32, u8>.
Bisogna specificare i parametri di tipo quando si chiama una funzione generica?
Di solito no. Il compilatore li deduce dai tipi degli argomenti. Chiamare makePair(40, 2_u8) basta: T diventa i32 e U diventa u8. Puoi fissare i parametri esplicitamente quando l'inferenza sceglierebbe il tipo sbagliato o quando vuoi che il punto di chiamata li documenti.
Cos'è un alias di tipo in Zero?
Un alias di tipo è un nome abbreviato per un'espressione di tipo più lunga. type BytePair = Pair<u8, u8> ti permette di scrivere BytePair ovunque altrimenti scriveresti Pair<u8, u8>. L'alias è una pura questione di nomi: non introduce un nuovo tipo, solo un modo più breve per riferirsi a uno esistente.
Dove compaiono i generics nella libreria standard di Zero?
Ovunque un tipo debba contenere o manipolare un tipo di elemento arbitrario. Maybe<T> per i valori opzionali, Span<u8> per le sequenze di byte, tipi contenitore parametrizzati sul loro elemento. Lo stesso meccanismo generico gestisce sia i tipi definiti dall'utente sia quelli della libreria standard.