Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 32
Il tipo Option

Importante per il linguaggio è il tipo Option, protagonista di questo paragrafo e di cui abbiamo già parlato. Si tratta sostanzialmente di un enumeratore che presenta al suo interno due varianti:

- Some, che contiene un valore del tipo T specificato
- None che è il caso vuoto, l'assenza di valore.

internamente infatti Option è definito come segue:

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

perchè è importante? un primo motivo è semplice, come abbiamo anche visto in precedenza, si tratta di un ottimo sistema per gestire situazioni che potrebbero mandare in panico, quindi in crash, il programma. Esso in un certo senso costringe il programmatore a prendere in considerazione quelle situazioni in cui potrebbe presentarsi un'assenza di valore, riducendo in modo significativo i rischi determinati da questa possibile situazione.
La logica di funzionamento lo vediamo in un classico esempio che merita una piccola introduzione. Parlando delle operazioni algebriche abbiamo sottolineato il problema degli overflow, al quale abbiamo dedicato un paragrafo. E ricorderete che abbiamo parlato delle operazioni checked, le quali presentavano come output proprio un tipo Option.

tipointero::checked_div(dividendo,divisore).

internamente  la funzione checked_div è definito come segue, nel caso degli i32:

pub const fn checked_div(self, rhs: i32) -> Option<i32>

la firma di questa funzione merita una descrizione, al fine che tutto sia chiaro:

- pub è un modificatore che indica che la funzione è pubblicamente accessibile. In Rust i modificatori non sono così usati come in altri linguaggi ma ne parleremo.
- const indica che la funzione può essere valutata a compile time (ricordate le costanti?)
- fn come noto è la keyword che introduce le funzioni
- checked_div è il nome della funzione
- self e rhs sono i parametri, ovvero gli argomenti, i valori che passiamo alla funzione e su cui essa lavorerà, come vedremo nel paragrafo delle funzioni. In particolare self: Questo parametro indica che checked_div è un metodo definito su un'istanza di un tipo. Il tipo self suggerisce che checked_div è chiamato su un valore di un tipo numerico, come i32. L'uso di self senza una referenza (&self) o una mutabilità (&mut self) indica che il metodo consuma il valore su cui è chiamato, ma per i tipi primitivi come i32, che sono Copy, questo non è rilevante come per i tipi che non implementano Copy. Invece rhs: i32: questo è il parametro per il metodo. "rhs" sta per "right-hand side" (lato destro dell'operatore), il che è comune per operazioni binarie come l'addizione, la sottrazione o, in questo caso, la divisione. Il tipo i32 indica che il secondo operando è anch'esso un intero a 32 bit.
- Option<I32> è invece l'elemento che ci interessa maggiormnte in questo capitolo. In pratica la funzione Checked_div ci restitutisce un tipo Option in cui il dato non nullo sarà un i32.

possiamo vedere finalmente l'esempio:


  Esempio 32.1
1
2
3
4
5
6
7
8
9
fn main() {
    let x = 7;
    let y = 0;
    let z = i32::checked_div(x, y);
    match z {
        Some(z) => println!("{}", z),
        None => println!("Errore!"),
    }
}

Questo programma propone una divisione che, di norma, non si potrebbe fare, ovvero 7/0. Usando la divisione normale il programma andrebbe in crash, o meglio in gergo rusticeano, in panico. Tale situazione andrebbe gestita (con metodi che vedremo altrove) ma usando checked_div il risultato dell'esecuzione di questo programma sarà:

Errore!

come proposto alla riga 7. Niente panico e il programma potrebbe continuare se vi fossero altre istruzioni.
Il tipo option è estremamente utile, come è facile capire, in tanti casi simili, quindi per la gestione di situazioni anomale.
Un problema che a volte si pone è quello estrarre materialmente il valore il valore che un Option si porta dietro all'interno di Some. Infatti se scrivessimo:

let x = 7;    
let y = 3;    
let z = i32::checked_div(x, y);    
println!("{:?}", z);

otterremo:

Some(2)

che non esattamente quello che potrebbe servirci nel senso che non è manipolabile come potrebbe esserlo un intero. Ci sono alcuni metodi abbastanza semplici per estrarre il valore:

1) usando unwrap, rischioso perchè il programma va in panico senza dirci  nulla se il risultato è none.

let x = 7;
let y = 3;
let z = i32::checked_div(x, y).unwrap();

se volete gestire il caso None senza panic potete usare unwrap_or(...) ad esempio modificherete l'ultima riga:

