Comparando la suposición con el número secreto
Ahora que tenemos la entrada del usuario y un número aleatorio, podemos compararlos. Ese paso se muestra en la Lista 2-4. Tenga en cuenta que este código todavía no se compilará, como se explicará.
Nombre del archivo: src/main.rs
use rand::Rng;
1 use std::cmp::Ordering;
use std::io;
fn main() {
--snip--
println!("Has adivinado: {guess}");
2 match guess.3 cmp(&secret_number) {
Ordering::Less => println!("Demasiado pequeño!"),
Ordering::Greater => println!("Demasiado grande!"),
Ordering::Equal => println!("¡Ganaste!"),
}
}
Lista 2-4: Manejar los posibles valores de retorno de la comparación de dos números
Primero agregamos otra declaración use [1], trayendo un tipo llamado std::cmp::Ordering al ámbito desde la biblioteca estándar. El tipo Ordering es otro enum y tiene las variantes Less, Greater y Equal. Estos son los tres resultados posibles cuando se comparan dos valores.
Luego agregamos cinco líneas nuevas al final que usan el tipo Ordering. El método cmp [3] compara dos valores y se puede llamar en cualquier cosa que se pueda comparar. Toma una referencia a lo que quieres comparar: aquí está comparando guess con secret_number. Luego devuelve una variante del enum Ordering que trajimos al ámbito con la declaración use. Usamos una expresión match [2] para decidir qué hacer a continuación basado en qué variante de Ordering se devolvió desde la llamada a cmp con los valores en guess y secret_number.
Una expresión match está compuesta por ramas. Una rama consta de un patrón contra el que se debe coincidir, y el código que debe ejecutarse si el valor dado a match coincide con el patrón de esa rama. Rust toma el valor dado a match y lo revisa en cada patrón de rama por turnos. Los patrones y la construcción match son características poderosas de Rust: te permiten expresar una variedad de situaciones que tu código podría encontrar y te aseguran que las manejes todas. Estas características se cubrirán en detalle en el Capítulo 6 y el Capítulo 18, respectivamente.
Veamos un ejemplo con la expresión match que usamos aquí. Digamos que el usuario ha adivinado 50 y el número secreto generado aleatoriamente esta vez es 38.
Cuando el código compara 50 con 38, el método cmp devolverá Ordering::Greater porque 50 es mayor que 38. La expresión match obtiene el valor Ordering::Greater y comienza a revisar cada patrón de rama. Mira el patrón de la primera rama, Ordering::Less, y ve que el valor Ordering::Greater no coincide con Ordering::Less, por lo que ignora el código en esa rama y pasa a la siguiente rama. El patrón de la siguiente rama es Ordering::Greater, que sí coincide con Ordering::Greater ¡El código asociado a esa rama se ejecutará y imprimirá Demasiado grande! en la pantalla. La expresión match termina después de la primera coincidencia exitosa, por lo que no revisará la última rama en este escenario.
Sin embargo, el código en la Lista 2-4 todavía no se compilará. Intentemoslo:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: tipos no coincidentes
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| ^^^^^^^^^^^^^^ se esperaba struct `String`, encontrado integer
|
= nota: se esperaba referencia `&String`
se encontró referencia `&{integer}`
El núcleo del error indica que hay tipos no coincidentes. Rust tiene un sistema de tipos estático fuerte. Sin embargo, también tiene inferencia de tipos. Cuando escribimos let mut guess = String::new(), Rust pudo inferir que guess debería ser un String y no nos hizo escribir el tipo. Por otro lado, secret_number es un tipo numérico. Algunos de los tipos numéricos de Rust pueden tener un valor entre 1 y 100: i32, un número de 32 bits; u32, un número sin signo de 32 bits; i64, un número de 64 bits; así como otros. A menos que se especifique lo contrario, Rust por defecto es un i32, que es el tipo de secret_number a menos que agregues información de tipo en otro lugar que haga que Rust infiera un tipo numérico diferente. La razón del error es que Rust no puede comparar una cadena y un tipo numérico.
En última instancia, queremos convertir la String que el programa lee como entrada en un tipo numérico real para que podamos compararla numéricamente con el número secreto. Lo hacemos agregando esta línea al cuerpo de la función main:
Nombre del archivo: src/main.rs
use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main() {
println!("Adivina el número!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("El número secreto es: {secret_number}");
println!("Por favor, ingresa tu suposición.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Falló al leer la línea");
let guess: u32 = guess
.trim()
.parse()
.expect("Por favor, escribe un número!");
println!("Has adivinado: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Demasiado pequeño!"),
Ordering::Greater => println!("Demasiado grande!"),
Ordering::Equal => println!("¡Ganaste!"),
}
}
Creamos una variable llamada guess. Pero espera, ¿no tiene el programa ya una variable llamada guess? Lo tiene, pero afortunadamente Rust permite que sobrescribamos el valor anterior de guess con uno nuevo. El sobreescritura nos permite reutilizar el nombre de variable guess en lugar de obligarnos a crear dos variables únicas, como guess_str y guess, por ejemplo. Cubriremos esto con más detalle en el Capítulo 3, pero por ahora, sabe que esta característica se usa a menudo cuando quieres convertir un valor de un tipo a otro tipo.
Asociamos esta nueva variable a la expresión guess.trim().parse(). La guess en la expresión se refiere a la variable original guess que contenía la entrada como una cadena. El método trim en una instancia de String eliminará cualquier espacio en blanco al principio y al final, lo que debemos hacer para poder comparar la cadena con el u32, que solo puede contener datos numéricos. El usuario debe presionar enter para satisfacer read_line e ingresar su suposición, lo que agrega un carácter de nueva línea a la cadena. Por ejemplo, si el usuario escribe 5 y presiona enter, guess se ve así: 5\n. El \n representa "nueva línea". (En Windows, presionar enter resulta en un retorno de carro y una nueva línea, \r\n.) El método trim elimina \n o \r\n, resultando en solo 5.
El método parse en cadenas convierte una cadena a otro tipo. Aquí, lo usamos para convertir de una cadena a un número. Necesitamos decirle a Rust el tipo numérico exacto que queremos usando let guess: u32. El dos puntos (:) después de guess le dice a Rust que anotaremos el tipo de la variable. Rust tiene algunos tipos numéricos integrados; el u32 que se ve aquí es un entero sin signo de 32 bits. Es una buena opción predeterminada para un número positivo pequeño. Aprenderás sobre otros tipos numéricos en el Capítulo 3.
Además, la anotación u32 en este programa de ejemplo y la comparación con secret_number significa que Rust inferirá que secret_number también debería ser un u32. Entonces, ahora la comparación será entre dos valores del mismo tipo ¡
El método parse solo funcionará en caracteres que se pueden convertir lógicamente en números y, por lo tanto, puede causar fácilmente errores. Si, por ejemplo, la cadena contuviera A👍%, no habría forma de convertir eso en un número. Debido a que podría fallar, el método parse devuelve un tipo Result, al igual que el método read_line (discutido anteriormente en "Manejar el fracaso potencial con Result"). Vamos a tratar este Result de la misma manera usando el método expect nuevamente. Si parse devuelve una variante Err de Result porque no pudo crear un número a partir de la cadena, la llamada a expect detendrá el juego y imprimirá el mensaje que le demos. Si parse puede convertir correctamente la cadena a un número, devolverá la variante Ok de Result, y expect devolverá el número que queremos del valor Ok.
Ahora ejecutemos el programa:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/guessing_game`
Adivina el número!
El número secreto es: 58
Por favor, ingresa tu suposición.
76
Has adivinado: 76
Demasiado grande!
¡Genial! Aunque se agregaron espacios antes de la suposición, el programa todavía pudo determinar que el usuario adivinó 76. Ejecute el programa varias veces para verificar el comportamiento diferente con diferentes tipos de entrada: adivine el número correctamente, adivine un número que sea demasiado alto y adivine un número que sea demasiado bajo.
Ya tenemos la mayor parte del juego funcionando, pero el usuario solo puede hacer una suposición. Cambiemos eso agregando un bucle ¡