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.
Questi linguaggi usano null o nil per indicare che una determinata variabile non ha alcun valore.
Tuttavia, questo rende l'uso accidentale di una variabile null un errore facile (e frequente) da commettere.
Come puoi immaginare, provare a chiamare una funzione che non c'è, o ad accedere a un valore che non esiste, può portare a ogni sorta di bug e crash.
Il creatore di null è arrivato persino a definirlo il suo «errore da un miliardo di dollari».
Option
Per evitare questi problemi, le variabili di Cairo possono usare l'enum Option per rappresentare in modo sicuro l'assenza di un valore.
Questo enum ha due varianti: None, l'equivalente di null in Cairo; e Some(T), dove T è un valore di qualsiasi tipo.
Si presenta così:
enum Option<T> {
None,
Some(T),
}
Puoi pensare a Option come a uno strato di sicurezza tra te e i problemi che i riferimenti null possono causare, pur conservando la loro utilità concettuale.
Option
Impostare una variabile a None è piuttosto semplice:
let nothing: Option<u32> = Option::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.
Devi usare Some:
let wrong_way: Option<i32> = -4; // This will not work
let right_way: Option<i32> = Option::Some(-4); // This will work
let another_right_way = Option::Some(-4); // Compiler infers that this is Option<i32>
Per lo stesso motivo, il codice seguente non funzionerà:
let number = 47;
let option_number = Option::Some(15);
let compile_error = number + option_number; // Cannot add an i32 and an Option<i32> - they are of different types
Per ottenere il valore contenuto in Some, devi prima verificare che esista:
let mut some_words = Option::Some("choose something to say"); // some_words set to something
match some_words {
Option::Some(str) => println!("Here, we will {}", str),
Option::None => println!("I've got nothing to say"),
} // Prints, "Here, we will choose something to say"
some_words = Option::None; // some_words now set to None
// Exactly the same match block as above
match some_words {
Option::Some(str) => println!("Here, we will {}", str),
Option::None => println!("I've got nothing to say"),
} // Prints, "I've got nothing to say"
Cairo ha altri strumenti oltre a match per verificare e accedere ai valori contenuti in Option, ma ormai match dovrebbe esserti familiare.
Considera questo come una dimostrazione del perché Cairo usa Option invece di un riferimento null.
Il punto è che devi assolutamente verificare se la variabile Option è Some (nel qual caso puoi procedere a estrarre e usare il valore che contiene), oppure None.
Altrimenti il 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,
}
I giocatori di questo gioco devono raggiungere il livello 10 prima di poter sbloccare una riserva di mana, che possono usare per lanciare incantesimi.
Prima di quel momento, il mana del Player è Option::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, se lo è, deve restituire una nuova istanza di Player con 100 punti salute.
Il metodo revive deve preservare il livello del Player.
Se il livello del Player è pari o superiore a 10, deve essere rianimato con 100 di mana.
Se il livello del Player è inferiore a 10, il suo mana deve essere Option::None.
let dead_player = Player { health: 0, mana: Option::None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: Option::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 Option::None.
let alive_player = Player { health: 1, mana: Option::Some(15), level: 11 };
alive_player.revive()
// Returns Option::None
Il metodo cast_spell prende un riferimento mutabile al Player e un parametro mana_cost che indica quanto mana costa l'incantesimo.
Restituisce la quantità di danni inflitti dall'incantesimo lanciato.
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. I danni restituiti devono essere 0.
let not_a_wizard_yet = Player { health: 79, mana: Option::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, Option::None);
Se il giocatore ha una riserva di mana ma mana insufficiente, il metodo non deve influire sulla riserva. I danni restituiti devono essere 0.
let low_mana_wizard = Player { health: 93, mana: Option::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, Option::Some(3));
Altrimenti, l'incantesimo viene lanciato con successo.
Il mana_cost deve essere detratto dalla riserva di mana del Player.
I danni restituiti devono essere il doppio del costo in mana dell'incantesimo.
let wizard = Player { health: 123, mana: Option::Some(30), level: 18 };
assert_eq!(wizard.cast_spell(10), 20);
assert_eq!(wizard.health, 123);
assert_eq!(wizard.mana, Option::Some(20));
Buon divertimento!
Iscriviti a Exercism per imparare e padroneggiare Cairo con 25 concetti68 esercizi e il mentoring di persone reali, tutto gratis.