let z = i32::checked_div(x,y).unwrap_or(numerochevolete)


2) utilizzando ovviamente match, gestendo il caso None:

let x = 7;
let y = 3;
let risultato = match z {        
    Some(numero) => numero,  // Se z è Some, estrai il numero        
    None => valorechevolete, // Se no valore da voi deciso  
    };    
println!("{}", risultato);   // Stampa il valore estratto

3) con if-let per il quale presento un esempio completo:

  Esempio 32.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use std::io;
fn main() {
    println!("Inserisci il dividendo:");
    let mut input_dividendo = String::new();
    io::stdin().read_line(&mut input_dividendo)
    .expect("Errore dividendo");
    let dividendo: i32 = input_dividendo.trim().parse()
    .expect("Inserisci un numero valido");
    println!("Inserisci il divisore:");
    let mut input_divisore = String::new();
    io::stdin().read_line(&mut input_divisore)
    .expect("Errore divisore");
    let divisore: i32 = input_divisore.trim().parse()
    .expect("Inserisci un numero valido ");
    // Inizializza x a zero o un altro valore di default
    let mut x = 0;
    if let Some(result) = dividendo.checked_div(divisore) {
    x = result; // Assegna il risultato a x
    println!("Il risultato della divisione è: {}", x);
    } else {
       println!("Errore: tentativo di divisione per zero");
    }
    // Qui possiamo usare x indipendentemente
    // dal risultato dell'if let
    println!("Il valore finale di x è: {}", x);
}


Definire una variabile di tipo Option senza ricorrere a funzioni che lo facciano per noi, è molto semplice:

let x = Some(5);

Possiamo quindi usare questa variabile attraverso i metodi visti in precedenza 

  Esempio 32.3
1
2
3
4
5
6
7
8
9
fn main() {
    let x = Some(5);
    println!("{}", std::any::type_name_of_val(&x));
    let risultato = match x {
        Some(numero) => numero,  // Se z è Some, estrae il numero        
        None => 0, // Se no valore da voi deciso  
        };
        println!("{}", risultato);
}

Prima di chiudere vediamo un altro esempio che non prende in esame i numeri ma le stringhe (è presente anche il concetto di lifetime che incontreremo nell'apposito paragrafo:

  Esempio 32.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn trova_nome<'a>(nomi: &'a [&'a str], target: &'a str)
-> Option<&'a str> {
    for &nome in nomi.iter() {
        if nome == target {
            return Some(nome);
        }
    }
    None
}
fn main() {
    let lista_nomi = ["Alice", "Beatrice", "Carlo", "Davide"];
    let nome_da_cercare = "Carlo";
    match trova_nome(&lista_nomi, nome_da_cercare) {
        Some(nome) => println!("Nome trovato: {}", nome),
        None => println!("Nome non trovato nella lista"),
    }
    // Proviamo a cercare un nome che non esiste nella lista
    let nome_non_presente = "Giulio";
    match trova_nome(&lista_nomi, nome_non_presente) {
        Some(nome) => println!("Nome trovato: {}", nome),
        None => println!("Nome non trovato nella lista"),
    }
}

In linea generale possiamo dire, per riassumere, che i vantaggi di usare il tipo Option  includono, in ordine sparso e senza pretesa di completezza:

Gestione degli errori: Option aiuta a gestire la presenza o l’assenza di un valore in modo sicuro. Se un valore può essere None (equivalente a null in altri linguaggi), Rust ti costringe a gestire esplicitamente questa possibilità, riducendo gli errori a runtime.
Sicurezza del tipo: Utilizzando Option, Rust garantisce che non si verificheranno errori dovuti a valori non inizializzati o nulli, poiché il compilatore richiede che tutti i casi possibili siano gestiti.
Espressioni condizionali: Option può essere usato con match o if let per eseguire codice solo se c’è un valore (Some) e gestire diversamente il caso None.
Interoperabilità con altre funzioni: Molti metodi nell’ecosistema Rust restituiscono Option, quindi l’uso di Some si integra bene con queste funzioni, permettendo composizioni di codice più pulite e sicure.
In sostanza, siamo davanti ad un costrutto che aiuta a scrivere codice più robusto e sicuro, evitando alcuni dei problemi comuni nei linguaggi che utilizzano puntatori nulli. Lo vedremo e lo abbiamo visto, in azione in molti casi durante il nostro percorso. Nel prossimo paragrafo invece vedremo un fratello quasi gemello di Option.