Si alguna vez has usado otro lenguaje de programación (C/C++, Python, Java, Ruby, Lisp, etc.), es probable que ya te hayas encontrado con null o nil.
El uso de null o nil es la forma en que estos lenguajes indican que una variable en particular no tiene valor.
Sin embargo, esto hace que usar por accidente una variable que apunta a null sea un error fácil (y frecuente) de cometer.
Como puedes imaginar, intentar llamar a una función que no está ahí, o acceder a un valor que no existe, puede provocar todo tipo de bugs y cierres inesperados.
El creador de null llegó incluso a llamarlo su 'error de mil millones de dólares'.
Option
Para evitar estos problemas, Rust no usa referencias nulas.
Sin embargo, aún necesita una forma segura de indicar que una variable en particular no tiene valor.
Aquí es donde entra Option.
En lugar de tener una variable que carece de valor, las variables de Rust pueden usar el enum Option.
Este enum tiene dos variantes: None, el equivalente a null en Rust; y Some(T), donde T es un valor de cualquier tipo.
Se ve así:
enum Option<T> {
None,
Some(T),
}
Puedes pensar en Option como una capa de seguridad entre tú y los problemas que pueden causar las referencias nulas, sin perder por ello su utilidad conceptual.
Option
Asignar None a una variable es bastante sencillo:
let nothing: Option<u32> = None; // Variable nothing is set to None
Sin embargo, si quieres que el tipo Option contenga un valor, no puedes asignar ese valor directamente.
Una variable de tipo Option y, por ejemplo, una variable de tipo i32 no son equivalentes.
Necesitarás usar 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>
Es también por esta razón que lo siguiente no funcionará:
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
Si quieres obtener el valor que contiene Some, primero tendrás que comprobar que existe:
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"
Además de match, Rust tiene otras herramientas disponibles para comprobar y acceder a los valores contenidos en Option, pero a estas alturas match ya debería resultarte familiar.
Además, considera esto una demostración de por qué Rust usa Option en lugar de una referencia nula.
El punto es que tú debes comprobar si la variable Option es Some (y en ese caso puedes extraer y usar el valor que contiene) o None.
Cualquier otra cosa, y tu programa no compilará; el compilador te mantiene a salvo de null.
Estás trabajando en la implementación de un juego de rol. El personaje del jugador se representa con lo siguiente:
pub struct Player {
health: u32,
mana: Option<u32>,
level: u32,
}
En este juego, los jugadores deben alcanzar el nivel 10 antes de desbloquear una reserva de maná, para que puedan empezar a lanzar hechizos. Antes de ese momento, el maná del jugador es None.
Estás trabajando en dos funcionalidades de este juego: la mecánica de revivir y la mecánica de lanzar hechizos.
El método revive debe verificar que el jugador esté realmente muerto (su salud llegó a 0) y, si lo está, el método debe devolver una nueva instancia de Player con 100 de salud.
Si el nivel del jugador es 10 o más, también debe revivir con 100 de maná.
Si el nivel del jugador es menor que 10, su maná debe ser None. El método revive debe conservar el nivel del jugador.
let dead_player = Player { health: 0, mana: None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: None, level: 2 }
Si se llama al método revive sobre un Player cuya salud sea 1 o más, el método debe devolver None.
let alive_player = Player { health: 1, mana: Some(15), level: 11 };
alive_player.revive()
// Returns None
El método cast_spell recibe una referencia mutable al Player y también un parámetro mana_cost que indica cuánto maná cuesta el hechizo. Devuelve la cantidad de daño que hace el hechizo lanzado, que siempre será dos veces el costo de maná del hechizo si el hechizo se lanza con éxito.
Si el jugador no tiene acceso a una reserva de maná, intentar lanzar el hechizo debe reducir su salud en el costo de maná del hechizo. El daño devuelto debe ser 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);
Si el jugador tiene una reserva de maná pero maná insuficiente, el método no debe afectar la reserva, sino devolver 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));
En caso contrario, el mana_cost debe restarse de la reserva de maná del jugador y debe devolverse la cantidad de daño correspondiente.
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));
¡Diviértete!
Regístrate en Exercism para aprender y dominar Rust con 99 ejercicios y mentoría humana real, todo gratis.