I generici

Talvolta, quando si scrive una funzione o un tipo di dati, potremmo volere che funzioni per più tipi di argomenti. In Rust, lo possiamo fare usando i generici. Nella teoria dei tipi, i generici sono chiamati ‘polimorfismo parametrico’, che significa che sono tipi o funzioni che hanno più forme (in greco ‘poli’ significa ‘plurimo‘, e ‘morfo’ significa ‘forma‘) in base a un dato parametro (da cui ‘parametrico’).

Comunque, basta così con la teoria dei tipi, guardiamo del codice generico. La libreria standard di Rust fornisce un tipo, Option<T>, che è generico:

fn main() { enum Option<T> { Some(T), None, } }
enum Option<T> {
    Some(T),
    None,
}

La parte <T>, che abbiamo già visto alcune volte, indica che questo è un tipo di dati generico. Ogni volta che nel nostro codice usiamo questo enum, specifichiamo un tipo che sostituisce il parametro T ogni volta che compare nella dichiarazione generica. Ecco un esempio di uso di Option<T>, con un'annotazione di tipo aggiuntiva:

fn main() { let x: Option<i32> = Some(5); }
let x: Option<i32> = Some(5);

Nella dichiarazione di tipo, diciamo Option<i32>. Si noti quanto simile appaia a Option<T>. Quindi, in questa particolare Option, T ha il valore di i32. Sul lato destro del legame, costruiamo un Some(T), dove l'oggetto di tipo T è 5. Dato che si tratta di un i32, i due lati combaciano, e Rust è contento. Se non combaciassero, otterremmo un errore:

fn main() { let x: Option<f64> = Some(5); // error: mismatched types: expected `core::option::Option<f64>`, // found `core::option::Option<_>` (expected f64 but found integral variable) }
let x: Option<f64> = Some(5);
// error: mismatched types: expected `core::option::Option<f64>`,
// found `core::option::Option<_>` (expected f64 but found integral variable)

Ciò non significa che non si possono costruire degli Option<T> che tengono f64! Solamente i due lati dell'assegnamento devono avere lo stesso tipo:

fn main() { let x: Option<i32> = Some(5); let y: Option<f64> = Some(5.0f64); }
let x: Option<i32> = Some(5);
let y: Option<f64> = Some(5.0f64);

Così va bene. Una sola definizione, utilizzi multipli.

I generici non sono limitati ad essere parametrizzati da un solo tipo. Si consideri un altro tipo simile, fornito dalla liberia standard di Rust, Result<T, E>:

fn main() { enum Result<T, E> { Ok(T), Err(E), } }
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Questo tipo è generico relativamente a due tipi: T ed E. Tra l'altro, le lettere maiuscole possono essere qualunque lettera si gradisca. Si sarebbe potuto definire Result<T, E> come:

fn main() { enum Result<A, Z> { Ok(A), Err(Z), } }
enum Result<A, Z> {
    Ok(A),
    Err(Z),
}

se si avesse voluto. Per convenzione il primo parametro generico dovrebbe essere T, per ‘tipo’, e si dovrebbe usare E per ‘errore’. Però a Rust non importa.

Il tipo Result<T, E> è pensato per essere usato come risultato di un'elaborazione, dando la possibilità di rendere un errore nel caso non si riuscisse a completare correttamente l'elaborazione.

Funzioni generiche

Si possono scrivere funzioni che prendono tipi generici, usando una sintassi simile:

fn main() { fn prende_qualunque_cosa<T>(x: T) { // fai qualcosa con x } }
fn prende_qualunque_cosa<T>(x: T) {
    // fai qualcosa con x
}

La sintassi ha due parti: la <T> dice “questa funzione è generica rispetto a un tipo, T”, e la x: T dice “x è di tipo T.”

Più argomenti possono essere dello stesso tipo generico:

fn main() { fn prende_due_oggetti_del_medesimo_tipo<T>(x: T, y: T) { // ... } }
fn prende_due_oggetti_del_medesimo_tipo<T>(x: T, y: T) {
    // ...
}

Si può anche scrivere una versione che prende più tipi:

fn main() { fn prende_due_oggetti<T, U>(x: T, y: U) { // ... } }
fn prende_due_oggetti<T, U>(x: T, y: U) {
    // ...
}

Struct generiche

Si può usare un tipo generico anche per i campi di una struct:

fn main() { struct Punto<T> { x: T, y: T, } let origine_intera = Punto { x: 0, y: 0 }; let origine_a_virgola_mobile = Punto { x: 0.0, y: 0.0 }; }
struct Punto<T> {
    x: T,
    y: T,
}

let origine_intera = Punto { x: 0, y: 0 };
let origine_a_virgola_mobile = Punto { x: 0.0, y: 0.0 };

Analogamente alle funzioni, la <T> è dove si dichiarano i parametri generici, che poi vengono usati nelle dichiarazioni dei campi x: T e y: T.

Quando si vuole aggiungere un'implementazione per una struct generica, si dichiara il parametro di tipo subito dopo la impl:

fn main() { struct Punto<T> { x: T, y: T, } impl<T> Punto<T> { fn swap(&mut self) { std::mem::swap(&mut self.x, &mut self.y); } } }
impl<T> Punto<T> {
    fn swap(&mut self) {
        std::mem::swap(&mut self.x, &mut self.y);
    }
}

Finora abbiamo visto dei generici che prendono assolutamente qualunque tipo. Questi servono in molti casi: abbiamo già visto Option<T>, e poi incontreremo i tipi contenitore universali, come Vec<T>. D'altra parte, spesso si vuole rinunciare a quella flessibilità per avere un maggior poter espressivo. Si legga la sezione sui legami di tratto per vedere come e perché.