|
Anche per quanto riguarda i tipi possiamo parlare di "generici". La finalità è sempre quella, avere codice più facilmente riutilizzabile perchè generico, appunto Il problema è sempre il solito: immaginiamo ad esempio di avere a che fare con una struct che individui un punto nello spazio ma che debba poter accettare sia numeri interi che con virgola. A questo punto avremmo bisogno di definire due struct diverse. Ma i generici vengono in nostro aiuto, consentendoci di limitare la proliferazione del codice. Vediamo quindi questo classico esempio:
La sintassi ricorda molto da vicino quella vista per le funzioni, con quel "T" che individua appunto un tipo generico (ma come detto anche in quel paragrafo, potete scegliere la lettera che volete voi). Fateci caso questo non è molto diverso dalla definizione ad esempio dei vettori che vengono indicati come vec<T> proprio perchè possono contenere elementi di qualsiasi tipo. Però in questo caso vediamo come in effetti anche sui tipi creati dall'utente possiamo usufruire dello stesso tipo di possibilità. Questo vale anche con gli enumerativi che si prestano bene a lavorare in congiunzione con il concetto di "tipo generico". Un ottimo esempio lo troviamo nelle librerie stesse del linguaggio ovvero con l'enumerativo Result con il quale in genere, come sappiamo, si gestiscono situazioni di errore. La definizione di Result internamente è infatti: enum Result Ok(T), Err(E), } usando due parametri generici.
L'aspetto generico è evidente se modificate il tipo di parametri che passate a resutl. Ad esempio potreste modificare (ammesso che abbia un senso) la prima riga così: let successo: Result<&str, String> = Result::Ok("pippo"); evidenziando la capacità di gestire più esiti. Un altro esempio, utile nella pratica, è il seguente, anch'esso un classico:
Provate poi a sostituire 2 con 0 alla riga 10. Questi ultimi due esempi tra l'altro mostrano il caso in cui vi sia la necessità di avere due tipi generici ma che, contestualmente, possono essere diversi tra di loro. I tipi generici possono essere "costretti" a sottostare a determinate condizioni, determinati requisiti, nella sezione dedicata ale funzioni genriche abbiamo già visto di che si tratta e lo ripetiamo con qualche aggiunta in questo paragrafo. Abbiamo due modi per indicare al compilaotre i requisiti che vogliamo siano soddisfatti. primo modo: lo abbiamo già visto: fn somma<T: std::ops::Add<Output = T>>(a: T, b: T) -> T { a + b } che significa che T deve soddisfare il Trait Add. nel caso di trait multipli come noto: fn stampa_somma<T: std::fmt::Display + std::ops::Add<Output = T>>(a: T, b: T) { println!("{}", a + b); } secondo modo: facciamo uso della keyword where di cui abbiamo già fatto cenno nel paragrafo dedicato alla implementazioni con impl e che, ricordiamolo, è usata specificatamente per dichiarare dei vincoli sui tipi generici. Il secondo esempio si potrà quindi riscrivere come segue con una leggibilità migliore: fn stampa_somma<T>(a: T, b: T) where T: std::fmt::Display + std::ops::Add<Output = T>, { println!("{}", a + b); } |