Programando un juego de adivinanzas
¡Vamos a empezar con Rust trabajando en un proyecto práctico! Este capítulo te
introduce a algunos conceptos comunes de Rust mostrándote cómo usarlos en un
programa real. ¡Aprenderás sobre let
, match
, métodos, funciones asociadas,
paquetes externos y más! En los capítulos siguientes, exploraremos estos
conceptos en más detalle. En este capítulo, solo practicarás los fundamentos.
Implementaremos un clásico problema de programación para principiantes: un juego de adivinanzas. Así es como funciona: el programa generará un número entero aleatorio entre 1 y 100. Luego le pedirá al jugador que ingrese una adivinanza. Después de ingresar una adivinanza, el programa indicará si la adivinanza es demasiado baja o demasiado alta. Si la adivinanza es correcta, el juego imprimirá un mensaje de felicitación y saldrá.
Configurando un nuevo proyecto
Para configurar un nuevo proyecto, vaya al directorio proyectos que creó en el Capítulo 1 y cree un nuevo proyecto usando Cargo, así:
$ cargo new guessing_game
$ cd guessing_game
El primer comando, cargo new
, toma el nombre del proyecto (guessing_game
)
como el primer argumento. El segundo comando cambia al directorio del nuevo
proyecto.
Mira el archivo Cargo.toml generado:
Nombre de archivo: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
[dependencies]
Como viste en el Capítulo 1, cargo new
genera un programa “Hola, mundo!” para
ti. Mira el archivo src/main.rs:
Nombre de archivo: src/main.rs
fn main() { println!("Hello, world!"); }
Ahora compilemos este programa “Hola, mundo!” y ejecutémoslo en el mismo paso
usando el comando cargo run
:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.20s
Running `target/debug/guessing_game`
Hello, world!
El comando run
es útil cuando necesitas iterar rápidamente en un proyecto,
como haremos en este juego, probando rápidamente cada iteración antes de
pasar a la siguiente.
Vuelve a abrir el archivo src/main.rs. Escribirás todo el código en este
Procesando una adivinanza
La primera parte del programa del juego de adivinanzas pedirá al usuario que ingrese un valor, procesará ese valor y verificará que el valor esté en el formato esperado. Para comenzar, permitiremos al jugador ingresar una adivinanza. Ingresa el código de la Lista 2-1 en src/main.rs.
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
Este código contiene mucha información, así que repasémoslo línea por línea.
Para obtener la entrada del usuario y luego imprimir el resultado como salida,
necesitamos traer la biblioteca de entrada/salida io
al alcance. La biblioteca
io
viene de la biblioteca estándar, conocida como std
:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
Por defecto, Rust tiene un conjunto de elementos definidos en la biblioteca estándar que trae al alcance de cada programa. Este conjunto se llama prelude, y puedes ver todo lo que contiene en la documentación de la biblioteca estándar.
Si un tipo que quieres usar no está en el prelude, tienes que traer ese tipo
al alcance explícitamente con una declaración use
. Usar la biblioteca std::io
te proporciona una serie de características útiles, incluyendo la capacidad de
aceptar la entrada del usuario.
Como viste en el Capítulo 1, la función main
es el punto de entrada al
programa:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
La sintaxis fn
declara una nueva función; los paréntesis, ()
, indican que
no hay parámetros; y la llave, {
, inicia el cuerpo de la función.
Como también aprendiste en el Capítulo 1, println!
es una macro que imprime
una cadena en la pantalla:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
Este código está imprimiendo una solicitud que indica qué es el juego y está solicitando la entrada del usuario.
Almacenando valores con variables
A continuación, crearemos una variable para almacenar la entrada del usuario, como esto:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
¡Ahora el programa está interesante! Hay mucho que está pasando en esta pequeña
línea. Usamos la declaración let
para crear la variable. Aquí hay otro
ejemplo:
let apples = 5;
Esta línea crea una nueva variable llamada apples
y la enlaza con el valor 5.
En Rust, las variables son inmutables por defecto, lo que significa que una vez
que le damos a la variable un valor, el valor no cambiará. Vamos a discutir
este concepto en detalle en la sección “Variables y Mutabilidad”
del Capítulo 3. Para hacer una variable mutable, agregamos mut
antes del
nombre de la variable:
let apples = 5; // immutable
let mut bananas = 5; // mutable
Nota: La sintaxis
//
inicia un comentario que continúa hasta el final de la línea. Rust ignora todo lo que está en los comentarios. Vamos a discutir los comentarios en más detalle en el Capítulo 3.
Regresando al programa del juego de adivinanzas, ahora sabes que let mut guess
introducirá una variable mutable llamada guess
. El signo igual (=
) le dice
a Rust que queremos enlazar algo a la variable ahora. A la derecha del signo
igual está el valor al que guess
está enlazado, que es el resultado de llamar
a String::new
, una función que devuelve una nueva instancia de un String
.
String
es un tipo de cadena proporcionado por la
biblioteca estándar que es una parte de texto codificada en UTF-8 que puede
crecer.
La sintaxis ::
en la línea ::new
indica que new
es una función asociada
del tipo String
. Una función asociada es una función que está implementada
en un tipo, en este caso String
. Esta función new
crea una nueva cadena
vacía. Encontrarás una función new
en muchos tipos porque es un nombre
común para una función que crea un nuevo valor de algún tipo.
En total, la línea let mut guess = String::new();
ha creado una variable
mutable que está actualmente enlazada a una nueva instancia vacía de un
String
. ¡Uf!
Recibiendo la entrada del usuario
Recuerda que incluimos la funcionalidad de entrada/salida de la biblioteca
estándar con use std::io;
en la primera línea del programa. Ahora llamaremos
a la función stdin
del módulo io
, que nos permitirá manejar la entrada del
usuario:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
Si no hubiéramos importado la biblioteca io
con use std::io;
al comienzo del
programa, aún podríamos usar la función escribiendo esta llamada de función
como std::io::stdin
. La función stdin
devuelve una instancia de
std::io::Stdin
, que es un tipo que representa un
manejador de la entrada estándar para tu terminal.
A continuación, la línea .read_line(&mut guess)
llama al método
read_line
en el manejador de entrada estándar para
obtener la entrada del usuario. También estamos pasando &mut guess
como
argumento a read_line
para decirle en qué cadena almacenar la entrada del
usuario. El trabajo completo de read_line
es tomar lo que el usuario escribe
en la entrada estándar y agregar eso a una cadena (sin sobrescribir su
contenido), por lo que, por lo tanto, pasamos esa cadena como argumento. La
cadena de argumentos debe ser mutable para que el método pueda cambiar el
contenido de la cadena.
El &
indica que este argumento es una referencia, que te da una forma de
permitir que varias partes de tu código accedan a una pieza de datos sin
necesidad de copiar esos datos en la memoria varias veces. Las referencias son
una característica compleja, y una de las principales ventajas de Rust es lo
seguro y fácil que es usar referencias. No necesitas saber mucho de esos
detalles para terminar este programa. Por ahora, todo lo que necesitas saber es
que, como las variables, las referencias son inmutables por defecto. Por lo
tanto, necesitas escribir &mut guess
en lugar de &guess
para hacerlo
mutable. (El capítulo 4 explicará las referencias con más detalle.)
Manejando el posible fallo con Result
Todavía estamos trabajando en esta línea de código. Ahora estamos discutiendo una tercera línea de texto, pero ten en cuenta que aún es parte de una sola línea lógica de código. La siguiente parte es este método:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
Podríamos haber escrito este código como:
io::stdin().read_line(&mut guess).expect("Failed to read line");
Sin embargo, una línea larga es difícil de leer, por lo que es mejor dividirla.
A menudo es sabio introducir un salto de línea y otros espacios en blanco para
ayudar a dividir líneas largas cuando llamas a un método con la sintaxis
.method_name()
. Ahora discutamos lo que hace esta línea.
Como se mencionó anteriormente, read_line
coloca lo que el usuario ingresa en
la cadena que le pasamos, pero también devuelve un valor Result
. Result
es una enumeración, a menudo
llamada enum, que es un tipo que puede estar en uno de varios estados
posibles. Llamamos a cada estado posible a una variante.
El Capítulo 6 cubrirá las enumeraciones con más
detalles. El propósito de estos tipos Result
es codificar información de
manejo de errores.
Las variantes de Result
son Ok
y Err
. La variante Ok
indica que la
operación fue exitosa, y dentro de Ok
está el valor generado con éxito. La
variante Err
significa que la operación falló, y Err
contiene información
sobre cómo o por qué la operación falló.
Los valores del tipo Result
, como los valores de cualquier tipo, tienen
métodos definidos en ellos. Una instancia de Result
tiene un método
expect
que puedes llamar. Si esta instancia de
Result
es un valor Err
, expect
hará que el programa se bloquee y muestre
el mensaje que pasaste como argumento a expect
. Si el método read_line
devuelve un Err
, probablemente sea el resultado de un error proveniente del
sistema operativo subyacente. Si esta instancia de Result
es un valor Ok
,
expect
tomará el valor de retorno que Ok
está sosteniendo y devolverá solo
ese valor para que lo puedas usar. En este caso, ese valor es el número de
bytes en la entrada del usuario.
Si no llamas a expect
, el programa se compilará, pero obtendrás una advertencia:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
10 | let _ = io::stdin().read_line(&mut guess);
| +++++++
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s
Rust advierte que no has usado el valor Result
devuelto por read_line
,
indicando que el programa no ha manejado un posible error.
La forma correcta de suprimir la advertencia es escribir realmente código de
manejo de errores, pero en nuestro caso solo queremos bloquear este programa
cuando ocurra un problema, por lo que podemos usar expect
. Aprenderás a
recuperarte de los errores en el Capítulo 9.
Imprimiendo valores con marcadores de posición println!
Además del corchete de cierre, solo hay una línea más que discutir en el código hasta ahora:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
Esta línea imprime la cadena que ahora contiene la entrada del usuario. El
conjunto de llaves {}
es un marcador de posición: piensa en {}
como pequeñas
pinzas de cangrejo que mantienen un valor en su lugar. Al imprimir el valor de
una variable, el nombre de la variable puede ir dentro de las llaves
curvas. Al imprimir el resultado de evaluar una expresión, coloca llaves
curvas vacías en la cadena de formato, luego sigue la cadena de formato con una
lista separada por comas de expresiones para imprimir en cada marcador de
posición vacío de llaves curvas en el mismo orden. Imprimir una variable y el
resultado de una expresión en una llamada a println!
se vería así:
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {x} and y + 2 = {}", y + 2); }
Este código imprimiría x = 5 and y + 2 = 12
.
Probando la primera parte
Probemos la primera parte del juego de adivinanzas. Ejecútalo usando cargo run
:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
En este punto, la primera parte del juego está terminada: estamos obteniendo entrada del teclado y luego la imprimimos.
Generando un número secreto
A continuación, necesitamos generar un número secreto que el usuario intentará
adivinar. El número secreto debe ser diferente cada vez para que el juego sea
divertido de jugar más de una vez. Usaremos un número aleatorio entre 1 y 100
para que el juego no sea demasiado difícil. Rust aún no incluye la
funcionalidad de números aleatorios en su biblioteca estándar. Sin embargo, el
equipo de Rust proporciona un rand
crate con dicha
funcionalidad.
Usando un Crate para obtener más funcionalidad
Recuerda que un crate es una colección de archivos de código fuente de Rust. El
proyecto que hemos estado construyendo es un binary crate, que es un
ejecutable. El crate rand
es un library crate, que contiene código que se
pretende usar en otros programas y no se puede ejecutar por sí solo.
La coordinación de los crates externos de Cargo es donde realmente brilla
Cargo. Antes de poder escribir código que use rand
, necesitamos modificar el
archivo Cargo.toml para incluir el crate rand
como una dependencia. Abre ese
archivo ahora y agrega la siguiente línea al final, debajo del encabezado de la
sección [dependencies]
que Cargo creó para ti. Asegúrate de especificar rand
exactamente como lo tenemos aquí, con este número de versión, o los ejemplos de
código en este tutorial pueden no funcionar:
Nombre de archivo: Cargo.toml
[dependencies]
rand = "0.8.5"
En el archivo Cargo.toml, todo lo que sigue a un encabezado es parte de esa
sección que continúa hasta que comienza otra sección. En [dependencies]
le
dices a Cargo qué crates externos depende tu proyecto y qué versiones de esos
crates requieres. En este caso, especificamos el crate rand
con el
especificador de versión semántica 0.8.5
. Cargo entiende Semantic
Versioning (a veces llamado SemVer), que es un
estándar para escribir números de versión. El especificador 0.8.5
es
realmente un atajo para ^0.8.5
, lo que significa cualquier versión que sea
al menos 0.8.5 pero inferior a 0.9.0.
Cargo considera que estas versiones tienen APIs públicas compatibles con la versión 0.8.5, y esta especificación asegura que obtendrá la última versión de corrección que aún se compilará con el código de este capítulo. Cualquier versión 0.9.0 o superior no está garantizada de tener la misma API que lo que usarán los siguientes ejemplos.
Ahora, sin cambiar ningún código, construyamos el proyecto, como se muestra en el Listado 2-2.
$ cargo build
Updating crates.io index
Locking 16 packages to latest compatible versions
Adding wasi v0.11.0+wasi-snapshot-preview1 (latest: v0.13.3+wasi-0.2.2)
Adding zerocopy v0.7.35 (latest: v0.8.9)
Adding zerocopy-derive v0.7.35 (latest: v0.8.9)
Downloaded syn v2.0.87
Downloaded 1 crate (278.1 KB) in 0.16s
Compiling proc-macro2 v1.0.89
Compiling unicode-ident v1.0.13
Compiling libc v0.2.161
Compiling cfg-if v1.0.0
Compiling byteorder v1.5.0
Compiling getrandom v0.2.15
Compiling rand_core v0.6.4
Compiling quote v1.0.37
Compiling syn v2.0.87
Compiling zerocopy-derive v0.7.35
Compiling zerocopy v0.7.35
Compiling ppv-lite86 v0.2.20
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.69s
Es posible que veas números de versión diferentes (¡pero todos serán compatibles con el código, gracias a SemVer!) y líneas diferentes (dependiendo del sistema operativo), y las líneas pueden estar en un orden diferente.
Cuando incluimos una dependencia externa, Cargo obtiene las últimas versiones de todo lo que la dependencia necesita del registro, que es una copia de datos de Crates.io. Crates.io es donde las personas en el ecosistema de Rust publican sus proyectos de Rust de código abierto para que otros los utilicen.
Después de actualizar el registro, Cargo verifica la sección [dependencies]
y descarga cualquier crate que se haya enumerado que aún no se haya
descargado. En este caso, aunque solo enumeramos rand
como una dependencia,
Cargo también tomó otros crates que rand
depende para funcionar. Después de
descargar los crates, Rust los compila y luego compila el proyecto con las
dependencias disponibles.
Si ejecuta cargo build
nuevamente sin hacer ningún cambio, no obtendrá
ninguna salida aparte de la línea Finished
. Cargo sabe que ya ha descargado y
compilado las dependencias, y no ha cambiado nada sobre ellas en su archivo
Cargo.toml. Cargo también sabe que no ha cambiado nada sobre su código, por
lo que tampoco lo vuelve a compilar. Sin nada que hacer, simplemente sale.
Si abre el archivo src/main.rs, realiza un cambio trivial y luego lo guarda y vuelve a construir, solo verá dos líneas de salida:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
Estas líneas muestran que Cargo solo actualiza la compilación con su pequeño cambio en el archivo src/main.rs. Sus dependencias no han cambiado, por lo que Cargo sabe que puede reutilizar lo que ya ha descargado y compilado para esas.
Garantizar compilaciones reproducibles con el archivo Cargo.lock
Cargo tiene un mecanismo que le garantiza que puede reconstruir el mismo
artefacto cada vez que usted o cualquier otra persona construye su código:
Cargo solo usará las versiones de las dependencias que haya especificado hasta
que indique lo contrario. Por ejemplo, digamos que la semana que viene sale la
versión 0.8.6 del crate rand
, y que esa versión contiene una corrección de
error importante, pero también contiene una regresión que romperá su código.
Para manejar esto, Rust crea el archivo Cargo.lock la primera vez que ejecuta
cargo build
, por lo que ahora tenemos esto en el directorio guessing_game
Cuando construye un proyecto por primera vez, Cargo determina todas las versiones de las dependencias que cumplen con los criterios y luego las escribe en el archivo Cargo.lock. Cuando construye su proyecto en el futuro, Cargo verá que el archivo Cargo.lock existe y usará las versiones especificadas allí en lugar de hacer todo el trabajo de averiguar las versiones nuevamente. Esto le permite tener una compilación reproducible de forma automática. En otras palabras, su proyecto permanecerá en 0.8.5 hasta que actualice explícitamente, gracias al archivo Cargo.lock. Debido a que el archivo Cargo.lock es importante para las compilaciones reproducibles, a menudo se verifica en el control de versiones con el resto del código en su proyecto.
Actualizar un crate para obtener una nueva versión
Cuando quiera actualizar un crate, Cargo proporciona el comando update
,
que ignorará el archivo Cargo.lock y determinará todas las últimas versiones
que cumplan con sus especificaciones en Cargo.toml. Cargo luego escribirá
esas versiones en el archivo Cargo.lock. En este caso, Cargo solo buscará
versiones mayores que 0.8.5 y menores que 0.9.0. Si el crate rand
ha lanzado
las dos nuevas versiones 0.8.6 y 0.9.0, vería lo siguiente si ejecutara
cargo update
:
$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
Cargo ignora el lanzamiento 0.9.0. En este punto, también notaría un cambio en
su archivo Cargo.lock que indica que la versión del crate rand
que ahora
está usando es 0.8.6. Para usar la versión 0.9.0 o cualquier versión en la
serie 0.9.x, tendría que actualizar el archivo Cargo.toml para que se
vea así:
[dependencies]
rand = "0.9.0"
La próxima vez que ejecute cargo build
, Cargo actualizará el registro de
crates disponibles y volverá a evaluar sus requisitos de rand
de acuerdo con
la nueva versión que ha especificado.
Hay mucho más que decir sobre Cargo y su ecosistema, que discutiremos en el Capítulo 14, pero por ahora, eso es todo lo que necesita saber. Cargo hace muy fácil reutilizar bibliotecas, por lo que los Rustaceans pueden escribir proyectos más pequeños que se ensamblan a partir de un número de paquetes.
Generar un numero aleatorio
Comencemos a usar rand
para generar un número para adivinar. El siguiente
paso es actualizar src/main.rs, como se muestra en el Listado 2-3.
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Primero agregamos la línea use rand::Rng;
. El trait Rng
define métodos que
los generadores de números aleatorios implementan, y este trait debe estar en
el alcance para que podamos usar esos métodos. El Capítulo 10 cubrirá los
traits en detalle.
A continuación, estamos agregando dos líneas en el medio. En la primera línea,
llamamos a la función rand::thread_rng
que nos da el generador de números
aleatorios particular que vamos a usar: uno que es local al hilo de ejecución
actual y está sembrado por el sistema operativo. Luego llamamos al método
gen_range
en el generador de números aleatorios. Este método está definido
por el trait Rng
que traemos al alcance con la declaración use rand::Rng;
.
El método gen_range
toma una expresión de rango como argumento y genera un
número aleatorio en el rango. El tipo de expresión de rango que estamos
utilizando aquí toma la forma start..=end
y es inclusivo en los límites
inferior y superior, por lo que necesitamos especificar 1..=100
para solicitar
un número entre 1 y 100.
Nota: No sabrá solo qué traits usar y qué métodos y funciones llamar desde un crate, por lo que cada crate tiene documentación con instrucciones para usarlo. Otra característica interesante de Cargo es que ejecutar el comando
cargo doc --open
construirá la documentación proporcionada por todas sus dependencias localmente y la abrirá en su navegador. Si está interesado en otra funcionalidad en el craterand
, por ejemplo, ejecutecargo doc --open
y haga clic enrand
en la barra lateral a la izquierda.
La segunda línea nueva imprime el número secreto. Esto es útil mientras desarrollamos el programa para poder probarlo, pero lo eliminaremos de la versión final. ¡No es mucho un juego si el programa imprime la respuesta tan pronto como comienza!
Intente ejecutar el programa varias veces:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
Debería obtener números aleatorios diferentes, y todos deberían ser números entre 1 y 100. ¡Gran trabajo!
Comparando la Adivinanza con el Número Secreto
Ahora que tenemos la entrada del usuario y un número aleatorio, podemos compararlos. Ese paso se muestra en el Listado 2-4. Tenga en cuenta que este código aún no se compilará, como explicaremos.
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Primero agregamos otra declaración use
, que trae un tipo llamado
std::cmp::Ordering
al alcance de 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 compara dos valores.
Luego agregamos cinco nuevas líneas al final que usan el tipo Ordering
. El
método cmp
compara dos valores y se puede llamar en cualquier cosa que se
pueda comparar. Toma una referencia a lo que quiera comparar: aquí está
comparando guess
con secret_number
. Luego devuelve una variante del enum
Ordering
que importamos al alcance con la declaración use
. Usamos una
expresión match
para decidir qué hacer a continuación
basándonos en qué variante de Ordering
se devolvió de la llamada a cmp
con
los valores en guess
y secret_number
.
Una expresión match
está compuesta por brazos. Un brazo consta de un
patrón para coincidir y el código que se debe ejecutar si el valor dado a
match
se ajusta al patrón del brazo. Rust toma el valor dado a match
y
busca cada patrón de brazo en orden. Los patrones y la construcción match
son
potentes características de Rust: le permiten expresar una variedad de
situaciones que su código puede encontrar y se aseguran de que los maneje
todos. Estas características se cubrirán en detalle en el Capítulo 6 y el
Capítulo 19, respectivamente.
Vamos a repasar 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 verificar el patrón de cada brazo. Mira
el patrón del primer brazo, Ordering::Less
, y ve que el valor
Ordering::Greater
no coincide con Ordering::Less
, ¡así que ignora el código
en ese brazo y se mueve al siguiente brazo! El patrón del siguiente brazo es
Ordering::Greater
, ¡que sí coincide con Ordering::Greater
! El código
asociado en ese brazo se ejecutará y mostrará Too big!
en la pantalla. La
expresión match
termina después de la primera coincidencia exitosa, ¡así que
no mirará el último brazo en este escenario.
Sin embargo, el código del Listado 2-4 aún no se compilará. Vamos a intentarlo:
$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
| |
| arguments to this method are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: method defined here
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/cmp.rs:838:8
|
838 | fn cmp(&self, other: &Self) -> Ordering;
| ^^^
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error
El núcleo del error indica que hay tipos no coincidentes. Rust tiene un
sistema de tipos fuerte y estático. Sin embargo, también tiene inferencia de
tipo. Cuando escribimos let mut guess = String::new()
, Rust pudo inferir que
guess
debería ser un String
y no nos obligó a escribir el tipo. El
secret_number
, por otro lado, es un tipo de número. Algunos de los tipos de
números 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 predetermina un i32
, que
es el tipo de secret_number
a menos que agregue 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.
Finalmente, queremos convertir la String
que el programa lee como entrada en
un tipo de número real para que podamos compararlo numéricamente con el número
secreto. Lo hacemos agregando esta línea al cuerpo de la función main
:
Nombre de archivo: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
La línea es:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
Creamos una variable llamada guess
. Pero espera, ¿no tiene el programa ya una
variable llamada guess
? Lo hace, pero Rust nos permite redefinir el valor
anterior de guess
con uno nuevo. Este concepto en Rust se le conoce como
Shadowing, nos permite volver a usar el nombre de la variable guess
en lugar de obligarnos a crear dos variables únicas, como guess_str
y guess
, por ejemplo. Lo cubriremos con más detalle en el
Capítulo 3, pero por ahora, sé que esta
característica se usa a menudo cuando desea convertir un valor de un tipo a
otro tipo.
Enlazamos esta nueva variable a la expresión guess.trim().parse()
. La guess
en la expresión se refiere a la variable guess
original que contenía la
entrada como una cadena. El método trim
en una instancia 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 conjetura, 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
, lo que resulta en solo 5
.
El método parse
en las cadenas convierte una cadena
en otro tipo. Aquí, lo usamos para convertir de una cadena a un número. Debemos
decirle a Rust el tipo de número exacto que queremos usando let guess: u32
.
Los dos puntos (:
) después de guess
le dicen a Rust que anotaremos el tipo
de variable. Rust tiene algunos tipos de número integrados; el u32
visto
aquí es un entero sin signo de 32 bits. Es una buena opción predeterminada para
un número positivo pequeño. Aprenderá sobre otros tipos de números 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 u32
. ¡Entonces la comparación será entre dos valores del mismo
tipo!
El método parse
solo funcionará en caracteres que se puedan convertir
lógicamente en números y, por lo tanto, pueden causar fácilmente errores. Si,
por ejemplo, la cadena contiene A👍%
, no habría manera de convertir eso en un
número. Debido a que podría fallar, el método parse
devuelve un tipo Result
,
tal como lo hace el método read_line
(discutido anteriormente en
“Manejo de posibles fallas con Result
”).
Trataremos este Result
de la misma manera usando el método expect
de nuevo.
Si parse
devuelve una variante Err
del tipo Result
porque no pudo crear
un número a partir de la cadena, la llamada expect
hará que el juego se
bloquee y muestre el mensaje que le damos. Si parse
puede convertir
exitosamente la cadena en un número, devolverá la variante Ok
del tipo
Result
, y expect
devolverá el número que queremos del valor Ok
.
¡Corramos el programa ahora!
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
¡Bien! Aunque se agregaron espacios antes de la adivinanza, el programa aún sabía 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.
Tenemos la mayoría del juego funcionando ahora, pero el usuario solo puede adivinar una vez. ¡Cambiamos eso agregando un bucle!
Permitir múltiples adivinanzas con bucles
La palabra clave loop
crea un bucle infinito. Agregaremos un bucle para darle
a los usuarios más oportunidades para adivinar el número:
Nombre de archivo: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
// --snip--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
Como puede ver, hemos movido todo desde la solicitud de entrada de adivinanzas hacia adelante en un bucle. Asegúrese de indentar las líneas dentro del bucle otras cuatro veces y ejecute el programa nuevamente. ¡El programa ahora pedirá otra adivinanza para siempre, lo que introduce un nuevo problema! ¡Parece que el usuario no puede salir!
El usuario siempre podría interrumpir el programa usando el atajo de teclado
ctrl-c. Pero hay otra forma de escapar de este monstruo insaciable,
como se mencionó en la discusión de parse
en
“Comparando la adivinanza con el número secreto”: si el usuario ingresa una respuesta que no es un número, el
programa se bloqueará. Podemos aprovechar eso para permitir que el usuario
salga, como se muestra aquí:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Al escribir quit
se cerrará el juego, pero como notará, también lo hará al
ingresar cualquier otra entrada que no sea un número. Esto es lo menos
óptimo, para decir lo menos; queremos que el juego también se detenga cuando se
adivine el número correcto.
Salir después de una adivinanza correcta
Programemos el juego para que se cierre cuando el usuario gane agregando una
declaración break
:
Nombre de archivo: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Agregando la línea break
después de You win!
hace que el programa salga del
bucle cuando el usuario adivina el número secreto correctamente. Salir del
bucle también significa salir del programa, porque el bucle es la última parte
de main
.
Manejo de entrada no válida
Para mejorar aún más el comportamiento del juego, en lugar de bloquear el
programa cuando el usuario ingresa un número no válido, hagamos que el juego
ignore un número no válido para que el usuario pueda seguir adivinando. Podemos
hacer eso alterando la línea donde guess
se convierte de un String
a un
u32
, como se muestra en el Listado 2-5.
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
// --snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Cambiamos de una llamada expect
a una expresión match
para pasar de
bloquear el programa en un error a manejar el error. Recuerde que parse
devuelve un tipo Result
y Result
es un enum que tiene las variantes Ok
y
Err
. Aquí estamos usando una expresión match
, como hicimos con el resultado
Ordering
del método cmp
.
Si parse
es capaz de convertir exitosamente la cadena en un número, devolverá
un valor Ok
que contiene el número resultante. Ese valor Ok
coincidirá con
el patrón de la primera rama y la expresión match
devolverá el valor num
que parse
produjo y puso dentro del valor Ok
. Ese número terminará en el
lugar correcto en la nueva variable guess
que estamos creando.
Si parse
no es capaz de convertir la cadena en un número, devolverá un
valor Err
que contiene más información sobre el error. El valor Err
no
coincide con el patrón Ok(num)
en la primera rama de match
, pero sí
coincide con el patrón Err(_)
en la segunda rama. El guión bajo, _
, es un
valor de captura; en este ejemplo, estamos diciendo que queremos coincidir con
todos los valores Err
, sin importar qué información tengan dentro. ¡Así que
el programa ejecutará el código de la segunda rama, continue
, que le dice al
programa que vaya a la siguiente iteración del loop
y pida otra adivinanza.
¡Así que, efectivamente, el programa ignora todos los errores que parse
puede
encontrar!
Ahora todo en el programa debería funcionar como se espera. Vamos a probarlo:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
¡Genial! Con un pequeño ajuste final, terminaremos el juego de adivinanzas.
Recuerde que el programa todavía está imprimiendo el número secreto. Eso
funcionó bien para las pruebas, pero arruina el juego. Vamos a eliminar el
println!
que muestra el número secreto. El listado 2-6 muestra el código
final.
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
En este punto, ha construido exitosamente el juego de adivinanzas. ¡Felicidades!
Resumen
Este proyecto fue una manera práctica de introducirle a muchos nuevos conceptos
de Rust: let
, match
, funciones, el uso de paquetes externos, y más. En los
próximos capítulos, aprenderá sobre estos conceptos en más detalle. El capítulo
3 cubre conceptos que la mayoría de los lenguajes de programación tienen, como
variables, tipos de datos y funciones, y muestra cómo usarlos en Rust. El
capítulo 4 explora la propiedad, una característica que hace que Rust sea
diferente de otros lenguajes. El capítulo 5 discute las estructuras y la
sintaxis de los métodos, y el capítulo 6 explica cómo funcionan los enums.