Se hai mai usato un altro linguaggio di programmazione (C/C++, Python, Java, Ruby, Lisp, ecc.), è probabile che tu abbia già incontrato null o nil.
L'uso di null o nil è il modo in cui questi linguaggi indicano che una particolare variabile non ha alcun valore.
Tuttavia, questo rende l'uso accidentale di una variabile che punta a null un errore facile (e frequente) da commettere.
Come puoi immaginare, provare a chiamare una funzione che non c'è, o accedere a un valore che non esiste, può portare a ogni sorta di bug e crash.
Il creatore di null è arrivato a definirlo il suo «errore da un miliardo di dollari».
Option
Per evitare questi problemi, Rust non usa i riferimenti nulli.
Tuttavia, ha comunque bisogno di un modo sicuro per indicare che una particolare variabile non ha alcun valore.
È qui che entra in gioco Option.
Invece di avere una variabile a cui manca un valore, le variabili Rust possono usare l'enum Option.
Questo enum ha due varianti: None, l'equivalente di null in Rust; e Some(T), dove T è un valore di qualsiasi tipo.
Assomiglia a questo:
enum Option<T> {
None,
Some(T),
}
Puoi pensare a Option come a uno strato di sicurezza tra te e i problemi che i riferimenti nulli possono causare, mantenendo comunque la loro utilità concettuale.
Option
Impostare una variabile a None è piuttosto semplice:
let nothing: Option<u32> = None; // Variable nothing is set to None
Tuttavia, se vuoi che il tipo Option contenga un valore, non puoi assegnare questo valore direttamente.
Una variabile di tipo Option e, per esempio, una variabile di tipo i32 non sono equivalenti.
Dovrai usare Some:
let wrong_way: Option<i32> = -4; // This will not work
let right_way: Option<i32> = Some(-4); // This will work
let another_right_way = Some(-4); // Compiler infers that this is Option<i32>
È anche per questo motivo che quanto segue non funziona:
let number = 47;
let option_number = Some(15);
let compile_error = number + option_number; // Cannot add an i32 and an Option<i32> - they are of different types
Se vuoi ottenere il valore contenuto in Some, dovrai prima verificare che esista:
let mut some_words = Some("choose something to say"); // some_words set to something
match some_words {
Some(str) => println!("Here, we will {}", str),
None => println!("I've got nothing to say"),
} // Prints "Here, we will choose something to say"
some_words = None; // some_words now set to None
// exactly the same match block as above
match some_words {
Some(str) => println!("Here, we will {}", str),
None => println!("I've got nothing to say"),
} // Prints "I've got nothing to say"
Oltre a match, Rust ha altri strumenti disponibili per controllare e accedere ai valori contenuti in Option, ma ormai match dovrebbe esserti familiare.
Inoltre, considera questo come una dimostrazione del perché Rust usa Option invece di un riferimento nullo.
Il punto è che devi assolutamente controllare se la variabile Option è Some (nel qual caso puoi procedere a estrarre e usare il valore che contiene), oppure None.
In qualsiasi altro caso, il tuo programma non verrà compilato: il compilatore ti tiene al sicuro da null.
Stai lavorando all'implementazione di un gioco di ruolo. Il personaggio del giocatore è rappresentato da quanto segue:
pub struct Player {
health: u32,
mana: Option<u32>,
level: u32,
}
In questo gioco, i giocatori devono raggiungere il livello 10 prima di sbloccare una riserva di mana, così da poter iniziare a lanciare incantesimi. Prima di quel momento, il mana del Player è None.
In questo gioco stai lavorando a due funzionalità: la meccanica di rianimazione e la meccanica di lancio degli incantesimi.
Il metodo revive deve verificare che il Player sia effettivamente morto (la sua salute ha raggiunto 0) e, in tal caso, deve restituire una nuova istanza di Player con 100 di salute.
Se il livello del Player è pari o superiore a 10, deve essere rianimato anche con 100 di mana.
Se il livello del Player è inferiore a 10, il suo mana deve essere None. Il metodo revive deve preservare il livello del Player.
let dead_player = Player { health: 0, mana: None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: None, level: 2 }
Se il metodo revive viene chiamato su un Player la cui salute è pari o superiore a 1, allora il metodo deve restituire None.
let alive_player = Player { health: 1, mana: Some(15), level: 11 };
alive_player.revive()
// Returns None
Il metodo cast_spell accetta un riferimento mutabile al Player e un parametro mana_cost che indica quanto mana costa l'incantesimo. Restituisce la quantità di danno che l'incantesimo lanciato infligge, che sarà sempre pari al doppio del costo in mana dell'incantesimo se il lancio riesce.
Se il giocatore non ha accesso a una riserva di mana, il tentativo di lanciare l'incantesimo deve ridurre la sua salute del costo in mana dell'incantesimo. Il danno restituito deve essere 0.
let not_a_wizard_yet = Player { health: 79, mana: None, level: 9 };
assert_eq!(not_a_wizard_yet.cast_spell(5), 0)
assert_eq!(not_a_wizard_yet.health, 74);
assert_eq!(not_a_wizard_yet.mana, None);
Se il giocatore ha una riserva di mana ma mana insufficiente, il metodo non deve modificare la riserva, ma restituire 0
let low_mana_wizard = Player { health: 93, mana: Some(3), level: 12 };
assert_eq!(low_mana_wizard.cast_spell(10), 0);
assert_eq!(low_mana_wizard.health, 93);
assert_eq!(low_mana_wizard.mana, Some(3));
Altrimenti, il mana_cost deve essere sottratto dalla riserva di mana del Player e deve essere restituita la quantità di danno appropriata.
let wizard = Player { health: 123, mana: Some(30), level: 18 };
assert_eq!(wizard.cast_spell(10), 20);
assert_eq!(wizard.health, 123);
assert_eq!(wizard.mana, Some(20));
Buon divertimento!
Iscriviti a Exercism per imparare e padroneggiare Rust con 99 esercizi e il mentoring di persone reali, tutto gratis.