Libro Rust
Libro Rust
Esta versión del texto asume que usas Rust 1.82.0 (publicado el 17/10/2024) o
posterior. Consulta la sección "Instalación" del Capítulo 1 para instalar o actualizar
Rust.
Prefacio
Tomemos, por ejemplo, el trabajo a nivel de sistemas que aborda los detalles
básicos de la gestión de memoria, la representación de datos y la concurrencia.
Tradicionalmente, este ámbito de la programación se considera arcano, accesible
solo para unos pocos que han dedicado los años necesarios al aprendizaje para
evitar sus infames trampas. E incluso quienes lo practican lo hacen con cautela,
para evitar que su código esté expuesto a vulnerabilidades, fallos o corrupción.
Los programadores que ya trabajan con código de bajo nivel pueden usar Rust
para ampliar sus horizontes. Por ejemplo, introducir paralelismo en Rust es una
operación de bajo riesgo: el compilador detectará automáticamente los errores
clásicos. Además, pueden realizar optimizaciones más agresivas en su código con
la confianza de no introducir accidentalmente fallos ni vulnerabilidades.
Introducción
Nota: Esta edición del libro es la misma que The Rust Programming
Language, disponible en formato impreso y de libro electrónico en No Starch
Press .
Rust es ideal para muchas personas por diversas razones. Analicemos algunos de
los grupos más importantes.
Equipos de desarrolladores
Rust está demostrando ser una herramienta productiva para la colaboración entre
grandes equipos de desarrolladores con distintos niveles de conocimiento en
programación de sistemas.
Estudiantes
Empresas
El lenguaje Rust también espera dar soporte a muchos otros usuarios; los
mencionados aquí son solo algunos de los principales interesados. En general, la
mayor ambición de Rust es eliminar las desventajas que los programadores han
aceptado durante décadas, ofreciendo seguridad , productividad,
velocidad y ergonomía. Prueba Rust y comprueba si sus opciones te funcionan.
Rust
jueves, 22 de mayo de 2025 : Página 5 de 719
Para quién es este libro
Este libro asume que has escrito código en otro lenguaje de programación, pero
no presupone cuál. Hemos intentado que el material sea accesible para personas
con diversos antecedentes en programación. No dedicamos mucho tiempo a
hablar sobre qué es la programación ni cómo pensar en ella. Si eres totalmente
nuevo en programación, te conviene más leer un libro que ofrezca una
introducción específica a la programación.
En general, este libro asume que se lee en secuencia, de principio a fin. Los
capítulos posteriores amplían los conceptos de los capítulos anteriores, y estos
últimos podrían no profundizar en los detalles de un tema en particular, sino que
lo retoman en un capítulo posterior.
El Capítulo 3 cubre las características de Rust que son similares a las de otros
lenguajes de programación,.
No hay una forma incorrecta de leer este libro: si quieres adelantar algo, ¡hazlo!
Quizás tengas que volver a capítulos anteriores si te confundes. Pero haz lo que te
funcione.
Ferri
Significado
s
Código fuente
Los archivos fuente a partir de los cuales se genera este libro se pueden
encontrar en GitHub .
Empezando
¡Comencemos tu aventura en Rust! Hay mucho que aprender, pero todo viaje
empieza en algún punto. En este capítulo, hablaremos sobre:
Instalación
Rust
jueves, 22 de mayo de 2025 : Página 9 de 719
Nota: Si prefiere no usarlo rustup por algún motivo, consulte la página Otros
métodos de instalación de Rust para obtener más opciones.
Los siguientes pasos instalan la última versión estable del compilador de Rust. Las
garantías de estabilidad de Rust garantizan que todos los ejemplos del libro que
compilan seguirán compilando con versiones más recientes de Rust. El resultado
puede variar ligeramente entre versiones, ya que Rust suele mejorar los mensajes
de error y las advertencias. En otras palabras, cualquier versión estable más
reciente de Rust que instale siguiendo estos pasos debería funcionar
correctamente con el contenido de este libro.
También necesitarás un enlazador , un programa que Rust usa para unir las
salidas compiladas en un solo archivo. Probablemente ya tengas uno. Si recibes
errores del enlazador, deberías instalar un compilador de C, que normalmente
Rust
jueves, 22 de mayo de 2025 : Página 10 de 719
$ xcode-select --install
El resto de este libro utiliza comandos que funcionan tanto en cmd.exe como en
PowerShell. Si existen diferencias específicas, explicaremos cuál usar.
Solución de problemas
$ rustc --version
En PowerShell, utilice:
$ echo $PATH
Si todo esto es correcto y Rust sigue sin funcionar, hay varios lugares donde
puedes obtener ayuda. Descubre cómo contactar con otros Rustáceos (un apodo
gracioso con el que nos llamamos) en la página de la comunidad .
Actualización y desinstalación
Una vez instalado Rust mediante [nombre del archivo] rustup, actualizar a una
versión nueva es fácil. Desde tu shell, ejecuta el siguiente script de actualización:
$ rustup update
Documentación local
Cada vez que la biblioteca estándar proporciona un tipo o una función y no está
seguro de qué hace o cómo usarlo, utilice la documentación de la interfaz de
programación de aplicaciones (API) para averiguarlo.
Este libro no presupone qué herramientas se utilizan para crear código en Rust.
¡Casi cualquier editor de texto servirá! Sin embargo, muchos editores de texto y
entornos de desarrollo integrados (IDE) son compatibles con Rust. Siempre
puedes encontrar una lista bastante actualizada de editores e IDE en la página de
herramientas del sitio web de Rust.
Rust
jueves, 22 de mayo de 2025 : Página 12 de 719
¡Hola Mundo!
Ahora que has instalado Rust, es hora de escribir tu primer programa en Rust. Al
aprender un nuevo lenguaje, es tradicional escribir un pequeño programa que
imprima el texto Hello, world! en pantalla, ¡así que haremos lo mismo aquí!
Abra una terminal e ingrese los siguientes comandos para crear un directorio de
proyectos y un directorio para el proyecto “Hola, mundo!” dentro del directorio de
proyectos .
$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
> fn main
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world
Ahora abra el archivo main.rs que acaba de crear e ingrese el código del Listado
1-1.
fn main() {
println!("Hello, world!");
}
$ rustc main.rs
$ ./main
Hello, world!
Repasemos este programa "¡Hola mundo!" en detalle. Aquí está la primera pieza
del rompecabezas:
Rust
jueves, 22 de mayo de 2025 : Página 14 de 719
fn main() {
Estas líneas definen una función llamada main. Esta mainfunción es especial:
siempre es el primer código que se ejecuta en cada programa ejecutable de Rust.
Aquí, la primera línea declara una función llamada mainque no tiene parámetros y
no devuelve nada. Si hubiera parámetros, irían entre paréntesis ().
println!("Hello, world!");
Esta línea realiza todo el trabajo en este pequeño programa: imprime texto en la
pantalla. Hay cuatro detalles importantes a tener en cuenta.
Primero, println! llama a una macro de Rust. Si hubiera llamado a una función, se
habría ingresado como println(sin el !). Analizaremos las macros de Rust con más
detalle en el Capítulo 20. Por ahora, solo necesita saber que usar un ! significa
que está llamando a una macro en lugar de a una función normal y que las
macros no siempre siguen las mismas reglas que las funciones.
En tercer lugar, terminamos la línea con un punto y coma ( ;), lo que indica que
esta expresión ha terminado y que la siguiente está lista para comenzar. La
mayoría de las líneas de código de Rust terminan con punto y coma.
Acaba de ejecutar un programa recién creado, así que examinemos cada paso del
proceso.
$ rustc main.rs
$ ls
main main.rs
En Linux y macOS, verás dos archivos. Con PowerShell en Windows, verás los
mismos tres archivos que con CMD. Con CMD en Windows, debes escribir lo
siguiente:
> dir /B %= the /B option says to only show the file names =%
main.exe
main.pdb
main.rs
alguien un archivo .rb , .py o .js , necesita tener instalada una implementación de
Ruby, Python o JavaScript (respectivamente). Pero en esos lenguajes, solo
necesitas un comando para compilar y ejecutar tu programa. Todo es cuestión de
equilibrio en el diseño del lenguaje.
Compilar con rustces suficiente para programas sencillos, pero a medida que tu
proyecto crezca, querrás gestionar todas las opciones y facilitar la compartición
de tu código. A continuación, te presentaremos la herramienta Cargo, que te
ayudará a escribir programas reales en Rust.
¡Hola, Cargo!
Los programas Rust más sencillos, como el que hemos escrito hasta ahora, no
tienen dependencias. Si hubiéramos creado el proyecto "¡Hola mundo!" con
Cargo, solo usaría la parte de Cargo que se encarga de la creación del código. A
medida que escribas programas Rust más complejos, irás añadiendo
dependencias, y si empiezas un proyecto con Cargo, añadir dependencias será
mucho más fácil.
Dado que la gran mayoría de los proyectos de Rust usan Cargo, el resto de este
libro asume que usted también lo usa. Cargo viene instalado con Rust si utilizó los
instaladores oficiales mencionados en la sección "Instalación" . Si instaló Rust por
otros medios, compruebe si Cargo está instalado introduciendo lo siguiente en su
terminal:
$ cargo --version
Si ves un número de versión, ¡la tienes! Si ves un error, como command not
found, consulta la documentación de tu método de instalación para determinar
cómo instalar Cargo por separado.
Accede al directorio hello_cargo y lista los archivos. Verás que Cargo ha generado
dos archivos y un directorio: un archivo Cargo.toml y un directorio src con
un archivo main.rs.
Abra Cargo.toml en su editor de texto preferido. Debería ser similar al código del
Listado 1-2.
[dependencies]
Listado 1-2: Contenido de Cargo.toml generado porcargo new
Este archivo está en formato TOML ( Tom's Obvious, Minimal Language ), que es
el formato de configuración de Cargo.
Rust
jueves, 22 de mayo de 2025 : Página 18 de 719
fn main() {
println!("Hello, world!");
}
Cargo ha generado un programa "¡Hola mundo!" para ti, igual al que escribimos
en el Listado 1-1. Hasta ahora, las diferencias entre nuestro proyecto y el que
generó Cargo son que Cargo colocó el código en el directorio src y tenemos un
archivo de configuración Cargo.toml en el directorio superior.
Si iniciaste un proyecto que no usa Cargo, como hicimos con el proyecto "¡Hola
mundo!", puedes convertirlo en uno que sí lo use. Mueve el código del proyecto
al directorio src y crea un archivo Cargo.toml apropiado . Una forma sencilla de
obtenerlo es ejecutar , que lo creará automáticamente.cargo init
Ahora veamos qué cambia al compilar y ejecutar el programa "¡Hola mundo!" con
Cargo. Desde el directorio hello_cargo , compila tu proyecto con el siguiente
comando:
$ cargo build
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/hello_cargo`
Hello, world!
Usarlo cargo runes más conveniente que tener que recordar ejecutar cargo buildy
luego usar la ruta completa al binario, por eso la mayoría de los desarrolladores
usan cargo run.
Tenga en cuenta que esta vez no vimos ningún resultado que indicara que Cargo
estaba compilando hello_cargo. Cargo detectó que los archivos no habían
cambiado, por lo que no recompiló, sino que simplemente ejecutó el binario. Si
Rust
jueves, 22 de mayo de 2025 : Página 20 de 719
$ cargo run
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs
Running `target/debug/hello_cargo`
Hello, world!
$ cargo check
Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
Una ventaja adicional de usar Cargo es que los comandos son los mismos
independientemente del sistema operativo. Por lo tanto, por ahora, ya no
proporcionaremos instrucciones específicas para Linux y macOS frente a
Windows.
Rust
jueves, 22 de mayo de 2025 : Página 21 de 719
Construyendo para la liberación
Resumen
¡Tu experiencia en Rust ya ha empezado con buen pie! En este capítulo, has
aprendido a:
[package]
name = "guessing_game"
Rust
jueves, 22 de mayo de 2025 : Página 23 de 719
version = "0.1.0"
edition = "2021"
[dependencies]
fn main() {
println!("Hello, world!");
}
$ 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!
La primera parte del programa del juego de adivinanzas solicitará la entrada del
usuario, la procesará y comprobará que esté en el formato esperado. Para
empezar, permitiremos que el jugador introduzca una suposición. Introduzca el
código del Listado 2-1 en src/main.rs .
fn main() {
println!("Guess the number!");
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
Este código contiene mucha información, así que vamos a analizarlo línea por
línea. Para obtener la entrada del usuario e imprimir el resultado como salida,
necesitamos incluir la iobiblioteca de entrada/salida en el alcance.
Esta iobiblioteca proviene de la biblioteca estándar, conocida como std:
use std::io;
fn main() {
La fnsintaxis 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:
¡Ahora el programa se pone interesante! Hay mucho que hacer en esta pequeña
línea. Usamos la letinstrucción para crear la variable. Aquí hay otro ejemplo:
let apples = 5;
Esta línea crea una nueva variable llamada applesy la vincula al valor 5. En Rust,
las variables son inmutables por defecto, lo que significa que una vez que les
asignamos un valor, este no cambia. Analizaremos este concepto en detalle en la
sección "Variables y Mutabilidad" del Capítulo 3. Para que una variable sea
mutable, añadimos mutantes del nombre de la variable:
Volviendo al programa del juego de adivinanzas, ahora sabes que let mut
guessintroducirá una variable mutable llamada guess. El signo igual ( =) indica a
Rust que queremos asociar algo a la variable. A la derecha del signo igual se
encuentra el valor guessal que se asocia, 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 consiste en un fragmento de texto ampliable y codificado en UTF-8.
io::stdin()
.read_line(&mut guess)
El &[nombre del argumento] indica que este argumento es una referencia , lo que
permite que varias partes del código accedan a un dato sin necesidad de copiarlo
en memoria varias veces. Las referencias son una función compleja, y una de las
principales ventajas de Rust es su seguridad y facilidad de uso. No necesitas
conocer muchos detalles para completar este programa. Por ahora, solo necesitas
saber que, al igual que las variables, las referencias son inmutables por defecto.
Por lo tanto, necesitas escribir &mut guessen lugar de &guesshacerlas mutables.
(El capítulo 4 explicará las referencias con más detalle).
Sin embargo, una línea larga es difícil de leer, por lo que es mejor dividirla. Suele
ser recomendable introducir un salto de línea y otros espacios en blanco para
dividir líneas largas al llamar a un método con la .method_name()sintaxis. Ahora
veamos qué hace esta línea.
Los valores de este Resulttipo, al igual que los de cualquier otro tipo, tienen
métodos definidos. Una instancia de Resulttiene un expectmétodo al que se
puede llamar. Si esta instancia de Resultes un Errvalor, expect provocará un fallo
del programa y mostrará el mensaje que se pasó como argumento a expect. Si
el read_linemétodo devuelve un Err, probablemente se deba a un error del
sistema operativo subyacente. Si esta instancia de Resultes
un Okvalor, expecttomará el valor de retorno que Okcontiene y le devolverá solo
ese valor para que pueda usarlo. En este caso, ese valor es el número de bytes en
la entrada del usuario.
$ 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);
Rust
jueves, 22 de mayo de 2025 : Página 28 de 719
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= 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);
| +++++++
Aparte de la llave de cierre, solo hay una línea más para discutir en el código
hasta ahora:
Esta línea imprime la cadena que ahora contiene la entrada del usuario.
El {}conjunto de llaves es un marcador de posición: imagínense {}como
pequeñas pinzas de cangrejo que sujetan un valor. Al imprimir el valor de una
variable, el nombre de la variable puede ir dentro de las llaves. Al imprimir el
resultado de la evaluación de una expresión, coloque llaves vacías en la cadena
de formato y, a continuación, una lista de expresiones separadas por comas para
imprimir en cada marcador de posición de llave vacía, en el mismo orden.
Imprimir una variable y el resultado de una expresión en una sola llamada
a println!se vería así:
let x = 5;
let y = 10;
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
Cargo considera que estas versiones tienen API públicas compatibles con la
versión 0.8.5, y esta especificación garantiza que recibirá la última versión del
parche, que compilará con el código de este capítulo. No se garantiza que
ninguna versión 0.9.0 o posterior tenga la misma API que la que utilizan los
siguientes ejemplos.
Ahora, sin cambiar nada del 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
Rust
jueves, 22 de mayo de 2025 : Página 31 de 719
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
Listado 2-2: El resultado de la ejecución cargo builddespués de agregar la caja
rand como dependencia
Al incluir una dependencia externa, Cargo obtiene las últimas versiones de todo lo
que necesita del registro , que es una copia de los datos de Crates.io . Crates.io es
donde los usuarios del ecosistema Rust publican sus proyectos de código abierto
para que otros los usen.
$ 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 tu pequeño
cambio en el archivo src/main.rs . Tus dependencias no han cambiado, así que
Cargo sabe que puede reutilizar lo que ya ha descargado y compilado para ellas.
Cargo cuenta con un mecanismo que garantiza que puedas reconstruir el mismo
artefacto cada vez que tú o cualquier otra persona compile tu código: Cargo usará
solo las versiones de las dependencias que especificaste hasta que indiques lo
contrario. Por ejemplo, supongamos que la semana que viene randsale la versión
0.8.6 del crate, que contiene una corrección importante de errores, pero también
una regresión que dañará tu código. Para solucionar esto, Rust crea el
archivo Cargo.lock la primera vez que ejecutas cargo build, por lo que ahora lo
tenemos en el juego de adivinanzas. .
Al compilar un proyecto por primera vez, Cargo determina todas las versiones de
las dependencias que cumplen los criterios y las escribe en el archivo Cargo.lock .
Al compilar su proyecto en el futuro, Cargo detectará la existencia del
archivo Cargo.lock y usará las versiones especificadas en él, en lugar de tener
que volver a calcularlas. Esto le permite tener una compilación reproducible
automáticamente. En otras palabras, su proyecto se mantendrá en la versión
0.8.5 hasta que actualice explícitamente, gracias al archivo Cargo.lock . Dado que
el archivo Cargo.lock es importante para compilaciones reproducibles, suele
incluirse en el control de código fuente junto con el resto del código del proyecto.
$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
[dependencies]
rand = "0.9.0"
Rust
jueves, 22 de mayo de 2025 : Página 33 de 719
La próxima vez que ejecute cargo build, Cargo actualizará el registro de cajas
disponibles y reevaluará sus randrequisitos de acuerdo con la nueva versión que
haya especificado.
Hay mucho más que decir sobre Cargo y su ecosistema , que analizaremos en el
Capítulo 14, pero por ahora, eso es todo lo que necesitas saber. Cargo facilita la
reutilización de bibliotecas, lo que permite a los Rustaceans escribir proyectos
más pequeños ensamblados a partir de varios paquetes.
fn main() {
println!("Guess the number!");
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
Primero añadimos la línea use rand::Rng;. El Rngrasgo define los métodos que
implementan los generadores de números aleatorios, y este rasgo debe estar
dentro del alcance para que podamos usarlos. El capítulo 10 tratará los rasgos en
detalle.
aleatorios que vamos a usar: uno local para el hilo de ejecución actual y generado
por el sistema operativo. Luego, llamamos al gen_range método del generador de
números aleatorios. Este método se define mediante la Rng propiedad que
incorporamos al alcance con la use rand::Rng;instrucción. El gen_rangemétodo
toma una expresión de rango como argumento y genera un número aleatorio
dentro del rango. El tipo de expresión de rango que usamos aquí tiene la
forma start..=endy es inclusivo en los límites inferior y superior, por lo que
debemos especificar 1..=100que se solicite un número entre 1 y 100.
Nota: No solo sabrás qué rasgos usar ni qué métodos y funciones llamar desde un
crate, ya que cada crate incluye documentación con instrucciones para su uso.
Otra característica interesante de Cargo es que al ejecutar el cargo doc --
opencomando, se compilará localmente la documentación proporcionada por
todas tus dependencias y se abrirá en tu navegador. Si te interesan otras
funciones del randcrate, por ejemplo, ejecuta cargo doc --openy haz clic randen la
barra lateral izquierda.
$ 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
Rust
jueves, 22 de mayo de 2025 : Página 35 de 719
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Listado 2-4: Manejo de los posibles valores de retorno de la comparación de dos
números
$ 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}`
| |
Rust
jueves, 22 de mayo de 2025 : Página 37 de 719
| 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 estático y robusto. Sin embargo, también tiene inferencia de tipos. Cuando
escribimos let mut guess = String::new(), Rust pudo inferir que guessdebería ser
a Stringy no nos obligó a escribir el tipo. El secret_number, por otro lado, es un
tipo numérico. Algunos tipos numéricos de Rust pueden tener un valor entre 1 y
100: i32, un número de 32 bits; u32, un número de 32 bits sin signo; i64, un
número de 64 bits; así como otros. A menos que se especifique lo contrario, Rust
usa de forma predeterminada an i32, que es el tipo de secret_numbera menos
que se añada información de tipo en otro lugar que haría que Rust infiriera un tipo
numérico diferente. El motivo del error es que Rust no puede comparar una
cadena y un tipo numérico.
// --snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
La linea es:
Creamos una variable llamada guess. Pero, un momento, ¿el programa no tiene
ya una variable llamada guess? Sí, pero Rust nos permite replicar el valor anterior
de guesscon uno nuevo. El replicado nos permite reutilizar el guess nombre de la
variable en lugar de obligarnos a crear dos variables únicas,
como guess_stry guess, por ejemplo. Abordaremos esto con más detalle en el
Capítulo 3 , pero por ahora, recuerda que esta función se usa a menudo cuando
se desea convertir un valor de un tipo a otro.
$ 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!
Ya funciona la mayor parte del juego, pero el usuario solo puede adivinar una
cosa. ¡Vamos a cambiar eso añadiendo un bucle!
La looppalabra clave crea un bucle infinito. Añadiremos un bucle para que los
usuarios tengan más posibilidades de acertar el número:
// --snip--
loop {
println!("Please input your guess.");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
$ 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
Rust
jueves, 22 de mayo de 2025 : Página 41 de 719
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
Escribir quitcerrará el juego, pero, como notarás, también lo hará cualquier otra
entrada que no sea un número. Esto no es óptimo, como mínimo; queremos que
el juego también se detenga al adivinar el número correcto.
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Añadir la breaklínea después You win!hace que el programa salga del bucle
cuando el usuario acierta el número secreto. Salir del bucle también implica salir
del programa, ya que el bucle es la última parte de main.
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
// --snip--
Listado 2-5: Ignorar una suposición que no es un número y solicitar otra
suposición en lugar de bloquear el programa
suposición. De esta forma, el programa ignora todos los errores que pueda
encontrar.ErrErrOk(num)matchErr(_)_Errcontinueloopparse
$ 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!
fn main() {
println!("Guess the number!");
loop {
println!("Please input your guess.");
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Listado 2-6: Código completo del juego de adivinanzas
Llegados a este punto, has creado el juego de adivinanzas con éxito. ¡Felicidades!
Resumen
Este proyecto fue una forma práctica de presentarte muchos conceptos nuevos
de Rust: letfunciones match, el uso de crates externos y más. En los próximos
capítulos, aprenderás sobre estos conceptos con más detalle. El capítulo 3 abarca
conceptos comunes a la mayoría de los lenguajes de programación, 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 diferencia a Rust de otros
lenguajes. El capítulo 5 trata sobre las estructuras y la sintaxis de los métodos, y
el capítulo 6 explica el funcionamiento de las enumeraciones.
Palabras clave
El lenguaje Rust cuenta con un conjunto de palabras clave reservadas para su uso
exclusivo, al igual que en otros lenguajes. Tenga en cuenta que no puede usar
estas palabras como nombres de variables o funciones. La mayoría de las
palabras clave tienen significados especiales y las usará para realizar diversas
tareas en sus programas Rust; algunas no tienen ninguna funcionalidad asociada
actualmente, pero se han reservado para funciones que podrían añadirse a Rust
en el futuro. Puede encontrar una lista de las palabras clave en el Apéndice A.
Variables y mutabilidad
Cuando una variable es inmutable, una vez que un valor se asocia a un nombre,
no se puede cambiar. Para ilustrar esto, genere un nuevo proyecto
llamado variables en el directorio de proyectos usando cargo new variables.
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Rust
jueves, 22 de mayo de 2025 : Página 46 de 719
Guarde y ejecute el programa con cargo run. Debería recibir un mensaje de error
sobre un error de inmutabilidad, como se muestra en este resultado:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2| let x = 5;
| - first assignment to `x`
3| println!("The value of x is: {x}");
4| x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2| let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
Pero la mutabilidad puede ser muy útil y facilita la escritura del código. Aunque
las variables son inmutables por defecto, puedes hacerlas mutables
añadiendo mutdelante del nombre de la variable, como hiciste en el Capítulo
2. Añadir muttambién transmite la intención a los futuros lectores del código,
indicando que otras partes del código cambiarán el valor de esta variable.
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
Constantes
Al igual que las variables inmutables, las constantes son valores que están
vinculados a un nombre y no pueden cambiar, pero existen algunas diferencias
entre constantes y variables.
Las constantes son válidas durante toda la ejecución de un programa, dentro del
ámbito en el que se declararon. Esta propiedad las hace útiles para valores del
dominio de la aplicación que varias partes del programa podrían necesitar
conocer, como el número máximo de puntos que puede obtener un jugador o la
velocidad de la luz.
Sombreado
Como viste en el tutorial del juego de adivinanzas del Capítulo 2 , puedes declarar
una nueva variable con el mismo nombre que una variable anterior. Los rustáceos
dicen que la primera variable es eclipsada por la segunda, lo que significa que
esta es lo que el compilador verá al usar su nombre. En efecto, la segunda
variable eclipsa a la primera, apropiándose de cualquier uso del nombre de la
Rust
jueves, 22 de mayo de 2025 : Página 49 de 719
variable hasta que se eclipse o finalice su alcance. Podemos eclipsar una variable
usando el mismo nombre de la variable y repitiendo el uso de la letpalabra clave
de la siguiente manera:
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2| let mut spaces = " ";
| ----- expected due to this value
3| spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
Ahora que hemos explorado cómo funcionan las variables, veamos más tipos de
datos que pueden tener.
Tipos de datos
Cada valor en Rust pertenece a un tipo de dato específico , lo que le indica a Rust
qué tipo de datos se especifican para que sepa cómo trabajar con ellos.
Analizaremos dos subconjuntos de tipos de datos: escalares y compuestos.
Tenga en cuenta que Rust es un lenguaje de tipado estático , lo que significa que
debe conocer los tipos de todas las variables en tiempo de compilación. El
Rust
jueves, 22 de mayo de 2025 : Página 51 de 719
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
--> src/main.rs:2:9
|
2| let guess = "42".parse().expect("Not a number!");
| ^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
|
2| let guess: /* Type */ = "42".parse().expect("Not a number!");
| ++++++++++++
For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due
to 1 previous error
Tipos escalares
Un tipo escalar representa un único valor. Rust tiene cuatro tipos escalares
principales: enteros, números de punto flotante, booleanos y caracteres. Quizás
los reconozcas de otros lenguajes de programación. Veamos cómo funcionan en
Rust.
Tipos enteros
asociado debe ser un entero sin signo (los tipos enteros con signo empiezan
por ien lugar de u) que ocupa 32 bits. La Tabla 3-1 muestra los tipos enteros
integrados en Rust. Podemos usar cualquiera de estas variantes para declarar el
tipo de un valor entero.
Longit Firmad No
ud o firmado
8 bits i8 u8
Cada variante puede tener o no signo y un tamaño específico. Con signo y sin
signo se refiere a si el número puede ser negativo; es decir, si necesita un signo
(con signo) o si solo será positivo y, por lo tanto, puede representarse sin signo
(sin signo). Es como escribir números en papel: cuando el signo importa, se
muestra con un signo más o menos; sin embargo, cuando se asume que es
positivo, se muestra sin signo. Los números con signo se almacenan mediante la
representación en complemento a dos .
Literales
Ejemplo
numéricos
Decimal 98_222
Maleficio 0xff
Octal 0o77
0b1111_00
Binario
00
Byte
b'A'
( u8solamente)
¿Cómo saber qué tipo de entero usar? Si no estás seguro, los valores
predeterminados de Rust suelen ser un buen punto de partida: los tipos enteros
tienen como valor predeterminado i32. La situación principal en la que
usarías isize`or` usizees al indexar algún tipo de colección.
Desbordamiento de enteros
Supongamos que tienes una variable de tipo u8que admite valores entre 0 y 255.
Si intentas cambiar la variable a un valor fuera de ese rango, como 256, se
producirá un desbordamiento de enteros , lo que puede provocar uno de dos
comportamientos. Al compilar en modo de depuración, Rust incluye
comprobaciones de desbordamiento de enteros que provocan que tu programa
entre en pánico en tiempo de ejecución si ocurre este comportamiento. Rust
utiliza el término "pánico" cuando un programa finaliza con un error; analizaremos
los pánicos con más detalle en la sección "Errores irrecuperables con panic!" del
capítulo 9.
Rust
jueves, 22 de mayo de 2025 : Página 54 de 719
Rust también tiene dos tipos primitivos para números de punto flotante , que son
números con coma decimal. Los tipos de punto flotante de Rust son f32y f64, que
tienen un tamaño de 32 y 64 bits, respectivamente. El tipo predeterminado
es f64 porque, en las CPU modernas, tiene una velocidad similar a la de , f32pero
mayor precisión. Todos los tipos de punto flotante tienen signo.
fn main() {
let x = 2.0; // f64
Operaciones numéricas
Rust admite las operaciones matemáticas básicas esperadas para todos los tipos
de números: suma, resta, multiplicación, división y resto. La división entera trunca
hacia cero hasta el entero más cercano. El siguiente código muestra cómo usar
cada operación numérica en una letsentencia:
fn main() {
// addition
let sum = 5 + 10;
// subtraction
let difference = 95.5 - 4.3;
// multiplication
let product = 4 * 30;
// division
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // Results in -1
// remainder
let remainder = 43 % 5;
}
El tipo booleano
fn main() {
let t = true;
Rust
jueves, 22 de mayo de 2025 : Página 56 de 719
let f: bool = false; // with explicit type annotation
}
El tipo de personaje
El tipo de Rust chares el tipo alfabético más primitivo del lenguaje. Aquí hay
algunos ejemplos de declaración charde valores:
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}
Tipos de compuestos
Los tipos compuestos pueden agrupar varios valores en un solo tipo. Rust tiene
dos tipos compuestos primitivos: tuplas y arrays.
El tipo tupla
Una tupla es una forma general de agrupar varios valores con diversos tipos en
un tipo compuesto. Las tuplas tienen una longitud fija: una vez declaradas, su
tamaño no puede aumentar ni disminuir.
Rust
jueves, 22 de mayo de 2025 : Página 57 de 719
Creamos una tupla escribiendo una lista de valores separados por comas entre
paréntesis. Cada posición en la tupla tiene un tipo, y los tipos de los diferentes
valores no tienen por qué ser iguales. En este ejemplo, hemos añadido
anotaciones de tipo opcionales:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
fn main() {
let tup = (500, 6.4, 1);
Este programa primero crea una tupla y la vincula a la variable tup. Luego, usa un
patrón con letpara tomarla tupy convertirla en tres variables
independientes: x, yy z. Esto se denomina desestructuración porque divide la
tupla en tres partes. Finalmente, el programa imprime el valor de y, que es 6.4.
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
Este programa crea la tupla xy accede a cada elemento mediante sus respectivos
índices. Como en la mayoría de los lenguajes de programación, el primer índice
de una tupla es 0.
La tupla sin valores tiene un nombre especial, unidad . Este valor y su tipo
correspondiente se escriben ()y representan un valor vacío o un tipo de retorno
vacío. Las expresiones devuelven implícitamente el valor de la unidad si no
devuelven ningún otro valor.
El tipo de matriz
Escribimos los valores en una matriz como una lista separada por comas dentro
de corchetes:
fn main() {
let a = [1, 2, 3, 4, 5];
}
Los arrays son útiles cuando se desea que los datos se asignen en la pila, al igual
que los otros tipos que hemos visto hasta ahora, en lugar del montón (hablaremos
más sobre la pila y el montón en el Capítulo 4 ) o cuando se desea asegurar un
número fijo de elementos. Sin embargo, un array no es tan flexible como el tipo
vector. Un vector es un tipo de colección similar proporcionado por la biblioteca
estándar, cuyo tamaño puede aumentar o disminuir. Si no está seguro de si usar
un array o un vector, probablemente deba usar un vector. El Capítulo 8 trata los
vectores con más detalle.
Sin embargo, los arrays son más útiles cuando se sabe que el número de
elementos no tendrá que cambiar. Por ejemplo, si se usaran los nombres de los
meses en un programa, probablemente se usaría un array en lugar de un vector,
ya que se sabe que siempre contendrá 12 elementos:
Rust
jueves, 22 de mayo de 2025 : Página 59 de 719
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
El tipo de una matriz se escribe utilizando corchetes con el tipo de cada elemento,
un punto y coma y luego el número de elementos en la matriz, de la siguiente
manera:
Aquí i32se muestra el tipo de cada elemento. Después del punto y coma, el
número 5 indica que la matriz contiene cinco elementos.
También puede inicializar una matriz para que contenga el mismo valor para cada
elemento especificando el valor inicial, seguido de un punto y coma y luego la
longitud de la matriz entre corchetes, como se muestra aquí:
fn main() {
let a = [1, 2, 3, 4, 5];
Veamos qué sucede si intentas acceder a un elemento de un array que está más
allá del final del array. Supongamos que ejecutas este código, similar al juego de
adivinanzas del Capítulo 2, para obtener el índice del array del usuario:
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
Rust comprobará que el índice especificado sea menor que la longitud del array.
Si el índice es mayor o igual a la longitud, Rust generará un error. Esta
comprobación debe realizarse en tiempo de ejecución, especialmente en este
caso, ya que el compilador no puede saber qué valor introducirá el usuario al
ejecutar el código posteriormente.
Funciones
Las funciones son comunes en el código de Rust. Ya has visto una de las
funciones más importantes del lenguaje: la mainfunción, que es el punto de
entrada de muchos programas. También has visto la fnpalabra clave, que permite
declarar nuevas funciones.
El código de Rust utiliza el formato Snake Case como estilo convencional para los
nombres de funciones y variables, donde todas las letras están en minúscula y se
subrayan las palabras por separado. A continuación, se muestra un programa con
un ejemplo de definición de función:
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
Parámetros
Podemos definir funciones con parámetros , que son variables especiales que
forman parte de la firma de una función. Cuando una función tiene parámetros, se
le pueden asignar valores concretos. Técnicamente, estos valores concretos se
denominan argumentos , pero en la conversación informal, se suelen usar los
términos parámetro y argumento indistintamente para referirse tanto a las
variables en la definición de una función como a los valores concretos que se
pasan al llamar a una función.
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
Rust
jueves, 22 de mayo de 2025 : Página 63 de 719
}
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
fn main() {
print_labeled_measurement(5, 'h');
}
$ cargo run
Rust
jueves, 22 de mayo de 2025 : Página 64 de 719
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
Debido a que llamamos a la función con 5como valor para valuey 'h'como valor
para unit_label, la salida del programa contiene esos valores.
Declaraciones y expresiones
fn main() {
let x = (let y = 6);
}
Cuando ejecutes este programa, el error que obtendrás será similar al siguiente:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2| let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
que es una expresión que se evalúa como el valor 11. Las expresiones pueden
formar parte de sentencias: en el Listado 3-1, el 6en la sentencia let y = 6;es una
expresión que se evalúa como el valor 6. Llamar a una función es una expresión.
Llamar a una macro es una expresión. Un nuevo bloque de ámbito creado entre
llaves es una expresión, por ejemplo:
fn main() {
let y = {
let x = 3;
x+1
};
Esta expresión:
{
let x = 3;
x+1
}
Es un bloque que, en este caso, evalúa como 4. Ese valor se vincula a y como
parte de la letsentencia. Tenga en cuenta que la x + 1línea no tiene punto y coma
al final, a diferencia de la mayoría de las líneas que ha visto hasta ahora. Las
expresiones no incluyen punto y coma al final. Si añade un punto y coma al final
de una expresión, la convierte en una sentencia y, por lo tanto, no devolverá
ningún valor. Tenga esto en cuenta al explorar los valores de retorno de funciones
y expresiones a continuación.
Las funciones pueden devolver valores al código que las invoca. No se nombran
los valores de retorno, pero se debe declarar su tipo después de una flecha ( ->).
En Rust, el valor de retorno de la función es sinónimo del valor de la expresión
final en el bloque del cuerpo de la función. Se puede regresar antes de una
función usando la returnpalabra clave y especificando un valor, pero la mayoría
de las funciones devuelven la última expresión implícitamente. A continuación, se
muestra un ejemplo de una función que devuelve un valor:
Rust
jueves, 22 de mayo de 2025 : Página 67 de 719
fn main() {
let x = five();
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5
El 5in fivees el valor de retorno de la función, por lo que el tipo de retorno es i32.
Analicemos esto con más detalle. Hay dos aspectos importantes: primero, la
línea let x = five();muestra que estamos usando el valor de retorno de una
función para inicializar una variable. Dado que la función fivedevuelve un 5, esa
línea es igual a la siguiente:
let x = 5;
fn main() {
let x = plus_one(5);
fn main() {
let x = plus_one(5);
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8| x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error
Comentarios
Todos los programadores se esfuerzan por que su código sea fácil de entender,
pero a veces se justifica una explicación adicional. En estos casos, los
programadores dejan comentarios en su código fuente que el compilador
ignorará, pero que quienes lo lean pueden encontrar útiles.
// hello, world
Los comentarios también se pueden colocar al final de las líneas que contienen
código:
fn main() {
let lucky_number = 7; // I’m feeling lucky today
}
Pero con más frecuencia los verás utilizados en este formato, con el comentario
en una línea separada encima del código que está anotando:
fn main() {
// I’m feeling lucky today
let lucky_number = 7;
}
Rust
jueves, 22 de mayo de 2025 : Página 70 de 719
Flujo de control
ifExpresiones
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
Todas iflas expresiones comienzan con la palabra clave if, seguida de una
condición. En este caso, la condición comprueba si la variable numbertiene un
valor menor que 5. Colocamos el bloque de código que se ejecutará si la
condición aparece trueinmediatamente después de la condición entre llaves. Los
bloques de código asociados con las condiciones en iflas expresiones a veces se
denominan brazos , al igual que los brazos en matchlas expresiones que
analizamos en la sección "Comparación de la suposición con el número
secreto" del Capítulo 2.
Rust
jueves, 22 de mayo de 2025 : Página 71 de 719
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
let number = 7;
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was false
También cabe destacar que la condición en este código debe ser un bool. Si no lo
es bool, se generará un error. Por ejemplo, intente ejecutar el siguiente código:
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}
Al ejecutar este código se imprimirá number was something other than zero.
Puedes usar varias condiciones combinando " ify" elseen una else if expresión. Por
ejemplo:
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
Rust
jueves, 22 de mayo de 2025 : Página 73 de 719
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
Este programa tiene cuatro rutas posibles. Tras ejecutarlo, debería ver el
siguiente resultado:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3
Usar demasiadas else ifexpresiones puede saturar el código, así que si tienes más
de una, quizás quieras refactorizarlo. El capítulo 6 describe una potente
construcción de ramificación de Rust necesaria matchpara estos casos.
Recuerde que los bloques de código se evalúan hasta su última expresión, y los
números por sí mismos también son expresiones. En este caso, el valor de toda
la ifexpresión depende del bloque de código que se ejecute. Esto significa que los
valores que potencialmente podrían ser resultados de cada brazo de [nombre del
grupo] ifdeben ser del mismo tipo; en el Listado 3-2, los resultados de
ambos ifbrazos else eran i32enteros. Si los tipos no coinciden, como en el
siguiente ejemplo, se generará un error:
fn main() {
let condition = true;
Al intentar compilar este código, obtendremos un error. Los tipos de valor ifde
los elsebrazos y son incompatibles, y Rust indica exactamente dónde encontrar el
problema en el programa:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:44
|
4| let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 75 de 719
Suele ser útil ejecutar un bloque de código más de una vez. Para ello, Rust ofrece
varios bucles que recorren el código dentro del cuerpo del bucle hasta el final y
luego vuelven a empezar inmediatamente desde el principio. Para experimentar
con bucles, creemos un nuevo proyecto llamado loops .
Rust tiene tres tipos de bucles: loop, while, y for. Probemos cada uno.
La looppalabra clave le dice a Rust que ejecute un bloque de código una y otra
vez para siempre o hasta que le indiques explícitamente que se detenga.
fn main() {
loop {
println!("again!");
}
}
$ cargo run
Rust
jueves, 22 de mayo de 2025 : Página 76 de 719
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
Uno de los usos de a loopes reintentar una operación que sabes que podría fallar,
como comprobar si un hilo ha completado su trabajo. También podrías necesitar
pasar el resultado de esa operación fuera del bucle al resto del código. Para ello,
puedes añadir el valor que quieres que se devuelva después de la breakexpresión
que usas para detener el bucle; ese valor se devolverá fuera del bucle para que
puedas usarlo, como se muestra aquí:
fn main() {
let mut counter = 0;
if counter == 10 {
break counter * 2;
}
};
Rust
jueves, 22 de mayo de 2025 : Página 77 de 719
println!("The result is {result}");
}
Si tiene bucles dentro de bucles breaky continuese aplica al bucle más interno en
ese punto, puede especificar opcionalmente una etiqueta de bucle en un bucle
que luego puede usar con breako continuepara especificar que esas palabras
clave se apliquen al bucle etiquetado en lugar del bucle más interno. Las
etiquetas de bucle deben comenzar con una comilla simple. A continuación, se
muestra un ejemplo con dos bucles anidados:
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
Rust
jueves, 22 de mayo de 2025 : Página 78 de 719
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
Rust
jueves, 22 de mayo de 2025 : Página 79 de 719
Listado 3-3: Uso de un whilebucle para ejecutar código mientras una condición es
verdadera
index += 1;
}
}
Listado 3-4: Recorrer cada elemento de una colección usando un whilebucle
Aquí, el código cuenta los elementos del array. Comienza en el índice 0y luego se
repite hasta llegar al último índice del array (es decir, cuando ya index < 5no
es true). Al ejecutar este código, se imprimirán todos los elementos del array:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
Como alternativa más concisa, puede usar un forbucle y ejecutar código para
cada elemento de una colección. Un forbucle se parece al código del Listado 3-5.
for element in a {
println!("the value is: {element}");
}
}
Listado 3-5: Recorrer cada elemento de una colección usando un forbucle
Al ejecutar este código, veremos el mismo resultado que en el Listado 3-4. Más
importante aún, hemos aumentado la seguridad del código y eliminado la
posibilidad de errores que podrían surgir al sobrepasar el final del array o al no ir
lo suficientemente lejos y omitir algunos elementos.
Así es como se vería la cuenta regresiva usando un forbucle y otro método del
que aún no hemos hablado, rev, para invertir el rango:
Rust
jueves, 22 de mayo de 2025 : Página 81 de 719
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
Resumen
¡Lo lograste! Este capítulo fue extenso: aprendiste sobre variables, tipos de datos
escalares y compuestos, funciones, comentarios, ifexpresiones y bucles. Para
practicar los conceptos de este capítulo, intenta crear programas que hagan lo
siguiente:
Cuando esté listo para continuar, hablaremos sobre un concepto en Rust que
comúnmente no existe en otros lenguajes de programación: propiedad.
Entendiendo la propiedad
¿Qué es la propiedad?
Cuando comprendas la propiedad, tendrás una base sólida para comprender las
características que hacen único a Rust. En este capítulo, aprenderás sobre la
propiedad mediante ejemplos centrados en una estructura de datos muy común:
las cadenas.
La pila y el montón
Tanto la pila como el montón son partes de la memoria disponibles para que tu
código las use en tiempo de ejecución, pero están estructuradas de diferentes
maneras. La pila almacena los valores en el orden en que los obtiene y los elimina
en el orden opuesto. Esto se conoce como último en entrar, primero en salir .
Piensa en una pila de platos: cuando agregas más platos, los pones en la parte
superior de la pila y, cuando necesitas un plato, tomas uno de la parte superior.
¡Agregar o quitar platos del medio o de la parte inferior no funcionaría tan bien!
Agregar datos se llama empujar hacia la pila , y quitar datos se llama sacar de la
pila . Todos los datos almacenados en la pila deben tener un tamaño conocido y
fijo. Los datos con un tamaño desconocido en tiempo de compilación o un tamaño
que pueda cambiar se deben almacenar en el montón.
Acceder a los datos en el montón es más lento que a los de la pila, ya que se
debe seguir un puntero para llegar a ellos. Los procesadores actuales son más
rápidos si saltan menos en la memoria. Siguiendo con la analogía, considere a un
camarero de un restaurante que toma pedidos de varias mesas. Es más eficiente
recibir todos los pedidos de una mesa antes de pasar a la siguiente. Tomar un
pedido de la mesa A, luego uno de la mesa B, luego otro de la A y luego otro de la
B sería un proceso mucho más lento. Del mismo modo, un procesador puede
funcionar mejor si trabaja con datos cercanos a otros (como en la pila) en lugar de
más alejados (como en el montón).
Monitorear qué partes del código usan qué datos en el montón, minimizar la
cantidad de datos duplicados y limpiar los datos no utilizados para no quedarse
sin espacio son problemas que la propiedad aborda. Una vez que comprenda la
propiedad, no necesitará pensar mucho en la pila ni en el montón, pero saber que
el propósito principal de la propiedad es administrar los datos del montón puede
ayudar a explicar por qué funciona como lo hace.
Rust
jueves, 22 de mayo de 2025 : Página 84 de 719
Reglas de propiedad
Alcance variable
let s = "hello";
// do stuff with s
} // this scope is now over, and s is no longer valid
Listado 4-1: Una variable y el ámbito en el que es válida
En este punto, la relación entre los ámbitos y la validez de las variables es similar
a la de otros lenguajes de programación. A partir de este conocimiento,
introduciremos el Stringtipo.
El Stringtipo
Para ilustrar las reglas de propiedad, necesitamos un tipo de dato más complejo
que los que vimos en la sección "Tipos de Datos" del Capítulo 3. Los tipos
mencionados anteriormente tienen un tamaño conocido, pueden almacenarse en
la pila y extraerse de ella al final de su alcance, y pueden copiarse rápida y
fácilmente para crear una nueva instancia independiente si otra parte del código
necesita usar el mismo valor en un alcance diferente. Sin embargo, queremos
analizar los datos almacenados en el montón y explorar cómo Rust sabe cuándo
limpiarlos, y este Stringtipo es un excelente ejemplo.
let s = String::from("hello");
::El operador de dos puntos nos permite crear un espacio de nombres para
esta from función en particular bajo el Stringtipo, en lugar de usar un nombre
como string_from. Analizaremos esta sintaxis con más detalle en la
sección "Sintaxis de métodos" del capítulo 5, y cuando tratemos el espacio de
nombres con módulos en "Rutas para hacer referencia a un elemento en el árbol
de módulos" del capítulo 7.
Rust
jueves, 22 de mayo de 2025 : Página 86 de 719
Memoria y asignación
{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no
// longer valid
En Rust, varias variables pueden interactuar con los mismos datos de diferentes
maneras. Veamos un ejemplo con un entero en el Listado 4-2.
let x = 5;
let y = x;
Listado 4-2: Asignación del valor entero de la variable xay
Rust
jueves, 22 de mayo de 2025 : Página 88 de 719
let s1 = String::from("hello");
let s2 = s1;
Esto parece muy similar, por lo que podríamos suponer que funciona de la misma
manera: es decir, la segunda línea copiaría el valor en s1y lo vincularía a s2. Pero
esto no es exactamente lo que ocurre.
Observe la Figura 4-1 para ver qué sucede en Stringel subconjunto. A Stringse
compone de tres partes, que se muestran a la izquierda: un puntero a la memoria
que contiene el contenido de la cadena, una longitud y una capacidad. Este grupo
de datos se almacena en la pila. A la derecha se encuentra la memoria en el
montón que contiene el contenido.
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
Recibirás un error como este porque Rust te impide usar la referencia invalidada:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:15
|
2| let s1 = String::from("hello");
Rust
jueves, 22 de mayo de 2025 : Página 90 de 719
| -- move occurs because `s1` has type `String`, which does not implement
the `Copy` trait
3| let s2 = s1;
| -- value moved here
4|
5| println!("{s1}, world!");
| ^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes
from the expansion of the macro `println` (in Nightly builds, run with -Z macro-
backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3| let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Si has oído los términos " copia superficial" y "copia profunda" al trabajar con
otros lenguajes, el concepto de copiar el puntero, la longitud y la capacidad sin
copiar los datos probablemente te suene a una copia superficial. Sin embargo,
como Rust también invalida la primera variable, en lugar de llamarse copia
superficial, se conoce como un movimiento . En este ejemplo, diríamos
que s1 se movió a s2. Por lo tanto, lo que realmente ocurre se muestra en la
Figura 4-4.
¡Eso soluciona nuestro problema! Con solo s2válido, cuando se sale del ámbito,
solo así se liberará la memoria, y listo.
Alcance y asignación
Rust
jueves, 22 de mayo de 2025 : Página 91 de 719
println!("{s}, world!");
Si queremos copiar en profundidad los datos del montón de String, no solo los de
la pila, podemos usar un método común llamado clone. Analizaremos la sintaxis
de los métodos en el capítulo 5, pero como los métodos son una característica
común en muchos lenguajes de programación, probablemente ya los hayas visto.
let s1 = String::from("hello");
let s2 = s1.clone();
Cuando ves una llamada a clone, sabes que se está ejecutando código arbitrario,
y que ese código puede ser costoso. Es un indicador visual de que algo diferente
está sucediendo.
Hay otro detalle que aún no hemos abordado. Este código que usa enteros (parte
del cual se mostró en el Listado 4-2) funciona y es válido:
let x = 5;
let y = x;
La razón es que tipos como los enteros, que tienen un tamaño conocido en tiempo
de compilación, se almacenan completamente en la pila, por lo que las copias de
los valores reales se crean rápidamente. Esto significa que no hay razón para que
queramos impedir xque sean válidos después de crear la variable y. En otras
palabras, no hay diferencia entre copia profunda y superficial, por lo que la
llamada cloneno tendría ningún efecto diferente a la copia superficial habitual, y
podemos omitirla.
Rust cuenta con una anotación especial llamada Copytrait que podemos aplicar a
tipos almacenados en la pila, como los enteros (hablaremos más sobre traits en el
Capítulo 10 ). Si un tipo implementa el Copy trait, las variables que lo usan no se
mueven, sino que se copian de forma trivial, lo que las hace válidas tras
asignarlas a otra variable.
Rust no nos permite anotar un tipo Copysi este, o alguna de sus partes, ha
implementado la Dropcaracterística. Si el tipo requiere que ocurra algo especial
cuando el valor queda fuera del ámbito y añadimos la Copyanotación,
obtendremos un error de compilación. Para saber cómo añadir la Copyanotación a
tu tipo para implementar la característica, consulta "Características
Derivables" en el Apéndice C.
Propiedad y funciones
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.
queda fuera del alcance, el valor se borrará, dropa menos que la propiedad de los
datos se haya movido a otra variable.
Si bien esto funciona, tomar y devolver la propiedad con cada función es un poco
tedioso. ¿Qué pasa si queremos que una función use un valor, pero no que tome
la propiedad? Es bastante molesto que todo lo que pasamos también tenga que
devolverse si queremos usarlo de nuevo, además de cualquier dato resultante del
cuerpo de la función que queramos devolver.
Rust nos permite devolver múltiples valores usando una tupla, como se muestra
en el Listado 4-5.
(s, length)
}
Listado 4-5: Devolución de la propiedad de los parámetros
Pero esto es demasiada ceremonia y mucho trabajo para un concepto que debería
ser común. Por suerte, Rust cuenta con una función para usar un valor sin
transferir la propiedad, llamada referencias .
Referencias y préstamos
El problema con el código de tupla del Listado 4-5 es que debemos devolver
`the` Stringa la función que lo llama para poder seguir usando
`the` Stringdespués de llamar a `to` calculate_length, ya que `the` Stringse
movió a calculate_length`. En su lugar, podemos proporcionar una referencia
al Stringvalor. Una referencia es como un puntero, ya que es una dirección que
podemos seguir para acceder a los datos almacenados en ella; esos datos
pertenecen a otra variable. A diferencia de un puntero, se garantiza que una
referencia apuntará a un valor válido de un tipo específico durante su vigencia.
Rust
jueves, 22 de mayo de 2025 : Página 96 de 719
let s1 = String::from("hello");
La &s1sintaxis permite crear una referencia que remite al valor de, s1 pero no lo
posee. Dado que la referencia no lo posee, el valor al que apunta no se eliminará
cuando la referencia deje de usarse.
Rust
jueves, 22 de mayo de 2025 : Página 97 de 719
Asimismo, la firma de la función suele &indicar que el tipo del parámetro ses una
referencia. Añadamos algunas anotaciones explicativas:
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
Listado 4-6: Intento de modificar un valor prestado
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&`
reference
--> src/main.rs:8:5
|
8| some_string.push_str(", world");
Rust
jueves, 22 de mayo de 2025 : Página 98 de 719
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to
cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Así como las variables son inmutables por defecto, también lo son las referencias.
No podemos modificar algo a lo que tenemos una referencia.
Referencias mutables
Podemos arreglar el código del Listado 4-6 para permitirnos modificar un valor
prestado con solo unos pequeños ajustes que utilizan, en su lugar, una referencia
mutable :
change(&mut s);
}
Primero, cambiamos sa mut. Luego, creamos una referencia mutable con &mut
sdonde llamamos a la changefunción y actualizamos la firma de la función para
que acepte una referencia mutable con some_string: &mut String. Esto deja claro
que la changefunción mutará el valor que toma prestado.
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4| let r1 = &mut s;
| ------ first mutable borrow occurs here
5| let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6|
7| println!("{}, {}", r1, r2);
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Este error indica que este código no es válido porque no podemos tomar
prestado scomo mutable más de una vez. El primer préstamo mutable está en
[nombre del archivo] r1y debe durar hasta que se use en [nombre del
archivo] println!, pero entre la creación de esa referencia mutable y su uso,
intentamos crear otra referencia mutable en [nombre del archivo] r2que toma
prestados los mismos datos que [nombre del archivo] r1.
Como siempre, podemos usar llaves para crear un nuevo ámbito, lo que permite
múltiples referencias mutables, pero no simultáneas :
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no
problems.
let r2 = &mut s;
Rust aplica una regla similar para combinar referencias mutables e inmutables.
Este código genera un error:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
--> src/main.rs:6:14
|
4| let r1 = &s; // no problem
| -- immutable borrow occurs here
5| let r2 = &s; // no problem
6| let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7|
8| println!("{}, {}, and {}", r1, r2, r3);
| -- immutable borrow later used here
Rust
jueves, 22 de mayo de 2025 : Página 101 de 719
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
¡Uf! Tampoco podemos tener una referencia mutable si tenemos una inmutable al
mismo valor.
Aunque los errores de préstamo pueden ser frustrantes a veces, recuerda que es
el compilador de Rust el que señala un posible error con antelación (en tiempo de
compilación, no de ejecución) y te muestra exactamente dónde está el problema.
Así, no tendrás que buscar por qué tus datos no son lo que creías.
Referencias colgantes
Intentemos crear una referencia colgante para ver cómo Rust los evita con un
error en tiempo de compilación:
fn main() {
let reference_to_nothing = dangle();
}
&s
}
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no
value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're
returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|
Este mensaje de error se refiere a una característica que aún no hemos abordado:
los tiempos de vida. Los analizaremos en detalle en el capítulo 10. Sin embargo, si
ignoramos la información sobre los tiempos de vida, el mensaje contiene la clave
del problema de este código:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
Dado que sse crea dentro de [nombre del archivo] , al finalizar dangleel código de
[ nombre del archivo], se desasignará. Pero intentamos devolver una referencia a
él. Esto significa que esta referencia estaría apuntando a un [nombre del archivo]
no válido . ¡Eso es un error! Rust no nos permite hacerlo.danglesString
s
}
El tipo de rebanada
Aquí hay un pequeño problema de programación: escribe una función que tome
una cadena de palabras separadas por espacios y devuelva la primera palabra
que encuentre en ella. Si la función no encuentra ningún espacio en la cadena, la
cadena completa debe ser una sola palabra, por lo que se debe devolver la
cadena completa.
Analicemos cómo escribiríamos la firma de esta función sin usar porciones, para
comprender el problema que resolverán las porciones:
s.len()
}
Listado 4-7: La first_wordfunción que devuelve un valor de índice de byte en
el Stringparámetro
Analizaremos los iteradores con más detalle en el Capítulo 13. Por ahora, sepa
que iteres un método que devuelve cada elemento de una colección y
que enumerateencapsula el resultado de itercada elemento, devolviéndolo como
parte de una tupla. El primer elemento de la tupla devuelta enumeratees el
índice, y el segundo elemento es una referencia al elemento. Esto es un poco más
práctico que calcular el índice nosotros mismos.
Dado que el enumeratemétodo devuelve una tupla, podemos usar patrones para
desestructurarla. Analizaremos los patrones con más detalle en el Capítulo 6. En
el forbucle, especificamos un patrón que tiene i para el índice de la tupla
y &itempara el byte único de la tupla. Dado que obtenemos una referencia al
elemento de [nombre de la tupla] .iter().enumerate(), usamos [nombre de
la &tupla] en el patrón.
s.len()
Rust
jueves, 22 de mayo de 2025 : Página 106 de 719
Ahora tenemos una forma de averiguar el índice del final de la primera palabra de
la cadena, pero hay un problema. Estamos devolviendo `a` usizepor sí solo, pero
solo es un número significativo en el contexto de ` &String. En otras palabras, al
ser un valor independiente de ` String, no hay garantía de que siga siendo válido
en el futuro. Considere el programa del Listado 4-8 que usa la first_wordfunción
del Listado 4-7.
// `word` still has the value `5` here, but `s` no longer has any content
// that we could meaningfully use with the value `5`, so `word` is now
// totally invalid!
}
Listado 4-8: Almacenamiento del resultado de llamar a la first_wordfunción y
luego cambiar el Stringcontenido
Preocuparse de que el índice wordse desincronice con los datos ses tedioso y
propenso a errores. Gestionar estos índices es aún más complejo si escribimos
una second_wordfunción. Su firma debería ser similar a esta:
Ahora rastreamos un índice inicial y uno final, y tenemos aún más valores
calculados a partir de datos de un estado específico, pero que no están vinculados
a él. Tenemos tres variables no relacionadas que deben mantenerse
sincronizadas.
Una porción de cadena es una referencia a una parte de una Stringy se ve así:
Con la sintaxis de rango de Rust .., si desea comenzar en el índice 0, puede omitir
el valor antes de los dos puntos. En otras palabras, son iguales:
let s = String::from("hello");
let s = String::from("hello");
También puedes omitir ambos valores para tomar una porción de la cadena
completa. Por lo tanto, son iguales:
let s = String::from("hello");
&s[..]
}
Ahora tenemos una API sencilla y mucho más difícil de cometer errores, ya que el
compilador garantizará que las referencias a [insert name] Stringsigan siendo
válidas. ¿Recuerdan el error del programa del Listado 4-8, cuando obtuvimos el
índice hasta el final de la primera palabra, pero luego borramos la cadena, por lo
que nuestro índice no era válido? Ese código era lógicamente incorrecto, pero no
mostró ningún error inmediato. Los problemas aparecerían más adelante si
seguíamos intentando usar el índice de la primera palabra con una cadena vacía.
Las porciones evitan este error y nos avisan mucho antes de que tenemos un
problema con nuestro código. Usar la versión de porciones de [insert
name] first_wordgenerará un error en tiempo de compilación:
fn main() {
let mut s = String::from("hello world");
s.clear(); // error!
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
--> src/main.rs:18:5
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
17 |
18 | s.clear(); // error!
Rust
jueves, 22 de mayo de 2025 : Página 110 de 719
| ^^^^^^^^^ mutable borrow occurs here
19 |
20 | println!("the first word is: {word}");
| ------ immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Recuerda que, según las reglas de préstamo, si tenemos una referencia inmutable
a algo, no podemos tomar también una referencia mutable. Dado
que clearnecesita truncarse String, necesita obtener una referencia mutable. El
[nombre de println! la referencia] después de la llamada a clearusa la referencia
en [nombre de la referencia] word, por lo que la referencia inmutable debe seguir
activa en ese momento. Rust impide que la referencia mutable en [nombre de la
referencia] cleary la referencia inmutable en [ wordnombre de la referencia]
existan simultáneamente, y la compilación falla. Rust no solo ha simplificado el
uso de nuestra API, sino que también ha eliminado toda una clase de errores en
tiempo de compilación.
El tipo de saquí es &str: es una porción que apunta a ese punto específico del
binario. Esta es también la razón por la que los literales de cadena son
inmutables; &stres una referencia inmutable.
Saber que puedes tomar porciones de literales y Stringvalores nos lleva a una
mejora más en first_word, y es su firma:
Definir una función para tomar una porción de cadena en lugar de una referencia
a String hace que nuestra API sea más general y útil sin perder ninguna
funcionalidad:
Otras rebanadas
Como puedes imaginar, las porciones de cadena son específicas de las cadenas.
Pero también existe un tipo de porción más general. Considera esta matriz:
Al igual que podríamos querer referirnos a una parte de una cadena, podríamos
querer referirnos a una parte de un array. Lo haríamos así:
Rust
jueves, 22 de mayo de 2025 : Página 112 de 719
let a = [1, 2, 3, 4, 5];
Resumen
Las estructuras son similares a las tuplas, que se describen en la sección "El tipo
de tupla" , ya que ambas contienen múltiples valores relacionados. Al igual que
las tuplas, las partes de una estructura pueden ser de diferentes tipos. A
diferencia de las tuplas, en una estructura se asigna un nombre a cada dato para
que quede claro el significado de los valores. Añadir estos nombres significa que
las estructuras son más flexibles que las tuplas: no es necesario depender del
orden de los datos para especificar o acceder a los valores de una instancia.
Para usar una estructura después de definirla, creamos una instancia de la misma
especificando valores concretos para cada campo. Creamos la instancia indicando
el nombre de la estructura y luego añadimos llaves que contienen pares clave-
valor , donde las claves son los nombres de los campos y los valores los datos que
queremos almacenar en ellos. No es necesario especificar los campos en el
mismo orden en que los declaramos en la estructura. En otras palabras, la
definición de la estructura funciona como una plantilla general para el tipo, y las
instancias la completan con datos específicos para crear valores del tipo. Por
ejemplo, podemos declarar un usuario específico como se muestra en el Listado
5-2.
user1.email = String::from("[email protected]");
}
Listado 5-3: Cambiar el valor en el emailcampo de una Userinstancia
Tenga en cuenta que toda la instancia debe ser mutable; Rust no permite marcar
solo ciertos campos como mutables. Como con cualquier expresión, podemos
construir una nueva instancia de la estructura como última expresión en el cuerpo
de la función para devolverla implícitamente.
El Listado 5-4 muestra una build_userfunción que devuelve una Userinstancia con
el correo electrónico y el nombre de usuario especificados. El activecampo
obtiene el valor de true, y el sign_in_countobtiene el valor de 1.
Tiene sentido nombrar los parámetros de la función con el mismo nombre que los
campos de la estructura, pero tener que repetir los emailnombres usernamede los
campos y las variables es un poco tedioso. Si la estructura tuviera más campos,
repetir cada nombre sería aún más molesto. ¡Por suerte, existe una abreviatura
muy práctica!
Debido a que los nombres de los parámetros y los nombres de los campos de la
estructura son exactamente los mismos en el Listado 5-4, podemos usar la
sintaxis abreviada de field init para reescribir build_userde modo que se comporte
exactamente igual pero no tenga la repetición de usernamey email, como se
muestra en el Listado 5-5.
Suele ser útil crear una nueva instancia de una estructura que incluya la mayoría
de los valores de otra instancia, pero modifique algunos. Puede hacerlo usando la
sintaxis de actualización de estructura .
El código del Listado 5-7 también crea una instancia en user2que tiene un valor
diferente para, emailpero con los mismos valores para los
campos username, active, y sign_in_countde user1. ..user1Debe aparecer al final
para especificar que los campos restantes deben obtener sus valores de los
campos correspondientes en user1, pero podemos especificar valores para todos
los campos que queramos en cualquier orden, independientemente del orden de
los campos en la definición de la estructura.
Rust
jueves, 22 de mayo de 2025 : Página 117 de 719
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Tenga en cuenta que los valores blacky originson de tipos diferentes porque son
instancias de estructuras de tuplas diferentes. Cada estructura que defina tiene
su propio tipo, aunque los campos dentro de ella puedan tener el mismo tipo. Por
ejemplo, una función que toma un parámetro de tipo Colorno puede tomar
Rust
jueves, 22 de mayo de 2025 : Página 118 de 719
fn main() {
let subject = AlwaysEqual;
}
que queremos que cada instancia de esta estructura posea todos sus datos y que
estos sean válidos mientras la estructura completa lo sea.
struct User {
active: bool,
username: &str,
email: &str,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: "someusername123",
email: "[email protected]",
sign_in_count: 1,
};
}
$ cargo run
Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3| username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2| active: bool,
3~ username: &'a str,
Rust
jueves, 22 de mayo de 2025 : Página 120 de 719
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` (bin "structs") due to 2 previous errors
En el Capítulo 10, analizaremos cómo solucionar estos errores para que pueda
almacenar referencias en estructuras, pero por ahora, solucionaremos errores
como estos utilizando tipos propios como Stringen lugar de referencias como &str.
Creemos un nuevo proyecto binario con Cargo llamado rectángulos. Este tomará
el ancho y la altura de un rectángulo especificado en píxeles y calculará su área.
El Listado 5-8 muestra un programa corto con una forma de hacer exactamente
eso en el archivo src/main.rs de nuestro proyecto .
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
Rust
jueves, 22 de mayo de 2025 : Página 121 de 719
fn area(width: u32, height: u32) -> u32 {
width * height
}
Listado 5-8: Cálculo del área de un rectángulo especificado por variables de ancho
y alto independientes
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
Este código logra determinar el área del rectángulo llamando a la areafunción con
cada dimensión, pero podemos hacer más para que este código sea claro y
legible.
El listado 5-9 muestra otra versión de nuestro programa que utiliza tuplas.
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
En cierto modo, este programa es mejor. Las tuplas nos permiten añadir algo de
estructura, y ahora solo pasamos un argumento. Pero, por otro lado, esta versión
es menos clara: las tuplas no nombran sus elementos, por lo que debemos
indexar las partes de la tupla, lo que hace que nuestro cálculo sea menos obvio.
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
Cuando compilamos este código, obtenemos un error con este mensaje principal:
La println!macro puede realizar muchos tipos de formato y, por defecto, las llaves
indican println!que se use el formato conocido como Display: output, destinado al
consumo directo del usuario final. Los tipos primitivos que hemos visto hasta
ahora se implementan Displaypor defecto porque solo hay una forma de mostrar
a 1o cualquier otro tipo primitivo a un usuario. Sin embargo, con las estructuras,
la forma println!de formatear la salida es menos clara porque hay más
posibilidades de visualización: ¿Se quieren comas o no? ¿Se quieren imprimir las
llaves? ¿Se deben mostrar todos los campos? Debido a esta ambigüedad, Rust no
intenta adivinar lo que queremos, y las estructuras no tienen una implementación
proporcionada de Displaypara usar con println!y el {}marcador de posición.
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1:?}");
}
Listado 5-12: Agregar el atributo para derivar el Debugrasgo e imprimir
la Rectangleinstancia usando formato de depuración
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
¡Genial! No es la salida más atractiva, pero muestra los valores de todos los
campos de esta instancia, lo que sin duda sería útil durante la depuración.
Cuando tenemos estructuras más grandes, es útil tener una salida más legible; en
esos casos, podemos usar ` {:#?}en lugar de` {:?}en la println!cadena. En este
ejemplo, usar el {:#?}estilo generará lo siguiente:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
Rust
jueves, 22 de mayo de 2025 : Página 127 de 719
Además del Debugrasgo, Rust proporciona varios rasgos que podemos usar con
el deriveatributo y que pueden añadir un comportamiento útil a nuestros tipos
personalizados. Estos rasgos y sus comportamientos se listan en el Apéndice
C. En el Capítulo 10, explicaremos cómo implementar estos rasgos con
comportamiento personalizado y cómo crear sus propios rasgos. También existen
muchos otros atributos derive; para más información, consulte la sección
"Atributos" de la Referencia de Rust .
Los métodos son similares a las funciones: se declaran con la fnpalabra clave y un
nombre, pueden tener parámetros y un valor de retorno, y contienen código que
se ejecuta cuando se invoca el método desde otro lugar. A diferencia de las
funciones, los métodos se definen dentro del contexto de una estructura (o una
enumeración o un objeto de rasgo, que abordamos en los capítulos 6 y 17 ,
respectivamente), y su primer parámetro es siempre self, que representa la
instancia de la estructura en la que se invoca el método.
Definición de métodos
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
Listado 5-13: Definición de un areamétodo en la Rectangleestructura
Tenga en cuenta que podemos asignar a un método el mismo nombre que uno de
los campos de la estructura. Por ejemplo, podemos definir un método
en Rectangleque también se llame width:
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}
método width. Cuando no se usan paréntesis, Rust sabe que nos referimos al
campo width.
p1.distance(&p2);
(&p1).distance(&p2);
Sabemos que queremos definir un método, por lo que estará dentro del impl
Rectangle bloque. El nombre del método será can_holdy tomará un préstamo
inmutable de otro Rectanglecomo parámetro. Podemos saber cuál será el tipo del
parámetro mirando el código que llama al
método: rect1.can_hold(&rect2)pasa &rect2, que es un préstamo inmutable
a rect2, una instancia de Rectangle. Esto tiene sentido porque solo necesitamos
leer rect2(en lugar de escribir, lo que significaría que necesitaríamos un préstamo
mutable), y queremos mainconservar la propiedad de rect2para que podamos
usarlo de nuevo después de llamar al can_holdmétodo. El valor de retorno
de can_holdserá un booleano, y la implementación comprobará si el ancho y la
Rust
jueves, 22 de mayo de 2025 : Página 132 de 719
Funciones asociadas
Las funciones asociadas que no son métodos se suelen usar para constructores
que devuelven una nueva instancia de la estructura. Suelen llamarse new,
pero newno es un nombre especial ni está integrado en el lenguaje. Por ejemplo,
podríamos proporcionar una función asociada llamada squareque tuviera un
parámetro de dimensión y lo usara como ancho y alto, facilitando así la creación
de un cuadrado Rectangleen lugar de tener que especificar el mismo valor dos
veces:
impl Rectangle {
Rust
jueves, 22 de mayo de 2025 : Página 133 de 719
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
implBloques múltiples
Cada estructura puede tener varios implbloques. Por ejemplo, el Listado 5-15 es
equivalente al código del Listado 5-16, que tiene cada método en su
propio implbloque.
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Listado 5-16: Reescritura del Listado 5-15 utilizando múltiples implbloques
No hay razón para separar estos métodos en varios implbloques, pero esta
sintaxis es válida. Veremos un caso en el que varios implbloques son útiles en el
Capítulo 10, donde analizamos los tipos y rasgos genéricos.
Resumen
Rust
jueves, 22 de mayo de 2025 : Página 134 de 719
Analicemos una situación que podríamos querer expresar en código y veamos por
qué las enumeraciones son útiles y más apropiadas que las estructuras en este
caso. Supongamos que necesitamos trabajar con direcciones IP. Actualmente, se
utilizan dos estándares principales para direcciones IP: la versión cuatro y la
versión seis. Dado que estas son las únicas posibilidades de dirección IP que
nuestro programa encontrará, podemos enumerar todas las variantes posibles, de
ahí el nombre de enumeración.
Rust
jueves, 22 de mayo de 2025 : Página 135 de 719
enum IpAddrKind {
V4,
V6,
}
Valores de enumeración
fn route(ip_kind: IpAddrKind) {}
route(IpAddrKind::V4);
route(IpAddrKind::V6);
Rust
jueves, 22 de mayo de 2025 : Página 136 de 719
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
Existe otra ventaja de usar una enumeración en lugar de una estructura: cada
variante puede tener diferentes tipos y cantidades de datos asociados. Las
direcciones IP de la versión cuatro siempre tendrán cuatro componentes
numéricos con valores entre 0 y 255. Si quisiéramos almacenar V4direcciones
como cuatro u8valores, pero expresarlas V6como un solo Stringvalor, no
podríamos hacerlo con una estructura. Las enumeraciones gestionan este caso
fácilmente:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
Este código ilustra que se puede incluir cualquier tipo de dato dentro de una
variante de enumeración: cadenas, tipos numéricos o estructuras, por ejemplo.
¡Incluso se puede incluir otra enumeración! Además, los tipos de la biblioteca
estándar no suelen ser mucho más complejos que los que se podrían crear.
Veamos otro ejemplo de una enumeración en el Listado 6-2: este tiene una
amplia variedad de tipos incorporados en sus variantes.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Listado 6-2: Una Messageenumeración cuyas variantes almacenan diferentes
cantidades y tipos de valores
Definir una enumeración con variantes como las del Listado 6-2 es similar a
definir diferentes tipos de definiciones de estructura, salvo que la enumeración no
utiliza la structpalabra clave y todas las variantes se agrupan bajo
el Message tipo. Las siguientes estructuras podrían contener los mismos datos
que las variantes de enumeración anteriores:
Pero si usáramos diferentes estructuras, cada una de las cuales tiene su propio
tipo, no podríamos definir tan fácilmente una función para tomar cualquiera de
estos tipos de mensajes como podríamos hacerlo con la Messageenumeración
definida en el Listado 6-2, que es de un solo tipo.
Existe otra similitud entre las enumeraciones y las estructuras: así como podemos
definir métodos en estructuras usando impl, también podemos definir métodos en
enumeraciones. Aquí hay un método llamado callque podríamos definir en
nuestra Messageenumeración:
impl Message {
fn call(&self) {
// method body would be defined here
}
}
let m = Message::Write(String::from("hello"));
m.call();
El cuerpo del método se usaría selfpara obtener el valor al que se invocó. En este
ejemplo, hemos creado una variable mcon el
valor Message::Write(String::from("hello")), que será el que selfse incluirá en el
cuerpo del callmétodo al m.call()ejecutarse.
Esta sección explora un caso práctico de Option, otra enumeración definida por la
biblioteca estándar. El Optiontipo codifica el escenario muy común en el que un
valor puede ser algo o nada.
El problema con los valores nulos es que, si intentas usar un valor nulo como un
valor no nulo, obtendrás algún tipo de error. Dado que esta propiedad de nulo o
no nulo es omnipresente, es extremadamente fácil cometer este tipo de error.
Sin embargo, el concepto que null intenta expresar sigue siendo útil: un null es un
valor que actualmente no es válido o está ausente por alguna razón.
enum Option<T> {
None,
Some(T),
}
En resumen, dado que Option<T>y T(donde Tpuede ser cualquier tipo) son tipos
diferentes, el compilador no nos permitirá usar un Option<T>valor como si fuera
definitivamente válido. Por ejemplo, este código no compila porque intenta
agregar un i8a un Option<i8>:
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5| let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
`&'a i8` implements `Add<i8>`
`&i8` implements `Add<&i8>`
`i8` implements `Add<&i8>`
`i8` implements `Add`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error
¡Intenso! En efecto, este mensaje de error significa que Rust no entiende cómo
sumar an i8y an Option<i8>, ya que son de tipos diferentes. Cuando tenemos un
valor de un tipo, como i8en Rust, el compilador se asegurará de que siempre
tengamos un valor válido. Podemos proceder con confianza sin tener que
comprobar si es nulo antes de usar ese valor. Solo cuando tenemos
an Option<i8>(o cualquier otro tipo de valor con el que estemos trabajando)
debemos preocuparnos por la posibilidad de no tener un valor, y el compilador se
asegurará de que gestionemos ese caso antes de usarlo.
los problemas más comunes con null: asumir que algo no es nulo cuando en
realidad lo es.
tamaños, y cada moneda cae por el primer agujero que encuentra en el que
encaja. De la misma manera, los valores pasan por cada patrón en unmatch , y en
el primer patrón en el que el valor "encaja", este cae en el bloque de código
asociado que se usará durante la ejecución.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
Listado 6-4: Una Coinenumeración en la que la Quartervariante también contiene
un UsStatevalor
Si llamáramos
a value_in_cents(Coin::Quarter(UsState::Alaska)), coin sería Coin::Quarter(UsState
::Alaska). Al comparar ese valor con cada uno de los brazos coincidentes, ninguno
Rust
jueves, 22 de mayo de 2025 : Página 147 de 719
Coincidencia conOption<T>
En la sección anterior, queríamos obtener el Tvalor interno fuera del Some caso al
usar ` Option<T>`; también podemos manejar Option<T>el uso de ` match`,
como hicimos con la Coinenumeración. En lugar de comparar monedas,
compararemos las variantes de ` Option<T>`, pero el funcionamiento de
la matchexpresión sigue siendo el mismo.
Esta función es muy fácil de escribir, gracias a match, y se verá como el Listado 6-
5.
¡Coincide! No hay ningún valor que añadir, así que el programa se detiene y
devuelve el Nonevalor a la derecha de =>. Como el primer brazo coincidió, no se
comparan los demás.
Hay otro aspecto que matchdebemos discutir: los patrones de los brazos deben
cubrir todas las posibilidades. Considere esta versión de nuestra plus_onefunción,
que tiene un error y no compila:
No gestionamos el Nonecaso, así que este código provocará un error. Por suerte,
Rust sabe cómo detectarlo. Si intentamos compilarlo, obtendremos este error:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
Rust
jueves, 22 de mayo de 2025 : Página 149 de 719
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/
option.rs:571:1
|
571 | pub enum Option<T> {
| ^^^^^^^^^^^^^^^^^^
...
575 | None,
| ---- not covered
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with
a wildcard pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error
Rust sabe que no cubrimos todos los casos posibles, ¡e incluso sabe qué patrón
olvidamos! Las coincidencias en Rust son exhaustivas : debemos agotar hasta la
última posibilidad para que el código sea válido. Especialmente en el caso de
[ nombre del archivo Option<T>], cuando Rust nos impide olvidarnos de manejar
explícitamente el [nombre del archivo]. None caso explícitamente, nos protege de
asumir que tenemos un valor cuando podríamos tener un valor nulo, lo que
imposibilita el error millonario mencionado anteriormente.
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
Para los dos primeros brazos, los patrones son los valores literales 3y 7. Para el
último brazo, que abarca todos los demás valores posibles, el patrón es la
variable que hemos llamado other. El código que se ejecuta para el otherbrazo
usa la variable pasándola a la move_playerfunción.
Este código se compila, aunque no hemos enumerado todos los valores posibles
que u8puede tener a, ya que el último patrón coincidirá con todos los valores no
especificados. Este patrón general cumple con el requisito de matchser
exhaustivo. Tenga en cuenta que debemos colocar el brazo general al final, ya
que los patrones se evalúan en orden. Si colocamos el brazo general antes, los
demás brazos nunca se ejecutarían, por lo que Rust nos avisará si añadimos
brazos después de un patrón general.
Rust también tiene un patrón que podemos usar cuando queremos un catch-all,
pero no queremos usar el valor que contiene: _es un patrón especial que coincide
con cualquier valor y no se vincula a él. Esto le indica a Rust que no usaremos el
valor, por lo que Rust no nos avisará sobre una variable no utilizada.
Cambiemos las reglas del juego: ahora, si sacas un número distinto a 3 o 7, debes
volver a tirar. Ya no necesitamos usar el valor general, así que podemos cambiar
nuestro código para usar _en lugar de la variable llamada other:
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
Rust
jueves, 22 de mayo de 2025 : Página 151 de 719
Finalmente, cambiaremos las reglas del juego una vez más para que no ocurra
nada más en tu turno si obtienes cualquier número que no sea un 3 o un 7.
Podemos expresarlo usando el valor de la unidad (el tipo de tupla vacía que
mencionamos en la sección "El tipo de tupla" ) como el código que va con
el _brazo:
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
Aquí, le decimos a Rust explícitamente que no vamos a utilizar ningún otro valor
que no coincida con un patrón en un brazo anterior y no queremos ejecutar
ningún código en este caso.
La if letsintaxis permite combinar ify letde una forma más sencilla para gestionar
valores que coinciden con un patrón e ignorar los demás. Considere el programa
del Listado 6-6 que coincide con un Option<u8>valor en la config_maxvariable,
pero solo desea ejecutar código si el valor es la Some variante.
En lugar de eso, podríamos escribir esto de forma más breve usando if let. El
siguiente código se comporta igual que el matchdel Listado 6-6:
Usarlo if letimplica menos escritura, menos sangría y menos código repetitivo. Sin
embargo, se pierde la comprobación exhaustiva que matchimpone. Elegir
entre matchyif let depende de lo que esté haciendo en su situación particular y de
si ganar concisión es una compensación adecuada por perder la comprobación
exhaustiva.
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
Listado 6-8: Uso if letpara producir un valor o retorno anticipado.
Sin embargo, es un poco molesto seguir esto a su manera. Una rama if letproduce
un valor y la otra retorna de la función por completo.
En el Listado 6-9, puede ver cómo se ve el Listado 6-8 al usar let- elseen lugar
de if let. Observe que, de esta manera, se mantiene en la ruta correcta en el
cuerpo principal de la función, sin tener un flujo de control significativamente
diferente para las dos ramas, como if letocurría con .
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
Listado 6-9: Uso de let- elsepara aclarar el flujo a través de la función.
Resumen
Hemos explicado cómo usar enumeraciones para crear tipos personalizados que
pueden ser uno de un conjunto de valores enumerados. Hemos mostrado cómo
el Option<T> tipo de la biblioteca estándar ayuda a usar el sistema de tipos para
evitar errores. Cuando los valores de enumeración contienen datos, se puede
usar matcho if letpara extraer y usar esos valores, según el número de casos que
se necesiten gestionar.
Para proporcionar a sus usuarios una API bien organizada que sea fácil de usar y
que solo exponga exactamente lo que necesitarán, pasemos ahora a los módulos
de Rust.
A medida que escriba programas grandes, organizar su código será cada vez más
importante. Al agrupar las funciones relacionadas y separar el código con
características distintivas, tendrá claro dónde encontrar el código que implementa
una característica específica y dónde ir para modificar su funcionamiento.
Rust cuenta con diversas funciones que permiten gestionar la organización del
código, incluyendo qué detalles se exponen, cuáles son privados y qué nombres
se incluyen en cada ámbito de los programas. Estas funciones, a veces
denominadas colectivamente como el sistema de módulos , incluyen:
Paquetes y cajas
Las primeras partes del sistema de módulos que cubriremos son los paquetes y
las cajas.
La raíz del cajón es un archivo fuente desde el cual se inicia el compilador de Rust
y conforma el módulo raíz de su cajón (explicaremos los módulos en profundidad
en la sección “Definición de módulos para controlar el alcance y la privacidad” ).
Después de ejecutar cargo new my-project, usamos lspara ver qué crea Cargo. En
el directorio del proyecto, hay un archivo Cargo.toml que nos proporciona un
paquete. También hay un directorio src que contiene main.rs. Abra Cargo.toml en
su editor de texto y observe que no se menciona src/main.rs . Cargo sigue la
convención de que src/main.rs es la raíz de un cajón binario con el mismo nombre
que el paquete. Asimismo, Cargo sabe que si el directorio del paquete
contiene src/lib.rs , el paquete contiene un cajón de biblioteca con el mismo
nombre, y src/lib.rs es su raíz. Cargo pasa los archivos raíz de este cajón a
[nombre del paquete] para rustccompilar la biblioteca o el binario.
Aquí tenemos un paquete que solo contiene src/main.rs , lo que significa que solo
contiene un contenedor binario llamado my-project. Si un paquete
contiene src/main.rs y src/lib.rs , tiene dos contenedores: un contenedor binario y
una biblioteca, ambos con el mismo nombre que el paquete. Un paquete puede
tener varios contenedores binarios colocando los archivos en el directorio src/bin :
cada archivo será un contenedor binario independiente.
En esta sección, hablaremos sobre los módulos y otras partes del sistema de
módulos, como las rutas , que permiten nombrar elementos; la usepalabra clave
que incluye una ruta en el ámbito; y la pubpalabra clave que permite que los
Rust
jueves, 22 de mayo de 2025 : Página 159 de 719
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rs
fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
}
Para estructurar nuestra caja de esta manera, podemos organizar sus funciones
en módulos anidados. Crea una nueva biblioteca llamada restaurantmediante la
ejecución de cargo new restaurant --lib. Luego, introduce el código del Listado 7-1
en src/lib.rs para definir algunos módulos y firmas de funciones; este código es la
sección de front-of-house.
fn seat_at_table() {}
}
mod serving {
Rust
jueves, 22 de mayo de 2025 : Página 162 de 719
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
Listado 7-1: Un front_of_housemódulo que contiene otros módulos que a su vez
contienen funciones
El listado 7-2 muestra el árbol de módulos para la estructura del Listado 7-1.
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
Listado 7-2: El árbol de módulos para el código del Listado 7-1
Rust
jueves, 22 de mayo de 2025 : Página 163 de 719
Este árbol muestra cómo algunos módulos se anidan dentro de otros; por
ejemplo, hostingse anida dentro de front_of_house. El árbol también muestra que
algunos módulos son hermanos , lo que significa que están definidos en el mismo
módulo; hostingy servingson hermanos definidos dentro de front_of_house. Si el
módulo A está contenido dentro del módulo B, decimos que el módulo A
es hijo del módulo B y que el módulo B es padre del módulo A. Observe que todo
el árbol de módulos tiene su raíz en el módulo implícito llamado crate.
pero existe otro problema que impedirá que este ejemplo se compile tal cual.
Explicaremos por qué en breve.
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
Listado 7-3: Llamada a la add_to_waitlistfunción utilizando rutas absolutas y
relativas
Elegir si usar una ruta relativa o absoluta es una decisión que tomará según su
proyecto y depende de si es más probable que mueva el código de definición de
elemento por separado o junto con el código que usa el elemento. Por ejemplo, si
movemos el front_of_housemódulo y la eat_at_restaurantfunción a un módulo
llamado customer_experience, necesitaríamos actualizar la ruta absoluta
a add_to_waitlist, pero la ruta relativa seguiría siendo válida. Sin embargo, si
movemos la eat_at_restaurantfunción por separado a un módulo llamado dining,
la ruta absoluta a la add_to_waitlistllamada permanecería igual, pero la ruta
relativa tendría que actualizarse. En general, preferimos especificar rutas
absolutas porque es más probable que queramos mover las definiciones de
código y las llamadas de elemento de forma independiente.
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9| crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not
publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2| mod hosting {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
Listado 7-4: Errores del compilador al crear el código del Listado 7-3
Los mensajes de error indican que el módulo hostinges privado. En otras palabras,
tenemos las rutas correctas para el hostingmódulo y la add_to_waitlist función,
pero Rust no nos permite usarlas porque no tiene acceso a las secciones privadas.
En Rust, todos los elementos (funciones, métodos, estructuras, enumeraciones,
módulos y constantes) son privados de los módulos principales por defecto. Para
que un elemento, como una función o una estructura, sea privado, se coloca en
un módulo.
Rust optó por que el sistema de módulos funcione de esta manera, de modo que
ocultar los detalles de implementación internos sea la opción predeterminada. De
esta forma, sabes qué partes del código interno puedes modificar sin afectar el
código externo. Sin embargo, Rust te permite exponer partes internas del código
de los módulos secundarios a los módulos ancestros externos usando
la pubpalabra clave para hacer público un elemento.
Volvamos al error del Listado 7-4 que indicaba que el hostingmódulo es privado.
Queremos que la eat_at_restaurantfunción del módulo principal tenga acceso a
la add_to_waitlistfunción del módulo secundario, por lo que marcamos
el hostingmódulo con la pubpalabra clave, como se muestra en el Listado 7-5.
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
Listado 7-5: Declaración del hostingmódulo para pubutilizarlo
desdeeat_at_restaurant
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:9:37
|
9| crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3| fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
Rust
jueves, 22 de mayo de 2025 : Página 168 de 719
Listado 7-6: Errores del compilador al crear el código del Listado 7-5
Los errores del Listado 7-6 indican que la add_to_waitlistfunción es privada. Las
reglas de privacidad se aplican tanto a estructuras, enumeraciones, funciones y
métodos como a módulos.
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
Listado 7-7: Agregar la pubpalabra clave a mod hostingy fn add_to_waitlistnos
permite llamar a la función desdeeat_at_restaurant
¡Ahora el código compilará! Para entender por qué añadir la pubpalabra clave nos
permite usar estas rutas eat_at_restaurantrespetando las reglas de privacidad,
veamos las rutas absolutas y relativas.
Si planea compartir su crate de biblioteca para que otros proyectos puedan usar
su código, su API pública es el contrato con los usuarios de su crate, que
determina cómo pueden interactuar con su código. Hay muchas consideraciones
sobre la gestión de cambios en su API pública para facilitar que los usuarios
dependan de su crate. Estas consideraciones quedan fuera del alcance de este
libro; si le interesa este tema, consulte las Directrices de la API de Rust .
puede usar la API pública. Esto te ayuda a diseñar una buena API; no solo eres el
autor, sino también el cliente.
Podemos construir rutas relativas que comiencen en el módulo padre, en lugar del
módulo actual o la raíz del crate, usando `` superal inicio de la ruta`. Esto es
como iniciar una ruta del sistema de archivos con la ..sintaxis. Usar `` supernos
permite referenciar un elemento que sabemos que está en el módulo padre, lo
que facilita la reorganización del árbol de módulos cuando el módulo está
estrechamente relacionado con el padre, pero este podría moverse a otra parte
del árbol de módulos en el futuro.
Considere el código del Listado 7-8 que modela la situación en la que un chef
prepara un pedido incorrecto y se lo entrega personalmente al cliente. La
función fix_incorrect_orderdefinida en el back_of_housemódulo llama a la
función deliver_orderdefinida en el módulo principal especificando la ruta
a deliver_order, comenzando por super.
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
Listado 7-8: Llamada a una función usando una ruta relativa que comienza
consuper
del contenedor. Por lo tanto, usamos superpara tener menos espacio para
actualizar el código en el futuro si este se mueve a otro módulo.
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal
// meal.seasonal_fruit = String::from("blueberries");
Rust
jueves, 22 de mayo de 2025 : Página 172 de 719
}
Listado 7-9: Una estructura con algunos campos públicos y algunos campos
privados
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
Listado 7-10: Designar una enumeración como pública hace que todas sus
variantes sean públicas
Las enumeraciones no son muy útiles a menos que sus variantes sean públicas;
sería molesto tener que anotar todas las variantes de enumeración con puben
todos los casos, por lo que el valor predeterminado para las variantes de
Rust
jueves, 22 de mayo de 2025 : Página 173 de 719
enumeración es que sean públicas. Las estructuras suelen ser útiles sin que sus
campos sean públicos, por lo que los campos de estructura siguen la regla
general de que todo es privado por defecto a menos que se anoten con pub.
Escribir las rutas para llamar a las funciones puede resultar incómodo y repetitivo.
En el Listado 7-7, independientemente de si elegimos la ruta absoluta o relativa a
la add_to_waitlistfunción, cada vez que queríamos
llamarla add_to_waitlist teníamos que especificar
también front_of_house"y hosting". Afortunadamente, hay una forma de
simplificar este proceso: podemos crear un acceso directo a una ruta con
la use palabra clave una vez y luego usar el nombre más corto en el resto del
ámbito.
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Listado 7-11: Incorporación de un módulo al alcance conuse
el hosting módulo se hubiera definido en la raíz del paquete. Las rutas añadidas al
ámbito use también verifican la privacidad, como cualquier otra ruta.
Tenga en cuenta que usesolo crea el acceso directo para el ámbito específico en
el que useocurre. El Listado 7-12 mueve la eat_at_restaurantfunción a un nuevo
módulo secundario llamado customer, que entonces tiene un ámbito diferente al
de la use instrucción, por lo que el cuerpo de la función no se compilará.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
Listado 7-12: Una usedeclaración solo se aplica en el ámbito en el que se
encuentra
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
--> src/lib.rs:11:9
|
11 | hosting::add_to_waitlist();
| ^^^^^^^ use of undeclared crate or module `hosting`
|
help: consider importing this module through its public re-export
|
10 + use crate::hosting;
|
For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning
emitted
Tenga en cuenta que también hay una advertencia que indica que useya no se
usa en su ámbito. Para solucionar este problema, mueva también el usedentro
del customermódulo o haga referencia al acceso directo en el módulo principal
con super::hostingdentro del customermódulo secundario.
En el Listado 7-11, es posible que se haya preguntado por qué especificamos use
crate::front_of_house::hostingy luego
llamamos hosting::add_to_waitlista eat_at_restaurant, en lugar de especificar
la useruta hasta la add_to_waitlistfunción para lograr el mismo resultado, como en
el Listado 7-13.
use crate::front_of_house::hosting::add_to_waitlist;
pub fn eat_at_restaurant() {
add_to_waitlist();
}
Listado 7-13: Incorporar la add_to_waitlistfunción al alcance con use, lo cual no es
idiomático
Aunque tanto el Listado 7-11 como el Listado 7-13 realizan la misma tarea, el
Listado 7-11 es la forma idiomática de incluir una función en el ámbito con use.
Incluir el módulo padre de la función en el ámbito con useimplica especificar dicho
módulo al llamarla. Especificar el módulo padre al llamarla deja claro que la
función no está definida localmente, a la vez que minimiza la repetición de la ruta
completa. El código del Listado 7-13 no especifica dónde add_to_waitlistse define.
Rust
jueves, 22 de mayo de 2025 : Página 176 de 719
Por otro lado, al importar estructuras, enumeraciones y otros elementos con use,
es idiomático especificar la ruta completa. El Listado 7-14 muestra la forma
idiomática de incluir la HashMapestructura de la biblioteca estándar en el ámbito
de un contenedor binario.
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
Listado 7-14: Traer HashMapal ámbito de una manera idiomática
Como puedes ver, usar los módulos principales distingue los dos Resulttipos. Si,
en cambio, escribiéramos use std::fmt::Resulty use std::io::Result, tendríamos
dos Resulttipos en el mismo ámbito, y Rust no sabría a cuál nos referíamos al
usar Result.
Existe otra solución al problema de incluir dos tipos con el mismo nombre en el
mismo ámbito con use: después de la ruta, podemos especificar asun nuevo
nombre local, o alias , para el tipo. El Listado 7-16 muestra otra forma de escribir
el código del Listado 7-15, renombrando uno de los dos Resulttipos con as.
El listado 7-17 muestra el código del listado 7-11 con useel módulo raíz cambiado
a pub use.
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Listado 7-17: Hacer que un nombre esté disponible para que cualquier código lo
use desde un nuevo ámbito conpub use
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}
use std::collections::HashMap;
Esta es una ruta absoluta que comienza con std, el nombre del paquete de la
biblioteca estándar.
En su lugar, podemos usar rutas anidadas para incluir los mismos elementos en el
ámbito de aplicación en una sola línea. Para ello, especificamos la parte común de
Rust
jueves, 22 de mayo de 2025 : Página 180 de 719
la ruta, seguida de dos puntos y, a continuación, entre llaves, una lista de las
partes de las rutas que difieren, como se muestra en el Listado 7-18.
Podemos usar una ruta anidada en cualquier nivel de una ruta, lo cual resulta útil
al combinar dos usesentencias que comparten una subruta. Por ejemplo, el
Listado 7-19 muestra dos usesentencias: una que incluye std::iodentro del
alcance y otra que incluye std::io::Writedentro del alcance.
La parte común de estas dos rutas es std::io, y esa es la primera ruta completa.
Para fusionarlas en una sola useinstrucción, podemos usar selfen la ruta anidada,
como se muestra en el Listado 7-20.
El operador Glob
Si queremos traer todos los elementos públicos definidos en una ruta al alcance,
podemos especificar esa ruta seguida por el *operador glob:
use std::collections::*;
Rust
jueves, 22 de mayo de 2025 : Página 181 de 719
El operador glob se utiliza a menudo durante las pruebas para traer todo lo que se
está probando al testsmódulo; hablaremos de eso en la sección “Cómo escribir
pruebas” en el Capítulo 11. El operador glob también se utiliza a veces como
parte del patrón de preludio: consulte la documentación de la biblioteca
estándar para obtener más información sobre ese patrón.
Hasta ahora, todos los ejemplos de este capítulo definieron varios módulos en un
solo archivo. Cuando los módulos sean grandes, conviene trasladar sus
definiciones a un archivo aparte para facilitar la navegación por el código.
Por ejemplo, comencemos con el código del Listado 7-17, que tenía varios
módulos de restaurante. Extraeremos los módulos a archivos en lugar de tenerlos
todos definidos en el archivo raíz del crate. En este caso, el archivo raíz del crate
es src/lib.rs , pero este procedimiento también funciona con crates binarios cuyo
archivo raíz del crate es src/main.rs .
mod front_of_house;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Listado 7-21: Declaración del front_of_housemódulo cuyo cuerpo estará
en src/front_of_house.rs
Rust
jueves, 22 de mayo de 2025 : Página 182 de 719
Hasta ahora hemos cubierto las rutas de archivo más comunes que usa el
compilador de Rust, pero Rust también admite un estilo más antiguo de ruta de
archivo. Para un módulo llamado front_of_housedeclarado en la raíz del paquete,
el compilador buscará el código del módulo en:
La principal desventaja del estilo que utiliza archivos llamados mod.rs es que su
proyecto puede terminar con muchos archivos llamados mod.rs , lo que puede
resultar confuso cuando los tiene abiertos en su editor al mismo tiempo.
Rust permite dividir un paquete en varias cajas y una caja en módulos para que
puedas hacer referencia a los elementos definidos en un módulo desde otro.
Puedes hacerlo especificando rutas absolutas o relativas. Estas rutas se pueden
incluir en el ámbito de aplicación mediante una usedeclaración, lo que permite
usar una ruta más corta para múltiples usos del elemento en ese ámbito. El
código del módulo es privado por defecto, pero puedes hacer públicas las
definiciones añadiendo la pubpalabra clave.
Colecciones comunes
Para obtener más información sobre los otros tipos de colecciones proporcionadas
por la biblioteca estándar, consulte la documentación .
Discutiremos cómo crear y actualizar vectores, cadenas y mapas hash, así como
también qué hace que cada uno sea especial.
Rust
jueves, 22 de mayo de 2025 : Página 185 de 719
Almacenamiento de listas de valores con vectores
Tenga en cuenta que añadimos una anotación de tipo. Dado que no insertamos
ningún valor en este vector, Rust desconoce qué tipo de elementos queremos
almacenar. Este es un punto importante. Los vectores se implementan mediante
genéricos; explicaremos cómo usarlos con sus propios tipos en el Capítulo 10. Por
ahora, tenga en cuenta que el Vec<T>tipo proporcionado por la biblioteca
estándar puede contener cualquier tipo. Al crear un vector para un tipo específico,
podemos especificarlo entre corchetes angulares. En el Listado 8-1, indicamos a
Rust que la Vec<T>instrucción in vcontendrá elementos de ese i32tipo.
Debido a que hemos proporcionado i32valores iniciales, Rust puede inferir que el
tipo de v esVec<i32> , y la anotación de tipo no es necesaria. A continuación,
veremos cómo modificar un vector.
Rust
jueves, 22 de mayo de 2025 : Página 186 de 719
Actualizar un vector
v.push(5);
v.push(6);
v.push(7);
v.push(8);
Listado 8-3: Uso del pushmétodo para agregar valores a un vector
Al igual que con cualquier variable, si queremos poder cambiar su valor, debemos
hacerla mutable usando la mutpalabra clave, como se explica en el Capítulo 3.
Los números que colocamos dentro son todos de tipo i32, y Rust lo infiere de los
datos, por lo que no necesitamos laVec<i32> anotación.
El listado 8-4 muestra ambos métodos para acceder a un valor en un vector, con
la sintaxis de indexación y el getmétodo.
Tenga en cuenta algunos detalles. Usamos el valor del índice de 2para obtener el
tercer elemento, ya que los vectores se indexan por número, comenzando desde
cero. Usar &y [] nos da una referencia al elemento en el valor del índice. Al usar
Rust
jueves, 22 de mayo de 2025 : Página 187 de 719
Rust ofrece estas dos maneras de referenciar un elemento para que puedas elegir
cómo se comporta el programa al intentar usar un valor de índice fuera del rango
de elementos existentes. A modo de ejemplo, veamos qué sucede cuando
tenemos un vector de cinco elementos e intentamos acceder a un elemento en el
índice 100 con cada técnica, como se muestra en el Listado 8-5.
Cuando getse pasa al método un índice fuera del vector, este retorna Nonesin
entrar en pánico. Este método se usaría si, en circunstancias normales, se pudiera
acceder a un elemento fuera del rango del vector. El código tendría entonces
lógica para gestionar la presencia de Some(&element)o None, como se explicó en
el Capítulo 6. Por ejemplo, el índice podría provenir de una persona que introduce
un número. Si accidentalmente introduce un número demasiado grande y el
programa obtiene un Nonevalor, se podría indicar al usuario cuántos elementos
hay en el vector actual y darle otra oportunidad para introducir un valor válido.
Esto sería más intuitivo que bloquear el programa por un error tipográfico.
v.push(6);
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
immutable
--> src/main.rs:6:5
|
4| let first = &v[0];
| - immutable borrow occurs here
5|
6| v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7|
8| println!("The first element is: {first}");
| ------- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error
El código del Listado 8-6 podría parecer funcional: ¿por qué una referencia al
primer elemento debería preocuparse por los cambios al final del vector? Este
error se debe al funcionamiento de los vectores: dado que los vectores colocan
los valores uno junto al otro en memoria, añadir un nuevo elemento al final del
vector podría requerir asignar nueva memoria y copiar los elementos antiguos al
nuevo espacio si no hay suficiente espacio para colocar todos los elementos uno
junto al otro donde se almacena actualmente el vector. En ese caso, la referencia
al primer elemento apuntaría a memoria desasignada. Las reglas de préstamo
evitan que los programas se encuentren en esa situación.
Rust
jueves, 22 de mayo de 2025 : Página 189 de 719
Para acceder a cada elemento de un vector, iteraremos por todos los elementos
en lugar de usar índices para acceder a cada uno. El listado 8-7 muestra cómo
usar un forbucle para obtener referencias inmutables a cada elemento de un
vector de i32valores e imprimirlas.
Los vectores solo pueden almacenar valores del mismo tipo. Esto puede ser un
inconveniente; sin duda, existen casos prácticos en los que es necesario
almacenar una lista de elementos de diferentes tipos. Afortunadamente, las
variantes de una enumeración se definen bajo el mismo tipo de enumeración, por
lo que cuando necesitamos que un tipo represente elementos de diferentes tipos,
podemos definir y usar una enumeración.
Por ejemplo, supongamos que queremos obtener valores de una fila en una hoja
de cálculo cuyas columnas contienen enteros, números de punto flotante y
cadenas. Podemos definir una enumeración cuyas variantes contendrán los
diferentes tipos de valor, y todas las variantes de la enumeración se considerarán
del mismo tipo: el de la enumeración. Después, podemos crear un vector que
contenga esa enumeración y, por lo tanto, contenga diferentes tipos. Esto se
demuestra en el Listado 8-9.
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
Rust necesita saber qué tipos estarán en el vector en tiempo de compilación para
saber exactamente cuánta memoria en el montón se necesitará para almacenar
cada elemento. También debemos ser explícitos sobre los tipos permitidos en
este vector. Si Rust permitiera que un vector contuviera cualquier tipo, existiría la
posibilidad de que uno o más de estos tipos causaran errores en las operaciones
realizadas con los elementos del vector. Usar una enumeración más
una matchexpresión significa que Rust garantizará en tiempo de compilación que
se gestionen todos los casos posibles, como se explica en el Capítulo 6.
Ahora que hemos analizado algunas de las formas más comunes de usar
vectores, asegúrese de revisar la documentación de la API para conocer todos los
métodos útiles definidos en Vec<T>la biblioteca estándar. Por ejemplo, además
de push, un popmétodo elimina y devuelve el último elemento.
Al igual que cualquier otro struct, un vector se libera cuando queda fuera del
alcance, como se indica en el Listado 8-10.
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
Listado 8-10: Mostrando dónde se colocan el vector y sus elementos
Esta línea crea una nueva cadena vacía llamada s, en la que podemos cargar
datos. A menudo, tendremos datos iniciales con los que queremos iniciar la
cadena. Para ello, usamos el to_stringmétodo , disponible en cualquier tipo que
implemente el Displayatributo, como lo hacen los literales de cadena. El Listado 8-
12 muestra dos ejemplos.
Dado que las cadenas se usan para tantas cosas, podemos usar diversas API
genéricas para cadenas, lo que nos brinda una gran variedad de opciones.
Algunas pueden parecer redundantes, ¡pero todas tienen su utilidad! En este
caso, String::fromy to_stringhacen lo mismo, así que la elección es cuestión de
estilo y legibilidad.
Recuerde que las cadenas están codificadas en UTF-8, por lo que podemos incluir
en ellas cualquier dato codificado correctamente, como se muestra en el Listado
8-14.
Primero, s2tiene un &, lo que significa que estamos agregando una referencia de
la segunda cadena a la primera. Esto se debe al sparámetro de la add función:
solo podemos agregar a &stra a String; no podemos sumar dos String valores.
Pero espera: el tipo de &s2es &String, no &str, como se especifica en el segundo
parámetro de add. Entonces, ¿por qué se compila el Listado 8-18?
La razón por la que podemos usar [nombre del método] &s2en la llamada a
[nombre del método] addes que el compilador
puede convertir el &Stringargumento en un [ nombre &strdel método]. Al llamar
al add método, Rust utiliza una conversión de desreferencia , que en este caso se
convierte &s2en [nombre del método &s2[..]]. Analizaremos la conversión de
desreferencia con más detalle en el Capítulo 15. Dado que [nombre del
método] addno toma posesión del sparámetro, s2seguirá siendo
válido String después de esta operación.
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
En este punto, sserá tic-tac-toe. Con tantos caracteres +y " , es difícil ver qué
sucede. Para combinar cadenas de forma más compleja, podemos usar la format!
macro:
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
Indexación en cadenas
let s1 = String::from("hello");
let h = s1[0];
Listado 8-19: Intento de utilizar la sintaxis de indexación con una cadena
For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` (bin "collections") due to 1 previous error
Representación interna
Ya sabes que answerno será Зla primera letra. Cuando se codifica en UTF-8, el
primer byte de Зes208 y el segundo es 151, por lo que parecería
que answerdebería ser 208, pero 208no es un carácter válido por sí mismo. 208Es
probable que el usuario no desee obtener la primera letra de esta cadena; sin
embargo, ese es el único dato que Rust tiene en el índice de byte 0. Los usuarios
generalmente no quieren que se devuelva el valor del byte, incluso si la cadena
contiene solo letras latinas: si &"hi"[0]fuera un código válido que devolviera el
valor del byte, devolvería 104, no h.
Otro punto sobre UTF-8 es que en realidad hay tres formas relevantes de ver las
cadenas desde la perspectiva de Rust: como bytes, valores escalares y grupos de
grafemas (lo más cercano a lo que llamaríamos letras ).
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
Esos son 18 bytes, y así es como las computadoras almacenan estos datos. Si los
consideramos como valores escalares Unicode, que son el chartipo de Rust, esos
bytes se ven así:
Aquí hay seis charvalores, pero el cuarto y el sexto no son letras: son diacríticos
que no tienen sentido por sí solos. Finalmente, si los consideramos como grupos
Rust
jueves, 22 de mayo de 2025 : Página 199 de 719
de grafemas, obtendríamos lo que una persona llamaría las cuatro letras que
componen la palabra hindi:
Una última razón por la que Rust no nos permite indexar en unString para obtener
un carácter es que se espera que las operaciones de indexación siempre tomen
un tiempo constante (O(1)). Sin embargo, no es posible garantizar ese
rendimiento con a String, ya que Rust tendría que recorrer el contenido desde el
principio hasta el índice para determinar cuántos caracteres válidos hay.
Cortar cuerdas
Indexar una cadena suele ser una mala idea, ya que no está claro cuál debería ser
el tipo de retorno de la operación: un valor de byte, un carácter, un grupo de
grafemas o un fragmento de cadena. Por lo tanto, si realmente necesita usar
índices para crear fragmentos de cadena, Rust le pide que sea más específico.
En lugar de indexar []con un solo número, puedes utilizar []un rango para crear
una porción de cadena que contenga bytes específicos:
let s = &hello[0..4];
Si intentáramos cortar solo una parte de los bytes de un carácter con algo
como &hello[0..1], Rust entraría en pánico en el tiempo de ejecución de la misma
manera que si se accediera a un índice no válido en un vector:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/collections`
Rust
jueves, 22 de mayo de 2025 : Página 200 de 719
thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Debe tener cuidado al crear segmentos de cadenas con rangos, ya que hacerlo
puede provocar que su programa se bloquee.
for c in "Зд".chars() {
println!("{c}");
}
З
д
Como alternativa, el bytesmétodo devuelve cada byte sin procesar, lo que podría
ser apropiado para su dominio:
for b in "Зд".bytes() {
println!("{b}");
}
Este código imprimirá los cuatro bytes que componen esta cadena:
208
151
208
180
Pero asegúrese de recordar que los valores escalares Unicode válidos pueden
estar compuestos de más de un byte.
Rust
jueves, 22 de mayo de 2025 : Página 201 de 719
Los mapas hash son útiles cuando se buscan datos no mediante un índice, como
ocurre con los vectores, sino mediante una clave de cualquier tipo. Por ejemplo,
en un partido, se podría registrar la puntuación de cada equipo en un mapa hash
donde cada clave es el nombre del equipo y los valores son la puntuación de cada
equipo. Dado el nombre de un equipo, se puede recuperar su puntuación.
Rust
jueves, 22 de mayo de 2025 : Página 202 de 719
En esta sección, repasaremos la API básica de los mapas hash, pero las funciones
definidas HashMap<K, V>por la biblioteca estándar ofrecen muchas más
ventajas. Como siempre, consulte la documentación de la biblioteca estándar
para obtener más información.
Una forma de crear un mapa hash vacío es usar newy para agregar elementos
con insert. En el Listado 8-20, registramos las puntuaciones de dos equipos
llamados Azul y Amarillo . El equipo Azul empieza con 10 puntos y el equipo
Amarillo con 50.
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Listado 8-20: Creación de un nuevo mapa hash e inserción de algunas claves y
valores
Al igual que los vectores, los mapas hash almacenan sus datos en el montículo.
Este HashMapcontiene claves de tipo Stringy valores de tipo i32. Al igual que los
vectores, los mapas hash son homogéneos: todas las claves y todos los valores
deben ser del mismo tipo.
Podemos obtener un valor del mapa hash proporcionando su clave al get método,
como se muestra en el Listado 8-21.
use std::collections::HashMap;
Aquí, scorese tendrá el valor asociado con el equipo Azul y el resultado será 10.
El getmétodo devuelve un Option<&V>; si no hay ningún valor para esa clave en
el mapa hash, getdevolverá None. Este programa gestiona
esto Optionllamando copieda ``` para obtener un`` Option<i32>en lugar de
un`` Option<&i32>y luego unwrap_orestableciéndolo scorea cero si scoresno hay
una entrada para la clave.
Podemos iterar sobre cada par clave-valor en un mapa hash de manera similar a
como lo hacemos con los vectores, usando un forbucle:
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Yellow: 50
Blue: 10
Para los tipos que implementan el Copyrasgo, como i32, los valores se copian en
el mapa hash. Para los valores propios, como String, se moverán y el mapa hash
será el propietario de dichos valores, como se muestra en el Listado 8-22.
use std::collections::HashMap;
Al cambiar los datos de un mapa hash, debe decidir cómo gestionar el caso en
que una clave ya tenga un valor asignado. Puede reemplazar el valor anterior por
el nuevo, ignorándolo por completo. Puede conservar el valor anterior e ignorar el
nuevo, añadiendo este último solo si la clave aún no tiene un valor. O bien, puede
combinar el valor anterior con el nuevo. ¡Veamos cómo hacerlo!
Sobrescribir un valor
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
Rust
jueves, 22 de mayo de 2025 : Página 205 de 719
scores.insert(String::from("Blue"), 25);
println!("{scores:?}");
Listado 8-23: Reemplazo de un valor almacenado con una clave particular
Es común verificar si una clave particular ya existe en el mapa hash con un valor
y luego tomar las siguientes acciones: si la clave existe en el mapa hash, el valor
existente debe permanecer como está; si la clave no existe, insertarla y un valor
para ella.
Los mapas hash tienen una API especial para esto, llamada ``` entry, que toma la
clave que se desea verificar como parámetro. El valor de retorno del entrymétodo
es una enumeración llamada ``` Entryque representa un valor que podría existir o
no. Supongamos que queremos verificar si la clave del equipo amarillo tiene un
valor asociado. Si no lo tiene, insertamos el valor ``` 50, y lo mismo para el
equipo azul. Usando la entryAPI, el código se parece al Listado 8-24.
use std::collections::HashMap;
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{scores:?}");
Listado 8-24: Uso del entrymétodo para insertar solo si la clave aún no tiene un
valor
Al ejecutar el código del Listado 8-24, se imprimirá {"Yellow": 50, "Blue": 10}. La
primera llamada a entryinsertará la clave del equipo Amarillo con el valor, 50ya
Rust
jueves, 22 de mayo de 2025 : Página 206 de 719
Otro uso común de los mapas hash es consultar el valor de una clave y
actualizarlo según el valor anterior. Por ejemplo, el Listado 8-25 muestra código
que cuenta cuántas veces aparece cada palabra en un texto. Usamos un mapa
hash con las palabras como claves e incrementamos el valor para registrar
cuántas veces la hemos visto. Si es la primera vez que vemos una palabra,
primero insertamos el valor 0.
use std::collections::HashMap;
println!("{map:?}");
Listado 8-25: Conteo de ocurrencias de palabras usando un mapa hash que
almacena palabras y las cuenta
Este código imprimirá {"world": 2, "hello": 1, "wonderful": 1}. Es posible que vea
los mismos pares clave-valor impresos en un orden diferente: recuerde que en la
sección "Acceso a valores en un mapa hash" se explicó que la iteración sobre un
mapa hash se realiza en un orden arbitrario.
Funciones hash
Rust
jueves, 22 de mayo de 2025 : Página 207 de 719
https://en.wikipedia.org/wiki/SipHash
Resumen
Nos adentramos en programas más complejos en los que las operaciones pueden
fallar, así que es el momento perfecto para hablar sobre la gestión de errores. ¡Lo
haremos a continuación!
Manejo de errores
Los errores son parte de la vida en el software, por lo que Rust cuenta con
diversas funciones para gestionar situaciones en las que algo sale mal. En muchos
casos, Rust requiere que reconozcas la posibilidad de un error y tomes medidas
antes de que tu código se compile. Este requisito fortalece tu programa al
garantizar que detectes errores y los gestiones adecuadamente antes de
implementar tu código en producción.
La mayoría de los lenguajes no distinguen entre estos dos tipos de errores y los
gestionan de la misma forma, mediante mecanismos como las excepciones. Rust
no tiene excepciones. En cambio, tiene el tipo Result<T, E>para errores
recuperables y la panic!macro que detiene la ejecución cuando el programa
encuentra un error irrecuperable. Este capítulo trata panic!primero la llamada y
luego la devolución Result<T, E>de valores. Además, exploraremos
consideraciones a la hora de decidir si intentar recuperarse de un error o detener
la ejecución.
A veces ocurren cosas malas en tu código y no hay nada que puedas hacer al
respecto. En estos casos, Rust cuenta con la panic!macro. En la práctica, hay dos
maneras de provocar un pánico: realizando una acción que provoque un pánico
en nuestro código (como acceder a un array después del final) o llamando
explícitamente a la panic!macro. En ambos casos, provocamos un pánico en
nuestro programa. Por defecto, estos pánicos mostrarán un mensaje de error, se
desenrollarán, limpiarán la pila y saldrán. Mediante una variable de entorno,
Rust
jueves, 22 de mayo de 2025 : Página 209 de 719
también puedes hacer que Rust muestre la pila de llamadas cuando se produzca
un pánico para facilitar la localización de su origen.
[profile.release]
panic = 'abort'
fn main() {
panic!("crash and burn");
}
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fn main() {
let v = vec![1, 2, 3];
v[99];
}
Listado 9-1: Intentar acceder a un elemento más allá del final de un vector, lo que
provocará una llamada apanic!
Aquí, intentamos acceder al elemento número 100 de nuestro vector (que está en
el índice 99 porque la indexación empieza en cero), pero el vector solo tiene tres
elementos. En esta situación, Rust entrará en pánico. []Se supone que "using"
devuelve un elemento, pero si se pasa un índice no válido, no hay ningún
elemento que Rust pueda devolver correctamente.
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
¡Eso es una gran cantidad de resultados! El resultado exacto que vea puede
variar según su sistema operativo y la versión de Rust. Para obtener backtraces
con esta información, debe habilitar los símbolos de depuración. Estos símbolos
están habilitados por defecto, independientemente de si se usa cargo
buildo cargo runno la --releasebandera, como en este caso.
En la salida del Listado 9-2, la línea 6 del backtrace apunta a la línea de nuestro
proyecto que causa el problema: la línea 4 de src/main.rs . Si no queremos que
nuestro programa entre en pánico, debemos comenzar nuestra investigación en
la ubicación indicada por la primera línea que menciona un archivo que
escribimos. En el Listado 9-1, donde escribimos código deliberadamente que
causaría el pánico, la forma de solucionarlo es no solicitar un elemento fuera del
rango de los índices del vector. Si su código entra en pánico en el futuro, deberá
determinar qué acción está tomando el código con qué valores para causar el
pánico y qué debería hacer el código en su lugar.
Rust
jueves, 22 de mayo de 2025 : Página 213 de 719
fn main() {
let greeting_file_result = File::open("hello.txt");
}
Listado 9-3: Apertura de un archivo
Rust
jueves, 22 de mayo de 2025 : Página 214 de 719
Necesitamos agregar al código del Listado 9-3 las acciones necesarias según el
valor File::opendevuelto. El Listado 9-4 muestra una forma de manejar
esto Resultusando una herramienta básica: la matchexpresión que analizamos en
el Capítulo 6.
fn main() {
let greeting_file_result = File::open("hello.txt");
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or
directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Como es habitual, este resultado nos indica exactamente qué salió mal.
fn main() {
let greeting_file_result = File::open("hello.txt");
Por ejemplo, aquí hay otra forma de escribir la misma lógica que se muestra en el
Listado 9-5, esta vez utilizando cierres y el unwrap_or_elsemétodo:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
Rust
jueves, 22 de mayo de 2025 : Página 217 de 719
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}
Usar matchfunciona bastante bien, pero puede ser un poco verboso y no siempre
comunica bien la intención. El Result<T, E>tipo tiene muchos métodos auxiliares
definidos para realizar diversas tareas más específicas. El unwrapmétodo es un
método abreviado, implementado igual que la matchexpresión que escribimos en
el Listado 9-4. Si el Resultvalor es la Okvariante, unwrapdevolverá el valor dentro
de [nombre de la variable Ok]. Si [nombre Resultde la variable] es
la Errvariante, unwrapllamará a la panic!macro automáticamente. Aquí hay un
ejemplo unwrapen acción:
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
Propagación de errores
Cuando la implementación de una función invoca algo que podría fallar, en lugar
de gestionar el error dentro de la propia función, se puede devolver el error al
código que la invoca para que este decida qué hacer. Esto se conoce
como propagación del error y otorga mayor control al código que la invoca, donde
podría haber más información o lógica que dicte cómo debe gestionarse el error
que la disponible en el contexto del código.
Por ejemplo, el Listado 9-6 muestra una función que lee un nombre de usuario de
un archivo. Si el archivo no existe o no se puede leer, esta función devolverá esos
errores al código que la llamó.
Esta función se puede escribir de forma mucho más breve, pero comenzaremos
por realizar gran parte de forma manual para explorar el manejo de errores; al
final, mostraremos la forma más breve. Veamos primero el tipo de retorno de la
función: Result<String, io::Error>. Esto significa que la función devuelve un valor
del tipo Result<T, E>, donde el parámetro genérico Tse ha rellenado con el tipo
concreto Stringy el tipo genérico Ese ha rellenado con el tipo concreto io::Error.
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
Listado 9-8: Encadenamiento de llamadas a métodos después del ?operador
El listado 9-9 muestra una forma de hacer esto aún más breve
utilizando fs::read_to_string.
Leer un archivo en una cadena es una operación bastante común, por lo que la
biblioteca estándar proporciona la fs::read_to_stringfunción conveniente que abre
el archivo, crea un nuevo String, lee el contenido del archivo, coloca el contenido
en eseString y lo devuelve. Por supuesto, usar fs::read_to_string no nos da la
oportunidad de explicar todo el manejo de errores, así que lo hicimos primero de
forma más extensa.
El ?operador solo se puede usar en funciones cuyo tipo de retorno sea compatible
con el valor ?en el que se usa. Esto se debe a que el ?operador está definido para
realizar un retorno anticipado de un valor fuera de la función, de la misma manera
que la matchexpresión que definimos en el Listado 9-6. En el Listado 9-6,
el matchusaba un Resultvalor, y el retorno anticipado arm devolvió
un Err(e) valor. El tipo de retorno de la función debe ser a Resultpara que sea
compatible con this return.
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
Listado 9-10: Intentando usar el ?en elmain función que retorna ()no se compilará.
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result`
or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4| let greeting_file = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that
returns `()`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not
implemented for `()`
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
Rust
jueves, 22 de mayo de 2025 : Página 224 de 719
4| let greeting_file = File::open("hello.txt")?;
5+ Ok(())
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous
error
Este error señala que solo se nos permite usar el ?operador en una función que
devuelve Result,Option u otro tipo que implemente FromResidual.
Para corregir el error, tiene dos opciones. Una es cambiar el tipo de retorno de su
función para que sea compatible con el valor ?en el que usa el operador, siempre
que no haya restricciones que lo impidan. La otra opción es usar una matcho una
de las siguientes:Result<T, E> métodos para manejar el Result<T, E> error de la
forma que sea adecuada.
Extrae ?la porción de cadena, y podemos llamar charsa esa porción de cadena
para obtener un iterador de sus caracteres. Nos interesa el último carácter de
esta primera línea, así que llamamos lasta para devolver el último elemento del
iterador. Esto se Optiondebe a que es posible que la primera línea sea la cadena
vacía; por ejemplo, `if` textcomienza con una línea en blanco pero tiene
caracteres en otras líneas, como en ` "\nhi". Sin embargo, si hay un último
carácter en la primera línea, se devolverá en la Somevariante. El ?operador en el
medio nos da una forma concisa de expresar esta lógica, permitiéndonos
implementar la función en una sola línea. Si no pudiéramos usar el ?operador en
` Option, tendríamos que implementar esta lógica usando más llamadas a
métodos o una matchexpresión.
Tenga en cuenta que puede usar el ?operador "on" Resulten una función que
devuelve "<sub>a</sub>" Result, así como el ?operador "on" Optionen una
función que devuelve "<sub>an </sub>" Option, pero no puede combinarlos. El ?
operador no convertirá automáticamente "a" Resulten "an" Optionni viceversa; en
esos casos, puede usar métodos como " okon" Resulto " ok_oron" Optionpara
realizar la conversión explícitamente.
Ok(())
}
Listado 9-12: Cambiar maina return Result<(), E>permite el uso del ?operador
onResult valores.
Rust
jueves, 22 de mayo de 2025 : Página 226 de 719
Ahora que hemos discutido los detalles de llamar panic!o devolver Result,
volvamos al tema de cómo decidir cuál es apropiado usar en qué casos.
De igual forma, los métodos unwrapy expectson muy útiles al crear prototipos,
antes de decidir cómo gestionar los errores. Dejan marcas claras en el código
para cuando esté listo para robustecer el programa.
Si una llamada a un método falla en una prueba, es deseable que toda la prueba
falle, incluso si ese método no es la funcionalidad bajo prueba. Porque panic!así
es como se marca una prueba como fallida, llamando unwrapoexpect es
exactamente lo que debería ocurrir.
use std::net::IpAddr;
Sin embargo, cuando se espera un fallo, es más apropiado devolver un Result que
realizar una panic!llamada. Por ejemplo, un analizador que recibe datos
incorrectos o una solicitud HTTP que devuelve un estado que indica que se ha
alcanzado un límite de velocidad. En estos casos, devolver un Resultindica que el
fallo es una posibilidad esperada y el código que realiza la llamada debe decidir
cómo gestionarlo.
Cuando tu código realiza una operación que podría poner en riesgo a un usuario si
se llama con valores no válidos, debería verificar primero la validez de los valores
y entrar en pánico si no lo son. Esto se debe principalmente a razones de
seguridad: intentar operar con datos no válidos puede exponer tu código a
vulnerabilidades. Esta es la razón principal por la que la biblioteca estándar
llamará panic!si intentas un acceso a memoria fuera de los límites: intentar
acceder a memoria que no pertenece a la estructura de datos actual es un
problema de seguridad común. Las funciones suelen tener contratos : su
comportamiento solo está garantizado si las entradas cumplen requisitos
específicos. Entrar en pánico cuando se viola el contrato tiene sentido porque una
violación del contrato siempre indica un error del lado de la llamada, y no es un
tipo de error que quieras que el código que llama tenga que gestionar
explícitamente. De hecho, no hay una forma razonable de que el código que llama
se recupere; los programadores que llaman deben corregir el código. Los
contratos de una función, especialmente cuando una violación causará un pánico,
deben explicarse en la documentación de la API de la función.
que tu función no tiene que comprobar ese caso en tiempo de ejecución. Otro
ejemplo es usar un tipo entero sin signo como u32, que garantiza que el
parámetro nunca sea negativo.
Una forma de hacer esto sería analizar la suposición como un i32en lugar de solo
un u32para permitir números potencialmente negativos y luego agregar una
verificación para que el número esté dentro del rango, de la siguiente manera:
match guess.cmp(&secret_number) {
// --snip--
}
Sin embargo, esta no es una solución ideal: si fuera absolutamente crítico que el
programa solo operara con valores entre 1 y 100, y tuviera muchas funciones con
este requisito, tener una verificación como esta en cada función sería tedioso (y
podría afectar el rendimiento).
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
Luego, implementamos una función asociada llamada " newon" Guessque crea
instancias de Guessvalores. La newfunción se define con un parámetro llamado
" valuede tipo" i32y devuelve un valor Guess. El código en el cuerpo de
la newfunción comprueba valueque esté entre 1 y 100. Si valueno supera la
Rust
jueves, 22 de mayo de 2025 : Página 232 de 719
Una función que tiene un parámetro o devuelve solo números entre 1 y 100
podría entonces declarar en su firma que toma o devuelve a Guessen lugar de
an i32y no necesitaría hacer ninguna verificación adicional en su cuerpo.
Resumen
Ahora que ha visto formas útiles en que la biblioteca estándar usa genéricos con
las Optionenumeraciones y Result, hablaremos sobre cómo funcionan los
genéricos y cómo puede usarlos en su código.
Los genéricos nos permiten reemplazar tipos específicos con un marcador que
representa varios tipos para eliminar la duplicación de código. Antes de
profundizar en la sintaxis de los genéricos, veamos cómo eliminar la duplicación
sin usar tipos genéricos, extrayendo una función que reemplaza valores
específicos con un marcador que representa varios valores. Después, aplicaremos
la misma técnica para extraer una función genérica. Al ver cómo reconocer código
duplicado que se puede extraer en una función, empezarás a reconocer código
duplicado que puede usar genéricos.
Comenzaremos con el programa corto del Listado 10-1 que encuentra el número
más grande en una lista.
Para eliminar esta duplicación, crearemos una abstracción definiendo una función
que opera sobre cualquier lista de enteros pasada como parámetro. Esta solución
simplifica nuestro código y nos permite expresar el concepto de encontrar el
número mayor en una lista de forma abstracta.
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
En resumen, estos son los pasos que seguimos para cambiar el código del Listado
10-2 al Listado 10-3:
Por ejemplo, supongamos que tenemos dos funciones: una que encuentra el
elemento más grande en un segmento de i32valores y otra que encuentra el
Rust
jueves, 22 de mayo de 2025 : Página 237 de 719
En definiciones de funciones
Al definir una función que usa genéricos, los colocamos en la firma de la función,
donde normalmente especificaríamos los tipos de datos de los parámetros y el
valor de retorno. Esto aumenta la flexibilidad del código y proporciona más
funcionalidad a quienes llaman a la función, a la vez que evita la duplicación de
código.
largest
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
Rust
jueves, 22 de mayo de 2025 : Página 240 de 719
--> src/main.rs:5:17
|
5| if item > largest {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Definiciones de estructura
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
Listado 10-6: Una Point<T>estructura que contiene xvalores yde tipoT
Rust
jueves, 22 de mayo de 2025 : Página 241 de 719
Tenga en cuenta que, dado que solo usamos un tipo genérico para
definir Point<T>, esta definición indica que la Point<T>estructura es genérica
sobre algún tipo , y queT los campos xy yson del mismo tipo, independientemente
de su tipo. Si creamos una instancia de a con valores de diferentes tipos, como en
el Listado 10-7, nuestro código no compilará.Point<T>
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
Listado 10-7: Los campos xy ydeben ser del mismo tipo porque ambos tienen el
mismo tipo de datos genérico T.
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7| let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number
For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Para definir una Pointestructura donde xy yson genéricos, pero podrían tener
tipos diferentes, podemos usar varios parámetros de tipo genérico. Por ejemplo,
Rust
jueves, 22 de mayo de 2025 : Página 242 de 719
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
Listado 10-8: Un Point<T, U>genérico sobre dos tipos de modo que xy ypueden
ser valores de diferentes tipos
En definiciones de enumeración
Al igual que hicimos con las estructuras, podemos definir enumeraciones para
almacenar tipos de datos genéricos en sus variantes. Analicemos nuevamente
la Option<T>enumeración que proporciona la biblioteca estándar, que usamos en
el capítulo 6:
enum Option<T> {
Some(T),
None,
}
Esta definición debería tener más sentido ahora. Como puede ver,
la Option<T>enumeración es genérica sobre el tipo Ty tiene dos variantes: Some,
que contiene un valor de tipo T, y una Nonevariante que no contiene ningún valor.
Al usar la Option<T>enumeración, podemos expresar el concepto abstracto de un
valor opcional y, al Option<T>ser genérica, podemos usar esta abstracción
independientemente del tipo del valor opcional.
Rust
jueves, 22 de mayo de 2025 : Página 243 de 719
En Definiciones de Métodos
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
Cuando Rust compila este código, realiza una monomorfización. Durante este
proceso, el compilador lee los valores usados en Option<T> las instancias e
identifica dos tipos de Option<T>: uno es i32y el otro es f64. Por lo tanto,
expande la definición genérica de Option<T>en dos definiciones especializadas
en i32y f64, reemplazando así la definición genérica por las específicas.
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
Definiendo un rasgo
Por ejemplo, digamos que tenemos múltiples estructuras que contienen distintos
tipos y cantidades de texto: una NewsArticleestructura que contiene una noticia
archivada en una ubicación particular y una Tweetque puede tener, como
máximo, 280 caracteres junto con metadatos que indican si fue un nuevo tweet,
un retweet o una respuesta a otro tweet.
Rust
jueves, 22 de mayo de 2025 : Página 248 de 719
Un rasgo puede tener múltiples métodos en su cuerpo: las firmas de los métodos
se enumeran una por línea y cada línea termina en punto y coma.
Ahora que hemos definido las firmas deseadas de los Summarymétodos del rasgo,
podemos implementarlo en los tipos de nuestro agregador de medios. El Listado
10-13 muestra una implementación del Summaryrasgo en
elNewsArticle estructura que utiliza el título, el autor y la ubicación para crear el
valor de retorno de summarize. Para la Tweetestructura,
definimos summarizecomo el nombre de usuario seguido del texto completo del
tuit, asumiendo que el contenido del tuit ya está limitado a 280 caracteres.
Rust
jueves, 22 de mayo de 2025 : Página 249 de 719
Nombre de archivo: src/lib.rs
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
Implementaciones predeterminadas
Ahora que sabe cómo definir e implementar rasgos, podemos explorar cómo
usarlos para definir funciones que acepten diversos tipos. Usaremos
el Summaryrasgo que implementamos en los tipos NewsArticley Tweetdel Listado
10-13 para definir una notifyfunción que invoque el summarizemétodo en
su itemparámetro, que es de algún tipo que implementa el Summary rasgo. Para
ello, usamos la impl Traitsiguiente sintaxis:
Esta forma más larga es equivalente al ejemplo de la sección anterior, pero más
detallada. Colocamos los límites de los rasgos con la declaración del parámetro de
tipo genérico después de dos puntos y entre corchetes angulares.
Usar demasiados límites de rasgo tiene sus desventajas. Cada genérico tiene sus
propios límites de rasgo, por lo que las funciones con múltiples parámetros de
Rust
jueves, 22 de mayo de 2025 : Página 255 de 719
Sin embargo, solo se puede usar impl Traitsi se devuelve un solo tipo. Por
ejemplo, este código que devuelve a NewsArticleo a Tweetcon el tipo de retorno
especificado como impl Summaryno funcionaría:
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
let s = 3.to_string();
Los rasgos y sus límites nos permiten escribir código que usa parámetros de tipo
genérico para reducir la duplicación, pero también para especificar al compilador
que queremos que el tipo genérico tenga un comportamiento específico. El
compilador puede entonces usar la información de los límites de rasgos para
verificar que todos los tipos concretos usados en nuestro código proporcionen el
comportamiento correcto. En lenguajes de tipado dinámico, obtendríamos un
error en tiempo de ejecución si invocáramos un método en un tipo que no lo
definiera. Pero Rust traslada estos errores al tiempo de compilación, por lo que
nos vemos obligados a corregir los problemas incluso antes de que nuestro código
pueda ejecutarse. Además, no tenemos que escribir código que verifique el
comportamiento en tiempo de ejecución, ya que ya lo hemos hecho en tiempo de
compilación. Esto mejora el rendimiento sin tener que renunciar a la flexibilidad
de los genéricos.
Los tiempos de vida son otro tipo de genérico que ya hemos estado usando. En
lugar de garantizar que un tipo tenga el comportamiento deseado, los tiempos de
vida garantizan que las referencias sean válidas mientras las necesitemos.
vida cuando los tiempos de vida de las referencias puedan estar relacionados de
diferentes maneras. Rust requiere que anotemos las relaciones utilizando
parámetros genéricos de tiempo de vida para garantizar que las referencias
utilizadas en tiempo de ejecución sean válidas.
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
Listado 10-16: Un intento de utilizar una referencia cuyo valor ha quedado fuera
del alcance
Nota: Los ejemplos de los Listados 10-16, 10-17 y 10-23 declaran variables sin
asignarles un valor inicial, por lo que el nombre de la variable existe en el ámbito
externo. A primera vista, esto podría parecer incompatible con el hecho de que
Rust no permite valores nulos. Sin embargo, si intentamos usar una variable
antes de asignarle un valor, obtendremos un error de compilación, lo que indica
que Rust no permite valores nulos.
El ámbito externo declara una variable llamada rsin valor inicial, y el ámbito
interno declara una variable llamada xcon el valor inicial de 5. Dentro del ámbito
Rust
jueves, 22 de mayo de 2025 : Página 260 de 719
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
5| let x = 5;
| - binding `x` declared here
6| r = &x;
| ^^ borrowed value does not live long enough
7| }
| - `x` dropped here while still borrowed
8|
9| println!("r: {r}");
| --- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
El mensaje de error indica que la variable x"no tiene una vida útil suficiente". Esto
se debe a que xquedará fuera de alcance cuando el ámbito interno termine en la
línea 7. Sin embargo, rsigue siendo válida para el ámbito externo; dado que su
alcance es mayor, decimos que "tiene una vida útil mayor". Si Rust permitiera que
este código funcionara, restaría haciendo referencia a memoria desasignada
al xquedar fuera de alcance, y cualquier acción que intentáramos realizar rno
funcionaría correctamente. Entonces, ¿cómo determina Rust que este código no
es válido? Utiliza un comprobador de préstamos.
El verificador de préstamos
fn main() {
let r; // ---------+-- 'a
// |
Rust
jueves, 22 de mayo de 2025 : Página 261 de 719
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
Listado 10-17: Anotaciones de las vidas de ry x, nombrados 'ay 'b,
respectivamente
El listado 10-18 corrige el código para que no tenga una referencia colgante y se
compile sin errores.
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {r}"); // | |
// --+ |
} // ----------+
Listado 10-18: Una referencia válida porque los datos tienen una vida útil más
larga que la referencia
Aquí, xtiene un tiempo de vida 'b, que en este caso es mayor que 'a. Esto significa
que rse puede hacer referencia x, ya que Rust sabe que la referencia en rsiempre
será válida mientras xsea válida.
Ahora que sabe qué son los tiempos de vida de las referencias y cómo Rust
analiza los tiempos de vida para garantizar que las referencias siempre sean
válidas, exploremos los tiempos de vida genéricos de los parámetros y los valores
de retorno en el contexto de las funciones.
Escribiremos una función que devuelva el fragmento de cadena más largo de dos.
Esta función tomará dos fragmentos de cadena y devolverá uno solo. Tras
implementar la longestfunción, el código del Listado 10-19 debería mostrar
`<sub> The longest string is abcd.
Tenga en cuenta que queremos que la función tome segmentos de cadena, que
son referencias, en lugar de cadenas, porque no queremos que la longestfunción
tome posesión de sus parámetros. Consulte la sección "Sectores de cadena como
parámetros" en el capítulo 4 para obtener más información sobre por qué los
parámetros que usamos en el listado 10-19 son los que necesitamos.
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
Rust
jueves, 22 de mayo de 2025 : Página 263 de 719
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature
does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Al definir esta función, desconocemos los valores concretos que se le pasarán, por
lo que desconocemos si se ejecutará el ifcaso o el elsecaso. Tampoco conocemos
la duración de las referencias que se pasarán, por lo que no podemos examinar
los ámbitos como hicimos en los Listados 10-17 y 10-18 para determinar si la
referencia que devolvemos siempre será válida. El verificador de préstamos
tampoco puede determinar esto, ya que desconoce la relación entre la duración
de xy yla duración del valor de retorno. Para corregir este error, añadiremos
parámetros de duración genéricos que definen la relación entre las referencias
para que el verificador de préstamos pueda realizar su análisis.
Las anotaciones de duración tienen una sintaxis un tanto inusual: los nombres de
los parámetros de duración deben comenzar con un apóstrofo ( ') y suelen ser
todos en minúsculas y muy cortos, como los tipos genéricos. La mayoría de las
Rust
jueves, 22 de mayo de 2025 : Página 264 de 719
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
La firma de la función ahora indica a Rust que, durante un tiempo de vida 'a, la
función toma dos parámetros, ambos fragmentos de cadena con una vida útil al
menos igual a la de un tiempo de vida 'a. La firma de la función también indica a
Rust que el fragmento de cadena devuelto por la función tendrá una vida útil al
menos igual a la de un tiempo de vida 'a. En la práctica, esto significa que la vida
útil de la referencia devuelta por la longestfunción es igual al menor de los
tiempos de vida de los valores a los que hacen referencia los argumentos de la
función. Estas relaciones son las que queremos que Rust utilice al analizar este
código.
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {result}");
}
}
Listado 10-22: Uso de la longestfunción con referencias a Stringvalores que tienen
diferentes duraciones de vida concretas
En este ejemplo, string1es válido hasta el final del ámbito externo, string2 es
válido hasta el final del ámbito interno y resulthace referencia a algo válido hasta
el final del ámbito interno. Ejecute este código y verá que el verificador de
préstamos lo aprueba; compilará e imprimirá ` The longest string is long string is
long.`.
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {result}");
}
Listado 10-23: El intento de usar result"after" string2ha quedado fuera del alcance
Rust
jueves, 22 de mayo de 2025 : Página 267 de 719
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
5| let string2 = String::from("xyz");
| ------- binding `string2` declared here
6| result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long
enough
7| }
| - `string2` dropped here while still borrowed
8| println!("The longest string is {result}");
| -------- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Como humanos, podemos observar este código y ver que string1es más largo
que string2y, por lo tanto, resultcontendrá una referencia a string1. Dado
que string1aún no ha salido del ámbito, una referencia a string1seguirá siendo
válida para la println!instrucción. Sin embargo, el compilador no puede ver que la
referencia es válida en este caso. Hemos indicado a Rust que el tiempo de vida de
la referencia devuelta por la longestfunción es igual al menor de los tiempos de
vida de las referencias pasadas. Por lo tanto, el verificador de préstamos rechaza
el código del Listado 10-23 por la posibilidad de que contenga una referencia no
válida.
Intenta diseñar más experimentos que varíen los valores y la duración de las
referencias pasadas a la longestfunción y cómo se usa la referencia devuelta.
Formula hipótesis sobre si tus experimentos pasarán la verificación de préstamos
antes de compilar; luego, ¡verifica si aciertas!
Al devolver una referencia desde una función, el parámetro de duración del tipo
de retorno debe coincidir con el parámetro de duración de uno de los parámetros.
Si la referencia devuelta no se refiere a uno de los parámetros, debe referirse a
un valor creado dentro de esta función. Sin embargo, esto sería una referencia
pendiente, ya que el valor quedaría fuera del ámbito al final de la función.
Considere este intento de implementación de la longestfunción que no compila:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
Rust
jueves, 22 de mayo de 2025 : Página 269 de 719
| returns a value referencing data owned by the current function
| `result` is borrowed here
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Hasta ahora, todas las estructuras definidas contienen tipos propios. Podemos
definir estructuras para contener referencias, pero en ese caso necesitaríamos
añadir una anotación de duración a cada referencia en la definición de la
estructura. El listado 10-24 contiene una estructura llamada ImportantExcerptque
contiene una porción de cadena.
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
Listado 10-24: Una estructura que contiene una referencia, que requiere una
anotación de duración
Rust
jueves, 22 de mayo de 2025 : Página 270 de 719
Esta estructura tiene un único campo partque contiene una porción de cadena,
que es una referencia. Al igual que con los tipos de datos genéricos, declaramos
el nombre del parámetro de duración genérico entre corchetes angulares después
del nombre de la estructura para poder usarlo en el cuerpo de la definición de la
estructura. Esta anotación significa que una instancia de ImportantExcerptno
puede sobrevivir a la referencia que contiene en su...part campo.
Has aprendido que cada referencia tiene un tiempo de vida y que es necesario
especificar parámetros de tiempo de vida para funciones o estructuras que usan
referencias. Sin embargo, teníamos una función en el Listado 4-9, que se muestra
de nuevo en el Listado 10-25, que se compilaba sin anotaciones de tiempo de
vida.
&s[..]
}
Listado 10-25: Una función que definimos en el Listado 4-9 que se compiló sin
anotaciones de duración, aunque el parámetro y el tipo de retorno son referencias
Tras escribir mucho código en Rust, el equipo descubrió que los programadores
introducían las mismas anotaciones de duración una y otra vez en situaciones
específicas. Estas situaciones eran predecibles y seguían ciertos patrones
deterministas. Los desarrolladores programaron estos patrones en el código del
compilador para que el verificador de préstamos pudiera inferir las duraciones en
estas situaciones y no necesitara anotaciones explícitas.
Luego, el compilador aplica la primera regla, que especifica que cada parámetro
tiene su propio tiempo de vida. La llamaremos 'acomo siempre, por lo que ahora
la firma es esta:
Ahora todas las referencias en esta firma de función tienen tiempos de vida, y el
compilador puede continuar su análisis sin necesidad de que el programador
anote los tiempos de vida en esta firma de función.
Apliquemos la primera regla: cada parámetro tiene su propio tiempo de vida. Esta
vez, tenemos dos parámetros en lugar de uno, por lo que tenemos dos tiempos de
vida:
Rust
jueves, 22 de mayo de 2025 : Página 273 de 719
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
Se puede observar que la segunda regla no aplica porque hay más de un tiempo
de vida de entrada. La tercera regla tampoco aplica, porque longestes una
función en lugar de un método, por lo que ninguno de los parámetros es self. Tras
aplicar las tres reglas, aún no hemos determinado el tiempo de vida del tipo de
retorno. Por eso recibimos un error al intentar compilar el código del Listado 10-
20: el compilador aplicó las reglas de elisión de tiempo de vida, pero no pudo
determinar todos los tiempos de vida de las referencias en la firma.
Dado que la tercera regla realmente solo se aplica en las firmas de métodos,
analizaremos los tiempos de vida en ese contexto a continuación para ver por qué
la tercera regla significa que no tenemos que anotar tiempos de vida en las firmas
de métodos muy a menudo.
En las firmas de métodos dentro del implbloque, las referencias pueden estar
vinculadas a la duración de las referencias en los campos de la estructura, o
pueden ser independientes. Además, las reglas de elisión de duración suelen
impedir que las anotaciones de duración sean necesarias en las firmas de
métodos. Veamos algunos ejemplos con la estructura
denominada ImportantExcerptque definimos en el Listado 10-24.
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
Rust
jueves, 22 de mayo de 2025 : Página 274 de 719
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}
Hay dos tiempos de vida de entrada, por lo que Rust aplica la primera regla de
elisión de tiempos de vida y proporciona ambos &selftiempos announcementde
vida. Como uno de los parámetros es &self, el tipo de retorno obtiene el tiempo
de vida de &self, y se han tenido en cuenta todos los tiempos de vida.
La vida estática
Un tiempo de vida especial que debemos analizar es 'static, que indica que la
referencia afectada puede existir durante toda la duración del programa. Todos
los literales de cadena tienen un 'statictiempo de vida, que podemos anotar de la
siguiente manera:
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {ann}");
if x.len() > y.len() {
x
} else {
y
}
}
Resumen
¡Hemos cubierto mucho en este capítulo! Ahora que conoces los parámetros de
tipo genéricos, los rasgos y los límites de rasgos, y los parámetros de duración de
vida genéricos, estás listo para escribir código sin repeticiones que funcione en
diversas situaciones. Los parámetros de tipo genéricos te permiten aplicar el
código a diferentes tipos. Los rasgos y los límites de rasgos garantizan que,
Rust
jueves, 22 de mayo de 2025 : Página 276 de 719
Aunque parezca increíble, hay mucho más que aprender sobre los temas que
tratamos en este capítulo: el capítulo 18 trata sobre los objetos de rasgos, que
son otra forma de usar rasgos. También existen escenarios más complejos que
involucran anotaciones de duración que solo necesitará en casos muy avanzados;
para ello, le recomendamos leer la Referencia de Rust . A continuación, aprenderá
a escribir pruebas en Rust para asegurarse de que su código funcione
correctamente.
Las pruebas son una habilidad compleja: aunque no podemos cubrir en un solo
capítulo todos los detalles sobre cómo escribir buenas pruebas, en este capítulo
analizaremos la mecánica de las herramientas de prueba de Rust. Hablaremos
sobre las anotaciones y macros disponibles al escribir las pruebas, el
comportamiento predeterminado y las opciones para ejecutarlas, y cómo
organizar las pruebas en pruebas unitarias y de integración.
Las pruebas son funciones de Rust que verifican que el código no relacionado con
las pruebas funcione correctamente. Los cuerpos de las funciones de prueba
suelen realizar estas tres acciones:
En su forma más simple, una prueba en Rust es una función anotada con
el test atributo. Los atributos son metadatos sobre fragmentos de código de Rust;
un ejemplo es el deriveatributo que usamos con estructuras en el Capítulo 5. Para
convertir una función en una función de prueba, añada #[test]la línea antes de fn.
Al ejecutar las pruebas con el cargo testcomando, Rust crea un binario del
ejecutor de pruebas que ejecuta las funciones anotadas e informa si cada función
de prueba es correcta o incorrecta.
Cada vez que creamos un nuevo proyecto de biblioteca con Cargo, se genera
automáticamente un módulo de pruebas con una función de prueba. Este módulo
te proporciona una plantilla para escribir tus pruebas, así no tienes que consultar
Rust
jueves, 22 de mayo de 2025 : Página 278 de 719
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Listado 11-1: El código generado automáticamente porcargo new
El archivo comienza con una addfunción de ejemplo, para que tengamos algo que
probar.
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
Running unittests src/lib.rs (file:///projects/adder/target/debug/deps/adder-
7acb243c25ffd9dc)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
Listado 11-2: El resultado de ejecutar la prueba generada automáticamente
Es posible marcar una prueba como ignorada para que no se ejecute en una
instancia específica; lo explicaremos en la sección "Ignorar algunas pruebas a
menos que se solicite específicamente" más adelante en este capítulo. Como no
lo hemos hecho aquí, el resumen muestra 0 ignored.
Rust
jueves, 22 de mayo de 2025 : Página 280 de 719
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
Ahora añadiremos otra prueba, pero esta vez crearemos una prueba que falla. Las
pruebas fallan cuando algo en la función de prueba entra en pánico. Cada prueba
se ejecuta en un nuevo hilo, y cuando el hilo principal detecta que un hilo de
prueba ha finalizado, la prueba se marca como fallida. En el capítulo 9,
explicamos cómo la forma más sencilla de entrar en pánico es llamar a la panic!
macro. Introduzca la nueva prueba como una función llamada another, de modo
que su archivo src/lib.rs se vea como el Listado 11-3.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn another() {
Rust
jueves, 22 de mayo de 2025 : Página 282 de 719
panic!("Make this test fail");
}
}
Listado 11-3: Agregar una segunda prueba que fallará porque llamamos a
lapanic! macro
Ejecute las pruebas de nuevo con cargo test. El resultado debería ser similar al
del Listado 11-4, que muestra si la explorationprueba fue exitosa o anotherfallida.
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok
failures:
failures:
tests::another
Ahora que has visto cómo se ven los resultados de las pruebas en diferentes
escenarios, veamos algunas macros distintas a las anteriores.panic! son útiles en
las pruebas.
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Listado 11-5: La Rectangleestructura y su can_holdmétodo del Capítulo 5
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
}
Listado 11-6: Una prueba can_holdque verifica si un rectángulo más grande puede
realmente contener un rectángulo más pequeño
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 1 test
test tests::larger_can_hold_smaller ... ok
Rust
jueves, 22 de mayo de 2025 : Página 285 de 719
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
¡Sí que funciona! Añadamos otra prueba, esta vez afirmando que un rectángulo
más pequeño no puede contener un rectángulo más grande:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
// --snip--
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Rust
jueves, 22 de mayo de 2025 : Página 286 de 719
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
¡Dos pruebas que pasan! Ahora veamos qué sucede con los resultados de
nuestras pruebas cuando introducimos un error en el código. Modificaremos la
implementación del can_hold método reemplazando el signo "mayor que" por uno
"menor que" al comparar los anchos:
// --snip--
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width < other.width && self.height > other.height
}
}
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok
failures:
failures:
tests::larger_can_hold_smaller
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
Rust
jueves, 22 de mayo de 2025 : Página 288 de 719
let result = add_two(2);
assert_eq!(result, 4);
}
}
Listado 11-7: Prueba de la función add_twousando la assert_eq!macro
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Rust
jueves, 22 de mayo de 2025 : Página 289 de 719
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... FAILED
failures:
failures:
tests::it_adds_two
semana en que ejecutamos las pruebas, lo mejor sería afirmar que la salida de la
función no es igual a la entrada.
Bajo la superficie, las macros assert_eq!y assert_ne!usan los operadores ==y !=,
respectivamente. Cuando las aserciones fallan, estas macros imprimen sus
argumentos usando formato de depuración, lo que significa que los valores que se
comparan deben implementar los rasgos PartialEqy Debug. Todos los tipos
primitivos y la mayoría de los tipos de la biblioteca estándar implementan estos
rasgos. Para las estructuras y enumeraciones que defina usted mismo, deberá
implementar PartialEqpara afirmar la igualdad de esos tipos. También deberá
implementar Debugpara imprimir los valores cuando la aserción falla. Debido a
que ambos rasgos son rasgos derivables, como se menciona en el Listado 5-12
del Capítulo 5, esto suele ser tan sencillo como agregar la #[derive(PartialEq,
Debug)]anotación a su definición de estructura o enumeración. Consulte el
Apéndice C, “Rasgos derivables”, para obtener más detalles sobre estos y otros
rasgos derivables.
Por ejemplo, digamos que tenemos una función que saluda a las personas por su
nombre y queremos probar que el nombre que pasamos a la función aparece en
la salida:
#[cfg(test)]
Rust
jueves, 22 de mayo de 2025 : Página 291 de 719
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
failures:
Rust
jueves, 22 de mayo de 2025 : Página 292 de 719
tests::greeting_contains_name
Este resultado solo indica que la aserción falló y en qué línea se encuentra. Un
mensaje de error más útil imprimiría el valor de la greetingfunción. Añadamos un
mensaje de error personalizado compuesto por una cadena de formato con un
marcador de posición con el valor real obtenido de la greetingfunción:
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{result}`"
);
}
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.93s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
failures:
tests::greeting_contains_name
El listado 11-8 muestra una prueba que verifica que las condiciones de
error Guess::new ocurren cuando esperamos que sucedan.
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Rust
jueves, 22 de mayo de 2025 : Página 294 de 719
Guess::new(200);
}
}
Listado 11-8: Prueba de que una condición causará unapanic!
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/guessing_game-
57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
Doc-tests guessing_game
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
failures:
tests::greater_than_100
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
} else if value > 100 {
Rust
jueves, 22 de mayo de 2025 : Página 296 de 719
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
Listado 11-9: Prueba de un panic!con un mensaje de pánico que contiene una
subcadena especificada
if value < 1 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
Rust
jueves, 22 de mayo de 2025 : Página 297 de 719
);
}
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/guessing_game-
57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
failures:
tests::greater_than_100
El mensaje de error indica que esta prueba sí generó un error como esperábamos,
pero no incluyó la cadena esperada less than or equal to 100. El mensaje de error
que recibimos en este caso fue: « Guess value must be greater than or equal to 1,
got 200.¡Ahora podemos empezar a averiguar dónde está el error!».
Hasta ahora, todas nuestras pruebas entran en pánico al fallar. También podemos
escribir pruebas que usen Result<T, E>!. Aquí está la prueba del Listado 11-1,
reescrita para usar Result<T, E>y devolver un Erren lugar de entrar en pánico:
#[test]
fn it_works() -> Result<(), String> {
Rust
jueves, 22 de mayo de 2025 : Página 298 de 719
let result = add(2, 2);
if result == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
Ahora que conoce varias formas de escribir pruebas, veamos qué sucede cuando
ejecutamos nuestras pruebas y exploremos las diferentes opciones que podemos
usar con cargo test.
del archivo] cargo testseguidos del separador --y luego los que se ejecutan en el
binario de prueba. "Ejecutar" cargo test --helpmuestra las opciones que se
pueden usar con [nombre del archivo cargo test] y "ejecutar" cargo test -- --
helpmuestra las opciones que se pueden usar después del separador. Estas
opciones también se documentan en la sección "Pruebas" del libro de rustc .
Por ejemplo, supongamos que cada prueba ejecuta código que crea un archivo en
el disco llamado test-output.txt y escribe datos en él. Cada prueba lee los datos
de ese archivo y confirma que contiene un valor específico, que varía en cada
prueba. Dado que las pruebas se ejecutan simultáneamente, una podría
sobrescribir el archivo entre la escritura y la lectura de otra. La segunda prueba
fallará, no porque el código sea incorrecto, sino porque las pruebas han
interferido entre sí al ejecutarse en paralelo. Una solución es asegurarse de que
cada prueba escriba en un archivo diferente; otra solución es ejecutar las pruebas
una a la vez.
A modo de ejemplo, el Listado 11-10 tiene una función tonta que imprime el valor
de su parámetro y devuelve 10, así como una prueba que pasa y una prueba que
falla.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(value, 10);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(value, 5);
}
}
Listado 11-10: Pruebas para una función que llamaprintln!
Cuando ejecutamos estas pruebas con cargo test, veremos el siguiente resultado:
$ cargo test
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/silly_function-
160869f38cff9166)
Rust
jueves, 22 de mayo de 2025 : Página 301 de 719
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
failures:
failures:
tests::this_test_will_fail
Tenga en cuenta que en esta salida no aparece I got the value 4, que se imprime
al ejecutarse la prueba que supera el resultado. Esta salida ya se ha capturado. La
salida de la prueba fallida, I got the value 8, aparece en la sección de la salida del
resumen de la prueba, que también muestra la causa del fallo.
Cuando ejecutamos nuevamente las pruebas del Listado 11-10 con la --show-
outputbandera, vemos el siguiente resultado:
running 2 tests
test tests::this_test_will_fail ... FAILED
Rust
jueves, 22 de mayo de 2025 : Página 302 de 719
test tests::this_test_will_pass ... ok
successes:
successes:
tests::this_test_will_pass
failures:
failures:
tests::this_test_will_fail
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
#[test]
fn add_three_and_two() {
let result = add_two(3);
assert_eq!(result, 5);
}
#[test]
fn one_hundred() {
let result = add_two(100);
assert_eq!(result, 102);
}
}
Listado 11-11: Tres pruebas con tres nombres diferentes
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
Doc-tests adder
running 0 tests
Rust
jueves, 22 de mayo de 2025 : Página 304 de 719
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
running 1 test
test tests::one_hundred ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in
0.00s
running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
Rust
jueves, 22 de mayo de 2025 : Página 305 de 719
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in
0.00s
Este comando ejecutó todas las pruebas con addel nombre y filtró la prueba
denominada one_hundred. Tenga en cuenta también que el módulo en el que
aparece una prueba se convierte en parte del nombre de la prueba, por lo que
podemos ejecutar todas las pruebas de un módulo filtrando por su nombre.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
}
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
Rust
jueves, 22 de mayo de 2025 : Página 306 de 719
running 2 tests
test tests::expensive_test ... ignored
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in
0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
running 1 test
test expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in
0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
Organización de pruebas
Rust
jueves, 22 de mayo de 2025 : Página 307 de 719
Como se mencionó al principio del capítulo, las pruebas son una disciplina
compleja, y cada persona utiliza una terminología y organización distintas. La
comunidad de Rust considera las pruebas en dos categorías principales: pruebas
unitarias y pruebas de integración. Las pruebas unitarias son pequeñas y más
específicas, prueban un módulo de forma aislada a la vez y pueden probar
interfaces privadas. Las pruebas de integración son completamente externas a la
biblioteca y utilizan el código de la misma manera que cualquier otro código
externo, utilizando solo la interfaz pública y potencialmente ejercitando varios
módulos por prueba.
Escribir ambos tipos de pruebas es importante para garantizar que las piezas de
su biblioteca hagan lo que usted espera, por separado y en conjunto.
Pruebas unitarias
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
Rust
jueves, 22 de mayo de 2025 : Página 309 de 719
fn internal() {
let result = internal_adder(2, 2);
assert_eq!(result, 4);
}
}
Listado 11-12: Prueba de una función privada
Pruebas de integración
El directorio de pruebas
Creemos una prueba de integración. Con el código del Listado 11-12 aún en el
archivo src/lib.rs , cree un directorio llamado tests y un nuevo archivo
llamado tests/integration_test.rs . La estructura de su directorio debería ser
similar a la siguiente:
adder
├── Cargo.lock
├── Cargo.toml
Rust
jueves, 22 de mayo de 2025 : Página 310 de 719
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
Listado 11-13: Una prueba de integración de una función en el addercajón
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Rust
jueves, 22 de mayo de 2025 : Página 311 de 719
La primera sección para las pruebas unitarias es la misma que hemos estado
viendo: una línea para cada prueba unitaria (una nombrada internalque agregamos
en el Listado 11-12) y luego una línea de resumen para las pruebas unitarias.
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
A medida que agregue más pruebas de integración, podría ser útil crear más
archivos en el directorio "tests" para organizarlos mejor; por ejemplo, puede
agrupar las funciones de prueba según la funcionalidad que estén probando.
Rust
jueves, 22 de mayo de 2025 : Página 312 de 719
Como se mencionó anteriormente, cada archivo del directorio "tests " se compila
como su propio crate, lo cual resulta útil para crear ámbitos independientes que
imiten con mayor precisión el uso que los usuarios finales le darán. Sin embargo,
esto significa que los archivos del directorio "tests" no se comportan igual que los
archivos del directorio "src ", como se explicó en el capítulo 7 sobre cómo separar
el código en módulos y archivos.
pub fn setup() {
// setup code specific to your library's tests would go here
}
Cuando volvamos a ejecutar las pruebas, veremos una nueva sección en la salida
de la prueba para el archivo common.rs , aunque este archivo no contiene
ninguna función de prueba ni llamamos a la setupfunción desde ningún lugar:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test it_adds_two ... ok
Rust
jueves, 22 de mayo de 2025 : Página 313 de 719
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
use adder::add_two;
mod common;
#[test]
Rust
jueves, 22 de mayo de 2025 : Página 314 de 719
fn it_adds_two() {
common::setup();
Esta es una de las razones por las que los proyectos de Rust que proporcionan un
binario tienen un archivo src/main.rs sencillo que invoca la lógica contenida en el
archivo src/lib.rs . Con esta estructura, las pruebas de integración pueden probar
la biblioteca usepara que la funcionalidad importante esté disponible. Si la
funcionalidad importante funciona, la pequeña cantidad de código en el
archivo src/main.rs también funcionará, y no es necesario probarla.
Resumen
Las funciones de prueba de Rust permiten especificar cómo debe funcionar el
código para garantizar que siga funcionando según lo previsto, incluso al realizar
cambios. Las pruebas unitarias evalúan las diferentes partes de una biblioteca por
separado y pueden probar detalles de implementación privados. Las pruebas de
integración comprueban que varias partes de la biblioteca funcionen
correctamente juntas y utilizan la API pública de la biblioteca para probar el
código de la misma forma que lo haría el código externo. Si bien el sistema de
tipos y las reglas de propiedad de Rust ayudan a prevenir algunos tipos de
errores, las pruebas siguen siendo importantes para reducir los errores lógicos
relacionados con el comportamiento esperado del código.
Rust
jueves, 22 de mayo de 2025 : Página 315 de 719
A lo largo del proceso, mostraremos cómo hacer que nuestra herramienta de línea
de comandos utilice las funciones de terminal que muchas otras herramientas de
línea de comandos utilizan. Leeremos el valor de una variable de entorno para
que el usuario pueda configurar el comportamiento de nuestra herramienta.
También imprimiremos los mensajes de error en el flujo de la consola de error
estándar ( stderr) en lugar de la salida estándar ( stdout) para que, por ejemplo, el
usuario pueda redirigir la salida correcta a un archivo sin dejar de ver los
mensajes de error en pantalla.
Nuestro grepproyecto combinará una serie de conceptos que has aprendido hasta
ahora:
Rust
jueves, 22 de mayo de 2025 : Página 316 de 719
un iterador para convertirlo en una colección, como un vector, que contiene todos
los elementos que produce.
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
Listado 12-1: Recopilación de los argumentos de la línea de comandos en un vector e impresión de los
mismos
Tenga en cuenta que std::env::argsse generará un pánico si algún argumento contiene Unicode no
válido. Si su programa necesita aceptar argumentos que contienen Unicode no válido,
utilice std::env::args_os``. Esta función devuelve un iterador que produce OsStringvalores en lugar
de String``. Hemos optado por usar std::env::args`` para simplificar, ya que OsStringlos valores
varían según la plataforma y son más complejos de manejar que Stringlos valores.
anotar tipos en Rust, collectes una función que sí se necesita anotar a menudo, ya
que Rust no puede inferir el tipo de colección que se desea.
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/minigrep`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
]
$ cargo run -- needle haystack
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
Running `target/debug/minigrep needle haystack`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
"needle",
"haystack",
]
fn main() {
let args: Vec<String> = env::args().collect();
Como vimos al imprimir el vector, el nombre del programa ocupa el primer valor
del vector en [<sub>(<sub>) args[0]; por lo tanto, los argumentos comienzan en
el índice 1. El primer argumento minigreptoma la cadena que buscamos, por lo que
incluimos una referencia al primer argumento en la variable
[<sub>(<sub> query). El segundo argumento será la ruta del archivo, por lo que
incluimos una referencia al segundo argumento en la variable
[<sub>(<sub>) file_path.
Leyendo un archivo
Ahora añadiremos la funcionalidad para leer el archivo especificado en
el file_path argumento. Primero, necesitamos un archivo de ejemplo para probarlo:
usaremos un archivo con poco texto en varias líneas y algunas palabras
repetidas. El Listado 12-3 contiene un poema de Emily Dickinson que funcionará
bien. Crea un archivo llamado poem.txt en la raíz de tu proyecto e introduce el
poema "¡No soy nadie! ¿Quién eres tú?".
Con el texto en su lugar, edite src/main.rs y agregue código para leer el archivo,
como se muestra en el Listado 12-4.
fn main() {
// --snip--
println!("In file {file_path}");
println!("With text:\n{contents}");
}
Listado 12-4: Lectura del contenido del archivo especificado por el segundo argumento
Ejecutemos este código con cualquier cadena como primer argumento de la línea
de comando (porque aún no hemos implementado la parte de búsqueda) y el
archivo poem.txt como segundo argumento:
¡Genial! El código leyó e imprimió el contenido del archivo. Sin embargo, presenta
algunas fallas. Actualmente, la mainfunción tiene múltiples responsabilidades:
generalmente, las funciones son más claras y fáciles de mantener si cada una se
encarga de una sola idea. El otro problema es que no estamos gestionando los
errores tan bien como podríamos. El programa aún es pequeño, así que estas
fallas no representan un gran problema, pero a medida que crezca, será más
difícil corregirlas correctamente. Es recomendable comenzar a refactorizar desde
el principio del desarrollo, ya que es mucho más fácil refactorizar pequeñas
cantidades de código. Lo haremos a continuación.
Este problema también se relaciona con el segundo: aunque queryy file_path son
variables de configuración de nuestro programa, las variables como contentsse
utilizan para ejecutar la lógica del programa. Cuanto más largo sea main sea el
programa, más variables necesitaremos incluir en el alcance; cuantas más
variables tengamos en el alcance, más difícil será comprender el propósito de
Rust
jueves, 22 de mayo de 2025 : Página 322 de 719
cada una. Es mejor agrupar las variables de configuración en una sola estructura
para aclarar su propósito.
// --snip--
}
(query, file_path)
}
Listado 12-5: Extracción de una parse_configfunción demain
Esta modificación puede parecer excesiva para nuestro pequeño programa, pero
la estamos refactorizando poco a poco. Después de realizar este cambio, vuelva a
ejecutar el programa para verificar que el análisis de argumentos siga
funcionando. Es recomendable revisar el progreso con frecuencia para identificar
la causa de los problemas cuando ocurran.
// --snip--
}
struct Config {
query: String,
file_path: String,
Rust
jueves, 22 de mayo de 2025 : Página 325 de 719
}
Hemos añadido una estructura llamada Configdefinida para contener los campos
llamados queryy file_path. La firma de parse_configahora indica que devuelve
un Configvalor. En el cuerpo de parse_config, donde antes devolvíamos fragmentos
de cadena que referenciaban Stringvalores en args, ahora la definimos Config para
que contenga Stringvalores propios. La argsvariable en maines la propietaria de los
valores de los argumentos y solo permite que la parse_configfunción los tome
prestados, lo que significa que violaríamos las reglas de préstamo de Rust
si Configintentáramos tomar posesión de los valores en . args .
Hay varias maneras de gestionar los Stringdatos; la más sencilla, aunque algo
ineficiente, es llamar al clonemétodo en los valores. Esto creará una copia
completa de los datos para la Configinstancia, lo que requiere más tiempo y
memoria que almacenar una referencia a los datos de la cadena. Sin embargo,
clonar los datos también simplifica mucho nuestro código, ya que no tenemos que
gestionar la duración de las referencias; en este caso, sacrificar algo de
rendimiento para ganar simplicidad es una buena compensación.
Muchos rustáceos tienden a evitar usar clonepara solucionar problemas de propiedad debido a su costo
en tiempo de ejecución. En el Capítulo 13 , aprenderás a usar métodos más eficientes en este tipo de
situaciones. Pero por ahora, puedes copiar algunas cadenas para seguir progresando, ya que solo las
harás una vez y la ruta de archivo y la cadena de consulta son muy pequeñas. Es mejor tener un
programa funcional que sea un poco ineficiente que intentar hiperoptimizar el código en la primera
pasada. A medida que adquieras más experiencia con Rust, será más fácil comenzar con la solución
más eficiente, pero por ahora, es perfectamente aceptable llamar a clone.
Ahora nuestro código transmite con mayor claridad que " queryy" file_pathestán
relacionados y que su propósito es configurar el funcionamiento del programa.
Cualquier código que utilice estos valores sabe cómo encontrarlos en
la configinstancia, en los campos designados para su propósito.
// --snip--
}
// --snip--
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Esta línea index out of bounds: the len is 1 but the index is 1 es un mensaje de error
dirigido a programadores. No ayudará a nuestros usuarios finales a entender qué
deben hacer. Vamos a solucionarlo ahora.
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Esta salida es mejor: ahora tenemos un mensaje de error razonable. Sin embargo,
también tenemos información superflua que no queremos compartir con nuestros
usuarios. Quizás la técnica que usamos en el Listado 9-13 no sea la más
adecuada: una llamada a panic!es más apropiada para un problema de
programación que para uno de uso, como se explicó en el Capítulo 9. En su lugar,
usaremos la otra técnica que aprendiste en el Capítulo 9: devolver un Result que
indica éxito o error.
El listado 12-9 muestra los cambios que debemos realizar en el valor de retorno
de la función que estamos llamando Config::buildy en el cuerpo de la función
necesario para devolver un Result. Tenga en cuenta que esto no se compilará
hasta que actualicemos maintambién, lo cual haremos en el siguiente listado.
impl Config {
Rust
jueves, 22 de mayo de 2025 : Página 329 de 719
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
fn main() {
let args: Vec<String> = env::args().collect();
// --snip--
Listado 12-10: Salir con un código de error si Configfalla la construcción
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
run(config);
}
fn run(config: Config) {
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
// --snip--
Listado 12-11: Extracción de una runfunción que contiene el resto de la lógica del programa
La runfunción ahora contiene toda la lógica restante de main, comenzando por leer
el archivo. La runfunción toma la Configinstancia como argumento.
// --snip--
Ok(())
}
Listado 12-12: Cambiar la runfunción a devolverResult
Rust nos indica que nuestro código ignoró el Resultvalor, lo Resultcual podría
indicar un error. Sin embargo, no estamos comprobando si hubo un error, y el
compilador nos recuerda que probablemente pretendíamos tener código de
gestión de errores. Corrijamos ese problema ahora.
fn main() {
// --snip--
la Configinstancia. Dado que run`devuelve` ()en caso de éxito, solo nos importa
detectar un error, por lo que no necesitamos unwrap_or_elsedevolver el valor sin
encapsular, que solo sería `` ().
La rundefinición de la función
Las usedeclaraciones pertinentes
La definición deConfig
La Config::builddefinición de la función
El contenido de src/lib.rs debe tener las firmas que se muestran en el Listado 12-
13 (para mayor brevedad, hemos omitido los cuerpos de las funciones). Tenga en
cuenta que esto no se compilará hasta que modifiquemos src/main.rs en el
Listado 12-14.
use std::error::Error;
use std::fs;
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
// --snip--
}
}
use minigrep::Config;
fn main() {
// --snip--
if let Err(e) = minigrep::run(config) {
// --snip--
}
}
Listado 12-14: Uso de la minigrepbiblioteca crate en src/main.rs
¡Uf! Fue mucho trabajo, pero nos hemos preparado para el éxito en el futuro.
Ahora es mucho más fácil gestionar los errores y hemos hecho el código más
modular. De ahora en adelante, casi todo nuestro trabajo se realizará
en src/lib.rs .
Aprovechemos esta nueva modularidad haciendo algo que hubiera sido difícil con
el código antiguo pero que es fácil con el nuevo: ¡escribiremos algunas pruebas!
1. Escriba una prueba que falle y ejecútela para asegurarse de que falle por el
motivo esperado.
2. Escriba o modifique el código suficiente para que la nueva prueba pase.
3. Refactorice el código que acaba de agregar o cambiar y asegúrese de que
las pruebas sigan pasando.
4. ¡Repita desde el paso 1!
Aunque es solo una de las muchas maneras de escribir software, TDD puede
ayudar a impulsar el diseño de código. Escribir la prueba antes de escribir el
código que la aprueba ayuda a mantener una alta cobertura de pruebas durante
todo el proceso.
#[cfg(test)]
mod tests {
use super::*;
#[test]
Rust
jueves, 22 de mayo de 2025 : Página 337 de 719
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
Esta prueba busca la cadena "duct". El texto que buscamos consta de tres líneas,
de las cuales solo una contiene"duct" texto incompleto]. (Tenga en cuenta que la
barra invertida después de la comilla doble inicial indica a Rust que no debe
colocar un carácter de nueva línea al principio del contenido de esta cadena
literal). Afirmamos que el valor devuelto por la searchfunción contiene solo la línea
esperada.
Aún no podemos ejecutar esta prueba y ver si falla porque ni siquiera se compila:
¡la searchfunción aún no existe! De acuerdo con los principios de TDD,
agregaremos el código justo para que la prueba se compile y se ejecute
agregando una definición desearch función que siempre devuelve un vector vacío,
como se muestra en el Listado 12-16. Entonces, la prueba debería compilarse y
fallar porque un vector vacío no coincide con un vector que contenga la
línea."safe, fast, productive."
Tenga en cuenta que debemos definir un tiempo de vida explícito 'aen la firma
de searchy usar ese tiempo de vida con el contentsargumento y el valor de retorno.
Recuerde que en el Capítulo 10 los parámetros de tiempo de vida especifican qué
tiempo de vida del argumento está conectado con el tiempo de vida del valor de
retorno. En este caso, indicamos que el vector devuelto debe contener segmentos
de cadena que hagan referencia a segmentos del argumento. contents (en lugar
del argumento query).
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:28:51
|
28 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not
say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
|
28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to 1 previous error
Rust no puede saber cuál de los dos argumentos necesitamos, así que debemos
decírselo explícitamente. Dado que contentses el argumento que contiene todo
nuestro texto y queremos devolver las partes de ese texto que coinciden,
sabemos...contents es" es el argumento que debe conectarse al valor de retorno
mediante la sintaxis de duración.
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.97s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... FAILED
Rust
jueves, 22 de mayo de 2025 : Página 339 de 719
failures:
failures:
tests::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Rust cuenta con un método útil para gestionar la iteración línea por línea de
cadenas, convenientemente llamado lines, que funciona como se muestra en el
Listado 12-17. Tenga en cuenta que esto aún no se compila.
Para completar esta función, necesitamos una forma de almacenar las líneas
coincidentes que queremos devolver. Para ello, podemos crear un vector mutable
antes del forbucle y llamar al pushmétodo para almacenar "a" lineen el vector.
Después del forbucle, devolvemos el vector, como se muestra en el Listado 12-19.
results
}
Listado 12-19: Almacenar las líneas que coinciden para que podamos devolverlas
Ahora la searchfunción debería devolver solo las líneas que contienen query, y
nuestra prueba debería ser exitosa. Ejecutemos la prueba:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Ok(())
}
¡Ahora debería funcionar todo el programa! Probémoslo, primero con una palabra
que debería devolver exactamente una línea del poema de Emily Dickinson: rana .
¡Genial! Ahora probemos con una palabra que coincida con varias líneas,
como "body" :
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Rust
jueves, 22 de mayo de 2025 : Página 344 de 719
Pick three.
Duct tape.";
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Listado 12-20: Agregar una nueva prueba fallida para la función que no distingue entre mayúsculas y
minúsculas que estamos a punto de agregar
Tenga en cuenta que también hemos editado las pruebas anteriores contents.
Hemos añadido una nueva línea con el texto con D"Duct tape." mayúscula , que no
debería coincidir con la consulta al buscar con distinción entre mayúsculas y
minúsculas. Al modificar la prueba anterior de esta manera, nos aseguramos de
no interrumpir accidentalmente la función de búsqueda con distinción entre
mayúsculas y minúsculas que ya implementamos. Esta prueba debería ser
correcta ahora y debería seguir siendo correcta a medida que trabajamos en la
búsqueda sin distinción entre mayúsculas y minúsculas. "duct"
Implementando la search_case_insensitivefunción
Rust
jueves, 22 de mayo de 2025 : Página 345 de 719
results
}
Listado 12-21: Definición de la search_case_insensitivefunción para convertir en minúsculas la consulta y
la línea antes de compararlas
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Ok(())
}
Listado 12-22: Llamar a cualquiera de los dos searcho search_case_insensitiveen función del valor
enconfig.ignore_case
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
Ok(Config {
Rust
jueves, 22 de mayo de 2025 : Página 348 de 719
query,
file_path,
ignore_case,
})
}
}
Listado 12-23: Comprobación de cualquier valor en una variable de entorno denominadaIGNORE_CASE
Aquí, creamos una nueva variable, ignore_case. Para establecer su valor, llamamos
a la env::varfunción y le pasamos el nombre de la IGNORE_CASEvariable de entorno.
La env::varfunción devuelve una Result, que será la Okvariante correcta que
contiene el valor de la variable de entorno si esta tiene cualquier valor. Devolverá
la Errvariante si la variable de entorno no tiene valor.
Para demostrar este comportamiento, ejecutaremos el programa con >la ruta del
archivo output.txt al que queremos redirigir el flujo de salida estándar. No
pasaremos ningún argumento, lo que debería generar un error:
Sí, nuestro mensaje de error se imprime en la salida estándar. Es mucho más útil
que este tipo de mensajes se impriman en la salida de error estándar para que
solo los datos de una ejecución exitosa se incluyan en el archivo. Lo
cambiaremos.
Usaremos el código del Listado 12-24 para cambiar la forma en que se imprimen
los mensajes de error. Debido a la refactorización realizada anteriormente en este
capítulo, todo el código que imprime los mensajes de error se encuentra en una
sola función, main. La biblioteca estándar proporciona la eprintln!macro que
imprime en el flujo de error estándar, así que cambiemos los dos lugares donde
llamábamos println!para imprimir errores eprintln! .
Esto demuestra que ahora utilizamos la salida estándar para una salida exitosa y
el error estándar para una salida de error según corresponda.
Resumen
Este capítulo resumió algunos de los conceptos principales que has aprendido
hasta ahora y abordó cómo realizar operaciones comunes de E/S en Rust. Al usar
argumentos de línea de comandos, archivos, variables de entorno y la eprintln!
macro para imprimir errores, estás preparado para escribir aplicaciones de línea
de comandos. Combinando los conceptos de los capítulos anteriores, tu código
estará bien organizado, almacenará datos eficazmente en las estructuras de
datos adecuadas, gestionará errores correctamente y estará bien probado.
Primero, examinaremos cómo podemos usar los cierres para capturar valores del
entorno en el que se definen para su uso posterior. Este es el escenario: De vez
en cuando, nuestra empresa de camisetas regala una camiseta exclusiva de
edición limitada a alguien de nuestra lista de correo como promoción. Los
usuarios de la lista de correo pueden agregar su color favorito a su perfil. Si la
persona elegida para recibir la camiseta gratis tiene su color favorito, recibe la
camiseta de ese color. Si no ha especificado un color favorito, recibe el color que
más tenga la empresa en ese momento.
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
El storeusuario definido maintiene dos camisetas azules y una roja por distribuir en
esta promoción de edición limitada. Llamamos al giveawaymétodo para un usuario
con preferencia por una camiseta roja y para un usuario sin preferencia.
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
Existen más diferencias entre funciones y cierres. Los cierres no suelen requerir la
anotación de los tipos de los parámetros ni del valor de retorno, como fnocurre
con las funciones. Las anotaciones de tipo son necesarias en las funciones porque
los tipos forman parte de una interfaz explícita expuesta a los usuarios. Definir
esta interfaz de forma rígida es importante para garantizar que todos estén de
acuerdo sobre los tipos de valores que usa y devuelve una función. Los cierres,
por otro lado, no se utilizan en una interfaz expuesta como esta: se almacenan en
variables y se utilizan sin nombrarlas ni exponerlas a los usuarios de nuestra
biblioteca.
Al igual que con las variables, podemos añadir anotaciones de tipo si queremos
ser más explícitos y claros, pero sin ser más detallados de lo estrictamente
necesario. La anotación de los tipos de un cierre se asemejaría a la definición que
se muestra en el Listado 13-2. En este ejemplo, definimos un cierre y lo
almacenamos en una variable, en lugar de definirlo donde lo pasamos como
argumento, como hicimos en el Listado 13-1.
Con las anotaciones de tipo añadidas, la sintaxis de los cierres se asemeja más a
la de las funciones. Aquí definimos una función que suma 1 a su parámetro y un
cierre con el mismo comportamiento, a modo de comparación. Hemos añadido
algunos espacios para alinear las partes relevantes. Esto ilustra cómo la sintaxis
de los cierres es similar a la de las funciones, salvo por el uso de barras verticales
y la cantidad de sintaxis opcional:
Para las definiciones de cierre, el compilador inferirá un tipo concreto para cada
uno de sus parámetros y para su valor de retorno. Por ejemplo, el Listado 13-3
muestra la definición de un cierre corto que simplemente devuelve el valor que
recibe como parámetro. Este cierre no es muy útil, salvo para este ejemplo.
Tenga en cuenta que no hemos añadido ninguna anotación de tipo a la definición.
Al no haber anotaciones de tipo, podemos llamar al cierre con cualquier tipo,
como hicimos Stringla primera vez. Si intentamos llamar example_closurecon un
entero, obtendremos un error.
let s = example_closure(String::from("hello"));
let n = example_closure(5);
Listado 13-3: Intento de llamar a un cierre cuyos tipos se infieren con dos tipos diferentes.
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5| let n = example_closure(5);
| --------------- ^- help: try using a conversion method: `.to_string()`
| | |
| | expected `String`, found integer
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
Rust
jueves, 22 de mayo de 2025 : Página 358 de 719
--> src/main.rs:4:29
|
4| let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is
of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2| let example_closure = |x| x;
| ^
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
Este ejemplo también ilustra que una variable puede vincularse a una definición
de cierre, y luego podemos llamar al cierre usando el nombre de la variable y
paréntesis como si el nombre de la variable fuera un nombre de función.
$ cargo run
Locking 1 package to latest compatible version
Adding closure-example v0.1.0 (/Users/chris/dev/rust-lang/book/tmp/listings/ch13-
functional-features/listing-13-04)
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]
borrows_mutably();
println!("After calling closure: {list:?}");
}
Listado 13-5: Definición y llamada de un cierre que captura una referencia mutable
$ cargo run
Locking 1 package to latest compatible version
Adding closure-example v0.1.0 (/Users/chris/dev/rust-lang/book/tmp/listings/ch13-
functional-features/listing-13-05)
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]
Rust
jueves, 22 de mayo de 2025 : Página 360 de 719
Si desea forzar el cierre a tomar posesión de los valores que utiliza en el entorno
aunque el cuerpo del cierre no necesite estrictamente posesión, puede utilizar
la movepalabra clave antes de la lista de parámetros.
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
en el hilo no sería válida. Por lo tanto, el compilador requiere que listse mueva al
cierre dado al nuevo hilo para que la referencia sea válida. ¡Intenta eliminar
la movepalabra clave o usar list en el hilo principal después de definir el cierre para
ver qué errores de compilación obtienes!
Una vez que un cierre ha capturado una referencia o la propiedad de un valor del
entorno donde se define (lo que afecta a lo que, si acaso, se mueve dentro del
cierre), el código del cuerpo del cierre define qué sucede con las referencias o
valores cuando el cierre se evalúa posteriormente (lo que afecta a lo que, si
acaso, se mueve fuera del cierre). Un cuerpo de cierre puede realizar cualquiera
de las siguientes acciones: mover un valor capturado fuera del cierre, mutar el
valor capturado, no mover ni mutar el valor, o no capturar nada del entorno desde
el principio.
La forma en que un cierre captura y gestiona los valores del entorno afecta los
rasgos que implementa, y los rasgos permiten que las funciones y estructuras
especifiquen qué tipos de cierres pueden usar. Los cierres implementarán
automáticamente uno, dos o los tres rasgos Fn, de forma aditiva, según cómo el
cuerpo del cierre gestione los valores:
1. FnOnceSe aplica a los cierres que se pueden llamar una vez. Todos los
cierres implementan al menos esta característica, ya que todos se pueden
llamar. Un cierre que extrae valores capturados de su cuerpo solo
implementará FnOnceesta Fncaracterística, ya que solo se puede llamar una
vez.
2. FnMutSe aplica a cierres que no extraen los valores capturados de su
cuerpo, pero que podrían mutarlos. Estos cierres pueden llamarse más de
una vez.
3. FnSe aplica a los cierres que no extraen los valores capturados de su cuerpo
ni los mutan, así como a los que no capturan nada de su entorno. Estos
cierres pueden llamarse más de una vez sin mutar su entorno, lo cual es
importante en casos como llamar a un cierre varias veces simultáneamente.
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
Rust
jueves, 22 de mayo de 2025 : Página 362 de 719
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
Recuerde que Tes el tipo genérico que representa el tipo del valor en
la Somevariante de un Option. Ese tipo Ttambién es el tipo de retorno de
la unwrap_or_elsefunción: código que invoca unwrap_or_elseun Option<String> , por
ejemplo, obtendrá un String.
Nota: Si lo que queremos hacer no requiere capturar un valor del entorno, podemos usar el nombre de
una función en lugar de un cierre. Por ejemplo, podríamos llamar unwrap_or_else(Vec::new)a
un Option<Vec<T>>valor para obtener un nuevo vector vacío si el valor es None. El compilador
implementa automáticamente el atributo Fnaplicable a la definición de una función.
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{list:#?}");
}
Listado 13-7: Uso sort_by_keypara ordenar rectángulos por ancho
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/rectangles`
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
Listado 13-8: Intento de utilizar un FnOncecierre consort_by_key
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ move occurs because `value` has type `String`, which does not
implement the `Copy` trait
|
Rust
jueves, 22 de mayo de 2025 : Página 365 de 719
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
El error apunta a la línea del cuerpo del cierre que se desplaza valuefuera del
entorno. Para solucionarlo, debemos modificar el cuerpo del cierre para que no
mueva valores fuera del entorno. Para contar el número de veces que se llama al
cierre, mantener un contador en el entorno e incrementar su valor en el cuerpo
del cierre es una forma más sencilla de calcularlo. El cierre del Listado 13-9
funciona sort_by_keyporque solo captura una referencia mutable
al num_sort_operationscontador y, por lo tanto, puede llamarse más de una vez:
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
Los Fnrasgos son importantes al definir o usar funciones o tipos que utilizan
cierres. En la siguiente sección, analizaremos los iteradores. Muchos métodos de
iterador aceptan argumentos de cierre, así que tenga en cuenta estos detalles al
continuar.
El patrón iterador permite ejecutar una tarea en una secuencia de elementos, uno
por uno. Un iterador se encarga de la lógica de iterar sobre cada elemento y
determinar cuándo finaliza la secuencia. Al usar iteradores, no es necesario
reimplementar esa lógica.
En Rust, los iteradores son perezosos , lo que significa que no tienen efecto hasta
que se invocan métodos que consumen el iterador para agotarlo. Por ejemplo, el
código del Listado 13-10 crea un iterador sobre los elementos del
vector v1invocando el itermétodo definido en Vec<T>. Este código por sí solo no
tiene ninguna utilidad.
El Iteratorrasgo y el nextmétodo
Observe que esta definición utiliza una sintaxis nueva: type Itemy Self::Item, que
definen un tipo asociado con este atributo. Hablaremos sobre los tipos asociados
en profundidad en el Capítulo 20. Por ahora, solo necesita saber que este código
indica que la implementación del Iteratoratributo requiere que también defina
un Itemtipo, y este Itemtipo se utiliza en el tipo de retorno del next método. En
otras palabras,Item tipo será el tipo devuelto por el iterador.
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
Rust
jueves, 22 de mayo de 2025 : Página 368 de 719
}
Listado 13-12: Llamada al nextmétodo en un iterador
Tenga en cuenta también que los valores que obtenemos de las llamadas
a nextson referencias inmutables a los valores del vector. El itermétodo genera un
iterador sobre referencias inmutables. Si queremos crear un iterador que tome
propiedad de v1valores propios y los devuelva, podemos llamar a into_iteren lugar
de a iter. De forma similar, si queremos iterar sobre referencias mutables,
podemos llamar a iter_muten lugar de a.iter .
assert_eq!(total, 6);
Rust
jueves, 22 de mayo de 2025 : Página 369 de 719
}
Listado 13-13: Llamada al summétodo para obtener el total de todos los elementos en el iterador
v1.iter().map(|x| x + 1);
Listado 13-14: Llamada al adaptador de iterador mappara crear un nuevo iterador
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4| v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4| let _ = v1.iter().map(|x| x + 1);
| +++++++
El código del Listado 13-14 no realiza ninguna acción; el cierre especificado nunca
se llama. La advertencia nos recuerda por qué: los adaptadores de iteradores son
perezosos y necesitamos consumir el iterador aquí.
Dado que mapse utiliza un cierre, podemos especificar cualquier operación que
queramos realizar en cada elemento. Este es un excelente ejemplo de cómo los
cierres permiten personalizar el comportamiento mientras se reutiliza el
comportamiento de iteración que Iteratorproporciona el atributo.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}
Rust
jueves, 22 de mayo de 2025 : Página 372 de 719
Listado 13-16: Uso del filtermétodo con un cierre que capturashoe_size
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
Listado 13-17: Reproducción de la Config::buildfunción del Listado 12-23
fn main() {
let args: Vec<String> = env::args().collect();
// --snip--
Rust
jueves, 22 de mayo de 2025 : Página 374 de 719
}
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
}
Listado 13-18: Pasando el valor de retorno de env::argsaConfig::build
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
Listado 13-19: Actualización de la firma de Config::buildpara esperar un iterador
sección "Rasgos como parámetros" del capítulo 10 significa que argspuede ser
cualquier tipo que implemente el Iteratorrasgo y devuelvaString elementos.
Dado que tomamos propiedad argsy mutaremos argsiterando sobre él, podemos
agregar la mutpalabra clave en la especificación del argsparámetro para hacerlo
mutable.
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
Listado 13-20: Cambiar el cuerpo de Config::buildpara usar métodos iteradores
results
}
Listado 13-21: La implementación de la searchfunción del Listado 12-19
coincidentes en otro vector con collect. ¡Mucho más simple! Puede hacer el mismo
cambio para usar métodos iterativos en la search_case_insensitivefunción.
La siguiente pregunta lógica es qué estilo debería elegir en su código y por qué: la
implementación original del Listado 13-21 o la versión que usa iteradores del
Listado 13-22. La mayoría de los programadores de Rust prefieren usar el estilo
iterador. Al principio, es un poco más difícil de entender, pero una vez que se
familiariza con los distintos adaptadores de iteradores y su función, los iteradores
se vuelven más fáciles de entender. En lugar de manipular los distintos
fragmentos del bucle y crear nuevos vectores, el código se centra en el objetivo
general del bucle. Esto abstrae parte del código común, lo que facilita la
comprensión de los conceptos exclusivos de este código, como la condición de
filtrado que debe pasar cada elemento del iterador.
for i in 12..buffer.len() {
let prediction = coefficients.iter()
.zip(&buffer[i - 12..i])
.map(|(&c, &s)| c * s as i64)
.sum::<i64>() >> qlp_shift;
let delta = buffer[i];
buffer[i] = prediction as i32 + delta;
}
Para calcular el valor de prediction, este código itera sobre cada uno de los 12
valores de coefficientsy utiliza el zipmétodo para emparejar los valores de los
coeficientes con los 12 valores anteriores de buffer. Luego, para cada par,
Rust
jueves, 22 de mayo de 2025 : Página 379 de 719
multiplicamos los valores, sumamos todos los resultados y desplazamos los bits
de la suma qlp_shifta la derecha.
Resumen
Los cierres e iteradores son características de Rust inspiradas en las ideas de los
lenguajes de programación funcional. Contribuyen a la capacidad de Rust para
expresar con claridad ideas de alto nivel con un rendimiento de bajo nivel. Las
implementaciones de cierres e iteradores no afectan el rendimiento en tiempo de
ejecución. Esto forma parte del objetivo de Rust de proporcionar abstracciones de
coste cero.
Cargo puede hacer incluso más que la funcionalidad que cubrimos en este
capítulo, por lo que para obtener una explicación completa de todas sus
características, consulte su documentación .
Cargo tiene dos perfiles principales: el devque usa al ejecutar cargo buildy
el releaseque usa al ejecutar cargo build --release. El devperfil se define con valores
predeterminados adecuados para el desarrollo y el releaseperfil también para las
versiones de lanzamiento.
Es posible que estos nombres de perfil le resulten familiares a partir del resultado
de sus compilaciones:
$ cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build --release
Finished `release` profile [optimized] target(s) in 0.32s
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
[profile.dev]
opt-level = 1
Documentar tus paquetes con precisión ayudará a otros usuarios a saber cómo y
cuándo usarlos, por lo que vale la pena dedicar tiempo a escribir la
documentación. En el Capítulo 3, explicamos cómo comentar el código de Rust
usando dos barras diagonales. //Rust también cuenta con un tipo particular de
comentario para la documentación, conocido como comentario de
documentación , que genera documentación HTML. El HTML muestra el contenido
de los comentarios de documentación para elementos de la API pública, dirigidos
a programadores interesados en saber cómo usar tu crate, en lugar de cómo
se implementa .
Doc-tests my_crate
running 1 test
test src/lib.rs - add_one (line 5) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
Observe que no hay código después de la última línea que empieza por //!. Como
empezamos los comentarios con //!en lugar de ///, documentamos el elemento que
contiene este comentario, no el elemento que lo sigue. En este caso, ese
elemento es el archivo src/lib.rs , que es la raíz del crate. Estos comentarios
describen el crate completo.
Figura 14-2: Documentación renderizada para my_crate, incluido el comentario que describe
la caja en su totalidad
Por ejemplo, supongamos que creamos una biblioteca llamada artpara modelar
conceptos artísticos. Esta biblioteca contiene dos módulos: un kindsmódulo con
dos enumeraciones llamadas PrimaryColory SecondaryColory un utilsmódulo con una
función llamada mix, como se muestra en el Listado 14-3:
Figura 14-3: Página principal de la documentación art que enumera los módulos kindsyutils
Otra caja que depende de esta biblioteca necesitaría useinstrucciones que traigan
los elementos artal ámbito, especificando la estructura del módulo definida
actualmente. El listado 14-4 muestra un ejemplo de una caja que utiliza
los elementos ` PrimaryColory` de la caja:mixart
fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
Listado 14-4: Una caja que utiliza artlos elementos de la caja con su estructura interna exportada
El autor del código del Listado 14-4, que usa el artcrate, tuvo que averiguar
qué PrimaryColorestá en el kindsmódulo y mixqué está en el utilsmódulo. La
estructura del módulo del artcrate es más relevante para los desarrolladores que
trabajan en artél que para quienes lo usan. La estructura interna no contiene
información útil para quienes intentan entender cómo usar el artcrate, sino que
Rust
jueves, 22 de mayo de 2025 : Página 389 de 719
genera confusión, ya que los desarrolladores que lo usan deben averiguar dónde
buscar y especificar los nombres de los módulos en el... use instrucciones.
La documentación de API que cargo docse genera para esta caja ahora enumerará
y vinculará las reexportaciones en la página principal, como se muestra en la
Figura 14-4, lo que hará que los PrimaryColortipos SecondaryColory la mixfunción
sean más fáciles de encontrar.
Rust
jueves, 22 de mayo de 2025 : Página 390 de 719
Figura 14-4: La página principal de la documentación art que enumera las reexportaciones
Los artusuarios del cajón aún pueden ver y usar la estructura interna del Listado
14-3 como se muestra en el Listado 14-4, o pueden usar la estructura más
conveniente del Listado 14-5, como se muestra en el Listado 14-6:
fn main() {
// --snip--
}
Listado 14-6: Un programa que utiliza los elementos reexportados de la artcaja
Crear una estructura de API pública útil es más un arte que una ciencia, y puedes
iterar para encontrar la API que mejor se adapte a tus usuarios. Elegir una pub
usete da flexibilidad para estructurar tu crate internamente y desvincula esa
estructura interna de lo que presentas a tus usuarios. Revisa el código de los
crates que has instalado para ver si su estructura interna difiere de su API pública.
$ cargo login
abcdefghijklmnopqrstuvwxyz012345
Supongamos que tienes una caja que quieres publicar. Antes de publicarla,
deberás agregar metadatos en la [package]sección del archivo Cargo.toml de la
caja .
$ cargo publish
Updating crates.io index
warning: manifest has no description, license, license-file, documentation, homepage or
repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
--snip--
error: failed to publish to registry at https://crates.io
Caused by:
the remote server responded with an error (status 400 Bad Request): missing or empty
metadata fields: description, license. Please see
https://doc.rust-lang.org/cargo/reference/manifest.html for more information on configuring
these field
Este error se debe a que falta información crucial: se requieren una descripción y
una licencia para que los usuarios sepan qué hace tu crate y bajo qué condiciones
pueden usarlo. En Cargo.toml , añade una descripción de una o dos frases, ya que
aparecerá con tu crate en los resultados de búsqueda. Para este licensecampo,
debes proporcionar un valor de identificador de licencia . El Intercambio de Datos
de Paquetes de Software (SPDX) de la Fundación Linux enumera los
identificadores que puedes usar para este valor. Por ejemplo, para especificar que
has licenciado tu crate con la Licencia MIT, añade el MITidentificador:
[package]
name = "guessing_game"
license = "MIT"
Si desea utilizar una licencia que no aparece en el SPDX, debe colocar el texto de
esa licencia en un archivo, incluir el archivo en su proyecto y luego usar license-
filepara especificar el nombre de ese archivo en lugar de usar la licenseclave.
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"
[dependencies]
Publicación en Crates.io
Ahora que has creado una cuenta, guardado tu token de API, elegido un nombre
para tu crate y especificado los metadatos necesarios, ¡estás listo para publicar!
Al publicar un crate, se carga una versión específica en crates.io. para que otros
la usen.
$ cargo publish
Updating crates.io index
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
Cuando hayas realizado cambios en tu crate y estés listo para publicar una nueva
versión, modifica el versionvalor especificado en tu archivo Cargo.toml y vuelve a
publicar. Usa las reglas de Versiones Semánticas para determinar el número de
versión apropiado según los cambios realizados. Luego, ejecuta el comando cargo
publishpara cargar la nueva versión.
Eliminar una versión impide que nuevos proyectos dependan de ella, a la vez que
permite que todos los proyectos existentes que dependen de ella continúen. En
esencia, eliminar una versión significa que todos los proyectos con
un Cargo.lock no se romperán, y los archivos Cargo.lock generados en el futuro no
usarán la versión eliminada.
Al agregar --undoal comando, también puede deshacer un yank y permitir que los
proyectos comiencen a depender de una versión nuevamente:
$ mkdir add
$ cd add
[workspace]
resolver = "2"
[workspace]
resolver = "2"
members = ["adder"]
En este punto, podemos crear el espacio de trabajo ejecutando cargo build. Los
archivos en el directorio de adición deberían verse así:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
[workspace]
resolver = "2"
members = ["adder", "add_one"]
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
[dependencies]
add_one = { path = "../add_one" }
$ cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
[dependencies]
rand = "0.8.5"
$ cargo build
Rust
jueves, 22 de mayo de 2025 : Página 399 de 719
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
--> add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1
suggestion)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
$ cargo build
--snip--
Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
--> adder/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(3, add_one(2));
}
}
$ cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
Running unittests src/lib.rs (target/debug/deps/add_one-f0253159197f7841)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Esta salida muestra cargo testque solo se ejecutaron las pruebas para
la add_onecaja y no se ejecutaron las adderpruebas de la caja.
¡Para practicar más, agrega una add_twocaja a este espacio de trabajo de manera
similar a la add_onecaja!
en crates.io . Tenga en cuenta que solo puede instalar paquetes con destinos
binarios. Un destino binario es el programa ejecutable que se crea si el crate tiene
un archivo src/main.rs u otro archivo especificado como binario, a diferencia de
un destino de biblioteca que no es ejecutable por sí solo, pero es adecuado para
incluirlo en otros programas. Normalmente, los crates tienen información en el
archivo README. de los crates indica si son bibliotecas, tienen un destino binario
o ambos.
Resumen
Compartir código con Cargo y crates.io es parte de lo que hace que el ecosistema
de Rust sea útil para diversas tareas. La biblioteca estándar de Rust es pequeña y
estable, pero los crates son fáciles de compartir, usar y mejorar en un plazo
diferente al del lenguaje. No dudes en compartir código que te sea útil
en crates.io ; ¡es probable que también le sea útil a alguien más!
Punteros inteligentes
Un puntero es un concepto general para una variable que contiene una dirección
en memoria. Esta dirección se refiere, o "apunta", a otros datos. El tipo de
puntero más común en Rust es una referencia, que aprendiste en el Capítulo 4.
Las referencias se indican con el &símbolo y toman prestado el valor al que
apuntan. No tienen ninguna función especial aparte de referirse a datos y no
tienen sobrecarga.
Los punteros inteligentes , por otro lado, son estructuras de datos que actúan
como un puntero, pero que también cuentan con metadatos y capacidades
adicionales. El concepto de punteros inteligentes no es exclusivo de Rust: se
originaron en C++ y también existen en otros lenguajes. Rust cuenta con diversos
punteros inteligentes definidos en la biblioteca estándar que ofrecen
funcionalidades adicionales a las de las referencias. Para explorar el concepto
general, analizaremos un par de ejemplos de punteros inteligentes, incluyendo un
tipo de puntero inteligente con conteo de referencias . Este puntero permite que
los datos tengan múltiples propietarios, controlando su número y, cuando no
quedan propietarios, limpiando los datos.
¡Vamos a sumergirnos!
Las cajas no tienen sobrecarga de rendimiento, salvo que almacenan sus datos en
el montón en lugar de en la pila. Sin embargo, tampoco ofrecen muchas funciones
adicionales. Se utilizan con mayor frecuencia en estas situaciones:
Colocar un solo valor en el montón no es muy útil, por lo que no se usarán las
cajas por sí solas de esta manera muy a menudo. Tener valores como uno
solo i32en la pila, donde se almacenan por defecto, es más apropiado en la
mayoría de las situaciones. Veamos un caso donde las cajas nos permiten definir
tipos que no podríamos definir sin ellas.
Un valor de tipo recursivo puede tener otro valor del mismo tipo como parte de sí
mismo. Los tipos recursivos plantean un problema porque, en tiempo de
compilación, Rust necesita saber cuánto espacio ocupa un tipo. Sin embargo, la
anidación de valores de tipos recursivos podría, en teoría, continuar infinitamente,
por lo que Rust no puede saber cuánto espacio necesita el valor. Dado que las
cajas tienen un tamaño conocido, podemos habilitar los tipos recursivos
insertando una caja en la definición del tipo recursivo.
Como ejemplo de un tipo recursivo, exploremos la lista de cons . Este tipo de dato
se encuentra comúnmente en lenguajes de programación funcional. El tipo de
lista de cons que definiremos es simple, salvo por la recursión; por lo tanto, los
conceptos del ejemplo que utilizaremos serán útiles en situaciones más complejas
que involucren tipos recursivos.
Cada elemento de una lista de cons contiene dos elementos: el valor del
elemento actual y el siguiente. El último elemento de la lista contiene solo un
valor llamado, Nil sin un elemento siguiente. Una lista de cons se genera mediante
una llamada recursiva a la cons función. El nombre canónico para el caso base de
la recursión es Nil. Tenga en cuenta que esto no es lo mismo que el concepto
"null" o "nil" del Capítulo 6, que representa un valor inválido o ausente.
El listado 15-2 contiene la definición de una enumeración para una lista de cons.
Tenga en cuenta que este código aún no se compila porque el Listtipo no tiene un
tamaño conocido, lo cual demostraremos.
enum List {
Cons(i32, List),
Nil,
}
Listado 15-2: El primer intento de definir una enumeración para representar una estructura de datos de
lista de i32valores de cons
i32Nota: Para este ejemplo, estamos implementando una lista de cons que solo contiene valores.
Podríamos haberla implementado usando genéricos, como se explicó en el capítulo 10, para definir un
tipo de lista de cons que pudiera almacenar valores de cualquier tipo.
El uso del Listtipo para almacenar la lista 1, 2, 3se vería como el código del Listado
15-3:
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
Listado 15-3: Uso de la Listenumeración para almacenar la lista1, 2, 3
Rust
jueves, 22 de mayo de 2025 : Página 408 de 719
El primer Consvalor se cumple 1y otro Listvalor. Este Listvalor es otro Consvalor que
se cumple 2y otro Listvalor. Este Listvalor es otro Consvalor que se cumple 3y
un Listvalor, que es finalmente Nil, la variante no recursiva que indica el final de la
lista.
El error indica que este tipo tiene un tamaño infinito. Esto se debe a que lo hemos
definido Listcon una variante recursiva: contiene directamente otro valor de sí
mismo. Como resultado, Rust no puede determinar cuánto espacio necesita para
almacenar un Listvalor. Analicemos por qué aparece este error. Primero, veremos
cómo Rust determina cuánto espacio necesita para almacenar un valor de un tipo
no recursivo.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Compare esto con lo que ocurre cuando Rust intenta determinar cuánto
espacio Listnecesita un tipo recursivo como la enumeración del Listado 15-2. El
compilador comienza examinando la Consvariante, que contiene un valor de
tipo i32y un valor de tipo List. Por lo tanto, Consnecesita una cantidad de espacio
igual al tamaño de an i32más el tamaño de a List. Para determinar cuánta
memoria List necesita el tipo, el compilador examina las variantes, comenzando
por la Cons variante. La Consvariante contiene un valor de tipo i32y un valor de
tipo List, y este proceso continúa infinitamente, como se muestra en la Figura 15-
1.
Como Rust no puede determinar cuánto espacio asignar para los tipos definidos
de forma recursiva, el compilador arroja un error con esta útil sugerencia:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2| Cons(i32, Box<List>),
| ++++ +
Rust
jueves, 22 de mayo de 2025 : Página 410 de 719
Como a Box<T>es un puntero, Rust siempre sabe cuánto espacio Box<T> necesita:
el tamaño de un puntero no cambia según la cantidad de datos a los que apunta.
Esto significa que podemos colocar a Box<T>dentro de la Consvariante en lugar de
otro Listvalor directamente. Box<T>Apuntará al siguiente List valor que estará en
el montón, en lugar de dentro de la Consvariante. Conceptualmente, seguimos
teniendo una lista, creada con listas que contienen otras listas, pero esta
implementación ahora se asemeja más a colocar los elementos uno al lado del
otro en lugar de uno dentro del otro.
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Listado 15-5: Definición de Listque utiliza Box<T>para tener un tamaño conocido
Nota: Hay una gran diferencia entre el MyBox<T>tipo que vamos a construir y el real Box<T>:
nuestra versión no almacenará sus datos en el montón. En este ejemplo, nos centramos en Deref, por lo
que el lugar donde se almacenan los datos es menos importante que el comportamiento similar al de un
puntero.
assert_eq!(5, x);
assert_eq!(5, *y);
}
Listado 15-6: Uso del operador de desreferencia para seguir una referencia a un i32valor
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6| assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-
backtrace for more info)
help: consider dereferencing here
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/macros/
mod.rs:46:35
|
46| if !(*left_val == **right_val) {
| +
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 413 de 719
Podemos reescribir el código del Listado 15-6 para utilizar a Box<T>en lugar de
una referencia; el operador de desreferencia utilizado en el Box<T>Listado 15-7
funciona de la misma manera que el operador de desreferencia utilizado en la
referencia en el Listado 15-6:
assert_eq!(5, x);
assert_eq!(5, *y);
}
Listado 15-7: Uso del operador de desreferencia en unBox<i32>
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
Listado 15-8: Definición de un MyBox<T>tipo
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Listado 15-9: Intentando utilizar MyBox<T>de la misma manera que usamos referencias yBox<T>
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
La type Target = T;sintaxis define un tipo asociado para el Deref atributo. Los tipos
asociados son una forma ligeramente distinta de declarar un parámetro genérico,
pero no es necesario preocuparse por ellos por ahora; los abordaremos con más
detalle en el Capítulo 20.
Completamos el cuerpo del derefmétodo con [nombre del método], por &self.0lo
que derefdevuelve una referencia al valor al que queremos acceder con
el *operador; recuerde la sección "Uso de estructuras de tupla sin campos con
nombre para crear diferentes tipos" del Capítulo 5, que .0accede al primer valor
de una estructura de tupla. La mainfunción del Listado 15-9 que llama *a [nombre
del método]MyBox<T> valor ahora compila y las aserciones pasan.
Cuando ingresamos *yal Listado 15-9, detrás de escena Rust ejecutó este código:
*(y.deref())
Rust
jueves, 22 de mayo de 2025 : Página 416 de 719
Rust sustituye el *operador con una llamada al derefmétodo y luego una simple
desreferencia, por lo que no tenemos que pensar si necesitamos llamar
al derefmétodo. Esta característica de Rust nos permite escribir código que
funciona de forma idéntica, independientemente de si tenemos una referencia
regular o un tipo que implementa Deref.
Cuando Derefse define el rasgo para los tipos involucrados, Rust analizará los tipos
y los usará Deref::dereftantas veces como sea necesario para obtener una
referencia que coincida con el tipo del parámetro. El número de veces
que Deref::derefse debe insertar se determina en tiempo de compilación, por lo
que no hay penalización en tiempo de ejecución por aprovechar la coerción de
deref.
Los dos primeros casos son iguales, excepto que el segundo implementa la
mutabilidad. El primero establece que si se tiene una variable &Ty T se
implementa Derefen algún tipo U, se puede obtener una variable &Ude forma
transparente. El segundo establece que la misma coerción de desreferencias
ocurre para las referencias mutables.
El tercer caso es más complejo: Rust también convierte una referencia mutable
en una inmutable. Pero lo contrario no es posible: las referencias inmutables
nunca convierten referencias mutables. Debido a las reglas de préstamo, si se
tiene una referencia mutable, esta debe ser la única referencia a esos datos (de lo
contrario, el programa no compilaría). Convertir una referencia mutable en una
inmutable nunca infringirá las reglas de préstamo. Convertir una referencia
inmutable en una mutable requeriría que la referencia inmutable inicial fuera la
única referencia inmutable a esos datos, pero las reglas de préstamo no lo
garantizan. Por lo tanto, Rust no puede asumir que sea posible convertir una
referencia inmutable en una mutable.
Rust
jueves, 22 de mayo de 2025 : Página 419 de 719
En algunos lenguajes, para ciertos tipos, el programador debe invocar código para
liberar memoria o recursos cada vez que termina de usar una instancia de esos
tipos. Algunos ejemplos son los manejadores de archivos, los sockets o los
bloqueos. Si se olvidan, el sistema podría sobrecargarse y bloquearse. En Rust, se
puede especificar que se ejecute un fragmento de código específico cuando un
valor se sale del ámbito, y el compilador lo insertará automáticamente. Por lo
tanto, no es necesario tener cuidado al colocar código de limpieza en todos los
lugares del programa donde se termina de usar una instancia de un tipo
específico; ¡aún así, no se perderán recursos!
Se especifica el código que se ejecuta cuando un valor queda fuera del ámbito
implementando el Droprasgo. El Droprasgo requiere que se implemente un método
llamado dropque tome una referencia mutable a self. Para ver cuándo Rust llama
a drop, implementemos las declaraciones dropcon println!por ahora.
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}
Listado 15-14: Una CustomSmartPointerestructura que implementa el Droprasgo donde colocaríamos
nuestro código de limpieza
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
Running `target/debug/drop-example`
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
c.drop();
println!("CustomSmartPointer dropped before the end of main.");
}
Listado 15-15: Intentar llamar al dropmétodo desde el Droprasgo manualmente para limpiarlo de manera
temprana
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
--> src/main.rs:16:7
|
16 | c.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
16 | drop(c);
| +++++ ~
For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 422 de 719
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.
contado
En la mayoría de los casos, la propiedad es clara: se sabe exactamente qué
variable posee un valor dado. Sin embargo, hay casos en los que un mismo valor
puede tener múltiples propietarios. Por ejemplo, en las estructuras de datos de
grafos, varias aristas pueden apuntar al mismo nodo, y ese nodo es
conceptualmente propiedad de todas las aristas que apuntan a él. Un nodo no
debe limpiarse a menos que no tenga aristas que lo apunten y, por lo tanto, no
tenga propietarios.
ImaginarRc<T> un televisor en una sala familiar. Cuando una persona entra a ver
la televisión, la enciende. Los demás pueden entrar a la habitación y verla.
Cuando la última persona sale, apaga el televisor porque ya no se usa. Si alguien
Rust
jueves, 22 de mayo de 2025 : Página 424 de 719
apaga el televisor mientras otros lo siguen viendo, ¡se armaría un alboroto entre
los demás!
Usamos este Rc<T>tipo cuando queremos asignar datos en el montón para que
varias partes de nuestro programa los lean y no podemos determinar en tiempo
de compilación qué parte terminará de usar los datos en último lugar. Si
supiéramos qué parte terminará en último lugar, podríamos simplemente
convertirla en la propietaria de los datos, y las reglas de propiedad habituales
aplicadas en tiempo de compilación se aplicarían.
Volvamos a nuestro ejemplo de lista de contras del Listado 15-5. Recordemos que
la definimos usando Box<T>. Esta vez, crearemos dos listas que comparten la
propiedad de una tercera lista. Conceptualmente, esto se parece a la Figura 15-3:
Crearemos una lista aque contenga 5 y luego 10. Luego haremos dos listas
más: buna que comience con 3 y cotra que comience con 4.
Ambas blistas c continuarán hasta la primera alista que contiene 5 y 10. En otras
palabras, ambas listas compartirán la primera lista que contiene 5 y 10.
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
Rust
jueves, 22 de mayo de 2025 : Página 425 de 719
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
Listado 15-17: Demostración de que no se nos permite tener dos listas Box<T>que intenten compartir la
propiedad de una tercera lista
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy`
trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
Las Consvariantes poseen los datos que contienen, por lo que, al crear
la blista, a se mueve a by bposee a. Luego, al intentar usarla ade nuevo al crear c,
no se nos permite hacerlo porque ase ha movido.
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
Listado 15-18: Una definición de Listque utilizaRc<T>
Cambiemos nuestro ejemplo de trabajo en el Listado 15-18 para que podamos ver
cómo cambian los recuentos de referencias a medida que creamos y eliminamos
referencias al Rc<List>in a.
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
Lo que no podemos ver en este ejemplo es que cuando by asalen del ámbito al
final de main, el recuento es 0 y Rc<List>se limpia por completo.
Usar Rc<T>permite que un mismo valor tenga varios propietarios, y el recuento
garantiza que el valor siga siendo válido mientras exista alguno de los
propietarios.
Rust
jueves, 22 de mayo de 2025 : Página 428 de 719
Podemos usar tipos que emplean el patrón de mutabilidad interna solo cuando
podemos garantizar que las reglas de préstamo se seguirán en tiempo de
ejecución, aunque el compilador no pueda garantizarlo. El unsafecódigo
involucrado se encapsula entonces en una API segura, y el tipo externo sigue
siendo inmutable.
fn main() {
let x = 5;
let y = &mut x;
}
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3| let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2| let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 431 de 719
Sin embargo, existen situaciones en las que sería útil que un valor se mutara a sí
mismo en sus métodos, pero que pareciera inmutable para otro código. El código
externo a los métodos del valor no podría mutarlo. Usar RefCell<T>es una forma
de obtener la capacidad de mutabilidad interna, pero RefCell<T> no evita
completamente las reglas de préstamo: el verificador de préstamos del
compilador permite esta mutabilidad interna, y las reglas de préstamo se
verifican en tiempo de ejecución. Si se infringen las reglas, se obtendrá un
error panic!en lugar de un error del compilador.
A veces, durante las pruebas, un programador usa un tipo en lugar de otro para
observar un comportamiento específico y asegurar su correcta implementación.
Este tipo de marcador se denomina " doble de prueba" . Piénselo como un "doble
de acción" en el cine, donde una persona sustituye a un actor en una escena
complicada. Los dobles de prueba sustituyen a otros tipos al ejecutar
pruebas. Los objetos simulados son tipos específicos de dobles de prueba que
registran lo que sucede durante una prueba para asegurar que se realizaron las
acciones correctas.
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
Rust
jueves, 22 de mayo de 2025 : Página 434 de 719
}
}
Listado 15-21: Un intento de implementar un MockMessengerque no está permitido por el verificador de
préstamos
Sin embargo, hay un problema con esta prueba, como se muestra aquí:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to
cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait`
definition
|
2 ~ fn send(&mut self, msg: &str);
3 |}
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 435 de 719
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
Listado 15-22: Uso RefCell<T>para mutar un valor interno mientras el valor externo se considera
inmutable
Rust
jueves, 22 de mayo de 2025 : Página 436 de 719
El último cambio que tenemos que hacer es en la afirmación: para ver cuántos
elementos hay en el vector interno, llamamos borrowa RefCell<Vec<String>>para
obtener una referencia inmutable al vector.
¡Ahora que has visto cómo usar RefCell<T>, profundicemos en cómo funciona!
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
Listado 15-23: Creación de dos referencias mutables en el mismo ámbito para ver
que RefCell<T>entrará en pánico
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Por ejemplo, recuerde el ejemplo de la lista cons del Listado 15-18, donde
permitíamos que Rc<T>varias listas compartieran la propiedad de otra. Dado
que Rc<T>solo contiene valores inmutables, no podemos cambiar ninguno de los
valores de la lista una vez creada. Añadamos [in] RefCell<T>para poder cambiar
los valores de las listas. El Listado 15-24 muestra que, al usar [a] RefCell<T>en
la Consdefinición, podemos modificar el valor almacenado en todas las listas:
fn main() {
let value = Rc::new(RefCell::new(5));
*value.borrow_mut() += 10;
Envolvemos la lista aen un Rc<T>de modo que cuando creamos listas by c, ambas
puedan hacer referencia a a, que es lo que hicimos en el Listado 15-18.
Tras crear las listas en a, by c, queremos sumar 10 al valor en value. Para ello,
llamamos borrow_muta on value, que utiliza la función de desreferenciación
automática que explicamos en el capítulo 5 (véase la sección "¿Dónde está el -
>operador?" ) para desreferenciar al valor Rc<T>interno RefCell<T>.
El borrow_mut método devuelve un RefMut<T>puntero inteligente, sobre el cual
utilizamos el operador de desreferenciación para modificar el valor interno.
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {}
Listado 15-25: Una definición de lista de contras que contiene una RefCell<T>para que podamos
modificar a qué Consse refiere una variante
Rust
jueves, 22 de mayo de 2025 : Página 441 de 719
En el Listado 15-26, agregamos una mainfunción que usa las definiciones del
Listado 15-25. Este código crea una lista en ay una lista en bque apunta a la lista
en a. Luego, modifica la lista en apara que apunte a b, creando un ciclo de
referencia. Hay println!instrucciones a lo largo del proceso para mostrar los
recuentos de referencia en varios puntos de este proceso.
Lo modificamos apara que apunte a ben lugar de a Nil, creando un ciclo. Para ello,
usamos el tailmétodo para obtener una referencia a RefCell<Rc<List>> in a, que
colocamos en la variable link. Luego, usamos el borrow_mut método
Rust
jueves, 22 de mayo de 2025 : Página 442 de 719
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
Dado que el valor al que Weak<T>hace referencia podría haberse descartado, para
realizar cualquier acción con el valor al que Weak<T>apunta a, debe asegurarse
de que el valor aún exista. Para ello, llame al upgrademétodo en
una Weak<T> instancia, que devolverá un Option<Rc<T>>. Obtendrá un resultado
de Some si el Rc<T>valor aún no se ha descartado y un resultado de Nonesi
el Rc<T>valor se ha descartado. Dado que upgradedevuelve un Option<Rc<T>>,
Rust se asegurará de queSome mayúsculas y Noneminúsculas, y no habrá un
puntero inválido.
A modo de ejemplo, en lugar de utilizar una lista cuyos elementos solo conocen el
siguiente elemento, crearemos un árbol cuyos elementos conocen sus elementos
secundarios y sus elementos primarios.
Para empezar, construiremos un árbol con nodos que conocen a sus nodos
secundarios. Crearemos una estructura llamada Nodeque contenga su
propio i32valor, así como referencias a sus nodos secundarios. Node los valores de
sus nodos secundarios:
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
Queremos que a Nodesea propietario de sus hijos y compartir esa propiedad con
las variables para poder acceder Nodedirectamente a cada una en el árbol. Para
ello, definimos los Vec<T>elementos como valores de tipo Rc<Node>. También
queremos modificar qué nodos son hijos de otro nodo, por lo que tenemos
un RefCell<T>in childrenalrededor deVec<Rc<Node>> .
Rust
jueves, 22 de mayo de 2025 : Página 445 de 719
Considerando las relaciones desde otra perspectiva, un nodo padre debería ser
propietario de sus hijos: si se elimina un nodo padre, sus hijos también deberían
eliminarse. Sin embargo, un hijo no debería ser propietario de su padre: si
eliminamos un nodo hijo, el padre debería seguir existiendo. ¡Este es un caso de
referencias débiles!
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
Un nodo podrá hacer referencia a su nodo padre, pero no será propietario de este.
En el Listado 15-28, actualizamos mainesta nueva definición para que el leaf nodo
pueda hacer referencia a su padre branch:
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
Al imprimir leafde nuevo el padre de, esta vez obtendremos una Somevariante que
contiene branch: ¡ahora leafpuede acceder a su padre! Al imprimir leaf, también
evitamos el ciclo que finalmente terminó en un desbordamiento de pila como el
del Listado 15-26; las Weak<Node>referencias se imprimen como (Weak):
La falta de salida infinita indica que este código no creó un ciclo de referencia.
También podemos comprobarlo observando los valores obtenidos al llamar a
` Rc::strong_county` Rc::weak_count.
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
{
let branch = Rc::new(Node {
Rust
jueves, 22 de mayo de 2025 : Página 448 de 719
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
Cuando el ámbito interno finaliza, branchse sale del ámbito y el conteo fuerte de
[número] Rc<Node>disminuye a 0, por lo que Nodese descarta. El conteo débil de
[número] de [número] leaf.parentno influye en Nodesu eliminación, por lo que no se
producen fugas de memoria.
Toda la lógica que gestiona los conteos y la eliminación de valores está integrada
en Rc<T>y Weak<T>y en sus implementaciones del Droprasgo. Al especificar que la
relación entre un elemento secundario y su elemento primario debe ser
una Weak<T>referencia en la definición de Node, se puede hacer que los nodos
primarios apunten a los secundarios y viceversa sin crear un ciclo de referencia ni
fugas de memoria.
Resumen
Este capítulo abordó cómo usar punteros inteligentes para generar garantías y
compensaciones diferentes a las que Rust ofrece por defecto con referencias
regulares. El Box<T>tipo tiene un tamaño conocido y apunta a datos asignados en
el montón. El Rc<T>tipo registra el número de referencias a los datos en el
montón, de modo que estos puedan tener múltiples propietarios. RefCell<T>Su
mutabilidad interna nos proporciona un tipo que podemos usar cuando
necesitamos un tipo inmutable, pero necesitamos cambiar un valor interno de ese
tipo; además, aplica las reglas de préstamo en tiempo de ejecución en lugar de
en tiempo de compilación.
También se analizaron los rasgos Derefy Drop, que habilitan gran parte de la
funcionalidad de los punteros inteligentes. Exploramos los ciclos de referencia que
pueden causar fugas de memoria y cómo prevenirlas mediante Weak<T>.
Nota: Para simplificar, nos referiremos a muchos de los problemas como concurrentes en lugar de, para
mayor precisión, decir concurrentes o paralelos . Si este libro tratara sobre concurrencia o paralelismo,
seríamos más específicos. En este capítulo, por favor, sustituya mentalmente concurrentes o
paralelos siempre que usemos concurrentes .
Muchos lenguajes son dogmáticos en cuanto a las soluciones que ofrecen para
gestionar problemas concurrentes. Por ejemplo, Erlang ofrece una funcionalidad
elegante para la concurrencia en el paso de mensajes, pero solo ofrece métodos
poco claros para compartir el estado entre hilos. Admitir solo un subconjunto de
posibles soluciones es una estrategia razonable para lenguajes de alto nivel, ya
que estos prometen beneficios al ceder parte del control para obtener
abstracciones. Sin embargo, se espera que los lenguajes de bajo nivel
proporcionen la solución con el mejor rendimiento en cualquier situación dada y
tengan menos abstracciones sobre el hardware. Por lo tanto, Rust ofrece diversas
herramientas para modelar problemas de la forma más adecuada para su
situación y requisitos.
Rust intenta mitigar los efectos negativos del uso de subprocesos, pero
programar en un contexto multiproceso aún requiere una reflexión cuidadosa y
una estructura de código diferente a la de los programas que se ejecutan en un
solo subproceso.
Rust
jueves, 22 de mayo de 2025 : Página 452 de 719
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
Listado 16-1: Creación de un nuevo hilo para imprimir una cosa mientras el hilo principal imprime otra
cosa
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
Rust
jueves, 22 de mayo de 2025 : Página 454 de 719
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
Listado 16-2: Guardar un JoinHandleformulario thread::spawnpara garantizar que el hilo se ejecute hasta
su finalización
Los dos hilos continúan alternándose, pero el hilo principal espera debido a la
llamada a handle.join()y no finaliza hasta que finaliza el hilo generado.
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap();
Rust
jueves, 22 de mayo de 2025 : Página 455 de 719
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
Pequeños detalles, como dónde joinse llama, pueden afectar si los subprocesos se
ejecutan al mismo tiempo o no.
fn main() {
let v = vec![1, 2, 3];
handle.join().unwrap();
}
Listado 16-3: Intento de utilizar un vector creado por el hilo principal en otro hilo
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by
the current function
--> src/main.rs:6:32
|
6| let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7| println!("Here's a vector: {v:?}");
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6| let handle = thread::spawn(|| {
| __________________^
7|| println!("Here's a vector: {v:?}");
8|| });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use
the `move` keyword
|
6| let handle = thread::spawn(move || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error
El listado 16-4 proporciona un escenario en el que es más probable que haya una
referencia v que no sea válida:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
drop(v); // oh no!
handle.join().unwrap();
}
Listado 16-4: Un hilo con un cierre que intenta capturar una referencia vdesde un hilo principal que
abandonav
Si Rust nos permitiera ejecutar este código, es posible que el hilo generado se
ponga inmediatamente en segundo plano sin ejecutarse. El hilo generado tiene
una referencia a vdentro, pero el hilo principal cierra inmediatamente v, usando
la dropfunción que explicamos en el Capítulo 15. Entonces, cuando el hilo
generado empieza a ejecutarse, vdeja de ser válido, por lo que una referencia a él
también es inválida. ¡Oh, no!
Para corregir el error del compilador en el Listado 16-3, podemos usar los
consejos del mensaje de error:
help: to force the closure to take ownership of `v` (and any other referenced variables), use
the `move` keyword
|
6| let handle = thread::spawn(move || {
| ++++
fn main() {
Rust
jueves, 22 de mayo de 2025 : Página 458 de 719
let v = vec![1, 2, 3];
handle.join().unwrap();
}
Listado 16-5: Uso de la movepalabra clave para forzar que un cierre tome posesión de los valores que
utiliza
Podríamos intentar lo mismo para corregir el código del Listado 16-4, donde el
hilo principal realizaba una llamada dropmediante un movecierre. Sin embargo,
esta solución no funcionará porque lo que el Listado 16-4 intenta hacer no está
permitido por otra razón. Si agregáramos algo moveal cierre, nos moveríamos val
entorno del cierre y ya no podríamos llamarlo dropen el hilo principal. En su lugar,
obtendríamos este error del compilador:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the
`Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {v:?}");
| - variable moved due to use in closure
...
10 | drop(v); // oh no!
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error
¡Las reglas de propiedad de Rust nos han salvado de nuevo! Recibimos un error
en el código del Listado 16-3 porque Rust estaba siendo conservador y solo
tomaba prestado vpara el hilo, lo que significaba que el hilo principal, en teoría,
podría invalidar la referencia del hilo generado. Al indicarle a Rust que transfiera
la propiedad val hilo generado, le garantizamos que el hilo principal vya no la
usará. Si modificamos el Listado 16-4 de la misma manera, violamos las reglas de
propiedad al intentar usar ven el hilo principal. Esta movepalabra clave anula el
método conservador predeterminado de Rust de tomar prestado; no nos permite
violar las reglas de propiedad.
Rust
jueves, 22 de mayo de 2025 : Página 459 de 719
Primero, en el Listado 16-6, crearemos un canal, pero no haremos nada con él.
Tenga en cuenta que esto no se compilará todavía porque Rust no puede
determinar qué tipo de valores queremos enviar a través del canal.
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
Rust
jueves, 22 de mayo de 2025 : Página 461 de 719
});
}
Listado 16-7: Pasar txa un hilo generado y enviar un "hola"
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
El receptor tiene dos métodos útiles: recvy try_recv. Usamos recv, abreviatura
de receive , que bloqueará la ejecución del hilo principal y esperará hasta que se
envíe un valor por el canal. Una vez enviado un valor, recvlo devolverá en
un Result<T, E>. Cuando el transmisor se cierra, recvdevolverá un error para
indicar que no se recibirán más valores.
mensajes en este momento. Usarlo try_recves útil si este hilo tiene otras tareas
que realizar mientras espera mensajes: podríamos escribir un bucle que
llame try_recvcon frecuencia, gestione un mensaje si hay uno disponible y, de lo
contrario, realice otras tareas durante un tiempo hasta que se verifique de nuevo.
Lo hemos utilizado recven este ejemplo para simplificar; no tenemos ningún otro
trabajo que hacer para el hilo principal excepto esperar mensajes, por lo que
bloquear el hilo principal es apropiado.
Cuando ejecutamos el código del Listado 16-8, veremos el valor impreso desde el
hilo principal:
Got: hi
¡Perfecto!
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
println!("val is {val}");
});
$ cargo run
Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:10:26
|
8 | let val = String::from("hi");
| --- move occurs because `val` has type `String`, which does not implement the
`Copy` trait
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!("val is {val}");
| ^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the
expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `message-passing` (bin "message-passing") due to 1 previous error
El código del Listado 16-8 se compiló y ejecutó, pero no mostró claramente que
dos hilos separados se comunicaban entre sí a través del canal. En el Listado 16-
10, hemos realizado algunas modificaciones que comprobarán que el código del
Listado 16-8 se ejecuta simultáneamente: el hilo generado ahora enviará varios
mensajes y se detendrá un segundo entre cada uno.
fn main() {
let (tx, rx) = mpsc::channel();
Rust
jueves, 22 de mayo de 2025 : Página 464 de 719
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for received in rx {
println!("Got: {received}");
}
}
Listado 16-10: Envío de múltiples mensajes y pausa entre cada uno
Esta vez, el hilo generado tiene un vector de cadenas que queremos enviar al hilo
principal. Las iteramos, enviándolas individualmente, y hacemos una pausa entre
cada una llamando a la thread::sleepfunción con un Durationvalor de 1 segundo.
Al ejecutar el código del Listado 16-10, debería ver el siguiente resultado con una
pausa de 1 segundo entre cada línea:
Got: hi
Got: from
Got: the
Got: thread
crear múltiples hilos que envíen valores al mismo receptor. Podemos hacerlo
clonando el transmisor, como se muestra en el Listado 16-11:
thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for received in rx {
println!("Got: {received}");
}
// --snip--
Listado 16-11: Envío de múltiples mensajes de múltiples productores
Esta vez, antes de crear el primer hilo generado, llamamos cloneal transmisor.
Esto nos dará un nuevo transmisor que podemos pasar al primer hilo generado.
Pasamos el transmisor original a un segundo hilo generado. Esto nos da dos hilos,
cada uno enviando mensajes diferentes al mismo receptor.
Es posible que veas los valores en otro orden, dependiendo de tu sistema. Esto es
lo que hace que la concurrencia sea interesante y a la vez difícil. Si experimentas
con [nombre del método] thread::sleep, asignándole diferentes valores en los
distintos subprocesos, cada ejecución será menos determinista y generará una
salida distinta cada vez.
Ahora que hemos visto cómo funcionan los canales, veamos un método diferente
de concurrencia.
Los mutex tienen fama de ser difíciles de usar porque hay que recordar dos
reglas:
Para una metáfora práctica de un mutex, imaginemos una mesa redonda en una
conferencia con un solo micrófono. Antes de que un panelista pueda hablar, debe
pedir o indicar que quiere usar el micrófono. Una vez que lo recibe, puede hablar
todo el tiempo que desee y luego cederlo al siguiente panelista que lo solicite. Si
un panelista olvida entregar el micrófono al terminar de usarlo, nadie más puede
hablar. Si la gestión del micrófono compartido falla, el panel no funcionará según
lo previsto.
La API deMutex<T>
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
Rust
jueves, 22 de mayo de 2025 : Página 468 de 719
}
println!("m = {m:?}");
}
Listado 16-12: Exploración de la API Mutex<T>en un contexto de un solo subproceso para simplificar
La llamada a lockfallaría si otro hilo con el bloqueo entrara en pánico. En ese caso,
nadie podría obtener el bloqueo, así que hemos optado por que unwrapeste hilo
entre en pánico si nos encontramos en esa situación.
Tras adquirir el bloqueo, podemos tratar el valor de retorno, numen este caso
llamado, como una referencia mutable a los datos internos. El sistema de tipos
garantiza que adquiramos un bloqueo antes de usar el valor en [nombre del
objeto] m. El tipo de [nombre del objeto] mes [nombre del objeto] Mutex<i32>y no
[nombre del objeto] i32, por lo que debemos llamar lockpara poder usar
el i32 valor. No podemos olvidarlo; de lo contrario, el sistema de tipos no nos
permitirá acceder al interior i32 .
Después de eliminar el bloqueo, podemos imprimir el valor del mutex y ver que
pudimos cambiar el interno i32a 6.
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
Rust
jueves, 22 de mayo de 2025 : Página 470 de 719
error[E0382]: borrow of moved value: `counter`
--> src/main.rs:21:29
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex<i32>`, which does not
implement the `Copy` trait
...
8 | for _ in 0..10 {
| -------------- inside of this loop
9 | let handle = thread::spawn(move || {
| ------- value moved into closure here, in previous iteration of loop
...
21 | println!("Result: {}", *counter.lock().unwrap());
| ^^^^^^^ value borrowed here after move
|
help: consider moving the expression out of the loop so it is only moved once
|
8 ~ let mut value = counter.lock();
9 ~ for _ in 0..10 {
10 | let handle = thread::spawn(move || {
11 ~ let mut num = value.unwrap();
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
Rust
jueves, 22 de mayo de 2025 : Página 471 de 719
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
|| |
|| required by a bound introduced by this call
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented
for `Rc<Mutex<i32>>`, which is required by `{closure@src/main.rs:11:36: 11:43}: Send`
note: required because it's used within this closure
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/std/src/thread/
mod.rs:675:8
|
672 | pub fn spawn<F, T>(f: F) -> JoinHandle<T>
| ----- required by a bound in this function
...
675 | F: Send + 'static,
| ^^^^ required by this bound in `spawn`
Rust
jueves, 22 de mayo de 2025 : Página 472 de 719
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
¡Vaya, ese mensaje de error es muy extenso! Aquí está la parte importante en la
que debemos centrarnos: `Rc<Mutex<i32>>` cannot be sent between threads safely . El
compilador también nos indica el motivo: the trait `Send` is not implemented for
`Rc<Mutex<i32>>`. Hablaremos de ello Senden la siguiente sección: es una de las
características que garantiza que los tipos que usamos con los subprocesos estén
diseñados para usarse en situaciones concurrentes.
Quizás te preguntes por qué no todos los tipos primitivos son atómicos y por qué
los tipos de la biblioteca estándar no están implementados para usarse Arc<T>por
defecto. La razón es que la seguridad de subprocesos conlleva una penalización
de rendimiento que solo conviene pagar cuando realmente es necesario. Si solo
realizas operaciones con valores dentro de un solo subproceso, tu código puede
ejecutarse más rápido si no tiene que aplicar las garantías que ofrecen los tipos
atómicos.
Rust
jueves, 22 de mayo de 2025 : Página 473 de 719
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
Result: 10
¡Lo logramos! Contamos del 0 al 10, lo cual puede no parecer muy impresionante,
pero nos enseñó mucho sobre Mutex<T>seguridad de subprocesos. También
puedes usar la estructura de este programa para realizar operaciones más
complejas que simplemente incrementar un contador. Con esta estrategia,
puedes dividir un cálculo en partes independientes, dividirlas entre subprocesos y
luego usar un Mutex<T>para que cada subproceso actualice el resultado final con
su parte.
atómico a tipos primitivos. En este ejemplo, optamos por usar Mutex<T>un tipo
primitivo para centrarnos en su Mutex<T>funcionamiento.
Quizás hayas notado que counteres inmutable, pero podríamos obtener una
referencia mutable a su valor; esto significa que Mutex<T>proporciona mutabilidad
interna, como la Cellfamilia. De la misma manera que usamos RefCell<T>en el
Capítulo 15 para permitirnos mutar el contenido dentro de un Rc<T>,
usamos Mutex<T> para mutar el contenido dentro de un Arc<T>.
Otro detalle a tener en cuenta es que Rust no puede protegerte de todo tipo de
errores lógicos cuando usas Mutex<T>. Recuerda en el Capítulo 15 que
usar Rc<T>conllevaba el riesgo de crear ciclos de referencia, donde
dos Rc<T>valores se refieren entre sí, causando fugas de memoria. De igual
forma, Mutex<T>conlleva el riesgo de crear interbloqueos . Estos ocurren cuando
una operación necesita bloquear dos recursos y dos subprocesos han adquirido
cada uno uno de los bloqueos, haciendo que esperen el uno al otro eternamente.
Si te interesan los interbloqueos, intenta crear un programa en Rust que tenga un
interbloqueo; luego, investiga estrategias de mitigación de interbloqueos para
mutex en cualquier lenguaje e intenta implementarlas en Rust. La documentación
de la API de la biblioteca estándar para Mutex<T>y MutexGuardofrece información
útil.
El Sendatributo marcador indica que la propiedad de los valores del tipo que
implementa Sendpuede transferirse entre subprocesos. Casi todos los tipos de
Rust son Send, pero hay algunas excepciones, como Rc<T>: esto no puede ser
así, Sendya que si se clonó un Rc<T>valor e intentó transferir la propiedad del clon
a otro subproceso, ambos subprocesos podrían actualizar el recuento de
referencias simultáneamente. Por esta razón, Rc<T>se implementa para su uso en
situaciones de un solo subproceso donde no se desea pagar la penalización de
rendimiento de la seguridad de subprocesos.
Por lo tanto, el sistema de tipos de Rust y los límites de rasgos garantizan que
nunca se pueda enviar accidentalmente un Rc<T>valor entre subprocesos de
forma insegura. Al intentar hacer esto en el Listado 16-14, obtuvimos el error the
trait Send is not implemented for Rc<Mutex<i32>> . Al cambiar a Arc<T>, que es Send,
el código se compiló.
El Syncrasgo marcador indica que es seguro que el tipo que implementa Syncsea
referenciado desde múltiples subprocesos. En otras palabras, cualquier
tipo Tes Syncsi &T(una referencia inmutable a T) es Send, lo que significa que la
referencia puede enviarse de forma segura a otro subproceso. Similar a Send, los
tipos primitivos son Sync, y los tipos compuestos completamente por tipos que
son Synctambién son Sync.
El puntero inteligente Rc<T>tampoco es Syncpor las mismas razones por las que
no es Send. El RefCell<T>tipo (del que hablamos en el Capítulo 15) y la familia
de Cell<T>tipos relacionados no son Sync. La implementación de la comprobación
de préstamos RefCell<T>en tiempo de ejecución no es segura para subprocesos. El
puntero inteligente Mutex<T>sí lo es Syncy puede usarse para compartir el acceso
con varios subprocesos, como se vio en la sección "Compartir un Mutex<T>entre
varios subprocesos" .
Dado que los tipos compuestos por rasgos Sendy Synctambién son
automáticamente Sendy Sync, no es necesario implementarlos manualmente.
Como rasgos marcadores, ni siquiera requieren métodos de implementación.
Simplemente son útiles para aplicar invariantes relacionadas con la concurrencia.
Resumen
Esto no es lo último que verá sobre concurrencia en este libro: todo el siguiente
capítulo se centra en la programación asincrónica, y el proyecto del Capítulo 21
utilizará los conceptos de este capítulo en una situación más realista que los
ejemplos más pequeños analizados aquí.
Fundamentos de la programación
asincrónica: Async, Await, Futures y
Streams
Rust
jueves, 22 de mayo de 2025 : Página 477 de 719
Nota: Si lo piensa bien, así es como funcionan la mayoría de las llamadas a funciones. Sin embargo, el
término "bloqueo" suele reservarse para llamadas a funciones que interactúan con archivos, la red u
otros recursos del ordenador, ya que en esos casos un programa individual se beneficiaría de una
operación no bloqueante.
Sin embargo, antes de ver cómo funciona async en la práctica, debemos hacer un
pequeño desvío para analizar las diferencias entre paralelismo y concurrencia.
Paralelismo y concurrencia
Figura 17-2: Un flujo de trabajo paralelo, donde el trabajo se realiza en la Tarea A y la Tarea B de
forma independiente
paralelo, pero parte sería serial : solo podría realizarse en serie, una tarea tras
otra, como se muestra en la Figura 17-3.
Figura 17-3: Un flujo de trabajo parcialmente paralelo, donde el trabajo se realiza en la Tarea A y
la Tarea B de forma independiente hasta que la Tarea A3 se bloquea en los resultados de la Tarea B3.
De igual manera, podrías darte cuenta de que una de tus tareas depende de otra.
Ahora tu trabajo simultáneo también se ha vuelto serial.
Un futuro es un valor que puede no estar listo ahora, pero que lo estará en algún
momento. (Este mismo concepto aparece en muchos lenguajes, a veces con otros
Rust
jueves, 22 de mayo de 2025 : Página 481 de 719
Todo esto puede parecer un poco abstracto, así que escribamos nuestro primer
programa asíncrono: un pequeño web scraper. Pasaremos dos URL desde la línea
de comandos, las recuperaremos simultáneamente y devolveremos el resultado
de la que termine primero. Este ejemplo tendrá bastante sintaxis nueva, pero no
se preocupen: les explicaremos todo lo necesario sobre la marcha.
En algunos casos, trpltambién renombra o encapsula las API originales para que te
centres en los detalles relevantes de este capítulo. Si quieres entender qué hace
el crate, te recomendamos consultar su código fuente . Podrás ver de qué crate
proviene cada reexportación, y hemos incluido comentarios detallados que
explican su función.
Ahora podemos usar las distintas piezas proporcionadas por trplpara escribir
nuestro primer programa asíncrono. Construiremos una pequeña herramienta de
línea de comandos que recupera dos páginas web, extrae el <title>elemento de
cada una e imprime el título de la página que complete primero el proceso.
Comencemos escribiendo una función que tome la URL de una página como
parámetro, le haga una solicitud y devuelva el texto del elemento de título (ver
Listado 17-1).
Nota: Esto difiere del comportamiento que vimos en el capítulo anterior al usar thread::spawn" Crear
un nuevo hilo" con "spawn" , donde el cierre que pasamos a otro hilo empezó a ejecutarse
inmediatamente. También difiere de cómo muchos otros lenguajes abordan el método "async". Sin
embargo, es importante para Rust, y veremos por qué más adelante.
Ten en cuenta que la palabra clave de Rust awaitva después de la expresión que
esperas, no antes. Es decir, es una palabra clave postfija . Esto puede ser
diferente a lo que estás acostumbrado si lo has usado asyncen otros lenguajes,
pero en Rust facilita mucho el trabajo con cadenas de métodos. Como resultado,
podemos cambiar el cuerpo de page_url_forpara encadenar las
llamadas trpl::geta textfunciones y entre awaitellas, como se muestra en el Listado
17-2.
Con esto, ¡hemos escrito con éxito nuestra primera función asíncrona! Antes de
añadir el código mainpara llamarla, hablemos un poco más sobre lo que hemos
escrito y su significado.
Por lo tanto, escribir async fnequivale a escribir una función que devuelve
un futuro del tipo de retorno. Para el compilador, una definición de función como
la async fn page_titledel Listado 17-1 equivale a una función no asíncrona definida
así:
use std::future::Future;
use trpl::Html;
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
}
Listado 17-3: Llamada a la page_titlefunción desde maincon un argumento proporcionado por el usuario
Aquí, y a lo largo del resto de este capítulo, usaremos la runfunción del trplcrate,
que toma un futuro como argumento y lo ejecuta hasta su finalización. Tras
bambalinas, la llamada runconfigura un entorno de ejecución que se utiliza para
Rust
jueves, 22 de mayo de 2025 : Página 487 de 719
trpl::run(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
})
}
Listado 17-4: Esperando un bloque asíncrono contrpl::run
¡Uf! ¡Por fin tenemos código asíncrono funcional! Pero antes de añadir el código
para que los dos sitios compitan entre sí, volvamos a analizar brevemente cómo
funcionan los futuros.
Cada punto de espera (es decir, cada lugar donde el código usa la await palabra
clave) representa un punto donde se devuelve el control al entorno de ejecución.
Para que esto funcione, Rust necesita registrar el estado del bloque asíncrono
para que el entorno de ejecución pueda iniciar otro trabajo y regresar cuando esté
listo para intentar avanzar el primero. Esta es una máquina de estados invisible,
Rust
jueves, 22 de mayo de 2025 : Página 488 de 719
como si se hubiera escrito una enumeración como esta para guardar el estado
actual en cada punto de espera:
enum PageTitleFuture<'a> {
Initial { url: &'a str },
GetAwaitPoint { url: &'a str },
TextAwaitPoint { response: trpl::Response },
}
Sin embargo, escribir el código para la transición entre cada estado manualmente
sería tedioso y propenso a errores, especialmente cuando se necesita añadir más
funcionalidad y estados al código posteriormente. Afortunadamente, el
compilador de Rust crea y gestiona automáticamente las estructuras de datos de
la máquina de estados para código asíncrono. Las reglas habituales de préstamo
y propiedad de las estructuras de datos siguen vigentes y, afortunadamente, el
compilador también se encarga de comprobarlas y proporciona mensajes de error
útiles. Analizaremos algunas de ellas más adelante en este capítulo.
En última instancia, algo tiene que ejecutar esta máquina de estados, y ese algo
es un entorno de ejecución. (Por eso, al consultar entornos de ejecución, puede
encontrar referencias a ejecutores : un ejecutor es la parte del entorno de
ejecución responsable de ejecutar el código asíncrono).
Ahora puedes ver por qué el compilador nos impidió convertirnos en mainuna
función asíncrona en el Listado 17-3. Si mainfuera una función asíncrona, otra
función tendría que gestionar la máquina de estados para cualquier
futuro maindevuelto, pero maines el punto de partida del programa. En su lugar,
llamamos a la trpl::run función mainpara configurar un entorno de ejecución y
ejecutar el futuro devuelto por el asyncbloque hasta que devuelva Ready.
Nota: Algunos entornos de ejecución proporcionan macros para escribir una función asíncrona main .
Estas macros se reescriben async fn main() { ... }como una función normal fn main, lo que hace lo
mismo que hicimos manualmente en el Listado 17-5: llamar a una función que ejecuta un futuro hasta
su finalización trpl::run.
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
Nota: En esencia, racese basa en una función más general, selectque encontrarás con más frecuencia
en el código de Rust. Una select función puede hacer muchas cosas que la trpl::racefunción no puede,
pero también presenta cierta complejidad adicional que podemos obviar por ahora.
antes, trpl::Either. El Eithertipo es similar a a Resulten que tiene dos casos. ResultSin
embargo, a diferencia de , no hay una noción de éxito o fracaso integrada
en Either. En su lugar, usa Lefty Rightpara indicar "uno u otro":
¡Ya has creado un pequeño raspador web funcional! Selecciona un par de URL y
ejecuta la herramienta de línea de comandos. Descubrirás que algunos sitios son
consistentemente más rápidos que otros, mientras que en otros casos la
velocidad varía según la ejecución. Y lo que es más importante, has aprendido los
fundamentos del trabajo con futuros, así que ahora podemos profundizar en lo
que podemos hacer con async.
En muchos casos, las API para trabajar con concurrencia mediante async son muy
similares a las de los subprocesos. En otros casos, resultan ser bastante
diferentes. Incluso cuando las API parecen similares entre subprocesos y async,
suelen tener un comportamiento distinto y, casi siempre, características de
rendimiento distintas.
La primera operación que abordamos en "Crear un nuevo hilo con Spawn" fue el
conteo progresivo en dos hilos separados. Hagamos lo mismo usando async.
El trplcrate proporciona una spawn_taskfunción muy similar a la thread::spawnAPI y
una sleepfunción que es una versión asíncrona de la thread::sleepAPI. Podemos
usarlas juntas para implementar el ejemplo de conteo, como se muestra en el
Listado 17-6.
fn main() {
trpl::run(async {
trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
}
Listado 17-6: Creación de una nueva tarea para imprimir una cosa mientras la tarea principal imprime
otra cosa
Nota: A partir de este punto del capítulo, todos los ejemplos incluirán este mismo código de ajuste
con trpl::runin main, por lo que a menudo lo omitiremos, al igual que con main. ¡No olvides incluirlo
en tu código!
Luego, escribimos dos bucles dentro de ese bloque, cada uno con
una trpl::sleepllamada que espera medio segundo (500 milisegundos) antes de
enviar el siguiente mensaje. Colocamos un bucle en el cuerpo de
"a" trpl::spawn_tasky el otro en un bucle de nivel superior for. También añadimos
"an" awaitdespués de las sleepllamadas.
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
handle.await.unwrap();
Listado 17-7: Uso awaitde un controlador de unión para ejecutar una tarea hasta su finalización
Hasta ahora, parece que async y los subprocesos nos dan los mismos resultados
básicos, solo que con una sintaxis diferente: usar awaiten lugar de llamar joinal
controlador de unión y esperar las sleepllamadas.
La mayor diferencia radica en que no necesitamos generar otro hilo del sistema
operativo para esto. De hecho, ni siquiera necesitamos generar una tarea. Dado
que los bloques asíncronos se compilan en futuros anónimos, podemos colocar
cada bucle en un bloque asíncrono y hacer que el entorno de ejecución los
ejecute completamente usando la trpl::join función.
trpl::join(fut1, fut2).await;
Listado 17-8: Uso trpl::joinpara esperar dos futuros anónimos
Ahora, verás exactamente el mismo orden cada vez, lo cual es muy diferente de
lo que vimos con los hilos. Esto se debe a que la trpl::joinfunción es justa , lo que
significa que comprueba cada futuro con la misma frecuencia, alternando entre
ellos, y nunca deja que uno avance si el otro está listo. Con los hilos, el sistema
operativo decide qué hilo comprobar y durante cuánto tiempo dejarlo correr. Con
Rust asíncrono, el entorno de ejecución decide qué tarea comprobar. (En la
práctica, los detalles se complican porque un entorno de ejecución asíncrono
puede usar hilos del sistema operativo bajo el capó como parte de cómo gestiona
la concurrencia, por lo que garantizar la imparcialidad puede ser más trabajo para
un entorno de ejecución, ¡pero sigue siendo posible!). Los entornos de ejecución
no tienen que garantizar la imparcialidad para ninguna operación dada, y a
menudo ofrecen diferentes API para permitirte elegir si quieres o no
imparcialidad.
Nota: Dado que todo este código asincrónico se ejecuta en un bloque asincrónico durante
una trpl::run llamada, todo lo que se encuentra dentro de él puede evitar el bloqueo. Sin embargo, el
código externo se bloqueará al runretornar la función. Ese es el propósito de la trpl::runfunción:
permite elegir. dónde bloquear un conjunto de código asincrónico y, por lo tanto, dónde realizar la
transición entre código sincrónico y asincrónico. En la mayoría de los entornos de ejecución
asincrónicos, runse llama así block_onprecisamente por esta razón.
Comencemos examinando por qué los mensajes llegan todos a la vez después del
retraso completo, en lugar de llegar con retrasos entre cada uno. Dentro de un
bloque asíncrono dado, el orden en que... await aparecen las palabras clave en el
código también coincide con el orden en que se ejecutan al ejecutarse el
programa.
Solo hay un bloque asíncrono en el Listado 17-10, por lo que todo se ejecuta
linealmente. Sigue sin haber concurrencia. Todas las tx.sendllamadas se realizan,
intercaladas con todas las trpl::sleepllamadas y sus puntos de espera asociados.
Solo entonces el while letbucle puede pasar por alguno de los await puntos
delrecv llamadas.
trpl::join(tx_fut, rx_fut).await;
Listado 17-11: Separarse senden recvsus propios asyncbloques y esperar el futuro de esos bloques
Sin embargo, el programa nunca sale, debido a la forma en que while letel bucle
interactúa con trpl::join:
El futuro retornado trpl::joinse completa solo una vez que ambos futuros que
se le pasaron se han completado.
El txfuturo se completa una vez que termina de dormir después de enviar el
último mensaje en vals.
El rxfuturo no estará completo hasta que el while letbucle termine.
El while letbucle no finalizará hasta que awaiting rx.recvproduzca None.
La espera rx.recvregresará Nonesólo una vez que el otro extremo del canal
esté cerrado.
El canal se cerrará sólo si llamamos rx.closeo cuando el lado del
remitente, tx, se cae.
No llamamos rx.closea ninguna parte y txno se descartará hasta
que trpl::runfinalice el bloque asíncrono más externo al que se pasa.
El bloqueo no puede finalizar porque está bloqueado al trpl::joincompletarse,
lo que nos lleva de nuevo al principio de esta lista.
Necesitamos otra forma de asegurarnos de que txse elimine antes. del final de la
función.
trpl::join(tx_fut, rx_fut).await;
Listado 17-12: Una revisión del código del Listado 17-11 que se apaga correctamente cuando se
completa
Rust
jueves, 22 de mayo de 2025 : Página 500 de 719
Primero, clonamos tx, creando tx1fuera del primer bloque asíncrono. Nos
movemos tx1a ese bloque como hicimos antes con tx. Después, movemos el
original txa un nuevo bloque asíncrono, donde enviamos más mensajes con un
retraso ligeramente menor. Este nuevo bloque asíncrono se coloca después del
Rust
jueves, 22 de mayo de 2025 : Página 501 de 719
bloque asíncrono para recibir mensajes, pero también podría ir antes. La clave
está en el orden en que se esperan los futuros, no en el que se crean.
Ambos bloques asíncronos para enviar mensajes deben ser async movebloques
para que tanto " txy " como tx1"se eliminen" al finalizar. De lo contrario,
volveremos al bucle infinito inicial. Finalmente, cambiamos de
" trpl::joina" trpl::join3para gestionar el futuro adicional.
Ahora vemos todos los mensajes de ambos futuros de envío y, debido a que los
futuros de envío utilizan retrasos ligeramente diferentes después del envío, los
mensajes también se reciben en esos intervalos diferentes.
received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'
Este es un buen comienzo, pero nos limita a unos pocos futuros: dos con joino tres
con join3. Veamos cómo podríamos trabajar con más futuros.
embargo, insertar futuros en una colección y luego esperar a que algunos o todos
se completen es un patrón común.
trpl::join_all(futures).await;
Listado 17-15: Almacenamiento de futuros anónimos en un vector y llamadajoin_all
Para que esto funcione, necesitamos usar objetos de rasgo , tal como lo hicimos
en “Devolver errores de la función de ejecución” en el Capítulo 12. (Cubriremos
los objetos de rasgo en detalle en el Capítulo 18). El uso de objetos de rasgo nos
Rust
jueves, 22 de mayo de 2025 : Página 503 de 719
permite tratar cada uno de los futuros anónimos producidos por estos tipos como
el mismo tipo, porque todos implementan el Futurerasgo.
Nota: En la sección del Capítulo 8, " Uso de una enumeración para almacenar múltiples valores" ,
analizamos otra forma de incluir múltiples tipos en un vector Vec: usar una enumeración para
representar cada tipo que puede aparecer en el vector. Sin embargo, no podemos hacerlo aquí. Por un
lado, no tenemos forma de nombrar los diferentes tipos, ya que son anónimos. Por otro lado, la razón
por la que usamos un vector, join_allen primer lugar, fue para poder trabajar con una colección
dinámica de futuros donde solo nos importa que tengan el mismo tipo de salida.
let futures =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
Listado 17-16: Uso Box::newpara alinear los tipos de futuros en unVec
Hay mucho que digerir, así que analicémoslo. La primera parte del mensaje indica
que el primer bloque async ( src/main.rs:8:23: 20:10) no implementa el Unpinatributo
y sugiere usar pin!o Box::pinpara resolverlo. Más adelante en el capítulo,
profundizaremos en algunos detalles sobre Piny Unpin. Por el momento, podemos
seguir las recomendaciones del compilador para solucionar el problema. En los
Listados 17-18, comenzamos actualizando la anotación de tipo para futures, con
un Pinencapsulado para cada Box. En segundo lugar, usamos Box::pinpara fijar los
futuros.
received 'hi'
received 'more'
received 'from'
received 'messages'
received 'the'
received 'for'
received 'future'
received 'you'
Rust
jueves, 22 de mayo de 2025 : Página 507 de 719
¡Uf!
Hay algo más que explorar aquí. Por un lado, usar Pin<Box<T>>añade una
pequeña sobrecarga al colocar estos futuros en el montón con Box—y solo lo
hacemos para que los tipos coincidan. Después de todo, no necesitamos la
asignación del montón: estos futuros son locales para esta función en particular.
Como se mencionó anteriormente, Pines en sí mismo un tipo contenedor, por lo
que podemos obtener la ventaja de tener un solo tipo en Vec—la razón original
por la que lo buscamos— sin tener que Boxrealizar una asignación del montón.
Podemos usar Pindirectamente con cada futuro mediante la std::pin::pinmacro.
Hemos llegado hasta aquí ignorando que podríamos tener diferentes Output tipos.
Por ejemplo, en el Listado 17-20, el futuro anónimo de aimplements Future<Output
= u32>, el futuro anónimo de bimplements Future<Output = &str>y el futuro
anónimo de cimplements Future<Output = bool>.
Podemos usar trpl::join!para esperarlos, ya que nos permite pasar múltiples tipos
de futuros y produce una tupla de esos tipos. No podemos usar trpl::join_all, ya que
requiere que todos los futuros pasados tengan el mismo tipo. Recuerda, ese error
fue lo que nos impulsó a empezar esta aventura con Pin!
trpl::race(slow, fast).await;
Listado 17-21: Uso racepara obtener el resultado de cualquier futuro que termine primero
Tenga en cuenta que si invierte el orden de los argumentos a race, el orden de los
mensajes "iniciados" cambia, aunque el fastfuturo siempre se complete primero.
Esto se debe a que la implementación de esta racefunción en particular no es
justa. Siempre ejecuta los futuros pasados como argumentos en el orden en que
se pasan. Otras implementaciones son justas y elegirán aleatoriamente qué
futuro sondear primero. Sin embargo, independientemente de si la
implementación de "race" que usemos es justa, uno de los futuros se ejecutará
hasta el primero awaiten su cuerpo antes de que pueda comenzar otra tarea.
Esto significa que si realizas mucho trabajo en un bloque asíncrono sin un punto
de espera, ese futuro impedirá que otros futuros avancen. A veces se dice que un
futuro está dejando sin recursos a otros futuros. En algunos casos, esto puede no
ser un gran problema. Sin embargo, si estás realizando una configuración costosa
o un trabajo de larga duración, o si tienes un futuro que realizará una tarea
específica indefinidamente, tendrás que pensar cuándo y dónde devolver el
control al entorno de ejecución.
Del mismo modo, si tiene operaciones de bloqueo de larga duración, async puede
ser una herramienta útil para proporcionar formas para que las diferentes partes
del programa se relacionen entre sí.
let b = async {
println!("'b' started.");
slow("b", 75);
slow("b", 10);
slow("b", 15);
slow("b", 350);
trpl::sleep(Duration::from_millis(50)).await;
println!("'b' finished.");
};
trpl::race(a, b).await;
Listado 17-23: Uso thread::sleeppara simular operaciones lentas
'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.
Rust
jueves, 22 de mayo de 2025 : Página 511 de 719
let a = async {
println!("'a' started.");
slow("a", 30);
trpl::sleep(one_ms).await;
slow("a", 10);
trpl::sleep(one_ms).await;
slow("a", 20);
trpl::sleep(one_ms).await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::sleep(one_ms).await;
slow("b", 10);
trpl::sleep(one_ms).await;
slow("b", 15);
trpl::sleep(one_ms).await;
slow("b", 35);
trpl::sleep(one_ms).await;
println!("'b' finished.");
};
Listado 17-24: Usosleep para permitir que las operaciones se apaguen y se avance
'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
Rust
jueves, 22 de mayo de 2025 : Página 512 de 719
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::yield_now().await;
slow("b", 10);
trpl::yield_now().await;
slow("b", 15);
trpl::yield_now().await;
slow("b", 35);
trpl::yield_now().await;
println!("'b' finished.");
};
Listado 17-25: Usoyield_now para permitir que las operaciones se apaguen y se avance
Este código es más claro sobre la intención real y puede ser significativamente
más rápido que usar sleep, ya que temporizadores como el usado por sleepsuelen
tener límites en su granularidad. La versión de que sleepestamos usando, por
ejemplo, siempre estará en reposo durante al menos un milisegundo, incluso si le
Rust
jueves, 22 de mayo de 2025 : Página 513 de 719
Esto significa que async puede ser útil incluso para tareas de computación,
dependiendo de qué otras tareas esté realizando el programa, ya que proporciona
una herramienta útil para estructurar las relaciones entre las diferentes partes del
Rust
jueves, 22 de mayo de 2025 : Página 514 de 719
programa. Esta es una forma de multitarea cooperativa , donde cada futuro tiene
la capacidad de determinar cuándo cede el control mediante puntos de espera.
Por lo tanto, cada futuro también tiene la responsabilidad de evitar bloqueos
prolongados. En algunos sistemas operativos embebidos basados en Rust, esta es
la única tipo de multitarea.
También podemos componer futuros para crear nuevos patrones. Por ejemplo,
podemos crear una timeoutfunción con bloques de construcción asíncronos que ya
tenemos. Al terminar, el resultado será otro bloque de construcción que podremos
usar para crear aún más abstracciones asíncronas.
También sabemos que raceno es justo sondear los argumentos en el orden en que
se pasan. Por lo tanto, pasamos future_to_trya raceprimero para que tenga la
oportunidad de completarse, incluso si max_timela duración es muy corta.
Si future_to_trytermina primero, raceretornará Leftcon la salida de future_to_try.
Si timertermina primero, raceretornará Rightcon la salida del temporizador de() .
// --snip--
fn main() {
trpl::run(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
Rust
jueves, 22 de mayo de 2025 : Página 516 de 719
"Finally finished"
};
Dado que los futuros se integran con otros futuros, se pueden crear herramientas
muy potentes utilizando bloques de construcción asíncronos más pequeños. Por
ejemplo, se puede usar este mismo enfoque para combinar tiempos de espera
con reintentos y, a su vez, usarlos con operaciones como llamadas de red (uno de
los ejemplos del principio del capítulo).
La similitud entre iteradores y flujos en Rust significa que podemos crear un flujo
a partir de cualquier iterador. Al igual que con un iterador, podemos trabajar con
un flujo llamando a su nextmétodo y esperando la salida, como en el Listado 17-
30.
error[E0599]: no method named `next` found for struct `Iter` in the current scope
--> src/main.rs:10:40
|
10 | while let Some(value) = stream.next().await {
| ^^^^
|
= note: the full type name has been written to
'file:///projects/async_await/target/debug/deps/async_await-9de943556a6001b8.long-type-
1281356139287206597.txt'
= note: consider using `--verbose` to print the full type name to the console
= help: items from traits can only be used if the trait is in scope
help: the following traits which provide `next` are implemented but not in scope; perhaps you
want to import one of them
|
1 + use crate::trpl::StreamExt;
|
1 + use futures_util::stream::stream::StreamExt;
|
1 + use std::iter::Iterator;
|
1 + use std::str::pattern::Searcher;
|
help: there is a method `try_next` with a similar name
|
10 | while let Some(value) = stream.try_next().await {
| ~~~~~~~~
Explicaremos los rasgos Streamy StreamExtcon un poco más de detalle al final del
capítulo, pero por ahora todo lo que necesita saber es que el Streamrasgo define
una interfaz de bajo nivel que combina efectivamente los
rasgos Iteratory Future. StreamExtproporciona un conjunto de API de nivel superior
además de Stream, incluido el nextmétodo , así como otros métodos de utilidad
similares a los proporcionados por el Iteratorrasgo. Streamy StreamExtaún no son
parte de la biblioteca estándar de Rust, pero la mayoría de las cajas de
ecosistemas usan la misma definición.
fn main() {
trpl::run(async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
Con todas esas piezas juntas, ¡este código funciona como queremos! Además,
ahora que está StreamExtdentro del alcance, podemos usar todos sus métodos de
utilidad, al igual que con los iteradores. Por ejemplo, en el Listado 17-32, usamos
el filtermétodo para filtrar todo excepto los múltiplos de tres y cinco.
fn main() {
trpl::run(async {
let values = 1..101;
let iter = values.map(|n| n * 2);
let stream = trpl::stream_from_iter(iter);
Claro que esto no es muy interesante, ya que podríamos hacer lo mismo con
iteradores normales y sin ningún tipo de async. Veamos qué podemos hacer
que sea exclusivo de los streams.
Composición de secuencias
fn main() {
trpl::run(async {
let mut messages = get_messages();
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
for message in messages {
tx.send(format!("Message: '{message}'")).unwrap();
}
ReceiverStream::new(rx)
Rust
jueves, 22 de mayo de 2025 : Página 521 de 719
}
Listado 17-33: Utilizando el rxreceptor comoReceiverStream
Message: 'a'
Message: 'b'
Message: 'c'
Message: 'd'
Message: 'e'
Message: 'f'
Message: 'g'
Message: 'h'
Message: 'i'
Message: 'j'
fn main() {
trpl::run(async {
let mut messages =
pin!(get_messages().timeout(Duration::from_millis(200)));
Sin embargo, como no hay retrasos entre mensajes, este tiempo de espera no
altera el comportamiento del programa. Añadamos una variable de retraso a los
mensajes que enviamos, como se muestra en el Listado 17-35.
trpl::spawn_task(async move {
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
for (index, message) in messages.into_iter().enumerate() {
let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
trpl::sleep(Duration::from_millis(time_to_sleep)).await;
tx.send(format!("Message: '{message}'")).unwrap();
}
});
ReceiverStream::new(rx)
}
Listado 17-35: Envío de mensajes txcon un retraso asíncrono sin crear get_messagesuna función
asíncrona
Nota: Llamar spawn_taskde esta manera funciona porque ya configuramos nuestro entorno de
ejecución; de lo contrario, se generaría un pánico. Otras implementaciones optan por diferentes
soluciones: pueden generar un nuevo entorno de ejecución y evitar el pánico, pero terminan con una
sobrecarga adicional, o simplemente no ofrecen una forma independiente de generar tareas sin
referencia a un entorno de ejecución. ¡Asegúrese de conocer la solución elegida por su entorno de
ejecución y escriba su código en consecuencia!
Ahora nuestro código tiene un resultado mucho más interesante. Entre cada par
de mensajes, un Problem: Elapsed(())error.
Message: 'a'
Problem: Elapsed(())
Message: 'b'
Message: 'c'
Problem: Elapsed(())
Message: 'd'
Message: 'e'
Problem: Elapsed(())
Message: 'f'
Message: 'g'
Problem: Elapsed(())
Message: 'h'
Message: 'i'
Problem: Elapsed(())
Message: 'j'
Fusión de secuencias
trpl::spawn_task(async move {
let mut count = 0;
loop {
trpl::sleep(Duration::from_millis(1)).await;
count += 1;
tx.send(count).unwrap();
}
});
ReceiverStream::new(rx)
}
Listado 17-36: Creación de una secuencia con un contador que se emitirá una vez cada milisegundo
Este tipo de bucle infinito, que termina solo cuando se desconecta todo el entorno
de ejecución, es bastante común en Rust asíncrono: muchos programas necesitan
Rust
jueves, 22 de mayo de 2025 : Página 525 de 719
Primero, podemos usar el mapmétodo auxiliar para transformar ``` intervalsen una
cadena. Segundo, necesitamos que coincida con ` Timeout`` messages. Sin
Rust
jueves, 22 de mayo de 2025 : Página 526 de 719
--snip--
Interval: 38
Interval: 39
Interval: 40
Message: 'a'
Interval: 41
Interval: 42
Interval: 43
--snip--
El listado 17-39 muestra una forma de resolver estos dos últimos problemas.
Interval: 1
Message: 'a'
Interval: 2
Interval: 3
Problem: Elapsed(())
Interval: 4
Message: 'b'
Interval: 5
Message: 'c'
Interval: 6
Interval: 7
Problem: Elapsed(())
Interval: 8
Message: 'd'
Interval: 9
Message: 'e'
Interval: 10
Interval: 11
Problem: Elapsed(())
Interval: 12
Hay un último aspecto que debemos gestionar: ¡los errores! Con ambos flujos
basados en canales, las sendllamadas podrían fallar al cerrarse el otro lado del
canal; esto depende de cómo el entorno de ejecución ejecute los futuros que
conforman el flujo. Hasta ahora, hemos ignorado esta posibilidad llamando
a unwrap, pero en una aplicación con buen funcionamiento, deberíamos gestionar
el error explícitamente, como mínimo finalizando el bucle para no intentar enviar
más mensajes. El listado 17-40 muestra una estrategia de error sencilla: imprimir
el problema y luego breakdesde los bucles.
trpl::spawn_task(async move {
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
Rust
jueves, 22 de mayo de 2025 : Página 528 de 719
ReceiverStream::new(rx)
}
trpl::spawn_task(async move {
let mut count = 0;
loop {
trpl::sleep(Duration::from_millis(1)).await;
count += 1;
ReceiverStream::new(rx)
}
Listado 17-40: Manejo de errores y cierre de bucles
Ahora que hemos visto un montón de async en la práctica, demos un paso atrás y
profundicemos en algunos de los detalles de cómo Future, Stream, y otras
características clave que Rust usa para hacer que async funcione.
con Rust. Sin embargo, a veces te encontrarás con situaciones en las que
necesitarás comprender algunos detalles adicionales. En esta sección,
profundizaremos lo suficiente para ayudarte en esos casos, dejando el
análisis más profundo para otra documentación.
El Futurerasgo
use std::pin::Pin;
use std::task::{Context, Poll};
enum Poll<T> {
Ready(T),
Pending,
}
Este Polltipo es similar a un Option. Tiene una variante con valor, Ready(T), y otra
sin valor, Pending. Sin embargo, Pollsignifica algo muy diferente de Option.
La Pendingvariante indica que el futuro aún tiene trabajo por hacer, por lo que el
invocador deberá volver a comprobarlo más tarde. La Ready variante indica que el
futuro ha finalizado su trabajo y el Tvalor está disponible.
Nota: En la mayoría de los futuros, el emisor no debe pollvolver a llamar después de que el futuro haya
regresado Ready. Muchos futuros entrarán en pánico si se les vuelve a sondear después de estar listos.
Rust
jueves, 22 de mayo de 2025 : Página 530 de 719
Los futuros que se pueden sondear de nuevo de forma segura lo indicarán explícitamente en su
documentación. Esto es similar a cómo Iterator::nextse comporta.
Cuando ves código que usa await, Rust lo compila internamente en código que
llama a poll. Si revisas el Listado 17-4, donde imprimimos el título de la página
para una URL una vez resuelta, Rust lo compila en algo similar (aunque no
exactamente) a esto:
match page_title(url).poll() {
Ready(page_title) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// But what goes here?
}
}
¿Qué debemos hacer cuando el futuro está en calma Pending? Necesitamos una
forma de intentarlo una y otra vez, hasta que el futuro esté finalmente listo. En
otras palabras, necesitamos un bucle:
Los detalles exactos de cómo un tiempo de ejecución hace eso están más allá del
alcance de este libro, pero la clave es ver la mecánica básica de los futuros: un
tiempo de ejecución sondea cada futuro del que es responsable, volviendo a
poner el futuro a dormir cuando aún no está listo.
Este mensaje de error nos indica no solo que necesitamos fijar los valores, sino
también por qué es necesario. La trpl::join_allfunción devuelve una estructura
llamada JoinAll. Esta estructura es genérica sobre un tipo F, que está restringido a
implementar el Futureatributo. Esperar directamente un futuro con , awaitfija el
futuro implícitamente. Por eso no necesitamos usarla pin!siempre que queramos
esperar futuros.
Rust
jueves, 22 de mayo de 2025 : Página 532 de 719
¡Hay mucho que asimilar! Para comprenderlo bien, profundicemos un poco más
en cómo Futurefunciona realmente el rasgo, en particular en lo que respecta a la
fijación .
use std::pin::Pin;
use std::task::{Context, Poll};
// Required method
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
Le dice a Rust qué tipo selfdebe ser el método para que se llame.
No puede ser cualquier tipo. Se limita al tipo en el que se implementa el
método, una referencia o un puntero inteligente a ese tipo, o
una Pinreferencia envolvente a ese tipo.
Veremos más sobre esta sintaxis en el Capítulo 18. Por ahora, basta con saber
que, si queremos sondear un futuro para comprobar si es Pendingo Ready(Output),
necesitamos una Pinreferencia mutable con encapsulado de tipo.
Pines un contenedor para tipos similares a punteros como &, &mut, Boxy Rc.
(Técnicamente, Pinfunciona con tipos que implementan los rasgos Derefo , pero
Rust
jueves, 22 de mayo de 2025 : Página 533 de 719
Sin embargo, por defecto, no es seguro mover ningún objeto que tenga una
referencia a sí mismo, ya que las referencias siempre apuntan a la dirección de
memoria real de aquello a lo que hacen referencia (véase la Figura 17-5). Si se
mueve la estructura de datos, esas referencias internas seguirán apuntando a la
ubicación anterior. Sin embargo, esa ubicación de memoria ya no es válida. Por
Rust
jueves, 22 de mayo de 2025 : Página 534 de 719
PinSe basa en esto para brindarnos la garantía exacta que necesitamos. Al fijar un
valor envolviendo un puntero a ese valor en [nombre del objeto] Pin, este ya no
puede moverse. Por lo tanto, si tiene [nombre del objeto] Pin<Box<SomeType>>, en
realidad fija el SomeTypevalor, no el Boxpuntero. La Figura 17-6 ilustra este
proceso.
Sin embargo, la mayoría de los tipos se pueden mover con total seguridad,
incluso si se encuentran detrás de un Pinpuntero. Solo debemos considerar la
fijación cuando los elementos tienen referencias internas. Los valores primitivos,
como números y booleanos, son seguros, ya que, obviamente, no tienen
Rust
jueves, 22 de mayo de 2025 : Página 535 de 719
Para concretarlo, piense en un String: tiene una longitud y los caracteres Unicode
que lo componen. Podemos encapsular un String, Pincomo se muestra en la Figura
17-8. Sin embargo, Stringimplementa automáticamente Unpin, como la mayoría de
los demás tipos en Rust.
Figura 17-8: Fijación de una `Cadena`; la línea punteada indica que la `Cadena` implementa el
rasgo `Desanclar` y, por lo tanto, no está fijada.
que Stringno tiene referencias internas que hagan que sea inseguro moverse.
Precisamente por eso implementa Unpinen lugar de !Unpin.
Figura 17-9: Reemplazo de la `String` con una `String` completamente diferente en la memoria.
Nota: Esta combinación de Piny Unpinpermite implementar de forma segura toda una clase de tipos
complejos en Rust, que de otro modo resultarían difíciles por ser autorreferenciales. Los tipos que
requieren Pinson más comunes en Rust asíncrono actualmente, pero ocasionalmente también pueden
verse en otros contextos.
Los detalles de cómo Pinfuncionan Unpiny las reglas que deben respetar se cubren extensamente en la
documentación de la API de std::pin, por lo que, si está interesado en obtener más información, este es
un excelente lugar para comenzar.
Si quieres entender con más detalle cómo funcionan las cosas bajo el capó, consulta los
Capítulos 2 y 4 de Programación asincrónica en Rust .
ElStreamrasgo
Ahora que comprende mejor los rasgos Future, Piny Unpin, podemos centrarnos en
el Streamrasgo. Como aprendió anteriormente en este capítulo, los flujos son
similares a los iteradores asíncronos. A diferencia de Iteratory Future, sin
embargo, Streamno tiene una definición en la biblioteca estándar al momento de
escribir este artículo, pero existe una definición muy común en la biblioteca
estándar.futures contenedor utilizado en todo el ecosistema.
Rust
jueves, 22 de mayo de 2025 : Página 537 de 719
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>
) -> Poll<Option<Self::Item>>;
}
// other methods...
}
Nota: La definición que usamos anteriormente en este capítulo es ligeramente diferente, ya que admite
versiones de Rust que aún no admitían el uso de funciones asíncronas en rasgos. Por lo tanto, se ve así:
Ese Nexttipo es un structque implementa Futurey nos permite nombrar el tiempo de vida de la
referencia a selfcon Next<'_, Self>, para que await pueda trabajar con este método.
Eso es todo lo que cubriremos en cuanto a los detalles básicos de estos rasgos.
Para concluir, veamos cómo se integran los futuros (incluidos los flujos), las tareas
y los hilos.
ReceiverStream::new(rx)
}
Listado 17-41: Uso de las std::threadAPI en lugar de las API asíncronas trplpara la get_intervalsfunción
Si ejecuta este código, la salida es idéntica a la del Listado 17-40. Observe los
pocos cambios desde la perspectiva del código de llamada. Es más, aunque una
de nuestras funciones generó una tarea asíncrona en el entorno de ejecución y la
otra generó un subproceso del sistema operativo, los flujos resultantes no se
vieron afectados por las diferencias.
Sin embargo, existe una razón por la que estas API son tan similares. Los hilos
actúan como límite para conjuntos de operaciones síncronas; la concurrencia es
posible entre hilos. Las tareas actúan como límite para conjuntos
de operaciones asíncronas ; la concurrencia es posible tanto entre tareas
como dentro de ellas, ya que una tarea puede alternar entre futuros en su cuerpo.
Finalmente, los futuros son la unidad de concurrencia más granular de Rust, y
cada futuro puede representar un árbol de otros futuros. El entorno de ejecución
(específicamente, su ejecutor) gestiona las tareas, y las tareas gestionan los
futuros. En ese sentido, las tareas son similares a hilos ligeros gestionados por el
entorno de ejecución, con capacidades adicionales gracias a que son gestionadas
por un entorno de ejecución en lugar del sistema operativo.
Esto no significa que las tareas asíncronas sean siempre mejores que los hilos (o
viceversa). La concurrencia con hilos es, en cierto modo, un modelo de
programación más sencillo que la concurrencia con async. Esto puede ser una
fortaleza o una debilidad. Los hilos son, en cierto modo, un proceso de "disparar y
olvidar"; no tienen un equivalente nativo a un futuro, por lo que simplemente se
ejecutan hasta su finalización sin ser interrumpidos, excepto por el propio sistema
operativo. Es decir, no tienen soporte integrado para la concurrencia
intratarea como sí lo tienen los futuros. Los hilos en Rust tampoco tienen
mecanismos de cancelación, un tema que no hemos abordado explícitamente en
Rust
jueves, 22 de mayo de 2025 : Página 541 de 719
este capítulo, pero que se insinuó por el hecho de que, al finalizar un futuro, su
estado se limpiaba correctamente.
Las tareas, por lo tanto, nos brindan mayor control sobre los futuros,
permitiéndonos elegir dónde y cómo agruparlos. Resulta que los hilos y las tareas
suelen funcionar muy bien juntos, ya que las tareas pueden (al menos en algunos
entornos de ejecución) moverse entre hilos. De hecho, el entorno de ejecución
que hemos estado usando, incluidas las funciones spawn_blockingy spawn_task, es
multihilo por defecto. Muchos entornos de ejecución utilizan un enfoque
llamado robo de trabajo para mover tareas de forma transparente entre hilos,
según su uso actual, y así mejorar el rendimiento general del sistema. Este
enfoque requiere hilos y tareas, y por lo tanto, futuros.
Al pensar qué método utilizar en cada caso, tenga en cuenta estas reglas
generales:
fn main() {
let (tx, mut rx) = trpl::channel();
thread::spawn(move || {
Rust
jueves, 22 de mayo de 2025 : Página 542 de 719
for i in 1..11 {
tx.send(i).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
trpl::run(async {
while let Some(message) = rx.recv().await {
println!("{message}");
}
});
}
Listado 17-42: Envío de mensajes con código de bloqueo en un hilo y espera de los mensajes en un
bloque asíncrono
Resumen
Esta no es la última vez que verá concurrencia en este libro. El proyecto
del Capítulo 21 aplicará estos conceptos en una situación más realista que los
ejemplos más sencillos aquí presentados y comparará la resolución de problemas
con subprocesos frente a las tareas de forma más directa.
analizaremos cómo se relacionan los modismos de Rust con aquellos que quizás
conozcas de la programación orientada a objetos.
Características de programación
orientada a objetos de Rust
La programación orientada a objetos (POO) es una forma de modelar programas.
Los objetos, como concepto programático, se introdujeron en el lenguaje de
programación Simula en la década de 1960. Estos objetos influyeron en la
arquitectura de programación de Alan Kay, en la que los objetos se intercambian
mensajes. Para describir esta arquitectura, acuñó el término " programación
orientada a objetos" en 1967. Existen muchas definiciones que compiten entre sí
para definir qué es la POO, y según algunas de ellas, Rust se considera orientado
a objetos, pero según otras no. En este capítulo, exploraremos ciertas
características que se consideran comúnmente orientadas a objetos y cómo se
traducen en el lenguaje común de Rust. A continuación, le mostraremos cómo
implementar un patrón de diseño orientado a objetos en Rust y analizaremos las
ventajas y desventajas de hacerlo frente a implementar una solución que
aproveche algunas de las ventajas de Rust.
Según esta definición, Rust está orientado a objetos: las estructuras y las
enumeraciones contienen datos, y impllos bloques proporcionan métodos en ellas.
Aunque las estructuras y las enumeraciones con métodos no
se denominan objetos, ofrecen la misma funcionalidad, según la definición de
objetos de la Banda de los Cuatro.
La estructura está marcada pubpara que otro código pueda usarla, pero sus
campos permanecen privados. Esto es importante en este caso, ya que queremos
asegurar que, al añadir o eliminar un valor de la lista, también se actualice el
promedio. Para ello, implementamos los métodos add, remove, y averageen la
estructura, como se muestra en el Listado 18-2:
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
Listado 18-2: Implementaciones de los métodos públicos add, remove, y averageenAveragedCollection
Los métodos públicos add, removey averageson las únicas formas de acceder o
modificar datos en una instancia de AveragedCollection. Cuando se añade o elimina
un elemento mediante listel método, las implementaciones de cada
uno también invocan el método privado que gestiona la actualización del
campo.addremoveupdate_averageaverage
Si un lenguaje debe tener herencia para ser orientado a objetos, Rust no lo es. No
es posible definir una estructura que herede los campos y las implementaciones
de métodos de la estructura principal sin usar una macro.
Se puede elegir la herencia por dos razones principales. Una es la reutilización del
código: se puede implementar un comportamiento específico para un tipo, y la
herencia permite reutilizar esa implementación para un tipo diferente. Esto se
puede hacer de forma limitada en el código de Rust mediante implementaciones
predeterminadas de métodos de traits, como se vio en el Listado 10-14 cuando
añadimos una implementación predeterminada del summarizemétodo en
el Summarytrait. Cualquier tipo que implemente el Summarytrait tendrá
el summarizemétodo disponible sin necesidad de código adicional. Esto es similar a
que una clase padre tenga la implementación de un método y una clase hija
Rust
jueves, 22 de mayo de 2025 : Página 547 de 719
La otra razón para usar la herencia se relaciona con el sistema de tipos: permitir
que un tipo hijo se use en los mismos lugares que el tipo padre. Esto también se
denomina polimorfismo , lo que significa que se pueden sustituir varios objetos
entre sí en tiempo de ejecución si comparten ciertas características.
Polimorfismo
Para muchos, el polimorfismo es sinónimo de herencia. Sin embargo, en realidad es un concepto más
general que se refiere al código que puede trabajar con datos de múltiples tipos. En el caso de la
herencia, estos tipos suelen ser subclases.
En cambio, Rust utiliza genéricos para abstraer diferentes tipos posibles y límites de características, e
imponer restricciones sobre lo que esos tipos deben proporcionar. Esto a veces se
denomina polimorfismo paramétrico acotado .
Por estas razones, Rust adopta un enfoque diferente: usar objetos de rasgo en
lugar de herencia. Veamos cómo los objetos de rasgo facilitan el polimorfismo en
Rust.
Para lograr esto en un lenguaje con herencia, podríamos definir una clase
llamada Componentque contenga un método llamado draw. Las demás clases,
como Button, Imagey SelectBox, heredarían de Componenty, por lo tanto, heredarían
el drawmétodo. Cada una podría sobrescribir el drawmétodo para definir su
comportamiento personalizado, pero el framework podría tratar todos los tipos
como si fueran Componentinstancias e invocarlos draw. Sin embargo, como Rust no
tiene herencia, necesitamos otra forma de estructurar la guibiblioteca para que los
usuarios puedan extenderla con nuevos tipos.
Esta sintaxis debería resultar familiar tras nuestra explicación sobre cómo definir
rasgos en el Capítulo 10. A continuación, se presenta una sintaxis nueva: el
Listado 18-4 define una estructura llamada Screenque contiene un vector
llamado components. Este vector es de tipo Box<dyn Draw>, que es un objeto de
Rust
jueves, 22 de mayo de 2025 : Página 550 de 719
Esto funciona de forma diferente a definir una estructura que utiliza un parámetro
de tipo genérico con límites de rasgo. Un parámetro de tipo genérico solo puede
sustituirse por un tipo concreto a la vez, mientras que los objetos de rasgo
permiten que varios tipos concretos lo reemplacen en tiempo de ejecución. Por
ejemplo, podríamos haber definido la Screenestructura utilizando un tipo genérico
y un límite de rasgo, como en el Listado 18-6:
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
Listado 18-6: Una implementación alternativa de la Screenestructura y su runmétodo utilizando
genéricos y límites de rasgos
Rust
jueves, 22 de mayo de 2025 : Página 551 de 719
Esto nos limita a una Screeninstancia con una lista de componentes, todos de
tipo Buttono todos de tipo TextField. Si solo se tienen colecciones homogéneas, es
preferible usar genéricos y límites de rasgos, ya que las definiciones se
monomorfizarán en tiempo de compilación para usar los tipos concretos.
Por otro lado, con el método que utiliza objetos de rasgo, una Screeninstancia
puede contener un objeto Vec<T>que contiene un objeto Box<Button>y un
objeto Box<TextField>. Veamos cómo funciona esto y luego hablaremos de las
implicaciones para el rendimiento en tiempo de ejecución.
Implementando el rasgo
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
Rust
jueves, 22 de mayo de 2025 : Página 553 de 719
}
Listado 18-9: Uso de objetos de rasgo para almacenar valores de diferentes tipos que implementan el
mismo rasgo
Este concepto —preocuparse únicamente por los mensajes a los que responde un
valor, en lugar de por su tipo concreto— es similar al concepto de tipado de
pato en lenguajes de tipado dinámico: si camina como un pato y grazna como un
pato, ¡debe ser un pato! En la implementación de runon Screen del Listado 18-
5, runno necesita saber cuál es el tipo concreto de cada componente. No
comprueba si un componente es una instancia de a Button o a SelectBox,
simplemente invoca el drawmétodo en el componente. Al especificar Box<dyn
Draw>el tipo de los valores en el components vector, hemos definido Screenque
necesitamos valores sobre los que podemos invocar el draw método.
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
Listado 18-10: Intento de usar un tipo que no implementa el rasgo del objeto de rasgo
Rust
jueves, 22 de mayo de 2025 : Página 554 de 719
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5| components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not
implemented for `String`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast from `Box<String>` to `Box<dyn Draw>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error
Este error nos permite saber que estamos pasando algo que Screenno queríamos
pasar y por lo tanto deberíamos pasar un tipo diferente o deberíamos
implementarlo Drawpara Stringpoder Screenllamarlo draw.
Cualquier otro cambio que se intente en una publicación no debería tener efecto.
Por ejemplo, si intentamos aprobar un borrador de una publicación del blog antes
de solicitar una revisión, la publicación debería permanecer como borrador no
publicado.
use blog::Post;
fn main() {
let mut post = Post::new();
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
Listado 18-11: Código que demuestra el comportamiento deseado que queremos blogque tenga nuestra
caja
Tenga en cuenta que el único tipo con el que interactuamos desde la caja es
el Post tipo. Este tipo usará el patrón de estado y contendrá un valor que será uno
de los tres objetos de estado que representan los diferentes estados en los que
puede encontrarse una publicación: borrador, en espera de revisión o publicada.
El cambio de un estado a otro se gestionará internamente dentro del Posttipo. Los
estados cambian en respuesta a los métodos llamados por los usuarios de nuestra
biblioteca en la Postinstancia, pero no tienen que gestionar los cambios de estado
directamente. Además, los usuarios no pueden cometer errores con los estados,
como publicar una publicación antes de que se revise.
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}
trait State {}
struct Draft {}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
}
Rust
jueves, 22 de mayo de 2025 : Página 560 de 719
struct Draft {}
struct PendingReview {}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
struct Published {}
impl Post {
// --snip--
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
// --snip--
}
Rust
jueves, 22 de mayo de 2025 : Página 563 de 719
Listado 18-17: Actualización del contentmétodo en Postpara delegar a un contentmétodo enState
Dado que el objetivo es mantener todas estas reglas dentro de las estructuras
que implementan State, llamamos a un contentmétodo en el valor statey pasamos
la instancia de la publicación (es decir, self) como argumento. Luego, devolvemos
el valor obtenido al usar el contentmétodo en el statevalor.
// --snip--
struct Published {}
Quizás te hayas preguntado por qué no usamos una enumcon los diferentes estados de publicación
posibles como variantes. Sin duda, es una posible solución; pruébala y compara los resultados para ver
cuál prefieres. Una desventaja de usar una enumeración es que cada lugar que verifique el valor de la
enumeración necesitará una matchexpresión o algo similar para gestionar todas las variantes posibles.
Esto podría resultar más repetitivo que esta solución con el objeto de rasgo.
Con el patrón de estado, los Postmétodos y los lugares que usamos Postno
necesitan matchexpresiones, y para agregar un nuevo estado, solo necesitaríamos
agregar una nueva estructura e implementar los métodos de rasgos en esa
estructura.
Una desventaja del patrón de estado es que, dado que los estados implementan
las transiciones entre estados, algunos están acoplados entre sí. Si añadimos otro
estado entre PendingReviewy Published, como Scheduled, tendríamos que cambiar el
código en PendingReview[transición a] Scheduleden su lugar. Sería menos trabajo
si PendingReviewno fuera necesario cambiar con la adición de un nuevo estado,
pero eso implicaría cambiar a otro patrón de diseño.
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Listado 18-19: A Postcon contentmétodo y DraftPostsin contentmétodo
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
Listado 18-20: Un PendingReviewPostque se crea al llamar request_reviewa DraftPosty un approvemétodo
que convierte a PendingReviewPosten un publicadoPost
fn main() {
let mut post = Post::new();
Los cambios que tuvimos que hacer para mainreasignar postsignifican que esta
implementación ya no sigue exactamente el patrón de estado orientado a
objetos: las transformaciones entre los estados ya no están encapsuladas
completamente dentro delPost implementación. Sin embargo, nuestra ventaja es
que los estados inválidos ahora son imposibles gracias al sistema de tipos y a la
comprobación de tipos que se realiza en tiempo de compilación. Esto garantiza
que ciertos errores, como la visualización del contenido de una entrada no
publicada, se detecten antes de que lleguen a producción.
Pruebe las tareas sugeridas al principio de esta sección en el blogcrate tal como
está después del Listado 18-21 para ver qué opina sobre el diseño de esta versión
del código. Tenga en cuenta que algunas tareas podrían ya estar completadas en
este diseño.
Resumen
Independientemente de si consideras o no que Rust es un lenguaje orientado a
objetos después de leer este capítulo, ahora sabes que puedes usar objetos trait
para obtener algunas características orientadas a objetos en Rust. El envío
dinámico puede dar a tu código cierta flexibilidad a cambio de un poco de
rendimiento en tiempo de ejecución. Puedes usar esta flexibilidad para
implementar patrones orientados a objetos que mejoren la mantenibilidad de tu
código. Rust también tiene otras características, como la propiedad, que los
lenguajes orientados a objetos no tienen. Un patrón orientado a objetos no
siempre será la mejor manera de aprovechar las fortalezas de Rust, pero es una
opción disponible.
Patrones y combinaciones
Los patrones son una sintaxis especial en Rust que permite la comparación con la
estructura de tipos, tanto complejos como simples. Usar patrones junto
con match expresiones y otras construcciones proporciona mayor control sobre el
flujo de control de un programa. Un patrón consiste en una combinación de los
siguientes elementos:
Literales
Matrices, enumeraciones, estructuras o tuplas desestructuradas
Variables
Comodines
Marcadores de posición
Rust
jueves, 22 de mayo de 2025 : Página 571 de 719
Este capítulo es una referencia sobre todo lo relacionado con los patrones.
Abordaremos los lugares válidos para su uso, la diferencia entre patrones
refutables e irrefutables, y los diferentes tipos de sintaxis de patrones que podría
encontrar. Al final del capítulo, sabrá cómo usar patrones para expresar diversos
conceptos con claridad.
matchBrazos
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
Rust
jueves, 22 de mayo de 2025 : Página 572 de 719
Por ejemplo, aquí está la matchexpresión del Listado 6-5 que coincide con
un Option<i32>valor en la variable x:
match x {
None => None,
Some(i) => Some(i + 1),
}
El patrón en particular _coincide con cualquier valor, pero nunca se vincula a una
variable, por lo que suele usarse en el último brazo de coincidencia. El _patrón
puede ser útil, por ejemplo, cuando se desea ignorar cualquier valor no
especificado. Abordaremos el _patrón con más detalle en la sección "Ignorar
valores en un patrón" más adelante en este capítulo.
if letExpresiones condicionales
El Listado 19-1 muestra que también es posible combinar expresiones if let, else if,
y else if let. Esto nos brinda mayor flexibilidad que una matchexpresión en la que
solo podemos expresar un valor para comparar con los patrones. Además, Rust no
requiere que las condiciones en una serie de brazos if let, else if, else if letestén
relacionadas entre sí.
El código del Listado 19-1 determina el color del fondo según una serie de
comprobaciones de varias condiciones. Para este ejemplo, hemos creado
variables con valores predefinidos que un programa real podría recibir de la
entrada del usuario.
Rust
jueves, 22 de mayo de 2025 : Página 573 de 719
Nombre de archivo: src/main.rs
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
Esta estructura condicional nos permite cumplir con requisitos complejos. Con los
valores codificados, este ejemplo imprimirá Using purple as the background color.
Como puede ver, if lettambién se pueden introducir nuevas variables que eclipsan
las existentes, de la misma forma que matcharms: la línea if let Ok(age) =
age introduce una nueva agevariable que contiene el valor dentro de la Okvariante,
eclipsando la agevariable existente. Esto significa que debemos colocar la if age >
30condición dentro de ese bloque: no podemos combinar estas dos condiciones
en [ <sub> if let Ok(age) = age && age > 30...age
forBucles
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
Running `target/debug/patterns`
a is at index 0
Rust
jueves, 22 de mayo de 2025 : Página 575 de 719
b is at index 1
c is at index 2
letDeclaraciones
Antes de este capítulo, solo habíamos hablado explícitamente del uso de patrones
con matchy if let, pero, de hecho, también los hemos usado en otros contextos,
incluso en letsentencias. Por ejemplo, considere esta sencilla asignación de
variable con let:
let x = 5;
Cada vez que has usado una letdeclaración como esta, has estado usando
patrones, ¡aunque quizá no te hayas dado cuenta! De forma más formal,
una letdeclaración se ve así:
Aquí, comparamos una tupla con un patrón. Rust compara el valor (1, 2, 3) con el
patrón (x, y, z)y ve que coincide con él, por lo que Rust se
vincula 1a x, 2paray y 3a z. Puedes imaginar este patrón de tupla como si anidara
tres patrones de variables individuales dentro de él.
Rust
jueves, 22 de mayo de 2025 : Página 576 de 719
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
--> src/main.rs:2:9
|
2| let (x, y) = (1, 2, 3);
| ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
| |
| expected a tuple with 3 elements, found one with 2 elements
|
= note: expected tuple `({integer}, {integer}, {integer})`
found tuple `(_, _)`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Para corregir el error, podríamos ignorar uno o más de los valores en la tupla
usando _o .., como verá en la sección “Ignorar valores en un patrón”. . Si el
problema radica en que tenemos demasiadas variables en el patrón, la solución
es hacer que los tipos coincidan eliminando variables para que el número de
variables sea igual al número de elementos de la tupla.
Parámetros de función
Los parámetros de función también pueden ser patrones. El código del Listado 19-
6, que declara una función llamada fooque toma un parámetro llamado xde
tipo i32, debería resultar familiar.
fn foo(x: i32) {
// code goes here
}
Listado 19-6: Una firma de función utiliza patrones en los parámetros
Rust
jueves, 22 de mayo de 2025 : Página 577 de 719
¡La xparte es un patrón! Como hicimos con let, podríamos hacer coincidir una
tupla en los argumentos de una función con el patrón. El listado 19-7 divide los
valores de una tupla al pasarla a una función.
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
Listado 19-7: Una función con parámetros que desestructura una tupla
Este código imprime Current location: (3, 5). Los valores &(3, 5)coinciden con el
patrón &(x, y), por lo que xes el valor 3y yes el valor 5.
Hasta ahora, has visto varias maneras de usar patrones, pero no funcionan igual
en todos los casos. En algunos casos, los patrones deben ser irrefutables; en otras
circunstancias, pueden serlo. A continuación, analizaremos estos dos conceptos.
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
--> src/main.rs:3:9
|
3| let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only
one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
|
3| let Some(x) = some_option_value else { todo!() };
| ++++++++++++++++
For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 579 de 719
Como no cubrimos (¡y no pudimos cubrir!) todos los valores válidos con el
patrón Some(x), Rust produce legítimamente un error de compilación.
¡Le hemos dado una salida al código! Este código ahora es perfectamente válido.
Sin embargo, si damos if letun patrón irrefutable (un patrón que siempre
coincidirá), como xse muestra en el Listado 19-10, el compilador emitirá una
advertencia.
if let x = 5 {
println!("{x}");
};
Listado 19-10: Intentar utilizar un patrón irrefutable conif let
Rust se queja de que no tiene sentido utilizar if let con un patrón irrefutable:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `if let` pattern
--> src/main.rs:2:8
|
2| if let x = 5 {
| ^^^^^^^^^
|
= note: this pattern will always match, so the `if let` is useless
= help: consider replacing the `if let` with a `let`
= note: `#[warn(irrefutable_let_patterns)]` on by default
Por esta razón, los brazos de coincidencia deben usar patrones refutables,
excepto el último brazo, que debe coincidir con los valores restantes con un
patrón irrefutable. Rust permite usar un patrón irrefutable en una
Rust
jueves, 22 de mayo de 2025 : Página 580 de 719
Ahora que sabes dónde usar patrones y la diferencia entre patrones refutables e
irrefutables, cubramos toda la sintaxis que podemos usar para crear patrones.
Sintaxis de patrones
En esta sección, reunimos toda la sintaxis válida en patrones y analizamos por
qué y cuándo es posible que desees utilizar cada uno.
Literales coincidentes
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
Este código se imprime oneporque el valor enx es 1. Esta sintaxis es útil cuando
desea que su código realice una acción si obtiene un valor concreto particular.
Las variables con nombre son patrones irrefutables que coinciden con cualquier
valor, y las hemos usado muchas veces en este libro. Sin embargo, existe una
complicación al usar variables con nombre en expresiones match, if leto while let.
Dado que cada uno de estos tipos de expresión inicia un nuevo ámbito, las
variables declaradas como parte de un patrón dentro de la expresión sombrearán
a las que tengan el mismo nombre en el exterior, como ocurre con todas las
variables. En el Listado 19-11, declaramos una variable con nombre xy Some(5)una
variable ycon el valor 10. Luego, creamos una matchexpresión con el valor x.
Observe los patrones en los brazos de coincidencia y println!al final, e intente
averiguar qué imprimirá el código antes de ejecutarlo o seguir leyendo.
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {x:?}"),
}
Para crear una matchexpresión que compare los valores de los valores
externos xy y, en lugar de introducir una nueva variable que reemplace la
existente y , necesitaríamos usar una condición de protección de coincidencia.
Hablaremos de las protecciones de coincidencia más adelante, en la
sección "Condicionales adicionales con protecciones de coincidencia" .
Patrones múltiples
Rust
jueves, 22 de mayo de 2025 : Página 582 de 719
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
let x = 'c';
match x {
Rust
jueves, 22 de mayo de 2025 : Página 583 de 719
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
El óxido puede decir que 'c'está dentro del rango del primer patrón y se
imprime early ASCII letter.
Desestructuración de estructuras
El listado 19-12 muestra una Pointestructura con dos campos, xy y, que podemos
dividir usando un patrón con una letdeclaración.
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
Listado 19-12: Desestructuración de los campos de una estructura en variables separadas
Este código crea las variables ay bque coinciden con los valores de los campos x y
de la estructura. Este ejemplo muestra que los nombres de las variables en el
patrón no tienen que coincidir con los nombres de los campos de la estructura.
Sin embargo, es común hacer coincidir los nombres de las variables con los
nombres de los campos para facilitar la memorización de las variables. Debido a
este uso común y a que la escritura conlleva mucha duplicación, Rust tiene una
abreviatura para los patrones que coinciden con los campos de la estructura: solo
se necesita indicar el nombre del campo de la estructura, y las variables creadas
a partir del patrón tendrán los mismos nombres. El Listado 19-13 se comporta de
la misma manera que el código del Listado 19-12, pero las variables creadas en
el patrón son y en lugar de y .yplet Point { x: x, y: y } = p;letxyab
Rust
jueves, 22 de mayo de 2025 : Página 584 de 719
Nombre de archivo: src/main.rs
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
Listado 19-13: Desestructuración de campos de estructura utilizando la abreviatura de campo de
estructura
Este código crea las variables xy yque coinciden con los campos xy de la variable.
El resultado es que las variables y contienen los valores de la estructura.ypxyp
También podemos desestructurar con valores literales como parte del patrón de
estructura, en lugar de crear variables para todos los campos. Esto nos permite
probar algunos campos para valores específicos mientras creamos variables para
desestructurar los demás.
match p {
Point { x, y: 0 } => println!("On the x axis at {x}"),
Point { x: 0, y } => println!("On the y axis at {y}"),
Point { x, y } => {
println!("On neither axis: ({x}, {y})");
}
}
}
Listado 19-14: Desestructuración y coincidencia de valores literales en un patrón
De forma similar, el segundo brazo coincide con cualquier punto del yeje
especificando que el xcampo coincide si su valor es [ ] 0y crea una variable ypara
dicho valor y. El tercer brazo no especifica ningún literal, por lo que coincide con
cualquier otro Pointy crea variables para los campos [ ] xy [ y].
Recuerde que una matchexpresión deja de verificar los brazos una vez que ha
encontrado el primer patrón coincidente, por lo que, aunque Point { x: 0, y: 0}esté
en el xeje y el yeje, este código solo se imprimiráOn the x axis at 0 .
Desestructuración de enumeraciones
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.");
}
Message::Move { x, y } => {
println!("Move in the x direction {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change the color to red {r}, green {g}, and blue {b}");
}
}
}
Rust
jueves, 22 de mayo de 2025 : Página 586 de 719
Listado 19-15: Desestructuración de variantes de enumeración que contienen diferentes tipos de valores
Este código se imprimirá Change the color to red 0, green 160, and blue 255 . Intenta
cambiar el valor de msgpara ver el código de la ejecución de otros brazos.
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
Rust
jueves, 22 de mayo de 2025 : Página 587 de 719
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}");
}
_ => (),
}
}
Listado 19-16: Coincidencia en enumeraciones anidadas
Este código nos permite dividir tipos complejos en sus partes componentes para
que podamos usar los valores que nos interesan por separado.
Has visto que a veces es útil ignorar valores en un patrón, como en el último
brazo de un match, para obtener un catchall que no hace nada, pero que sí tiene
en cuenta todos los valores posibles restantes. Hay varias maneras de ignorar
valores completos o partes de valores en un patrón: usando el _ patrón (como ya
has visto), usando el _patrón dentro de otro patrón, usando un nombre que
Rust
jueves, 22 de mayo de 2025 : Página 588 de 719
empiece por un guion bajo o usando.. para ignorar las partes restantes de un
valor. Exploremos cómo y por qué usar cada uno de estos patrones.
Hemos usado el guión bajo como patrón comodín que coincide con cualquier
valor, pero no se vincula a él. Esto es especialmente útil como último brazo de
una match expresión, pero también podemos usarlo en cualquier patrón,
incluyendo parámetros de función, como se muestra en el Listado 19-17.
fn main() {
foo(3, 4);
}
Listado 19-17: Uso _en una firma de función
Este código ignorará por completo el valor 3pasado como primer argumento e
imprimirá This code only uses the y parameter: 4.
También podemos usar _dentro de otro patrón para ignorar solo una parte de un
valor, por ejemplo, cuando queremos probar solo una parte de un valor, pero no
necesitamos las demás partes en el código correspondiente que queremos
ejecutar. El Listado 19-18 muestra el código responsable de gestionar el valor de
una configuración. Los requisitos de negocio establecen que el usuario no debe
poder sobrescribir una personalización existente de una configuración, pero
puede deshabilitarla y asignarle un valor si no está configurada.
println!("setting is {setting_value:?}");
Listado 19-18: Uso de un guión bajo dentro de patrones que coinciden Somecon variantes cuando no
necesitamos usar el valor dentro delSome
Este código imprimirá Can't overwrite an existing customized valuey luego setting is
Some(5). En el primer brazo de coincidencia, no necesitamos hacer coincidir ni
usar los valores dentro de ninguna de Somelas variantes, pero sí debemos
comprobar si setting_valuey new_setting_valueson la Somevariante. En ese caso,
imprimimos el motivo por el cual no se cambia setting_value, y no se modifica.
También podemos usar guiones bajos en varios lugares dentro de un patrón para
ignorar valores específicos. El Listado 19-19 muestra un ejemplo de cómo ignorar
el segundo y el cuarto valor en una tupla de cinco elementos.
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}")
}
}
Listado 19-19: Ignorar varias partes de una tupla
Este código imprimirá Some numbers: 2, 8, 32y los valores 4 y 16 serán ignorados.
Si creas una variable pero no la usas, Rust suele emitir una advertencia, ya que
una variable sin usar podría ser un error. Sin embargo, a veces es útil crear una
variable que aún no usarás, como al crear prototipos o al iniciar un proyecto. En
este caso, puedes indicar a Rust que no te advierta sobre la variable sin usar
Rust
jueves, 22 de mayo de 2025 : Página 590 de 719
Tenga en cuenta que existe una sutil diferencia entre usar solo _y usar un nombre
que empieza con un guion bajo. La sintaxis _xsigue vinculando el valor a la
variable, mientras que _no lo hace en absoluto. Para ilustrar un caso donde esta
distinción es importante, el Listado 19-21 nos mostrará un error.
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
Listado 19-21: Una variable no utilizada que comienza con un guión bajo aún vincula el valor, que
podría tomar posesión del valor.
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
Listado 19-22: El uso de un guión bajo no vincula el valor
Este código funciona bien porque nunca nos vinculamos sa nada; no se mueve.
Rust
jueves, 22 de mayo de 2025 : Página 591 de 719
Ignorar las partes restantes de un valor con..
Con valores con muchas partes, podemos usar la ..sintaxis para usar partes
específicas e ignorar el resto, evitando así la necesidad de incluir guiones bajos
para cada valor ignorado. El ..patrón ignora cualquier parte de un valor que no
hayamos encontrado explícitamente en el resto del patrón. En el Listado 19-23,
tenemos una Pointestructura que contiene una coordenada en un espacio
tridimensional. En la matchexpresión, queremos operar solo sobre la xcoordenada
e ignorar los valores de los campos ` y`y`` z.
struct Point {
x: i32,
y: i32,
z: i32,
}
match origin {
Point { x, .. } => println!("x is {x}"),
}
Listado 19-23: Ignorar todos los campos de a Pointexcepto xmediante el uso..
Enumeramos el xvalor y luego incluimos el ..patrón. Esto es más rápido que tener
que enumerar y: _y z: _, sobre todo cuando trabajamos con estructuras con
muchos campos en situaciones donde solo uno o dos son relevantes.
La sintaxis ..se expandirá a tantos valores como sea necesario. El listado 19-24
muestra cómo usarla ..con una tupla.
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
Listado 19-24: Coincidencia solo del primer y último valor de una tupla e ignoración de todos los
demás valores
En este código, el primer y el último valor se comparan con first"y" last. ..Coincidirá
e ignorará todo lo intermedio.
Rust
jueves, 22 de mayo de 2025 : Página 592 de 719
Sin embargo, el uso ..debe ser inequívoco. Si no está claro qué valores deben
coincidir y cuáles deben ignorarse, Rust generará un error. El Listado 19-25
muestra un ejemplo de uso ..ambiguo, por lo que no compilará.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
Listado 19-25: Un intento de uso ..de forma ambigua
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5| (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
complejas que las que permite un patrón por sí solo. Solo están disponibles
en matchexpresiones, no en expresiones if leto .while let
match num {
Some(x) if x % 2 == 0 => println!("The number {x} is even"),
Some(x) => println!("The number {x} is odd"),
None => (),
}
Listado 19-26: Cómo añadir un protector de fósforo a un patrón
Este ejemplo imprimirá The number 4 is even. Al numcompararlo con el patrón del
primer brazo, coincide, ya que Some(4)coincide con Some(x). Luego, el protector de
coincidencias comprueba si el resto de la división xentre 2 es igual a 0 y, como lo
es, se selecciona el primer brazo.
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {n}"),
_ => println!("Default case, x = {x:?}"),
}
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
Listado 19-28: Combinación de múltiples patrones con un protector de fósforo
y este programa imprime no. Esto se debe a que la if condición se aplica a todo el
patrón 4 | 5 | 6, no solo al último valor 6. En otras palabras, la precedencia de una
protección de coincidencia con respecto a un patrón se comporta así:
(4 | 5 | 6) if y => ...
En lugar de esto:
4 | 5 | (6 if y) => ...
@Encuadernaciones
El operador `at`@ nos permite crear una variable que contiene un valor al mismo
tiempo que probamos dicho valor para una coincidencia de patrón. En el Listado
19-29, queremos comprobar que un Message::Hello idcampo esté dentro del
rango 3..=7. También queremos vincular el valor a la variable id_variablepara poder
usarlo en el código asociado con el brazo. Podríamos nombrar esta variable idigual
que el campo, pero para este ejemplo usaremos un nombre diferente.
enum Message {
Hello { id: i32 },
}
match msg {
Message::Hello {
id: id_variable @ 3..=7,
} => println!("Found an id in range: {id_variable}"),
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
Listado 19-29: Uso @para enlazar a un valor en un patrón mientras también se prueba
En el último brazo, donde especificamos una variable sin rango, el valor está
disponible para su uso en el código del brazo en una variable llamada id. Esto se
debe a que usamos la sintaxis abreviada de campo de estructura. Sin embargo,
no aplicamos ninguna prueba al valor del idcampo en este brazo, como hicimos
con los dos primeros: cualquier valor coincidiría con este patrón.
Resumen
Los patrones de Rust son muy útiles para distinguir entre diferentes tipos de
datos. Al usarlos en matchexpresiones, Rust garantiza que los patrones cubran
todos los valores posibles; de lo contrario, el programa no compilará. Los patrones
en letsentencias y parámetros de funciones hacen que estas construcciones sean
más útiles, permitiendo la desestructuración de valores en partes más pequeñas
al mismo tiempo que se asignan a variables. Podemos crear patrones simples o
complejos según nuestras necesidades.
Funciones avanzadas
Ya has aprendido las partes más comunes del lenguaje de programación Rust.
Antes de continuar con otro proyecto en el Capítulo 21, analizaremos algunos
aspectos del lenguaje que podrías encontrarte de vez en cuando, pero que quizás
no uses a diario. Puedes usar este capítulo como referencia cuando te encuentres
con alguna incógnita. Las funciones que se tratan aquí son útiles en situaciones
muy específicas. Aunque quizás no las uses a menudo, queremos asegurarnos de
que domines todas las funciones que Rust ofrece.
¡Es una gran variedad de funciones de Rust para todos los gustos! ¡Vamos a
profundizar!
Óxido peligroso
Todo el código que hemos analizado hasta ahora tenía las garantías de seguridad
de memoria de Rust implementadas en tiempo de compilación. Sin embargo, Rust
tiene un segundo lenguaje oculto que no las implementa: se llama Rust no
seguro y funciona igual que el Rust normal, pero con superpoderes adicionales.
Otra razón por la que Rust tiene un alter ego inseguro es que el hardware
subyacente es inherentemente inseguro. Si Rust no permitiera realizar
operaciones inseguras, no se podrían realizar ciertas tareas. Rust necesita
permitir la programación de sistemas de bajo nivel, como interactuar
directamente con el sistema operativo o incluso escribir el propio. Trabajar con
programación de sistemas de bajo nivel es uno de los objetivos del lenguaje.
Exploremos qué podemos hacer con Rust inseguro y cómo hacerlo.
Rust
jueves, 22 de mayo de 2025 : Página 598 de 719
Superpotencias inseguras
Para cambiar a Rust inseguro, usa la unsafepalabra clave y luego crea un nuevo
bloque que contenga el código inseguro. Puedes realizar cinco acciones en Rust
inseguro que no puedes realizar en Rust seguro, llamadas superpoderes
inseguros . Estos superpoderes incluyen la capacidad de:
Además, unsafeno significa que el código dentro del bloque sea necesariamente
peligroso o que definitivamente tendrá problemas de seguridad de memoria: la
intención es que, como programador, te asegures de que el código dentro de un
bloque sea seguro.unsafe bloque accederá a la memoria de manera válida.
Las personas son falibles y se cometen errores, pero al exigir que estas cinco
operaciones inseguras se realicen dentro de bloques anotados, unsafesabrá que
cualquier error relacionado con la seguridad de la memoria debe estar dentro de
un unsafebloque. Mantenga unsafebloques pequeños; lo agradecerá más adelante
cuando investigue errores de memoria.
Al optar por no permitir que Rust aplique estas garantías, puede renunciar a la
seguridad garantizada a cambio de un mayor rendimiento o la capacidad de
interactuar con otro lenguaje o hardware donde las garantías de Rust no se
aplican.
El listado 20-1 muestra cómo crear un puntero sin formato inmutable y uno
mutable.
Hemos creado punteros sin procesar mediante los operadores de préstamo sin
procesar: &raw const num crea un *const i32puntero sin procesar inmutable y &raw
mut numcrea un *mut i32puntero sin procesar mutable. Dado que los creamos
directamente desde una variable local, sabemos que estos punteros sin procesar
son válidos, pero no podemos asumirlo con cualquier puntero sin procesar.
Para demostrar esto, crearemos un puntero sin formato cuya validez no es tan
segura, utilizando asla conversión de un valor en lugar de los operadores de
referencia sin formato. El Listado 20-2 muestra cómo crear un puntero sin formato
a una ubicación arbitraria en memoria. Intentar usar memoria arbitraria no está
definido: podría haber datos en esa dirección o no, el compilador podría optimizar
el código para que no haya acceso a memoria, o el programa podría generar un
error de segmentación. Normalmente, no hay una buena razón para escribir
código como este, especialmente cuando se puede usar un operador de préstamo
sin formato, pero es posible.
Recuerde que podemos crear punteros sin procesar en código seguro, pero no
podemos desreferenciarlos ni leer los datos a los que apuntan. En el Listado 20-3,
usamos el operador de desreferencia *en un puntero sin procesar que requiere
un unsafebloque.
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
Listado 20-3: Desreferenciación de punteros sin procesar dentro de un unsafebloque
Crear un puntero no hace daño; solo cuando intentamos acceder al valor al que
apunta es cuando podemos terminar tratando con un valor no válido.
Con todos estos peligros, ¿por qué usar punteros sin formato? Un caso de uso
importante es la interacción con código C, como verá en la siguiente
sección, "Llamar a una función o método inseguro". Otro caso es la creación de
abstracciones seguras que el verificador de préstamos no comprende.
Presentaremos las funciones inseguras y luego veremos un ejemplo de una
abstracción segura que utiliza código inseguro.
Aquí hay una función insegura llamada dangerousque no hace nada en su cuerpo:
unsafe fn dangerous() {}
unsafe {
dangerous();
}
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe function or
block
--> src/main.rs:4:5
|
4| dangerous();
| ^^^^^^^^^^^ call to unsafe function
|
Rust
jueves, 22 de mayo de 2025 : Página 602 de 719
= note: consult the function's documentation for information on how to avoid undefined
behavior
For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
El hecho de que una función contenga código inseguro no significa que debamos
marcarla como tal. De hecho, encapsular código inseguro en una función segura
es una abstracción común. A modo de ejemplo, estudiemos la split_at_mutfunción
de la biblioteca estándar, que requiere código inseguro. Exploraremos cómo
implementarla. Este método seguro se define en segmentos mutables: toma un
segmento y lo convierte en dos dividiéndolo en el índice dado como argumento. El
listado 20-4 muestra cómo usar split_at_mut.
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
Rust
jueves, 22 de mayo de 2025 : Página 603 de 719
Esta función primero obtiene la longitud total del segmento. Luego, verifica si el
índice dado como parámetro está dentro del segmento, verificando si es menor o
igual a la longitud. Esto significa que si pasamos un índice mayor que la longitud
para dividir el segmento, la función entrará en pánico antes de intentar usarlo.
Luego devolvemos dos porciones mutables en una tupla: una desde el comienzo
de la porción original hasta el midíndice y otra desde midel final de la porción.
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:6:31
|
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6| (&mut values[..mid], &mut values[mid..])
| --------------------------^^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*values` is borrowed for `'1`
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
use std::slice;
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr();
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
Listado 20-6: Uso de código inseguro en la implementación de la split_at_mutfunción
Recuerde de la sección "El tipo de rebanada" del capítulo 4 que las rebanadas son
un puntero a datos y su longitud. Usamos el lenmétodo para obtener la longitud
de una rebanada y el as_mut_ptr método para acceder a su puntero sin formato. En
este caso, dado que tenemos una rebanada mutable
a i32valores, as_mut_ptrdevuelve un puntero sin formato con el tipo *mut i32, que
hemos almacenado en la variable ptr.
formato utilizados dentro del unsafebloque serán punteros válidos a los datos
dentro de la porción. Este es un uso aceptable y apropiado de [nombre del
método].unsafe .
use std::slice;
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
Listado 20-8: Declaración y llamada de una externfunción definida en otro lenguaje
Dentro del unsafe extern "C"bloque, listamos los nombres y las firmas de las
funciones externas de otro lenguaje que queremos llamar. Esta "C"parte define
la interfaz binaria de aplicación (ABI) que utiliza la función externa: la ABI define
cómo llamar a la función a nivel de ensamblador. La "C"ABI es la más común y
sigue la ABI del lenguaje de programación C.
fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
Listado 20-9: Marcar explícitamente una función como safedentro de un unsafe externbloque y llamarla
de forma segura
También podemos usar esta externfunción para crear una interfaz que permita a otros lenguajes llamar
a funciones de Rust. En lugar de crear un bloque completo ,extern añadimos la externpalabra clave y
especificamos la ABI que se usará justo antes de la fnpalabra clave para la función correspondiente.
Rust
jueves, 22 de mayo de 2025 : Página 607 de 719
En el siguiente ejemplo, hacemos que la call_from_cfunción sea accesible desde el código C, después
de compilarla en una biblioteca compartida y vincularla desde C:
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
En este libro, aún no hemos hablado de las variables globales , que Rust admite,
pero que pueden ser problemáticas con sus reglas de propiedad. Si dos hilos
acceden a la misma variable global mutable, puede causar una carrera de datos.
fn main() {
println!("name is: {HELLO_WORLD}");
}
Listado 20-10: Definición y uso de una variable estática inmutable
Una sutil diferencia entre las constantes y las variables estáticas inmutables es
que los valores de una variable estática tienen una dirección fija en memoria.
Usar el valor siempre accederá a los mismos datos. Las constantes, en cambio,
pueden duplicar sus datos al usarse. Otra diferencia es que las variables estáticas
pueden ser mutables. Acceder y modificar variables estáticas mutables no
es seguro . El listado 20-11 muestra cómo declarar, acceder y modificar una
variable estática mutable llamada COUNTER.
/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
unsafe {
// SAFETY: This is only called from a single thread in `main`.
add_to_count(3);
println!("COUNTER: {}", *(&raw const COUNTER));
}
}
Listado 20-11: Leer o escribir en una variable estática mutable no es seguro
fn main() {}
Listado 20-12: Definición e implementación de un rasgo inseguro
La última acción que solo funciona con unsafees acceder a los campos de
una unión . A uniones similar a a struct, pero solo se usa un campo declarado en
cada instancia a la vez. Las uniones se usan principalmente para interactuar con
uniones en código C. Acceder a los campos de unión no es seguro porque Rust no
puede garantizar el tipo de los datos almacenados en la instancia de unión.
Puedes obtener más información sobre las uniones en la Referencia de Rust .
Usar Miri requiere una compilación nocturna de Rust (de la que hablaremos más
en el Apéndice G: Cómo se crea Rust y "Nightly Rust" ). Puedes instalar tanto la
versión nocturna de Rust como la herramienta Miri escribiendo rustup +nightly
component add miri. Esto no cambia la versión de Rust que usa tu proyecto; solo
añade la herramienta a tu sistema para que puedas usarla cuando quieras.
Puedes ejecutar Miri en un proyecto escribiendo cargo +nightly miri runo cargo
+nightly miri test.
Para ver un ejemplo de lo útil que puede ser esto, considere lo que sucede cuando
lo ejecutamos contra el Listado 20-11:
COUNTER: 3
Miri no detecta todos los errores que podrías cometer al escribir código inseguro.
Por un lado, al ser una comprobación dinámica, solo detecta problemas en el
código que realmente se ejecuta. Esto significa que deberás usarla junto con
buenas técnicas de prueba para aumentar tu confianza en el código inseguro que
has escrito. Por otro lado, no cubre todas las posibles maneras en que tu código
puede ser incorrecto. Si Miri detecta un problema, sabes que hay un error, pero
que Miri no detecte un error no significa que no haya un problema. Sin embargo,
Miri puede detectar muchos. ¡Intenta ejecutarla en los otros ejemplos de código
inseguro de este capítulo y comprueba lo que dice!
Usar unsafepara realizar una de las cinco acciones (superpoderes) que acabamos
de mencionar no es incorrecto ni está mal visto. Sin embargo, es más complicado
que unsafeel código sea correcto porque el compilador no puede ayudar a
mantener la seguridad de la memoria. Cuando se tiene una razón para
usar unsafecódigo, se puede hacer, y tener la unsafe anotación explícita facilita la
localización del origen de los problemas cuando ocurren. Siempre que se escribe
Rust
jueves, 22 de mayo de 2025 : Página 612 de 719
código inseguro, se puede usar Miri para tener mayor confianza en que el código
escrito cumple con las reglas de Rust.
Para una exploración mucho más profunda de cómo trabajar eficazmente con
Rust inseguro, lea la guía oficial de Rust sobre el tema, el Rustonomicon .
Rasgos avanzados
Primero abordamos los rasgos en la sección "Rasgos: Definición del
comportamiento compartido" del Capítulo 10, pero no abordamos los detalles más
avanzados. Ahora que conoces más sobre Rust, podemos profundizar en los
detalles.
Los tipos asociados también forman parte del contrato del rasgo: los
implementadores del rasgo deben proporcionar un tipo que sustituya al marcador
de tipo asociado. Los tipos asociados suelen tener un nombre que describe cómo
se usarán, y documentarlos en la documentación de la API es una buena práctica.
trait Add<Rhs=Self> {
type Output;
Este código debería resultar familiar: un trait con un método y un tipo asociado.
La novedad es que Rhs=Selfesta sintaxis se denomina "parámetros de tipo
predeterminados ". El Rhsparámetro de tipo genérico (abreviatura de "lado
derecho") define el tipo del rhsparámetro en el addmétodo. Si no especificamos un
tipo concreto Rhsal implementar el Addtrait, el tipo Rhspredeterminado será " Self,
que será el tipo en el que lo implementamos" Add.
struct Millimeters(u32);
struct Meters(u32);
En Rust, nada impide que un rasgo tenga un método con el mismo nombre que el
de otro rasgo, ni impide implementar ambos rasgos en un mismo tipo. También es
posible implementar un método directamente en el tipo con el mismo nombre que
los métodos de los rasgos.
Rust
jueves, 22 de mayo de 2025 : Página 617 de 719
Al llamar a métodos con el mismo nombre, deberá indicarle a Rust cuál desea
usar. Considere el código del Listado 20-17, donde definimos dos
rasgos, Piloty Wizard, y ambos tienen un método llamado fly. Luego,
implementamos ambos rasgos en un tipo Humanque ya tiene implementado un
método llamado fly. Cada flymétodo realiza una función diferente.
trait Wizard {
fn fly(&self);
}
struct Human;
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
Listado 20-17: Se definen dos rasgos que hacen que un method and are implemented on thehumano type,
and avuele method is implemented ondirectamente.
Cuando llamamos flya una instancia de Human, el compilador por defecto llama al
método que está implementado directamente en el tipo, como se muestra en el
Listado 20-18.
Al ejecutar este código se imprimirá *waving arms furiously*, mostrando que Rust
llamó al flymétodo implementado Humandirectamente.
Especificar el nombre del rasgo antes del nombre del método le indica a Rust qué
implementación de flyqueremos llamar. También podríamos
escribir Human::fly(&person)```, equivalente al person.fly()que usamos en el Listado
20-19, pero es un poco más largo si no necesitamos desambiguar``.
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
Listado 20-20: Un rasgo con una función asociada y un tipo con una función asociada del mismo
nombre que también implementa el rasgo
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
Listado 20-21: Intentando llamar a la baby_namefunción desde elAnimal rasgo, pero Rust no sabe qué
implementación usar
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding
`impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function
of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
Para las funciones asociadas que no sean métodos, no habría una receiver :`, solo
la lista de otros argumentos. Se podría usar la sintaxis completa siempre que se
llamen funciones o métodos. Sin embargo, se permite omitir cualquier parte de
esta sintaxis que Rust pueda deducir de otra información del programa. Solo se
necesita usar esta sintaxis más detallada cuando haya varias implementaciones
con el mismo nombre y Rust necesite ayuda para identificar a qué
implementación se desea llamar.
A veces, se puede escribir una definición de rasgo que depende de otro rasgo:
para que un tipo implemente el primer rasgo, se requiere que ese tipo también
implemente el segundo. Esto se hace para que la definición de rasgo pueda usar
los elementos asociados del segundo rasgo. El rasgo en el que se basa la
definición de rasgo se denomina superrasgo .
**********
* *
* (1, 3) *
* *
**********
Rust
jueves, 22 de mayo de 2025 : Página 622 de 719
struct Point {
x: i32,
y: i32,
}
Recibimos un error que dice que Displayes obligatorio pero no está implementado:
Rust
jueves, 22 de mayo de 2025 : Página 623 de 719
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
Por ejemplo, supongamos que queremos implementar Displayon Vec<T>, algo que
la regla de objetos huérfanos nos impide hacer directamente porque
el Displayrasgo y el Vec<T>tipo están definidos fuera de nuestro crate. Podemos
crear una Wrapperestructura que contenga una instancia de Vec<T>; luego,
podemos implementar Displayon Wrappery usar el Vec<T>valor, como se muestra
en el Listado 20-24.
struct Wrapper(Vec<String>);
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {w}");
}
Listado 20-24: Creación de un Wrappertipo Vec<String>para implementarDisplay
Rust
jueves, 22 de mayo de 2025 : Página 625 de 719
La desventaja de usar esta técnica es que, Wrapperal ser un tipo nuevo, no tiene
los métodos del valor que contiene. Tendríamos que implementar todos los
métodos de Vec<T>directamente en [ Wrappernombre del tipo] para que deleguen
a [nombre del tipo] self.0, lo que nos permitiría tratarlo Wrapperexactamente como
un [nombre del tipo Vec<T>]. Si quisiéramos que el nuevo tipo tuviera todos los
métodos del tipo interno, implementar el Derefatributo [nombre del tipo]
(discutido en el Capítulo 15, en la sección "Tratar punteros inteligentes como
referencias regulares con el Deref atributo" ) en [ nombre del tipo Wrapper] para
devolver el tipo interno sería una solución. Si no queremos que el Wrappertipo
tenga todos los métodos del tipo interno (por ejemplo, para restringir
su Wrappercomportamiento), tendríamos que implementar manualmente solo los
métodos que necesitamos.
Este nuevo patrón de tipo también es útil incluso cuando no se utilizan rasgos.
Cambiemos de enfoque y veamos algunas formas avanzadas de interactuar con el
sistema de tipos de Rust.
Tipos avanzados
El sistema de tipos de Rust tiene algunas características que ya hemos
mencionado, pero que aún no hemos analizado. Comenzaremos analizando los
nuevos tipos en general, y analizaremos su utilidad como tipos. Después,
abordaremos los alias de tipo, una característica similar a la de los nuevos tipos,
pero con una semántica ligeramente diferente. También analizaremos el !tipo y
los tipos de tamaño dinámico.
Nota: Esta sección asume que ha leído la sección anterior “Uso del patrón Newtype para implementar
rasgos externos en tipos externos”.
El patrón newtype también es útil para tareas distintas a las que hemos descrito
hasta ahora, como asegurar estáticamente que los valores nunca se confundan e
indicar las unidades de un valor. Vimos un ejemplo del uso de newtypes para
indicar unidades en el Listado 20-16: recordemos que las
Rust
jueves, 22 de mayo de 2025 : Página 626 de 719
Los nuevos tipos también pueden ocultar la implementación interna. Por ejemplo,
podríamos proporcionar un Peopletipo para encapsular un HashMap<i32, String>que
almacene el ID de una persona asociado a su nombre. Código que usa People solo
interactuaría con la API pública que proporcionamos, como un método para
agregar una cadena de nombre a la People colección; ese código no necesitaría
saber que asignamos un i32ID a los nombres internamente. El patrón newtype es
una forma ligera de lograr la encapsulación para ocultar los detalles de
implementación, lo cual explicamos en la sección "Encapsulación que oculta los
detalles de implementación" del Capítulo 18.
Rust permite declarar un alias de tipo para darle otro nombre a un tipo existente.
Para ello, usamos la typepalabra clave. Por ejemplo, podemos crear el
alias Kilometersde i32la siguiente manera:
let x: i32 = 5;
let y: Kilometers = 5;
Porque Kilometersyi32 son del mismo tipo, podemos sumar valores de ambos tipos
y pasar Kilometersvalores a funciones que aceptan i32 parámetros. Sin embargo,
con este método, no obtenemos las ventajas de la verificación de tipos que ofrece
Rust
jueves, 22 de mayo de 2025 : Página 627 de 719
Escribir este tipo extenso en las firmas de funciones y como anotaciones de tipo
en todo el código puede ser tedioso y propenso a errores. Imagine tener un
proyecto lleno de código como el del Listado 20-25.
fn takes_long_type(f: Thunk) {
// --snip--
}
¡Este código es mucho más fácil de leer y escribir! Elegir un nombre significativo
para un alias de tipo también puede ayudar a comunicar tu intención. thunk se
refiere al código que se evaluará posteriormente, por lo que es un nombre
apropiado para un cierre que se almacena).
Rust
jueves, 22 de mayo de 2025 : Página 628 de 719
Los alias de tipo también se usan comúnmente con el Result<T, E>tipo para
reducir la repetición. Considere el std::iomódulo de la biblioteca estándar. Las
operaciones de E/S suelen devolver un Result<T, E>para gestionar situaciones en
las que las operaciones fallan. Esta biblioteca tiene una std::io::Errorestructura que
representa todos los posibles errores de E/S. Muchas de las funciones
de std::iodevolverán Result<T, E>donde el tipo Ees std::io::Error, como estas
funciones en el Writerasgo:
use std::fmt;
use std::io::Error;
El alias de tipo ayuda de dos maneras: facilita la escritura del código y nos
proporciona una interfaz consistente en todo el código std::io. Al ser un alias, es
simplemente otro.Result<T, E> , lo que significa que podemos usar cualquier
método compatible Result<T, E>con él, así como sintaxis especial como el ?
operador.
Rust tiene un tipo especial llamado tipo vacío! en la jerga de la teoría de tipos, ya
que no tiene valores. Preferimos llamarlo tipo "nunca" porque reemplaza al tipo
de retorno cuando una función nunca retorna. Aquí hay un ejemplo:
fn bar() -> ! {
// --snip--
}
Este código se lee como "la función barnunca retorna". Las funciones que nunca
retornarán se denominan funciones divergentes . No podemos crear valores de
este tipo, ! por lo que barnunca podrían retornarse.
Pero ¿de qué sirve un tipo para el que nunca se pueden crear valores? Recuerda
el código del Listado 2-5, parte del juego de adivinanzas; hemos reproducido un
fragmento aquí en el Listado 20-27.
El tipo de guessen este código tendría que ser un entero y una cadena, y Rust
requiere que guesssolo tengan un tipo. Entonces, ¿qué continue devuelve? ¿Cómo
se nos permitió devolver "a" u32de un brazo y tener otro brazo que termina en
"" continueen el Listado 20-27?
panic!El tipo "never" también es útil con la macro. Recuerde la unwrap función que
llamamos en Option<T>valores para producir un valor o pánico con esta definición:
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
print!("forever ");
loop {
print!("and ever ");
}
Aquí, el bucle nunca termina, al igual !que el valor de la expresión. Sin embargo,
esto no sería cierto si incluyéramos unbreak , ya que el bucle terminaría al llegar
al break.
Rust necesita conocer ciertos detalles sobre sus tipos, como cuánto espacio
asignar a un valor de un tipo en particular. Esto deja un aspecto de su sistema de
tipos un poco confuso al principio: el concepto de tipos de tamaño dinámico . A
veces se les conoce como DST o tipos sin tamaño , estos tipos permiten escribir
código con valores cuyo tamaño solo se conoce en tiempo de ejecución.
Rust
jueves, 22 de mayo de 2025 : Página 631 de 719
Entonces, ¿qué hacemos? En este caso, ya sabes la respuesta: hacemos los tipos
de s1y s2a &stren lugar de a str. Recuerda de la sección “Segmentos de
cadenas” del Capítulo 4 que la estructura de datos del segmento solo almacena la
posición inicial y la longitud del segmento. Entonces, aunque a &Tes un valor
único que almacena la dirección de memoria de donde Tse encuentra the ,
a &strson dos valores: la dirección de the stry su longitud. Como tal, podemos
saber el tamaño de un &strvalor en tiempo de compilación: es el doble de la
longitud de a usize. Es decir, siempre sabemos el tamaño de a &str, sin importar
cuán larga sea la cadena a la que se refiere. En general, esta es la forma en que
se usan los tipos de tamaño dinámico en Rust: tienen un bit adicional de
metadatos que almacena el tamaño de la información dinámica. La regla de oro
de los tipos de tamaño dinámico es que siempre debemos poner valores de tipos
de tamaño dinámico detrás de un puntero de algún tipo.
fn generic<T>(t: T) {
// --snip--
}
fn generic<T: Sized>(t: T) {
// --snip--
}
De forma predeterminada, las funciones genéricas solo funcionan con tipos cuyo
tamaño se conoce en tiempo de compilación. Sin embargo, puede usar la
siguiente sintaxis especial para flexibilizar esta restricción:
Un atributo enlazado ?Sizedsignifica " Tpuede o no ser Sized" y esta notación anula
el valor predeterminado que establece que los tipos genéricos deben tener un
tamaño conocido en tiempo de compilación. La ?Traitsintaxis con este significado
solo está disponible para Sized , no para ningún otro atributo.
Tenga en cuenta también que cambiamos el tipo del tparámetro de Ta &T. Porque
el tipo podría no serSized , necesitamos usarlo detrás de algún tipo de puntero. En
este caso, hemos elegido una referencia.
Punteros de función
Rust
jueves, 22 de mayo de 2025 : Página 633 de 719
Hemos hablado sobre cómo pasar clausuras a funciones; ¡también puedes pasar
funciones regulares a funciones! Esta técnica es útil cuando quieres pasar una
función ya definida en lugar de definir una nueva clausura. Las funciones
coaccionan al tipo fn(con f minúscula), que no debe confundirse con el Fn rasgo de
clausura. El fntipo se denomina puntero a función . Pasar funciones con punteros a
función te permitirá usar funciones como argumentos de otras funciones.
fn main() {
let answer = do_twice(add_one, 5);
Los punteros de función implementan los tres rasgos de cierre ( Fn, FnMut,
y FnOnce ), lo que significa que siempre se puede pasar un puntero a función
como argumento para una función que espera un cierre. Es recomendable escribir
Rust
jueves, 22 de mayo de 2025 : Página 634 de 719
funciones con un tipo genérico y uno de los rasgos de cierre para que las
funciones puedan aceptar funciones o cierres.
Dicho esto, un ejemplo de dónde solo querrías aceptar fny no cierres es cuando
interactúas con código externo que no tiene cierres: las funciones de C pueden
aceptar funciones como argumentos, pero C no tiene cierres.
Como ejemplo de dónde se puede usar una clausura definida en línea o una
función con nombre, veamos el uso del mapmétodo proporcionado por
el Iterator rasgo en la biblioteca estándar. Para usar la mapfunción y convertir un
vector de números en un vector de cadenas, podríamos usar una clausura como
esta:
O podríamos nombrar una función como argumento mapen lugar del cierre, de
esta manera:
enum Status {
Value(u32),
Stop,
}
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
Macros
Hemos usado macros println!a lo largo de este libro, pero no hemos explorado
completamente qué es una macro y cómo funciona. El término macro se refiere a
una familia de características en Rust: macros declarativasmacro_rules! y tres tipos
de macros procedimentales :
Hablaremos de cada uno de ellos por turno, pero primero veamos por qué
necesitamos macros cuando ya tenemos funciones.
Fundamentalmente, las macros son una forma de escribir código que a su vez
escribe otro código, lo que se conoce como metaprogramación . En el Apéndice C,
analizamos el derive atributo, que genera una implementación de varios rasgos.
También hemos utilizado las macros println!y vec!a lo largo del libro. Todas estas
macros se expanden para producir más código que el que se ha escrito
manualmente.
Otra diferencia importante entre macros y funciones es que debes definir macros
o incluirlas en el alcance antes llamarlas en un archivo, a diferencia de las
funciones que puedes definir en cualquier lugar y llamar en cualquier lugar.
Nota: La definición real de la vec!macro en la biblioteca estándar incluye código para preasignar la
cantidad correcta de memoria desde el principio. Este código es una optimización que no incluimos
aquí para simplificar el ejemplo.
La coma que sigue $()indica que debe aparecer un separador de coma literal
entre cada instancia del código que coincida con el código dentro de $().
Esto *especifica que el patrón coincide con cero o más de lo que precede a *.
Cuando llamamos a esta macro con vec![1, 2, 3];, el $xpatrón coincide tres veces
con las tres expresiones 1, 2, y 3.
Ahora veamos el patrón en el cuerpo del código asociado con este brazo:
" temp_vec.push()within" $()*se genera para cada parte que coincide $() con el
patrón cero o más veces, dependiendo de cuántas veces coincida. "The" $xse
reemplaza con cada expresión coincidente. Al llamar a esta macro con "with" vec!
[1, 2, 3];, el código generado que reemplaza esta llamada a la macro será el
siguiente:
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
Hemos definido una macro que puede tomar cualquier número de argumentos de
cualquier tipo y puede generar código para crear un vector que contenga los
elementos especificados.
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Listado 20-30: Un ejemplo de definición de una macro procedimental
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
Listado 20-31: El código que un usuario de nuestro cajón podrá escribir al usar nuestra macro de
procedimiento
use hello_macro::HelloMacro;
struct Pancakes;
fn main() {
Pancakes::hello_macro();
Rust
jueves, 22 de mayo de 2025 : Página 642 de 719
}
[dependencies]
syn = "2.0"
quote = "1.0"
Para empezar a definir la macro procedural, inserte el código del Listado 20-32 en
el archivo src/lib.rs de la hello_macro_derivecaja. Tenga en cuenta que este código
no se compilará hasta que agreguemos una definición para
la impl_hello_macrofunción.
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
Hemos introducido tres nuevos crates: proc_macro, syny quote. Este proc_macrocrate
viene con Rust, así que no fue necesario añadirlo a las dependencias
de Cargo.toml . El proc_macrocrate es la API del compilador que nos permite leer y
manipular código de Rust desde nuestro código.
El syncrate analiza el código de Rust desde una cadena a una estructura de datos
con la que podemos realizar operaciones. El quotecrate convierte synlas
estructuras de datos de nuevo en código de Rust. Estos crates simplifican
Rust
jueves, 22 de mayo de 2025 : Página 644 de 719
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
Listado 20-33: La DeriveInputinstancia que obtenemos al analizar el código que tiene el atributo de la
macro en el Listado 20-31
Los campos de esta estructura indican que el código de Rust analizado es una
estructura unitaria con el identidentificador (es decir, el nombre) de Pancakes. Hay
más campos en esta estructura para describir todo tipo de código de Rust;
consulta la syn documentación paraDeriveInput obtener más información.
Ahora que tenemos el código para convertir el código Rust anotado de una
instancia TokenStream a una DeriveInputinstancia, generemos el código que
implementa el HelloMacrorasgo en el tipo anotado, como se muestra en el Listado
20-34.
Coloque el código del Listado 20-31 en src/main.rs y ejecútelo cargo run: debería
imprimir Hello, Macro! My name is Pancakes!La implementación del HelloMacrorasgo de
la macro de procedimiento se incluyó sin que la pancakescaja necesitara
implementarlo;#[derive(HelloMacro)] se agregó la implementación del rasgo.
Rust
jueves, 22 de mayo de 2025 : Página 647 de 719
#[route(GET, "/")]
fn index() {
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
Aparte de eso, las macros de tipo atributo funcionan de la misma manera que las
macros derivadas personalizadas: creas un paquete con el proc-macrotipo de
paquete e implementas una función que genera el código que quieres.
código Rust, al igual que los otros dos tipos de macros procedimentales. Un
ejemplo de una macro de tipo función es una sql!macro que podría llamarse así:
Esta macro analizaría la declaración SQL dentro de ella y verificaría que sea
sintácticamente correcta, lo que es un procesamiento mucho más complejo que
una macro_rules! puede realizar una macro. La sql!macro se definiría así:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
Resumen
¡Uf! Ahora tienes algunas funciones de Rust en tu caja de herramientas que
probablemente no uses a menudo, pero sabrás que están disponibles en
circunstancias muy particulares. Hemos presentado varios temas complejos para
que, cuando los encuentres en sugerencias de mensajes de error o en el código
de otros usuarios, puedas reconocer estos conceptos y su sintaxis. Usa este
capítulo como referencia para encontrar soluciones.
Para nuestro proyecto final, crearemos un servidor web que diga "hola" y se vea
como la Figura 21-1 en un navegador web.
Rust
jueves, 22 de mayo de 2025 : Página 649 de 719
Los dos protocolos principales que intervienen en los servidores web son el
Protocolo de Transferencia de Hipertexto (HTTP) y el Protocolo de Control de
Transmisión (TCP) . Ambos protocolos son de solicitud-respuesta , lo que significa
que un cliente inicia las solicitudes y un servidor... las escucha y proporciona una
respuesta al cliente. El contenido de dichas solicitudes y respuestas está definido
por los protocolos.
Nuestro servidor web necesita escuchar una conexión TCP, así que esa es la
primera parte en la que trabajaremos. La biblioteca estándar ofrece
un std::netmódulo que nos permite hacerlo. Creemos un nuevo proyecto de la
forma habitual:
Ahora introduzca el código del Listado 21-1 en src/main.rs para comenzar. Este
código escuchará en la dirección local 127.0.0.1:7878los flujos TCP entrantes. Al
recibir un flujo entrante, imprimirá "<sub>" (<sub>1</sub>) Connection
established!.
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
println!("Connection established!");
}
}
Listado 21-1: Escuchar transmisiones entrantes e imprimir un mensaje cuando recibimos una
transmisión
Por ahora, nuestra gestión del flujo consiste en llamar unwrappara terminar
nuestro programa si el flujo presenta algún error; si no hay errores, el programa
imprime un mensaje. Añadiremos más funcionalidad para el caso de éxito en la
siguiente lista. La razón por la que podríamos recibir errores del incomingmétodo
cuando un cliente se conecta al servidor es que en realidad no estamos iterando
sobre las conexiones. En su lugar, estamos iterando sobre los intentos de
conexión . La conexión podría no ser exitosa por varias razones, muchas de ellas
específicas del sistema operativo. Por ejemplo, muchos sistemas operativos
tienen un límite en la cantidad de conexiones abiertas simultáneas que pueden
admitir; los nuevos intentos de conexión que superen ese número producirán un
error hasta que se cierren algunas de las conexiones abiertas.
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
A veces, verás varios mensajes impresos para una solicitud del navegador; el
motivo puede ser que el navegador esté realizando una solicitud para la página y
también para otros recursos, como el ícono favicon.ico que aparece en la pestaña
del navegador.
Rust
jueves, 22 de mayo de 2025 : Página 653 de 719
Leyendo la solicitud
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
handle_connection(stream);
}
}
println!("Request: {http_request:#?}");
Rust
jueves, 22 de mayo de 2025 : Página 654 de 719
}
Listado 21-2: Lectura TcpStreame impresión de datos
El navegador indica el final de una solicitud HTTP enviando dos saltos de línea
consecutivos. Para obtener una solicitud del flujo, tomamos líneas hasta obtener
una que sea la cadena vacía. Una vez recopiladas las líneas en el vector, las
imprimimos con un formato de depuración optimizado para poder revisar las
instrucciones que el navegador web envía a nuestro servidor.
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello`
Request: [
"GET / HTTP/1.1",
Rust
jueves, 22 de mayo de 2025 : Página 655 de 719
"Host: 127.0.0.1:7878",
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101
Firefox/99.0",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/
*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip, deflate, br",
"DNT: 1",
"Connection: keep-alive",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Cache-Control: max-age=0",
]
URL no es relevante para los fines de este capítulo, pero la especificación HTTP
usa el término URI, así que podemos sustituir "URI" por "URL".
Al observar los datos de la línea de solicitud que recibimos hasta ahora al ejecutar
nuestro programa, vemos que GETes el método, / es la URI de la solicitud
y HTTP/1.1es la versión.
La primera línea es una línea de estado que contiene la versión HTTP utilizada en
la respuesta, un código de estado numérico que resume el resultado de la
solicitud y una frase de motivo que proporciona una descripción textual del código
de estado. Después de la secuencia CRLF se encuentran los encabezados, otra
secuencia CRLF y el cuerpo de la respuesta.
Rust
jueves, 22 de mayo de 2025 : Página 657 de 719
stream.write_all(response.as_bytes()).unwrap();
}
Listado 21-3: Cómo escribir una pequeña respuesta HTTP exitosa a la transmisión
La primera línea nueva define la responsevariable que contiene los datos del
mensaje de éxito. Luego, llamamos as_bytesa `our` responsepara convertir los
datos de la cadena a bytes. El write_allmétodo `on` streamtoma `a` &[u8]y envía
esos bytes directamente a través de la conexión. Dado que la write_alloperación
podría fallar, usamos `on` unwrappara cualquier resultado de error, como antes.
En una aplicación real, se añadiría aquí la gestión de errores.
directorio src . Puede introducir el HTML que desee; el Listado 21-4 muestra una
posibilidad.
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
Listado 21-5: Envío del contenido de hello.html como cuerpo de la respuesta
Rust
jueves, 22 de mayo de 2025 : Página 659 de 719
Hemos ampliado fsla usedeclaración para incluir el módulo del sistema de archivos
de la biblioteca estándar. El código para leer el contenido de un archivo en una
cadena debería resultar familiar; lo usamos en el Capítulo 12 al leer el contenido
de un archivo para nuestro proyecto de E/S (Listado 12-4).
stream.write_all(response.as_bytes()).unwrap();
} else {
// some other request
}
}
Listado 21-6: Manejo de solicitudes a / de manera diferente a otras solicitudes
Solo analizaremos la primera línea de la solicitud HTTP, así que, en lugar de leer la
solicitud completa en un vector, llamaremos a [nombre del nextiterador] para
obtener el primer elemento del iterador. El primer unwrapiterador se encarga de
[nombre del iterador] Optiony detiene el programa si no hay elementos en el
iterador. El segundo unwrapmaneja [nombre del iterador Result] y tiene el mismo
efecto que el unwrap[nombre del iterador] mapañadido en el Listado 21-2.
Ahora, agreguemos el código del Listado 21-7 al elsebloque para devolver una
respuesta con el código de estado 404, que indica que no se encontró el
contenido de la solicitud. También devolveremos HTML para que una página se
visualice en el navegador indicando la respuesta al usuario final.
stream.write_all(response.as_bytes()).unwrap();
}
Listado 21-7: Responder con el código de estado 404 y una página de error si se solicitó algo distinto
a/
Aquí, nuestra respuesta tiene una línea de estado con el código de estado 404 y
la frase de motivo NOT FOUND. El cuerpo de la respuesta será el HTML del
archivo 404.html . Deberá crear un archivo 404.html junto a hello.html para la
página de error; puede usar el HTML que desee o el HTML de ejemplo del Listado
21-8.
Un toque de refactorización
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
Listado 21-9: Refactorización de los ifbloques elsey para que contengan solo el código que difiere entre
los dos casos
Ahora los bloques ify elsesolo devuelven los valores apropiados para la línea de
estado y el nombre del archivo en una tupla; luego usamos la desestructuración
para asignar estos dos valores a status_liney filenameusando un patrón en
ellet declaración, como se analiza en el Capítulo 19.
El código previamente duplicado ahora está fuera de los ifbloques elsey y utiliza
el status_lineyfilename . Esto facilita la diferencia entre ambos casos y significa que
solo tenemos un lugar para actualizar el código si queremos cambiar el
funcionamiento de la lectura de archivos y la escritura de respuestas. El
comportamiento del código del Listado 21-9 será el mismo que el del Listado 21-
7.
Actualmente, nuestro servidor funciona en un solo hilo, lo que significa que solo
puede atender una solicitud a la vez. Analicemos cómo esto puede ser un
problema simulando algunas solicitudes lentas. Luego, lo solucionaremos para
que nuestro servidor pueda procesar varias solicitudes a la vez.
Rust
jueves, 22 de mayo de 2025 : Página 663 de 719
// --snip--
}
Listado 21-10: Simulación de una solicitud lenta durmiendo durante 5 segundos
Rust
jueves, 22 de mayo de 2025 : Página 664 de 719
Pasamos de [a] ifa [a] y matchahora tenemos tres casos. Necesitamos hacer
coincidir explícitamente en un segmento de [ request_linea] para la coincidencia de
patrones con los valores literales de cadena; matchno realiza referencias ni
desreferencias automáticas como el método de igualdad.
El primer brazo es igual al ifbloque del Listado 21-9. El segundo brazo corresponde
a una solicitud con /sleep . Al recibirla, el servidor se suspenderá durante 5
segundos antes de renderizar la página HTML correctamente. El tercer brazo es
igual al elsebloque del Listado 21-9.
Puedes ver lo primitivo que es nuestro servidor: ¡las bibliotecas reales manejarían
el reconocimiento de múltiples solicitudes de una manera mucho menos
detallada!
Inicie el servidor usando cargo run. Luego abra dos ventanas del navegador: una
para http://127.0.0.1:7878/ y otra para http://127.0.0.1:7878/sleep . Si introduce
la URI / varias veces, como antes, verá que responde rápidamente. Pero si
introduce /sleep y luego carga / , verá que / espera hasta sleep se suspenda
durante los 5 segundos completos antes de cargarse.
Hay varias técnicas que podemos usar para evitar que las solicitudes se acumulen
detrás de una solicitud lenta, incluido el uso de async como hicimos en el Capítulo
17; lo que implementaremos es un grupo de subprocesos.
Primero, exploremos cómo se vería nuestro código si se creara un nuevo hilo para
cada conexión. Como se mencionó anteriormente, este no es nuestro plan final
debido a los problemas que conlleva la posibilidad de generar un número
Rust
jueves, 22 de mayo de 2025 : Página 666 de 719
thread::spawn(|| {
handle_connection(stream);
});
}
}
Listado 21-11: Generar un nuevo hilo para cada transmisión
fn main() {
Rust
jueves, 22 de mayo de 2025 : Página 667 de 719
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
pool.execute(|| {
handle_connection(stream);
});
}
}
Listado 21-12: Nuestra ThreadPoolinterfaz ideal
Realice los cambios del Listado 21-12 en src/main.rs y luego usemos los errores
del compilador cargo checkpara guiar nuestro desarrollo. Este es el primer error
que recibimos:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
--> src/main.rs:11:16
|
11 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^ use of undeclared type `ThreadPool`
For more information about this error, try `rustc --explain E0433`.
error: could not compile `hello` (bin "hello") due to 1 previous error
¡Genial! Este error nos indica que necesitamos un ThreadPooltipo o módulo, así que
lo crearemos ahora. Nuestra ThreadPoolimplementación será independiente del
tipo de trabajo que esté realizando nuestro servidor web. Por lo tanto, cambiemos
el hellocontenedor de binarios a contenedor de biblioteca para alojar
nuestra ThreadPoolimplementación. Después de cambiar a contenedor de
biblioteca, también podríamos usar la biblioteca del grupo de subprocesos
independiente para cualquier trabajo que queramos realizar con un grupo de
subprocesos, no solo para atender solicitudes web.
Rust
jueves, 22 de mayo de 2025 : Página 668 de 719
Luego edite el archivo main.rs para traerlo ThreadPoolal alcance desde el cajón de
la biblioteca agregando el siguiente código en la parte superior de src/main.rs :
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in
the current scope
--> src/main.rs:12:28
|
12 | let pool = ThreadPool::new(4);
| ^^^ function or associated item not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
Este error indica que, a continuación, debemos crear una función asociada
llamada newfor ThreadPool. También sabemos que newdebe tener un parámetro
que pueda aceptar 4como argumento y devolver una ThreadPoolinstancia.
Implementemos la newfunción más simple que tenga estas características:
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
}
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope
--> src/main.rs:17:14
|
17 | pool.execute(|| {
| -----^^^^^^^ method not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
¡Compila! Pero ten en cuenta que si intentas cargo runrealizar una solicitud en el
navegador, verás los errores que vimos al principio del capítulo. ¡Nuestra
biblioteca execute aún no está llamando al cierre que se le pasó!
Nota: Un dicho que quizás escuches sobre lenguajes con compiladores estrictos, como Haskell y Rust,
es "si el código compila, funciona". Sin embargo, este dicho no es universalmente cierto. Nuestro
proyecto compila, ¡pero no hace absolutamente nada! Si estuviéramos desarrollando un proyecto real y
completo, este sería un buen momento para empezar a escribir pruebas unitarias para comprobar que el
código compila y se comporta como queremos.
ThreadPool
}
// --snip--
}
Listado 21-13: ImplementaciónThreadPool::new para entrar en pánico si sizees cero
El código del Listado 21-14 se compilará, pero aún no crea ningún hilo. Hemos
modificado la definición de ThreadPoolpara que contenga un vector
de thread::JoinHandle<()>instancias, inicializado el vector con una capacidad de size,
configurado un forbucle que ejecutará código para crear los hilos y devuelto
una ThreadPoolinstancia que los contiene.
use std::thread;
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
for _ in 0..size {
// create some threads and store them in the vector
}
ThreadPool { threads }
}
// --snip--
}
Rust
jueves, 22 de mayo de 2025 : Página 673 de 719
Listado 21-14: Creación de un vector para ThreadPoolcontener los hilos
Una vez recibido un tamaño válido, ThreadPoolcrea un nuevo vector que puede
contener sizeelementos. La with_capacityfunción realiza la misma tarea
que, Vec::newpero con una diferencia importante: preasigna espacio en el vector.
Como sabemos que necesitamos almacenar sizeelementos en el vector, realizar
esta asignación por adelantado es ligeramente más eficiente que usar Vec::new,
que se redimensiona automáticamente a medida que se insertan los elementos.
¿Listo? Aquí está el Listado 21-15 con una forma de realizar las modificaciones
anteriores.
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
for id in 0..size {
workers.push(Worker::new(id));
}
ThreadPool { workers }
}
// --snip--
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
Rust
jueves, 22 de mayo de 2025 : Página 675 de 719
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});
Nota: Si el sistema operativo no puede crear un hilo por falta de recursos, thread::spawnentrará en
pánico. Esto provocará un pánico en todo el servidor, aunque la creación de algunos hilos pueda ser
exitosa. Para simplificar, este comportamiento es correcto, pero en una implementación de un grupo de
hilos en producción, probablemente prefiera usar std::thread::Buildery su spawnmétodo que
retorna Result.
struct Job;
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
for id in 0..size {
workers.push(Worker::new(id));
}
Intentemos pasar un receptor del canal a cada trabajador a medida que el grupo
de subprocesos crea el canal. Sabemos que queremos usar el receptor en el
subproceso que generan los trabajadores, así que haremos referencia
al receiverparámetro en el cierre. El código del Listado 21-17 aún no compila del
todo.
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
for id in 0..size {
workers.push(Worker::new(id, receiver));
}
// --snip--
impl Worker {
fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0382]: use of moved value: `receiver`
--> src/lib.rs:26:42
|
21 | let (sender, receiver) = mpsc::channel();
| -------- move occurs because `receiver` has type
`std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
25 | for id in 0..size {
| ----------------- inside of this loop
26 | workers.push(Worker::new(id, receiver));
| ^^^^^^^^ value moved here, in previous iteration of loop
|
note: consider changing this parameter type in method `new` to borrow instead if owning the
value isn't necessary
--> src/lib.rs:47:33
|
47 | fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
| --- in this method ^^^^^^^^^^^^^^^^^^^ this parameter takes ownership
of the value
help: consider moving the expression out of the loop so it is only moved once
|
25 ~ let mut value = Worker::new(id, receiver);
26 ~ for id in 0..size {
27 ~ workers.push(value);
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello` (lib) due to 1 previous error
Además, sacar un trabajo de la cola del canal implica mutar el receiver, por lo que
los subprocesos necesitan una forma segura de compartir y modificar receiver ; de
lo contrario, podríamos obtener condiciones de carrera (como se cubre en el
Capítulo 16).
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
// --snip--
}
// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
// --snip--
}
}
Listado 21-18: Compartir el receptor entre los trabajadores que utilizan ArcyMutex
Implementando el executemétodo
Rust
jueves, 22 de mayo de 2025 : Página 680 de 719
impl ThreadPool {
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
// --snip--
Listado 21-19: Creación de un Jobalias de tipo para a Boxque contiene cada cierre y luego envío del
trabajo por el canal
Tras crear una nueva Jobinstancia con el cierre que obtenemos en [insertar
valor] execute, enviamos ese trabajo por el extremo emisor del canal.
Llamamos unwrapa send[insertar valor] en caso de que el envío falle. Esto podría
ocurrir si, por ejemplo, detenemos la ejecución de todos nuestros hilos, lo que
significa que el extremo receptor ha dejado de recibir mensajes nuevos.
Actualmente, no podemos detener la ejecución de nuestros hilos: estos continúan
ejecutándose mientras exista el grupo. La razón por la que usamos [insertar
valor] unwrapes que sabemos que el fallo no ocurrirá, pero el compilador lo
desconoce.
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || loop {
let job = receiver.lock().unwrap().recv().unwrap();
job();
});
Aquí, primero llamamos locka [ nombre receiverdel hilo] para adquirir el mutex y
luego llamamos unwrapa [nombre del hilo] para entrar en pánico ante cualquier
error. La adquisición de un bloqueo podría fallar si el mutex está en estado de
envenenamiento , lo cual puede ocurrir si otro hilo entró en pánico mientras
mantenía el bloqueo en lugar de liberarlo. En esta situación, llamar unwrapa
[nombre del hilo] para que este hilo entre en pánico es la acción correcta. Puede
cambiar esto unwrapa [nombre del hilo]expect con un mensaje de error que le
resulte significativo.
La llamada a recvse bloquea, por lo que, si aún no hay un trabajo, el hilo actual
esperará hasta que haya uno disponible. Esto Mutex<T>garantiza que solo
un Workerhilo a la vez intente solicitar un trabajo.
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
warning: field `workers` is never read
--> src/lib.rs:7:5
|
6 | pub struct ThreadPool {
| ---------- field in this struct
Rust
jueves, 22 de mayo de 2025 : Página 682 de 719
7| workers: Vec<Worker>,
| ^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
Nota: Si abre /suspende varias ventanas del navegador simultáneamente, podrían cargarse una a la vez
en intervalos de 5 segundos. Algunos navegadores web ejecutan varias instancias de la misma solicitud
secuencialmente por razones de almacenamiento en caché. Esta limitación no se debe a nuestro
servidor web.
// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
while let Ok(job) = receiver.lock().unwrap().recv() {
println!("Worker {id} got a job; executing.");
job();
}
});
Una cosa a tener en cuenta a medida que avanzamos: nada de esto afecta las
partes del código que manejan la ejecución de los cierres, por lo que todo aquí
sería igual si estuviéramos usando un grupo de subprocesos para un tiempo de
ejecución asincrónico.
worker.thread.join().unwrap();
}
}
}
Listado 21-22: Unirse a cada hilo cuando el grupo de hilos queda fuera del alcance
Rust
jueves, 22 de mayo de 2025 : Página 685 de 719
Primero, recorremos cada uno de los hilos del grupo de hilos workers. Usamos
[nombre del hilo &mut] para esto porque selfes una referencia mutable y también
necesitamos poder mutar [ workernombre del hilo]. Para cada trabajador,
imprimimos un mensaje indicando que este trabajador en particular se está
cerrando y luego llamamos joinal hilo de ese trabajador. Si la llamada joinfalla,
usamos [nombre del hilo]unwrap nombre del hilo ] para provocar que Rust entre
en pánico y se cierre de forma incorrecta.
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference
--> src/lib.rs:52:13
|
52 | worker.thread.join().unwrap();
| ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call
| |
| move occurs because `worker.thread` has type `JoinHandle<()>`, which does not
implement the `Copy` trait
|
note: `JoinHandle::<T>::join` takes ownership of the receiver `self`, which moves
`worker.thread`
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/std/src/thread/
mod.rs:1763:17
|
1763 | pub fn join(self) -> Result<T> {
| ^^^^
For more information about this error, try `rustc --explain E0507`.
error: could not compile `hello` (lib) due to 1 previous error
worker.thread.join().unwrap();
}
}
}
Esto resuelve el error del compilador y no requiere ningún otro cambio en nuestro
código.
Con todos los cambios realizados, nuestro código compila sin advertencias. Sin
embargo, la mala noticia es que aún no funciona como queremos. La clave reside
en la lógica de los cierres ejecutados por los hilos de las Worker instancias: por el
momento, llamamos a join, pero esto no cerrará los hilos, ya que loopbuscan
constantemente tareas. Si intentamos eliminar nuestro ThreadPoolcon nuestra
implementación actual de drop, el hilo principal se bloqueará indefinidamente
esperando a que el primer hilo termine.
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
worker.thread.join().unwrap();
}
}
}
Listado 21-23: Eliminar explícitamente senderantes de unirse a los subprocesos de trabajo
match message {
Ok(job) => {
println!("Worker {id} got a job; executing.");
job();
}
Err(_) => {
println!("Worker {id} disconnected; shutting down.");
break;
}
}
});
Para ver este código en acción, modifiquémoslo mainpara aceptar solo dos
solicitudes antes de apagar correctamente el servidor, como se muestra en el
Listado 21-25.
pool.execute(|| {
handle_connection(stream);
});
}
println!("Shutting down.");
}
Listado 21-25: Apague el servidor después de atender dos solicitudes saliendo del bucle
No querrías que un servidor web real se apagara tras atender solo dos solicitudes.
Este código simplemente demuestra que el apagado y la limpieza correctos
funcionan correctamente.
Rust
jueves, 22 de mayo de 2025 : Página 689 de 719
Inicie el servidor con cargo runy realice tres solicitudes. La tercera solicitud debería
generar un error y en su terminal debería ver un resultado similar a este:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/hello`
Worker 0 got a job; executing.
Shutting down.
Shutting down worker 0
Worker 3 got a job; executing.
Worker 1 disconnected; shutting down.
Worker 2 disconnected; shutting down.
Worker 3 disconnected; shutting down.
Worker 0 disconnected; shutting down.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
pool.execute(|| {
handle_connection(stream);
});
}
println!("Shutting down.");
}
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
Rust
jueves, 22 de mayo de 2025 : Página 691 de 719
}
Nombre de archivo: src/lib.rs
use std::{
sync::{mpsc, Arc, Mutex},
thread,
};
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
struct Worker {
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || loop {
let message = receiver.lock().unwrap().recv();
match message {
Ok(job) => {
println!("Worker {id} got a job; executing.");
job();
}
Err(_) => {
println!("Worker {id} disconnected; shutting down.");
break;
}
}
});
Worker {
id,
thread: Some(thread),
}
}
}
¡Podríamos hacer más! Si quieres seguir mejorando este proyecto, aquí tienes
algunas ideas:
Resumen
¡Bien hecho! ¡Has llegado al final del libro! Queremos agradecerte por
acompañarnos en este recorrido por Rust. Ya estás listo para implementar tus
propios proyectos en Rust y colaborar con los de otros. Recuerda que hay una
comunidad acogedora de otros Rustaceanos que estarán encantados de ayudarte
con cualquier desafío que encuentres en tu camino hacia Rust.
Apéndice
Las siguientes secciones contienen material de referencia que puede resultarle
útil en su experiencia en Rust.
Las siguientes palabras clave aún no tienen ninguna funcionalidad, pero Rust las
reserva para un posible uso futuro.
abstract
become
box
do
final
gen
macro
override
priv
try
typeof
unsized
virtual
yield
Los identificadores sin formato son la sintaxis que permite usar palabras clave
donde normalmente no estarían permitidas. Se usan anteponiendo "<nombre del
identificador>" a una palabra clave r#.
Por ejemplo, matches una palabra clave. Si intenta compilar la siguiente función
que usa matchcomo nombre:
fn main() {
assert!(r#match("foo", "foobar"));
}
Operadores
Rust
jueves, 22 de mayo de 2025 : Página 697 de 719
Operad ¿Sobrecarga
Ejemplo Explicación
or ble?
! ident!(...), ident!{...},ident![...] Expansión macro
Complemento bit a
! !expr Not
bit o lógico
Comparación de
!= expr != expr PartialEq
desigualdades
% expr % expr resto aritmético Rem
Resto aritmético y
%= var %= expr RemAssign
asignación
& &expr,&mut expr Pedir prestado
&type, &mut type, &'a type,&'a Tipo de puntero
&
mut type prestado
& expr & expr AND bit a bit BitAnd
AND bit a bit y BitAndAssig
&= var &= expr
asignación n
Cortocircuito
&& expr && expr
lógico AND
Multiplicación
* expr * expr Mul
aritmética
Multiplicación y
*= var *= expr asignación MulAssign
aritmética
* *expr Desreferenciar Deref
Puntero sin
* *const type,*mut type
procesar
Restricción de tipo
+ trait + trait,'a + trait
compuesto
+ expr + expr Suma aritmética Add
Suma y asignación
+= var += expr AddAssign
aritmética
, expr, expr Separador de
argumentos y
Rust
jueves, 22 de mayo de 2025 : Página 698 de 719
Operad ¿Sobrecarga
Ejemplo Explicación
or ble?
elementos
Negación
- - expr Neg
aritmética
- expr - expr Resta aritmética Sub
Resta y asignación
-= var -= expr SubAssign
aritmética
Tipo de retorno de
-> fn(...) -> type,|…| -> type
función y cierre
Acceso para
. expr.ident
miembros
Literal de rango
.. .., expr.., ..expr,expr..expr exclusivo a la PartialOrd
derecha
Literal de rango
..= ..=expr,expr..=expr inclusivo a la PartialOrd
derecha
Sintaxis de
actualización de
.. ..expr
literales de
estructura
Patrón de
.. variant(x, ..),struct_type { x, .. } encuadernación “Y
el resto”
(Obsoleto,
úselo ..=en su
... expr...expr lugar) En un
patrón: patrón de
rango inclusivo
/ expr / expr División aritmética Div
División y
/= var /= expr asignación DivAssign
aritmética
: pat: type,ident: type Restricciones
Inicializador de
: ident: expr campo de
estructura
: 'a: loop {...} Etiqueta de bucle
; expr; Terminador de
declaración y
Rust
jueves, 22 de mayo de 2025 : Página 699 de 719
Operad ¿Sobrecarga
Ejemplo Explicación
or ble?
artículo
Parte de la sintaxis
; [...; len] de matriz de
tamaño fijo
Desplazamiento a
<< expr << expr Shl
la izquierda
Desplazamiento a
<<= var <<= expr la izquierda y ShlAssign
asignación
Menos que la
< expr < expr PartialOrd
comparación
Menor o igual a la
<= expr <= expr PartialOrd
comparación
Asignación/
= var = expr,ident = type
equivalencia
Comparación de
== expr == expr PartialEq
igualdad
Parte de la sintaxis
=> pat => expr del brazo de
coincidencia
Mayor que la
> expr > expr PartialOrd
comparación
Mayor o igual que
>= expr >= expr PartialOrd
la comparación
Desplazamiento a
>> expr >> expr Shr
la derecha
Desplazamiento a
>>= var >>= expr la derecha y ShrAssign
asignación
Encuadernación de
@ ident @ pat
patrones
OR exclusivo bit a
^ expr ^ expr BitXor
bit
OR exclusivo bit a
^= var ^= expr BitXorAssign
bit y asignación
Alternativas de
| pat | pat
patrones
| expr | expr OR bit a bit BitOr
|= var |= expr OR bit a bit y BitOrAssign
Rust
jueves, 22 de mayo de 2025 : Página 700 de 719
Operad ¿Sobrecarga
Ejemplo Explicación
or ble?
asignación
Cortocircuito
|| expr || expr
lógico OR
Propagación de
? expr?
errores
La siguiente lista contiene todos los símbolos que no funcionan como operadores;
es decir, no se comportan como una llamada de función o método.
La Tabla B-2 muestra símbolos que aparecen solos y son válidos en una variedad
de ubicaciones.
Símbolo Explicación
Etiqueta de duración de vida o de bucle con
'ident
nombre
...u8, ...i32, ...f64, ...usize, etc. Literal numérico de tipo específico
"..." literal de cadena
Literal de cadena sin formato, caracteres de escape
r"...", r#"..."#, r##"..."##, etc.
no procesados
Literal de cadena de bytes; construye una matriz
b"..."
de bytes en lugar de una cadena
Literal de cadena de bytes sin procesar,
br"...", br#"..."#, br##"..."##, etc. combinación de literal de cadena de bytes sin
procesar
'...' Carácter literal
b'...' literal de byte ASCII
|…| expr Cierre
Tipo de fondo siempre vacío para funciones
!
divergentes
Vinculación de patrones “ignorados”; también se
_
utiliza para hacer legibles los literales enteros
La Tabla B-3 muestra los símbolos que aparecen en el contexto de una ruta a
través de la jerarquía del módulo hasta un elemento.
Rust
jueves, 22 de mayo de 2025 : Página 701 de 719
Símbolo Explicación
ident::ident Ruta del espacio de nombres
Ruta relativa al preludio externo, donde se encuentran todas
::path las demás cajas (es decir, una ruta explícitamente absoluta
que incluye el nombre de la caja)
Ruta relativa al módulo actual (es decir, una ruta
self::path
explícitamente relativa).
super::path Ruta relativa al padre del módulo actual
type::ident,<type as
Constantes, funciones y tipos asociados
trait>::ident
Elemento asociado a un tipo que no se puede nombrar
<type>::...
directamente (por ejemplo, <&T>::..., <[T]>::..., etc.)
Desambiguación de una llamada de método nombrando el
trait::method(...)
rasgo que la define
Desambiguación de una llamada de método nombrando el
type::method(...)
tipo para el que está definido
<type as Desambiguación de una llamada de método nombrando el
trait>::method(...) rasgo y el tipo
La Tabla B-4 muestra los símbolos que aparecen en el contexto del uso de
parámetros de tipo genérico.
Símbolo Explicación
Especifica parámetros para un tipo genérico en un tipo
path<...>
(por ejemplo, Vec<u8>)
Especifica parámetros para un tipo, función o método
path::<...>,method::<...> genérico en una expresión; a menudo denominado
turbofish (por ejemplo, "42".parse::<i32>())
fn ident<...> ... Definir función genérica
struct ident<...> ... Definir estructura genérica
enum ident<...> ... Definir enumeración genérica
impl<...> ... Definir implementación genérica
for<...> type Límites de vida de mayor rango
Un tipo genérico donde uno o más tipos asociados tienen
type<ident=type> asignaciones específicas (por
ejemplo, Iterator<Item=T>)
Rust
jueves, 22 de mayo de 2025 : Página 702 de 719
Símbolo Explicación
T: U Parámetro genérico Trestringido a tipos que implementanU
El tipo genérico Tdebe sobrevivir a la vida útil 'a(lo que significa que
T: 'a el tipo no puede contener de manera transitiva ninguna referencia con
una vida útil menor a 'a)
El tipo genérico Tno contiene referencias prestadas distintas
T: 'static
a 'staticlas
'b: 'a La vida útil genérica 'bdebe durar más que la vida útil'a
Permitir que el parámetro de tipo genérico sea un tipo de tamaño
T: ?Sized
dinámico
'a + trait,trait +
Restricción de tipo compuesto
trait
La Tabla B-6 muestra los símbolos que aparecen en el contexto de llamar o definir
macros y especificar atributos en un elemento.
Símbolo Explicación
#[meta] Atributo externo
#![meta] Atributo interno
$ident Sustitución macro
$ident:kind Captura de macro
$(…)… Repetición de macro
ident!(...), ident!{...},ident![...] Invocación de macro
Símbolo Explicación
// Comentario de línea
//! Comentario del documento de línea interna
/// Comentario del documento de línea exterior
Rust
jueves, 22 de mayo de 2025 : Página 703 de 719
Símbolo Explicación
/*...*/ Bloquear comentario
/*!...*/ Comentario del documento del bloque interno
/**...*/ Comentario del documento del bloque externo
La Tabla B-8 muestra los símbolos que aparecen en el contexto del uso de tuplas.
Símbolo Explicación
Tupla vacía (también conocida como unidad), tanto literal como de
()
tipo
(expr) Expresión entre paréntesis
(expr,) Expresión de tupla de un solo elemento
(type,) Tipo de tupla de un solo elemento
(expr, ...) Expresión de tupla
(type, ...) Tipo de tupla
Expresión de llamada de función; también se utiliza para inicializar
expr(expr, ...)
tuplas structy enumvariantes de tuplas
expr.0, expr.1,
Indexación de tuplas
etc.
Contexto Explicación
{...} Expresión de bloque
Type {...} structliteral
Contexto Explicación
[...] literal de matriz
Matriz literal que contiene lencopias
[expr; len]
deexpr
[type; len] Tipo de matriz que
Rust
jueves, 22 de mayo de 2025 : Página 704 de 719
Contexto Explicación
contiene leninstancias detype
Indexación de colecciones.
expr[expr]
Sobrecargable ( Index, IndexMut)
Indexación de colecciones que simula
ser una segmentación de colecciones,
expr[..], expr[a..], expr[..b],expr[a..b]
utilizando Range, RangeFrom, RangeT
o, o RangeFullcomo el “índice”
Estos rasgos aquí listados son los únicos definidos por la biblioteca estándar que
pueden implementarse en sus tipos usando derive. Otros rasgos definidos en la
biblioteca estándar no tienen un comportamiento predeterminado sensato, por lo
que depende de usted implementarlos de la forma que mejor se adapte a su
objetivo.
El Eqrasgo no tiene métodos. Su propósito es indicar que, para cada valor del tipo
anotado, este es igual a sí mismo. El Eqrasgo solo se puede aplicar a tipos que
también implementan ``` PartialEq, aunque no todos los tipos que implementan
``` PartialEqpueden implementar `` Eq`. Un ejemplo de esto son los tipos de
Rust
jueves, 22 de mayo de 2025 : Página 706 de 719
El Ordrasgo permite saber que, para dos valores cualesquiera del tipo anotado,
existirá un orden válido. El Ordrasgo implementa el cmpmétodo, que devuelve
un Orderingen lugar de un Option<Ordering>porque siempre será posible un orden
válido. Solo se puede aplicar el Ordrasgo a tipos que también
implementan PartialOrdy Eq(y Eqrequieren PartialEq). Cuando se deriva de
estructuras y enumeraciones, cmpse comporta igual que la implementación
derivada de for partial_cmpcon PartialOrd.
Rust
jueves, 22 de mayo de 2025 : Página 707 de 719
Se puede derivar Copyde cualquier tipo cuyas partes implementen Copy. Un tipo
que implementa Copytambién debe implementar Clone, porque un tipo que
implementa Copytiene una implementación trivial deClone que realiza la misma
tarea que Copy.
Todo lo que es posible con Copyusted también se puede lograr con Clone, pero el
código puede ser más lento o tener que usar clone en algunos lugares.
$ cargo fmt
fn main() {
let mut x = 42;
println!("{x}");
}
Aquí, definimos una variable xcomo mutable, pero nunca la mutamos. Rust nos
advierte al respecto:
$ cargo build
Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: variable does not need to be mutable
--> src/main.rs:2:9
|
2| let mut x = 0;
| ----^
| |
Rust
jueves, 22 de mayo de 2025 : Página 710 de 719
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default
$ cargo fix
Checking myprogram v0.1.0 (file:///projects/myprogram)
Fixing src/main.rs (1 fix)
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
fn main() {
let x = 42;
println!("{x}");
}
También puedes usar el cargo fixcomando para cambiar tu código entre diferentes
ediciones de Rust. Las ediciones se describen en el Apéndice E.
$ cargo clippy
Por ejemplo, digamos que escribes un programa que utiliza una aproximación de
una constante matemática, como pi, como lo hace este programa:
Rust
jueves, 22 de mayo de 2025 : Página 711 de 719
fn main() {
let x = 3.1415;
let r = 8.0;
println!("the area of the circle is {}", x * r * r);
}
Este error te indica que Rust ya tiene PIdefinida una constante más precisa y que
tu programa funcionaría mejor si la usaras. Entonces, modificarías tu código para
usarla PI. El siguiente código no genera errores ni advertencias de Clippy:
fn main() {
let x = std::f64::consts::PI;
let r = 8.0;
println!("the area of the circle is {}", x * r * r);
}
Apéndice E - Ediciones
En el Capítulo 1, viste que cargo newse agregan metadatos a tu
archivo Cargo.toml sobre una edición. ¡Este apéndice explica qué significa eso!
Cada dos o tres años, el equipo de Rust publica una nueva edición de Rust . Cada
edición reúne las características implementadas en un paquete claro, con
documentación y herramientas completamente actualizadas. Las nuevas
ediciones se lanzan como parte del proceso habitual de lanzamiento de seis
semanas.
Para los usuarios activos de Rust, una nueva edición reúne cambios
incrementales en un paquete fácil de entender.
Para los que no son usuarios, una nueva edición indica que se han
producido algunos avances importantes que podrían hacer que valga la
pena darle otra mirada a Rust.
Para aquellos que desarrollan Rust, una nueva edición proporciona un punto
de encuentro para el proyecto en su conjunto.
Cada proyecto puede optar por una edición distinta a la predeterminada de 2015.
Las ediciones pueden contener cambios incompatibles, como la inclusión de una
nueva palabra clave que entre en conflicto con los identificadores del código. Sin
embargo, a menos que acepte estos cambios, su código seguirá compilándose
incluso si actualiza la versión del compilador de Rust que utiliza.
Todas las versiones del compilador Rust son compatibles con cualquier edición
anterior a su lanzamiento y pueden vincular crates de cualquier edición
compatible. Los cambios de edición solo afectan la forma en que el compilador
analiza el código inicialmente. Por lo tanto, si usas Rust 2015 y una de tus
dependencias usa Rust 2018, tu proyecto compilará y podrá usarla. La situación
contraria, donde tu proyecto usa Rust 2018 y una dependencia usa Rust 2015,
también funciona.
Para ser claros: la mayoría de las funciones estarán disponibles en todas las
ediciones. Los desarrolladores que usen cualquier edición de Rust seguirán viendo
mejoras a medida que se publiquen nuevas versiones estables. Sin embargo, en
algunos casos, principalmente al añadir nuevas palabras clave, es posible que
algunas funciones nuevas solo estén disponibles en ediciones posteriores.
Tendrás que cambiar de edición si quieres aprovecharlas.
Portugués (BR)
Portugués (PT)
Fuentes: KaiserY/trpl-zh-cn , gnu4cn/rust-lang-Zh_CN
Lengua china
Ukraynska
Español , alternativo , Español por RustLangES
Ruso
Coreano
Rust
jueves, 22 de mayo de 2025 : Página 714 de 719
Japonés
Francés
Polaco
Cebuano
Tagalo
esperanto
elinicia
Sueco
Farsi , persa (FA)
Alemán
Hindi
Ya
Danés
Nocturno
Beta
Estable
nightly: * - - * - - *
Cada seis semanas, ¡es hora de preparar una nueva versión! La betarama del
repositorio de Rust se deriva de la masterrama que usa Nightly. Ahora hay dos
versiones:
nightly: * - - * - - *
|
beta: *
La mayoría de los usuarios de Rust no usan las versiones beta activamente, sino
que prueban la versión beta en su sistema de integración continua para ayudar a
Rust a detectar posibles regresiones. Mientras tanto, todavía hay una versión
nocturna cada noche:
nightly: * - - * - - * - - * - - *
|
beta: *
Supongamos que se encuentra una regresión. ¡Qué bueno que tuvimos tiempo
para probar la versión beta antes de que la regresión se colara en la versión
Rust
jueves, 22 de mayo de 2025 : Página 716 de 719
nightly: * - - * - - * - - * - - * - - *
|
beta: *--------*
nightly: * - - * - - * - - * - - * - - * - * - *
|
beta: *--------*
|
stable: *
¡Genial! ¡Rust 1.5 ya está listo! Sin embargo, olvidamos algo: como han pasado
seis semanas, también necesitamos una nueva beta de la próxima versión de
Rust, la 1.6. Así que, después de stableque se ramificara de beta, la próxima
versión de betase ramificara nightlyde nuevo:
nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: *--------* *
|
stable: *
Esto se llama el “modelo de tren” porque cada seis semanas, una versión “sale de
la estación”, pero todavía tiene que realizar un viaje a través del canal beta antes
de llegar como una versión estable.
que salga la próxima versión estable. Los fallos en una versión beta son
relativamente raros, pero rustcsiguen siendo software, y los errores existen.
Tiempo de mantenimiento
El proyecto Rust ofrece soporte para la versión estable más reciente. Cuando se
lanza una nueva versión estable, la versión anterior alcanza el fin de su ciclo de
vida (EOL). Esto significa que cada versión recibe soporte durante seis semanas.
Características inestables
Este libro solo contiene información sobre las funciones estables, ya que las
funciones en desarrollo siguen cambiando y seguramente serán diferentes entre
su redacción y su habilitación en las compilaciones estables. Puede encontrar
documentación sobre las funciones de actualización nocturna en línea.
As you can see, the stable toolchain is the default. Most Rust users use stable
most of the time. You might want to use stable most of the time, but use nightly
on a specific project, because you care about a cutting-edge feature. To do so,
you can use rustup override in that project’s directory to set the nightly toolchain as
the one rustup should use when you’re in that directory:
$ cd ~/projects/needs-nightly
$ rustup override set nightly
So how do you learn about these new features? Rust’s development model follows
a Request For Comments (RFC) process. If you’d like an improvement in Rust, you
can write up a proposal, called an RFC.
Anyone can write RFCs to improve Rust, and the proposals are reviewed and
discussed by the Rust team, which is comprised of many topic subteams. There’s
a full list of the teams on Rust’s website, which includes teams for each area of
the project: language design, compiler implementation, infrastructure,
documentation, and more. The appropriate team reads the proposal and the
comments, writes some comments of their own, and eventually, there’s
consensus to accept or reject the feature.
If the feature is accepted, an issue is opened on the Rust repository, and someone
can implement it. The person who implements it very well may not be the person
who proposed the feature in the first place! When the implementation is ready, it
lands on the master branch behind a feature gate, as we discussed in
the “Unstable Features” section.
Rust
jueves, 22 de mayo de 2025 : Página 719 de 719
After some time, once Rust developers who use nightly releases have been able
to try out the new feature, team members will discuss the feature, how it’s
worked out on nightly, and decide if it should make it into stable Rust or not. If the
decision is to move forward, the feature gate is removed, and the feature is now
considered stable! It rides the trains into a new stable release of Rust.