El Lenguaje de Programación Rust
El Lenguaje de Programación Rust
Esta versión del texto asume que estás usando Rust 1.78.0 (lanzado 2024-05-02) o posterior.
Vea la sección “Instalación” del Capítulo 1 para instalar o actualizar Rust.
Este texto está disponible en formato de libro impreso y ebook de No Starch Press.
https://book.rustlang-es.org/print 1/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Prefacio
No siempre fue tan claro, pero el lenguaje de programación Rust es fundamentalmente
sobre empoderamiento: no importa qué tipo de código estás escribiendo ahora, Rust te
empodera para llegar más lejos, para programar con confianza en una mayor variedad de
dominios de lo que lo hiciste antes.
Tomemos, por ejemplo, el trabajo de “sistemas” que se ocupa de los detalles de bajo nivel
de la administración de la memoria, la representación de datos y la concurrencia.
Tradicionalmente, este reino de la programación se ve como arcano, accesible sólo a unos
pocos que han dedicado los años necesarios para aprender a evitar sus infames trampas. Y
aunque aquellos que lo practican lo hacen con precaución, para que su código no esté
abierto a vulnerabilidades que puedan ser explotadas, caídas o corrupción.
Rust rompe estas barreras al eliminar las antiguas trampas y proporciona un conjunto
amigable y pulido de herramientas para ayudarte en el camino. Los programadores que
necesitan “sumergirse” en un control de bajo nivel pueden hacerlo con Rust, sin asumir el
riesgo habitual de caídas o agujeros de seguridad, y sin tener que aprender los puntos finos
de una cadena de herramientas caprichosa. Lo mejor de todo, el lenguaje está diseñado
para guiarte naturalmente hacia un código confiable que es eficiente en términos de
velocidad y uso de memoria.
Los programadores que ya están trabajando con código de bajo nivel pueden usar Rust para
elevar sus ambiciones. Por ejemplo, introducir la paralelización en Rust es una operación
relativamente de bajo riesgo: el compilador lo detectará por ti. Así puedes abordar
optimizaciones más agresivas en tu código con la confianza de que no introducirás
accidentalmente caídas o vulnerabilidades.
Este libro abraza plenamente el potencial de Rust para empoderar a tus usuarios. Es un
texto amigable y accesible que tiene como objetivo ayudarte a mejorar no sólo tu
conocimiento de Rust, sino también tu alcance y confianza como programador en general.
Así que sumérgete, prepárate para aprender y ¡bienvenido a la comunidad Rust!
https://book.rustlang-es.org/print 2/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Introducción
Nota: Esta edición del libro es la misma que The Rust Programming Language
disponible en formato impreso y ebook de No Starch Press.
Equipos de Desarrolladores
Rust está demostrando ser una herramienta productiva para colaborar entre equipos
grandes de desarrolladores con diferentes niveles de conocimiento de programación de
sistemas. El código de bajo nivel tiende a tener varios sutiles errores, que en la mayoría de
otros lenguajes solo pueden ser detectados a través de pruebas extensivas y una revisión
cuidadosa del código por parte de desarrolladores experimentados. En Rust, el compilador
juega un rol de guardián al negarse a compilar código con estos errores elusivos, incluidos
los errores de concurrencia. Trabajando junto al compilador, el equipo puede dedicar su
tiempo a enfocarse en la lógica del programa en lugar de perseguir errores.
https://book.rustlang-es.org/print 3/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Al usar estas y otras herramientas en el ecosistema de Rust, los desarrolladores pueden ser
productivos mientras escriben código de nivel de sistemas.
Estudiantes
Empresas
Cientos de empresas, grandes y pequeñas, usan Rust en producción para una variedad de
tareas, incluidas herramientas de línea de comandos, servicios web, herramientas de
DevOps, dispositivos incrustados, análisis y transcodificación de audio y video,
criptomonedas, bioinformática, motores de búsqueda, aplicaciones de Internet de las cosas,
aprendizaje automático e incluso partes importantes del navegador web Firefox.
Rust es para personas que anhelan velocidad y estabilidad en un lenguaje. Por velocidad,
nos referimos tanto a la rapidez con que el código Rust puede ejecutarse como a la
velocidad con que Rust te permite escribir programas. Las verificaciones del compilador de
Rust garantizan la estabilidad a través de adiciones de funcionalidades y refactorizaciones.
Esto contrasta con el código heredado quebradizo en lenguajes sin estas verificaciones, que
los desarrolladores a menudo tienen miedo de modificar. Al esforzarse por lograr
abstracciones de costo cero, características de alto nivel que se compilan en código de bajo
nivel tan rápido como el código escrito manualmente, Rust se esfuerza por hacer que el
código seguro sea también código rápido.
El lenguaje Rust también espera apoyar a muchos otros usuarios; los mencionados aquí son
solo algunos de los principales interesados. En general, la mayor ambición de Rust es
eliminar los compromisos que los programadores han aceptado durante décadas al
https://book.rustlang-es.org/print 4/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El capítulo 1 explica cómo instalar Rust, cómo escribir un programa “Hola, mundo!” Y cómo
usar Cargo, el administrador de paquetes y herramienta de compilación de Rust. El capítulo
2 es una introducción práctica a la escritura de un programa en Rust, teniendo que construir
un juego de adivinanzas. Aquí tratamos los conceptos a un nivel alto, y capítulos posteriores
proporcionarán detalles adicionales. Si quieres ponerte manos a la obra de inmediato, el
capítulo 2 es el lugar para eso. El capítulo 3 cubre las características de Rust que son
similares a las de otros lenguajes de programación, y en el capítulo 4 aprenderás sobre el
sistema de propiedad de Rust. Si eres un aprendiz particularmente meticuloso que prefiere
aprender todos los detalles antes de pasar al siguiente, es posible que desees omitir el
capítulo 2 y dirigirte directamente al capítulo 3, regresando al capítulo 2 cuando gustes
trabajar en un proyecto aplicando los detalles que has aprendido.
El capítulo 5 discute las estructuras y los métodos, y el capítulo 6 cubre las enumeraciones,
las expresiones match , y la construcción de flujo de control if let . Usarás estructuras y
enumeraciones para crear tipos personalizados en Rust.
https://book.rustlang-es.org/print 5/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El capítulo 10 se adentra en los genéricos, los traits y los lifetimes, que te dan el poder de
definir código que se aplique a varios tipos. El capítulo 11 trata sobre las pruebas, que
incluso con las garantías de seguridad de Rust, son necesarias para asegurar que la lógica
de tu programa sea correcta. En el capítulo 12, construiremos nuestra propia
implementación de un subconjunto de la funcionalidad del comando de línea de comandos
grep que busca texto dentro de archivos. Para esto, usaremos muchos de los conceptos
que discutimos en los capítulos anteriores.
El capítulo 13 explora las closures y los iteradores: características de Rust que provienen de
los lenguajes de programación funcional. En el capítulo 14, examinaremos Cargo en más
profundidad y hablaremos sobre las mejores prácticas para compartir tus bibliotecas con
otros. El capítulo 15 discute los punteros inteligentes que proporciona la biblioteca estándar
y las características que habilitan su funcionalidad.
No hay una forma incorrecta de leer este libro: ¡si quieres adelantarte, hazlo! Es posible que
debas volver a los capítulos anteriores si experimentas alguna confusión. Pero haz lo que
funcione para ti.
Una parte importante del proceso de aprendizaje de Rust es aprender a leer los mensajes
de error que muestra el compilador: estos te guiarán hacia el código funcional. Por lo tanto,
proporcionaremos muchos ejemplos que no se compilan junto con el mensaje de error que
https://book.rustlang-es.org/print 6/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Ferris Significado
Código fuente
Los archivos de origen de los que se genera este libro se pueden encontrar en GitHub.
https://book.rustlang-es.org/print 7/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Empezando
¡Empecemos tu viaje con Rust! Hay mucho que aprender, pero todo viaje comienza en algún
lugar. En este capítulo, discutiremos:
https://book.rustlang-es.org/print 8/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Instalación
El primer paso es instalar Rust. Descargaremos Rust a través de rustup , una herramienta
de línea de comandos para administrar las versiones de Rust y las herramientas asociadas.
Necesitarás una conexión a Internet para la descarga.
Nota: Si prefieres no usar rustup por alguna razón, consulta 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 aseguran que todos los ejemplos del libro que se compilan seguirán
compilando con versiones más nuevas de Rust. La salida puede diferir ligeramente entre
versiones porque Rust a menudo mejora los mensajes de error y las advertencias. En otras
palabras, cualquier versión más nueva, estable de Rust que instales usando estos pasos
debería funcionar como se espera con el contenido de este libro.
https://book.rustlang-es.org/print 9/656
12/7/24, 22:37 El Lenguaje de Programación Rust
También necesitarás un enlazador, que es un programa que Rust utiliza para unir sus salidas
compiladas en un solo archivo. Es probable que ya lo tengas. Si obtienes errores de enlace,
debes instalar un compilador C, que generalmente incluye un enlazador. Un compilador C
también es útil porque algunos paquetes comunes de Rust dependen de código C y
necesitarán un compilador C.
$ xcode-select --install
Los usuarios de Linux deben instalar generalmente GCC o Clang, según la documentación
de su distribución. Por ejemplo, si usas Ubuntu, puede instalar el paquete build-
essential .
Para obtener las herramientas de compilación, deberás instalar Visual Studio. Cuando se te
pregunte qué paquetes de trabajo instalar, incluye:
El resto de este libro usa comandos que funcionan tanto en cmd.exe como en PowerShell. Si
hay diferencias específicas, explicaremos cuál usar.
Si tu necesitas más ayuda con este paso, mira MSVC prerequisites o escríbenos en nuestro
discord
Solución de problemas
Para verificar si has instalado Rust correctamente, abra una shell y escribe esta línea:
$ rustc --version
https://book.rustlang-es.org/print 10/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Si ves esta información, ¡has instalado Rust correctamente! Si no ves esta información,
verifica que Rust esté en la variable de sistema %PATH% de la siguiente manera.
En PowerShell, usa:
$ echo $PATH
Si todo está correcto y Rust aún no funciona, hay varios lugares donde puedes obtener
ayuda. Obten información sobre cómo comunicarte con otros Rustaceans (un apodo tonto
que nos llamamos a nosotros mismos) en la página de la comunidad.
Actualización y desinstalación
Una vez que Rust se instala a través de rustup , actualizar a una versión recién lanzada es
fácil. Desde tu shell, ejecuta el siguiente script de actualización:
$ rustup update
Para desinstalar Rust y rustup , ejecuta el siguiente script de desinstalación desde tu shell:
Documentación local
La instalación de Rust también incluye una copia local de la documentación para que
puedas leerla sin conexión. Ejecuta rustup doc para abrir la documentación local en tu
navegador.
https://book.rustlang-es.org/print 11/656
12/7/24, 22:37 El Lenguaje de Programación Rust
¡Hola, mundo!
Ahora que has instalado Rust, es hora de escribir tu primer programa en Rust. Es tradicional
cuando se aprende un nuevo lenguaje escribir un pequeño programa que imprima el texto
¡Hola, mundo! en la pantalla, ¡así que haremos lo mismo aquí!
Nota: Este libro asume una familiaridad básica con la línea de comandos. Rust no
asume cosas específicas sobre tu editor o herramientas o dónde vive tu código, por lo
que si prefieres usar un entorno de desarrollo integrado (IDE) en lugar de la línea de
comandos, siéntete libre de usar tu IDE favorito. Muchos IDEs ahora tienen algún
grado de soporte para Rust; consulta la documentación del IDE para obtener más
detalles. El equipo de Rust se ha centrado en habilitar un gran soporte a IDEs a través
de rust-analyzer . Consulta Apéndice D para obtener más detalles.
Abre una terminal y escribe los siguientes comandos para crear un directorio proyectos y un
directorio para el proyecto “¡Hola, mundo!” dentro del directorio proyectos.
$ mkdir ~/proyectos
$ cd ~/proyectos
$ mkdir hola_mundo
$ cd hola_mundo
https://book.rustlang-es.org/print 12/656
12/7/24, 22:37 El Lenguaje de Programación Rust
A continuación, crea un nuevo archivo de texto y llámalo main.rs. Los archivos Rust siempre
terminan con la extensión .rs. Si estás usando más de una palabra en tu nombre de archivo,
la convención es usar un guión bajo para separarlos. Por ejemplo, usa hola_mundo.rs en
lugar de holamundo.rs.
Ahora abre el archivo main.rs que acabas de crear y escribe el código en el Listado 1-1.
fn main() {
println!("¡Hola, mundo!");
}
$ rustc main.rs
$ ./main
¡Hola, mundo!
Revisemos este programa “¡Hola, mundo!” en detalle. Aquí está la primera parte del
rompecabezas:
fn main() {
https://book.rustlang-es.org/print 13/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Estas líneas definen una función llamada main . La función main 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 main que no tiene parámetros y no devuelve nada. Si hubiera
parámetros, irían dentro de los paréntesis () .
El cuerpo de la función está envuelto en {} . Rust requiere llaves alrededor de todos los
cuerpos de función. Es buena costumbre colocar la llave de apertura en la misma línea que
la declaración de la función, agregando un espacio entre ambos.
Nota: Si deseas mantener un estilo estándar en todos los proyectos de Rust, puedes
usar una herramienta de formateo automático llamada rustfmt para formatear tu
código en un estilo particular (más sobre rustfmt en Apéndice D). El equipo de Rust
ha incluido esta herramienta con la distribución estándar de Rust, como rustc , por lo
que debería estar instalado en tu computadora.
println!("¡Hola, mundo!");
Esta línea hace todo el trabajo en este pequeño programa: imprime texto en la pantalla. Hay
cuatro detalles importantes que hay que notar aquí.
Primero, el estilo de Rust es indentar con cuatro espacios, no con una tabulación.
Segundo, println! llamamos a una macro de Rust. Si hubiéramos llamado a una función
en su lugar, habríamos ingresado println (sin el ! ). Discutiremos las macros de Rust en
más detalle en el Capítulo 19. Por ahora, solo necesitas saber que usar un ! significa que
estamos llamando a una macro en lugar de una función normal y que las macros no
siempre siguen las mismas reglas que las funciones.
Cuarto, terminamos la línea con un punto y coma ( ; ), lo que indica que esta expresión ha
terminado y la siguiente está lista para comenzar. La mayoría de las líneas de código de Rust
terminan con un punto y coma.
Acabas de ejecutar un programa recién creado, así que examinemos cada paso en el
proceso.
$ rustc main.rs
Si tienes un trasfondo en C o C ++, notarás que esto es similar a gcc o clang . Después de
compilar con éxito, Rust genera un ejecutable binario.
$ ls
main main.rs
En Linux y macOS, verás dos archivos. Con PowerShell en Windows, verás los mismos tres
archivos que verías con CMD. Con CMD en Windows, ingresarías lo siguiente:
> dir /B %= la /B significa que solo mostrara los nombres de los archivos =%
main.exe
main.pdb
main.rs
Esto muestra el archivo de código fuente con la extensión .rs, el archivo ejecutable (main.exe
en Windows, pero main en todas las otras plataformas), y, cuando se usa Windows, un
archivo que contiene información de depuración con la extensión .pdb. Desde aquí, ejecuta
el archivo main o main.exe, así:
Si estás más familiarizado con un lenguaje dinámico, como Ruby, Python o JavaScript, puede
que no estés acostumbrado a compilar y ejecutar un programa como pasos separados. Rust
es un lenguaje compilado de antemano, lo que significa que puedes compilar un programa y
dar el ejecutable a otra persona, y pueden ejecutarlo incluso sin tener Rust instalado. Si le
das a alguien un archivo .rb, .py o .js, necesitan tener una implementación de Ruby, Python o
JavaScript instalada (respectivamente). Pero en esos lenguajes, sólo necesitas un comando
para compilar y ejecutar tu programa. Todo depende de las concesiones hechas al
momento de diseñar un lenguaje.
Solo compilar con rustc está bien para programas simples, pero a medida que tu proyecto
crece, querrás administrar todas las opciones y facilitar el compartir tu código. A
continuación, te presentaremos la herramienta Cargo, que te ayudará a escribir programas
de Rust reales.
https://book.rustlang-es.org/print 15/656
12/7/24, 22:37 El Lenguaje de Programación Rust
¡Hola, Cargo!
Cargo es el sistema de compilación y administrador de paquetes de Rust. La mayoría de los
Rustaceans usan esta herramienta para administrar sus proyectos Rust porque Cargo
maneja muchas tareas para ti, como compilar tu código, descargar las bibliotecas de las que
depende tu código y compilar esas bibliotecas. (Llamamos dependencias a las bibliotecas de
las que depende tu código).
Los programas Rust más simples, como el que hemos escrito hasta ahora, no tienen
dependencias. Si hubiéramos construido el proyecto “¡Hola, mundo!” con Cargo, sólo usaría
la parte de Cargo que maneja la compilación de tu código. A medida que escribas
programas Rust más complejos, agregarás dependencias, y si comienzas un proyecto
usando Cargo, agregar dependencias será mucho más fácil de hacer.
Debido a que la gran mayoría de los proyectos Rust usan Cargo, el resto de este libro asume
que también estás usando Cargo. Cargo viene instalado con Rust si usaste los instaladores
oficiales que se discuten en la sección “Installation”. Si instalaste Rust a través de algunos
otros medios, verifica si Cargo está instalado ingresando lo siguiente en tu terminal:
$ cargo --version
Si ves un número de versión, ¡lo 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.
Vamos a crear un nuevo proyecto usando Cargo y ver cómo difiere de nuestro proyecto
original “¡Hola, mundo!”. Navega de vuelta a tu directorio proyectos (o dondequiera que
hayas decidido almacenar tu código). Luego, en cualquier sistema operativo, ejecuta lo
siguiente:
Ve al directorio hello_cargo y lista los archivos. Verás que Cargo ha generado dos archivos y
un directorio para nosotros: un archivo Cargo.toml y un directorio src con un archivo main.rs
dentro.
https://book.rustlang-es.org/print 16/656
12/7/24, 22:37 El Lenguaje de Programación Rust
También ha inicializado un nuevo repositorio Git junto con un archivo .gitignore. Los archivos
Git no se generarán si ejecutas cargo new dentro de un repositorio Git existente; puedes
anular este comportamiento usando cargo new --vcs=git .
Nota: Git es un sistema de control de versiones común. Puedes cambiar cargo new
para usar un sistema de control de versiones diferente o ningún sistema de control de
versiones usando la bandera --vcs . Ejecuta cargo new --help para ver las opciones
disponibles.
Abre Cargo.toml en tu editor de texto de elección. Debería verse similar al código del Listado
1-2.
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
[dependencies]
Este archivo está en el formato TOML (Tom’s Obvious, Minimal Language), que es el formato
de configuración de Cargo.
La primera línea, [package] , es un encabezado de sección que indica que las siguientes
declaraciones están configurando un paquete. A medida que agreguemos más información
a este archivo, agregaremos otras secciones.
Las próximas tres líneas establecen la información de configuración que Cargo necesita
para compilar tu programa: el nombre, la versión y la edición de Rust que se usará.
Hablaremos sobre la entrada edition en Apéndice E
https://book.rustlang-es.org/print 17/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
println!("Hello, world!");
}
¡Cargo ha generado un programa “Hello, world!”/“¡Hola, mundo!” para ti, ¡igual que el que
escribimos enl Listado 1-1! Hasta ahora, las diferencias entre nuestro proyecto y el proyecto
generado por 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.
Cargo espera que tus archivos de origen vivan dentro del directorio src. El directorio del
proyecto de nivel superior es solo para archivos README, información de licencia, archivos
de configuración y cualquier otra cosa que no esté relacionada con tu código. Usar Cargo te
ayuda a organizar tus proyectos. Hay un lugar para todo, y todo está en su lugar.
Si comenzaste un proyecto que no usa Cargo, como hicimos con el proyecto “¡Hola,
mundo!”, puedes convertirlo en un proyecto que sí use Cargo. Mueve el código del proyecto
al directorio src y crea un archivo Cargo.toml adecuado.
$ cargo build
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs
Si todo va bien, Hello, world! debería imprimirse en la terminal. Ejecutar cargo build
por primera vez también hace que Cargo cree un nuevo archivo en el nivel superior:
Cargo.lock. Este archivo rastrea las versiones exactas de las dependencias de tu proyecto.
Este proyecto no tiene dependencias, por lo que el archivo es un poco escaso. Nunca
necesitarás cambiar este archivo manualmente; Cargo administra su contenido para ti.
https://book.rustlang-es.org/print 18/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/hello_cargo`
Hello, world!
Usar cargo run es más conveniente que tener que recordar ejecutar cargo build y luego
usar la ruta completa al binario, por lo que la mayoría de los desarrolladores usan cargo
run .
Ten en cuenta que esta vez no vimos salida que indicara que Cargo estaba compilando
hello_cargo . Cargo supo que los archivos no habían cambiado, por lo que no volvió a
construir, sino que solo ejecutó el binario. Si hubieras modificado tu código fuente, Cargo
habría reconstruido el proyecto antes de ejecutarlo, y habrías visto esta salida:
$ 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 también proporciona un comando llamado cargo check . Este comando comprueba
rápidamente tu código para asegurarse de que compila, pero no produce un ejecutable:
$ cargo check
Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
¿Por qué no querrías un ejecutable? A menudo, cargo check es mucho más rápido que
cargo build porque omite el paso de producir un ejecutable. Si estás verificando
continuamente tu trabajo mientras escribes el código, usar cargo check acelerará el
proceso de informarte si tu proyecto todavía aún está compilando. ¡Por lo tanto, muchos
Rustaceans ejecutan cargo check periódicamente mientras escriben su programa para
asegurarse de que compila! Luego ejecutan cargo build cuando están listos para usar el
ejecutable.
Una ventaja adicional de usar Cargo es que los comandos son los mismos sin importar en
qué sistema operativo estés trabajando. Por lo tanto, en este punto, ya no
https://book.rustlang-es.org/print 19/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cuando tu proyecto finalmente esté listo para su lanzamiento, puedes usar cargo build --
release para compilarlo con optimizaciones. Este comando creará un ejecutable en
target/release en lugar de target/debug. Las optimizaciones hacen que tu código Rust se
ejecute más rápido, pero al activarlos se alarga el tiempo que tarda tu programa en
compilarse. Es por eso que hay dos perfiles diferentes: uno para el desarrollo, cuando
deseas reconstruir rápidamente y con frecuencia, y otro para construir el programa final
que le darás al usuario, que no se reconstruirá repetidamente y que se ejecutará lo más
rápido posible. Si estás midiendo el tiempo de ejecución de tu código, asegúrate de ejecutar
cargo build --release y realizar la prueba de rendimiento con el ejecutable en
target/release.
Con proyectos simples, Cargo no proporciona mucho valor por sobre sólo usar rustc , pero
demostrará su valor a medida que tus programas se vuelvan más intrincados. Una vez que
los programas crecen a múltiples archivos o necesitan una dependencia, es mucho más fácil
dejar que Cargo coordine la construcción.
Aunque el proyecto hello_cargo es simple, ahora usas muchas de las herramientas reales
que usarás en el resto de tu carrera en Rust. De hecho, para trabajar en cualquier proyecto
existente, puedes usar los siguientes comandos para verificar el código usando Git, cambiar
al directorio del proyecto y construir:
Resumen
¡Ya estás en un gran comienzo en tu viaje de Rust! En este capítulo, has aprendido cómo:
Es un buen momento para construir un programa más sustancial para acostumbrarse a leer
y escribir código Rust. Entonces, en el capítulo 2, construiremos un programa de juego de
adivinanzas. Si prefieres comenzar aprendiendo cómo funcionan los conceptos de
programación comunes en Rust, consulta el capítulo 3 y luego regresa al capítulo 2.
https://book.rustlang-es.org/print 21/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El primer comando, cargo new , toma el nombre del proyecto ( guessing_game ) como el
primer argumento. El segundo comando cambia al directorio del nuevo proyecto.
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
[dependencies]
Como viste en el Capítulo 1, cargo new genera un programa “Hola, mundo!” para ti. Mira el
archivo src/main.rs:
https://book.rustlang-es.org/print 22/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
println!("Hello, world!");
}
Ahora compilemos este programa “Hola, mundo!” y ejecutémoslo en el mismo paso usando
el comando cargo run :
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Hello, world!
use std::io;
fn main() {
println!("Guess the number!");
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
Lista 2-1: Código que obtiene una adivinanza del usuario y la imprime
Este código contiene mucha información, así que repasémoslo línea por línea. Para obtener
la entrada del usuario y luego imprimir el resultado como salida, necesitamos traer la
https://book.rustlang-es.org/print 23/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::io;
Por defecto, Rust tiene un conjunto de elementos definidos en la biblioteca estándar que
trae al alcance de cada programa. Este conjunto se llama prelude, y puedes ver todo lo que
contiene en la documentación de la biblioteca estándar.
Si un tipo que quieres usar no está en el prelude, tienes que traer ese tipo al alcance
explícitamente con una declaración use . Usar la biblioteca std::io te proporciona una
serie de características útiles, incluyendo la capacidad de aceptar la entrada del usuario.
fn main() {
La sintaxis fn declara una nueva función; los paréntesis, () , indican que no hay
parámetros; y la llave, { , inicia el cuerpo de la función.
Como también aprendiste en el Capítulo 1, println! es una macro que imprime una
cadena en la pantalla:
Este código está imprimiendo una solicitud que indica qué es el juego y está solicitando la
entrada del usuario.
A continuación, crearemos una variable para almacenar la entrada del usuario, como esto:
¡Ahora el programa está interesante! Hay mucho que está pasando en esta pequeña línea.
Usamos la declaración let para crear la variable. Aquí hay otro ejemplo:
let apples = 5;
Esta línea crea una nueva variable llamada apples y la enlaza con el valor 5. En Rust, las
variables son inmutables por defecto, lo que significa que una vez que le damos a la variable
un valor, el valor no cambiará. Vamos a discutir este concepto en detalle en la sección
https://book.rustlang-es.org/print 24/656
12/7/24, 22:37 El Lenguaje de Programación Rust
“Variables y Mutabilidad” del Capítulo 3. Para hacer una variable mutable, agregamos mut
antes del nombre de la variable:
Nota: La sintaxis // inicia un comentario que continúa hasta el final de la línea. Rust
ignora todo lo que está en los comentarios. Vamos a discutir los comentarios en más
detalle en el Capítulo 3.
Regresando al programa del juego de adivinanzas, ahora sabes que let mut guess
introducirá una variable mutable llamada guess . El signo igual ( = ) le dice a Rust que
queremos enlazar algo a la variable ahora. A la derecha del signo igual está el valor al que
guess está enlazado, que es el resultado de llamar a String::new , una función que
devuelve una nueva instancia de un String . String es un tipo de cadena proporcionado
por la biblioteca estándar que es una parte de texto codificada en UTF-8 que puede crecer.
La sintaxis :: en la línea ::new indica que new es una función asociada del tipo String .
Una función asociada es una función que está implementada en un tipo, en este caso
String . Esta función new crea una nueva cadena vacía. Encontrarás una función new en
muchos tipos porque es un nombre común para una función que crea un nuevo valor de
algún tipo.
En total, la línea let mut guess = String::new(); ha creado una variable mutable que
está actualmente enlazada a una nueva instancia vacía de un String . ¡Uf!
io::stdin()
.read_line(&mut guess)
entrada del usuario. El trabajo completo de read_line es tomar lo que el usuario escribe
en la entrada estándar y agregar eso a una cadena (sin sobrescribir su contenido), por lo
que, por lo tanto, pasamos esa cadena como argumento. La cadena de argumentos debe
ser mutable para que el método pueda cambiar el contenido de la cadena.
El & indica que este argumento es una referencia, que te da una forma de permitir que
varias partes de tu código accedan a una pieza de datos sin necesidad de copiar esos datos
en la memoria varias veces. Las referencias son una característica compleja, y una de las
principales ventajas de Rust es lo seguro y fácil que es usar referencias. No necesitas saber
mucho de esos detalles para terminar este programa. Por ahora, todo lo que necesitas
saber es que, como las variables, las referencias son inmutables por defecto. Por lo tanto,
necesitas escribir &mut guess en lugar de &guess para hacerlo mutable. (El capítulo 4
explicará las referencias con más detalle.)
Todavía estamos trabajando en esta línea de código. Ahora estamos discutiendo una tercera
línea de texto, pero ten en cuenta que aún es parte de una sola línea lógica de código. La
siguiente parte es este método:
Sin embargo, una línea larga es difícil de leer, por lo que es mejor dividirla. A menudo es
sabio introducir un salto de línea y otros espacios en blanco para ayudar a dividir líneas
largas cuando llamas a un método con la sintaxis .method_name() . Ahora discutamos lo
que hace esta línea.
El Capítulo 6 cubrirá las enumeraciones con más detalles. El propósito de estos tipos
Result es codificar información de manejo de errores.
Las variantes de Result son Ok y Err . La variante Ok indica que la operación fue exitosa,
y dentro de Ok está el valor generado con éxito. La variante Err significa que la operación
falló, y Err contiene información sobre cómo o por qué la operación falló.
Los valores del tipo Result , como los valores de cualquier tipo, tienen métodos definidos
en ellos. Una instancia de Result tiene un método expect que puedes llamar. Si esta
https://book.rustlang-es.org/print 26/656
12/7/24, 22:37 El Lenguaje de Programación Rust
instancia de Result es un valor Err , expect hará que el programa se bloquee y muestre
el mensaje que pasaste como argumento a expect . Si el método read_line devuelve un
Err , probablemente sea el resultado de un error proveniente del sistema operativo
subyacente. Si esta instancia de Result es un valor Ok , expect tomará el valor de retorno
que Ok está sosteniendo y devolverá solo ese valor para que lo puedas usar. En este caso,
ese valor es el número de bytes en la entrada del usuario.
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
10 | let _ = io::stdin().read_line(&mut guess);
| +++++++
Rust advierte que no has usado el valor Result devuelto por read_line , indicando que el
programa no ha manejado un posible error.
Además del corchete de cierre, solo hay una línea más que 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: piensa en {} como pequeñas pinzas de cangrejo
que mantienen un valor en su lugar. Al imprimir el valor de una variable, el nombre de la
variable puede ir dentro de las llaves curvas. Al imprimir el resultado de evaluar una
expresión, coloca llaves curvas vacías en la cadena de formato, luego sigue la cadena de
formato con una lista separada por comas de expresiones para imprimir en cada marcador
https://book.rustlang-es.org/print 27/656
12/7/24, 22:37 El Lenguaje de Programación Rust
de posición vacío de llaves curvas en el mismo orden. Imprimir una variable y el resultado
de una expresión en una llamada a println! se vería así:
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 [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
En este punto, la primera parte del juego está terminada: estamos obteniendo entrada del
teclado y luego la imprimimos.
Recuerda que un crate es una colección de archivos de código fuente de Rust. El proyecto
que hemos estado construyendo es un binary crate, que es un ejecutable. El crate rand es
un library crate, que contiene código que se pretende usar en otros programas y no se
puede ejecutar por sí solo.
https://book.rustlang-es.org/print 28/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La coordinación de los crates externos de Cargo es donde realmente brilla Cargo. Antes de
poder escribir código que use rand , necesitamos modificar el archivo Cargo.toml para
incluir el crate rand como una dependencia. Abre ese archivo ahora y agrega la siguiente
línea al final, debajo del encabezado de la sección [dependencies] que Cargo creó para ti.
Asegúrate de especificar rand exactamente como lo tenemos aquí, con este número de
versión, o los ejemplos de código en este tutorial pueden no funcionar:
[dependencies]
rand = "0.8.5"
En el archivo Cargo.toml, todo lo que sigue a un encabezado es parte de esa sección que
continúa hasta que comienza otra sección. En [dependencies] le dices a Cargo qué crates
externos depende tu proyecto y qué versiones de esos crates requieres. En este caso,
especificamos el crate rand con el especificador de versión semántica 0.8.5 . Cargo
entiende Semantic Versioning (a veces llamado SemVer), que es un estándar para escribir
números de versión. El especificador 0.8.5 es realmente un atajo para ^0.8.5 , lo que
significa cualquier versión que sea al menos 0.8.5 pero inferior a 0.9.0.
Cargo considera que estas versiones tienen APIs públicas compatibles con la versión 0.8.5, y
esta especificación asegura que obtendrá la última versión de corrección que aún se
compilará con el código de este capítulo. Cualquier versión 0.9.0 o superior no está
garantizada de tener la misma API que lo que usarán los siguientes ejemplos.
Ahora, sin cambiar ningún código, construyamos el proyecto, como se muestra en el Listado
2-2.
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
Downloaded libc v0.2.127
Downloaded getrandom v0.2.7
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.16
Downloaded rand_chacha v0.3.1
Downloaded rand_core v0.6.3
Compiling libc v0.2.127
Compiling getrandom v0.2.7
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Listado 2-2: La salida de ejecutar cargo build después de agregar el crate rand como una dependencia
https://book.rustlang-es.org/print 29/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Es posible que veas números de versión diferentes (¡pero todos serán compatibles con el
código, gracias a SemVer!) y líneas diferentes (dependiendo del sistema operativo), y las
líneas pueden estar en un orden diferente.
Cuando incluimos una dependencia externa, Cargo obtiene las últimas versiones de todo lo
que la dependencia necesita del registro, que es una copia de datos de Crates.io. Crates.io es
donde las personas en el ecosistema de Rust publican sus proyectos de Rust de código
abierto para que otros los utilicen.
Si ejecuta cargo build nuevamente sin hacer ningún cambio, no obtendrá ninguna salida
aparte de la línea Finished . Cargo sabe que ya ha descargado y compilado las
dependencias, y no ha cambiado nada sobre ellas en su archivo Cargo.toml. Cargo también
sabe que no ha cambiado nada sobre su código, por lo que tampoco lo vuelve a compilar.
Sin nada que hacer, simplemente sale.
Si abre el archivo src/main.rs, realiza un cambio trivial y luego lo guarda y vuelve a construir,
solo verá dos líneas de salida:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
Estas líneas muestran que Cargo solo actualiza la compilación con su pequeño cambio en el
archivo src/main.rs. Sus dependencias no han cambiado, por lo que Cargo sabe que puede
reutilizar lo que ya ha descargado y compilado para esas.
Cargo tiene un mecanismo que le garantiza que puede reconstruir el mismo artefacto cada
vez que usted o cualquier otra persona construye su código: Cargo solo usará las versiones
de las dependencias que haya especificado hasta que indique lo contrario. Por ejemplo,
digamos que la semana que viene sale la versión 0.8.6 del crate rand , y que esa versión
contiene una corrección de error importante, pero también contiene una regresión que
romperá su código. Para manejar esto, Rust crea el archivo Cargo.lock la primera vez que
ejecuta cargo build , por lo que ahora tenemos esto en el directorio guessing_game
Cuando construye un proyecto por primera vez, Cargo determina todas las versiones de las
dependencias que cumplen con los criterios y luego las escribe en el archivo Cargo.lock.
Cuando construye su proyecto en el futuro, Cargo verá que el archivo Cargo.lock existe y
usará las versiones especificadas allí en lugar de hacer todo el trabajo de averiguar las
https://book.rustlang-es.org/print 30/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cuando quiera actualizar un crate, Cargo proporciona el comando update , que ignorará el
archivo Cargo.lock y determinará todas las últimas versiones que cumplan con sus
especificaciones en Cargo.toml. Cargo luego escribirá esas versiones en el archivo Cargo.lock.
En este caso, Cargo solo buscará versiones mayores que 0.8.5 y menores que 0.9.0. Si el
crate rand ha lanzado las dos nuevas versiones 0.8.6 y 0.9.0, vería lo siguiente si ejecutara
cargo update :
$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
Cargo ignora el lanzamiento 0.9.0. En este punto, también notaría un cambio en su archivo
Cargo.lock que indica que la versión del crate rand que ahora está usando es 0.8.6. Para
usar la versión 0.9.0 o cualquier versión en la serie 0.9.x, tendría que actualizar el archivo
Cargo.toml para que se vea así:
[dependencies]
rand = "0.9.0"
La próxima vez que ejecute cargo build , Cargo actualizará el registro de crates disponibles
y volverá a evaluar sus requisitos de rand de acuerdo con la nueva versión que ha
especificado.
Hay mucho más que decir sobre Cargo y su ecosistema, que discutiremos en el Capítulo 14,
pero por ahora, eso es todo lo que necesita saber. Cargo hace muy fácil reutilizar
bibliotecas, por lo que los Rustaceans pueden escribir proyectos más pequeños que se
ensamblan a partir de un número de paquetes.
Comencemos a usar rand para generar un número para adivinar. El siguiente paso es
actualizar src/main.rs, como se muestra en el Listado 2-3.
https://book.rustlang-es.org/print 31/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
Primero agregamos la línea use rand::Rng; . El trait Rng define métodos que los
generadores de números aleatorios implementan, y este trait debe estar en el alcance para
que podamos usar esos métodos. El Capítulo 10 cubrirá los traits en detalle.
Nota: No sabrá solo qué traits usar y qué métodos y funciones llamar desde un crate,
por lo que cada crate tiene documentación con instrucciones para usarlo. Otra
característica interesante de Cargo es que ejecutar el comando cargo doc --open
construirá la documentación proporcionada por todas sus dependencias localmente y
la abrirá en su navegador. Si está interesado en otra funcionalidad en el crate rand ,
por ejemplo, ejecute cargo doc --open y haga clic en rand en la barra lateral a la
izquierda.
La segunda línea nueva imprime el número secreto. Esto es útil mientras desarrollamos el
programa para poder probarlo, pero lo eliminaremos de la versión final. ¡No es mucho un
juego si el programa imprime la respuesta tan pronto como comienza!
https://book.rustlang-es.org/print 32/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
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 [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
Debería obtener números aleatorios diferentes, y todos deberían ser números entre 1 y
100. ¡Gran trabajo!
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
https://book.rustlang-es.org/print 33/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Primero agregamos otra declaración use , que trae un tipo llamado std::cmp::Ordering al
alcance de la biblioteca estándar. El tipo Ordering es otro enum y tiene las variantes Less ,
Greater y Equal . Estos son los tres resultados posibles cuando compara dos valores.
Luego agregamos cinco nuevas líneas al final que usan el tipo Ordering . El método cmp
compara dos valores y se puede llamar en cualquier cosa que se pueda comparar. Toma
una referencia a lo que quiera comparar: aquí está comparando guess con
secret_number . Luego devuelve una variante del enum Ordering que importamos al
alcance con la declaración use . Usamos una expresión match para decidir qué hacer a
continuación basándonos en qué variante de Ordering se devolvió de la llamada a cmp con
los valores en guess y secret_number .
Una expresión match está compuesta por brazos. Un brazo consta de un patrón para
coincidir y el código que se debe ejecutar si el valor dado a match se ajusta al patrón del
brazo. Rust toma el valor dado a match y busca cada patrón de brazo en orden. Los
patrones y la construcción match son potentes características de Rust: le permiten expresar
una variedad de situaciones que su código puede encontrar y se aseguran de que los
maneje todos. Estas características se cubrirán en detalle en el Capítulo 6 y el Capítulo 18,
respectivamente.
Vamos a repasar un ejemplo con la expresión match que usamos aquí. Digamos que el
usuario ha adivinado 50 y el número secreto generado aleatoriamente esta vez es 38.
Cuando el código compara 50 con 38, el método cmp devolverá Ordering::Greater porque
50 es mayor que 38. La expresión match obtiene el valor Ordering::Greater y comienza a
verificar el patrón de cada brazo. Mira el patrón del primer brazo, Ordering::Less , y ve que
el valor Ordering::Greater no coincide con Ordering::Less , ¡así que ignora el código en
ese brazo y se mueve al siguiente brazo! El patrón del siguiente brazo es
Ordering::Greater , ¡que sí coincide con Ordering::Greater ! El código asociado en ese
brazo se ejecutará y mostrará Too big! en la pantalla. La expresión match termina
después de la primera coincidencia exitosa, ¡así que no mirará el último brazo en este
escenario.
Sin embargo, el código del Listado 2-4 aún no se compilará. Vamos a intentarlo:
https://book.rustlang-es.org/print 34/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
| |
| arguments to this method are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: method defined here
-->
/rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/cmp.rs:836:8
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1
previous error
El núcleo del error indica que hay tipos no coincidentes. Rust tiene un sistema de tipos fuerte
y estático. Sin embargo, también tiene inferencia de tipo. Cuando escribimos let mut guess
= String::new() , Rust pudo inferir que guess debería ser un String y no nos obligó a
escribir el tipo. El secret_number , por otro lado, es un tipo de número. Algunos de los tipos
de números de Rust pueden tener un valor entre 1 y 100: i32 , un número de 32 bits; u32 ,
un número sin signo de 32 bits; i64 , un número de 64 bits; así como otros. A menos que se
especifique lo contrario, Rust predetermina un i32 , que es el tipo de secret_number a
menos que agregue información de tipo en otro lugar que haga que Rust infiera un tipo
numérico diferente. La razón del error es que Rust no puede comparar una cadena y un tipo
numérico.
Finalmente, queremos convertir la String que el programa lee como entrada en un tipo de
número real para que podamos compararlo numéricamente con el número secreto. Lo
hacemos agregando esta línea al cuerpo de la función main :
https://book.rustlang-es.org/print 35/656
12/7/24, 22:37 El Lenguaje de Programación Rust
// --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 línea es:
Creamos una variable llamada guess . Pero espera, ¿no tiene el programa ya una variable
llamada guess ? Lo hace, pero Rust nos permite redefinir el valor anterior de guess con
uno nuevo. Este concepto en Rust se le conoce como Shadowing, nos permite volver a usar
el nombre de la variable guess en lugar de obligarnos a crear dos variables únicas, como
guess_str y guess , por ejemplo. Lo cubriremos con más detalle en el Capítulo 3, pero por
ahora, sé que esta característica se usa a menudo cuando desea convertir un valor de un
tipo a otro tipo.
El método parse en las cadenas convierte una cadena en otro tipo. Aquí, lo usamos para
convertir de una cadena a un número. Debemos decirle a Rust el tipo de número exacto
que queremos usando let guess: u32 . Los dos puntos ( : ) después de guess le dicen a
Rust que anotaremos el tipo de variable. Rust tiene algunos tipos de número integrados; el
u32 visto aquí es un entero sin signo de 32 bits. Es una buena opción predeterminada para
un número positivo pequeño. Aprenderá sobre otros tipos de números en el Capítulo 3.
https://book.rustlang-es.org/print 36/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
¡Bien! Aunque se agregaron espacios antes de la adivinanza, el programa aún sabía que el
usuario adivinó 76. Ejecute el programa varias veces para verificar el comportamiento
diferente con diferentes tipos de entrada: adivine el número correctamente, adivine un
número que sea demasiado alto y adivine un número que sea demasiado bajo.
Tenemos la mayoría del juego funcionando ahora, pero el usuario solo puede adivinar una
vez. ¡Cambiamos eso agregando un bucle!
Filename: src/main.rs
https://book.rustlang-es.org/print 37/656
12/7/24, 22:37 El Lenguaje de Programación Rust
// --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!"),
}
}
}
Como puede ver, hemos movido todo desde la solicitud de entrada de adivinanzas hacia
adelante en un bucle. Asegúrese de indentar las líneas dentro del bucle otras cuatro veces y
ejecute el programa nuevamente. ¡El programa ahora pedirá otra adivinanza para siempre,
lo que introduce un nuevo problema! ¡Parece que el usuario no puede salir!
El usuario siempre podría interrumpir el programa usando el atajo de teclado ctrl-c. Pero
hay otra forma de escapar de este monstruo insaciable, como se mencionó en la discusión
de parse en “Comparando la adivinanza con el número secreto”: si el usuario ingresa una
respuesta que no es un número, el programa se bloqueará. Podemos aprovechar eso para
permitir que el usuario salga, como se muestra aquí:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind:
InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
https://book.rustlang-es.org/print 38/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Al escribir quit se cerrará el juego, pero como notará, también lo hará al ingresar cualquier
otra entrada que no sea un número. Esto es lo menos óptimo, para decir lo menos;
queremos que el juego también se detenga cuando se adivine el número correcto.
Programemos el juego para que se cierre cuando el usuario gane agregando una
declaración break :
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Agregando la línea break después de You win! hace que el programa salga del bucle
cuando el usuario adivina el número secreto correctamente. Salir del bucle también significa
salir del programa, porque el bucle es la última parte de main .
Para mejorar aún más el comportamiento del juego, en lugar de bloquear el programa
cuando el usuario ingresa un número no válido, hagamos que el juego ignore un número no
válido para que el usuario pueda seguir adivinando. Podemos hacer eso alterando la línea
donde guess se convierte de un String a un u32 , como se muestra en el Listado 2-5.
https://book.rustlang-es.org/print 39/656
12/7/24, 22:37 El Lenguaje de Programación Rust
// --snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
// --snip--
Listado 2-5: Ignorar una adivinanza que no es un número y pedir otra adivinanza en lugar de bloquear el
programa
Cambiamos de una llamada expect a una expresión match para pasar de bloquear el
programa en un error a manejar el error. Recuerde que parse devuelve un tipo Result y
Result es un enum que tiene las variantes Ok y Err . Aquí estamos usando una expresión
match , como hicimos con el resultado Ordering del método cmp .
https://book.rustlang-es.org/print 40/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 4.45s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
¡Genial! Con un pequeño ajuste final, terminaremos el juego de adivinanzas. Recuerde que
el programa todavía está imprimiendo el número secreto. Eso funcionó bien para las
pruebas, pero arruina el juego. Vamos a eliminar el println! que muestra el número
secreto. El listado 2-6 muestra el código final.
https://book.rustlang-es.org/print 41/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use rand::Rng;
use std::cmp::Ordering;
use std::io;
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;
}
}
}
}
Resumen
Este proyecto fue una manera práctica de introducirle a muchos nuevos conceptos de Rust:
let , match , funciones, el uso de paquetes externos, y más. En los próximos capítulos,
aprenderá sobre estos conceptos en más detalle. El capítulo 3 cubre conceptos que la
mayoría de los lenguajes de programación tienen, como variables, tipos de datos y
funciones, y muestra cómo usarlos en Rust. El capítulo 4 explora la propiedad, una
característica que hace que Rust sea diferente de otros lenguajes. El capítulo 5 discute las
estructuras y la sintaxis de los métodos, y el capítulo 6 explica cómo funcionan los enums.
https://book.rustlang-es.org/print 42/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Palabras clave
El lenguaje Rust tiene un conjunto de palabras clave que están reservadas para su uso
exclusivo por el lenguaje, 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 varias tareas en
sus programas de Rust; algunas no tienen ninguna funcionalidad actual asociada con
ellas, pero se han reservado para funcionalidad que podría agregarse a Rust en el
futuro. Puede encontrar una lista de las palabras clave en Apéndice A.
https://book.rustlang-es.org/print 43/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Variables y Mutabilidad
Como se mencionó en la sección “Almacenando valores con variables” , por defecto, las
variables son inmutables. Este es uno de los muchos empujes que Rust le da para que
escriba su código de una manera que aproveche la seguridad y la fácil concurrencia que
ofrece Rust. Sin embargo, todavía tiene la opción de hacer sus variables mutables.
Exploremos cómo y por qué Rust le anima a favorecer la inmutabilidad y por qué a veces
podría querer optar por no hacerlo.
Cuando una variable es inmutable, una vez que un valor está vinculado a un nombre, no
puede cambiar ese valor. Para ilustrar esto, genere un nuevo proyecto llamado variables en
su directorio 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}");
}
Guarde y ejecute el programa usando cargo run . Debería recibir un mensaje de error
relacionado con un error de inmutabilidad, como se muestra en esta salida:
$ 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`
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
Este ejemplo muestra cómo el compilador le ayuda a encontrar errores en sus programas.
Los errores de compilación pueden ser frustrantes, pero realmente solo significa que su
https://book.rustlang-es.org/print 44/656
12/7/24, 22:37 El Lenguaje de Programación Rust
programa aún no está realizando de manera segura lo que desea que haga; no significa que
no es un buen programador! Los Rustaceans experimentados aún reciben errores de
compilación.
Recibió el mensaje de error cannot assign twice to immutable variable `x` porque
intentó asignar un segundo valor a la variable inmutable x .
Pero la mutabilidad puede ser muy útil y puede hacer que el código sea más conveniente de
escribir. Aunque las variables son inmutables por defecto, puede hacerlas mutables
agregando mut delante del nombre de la variable como lo hizo en el Capitulo 2. Agregando
mut también comunica la intención a los lectores futuros 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
https://book.rustlang-es.org/print 45/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Constantes
Al igual que las variables inmutables, las constantes son valores que están vinculados a un
nombre y no se les permite cambiar, pero hay algunas diferencias entre las constantes y las
variables.
Primero, no se le permite usar mut con constantes. Las constantes no son solo inmutables
por defecto, siempre son inmutables. Declara constantes usando la palabra clave const en
lugar de la palabra clave let , y el tipo del valor debe estar anotado. Cubriremos los tipos y
las anotaciones de tipo en la siguiente sección, “Tipos de datos”, por lo que no se preocupe
por los detalles ahora. Solo sepa que siempre debe anotar el tipo.
Las constantes se pueden declarar en cualquier ámbito, incluido el ámbito global, lo que las
hace útiles para valores que muchas partes del código necesitan conocer.
La última diferencia es que las constantes solo se pueden establecer en una expresión
constante, no en el resultado de un valor que solo se podría calcular en tiempo de
ejecución.
Las constantes son válidas durante todo el tiempo que se ejecuta un programa, dentro del
ámbito en el que se declararon. Esta propiedad hace que las constantes sean útiles para los
valores en el dominio de su aplicación que varias partes del programa podrían necesitar
conocer, como el número máximo de puntos que cualquier jugador de un juego puede
obtener o la velocidad de la luz.
https://book.rustlang-es.org/print 46/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Shadowing
Como vio en el tutorial del juego de adivinanzas en Capítulo 2, puede declarar una nueva
variable con el mismo nombre que una variable anterior. Los Rustaceans dicen que la
primera variable es ocultada por la segunda, lo que significa que la segunda variable es lo
que el compilador verá cuando use el nombre de la variable. En efecto, la segunda variable
oculta la primera, tomando cualquier uso del nombre de la variable para sí misma hasta que
se haga shadowing sobre la misma variable o el ámbito finalice. Podemos ocultar una
variable usando el mismo nombre de variable y repitiendo el uso de la palabra clave let 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}");
}
Este programa primero vincula a x el valor de 5 . Luego crea una nueva variable x
repitiendo let x = , tomando el valor original y agregando 1 para que el valor de x sea
entonces 6 . Luego, dentro de un ámbito interno creado con las llaves, la tercera
declaración let también proyecta x y crea una nueva variable, multiplicando el valor
anterior por 2 para darle a x un valor de 12 . Cuando ese ámbito finaliza, la proyección
interna finaliza y x vuelve a ser 6 . Cuando ejecutamos este programa, se mostrará lo
siguiente:
$ 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
El Shadowing es diferente de marcar una variable como mut porque obtendremos un error
de tiempo de compilación si accidentalmente intentamos volver a asignar esta variable sin
usar la palabra clave let . Al usar let , podemos realizar algunas transformaciones en un
valor, pero la variable debe ser inmutable después de que se hayan completado esas
transformaciones.
https://book.rustlang-es.org/print 47/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La otra diferencia entre mut y el shadowing es que, debido a que efectivamente estamos
creando una nueva variable cuando usamos la palabra clave let nuevamente, podemos
cambiar el tipo de valor pero reutilizar el mismo nombre. Por ejemplo, digamos que nuestro
programa le pide al usuario que muestre cuántos espacios desea entre algún texto
ingresando caracteres de espacio, y luego queremos almacenar esa entrada como un
número:
$ 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.
https://book.rustlang-es.org/print 48/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Tipos de datos
Cada valor en Rust es de un cierto tipo de dato, que le dice a Rust qué tipo de dato se está
especificando para que sepa cómo trabajar con ese dato. Veremos dos subconjuntos de
tipos de datos: escalares y compuestos.
Tenga en cuenta que Rust es un lenguaje estáticamente tipado, lo que significa que debe
conocer los tipos de todas las variables en tiempo de compilación. El compilador
generalmente puede inferir qué tipo queremos usar en función del valor y cómo lo usamos.
En los casos en que muchos tipos son posibles, como cuando convertimos un String en un
tipo numérico usando parse en la sección “Comparando la Adivinanza con el Número
Secreto” del capítulo 2, debemos agregar una anotación de tipo, como esta:
$ 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 solo valor. Rust tiene cuatro tipos escalares principales:
enteros, números de punto flotante, booleanos y caracteres. Puede reconocerlos de otros
lenguajes de programación. Vamos a ver cómo funcionan en Rust.
https://book.rustlang-es.org/print 49/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Tipos de Enteros
Cada variante puede ser signed (con signo) o unsigned (sin signo) y tiene un tamaño explícito.
Signed y unsigned se refieren a si es posible que el número sea negativo, es decir, si el
número necesita tener un signo con él (signed) o si solo será positivo y por lo tanto puede
representarse sin signo (unsigned). Es como escribir números en papel: cuando el signo
importa, un número se muestra con un signo más o un signo menos; sin embargo, cuando
es seguro suponer que el número es positivo, se muestra sin signo. Los números con signo
se almacenan usando la representación de complemento a dos.
Puede escribir literales enteros en cualquiera de las formas que se muestran en la Tabla 3-2.
Tenga en cuenta que los literales numéricos que pueden ser múltiples tipos numéricos
permiten un sufijo de tipo, como 57u8 , para designar el tipo. Los literales numéricos
también pueden usar _ como un separador visual para facilitar la lectura del número,
como 1_000 , que tendrá el mismo valor que si hubiera especificado 1000 .
https://book.rustlang-es.org/print 50/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Hex 0xff
Octal 0o77
Binario 0b1111_0000
Entonces, ¿cómo sabe qué tipo de entero usar? Si no está seguro, los valores
predeterminados de Rust son generalmente buenos lugares para comenzar: los tipos
enteros se configuran predeterminadamente en i32 . La situación principal en la que usaría
isize o usize es cuando indexa algún tipo de colección.
Desbordamiento de enteros
Digamos que tiene una variable de tipo u8 que puede contener valores entre 0 y 255.
Si intenta cambiar la variable a un valor fuera de ese rango, como 256, se producirá un
desbordamiento de enteros, que puede resultar en uno de dos comportamientos.
Cuando está compilando en modo de depuración, Rust incluye comprobaciones para
el desbordamiento de enteros que hacen que su programa se desborde en tiempo de
ejecución si ocurre este comportamiento. Rust usa el término desbordamiento cuando
un programa sale con un error; discutiremos los desbordamientos con más
profundidad en la sección “Errores irrecuperables con panic! ” del Capítulo 9.
Envolver en todos los modos con los métodos wrapping_* , como wrapping_add .
Devolver el valor None si hay desbordamiento con los métodos checked_* .
Devolver el valor y un booleano que indica si hubo desbordamiento con los
métodos overflowing_* .
https://book.rustlang-es.org/print 51/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Saturar en los valores mínimos o máximos del valor con los métodos
saturating_* .
Rust también tiene dos tipos primitivos para números de punto flotante, que son números
con puntos decimales. Los tipos de punto flotante de Rust son f32 y f64 , que tienen 32
bits y 64 bits de tamaño, respectivamente. El tipo predeterminado es f64 porque en CPUs
modernas, es aproximadamente la misma velocidad que f32 pero es capaz de más
precisión. Todos los tipos de punto flotante son con signo.
fn main() {
let x = 2.0; // f64
Los números de punto flotante se representan de acuerdo con el estándar IEEE-754. El tipo
f32 es un punto flotante de precisión simple, y f64 tiene doble precisión.
Operaciones numéricas
Rust admite las operaciones matemáticas básicas que esperaría para todos los tipos de
números: adición, sustracción, multiplicación, división y resto. La división entera se trunca
hacia cero al entero más cercano. El siguiente código muestra cómo usaría cada operación
numérica en una declaración let :
https://book.rustlang-es.org/print 52/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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
Como en la mayoría de los otros lenguajes de programación, un tipo booleano en Rust tiene
dos posibles valores: true y false . Los booleanos tienen un byte de tamaño. El tipo
booleano en Rust se especifica usando bool . Por ejemplo:
fn main() {
let t = true;
El tipo de carácter
El tipo char de Rust es el tipo alfabético más primitivo del lenguaje. Estos son algunos
ejemplos de declaración de valores char :
https://book.rustlang-es.org/print 53/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}
Tenga en cuenta que especificamos literales char con comillas simples, en oposición a
literales de cadena, que usan comillas dobles. El tipo char de Rust tiene un tamaño de
cuatro bytes y representa un valor escalar Unicode, lo que significa que puede representar
mucho más que ASCII. Letras acentuadas; Caracteres chinos, japoneses y coreanos; Emojis;
y espacios de ancho cero son todos valores char válidos en Rust. Los valores escalar de
Unicode van desde U+0000 a U+D7FF y U+E000 a U+10FFFF inclusive. Sin embargo, un
"carácter" no es realmente un concepto en Unicode, por lo que su intuición humana sobre
lo que es un "carácter" puede no coincidir con lo que es un char en Rust. Discutiremos este
tema en detalle en “Almacenar texto codificado en UTF-8 con cadenas” en el capítulo 8.
Tipos compuestos
Tipos compuestos pueden agrupar múltiples valores en un solo tipo. Rust tiene dos tipos
compuestos primitivos: tuplas y arreglos.
El Tipo Tupla
Una tupla es una forma general de agrupar varios valores de distintos tipos en un tipo
compuesto. Las tuplas tienen una longitud fija: una vez declaradas, su tamaño no puede
aumentar ni disminuir.
Creamos una tupla escribiendo una lista de valores separados por comas dentro de
paréntesis. Cada posición de la tupla tiene un tipo, y los tipos de los distintos valores de la
tupla 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);
}
La variable tup se vincula a toda la tupla porque una tupla se considera un único elemento
compuesto. Para obtener los valores individuales de una tupla, podemos utilizar la
concordancia de patrones para desestructurar un valor de tupla, así:
https://book.rustlang-es.org/print 54/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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 let para tomar tup y convertirla en tres variables separadas, x , y y z . Esto se llama
desestructuración porque rompe la única 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 x y luego accede a cada elemento de la tupla usando sus
respectivos índices. Al igual que la mayoría de los lenguajes de programación, el primer
índice en una tupla es 0.
La tupla sin ningún valor tiene un nombre especial, unit. Este valor y su tipo correspondiente
están escritos ambos como () y representan un valor vacío o un tipo de retorno vacío. Las
expresiones devuelven implícitamente el valor unit si no devuelven ningún otro valor.
El Tipo Arreglo
Otra forma de tener una colección de múltiples valores es con un arreglo. A diferencia de
una tupla, cada elemento de un arreglo debe tener el mismo tipo. A diferencia de los
arreglos en algunos otros lenguajes, los arreglos en Rust tienen una longitud fija.
Escribimos los valores en un arreglo como una lista separada por comas dentro de
corchetes:
https://book.rustlang-es.org/print 55/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let a = [1, 2, 3, 4, 5];
}
Los arreglos son útiles cuando desea que sus datos se asignen en el stack (pila) en lugar del
heap (montículo) (hablaremos más sobre el stack y el heap en el Capítulo 4) o cuando desea
asegurarse de que siempre tenga un número fijo de elementos. Sin embargo, un arreglo no
es tan flexible como el tipo vector. Un vector es un tipo de colección similar proporcionado
por la biblioteca estándar que puede crecer o reducir su tamaño. Si no está seguro de si
debe usar un arreglo o un vector, es probable que deba usar un vector. El Capítulo 8 discute
los vectores en más detalle.
Sin embargo, los arreglos son más útiles cuando sabe que el número de elementos no
cambiará. Por ejemplo, si está utilizando los nombres del mes en un programa,
probablemente usaría un arreglo en lugar de un vector porque sabe que siempre contendrá
12 elementos:
Escribe el tipo de un arreglo usando corchetes con el tipo de cada elemento, un punto y
coma y luego el número de elementos en el arreglo, así:
Aquí, i32 es el tipo de cada elemento. Después del punto y coma, el número 5 indica que
el arreglo contiene cinco elementos.
También puede inicializar un arreglo para contener el mismo valor para cada elemento
especificando el valor inicial, seguido de un punto y coma y luego la longitud del arreglo en
corchetes, como se muestra aquí:
Un arreglo es un trozo de memoria de tamaño fijo y conocido que puede asignarse a la pila.
Se puede acceder a los elementos de una arreglo utilizando la indexación, de la siguiente
manera:
https://book.rustlang-es.org/print 56/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let a = [1, 2, 3, 4, 5];
En este ejemplo, la variable llamada first obtendrá el valor 1 porque ese es el valor en el
índice [0] en el arreglo. La variable llamada second obtendrá el valor 2 del índice [1] en
el arreglo.
Veamos qué sucede si intenta acceder a un elemento de un arreglo que está más allá del
final del arreglo. Digamos que ejecuta este código, similar al juego de adivinanzas del
Capítulo 2, para obtener un índice de arreglo 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");
Este código se compila con éxito. Si ejecuta este código usando cargo run y ingresa 0 , 1 ,
2 , 3 o 4 , el programa imprimirá el valor correspondiente en ese índice en el arreglo. Si en
cambio ingresa un número más allá del final del arreglo, como 10 , verá una salida como
esta:
https://book.rustlang-es.org/print 57/656
12/7/24, 22:37 El Lenguaje de Programación Rust
https://book.rustlang-es.org/print 58/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Funciones
Las Funciones son muy comunes en el código Rust. Ya has visto una de las funciones más
importantes del lenguaje: la función main , que es el punto de entrada de muchos
programas. También has visto la palabra clave fn , que te permite declarar nuevas
funciones.
El código en Rust usa snake case como estilo convencional para los nombres de funciones y
variables, en el que todas las letras son minúsculas y los guiones bajos separan las palabras.
Aquí hay un programa que contiene un ejemplo de definición de una función:
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
Podemos llamar a cualquier función que hayamos definido escribiendo su nombre seguido
de un conjunto de paréntesis. Como another_function está definida en el programa, se
puede llamar desde dentro de la función main . Ten en cuenta que definimos
another_function después de la función main en el código fuente; también podríamos
haberla definido antes. A Rust no le importa dónde definas tus funciones, sólo que estén
definidas en algún lugar en un ámbito que pueda ser visto por el invocador.
Empecemos un nuevo proyecto binario llamado functions para explorar las funciones más a
fondo. Coloca el ejemplo de another_function en src/main.rs y ejecútalo. Deberías ver la
siguiente salida:
$ 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.
https://book.rustlang-es.org/print 59/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Las líneas se ejecutan en el orden en que aparecen en la función main . Primero se imprime
el mensaje “Hello, world!”, y luego se llama a another_function y se imprime su mensaje.
Parámetros
Podemos definir funciones para que tengan parámetros, que son variables especiales que
forman parte de la firma de una función. Cuando una función tiene parámetros, puedes
proporcionarle valores concretos para esos parámetros. Técnicamente, los valores
concretos se llaman argumentos, pero coloquialmente, la gente tiende a usar las palabras
parámetro y argumento indistintamente para las variables en la definición de una función o
los valores concretos que se pasan cuando llamas a una función.
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
$ 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
En las firmas de las funciones, debes declarar el tipo de cada parámetro. Esta es una
decisión deliberada en el diseño de Rust: requerir anotaciones de tipo en las definiciones de
las funciones significa que el compilador casi nunca necesita que las uses en otro lugar del
código para averiguar a qué tipo te refieres. El compilador también puede dar mensajes de
error más útiles si sabe qué tipos espera la función.
Al definir múltiples parámetros, separa las declaraciones de parámetros con comas, como
esto:
https://book.rustlang-es.org/print 60/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
print_labeled_measurement(5, 'h');
}
Este ejemplo crea una función llamada print_labeled_measurement con dos parámetros. El
primer parámetro se llama value y es un i32 . El segundo parámetro se llama unit_label
y es de tipo char . Luego, la función imprime texto que contiene tanto el value como el
unit_label .
$ cargo run
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
Como hemos llamamos a la función con 5 como valor para value y 'h' como valor para
unit_label , la salida del programa contiene esos valores.
Sentencias y Expresiones
Los cuerpos de las funciones están compuestos por una serie de sentencias opcionalmente
terminadas en una expresión. Hasta ahora, las funciones que hemos visto no incluyen una
expresión final, pero has visto una expresión como parte de una sentencia. Debido a que
Rust es un lenguaje basado en expresiones, esta es una distinción importante de entender.
Otros lenguajes no tienen las mismas distinciones, así que veamos qué son las sentencias y
las expresiones y cómo sus diferencias afectan a los cuerpos de las funciones.
Hemos usado realmente sentencias y expresiones. Crear una variable y asignarle un valor
con la palabra clave let es una sentencia. En el Listado 3-1, let y = 6; es una sentencia.
fn main() {
let y = 6;
}
Listado 3-1: Una declaración de la función main que contiene una sentencia
https://book.rustlang-es.org/print 61/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Las definiciones de las funciones también son sentencias; todo el ejemplo anterior es una
sentencia en sí misma.
Las sentencias no devuelven valores. Por lo tanto, no puedes asignar una sentencia let a
otra variable, como intenta hacer el siguiente código; obtendrás un error:
fn main() {
let x = (let y = 6);
}
$ 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
La sentencia let y = 6 no devuelve un valor, por lo que no hay nada a lo que x se pueda
vincular. Esto es diferente a lo que ocurre en otros lenguajes, como C y Ruby, donde la
asignación devuelve el valor de la asignación. En esos lenguajes, puedes escribir x = y = 6
y hacer que tanto x como y tengan el valor 6 ; eso no es el caso en Rust.
Las expresiones evalúan a un valor y componen la mayor parte del resto del código que
escribirás en Rust. Considera una operación matemática, como 5 + 6 , que es una
expresión que evalúa al valor 11 . Las expresiones pueden ser parte de las sentencias: en el
Listado 3-1, el 6 en la sentencia let y = 6; es una expresión que evalúa al valor 6 .
https://book.rustlang-es.org/print 62/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Llamar a una función es una expresión. Llamar a una macro es una expresión. Un nuevo
bloque de ámbito creado con 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 a 4 . Ese valor se enlaza a y como parte de la
sentencia let . Ten en cuenta que la línea x + 1 no tiene un punto y coma al final, lo que
es diferente a la mayoría de las líneas que has visto hasta ahora. Las expresiones no
incluyen punto y coma al final. Si agregas un punto y coma al final de una expresión, la
conviertes en una sentencia, y entonces no devolverá un valor. Ten esto en cuenta a medida
que exploras los valores de retorno de las funciones y las expresiones a continuación.
Las funciones pueden devolver valores al código que las llama. No nombramos los valores
de retorno, pero debemos 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 última expresión en el bloque del cuerpo
de una función. Puedes devolver un valor antes de que la función finalice utilizando la
palabra clave return y especificando un valor, pero la mayoría de las funciones devuelven la
última expresión implícitamente. Aquí hay un ejemplo de una función que devuelve un
valor:
https://book.rustlang-es.org/print 63/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let x = five();
No hay llamadas a funciones, macros, ni siquiera sentencias let en la función five - solo
el número 5 por sí solo. Esa es una función perfectamente válida en Rust. Ten en cuenta
que también se especifica el tipo de retorno de la función, como -> i32 . Intenta ejecutar
este código; la salida debería verse así:
$ 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
let x = 5;
Segundo, la función five no tiene parámetros y define el tipo del valor de retorno, pero el
cuerpo de la función es un solitario 5 sin punto y coma porque es una expresión cuyo valor
queremos devolver.
fn main() {
let x = plus_one(5);
La ejecución de este código imprimirá The value of x is: 6 . Pero si colocamos un punto
y coma al final de la línea que contiene x + 1 , cambiándolo de una expresión a una
https://book.rustlang-es.org/print 64/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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
El mensaje de error principal, mismatched types , revela el problema principal con este
código. La definición de la función plus_one dice que devolverá un i32 , pero las
sentencias no evalúan un valor, lo que se expresa por () , el tipo unitario. Por lo tanto, no
se devuelve nada, lo que contradice la definición de la función y da como resultado un error.
En esta salida, Rust proporciona un mensaje para posiblemente ayudar a corregir este
problema: sugiere eliminar el punto y coma, lo que arreglaría el error.
https://book.rustlang-es.org/print 65/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Comentarios
Todos los programadores se esfuerzan por hacer que su código sea fácil de entender, pero
a veces se requiere una explicación adicional. En estos casos, los programadores dejan
comentarios en su código fuente que el compilador ignorará pero que las personas que lean
el código fuente pueden encontrar útiles.
// hola, mundo
Los comentarios también se pueden colocar al final de las líneas que contienen código:
fn main() {
let numero_de_la_suerte = 7; // Me siento afortunado hoy
}
Pero más a menudo verás que se usan en este formato, con el comentario en una línea
separada por encima del código que está anotando:
fn main() {
// Me siento afortunado hoy
let numero_de_la_suerte = 7;
}
https://book.rustlang-es.org/print 66/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Flujo de Control
La capacidad de ejecutar algún código dependiendo de si una condición es true y ejecutar
algún código repetidamente mientras una condición es true son elementos básicos en la
mayoría de los lenguajes de programación. Las construcciones más comunes que le
permiten controlar el flujo de ejecución del código Rust son las expresiones if y los bucles.
Expresiones if
Una expresión if le permite dividir su código según las condiciones. Proporciona una
condición y luego dice: “Si se cumple esta condición, ejecute este bloque de código. Si la
condición no se cumple, no ejecute este bloque de código.”
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
Todas las expresiones if comienzan con la palabra clave if , seguida de una condición. En
este caso, la condición comprueba si la variable number tiene un valor menor que 5.
Colocamos el bloque de código para ejecutar si la condición es true inmediatamente
después de la condición dentro de llaves. Los bloques de código asociados con las
condiciones en las expresiones if a veces se llaman brazos, al igual que los brazos en las
expresiones match que discutimos en la sección “Comparando la Adivinanza con el Número
Secreto” del Capítulo 2.
Opcionalmente, también podemos incluir una expresión else , que elegimos hacer aquí,
para dar al programa un bloque de código alternativo para ejecutar si la condición evaluada
es false . Si no proporciona una expresión else y la condición es false , el programa va a
ignorar el bloque if y continuará con el siguiente fragmento de código.
https://book.rustlang-es.org/print 67/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
Vamos a intentar cambiar el valor de number a un valor que haga que la condición sea
false para ver qué sucede:
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 vale la pena señalar que la condición en este código debe ser un bool . Si la
condición no es un bool , obtendremos un error. Por ejemplo, intente ejecutar el siguiente
código:
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
El error indica que Rust esperaba un bool pero obtuvo un entero. A diferencia de los
lenguajes como Ruby y JavaScript, Rust no intentará convertir automáticamente los tipos no
booleanos en un booleano. Debes ser explícito y siempre proporcionar a if un booleano
https://book.rustlang-es.org/print 68/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}
Ejecutando este código imprimirá number was something other than zero .
Puede usar múltiples condiciones combinando if y else en una expresión else if . 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 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
Este programa tiene cuatro posibles caminos que puede tomar. Después de ejecutarlo,
debería ver la siguiente salida:
$ 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
Cuando se ejecuta este programa, verifica cada expresión if en orden y ejecuta el primer
cuerpo para el cual la condición se evalúa como true . Tenga en cuenta que incluso si 6 es
divisible por 2, no vemos la salida number is divisible by 2 , ni vemos el texto number is
not divisible by 4, 3, or 2 del bloque else . Esto se debe a que Rust solo ejecuta el
https://book.rustlang-es.org/print 69/656
12/7/24, 22:37 El Lenguaje de Programación Rust
bloque para la primera condición true , y una vez que encuentra una, ni siquiera verifica el
resto.
El uso de demasiadas expresiones else if puede ensuciar su código, por lo que si tiene
más de una, es posible que desee refactorizar su código. El capítulo 6 describe una
poderosa construcción de ramificación de Rust llamada match para estos casos.
Dado que if es una expresión, podemos usarlo en el lado derecho de una declaración let
para asignar el resultado a una variable, como en el Listado 3-2.
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5
Recuerde que los bloques de código se evalúan en la última expresión de ellos y los
números por sí mismos también son expresiones. En este caso, el valor de la expresión if
en su conjunto depende de qué bloque de código se ejecuta. Esto significa que los valores
que tienen el potencial de ser resultados de cada rama del if deben ser del mismo tipo; en
el Listado 3-2, los resultados de ambas ramas del if y la rama else fueron enteros i32 .
Si los tipos no coinciden, como en el siguiente ejemplo, obtendremos un error:
fn main() {
let condition = true;
https://book.rustlang-es.org/print 70/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cuando intentamos compilar este código, obtendremos un error. Las ramas if y else
tienen tipos de valor que 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
A menudo es útil ejecutar un bloque de código más de una vez. Para esta tarea, Rust
proporciona varios bucles, que ejecutarán el código dentro del cuerpo del bucle hasta el final
y luego comenzarán de inmediato desde el principio. Para experimentar con los bucles,
hagamos un nuevo proyecto llamado loops.
Rust tiene tres tipos de bucles: loop , while y for . Vamos a probar cada uno.
La palabra clave loop le dice a Rust que ejecute un bloque de código una y otra vez para
siempre o hasta que le indique explícitamente que se detenga.
Como ejemplo, cambie el archivo src/main.rs en su directorio loops para que se vea así:
https://book.rustlang-es.org/print 71/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
loop {
println!("again!");
}
}
Cuando ejecutemos este programa, veremos again! impreso una y otra vez
continuamente hasta que detengamos manualmente el programa. La mayoría de los
terminales admiten el atajo de teclado ctrl-c para interrumpir un programa que está
atascado en un bucle continuo. Inténtelo:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
El símbolo ^C representa dónde presionó ctrl-c. Puede que vea o no la palabra again!
impresa después del ^C , dependiendo de dónde estaba el código en el bucle cuando
recibió la señal de interrupción.
Una de las aplicaciones de un loop es volver a intentar una operación que sabe que puede
fallar, como verificar si un hilo ha completado su trabajo. Es posible que también necesite
pasar el resultado de esa operación fuera del bucle al resto de su código. Para hacer esto,
puede agregar el valor que desea devolver después de la expresión break que usa para
detener el bucle; ese valor se devolverá fuera del bucle para que pueda usarlo, como se
muestra aquí:
https://book.rustlang-es.org/print 72/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let mut counter = 0;
if counter == 10 {
break counter * 2;
}
};
Antes del bucle, declaramos una variable llamada counter e inicializamos en 0 . Luego
declaramos una variable llamada result para contener el valor devuelto del bucle. En cada
iteración del bucle, agregamos 1 a la variable counter , y luego verificamos si el counter
es igual a 10 . Cuando lo es, usamos la palabra clave break con el valor counter * 2 .
Después del bucle, usamos un punto y coma para terminar la instrucción que asigna el valor
a result . Finalmente, imprimimos el valor en result , que en este caso es 20 .
Tu puedes también usar return dentro de un loop. Mientras break solo existe para el loop
actual, return siempre existe para la función actual.
Si tiene bucles dentro de bucles, break y continue se aplican al bucle más interior en ese
punto. Opcionalmente, puede especificar una etiqueta de bucle en un bucle que luego puede
usar con break o continue para especificar que esas palabras clave se aplican al bucle
etiquetado en lugar del bucle más interior. Las etiquetas de bucle deben comenzar con una
comilla simple. Aquí hay un ejemplo con dos bucles anidados:
https://book.rustlang-es.org/print 73/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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}");
}
$ 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
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
Listado 3-3: Usando un bucle while para ejecutar código mientras una condición es verdadera
Esta expresion elimina mucho anidamiento que sería necesario si usara loop , if , else y
break , y es más claro. Mientras una condición se evalúa como true , el código se ejecuta;
de lo contrario, sale del bucle.
Tu puedes también puedes usar el while para recorrer los elementos de una colección,
justo como un arreglo. Por ejemplo, el bucle en el Listado 3-4 muestra cada elemento en el
arreglo a .
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
index += 1;
}
}
Listado 3-4: Bucle a través de cada elemento de una colección usando un bucle while
Aquí, el código cuenta hacia arriba a través de los elementos en el arreglo. Se inicia en el
índice 0 , y luego se ejecuta hasta que alcanza el índice final en el arreglo (es decir, cuando
index < 5 ya no es true ). Ejecutar este código imprimirá cada elemento en el arreglo:
https://book.rustlang-es.org/print 75/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
Los cinco valores del arreglo aparecen en la terminal, como se esperaba. Aunque index
llegará a un valor de 5 en algún momento, el bucle deja de ejecutarse antes de intentar
obtener un sexto valor del arreglo.
Sin embargo, este enfoque es propenso a errores; podríamos causar que el programa se
descomponga si el valor del índice o la condición de prueba es incorrecta. Por ejemplo, si
cambia la definición del arreglo a para tener cuatro elementos, pero olvida actualizar la
condición a while index < 4 , el código se descompondría. También es lento, porque el
compilador agrega código de tiempo de ejecución para realizar la verificación condicional de
si el índice está dentro de los límites del arreglo en cada iteración del bucle.
Como una alternativa más concisa, puede usar un bucle for y ejecutar algún código para
cada elemento en una colección. Un bucle for se ve como el código en el Listado 3-5.
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
Listado 3-5: Bucle a través de cada elemento de una colección usando un bucle for
Cuando ejecutamos este código, veremos la misma salida que en el Listado 3-4. Lo más
importante es que ahora hemos aumentado la seguridad del código y eliminado la
posibilidad de errores que podrían deberse a ir más allá del final del arreglo o no ir lo
suficientemente lejos y perder algunos elementos.
Usando el bucle for , no necesitaría recordar cambiar cualquier otro código si cambiara el
número de valores en el arreglo, como lo haría con el método usado en el Listado 3-4.
La seguridad y concisión de los bucles for los convierten en la estructura de bucle más
utilizada en Rust. Incluso en situaciones en las que quiera ejecutar algún código un cierto
número de veces, como en el ejemplo de cuenta regresiva que usó un bucle while en el
Listado 3-3, la mayoría de los Rustaceans usarían un bucle for . La forma de hacerlo sería
https://book.rustlang-es.org/print 76/656
12/7/24, 22:37 El Lenguaje de Programación Rust
usar un Range , proporcionado por la biblioteca estándar, que genera todos los números en
secuencia a partir de un número y termina antes de otro número.
Así es como se vería la cuenta regresiva usando un bucle for y otro método que aún no
hemos hablado, rev , para invertir el rango:
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
Resumen
¡Lo lograste! Este fue un capítulo de gran tamaño: aprendiste sobre variables, tipos de datos
escalares y compuestos, funciones, comentarios, expresiones if y bucles. Para practicar
con los conceptos discutidos en este capítulo, intente construir programas para hacer lo
siguiente:
Cuando esté listo para continuar, hablaremos sobre un concepto en Rust que no existe
comúnmente en otros lenguajes de programación: la propiedad.
https://book.rustlang-es.org/print 77/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Entendiendo el Ownership
El Ownership es la característica más única de Rust y tiene implicaciones profundas para el
resto del lenguaje. Permite a Rust hacer garantías de seguridad de memoria sin necesidad
de un recolector de basura, por lo que es importante entender cómo funciona el
Ownership. En este capítulo, hablaremos sobre el Ownership así como varias características
relacionadas: préstamo (borrowing), slices, y cómo Rust organiza los datos en la memoria.
https://book.rustlang-es.org/print 78/656
12/7/24, 22:37 El Lenguaje de Programación Rust
¿Qué es el Ownership?
El ownership es un conjunto de reglas que definen cómo un programa de Rust administra la
memoria. Todos los programas tienen que administrar la forma en que usan la memoria de
un computador mientras se ejecutan. Algunos lenguajes tienen recolección de basura que
busca regularmente la memoria que ya no se usa mientras el programa se ejecuta; en otros
lenguajes, el programador debe asignar y liberar la memoria explícitamente. Rust usa un
tercer enfoque: la memoria se administra a través de un sistema de ownership con un
conjunto de reglas que el compilador verifica. Si alguna de las reglas se viola, el programa
no se compilará. Ninguna de las características del ownership ralentizará su programa
mientras se ejecuta.
Cuando entienda el ownership, tendrá una base sólida para comprender las características
que hacen que Rust sea único. En este capítulo, aprenderá ownership trabajando en
algunos ejemplos que se centran en una estructura de datos muy común: las cadenas de
caracteres.
El Stack y el Heap
https://book.rustlang-es.org/print 79/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Tanto el stack como el heap son partes de la memoria disponible para su código para
usar en tiempo de ejecución, pero están estructurados de formas diferentes. El stack
almacena valores en el orden en que los recibe y elimina los valores en el orden
opuesto. Esto se conoce como LIFO que es el acrónimo inglés de Last In, First Out o en
español El último en entrar, es el primero en salir. Piense en una pila de platos: cuando
agrega más platos, los coloca en la parte superior de la pila, y cuando necesita un
plato, toma uno de la parte superior. Agregar o eliminar platos del medio o de la parte
inferior no funcionaría tan bien! Agregar datos se llama empujar en el stack, y eliminar
datos se llama sacar del stack. Todos los datos almacenados en el stack 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 puede cambiar deben almacenarse en el heap en su
lugar.
El heap es menos organizado: cuando coloca datos en el heap, solicita una cierta
cantidad de espacio. El administrador de memoria encuentra un lugar vacío en el heap
que sea lo suficientemente grande, lo marca como en uso y devuelve un puntero, que
es la dirección de esa ubicación. Este proceso se llama asignar en el heap y a veces se
abrevia como solo asignar (empujar valores en el stack no se considera asignar).
Debido a que el puntero al heap es un tamaño conocido y fijo, puede almacenar el
puntero en el stack, pero cuando desea los datos reales, debe seguir el puntero.
Piense en estar sentado en un restaurante. Cuando ingresa, indica la cantidad de
personas en su grupo, y el anfitrión encuentra una mesa vacía que quepa a todos y los
lleva allí. Si alguien en su grupo llega tarde, puede preguntar dónde se ha sentado para
encontrarlo.
Acceder a los datos en el heap es más lento que acceder a los datos en el stack porque
debe seguir un puntero para llegar allí. Los procesadores contemporáneos son más
rápidos si saltan menos en la memoria. Continuando con la analogía, considere un
servidor en un restaurante que toma pedidos de muchas mesas. Es más eficiente
obtener todos los pedidos de una mesa antes de pasar a la siguiente mesa. Tomar un
pedido de la mesa A, luego un pedido de la mesa B, luego uno de la A nuevamente y
luego uno de la B nuevamente sería un proceso mucho más lento. Del mismo modo,
un procesador puede hacer su trabajo mejor si trabaja con datos que están cerca de
otros datos (como lo están en el stack) en lugar de más lejos (como pueden estar en el
heap).
Cuando su código llama a una función, los valores que se pasan a la función (incluidos,
posiblemente, punteros a datos en el heap) y las variables locales de la función se
empujan en el stack. Cuando la función termina, esos valores se sacan del stack.
https://book.rustlang-es.org/print 80/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Mantener un registro de qué partes del código están utilizando qué datos en el heap,
minimizar la cantidad de datos duplicados en el heap y limpiar los datos no utilizados
en el heap para que no se quede sin espacio son todos problemas que ownership
aborda. Una vez que comprenda ownership, no tendrá que pensar mucho en el stack y
el heap, pero saber que el principal propósito de ownership es administrar datos en el
heap puede ayudar a explicar por qué funciona de la manera en que lo hace.
Reglas de Ownership
Primero, echemos un vistazo a las reglas de ownership. Mantenga estas reglas en mente
mientras trabajamos a través de los ejemplos que las ilustran:
Ahora que hemos pasado la sintaxis básica de Rust, no incluiremos todo el código fn
main() { en los ejemplos, por lo que si está siguiendo, asegúrese de colocar los siguientes
ejemplos dentro de una función main manualmente. Como resultado, nuestros ejemplos
serán un poco más concisos, permitiéndonos centrarnos en los detalles reales en lugar del
código repetitivo.
let s = "hola";
En este punto, la relación entre los contextos de ejecución y cuándo las variables son válidas
es similar a la de otros lenguajes de programación. Ahora construiremos sobre este
entendimiento al introducir el tipo String .
El Tipo String
Para ilustrar las reglas de ownership, necesitamos un tipo de datos más complejo que los
que cubrimos en la sección “Tipos de Datos” del Capítulo 3. Los tipos cubiertos
anteriormente son de un tamaño conocido, pueden almacenarse en el stack y se pueden
sacar del stack cuando su contexto de ejecución termina, y se pueden copiar rápidamente y
trivialmente para crear una nueva instancia independiente si otra parte del código necesita
usar el mismo valor en un contexto de ejecución diferente. Pero queremos ver los datos que
se almacenan en el heap y explorar cómo Rust sabe cuándo limpiar esos datos, y el tipo
String es un gran ejemplo.
Nos centraremos en las partes de String que se relacionan con el ownership. Estos
aspectos también se aplican a otros tipos de datos complejos, ya sean suministrados por la
biblioteca estándar o creados por usted. Discutiremos String con más profundidad en el
Capítulo 8.
Ya hemos visto literales de cadena, donde un valor de cadena está codificado en nuestro
programa. Los literales de cadena son convenientes, pero no son adecuados para todas las
situaciones en las que podríamos querer usar texto. Una razón es que son inmutables. Otra
es que no todos los valores de cadena se pueden conocer cuando escribimos nuestro
código: ¿y si queremos tomar la entrada del usuario y almacenarla? Para estas situaciones,
Rust tiene un segundo tipo de cadena, String . Este tipo administra datos asignados en el
heap y, como tal, es capaz de almacenar una cantidad de texto que no conocemos en el
tiempo de compilación. Puede crear un String a partir de un literal de cadena usando la
función from , así:
let s = String::from("hola");
El operador doble dos puntos :: nos permite usar el namespace (nombre de espacio) de
esta función from particular bajo el tipo String en lugar de usar algún tipo de nombre
como string_from . Discutiremos esta sintaxis más en la sección “Sintaxis de Método” del
Capítulo 5, y cuando hablamos sobre el uso de namespaces con módulos en “Rutas para
Referir a un Elemento en el Árbol de Módulos”
https://book.rustlang-es.org/print 82/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Entonces, ¿cuál es la diferencia aquí? ¿Por qué String puede ser mutable pero los literales
no pueden? La diferencia está en cómo estos dos tipos manejan la memoria.
Memoria y Asignación
Con el tipo String , para poder soportar una pieza mutable y extensible de texto,
necesitamos asignar una cantidad de memoria en el heap, desconocida en tiempo de
compilación, para contener el contenido. Esto significa:
Sin embargo, la segunda parte es diferente. En los lenguajes con un recolector de basura
(Garbage Collector), el recolector de basura rastrea y limpia la memoria que ya no se está
usando y no necesitamos pensar en ello. En la mayoría de los lenguajes sin un recolector de
basura, es nuestra responsabilidad identificar cuándo la memoria ya no se está usando y
llamar al código para liberarla explícitamente, tal como lo hicimos para solicitarla. Hacer
esto correctamente ha sido históricamente un problema difícil de programación. Si lo
olvidamos, desperdiciaremos memoria. Si lo hacemos demasiado pronto, tendremos una
variable inválida. Si lo hacemos dos veces, eso también es un error. Necesitamos emparejar
exactamente una asignación con exactamente una liberación .
Rust toma un camino diferente: la memoria se devuelve automáticamente una vez que la
variable que la posee sale del contexto de ejecución. Aquí hay una versión de nuestro
ejemplo de alcance de la Lista 4-1 usando un String en lugar de un literal de cadena:
https://book.rustlang-es.org/print 83/656
12/7/24, 22:37 El Lenguaje de Programación Rust
{
let s = String::from("hola"); // s es valido desde aquí
Hay un punto natural en el que podemos devolver la memoria que necesita nuestro String
al administrador: cuando s sale del alcance. Cuando una variable sale del contexto de
ejecución, Rust llama a una función especial para nosotros. Esta función se llama drop , y es
donde el autor de String puede poner el código para devolver la memoria. Rust llama a
drop automáticamente en la llave de cierre.
Este patrón tiene un profundo impacto en la forma en que se escribe el código Rust. Puede
parecer simple ahora, pero el comportamiento del código puede ser inesperado en
situaciones más complejas cuando queremos que varias variables usen los datos que
hemos asignado en el heap. Exploremos algunas de esas situaciones ahora.
Varias variables pueden interactuar con los mismos datos de diferentes formas en Rust.
Veamos un ejemplo usando un entero en la Lista 4-2.
let x = 5;
let y = x;
Podemos adivinar lo que está haciendo: "vincular el valor 5 a x ; luego hacer una copia del
valor en x y vincularlo a y ". Ahora tenemos dos variables, x y y , y ambos son 5 . Esto es
lo que está sucediendo, porque los enteros son valores simples con un tamaño conocido y
fijo, y estos dos valores 5 se empujan en la pila.
let s1 = String::from("hola");
let s2 = s1;
Esto se ve muy similar, por lo que podríamos suponer que la forma en que funciona sería la
misma: es decir, la segunda línea haría una copia del valor en s1 y lo vincularía a s2 . Pero
esto no es exactamente lo que sucede.
https://book.rustlang-es.org/print 84/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Mire la Figura 4-1 para ver lo que está sucediendo en realidad con el String . Un String
está compuesto por tres partes, mostradas 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 está la memoria en el heap que contiene el contenido.
s1
nombre valor indice valor
ptr 0 h
longitud 4 1 o
capacidad 4 2 l
3 a
Figura 4-1: Representación en memoria de un String que contiene el valor "hola" vinculado a s1
La longitud es cuánta memoria, en bytes, los contenidos del String están utilizando
actualmente. La capacidad es la cantidad total de memoria, en bytes, que el String ha
recibido del administrador. La diferencia entre longitud y capacidad importa, pero no en
este contexto, por lo que por ahora está bien ignorar la capacidad.
Cuando asignamos s1 a s2 , los datos de String se copian, lo que significa que copiamos
el puntero, la longitud y la capacidad que están en la pila. No copiamos los datos en el heap
al que hace referencia el puntero. En otras palabras, la representación de datos en memoria
se ve como la Figura 4-2.
s1
nombre valor
puntero
longitud 4
capacidad 4 indice valor
0 h
s2 1 o
nombre valor 2 l
puntero 3 a
longitud 4
capacidad 4
Figura 4-2: Representación en memoria de la variable s2 que tiene una copia del puntero, la longitud y la
capacidad de s1 .
s1
nombre valor indice valor
puntero 0 h
longitud 4 1 o
capacidad 4 2 l
3 a
s2
nombre valor indice valor
puntero 0 h
longitud 4 1 o
capacidad 4 2 l
3 a
Figura 4-3: Otra posibilidad de lo que s2 = s1 podría hacer si Rust copiara también los datos del heap
Anteriormente, dijimos que cuando una variable sale de contexto de ejecución, Rust llama
automáticamente a la función drop y limpia la memoria del heap para esa variable. Pero la
Figura 4-2 muestra que ambos punteros de datos apuntan al mismo lugar. Esto es un
problema: cuando s2 y s1 salen de contexto de ejecución, ambos intentarán liberar la
misma memoria. Esto se conoce como un error de doble liberación y es uno de los errores
de seguridad de la memoria que mencionamos anteriormente. Liberar la memoria dos
veces puede conducir a la corrupción de memoria, lo que puede conducir a vulnerabilidades
de seguridad.
let s1 = String::from("hola");
let s2 = s1;
println!("{s1}, mundo!");
Obtendrás un error como este porque Rust te impide usar la referencia invalidada:
https://book.rustlang-es.org/print 86/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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("hola");
| -- 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}, mundo!");
| ^^^^ 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 escuchado los términos copia superficial y copia profunda mientras trabajabas con
otros lenguajes, el concepto de copiar el puntero, la longitud y la capacidad sin copiar los
datos probablemente suene a hacer una copia superficial. Pero debido a que Rust también
invalida la primera variable, en vez de llamarse una copia superficial, se conoce como un
movimiento. En este ejemplo, diríamos que s1 fue movido a s2 . Entonces, lo que realmente
sucede se muestra en la Figura 4-4.
s1
nombre valor
puntero
longitud 4
capacidad 4 indice valor
0 h
s2 1 o
nombre valor 2 l
puntero 3 a
longitud 4
capacidad 4
https://book.rustlang-es.org/print 87/656
12/7/24, 22:37 El Lenguaje de Programación Rust
¡Eso resuelve nuestro problema! Con solo s2 válido, cuando sale de ámbito solo él liberará
la memoria, y ya está.
Además, hay una elección de diseño que se infiere de esto: Rust nunca creará
automáticamente "copias profundas" de tus datos. Por lo tanto, cualquier copia automática
se puede asumir que es económica en términos de rendimiento en tiempo de ejecución.
Si queremos copiar profundamente los datos del heap de la String , no solo los datos de la
pila, podemos usar un método común llamado clone . Discutiremos la sintaxis del método
en el Capítulo 5, pero debido a que los métodos son una característica común en muchos
lenguajes de programación, probablemente los hayas visto antes.
let s1 = String::from("hola");
let s2 = s1.clone();
Cuando veas una llamada a clone , sabrás que se está ejecutando algún código arbitrario y
que ese código puede ser costoso. Es un indicador visual de que algo diferente está
sucediendo.
Hay otro problema que aún no hemos hablado. Este código usando enteros - parte de lo
que se mostró en el Listado 4-2 - funciona y es válido:
let x = 5;
let y = x;
Pero este código parece contradecir lo que acabamos de aprender: no tenemos una
llamada a clone , pero x sigue siendo válido y no se movió a y .
La razón es que los tipos como los enteros que tienen un tamaño conocido en el momento
de la compilación se almacenan completamente en la pila, por lo que copiar los valores
reales es rápido. Eso significa que no hay razón para que queramos evitar que x sea válido
después de crear la variable y . En otras palabras, no hay diferencia entre copiar superficial
https://book.rustlang-es.org/print 88/656
12/7/24, 22:37 El Lenguaje de Programación Rust
y profunda aquí, por lo que llamar a clone no haría nada diferente de la copia superficial
habitual, y podemos dejarlo fuera.
Rust tiene una anotación especial llamada Copy que podemos colocar en tipos que se
almacenan en la pila, como los enteros (hablaremos más sobre los 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 trivialmente, haciendo que sigan siendo válidas después de asignarlas a otra
variable.
Rust no nos permitirá anotar un tipo con Copy si el tipo, o cualquiera de sus partes, ha
implementado el trait Drop . Si el tipo necesita que algo especial suceda cuando el valor sale
del alcance y agregamos la anotación Copy a ese tipo, obtendremos un error de tiempo de
compilación. Para aprender cómo agregar la anotación Copy a tu tipo para implementar el
trait, consulta “Traits derivables” en el Apéndice C.
Entonces, ¿qué tipos implementan el trait Copy ? Puedes consultar la documentación del
tipo dado para asegurarte, pero como regla general, cualquier grupo de valores escalares
simples puede implementar Copy , y nada que requiera asignación o sea alguna forma de
recurso puede implementar Copy . Aquí hay algunos de los tipos que implementan Copy :
Propiedad y funciones
Las mecánicas de pasar un valor a una función son similares a las de asignar un valor a una
variable. Pasar una variable a una función moverá o copiará, como hace la asignación. La
Lista 4-3 tiene un ejemplo con algunas anotaciones que muestran dónde entran y salen las
variables del alcance.
https://book.rustlang-es.org/print 89/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let s = String::from("hola"); // s aparece en el ámbito
Los valores de retorno también pueden transferir la propiedad. La Lista 4-4 muestra un
ejemplo de una función que devuelve algún valor, con anotaciones similares a las de la Lista
4-3.
https://book.rustlang-es.org/print 90/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let s1 = da_un_ownership(); // da_un_ownership es llamado y
// devuelve el valor de retorno
// a s1
let s3 = toma_y_devuelve(s2);
// s2 es movido a la función
// toma_y_devuelve, que también
// retorna el valor de s2 a s3
} // Fin el ámbito, s3 es destruido con drop y se libera la memoria.
// s2 fue movido previamente, entonces no pasa nada.
// s1 es destruido con drop y se libera la memoria.
La propiedad (ownership) de una variable sigue el mismo patrón cada vez: asignar un valor
a otra variable lo mueve. Cuando una variable que incluye datos en el heap sale del
contexto de ejecución, el valor se limpiará por drop a menos que la propiedad de los datos
se haya movido a otra variable.
Aunque esto funciona, tomar la propiedad y luego devolver la propiedad con cada función
es un poco tedioso. ¿Qué pasa si queremos que una función use un valor pero no tome la
propiedad? Es bastante molesto que todo lo que pasamos también necesite volver a pasar
si queremos usarlo de nuevo, además de cualquier dato que resulte del cuerpo de la
función que también podríamos querer devolver.
Rust nos permite devolver múltiples valores usando una tupla, como se muestra en la Lista
4-5.
https://book.rustlang-es.org/print 91/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let s1 = String::from("hola");
(s, length)
}
Pero esto es demasiado ceremonioso y mucho trabajo para un concepto que debería ser
común. Afortunadamente para nosotros, Rust tiene una característica para usar un valor sin
transferir la propiedad, llamada referencias.
https://book.rustlang-es.org/print 92/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Referencias y Prestamos
El problema con la tupla de código en el Listado 4-5 es que tenemos que devolver el String
a la función que lo llama para que podamos seguir usando el String después de la llamada
a calcular_longitud , porque el String se movió a calcular_longitud . En lugar de eso,
podemos proporcionar una referencia al valor String . Una referencia es como un puntero
en que es una dirección que podemos seguir para acceder a los datos almacenados en esa
dirección; esos datos son propiedad de otra variable. A diferencia de un puntero, una
referencia garantiza que apunte a un valor válido de un tipo particular para la vida de esa
referencia.
Aquí está cómo definirías y usarías una función calcular_longitud que tiene una
referencia a un objeto como parámetro en lugar de tomar la propiedad del valor.
fn main() {
let s1 = String::from("hola");
https://book.rustlang-es.org/print 93/656
12/7/24, 22:37 El Lenguaje de Programación Rust
s s1
nombre valor nombre valor indice valor
puntero puntero 0 h
longitud 4 1 o
capacidad 4 2 l
3 a
let s1 = String::from("hola");
La sintaxis &s1 nos permite crear una referencia que se refiere al valor de s1 pero sin ser el
propietario. Por este motivo, el valor al que apunta no se descartará cuando la referencia
deje de usarse.
Del mismo modo, la firma de la función usa & para indicar que el tipo del parámetro s es
una referencia. Vamos a agregar algunas anotaciones:
https://book.rustlang-es.org/print 94/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Llamamos a la acción de crear una referencia prestar (borrowing en ingles). Como en la vida
real, si una persona posee algo, puedes pedir prestado. Cuando termines, tienes que
devolverlo. No lo posees.
Entonces, ¿qué pasa si intentamos modificar algo que estamos prestando? Prueba el código
en el Listado 4-6. Spoiler alert: ¡no funciona!
fn main() {
let s = String::from("hola");
modificar(&s);
}
fn modificar(un_string: &String) {
un_string.push_str(", mundo");
}
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*un_string` as mutable, as it is behind a `&`
reference
--> src/main.rs:8:5
|
8 | un_string.push_str(", world");
| ^^^^^^^^^^^ `un_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 modificar(un_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
Al igual que las variables son inmutables por defecto, también lo son las referencias. No se
nos permite modificar algo al 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 cambios que usen, en su lugar, una referencia mutable:
https://book.rustlang-es.org/print 95/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let mut s = String::from("hola");
modificar(&mut s);
}
Primero cambiamos s a mut . Luego creamos una referencia mutable con &mut s donde
llamamos a la función modificar , y actualizamos la firma de la función para aceptar una
referencia mutable con un_string: &mut String . Esto hace muy claro que la función
modificar mutará el valor que presta.
Las referencias mutables tienen una gran restricción: si tienes una referencia mutable a un
valor, no puedes tener otras referencias a ese valor. Este código que intenta crear dos
referencias mutables a s fallará:
let r1 = &mut s;
let r2 = &mut s;
$ 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 dice que este código es inválido porque no podemos prestar s como mutable
más de una vez. El primer préstamo mutable está en r1 y debe durar hasta que se use en
el println! , pero entre la creación de esa referencia mutable y su uso, intentamos crear
otra referencia mutable en r2 que presta los mismos datos que r1 .
https://book.rustlang-es.org/print 96/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La restricción que impide múltiples referencias mutables a los mismos datos al mismo
tiempo permite la mutación pero de una manera muy controlada. Es algo con lo que los
nuevos Rustaceans luchan porque la mayoría de los lenguajes te permiten mutar cuando
quieras. El beneficio de tener esta restricción es que Rust puede prevenir las carreras de
datos en tiempo de compilación. Una carrera de datos es similar a una condición de carrera
y ocurre cuando ocurren estos tres comportamientos:
Como siempre, podemos usar llaves para crear un nuevo contexto de ejecución,
permitiendo múltiples referencias mutables, solo no simultáneas:
{
let r1 = &mut s;
} // r1 se sale de su ámbito aquí, por lo que no hay problema
// si creamos otra referencia mutable
let r2 = &mut s;
Rust impone una regla similar para combinar referencias mutables e inmutables. Este
código da como resultado un error:
https://book.rustlang-es.org/print 97/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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 hay problema
| -- immutable borrow occurs here
5 | let r2 = &s; // no hay problema
6 | let r3 = &mut s; // ¡ UN GRAN PROBLEMA !
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}, {}, y {}", r1, r2, r3);
| -- 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
¡Uf! También no podemos tener una referencia mutable mientras tenemos una inmutable al
mismo valor.
¡Los usuarios de una referencia inmutable no esperan que el valor cambie repentinamente
debajo de ellos! Sin embargo, se permiten múltiples referencias inmutables porque nadie
que solo está leyendo los datos tiene la capacidad de afectar la lectura de los datos de nadie
más.
Tenga en cuenta que el contexto de ejecución de una referencia comienza desde donde se
introduce y continúa hasta la última vez que se usa la referencia. Por ejemplo, este código
se compilará porque el último uso de las referencias inmutables, el println! , ocurre antes
de que se introduzca la referencia mutable:
Aunque los errores de préstamo a veces pueden ser frustrantes, recuerda que es el
compilador de Rust que señala un error potencial temprano (en tiempo de compilación en
https://book.rustlang-es.org/print 98/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Referencias colgantes
Intentemos crear una referencia colgante para ver cómo Rust los previene con un error de
tiempo de compilación:
Filename: src/main.rs
fn main() {
let referencia_a_la_nada = colgar();
}
&s
}
https://book.rustlang-es.org/print 99/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn colgar() -> &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 colgar() -> &'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 cubierto: los
tiempos de vida. Discutiremos los tiempos de vida en detalle en el Capítulo 10. Pero, si
ignora las partes sobre los tiempos de vida, el mensaje contiene la clave para saber por qué
este código es un problema:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
https://book.rustlang-es.org/print 100/656
12/7/24, 22:37 El Lenguaje de Programación Rust
```rust,ignore,does_not_compile
# fn main() {
# let referencia_a_la_nada = colgar();
# }
#
fn colgar() -> &String { // colgar retorna una referencia a un String
s
}
En cualquier momento dado, puedes tener o bien una referencia mutable o cualquier
número de referencias inmutables.
Las referencias deben ser siempre válidas.
https://book.rustlang-es.org/print 101/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El Tipo Slice
Los Slices te permiten referenciar a una secuencia contigua de elementos en una colección
en lugar de la colección completa. Un slice es una especie de referencia, por lo que no tiene
ownership.
Aquí hay un pequeño problema de programación: escribe una función que tome un string
de palabras separadas por espacios y retorne la primera palabra que encuentre en ese
string. Si la función no encuentra ningún espacio en el string, todo el string debe ser una
sola palabra, por lo que se debe retornar todo el string.
Trabajemos en cómo escribiríamos la firma de esta función sin usar slices, para entender el
problema que los slices resolverán:
Filename: src/main.rs
s.len()
}
Listing 4-7: La función first_word retorna un valor de índice en bytes dentro de un parámetro String
Dado que necesitamos recorrer el String elemento por elemento y comprobar si un valor
es un espacio, convertiremos nuestro String a un array de bytes usando el método
as_bytes .
https://book.rustlang-es.org/print 102/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Hablaremos más detalladamente sobre los iterators en Procesando una serie de elementos
con Iteradores. Por ahora, sabemos que iter es un método que retorna cada elemento en
una colección y que enumerate envuelve el resultado de iter y retorna cada elemento
como parte de una tupla. El primer elemento de la tupla que retorna enumerate es el
índice, y el segundo elemento es una referencia al elemento. Esto es un poco más
conveniente que calcular el índice nosotros mismos.
Debido a que el método enumerate retorna una tupla, podemos usar patrones para
desestructurar esa tupla. Hablaremos más sobre los patrones en Patrones que vinculan
valores. En el ciclo for , especificamos un patrón que tiene i para el índice de la tupla e
&item para el byte único en la tupla. Debido a que tomamos una referencia al elemento de
.iter().enumerate() , podemos usar & en el patrón.
Dentro del ciclo for , buscamos el byte que representa el espacio usando la sintaxis literal
del byte. Si encontramos un espacio, retornamos su posición. De lo contrario, retornamos la
longitud del string usando s.len() .
s.len()
Ahora tenemos una forma de averiguar el índice del final de la primera palabra en el string,
pero tenemos un problema. Estamos retornando un usize por si solo, pero es solo un
número significativo en el contexto de él &String . En otras palabras, debido a que es un
valor separado del String , no hay garantía de que siga siendo válido en el futuro.
Considera el programa en Listing 4-8 que usa la función first_word del Listing 4-7.
Filename: src/main.rs
fn main() {
let mut s = String::from("hello world");
// word aún tiene el valor 5 aquí, pero ya no hay un string para que
// usar el valor 5 tenga sentido, ¡word es totalmente invalida!
}
Listing 4-8: Almacenando el resultado de llamar a la función first_word y luego cambiar el contenido del
String
https://book.rustlang-es.org/print 103/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Este programa compila sin errores y también lo hará si usáramos word después de llamar a
s.clear() . Debido a que word no está conectado al estado de s en absoluto, word aún
contiene el valor 5 . Podríamos usar el valor 5 con la variable s para intentar extraer la
primera palabra, pero esto sería un error porque el contenido de s ha cambiado desde que
guardamos 5 en word .
¡Tener que preocuparse de que el índice de word no esté sincronizado con los datos en s
es tedioso y propenso a errores! El manejo de estos índices es aún más frágil si escribimos
una segunda función llamada second_word . Su firma debería ser algo como esto:
Ahora que estamos rastreando tanto el índice de inicio como el de fin, y tenemos aún más
que se calcularon a partir de los datos en un estado particular, pero que no están
vinculados a ese estado en absoluto. Tenemos tres variables no relacionados flotando por
ahi que necesitan mantenerse sincronizadas.
Afortunadamente, Rust tiene una solución a este problema: los string slices.
String Slices
En lugar de referenciar todo el String , hello es una referencia a una porción del String ,
especificada en el segmento adicional [0..5] . Creamos slices usando un rango dentro de
corchetes, especificando [starting_index..ending_index] , donde starting_index es la
primera posición en el slice y ending_index es una posición más que la última posición en
el slice. Internamente, la estructura de datos del slice almacena la posición inicial y la
longitud del slice, lo que corresponde a ending_index menos starting_index . Por lo
tanto, en el caso de let world = &s[6..11]; , world sería un slice que contiene un
puntero al byte en el índice 6 de s con un valor de longitud de 5 .
https://book.rustlang-es.org/print 104/656
12/7/24, 22:37 El Lenguaje de Programación Rust
s
name value index value
ptr 0 h
len 11 1 e
capacity 11 2 l
3 l
world 4 o
name value 5
ptr 6 w
len 5 7 o
8 r
9 l
10 d
let s = String::from("hello");
Del mismo modo, si el slice incluye el último byte del String , podemos eliminar el número
final. Esto significa que son iguales:
let s = String::from("hello");
También podemos omitir ambos valores para tomar un slice de todo el string. Entonces
estos son iguales:
https://book.rustlang-es.org/print 105/656
12/7/24, 22:37 El Lenguaje de Programación Rust
let s = String::from("hello");
Nota: Los índices del rango del string slice deben ocurrir en límites válidos de
caracteres UTF-8. Si intentamos crear un string slice en medio de un caracter
multibyte, tu programa saldrá con un error. Para fines de introducción a los string
slices, estamos asumiendo solo ASCII en esta sección; una discusión más completa
sobre el manejo de UTF-8 se encuentra en la sección Almacenando texto codificado en
UTF-8 con Strings del Capítulo 8.
Con toda esta información en mente, reescribamos first_word para retornar un slice. El
tipo que significa “string slice” se escribe como &str :
Filename: src/main.rs
&s[..]
}
Ahora cuando llamamos a first_word , obtenemos un único valor que está vinculado a los
datos subyacentes. El valor se compone de una referencia al punto de inicio del slice y el
número de elementos en el slice.
Ahora tenemos una API sencilla que es mucho más difícil de estropear porque el compilador
se asegurará de que las referencias dentro del String sigan siendo válidas. ¿Recuerdas el
error en el programa en Listing 4-8, cuando obtuvimos el índice para el final de la primera
https://book.rustlang-es.org/print 106/656
12/7/24, 22:37 El Lenguaje de Programación Rust
palabra, pero luego limpiamos el string de modo que nuestro índice era inválido? Ese código
era lógicamente incorrecto, pero no mostraba errores inmediatos. Los problemas
aparecerían más tarde si seguimos intentando usar el índice de la primera palabra con un
string vacío. Los Slices hacen que este error sea imposible y nos permiten saber que
tenemos un problema en nuestro código mucho antes. El uso de la versión slice de
first_word arrojará un error en tiempo de compilación:
Filename: src/main.rs
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!
| ^^^^^^^^^ 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
Recordemos que las reglas del borrowing si tenemos una referencia immutable a algo, no
podemos tomar también una referencia mutable. Debido a que clear necesita truncar el
String , necesita obtener una referencia mutable. El println! después de la llamada a
clear usa la referencia en word , por lo que la referencia inmutable debe seguir activa en
ese punto. Rust impide que la referencia mutable en clear y la referencia inmutable en
word existan al mismo tiempo, y la compilación falla. No solo Rust ha hecho más fácil
nuestra API, sino que también ha eliminado una clase entera de errores en tiempo de
compilación.
https://book.rustlang-es.org/print 107/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Recordemos que hablamos sobre que los string literales se almacenan dentro del binario.
Ahora que sabemos sobre los slices, podemos entender correctamente los string literales:
El tipo de s aquí es &str : es un slice apuntando a ese punto específico del binario. Esto
también es por qué los literales de string son inmutables; &str es una referencia
inmutable.
Conociendo que puedes tomar slices de literales y valores String nos lleva a una mejora
más en first_word , y es su firma:
Listing 4-9: Mejorando la función first_word usando un string slice como parámetro para el tipo del
parámetro de s
Si tenemos un string slice, podemos pasar directamente ese valor. Si tenemos un String ,
podemos pasar un slice del String o una referencia al String . Esta flexibilidad aprovecha
las deref coercions, una característica que veremos en la sección "Tratando los Smart
Pointers como Referencias Regulares con el Trait Deref" del Capítulo 15.
Definir una función para tomar un string slice en lugar de una referencia a un String hace
que nuestra API sea más general y útil sin perder ninguna funcionalidad:
Filename: src/main.rs
https://book.rustlang-es.org/print 108/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let my_string = String::from("hello world");
Otros Slices
Los string slices, como puedes imaginar, son específicos para strings. Pero hay un tipo de
slice más general. Considera este array:
Así como podemos querer referirnos a parte de un string, también podemos querer
referirnos a parte de un array. Lo haríamos de esta manera:
Este slice tiene el tipo &[i32] . Funciona de la misma manera que los slices de string,
almacenando una referencia al primer elemento y una longitud. Usarás este tipo de slice
para todo tipo de colecciones. Hablaremos de estas colecciones en detalle cuando veamos
vectores en el Capítulo 8.
https://book.rustlang-es.org/print 109/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Resumen
Los conceptos de ownership, borrowing, y slices aseguran la seguridad de la memoria en los
programas Rust en tiempo de compilación. El lenguaje Rust te da control sobre el uso de la
memoria de la misma manera que otros lenguajes de programación de sistemas, pero el
hecho de que el propietario de los datos limpie automáticamente esos datos cuando salen
del scope significa que no tienes que escribir y depurar código extra para obtener este
control.
El ownership afecta a cómo funcionan otras partes de Rust, así que hablaremos de estos
conceptos más adelante en el libro. Vamos a pasar al Capítulo 5 y ver cómo agrupar piezas
de datos en un struct .
https://book.rustlang-es.org/print 110/656
12/7/24, 22:37 El Lenguaje de Programación Rust
https://book.rustlang-es.org/print 111/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para definir un struct, debemos usar la palabra clave struct y el nombre del struct
completo. El nombre del struct debe describir el significado de los datos que se agrupan.
Entonces, entre llaves, definimos los nombres y tipos de datos, que llamaremos campos. Por
ejemplo, en el Listing 5-1 mostramos una definición de un struct que almacena información
sobre una cuenta de usuario.
Filename: src/main.rs
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
Para usar un struct después de haberlo definido, creamos una instancia de ese struct
especificando valores concretos para cada uno de los campos. Creamos una instancia al
declarar el nombre del struct y luego agregar llaves que contienen clave: valor pares, donde
las claves son los nombres de los campos y los valores son los datos que queremos
almacenar en esos campos. No tenemos que especificar los campos en el mismo orden en
el que los declaramos en el struct. En otras palabras, la definición del struct es como una
plantilla general para el tipo, y las instancias llenan esa plantilla con datos particulares para
crear valores del tipo. Por ejemplo, podemos declarar un usuario en particular como se
muestra en el Listing 5-2.
Filename: src/main.rs
https://book.rustlang-es.org/print 112/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};
}
Para acceder a un valor específico de un struct, usamos la notación de punto. Por ejemplo,
para acceder a la dirección de correo electrónico de este usuario, usamos user1.email . Si
la instancia es mutable, podemos cambiar un valor asignando en un campo particular. El
Listing 5-3 muestra cómo cambiar el valor en el campo email de una instancia mutable de
User .
Filename: src/main.rs
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};
user1.email = String::from("[email protected]");
}
Nota que toda la instancia debe ser mutable; Rust no nos permite marcar solo ciertos
campos como mutables. Como cualquier expresión, podemos construir una nueva instancia
del struct como la última expresión en el cuerpo de la función para devolver implícitamente
esa nueva instancia.
Listing 5-4 muestra una función build_user que devuelve una instancia de User con el
correo electrónico y el nombre de usuario dados. El campo active obtiene el valor de
true , y el campo sign_in_count obtiene el valor de 1 .
Filename: src/main.rs
https://book.rustlang-es.org/print 113/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Listing 5-4: Una función build_user que toma un email y username y devuelve una instancia User
Tiene sentido nombrar los parámetros de la función con el mismo nombre que los campos
del struct, pero tener que repetir los nombres de los campos y variables email y username
es un poco tedioso. Si el struct tuviera más campos, repetir cada nombre sería aún más
molesto. Afortunadamente, hay una conveniente forma abreviada.
Debido a que los nombres de los parámetros y los nombres de los campos del struct son
exactamente los mismos en el Listing 5-4, podemos usar la abreviatura Field Init para
reescribir build_user para que se comporte exactamente igual pero no tenga la repetición
de username y email , como se muestra en el Listing 5-5.
Filename: src/main.rs
Listing 5-5: Una función build_user que usa field init abreviado porque los parámetros username e email
Aquí, estamos creando una nueva instancia del struct User , que tiene un campo llamado
email . Queremos establecer el valor del campo email en el valor del parámetro email de
la función build_user . Debido a que el campo email y el parámetro email tienen el
mismo nombre, solo necesitamos escribir email en lugar de email: email .
Suele ser útil crear una nueva instancia de un struct que incluya la mayoría de los valores de
otra instancia, pero cambie algunos. Puede hacer esto usando la sintaxis de struct update.
https://book.rustlang-es.org/print 114/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Primero, en el Listing 5-6 mostramos cómo crear una nueva instancia de User
regularmente, sin la sintaxis de actualización. Establecemos un nuevo valor para email ,
pero de lo contrario usamos los mismos valores de user1 que creamos en el Listing 5-2.
Filename: src/main.rs
fn main() {
// --snip--
Listing 5-6: Creando una nueva instancia User usando uno de los valores de user1
Usando la sintaxis de struct update, podemos lograr el mismo efecto con menos código,
como se muestra en el Listing 5-7. La sintaxis .. especifica que los campos restantes que
no se establecen explícitamente deben tener el mismo valor que los campos en la instancia
dada.
Filename: src/main.rs
fn main() {
// --snip--
Listing 5-7: Usando una sintaxis de struct update para introducir un nuevo valor email para una instancia
User pero para usar el resto de los valores de user1
El código en el Listing 5-7 también crea una instancia en user2 que tiene un valor diferente
para email pero tiene los mismos valores para los campos username , active y
sign_in_count de user1 . Él ..user1 debe ir al final para especificar que cualquier campo
restante debe obtener sus valores del campo correspondiente en user1 , pero podemos
elegir especificar valores para tantos campos como queramos en cualquier orden,
independientemente del orden de los campos en la definición del struct.
Nota que la sintaxis de update struct usa = como una asignación; esto es porque mueve los
datos, como vimos en la sección “Variables y datos interactuando con Move”. En este
ejemplo, ya no podemos usar user1 como un todo después de crear user2 porque el
String en el campo username de user1 se movió a user2 . Si hubiéramos dado a user2
https://book.rustlang-es.org/print 115/656
12/7/24, 22:37 El Lenguaje de Programación Rust
nuevos valores String para email y username , y por lo tanto solo usamos los valores de
active y sign_in_count de user1 , entonces user1 todavía sería válido después de crear
user2 . Tanto active como sign_in_count son tipos que implementan la trait Copy , por
lo que el comportamiento que discutimos en la sección “Datos de pila: Copy” se aplicaría.
Rust también admite structs que se parecen a tuplas, llamados structs de tuplas. Los structs
de tuplas tienen el significado adicional que proporciona el nombre del struct, pero no
tienen nombres asociados a sus campos; en su lugar, solo tienen los tipos de los campos.
Los structs de tuplas son útiles cuando desea darle un nombre al conjunto completo y hacer
que el conjunto sea un tipo diferente de otros conjuntos, y cuando nombrar cada campo
como en un struct regular sería verboso o redundante.
Para definir un struct de tupla, comience con la palabra clave struct y el nombre del struct
seguido por los tipos en la tupla. Por ejemplo, aquí definimos y usamos dos structs de tupla
llamados Color y Point :
Filename: src/main.rs
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Nota que los valores black y origin son diferentes tipos porque son instancias de
diferentes structs de tupla. Cada struct que defina es su propio tipo, incluso si los campos
dentro del struct tienen los mismos tipos. Por ejemplo, una función que toma un parámetro
de tipo Color no puede tomar un Point como argumento, incluso si ambos tipos están
compuestos por tres valores i32 . De lo contrario, las instancias de structs de tupla son
similares a las tuplas en que puede descomponerlas en sus piezas individuales, y puede
usar un . seguido por el índice para acceder a un valor individual.
También puede definir structs que no tienen ningún campo. Estos se llaman structs de
unidad porque se comportan de manera similar a () , el tipo de unidad que mencionamos
en la sección “El tipo de tupla”. Los structs de unidad pueden ser útiles cuando necesita
implementar un trait en algún tipo, pero no tiene datos que desea almacenar en el tipo
https://book.rustlang-es.org/print 116/656
12/7/24, 22:37 El Lenguaje de Programación Rust
propio. Discutiremos los traits en el Capítulo 10. Aquí hay un ejemplo de declarar e
instanciar un struct de unidad llamado AlwaysEqual :
Filename: src/main.rs
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
Para definir AlwaysEqual , usamos la palabra clave struct , el nombre que queremos y
luego un punto y coma. ¡No se necesitan llaves ni paréntesis! Luego podemos obtener una
instancia de AlwaysEqual en la variable subject de la misma manera: usando el nombre
que definimos, sin llaves ni paréntesis. Imagina que más tarde implementaremos un
comportamiento para este tipo de tal manera que cada instancia de AlwaysEqual siempre
sea igual a cada instancia de cualquier otro tipo, tal vez para tener un resultado conocido
para fines de prueba. No necesitaríamos ningún dato para implementar ese
comportamiento. Verás en el Capítulo 10 cómo definir traits e implementarlos en cualquier
tipo, incluidos los structs de unidad.
En el struct User de la definición en el Listing 5-1, usamos el tipo String en lugar del
tipo &str de la cadena de caracteres. Esta es una elección deliberada porque
queremos que cada instancia de este struct tenga todos sus datos y que esos datos
sean válidos durante todo el tiempo que el struct sea válido.
También es posible para los structs almacenar referencias a datos que son propiedad
de algo más, pero para hacerlo requiere el uso de lifetimes, una característica de Rust
que discutiremos en el Capítulo 10. Los lifetimes garantizan que los datos
referenciados por un struct sean válidos durante el tiempo que el struct sea válido.
Digamos que intentas almacenar una referencia en un struct sin especificar lifetimes,
como el siguiente; esto no funcionará:
Filename: src/main.rs
https://book.rustlang-es.org/print 117/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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,
|
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, discutiremos como solucionar estos errores para que puedas
almacenar referencias en structs, pero por ahora, solucionaremos los errores usando
tipos propios como String en lugar de referencias como &str .
https://book.rustlang-es.org/print 118/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Hagamos un nuevo proyecto binario con Cargo llamado rectangles que tomará el ancho y el
alto de un rectángulo especificado en píxeles y calculará el área del rectángulo. La lista 5-8
muestra un programa corto con una forma de hacer exactamente eso en el src/main.rs de
nuestro proyecto.
Filename: src/main.rs
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
Listing 5-8: Calculando el área de un rectángulo especificado por separado en variables ancho y alto
$ 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 calcular el área del rectángulo llamando a la función area con cada
dimensión, pero podemos hacer más para hacer este código claro y legible.
La función area está supuestamente para calcular el área de un rectángulo, pero la función
que escribimos tiene dos parámetros, y no está claro en ningún lugar de nuestro programa
https://book.rustlang-es.org/print 119/656
12/7/24, 22:37 El Lenguaje de Programación Rust
que los parámetros están relacionados. Sería más legible y más manejable agrupar el ancho
y el alto juntos. Ya hemos discutido una forma de hacerlo en la sección “El Tipo Tupla” del
Capítulo 3: usando tuplas.
Listings 5-9 muestra otra versión de nuestro programa que usa tuplas.
Filename: src/main.rs
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
Listing 5-9: Especificando el ancho y alto del rectángulo con una tupla
En un sentido, este programa es mejor. Las tuplas nos permiten agregar un poco de
estructura, y ahora estamos pasando solo un argumento. Pero en otro sentido, esta versión
es menos clara: las tuplas no nombran sus elementos, por lo que tenemos que indexar los
componentes de la tupla, haciendo que nuestro cálculo sea menos obvio.
Mezclar el ancho y el alto no importaría para el cálculo del área, pero si queremos dibujar el
rectángulo en la pantalla, ¡importaría! Tendríamos que tener en cuenta que width es el
índice de la tupla 0 y height es el índice de la tupla 1 . ¡Esto sería aún más difícil para que
otra persona lo descubriera y lo tuviera en cuenta si usara nuestro código! Debido a que no
hemos transmitido el significado de nuestros datos en nuestro código, ahora es más fácil
introducir errores.
Hemos usado structs para agregar significado al etiquetar los datos. Podemos transformar
la tupla que estamos usando en un struct con un nombre para la estructura y nombres para
los campos, como se muestra en la lista 5-10.
Filename: src/main.rs
https://book.rustlang-es.org/print 120/656
12/7/24, 22:37 El Lenguaje de Programación Rust
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
Hemos definido un struct y lo hemos llamado Rectangle . Dentro de las llaves, hemos
definido los campos como width y height , ambos de los cuales tienen el tipo u32 . Luego,
en main , hemos creado una instancia particular de Rectangle que tiene un ancho de 30 y
un alto de 50 .
Nuestra función area ahora toma un argumento que es una referencia a una instancia de
Rectangle en lugar de dos parámetros numéricos. En la función, usamos el punto para
acceder a los campos de la instancia de Rectangle que recibimos como argumento. En
main , creamos una instancia de Rectangle y llamamos a la función area con la instancia
de Rectangle como argumento.
Sería útil poder imprimir una instancia de Rectangle mientras estamos depurando nuestro
programa y ver los valores de todos sus campos. La lista 5-11 intenta usar la macro
println! como hemos usado en capítulos anteriores. Sin embargo, esto no funcionará.
https://book.rustlang-es.org/print 121/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/main.rs
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 macro println! puede hacer muchos tipos de formateo, y por defecto, las llaves curvas
le dicen a println! que use el formateo conocido como Display : salida destinada al
consumo directo del usuario final. Los tipos primitivos que hemos visto hasta ahora
implementan Display por defecto porque solo hay una forma en que querrías mostrar un
1 u otro tipo primitivo a un usuario. Pero con las estructuras, la forma en que println!
debe formatear la salida es menos clara porque hay más posibilidades de visualización:
¿Quieres comas o no? ¿Quieres imprimir las llaves curvas? ¿Deben mostrarse 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 Display para usar con
println! y el marcador de posición {} .
Compilamos el código con este cambio. ¡Oh, no! Todavía obtenemos un error:
https://book.rustlang-es.org/print 122/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Rust si incluye la funcionalidad para imprimir información de depuración, pero tenemos que
optar explícitamente para hacer que esa funcionalidad esté disponible para nuestra
estructura. Para hacer eso, agregamos el atributo externo #[derive(Debug)] justo antes de
la definición de la estructura, como se muestra en la lista 5-12.
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1:?}");
}
Listing 5-12: Agregando el atributo para derivar el trait Debug e imprimiendo la instancia Rectangle usando el
formato debug
$ 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 }
¡Bien! No es la salida más bonita, pero muestra los valores de todos los campos de esta
instancia, lo que definitivamente ayudaría durante la depuración. Cuando tenemos
estructuras más grandes, es útil tener una salida que sea un poco más fácil de leer; en esos
casos, podemos usar {:#?} en lugar de {:?} en el string println! . En este ejemplo, el
uso del estilo {:#?} producirá la siguiente salida:
https://book.rustlang-es.org/print 123/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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,
}
Otra forma de imprimir un valor usando el formato Debug es usar la macro dbg! , que
toma el ownership de una expresión (en oposición a println! , que toma una referencia),
imprime el archivo y el número de línea donde se produce esa llamada a la macro dbg! en
su código junto con el valor resultante de esa expresión, y devuelve el ownership del valor.
Aquí hay un ejemplo en el que estamos interesados en el valor que se asigna al campo
width , así como el valor de todo el struct en rect1 :
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
https://book.rustlang-es.org/print 124/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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,
}
Podemos ver que el primer bit de salida proviene de la linea 10 de src/main.rs donde
estamos depurando la expresión 30 * scale , y su valor resultante es 60 (el formato de
Debug implementado para enteros es imprimir sólo su valor). La llamada a dbg! en la línea
14 de src/main.rs produce el valor de &rect1 , que es la estructura de Rectangle . Esta
salida usa el formato Debug de la estructura Rectangle . La macro dbg! puede ser
realmente útil cuando está tratando de averiguar qué está haciendo su código.
Además del trait Debug , Rust nos ha proporcionado un número de traits para que podamos
usar con el atributo derive que pueden agregar un comportamiento útil a nuestros tipos
personalizados. Esos traits y sus comportamientos se enumeran en el Apéndice C.
Cubriremos cómo implementar estos traits con un comportamiento personalizado, así
como cómo crear sus propios traits en el Capítulo 10. También hay muchos atributos más
allá de derive ; para obtener más información, consulte la sección “Atributos” de la
Referencia de Rust.
Nuestra función area es muy específica: solo calcula el área de rectángulos. Sería útil
vincular este comportamiento más estrechamente a nuestra estructura Rectangle porque
no funcionará con ningún otro tipo. Veamos cómo podemos continuar refactorizando este
código al convertir la función area en un método area definido en nuestro tipo
Rectangle .
https://book.rustlang-es.org/print 125/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Sintaxis de Métodos
Los métodos son similares a las funciones: los declaramos con la palabra clave fn y un
nombre, pueden tener parámetros y un valor de retorno, y contienen alguno código que se
ejecuta cuando el método es llamado desde otro lugar. A diferencia de las funciones, los
métodos se definen dentro del contexto de una estructura (o un enum o un objeto de tipo
trait, que cubriremos en el Capítulo 6 y el Capítulo 17, respectivamente), y su primer
parámetro siempre es self , que representa la instancia de la estructura en la que se está
llamando al método.
Definiendo Metodos
Vamos a cambiar la función area que tiene una instancia de Rectangle como parámetro y
en vez de eso definamos un método area en el struct Rectangle , como se muestra en el
Listado 5-13.
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
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()
);
}
Para definir la función dentro del contexto de Rectangle , iniciamos un bloque impl
(implementación). Todo lo que esté dentro de este bloque impl estará asociado al tipo
https://book.rustlang-es.org/print 126/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Rectangle . Luego movemos la función area dentro de las llaves del bloque impl y
cambiamos el primer (y en este caso, único) parámetro para ser self en la firma y en todas
partes dentro del cuerpo. En main , donde llamamos a la función area y pasamos rect1
como argumento, podemos en vez de eso usar la sintaxis de método para llamar al método
area en nuestra instancia de Rectangle . La sintaxis de método va después de una
instancia: se agrega un punto seguido del nombre del método, paréntesis y cualquier
argumento.
Elegimos &self aquí por la misma razón que usamos &Rectangle en la versión de la
función: no queremos tomar la propiedad, y solo queremos leer los datos en la estructura,
no escribir en ella. Si quisiéramos cambiar la instancia en la que hemos llamado al método
como parte de lo que el método hace, usaríamos &mut self como primer parámetro.
Tener un método que tome la propiedad de la instancia usando solo self como primer
parámetro es raro; esta técnica se usa normalmente cuando el método transforma self en
otra cosa y quieres evitar que el que llama al método use la instancia original después de la
transformación.
La razón principal para usar métodos en vez de funciones, además de proveer la sintaxis de
método y no tener que repetir el tipo de self en cada firma de método, es para la
organización. Hemos puesto todas las cosas que podemos hacer con una instancia de un
tipo en un bloque impl en vez de hacer que los usuarios futuros de nuestro código
busquen las capacidades de Rectangle en varios lugares en la biblioteca que proveemos.
Nota que podemos elegir darle al método el mismo nombre que uno de los campos del
struct. Por ejemplo, podemos definir un método en Rectangle que se llame width :
Filename: src/main.rs
https://book.rustlang-es.org/print 127/656
12/7/24, 22:37 El Lenguaje de Programación Rust
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}
Aquí, estamos eligiendo que el método width retorne true si el valor en el campo width
de la instancia es mayor que 0 y false si el valor es 0 : podemos usar un campo dentro de
un método del mismo nombre para cualquier propósito. En main , cuando seguimos
rect1.width con paréntesis, Rust sabe que queremos decir el método width . Cuando no
usamos paréntesis, Rust sabe que queremos decir el campo width .
A veces, pero no siempre, cuando damos un método el mismo nombre que un campo
queremos que solo retorne el valor en el campo y no haga nada más. Los métodos como
este se llaman getters, y Rust no los implementa automáticamente para los campos de un
struct como lo hacen otros lenguajes. Los getters son útiles porque puedes hacer que el
campo sea privado, pero el método sea público, y así permitir acceso de solo lectura a ese
campo como parte de la API pública del tipo. Hablaremos de qué es público y privado y
cómo designar un campo o método como público o privado en el Capítulo 7.
Rust no tiene un equivalente al operador -> ; en su lugar, Rust tiene una característica
llamada referenciación y desreferenciación automáticas. Llamar a métodos es uno de los
pocos lugares en Rust donde se tiene este comportamiento.
https://book.rustlang-es.org/print 128/656
12/7/24, 22:37 El Lenguaje de Programación Rust
p1.distance(&p2);
(&p1).distance(&p2);
Filename: src/main.rs
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
La salida esperada se vería como la siguiente porque ambas dimensiones de rect2 son
más pequeñas que las dimensiones de rect1 , pero rect3 es más ancha que rect1 :
https://book.rustlang-es.org/print 129/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Sabemos que queremos definir un método, por lo que estará dentro del bloque impl
Rectangle . El nombre del método será can_hold , y tomará un préstamo inmutable de otro
Rectangle como parámetro. Podemos decir 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 que main conserve la propiedad de rect2 para que
podamos usarlo nuevamente después de llamar al método can_hold . El valor de retorno
de can_hold será un Booleano, y la implementación verificará si el ancho y alto de self
son mayores que el ancho y alto del otro Rectangle , respectivamente. Agreguemos el
nuevo método can_hold al bloque impl del Listing 5-13 que se muestra en el Listing 5-15.
Filename: src/main.rs
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
Listing 5-15: Implementando el método can_hold en Rectangle que toma otra instancia de Rectangle como
un parámetro
Cuando ejecutamos este código con la función main en el Listing 5-14, obtendremos el
resultado deseado. Los métodos pueden tomar múltiples parámetros que agregamos a la
firma después del parámetro self , y esos parámetros funcionan igual que los parámetros
en las funciones.
Funciones asociadas
Todas las funciones definidas dentro de un bloque impl se llaman funciones asociadas
porque están asociadas con el tipo nombrado después del impl . Podemos definir
funciones asociadas que no tengan self como su primer parámetro (y, por lo tanto, no
sean métodos) porque no necesitan una instancia del tipo con el que trabajar. Ya hemos
usado una función como esta: la función String::from que está definida en el tipo String .
Las funciones asociadas que no son métodos son a menudo utilizadas para constructores
que devolverán una nueva instancia de la estructura. Estás a menudo se llaman new , pero
new no es un nombre especial y no está incorporado en el lenguaje. Por ejemplo,
podríamos elegir proporcionar una función asociada llamada square que tendría un
https://book.rustlang-es.org/print 130/656
12/7/24, 22:37 El Lenguaje de Programación Rust
parámetro de dimensión y lo usaría como ancho y alto, de modo que sea más fácil crear un
Rectangle cuadrado en lugar de tener que especificar el mismo valor dos veces:
Filename: src/main.rs
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
Para llamar a esa función asociada, usamos la sintaxis :: con el nombre de la estructura;
let sq = Rectangle::square(3); es un ejemplo. Esta función está dentro del namespace
de la estructura. La sintaxis :: se usa tanto para las funciones asociadas como para los
namespaces creados por los módulos. Discutiremos los módulos en el Capítulo 7.
Cada struct es permitido tener múltiples bloques impl . Por ejemplo, el Listing 5-15 es
equivalente al código mostrado en el Listing 5-16, que tiene cada método en su propio
bloque impl .
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
}
}
No hay razón para separar estos métodos en múltiples bloques impl aquí, pero esta es una
sintaxis válida. Veremos un caso en el que los múltiples bloques impl son útiles en el
Capítulo 10, donde discutiremos los tipos genéricos y los traits.
https://book.rustlang-es.org/print 131/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Resumen
Los structs te permiten crear tipos personalizados que son significativos para su dominio. Al
usar structs, puede mantener piezas de datos asociadas entre sí y nombrar cada pieza para
hacer que su código sea claro. En los bloques impl , puede definir funciones que están
asociadas con su tipo, y los métodos son un tipo de función asociada que le permite
especificar el comportamiento que tienen las instancias de sus structs.
Pero los structs no son la única forma de crear tipos personalizados: pasemos a la función
enum de Rust para agregar otra herramienta a su toolbox.
https://book.rustlang-es.org/print 132/656
12/7/24, 22:37 El Lenguaje de Programación Rust
https://book.rustlang-es.org/print 133/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Definiendo un Enum
Los struct te permiten agrupar campos relacionados y datos, como un Rectángulo con su
ancho y largo . Por otro lado, los enums te permiten decir que un valor es uno de un
conjunto de posibles valores. Por ejemplo, podríamos querer decir que Rectángulo es uno
de un conjunto de posibles formas que también incluye Circulo y Triangulo . Para hacer
esto, Rust nos permite codificar estas posibilidades como un enum .
Vamos a ver una situación que podemos expresar en código y veremos por qué los enums
son útiles y más apropiados que los structs en este caso. Digamos que tenemos que
trabajar con direcciones IP. Actualmente, existen dos estándares que se usan para
direcciones IP: la versión cuatro y la versión seis. Como estos son los únicos posibles tipos
de direcciones IP que nuestro programa encontrará, podemos enumerar todas las variantes
posibles, de donde viene el nombre de enum .
Cualquier dirección IP puede ser una dirección de la versión cuatro o la versión seis, pero no
ambas al mismo tiempo. Esa propiedad de las direcciones IP hace que la estructura de
datos enum sea apropiada porque un valor enum puede ser sólo una de sus variantes.
Tanto las direcciones de la versión cuatro como la versión seis siguen siendo
fundamentalmente direcciones IP, por lo que deben ser tratadas como el mismo tipo
cuando el código está manejando situaciones que se aplican a cualquier tipo de dirección IP.
enum IpAddrKind {
V4,
V6,
}
IpAddrKind ahora es un tipo de datos personalizado que podemos usar en otras partes de
nuestro código.
Valores Enum
Podemos crear instancias de cada una de las dos variantes de IpAddrKind de esta manera:
Nota que las variantes del enum están en el mismo espacio de nombres bajo su
identificador, y usamos dos puntos para separar los dos. Esto es útil porque ahora ambos
https://book.rustlang-es.org/print 134/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn route(ip_kind: IpAddrKind) {}
route(IpAddrKind::V4);
route(IpAddrKind::V6);
Usando enum tiene aún más ventajas. Pensando más en nuestro tipo de dirección IP, en
este momento no tenemos una forma de almacenar los datos reales de la dirección IP; solo
sabemos qué tipo es. Dado que acabas de aprender sobre los structs en el Capítulo 5,
podrías estar tentado a abordar este problema con structs como se muestra en el Listing 6-
1.
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
Listing 6-1: Almacenando los datos y la variante IpAddrKind de una dirección IP usando un struct
Aquí, hemos definido un struct IpAddr que tiene dos campos: un campo kind que es de
tipo IpAddrKind (el enum que definimos anteriormente) y un campo address de tipo
String . Tenemos dos instancias de este struct. La primera es home , y tiene el valor
IpAddrKind::V4 como su kind como datos de dirección asociados de 127.0.0.1 . La
segunda instancia es loopback . Tiene la otra variante de IpAddrKind como su valor kind ,
V6 , y tiene la dirección ::1 asociada con ella. Hemos usado un struct para agrupar los
valores kind y address juntos, así que ahora la variante está asociada con el valor.
Sin embargo, representar el mismo concepto usando sólo un enum es más conciso: en lugar
de un enum dentro de un struct, podemos poner datos directamente en cada variante de
https://book.rustlang-es.org/print 135/656
12/7/24, 22:37 El Lenguaje de Programación Rust
enum . Esta nueva definición del enum IpAddr dice que tanto las variantes V4 como V6
tendrán valores String asociados:
enum IpAddr {
V4(String),
V6(String),
}
Adjuntamos datos a cada variante del enum directamente, por lo que no hay necesidad de
un struct extra. Aquí, también es más fácil ver otro detalle de cómo funcionan los enums: el
nombre de cada variante de enum que definimos también se convierte en una función que
construye una instancia del tipo enum . Es decir, IpAddr::V4() es una llamada a función
que toma un argumento String y devuelve una instancia del tipo IpAddr . Obtenemos
automáticamente esta función constructora definida como resultado de definir el enum .
Hay otra ventaja de usar un enum en lugar de un struct: cada variante puede tener
diferentes tipos y cantidades de datos asociados con ella. La versión cuatro de las
direcciones IP siempre tendrá cuatro componentes numéricos que tendrán valores entre 0 y
255. Si quisiéramos almacenar las direcciones V4 como cuatro valores u8 pero aun así
expresar las direcciones V6 como un valor String , no podríamos hacerlo con un struct.
Los enums manejan este caso con facilidad:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
Hemos mostrado varias formas diferentes de definir estructuras de datos para almacenar
direcciones IP de la versión cuatro y de la versión seis. Sin embargo, resulta que querer
almacenar direcciones IP y codificar de que tipo son es tan común que la biblioteca estándar
tiene una definición que podemos usar! Veamos cómo define la biblioteca estándar IpAddr :
tiene el enum exacto y las variantes que hemos definido y usado, pero incrusta los datos de
dirección dentro de las variantes en forma de dos structs diferentes, que se definen de
manera diferente para cada variante:
https://book.rustlang-es.org/print 136/656
12/7/24, 22:37 El Lenguaje de Programación Rust
struct Ipv4Addr {
// --snip--
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
Este código ilustra que puedes poner cualquier tipo de datos dentro de una variante de
enum : strings, tipos numéricos o structs, por ejemplo. ¡Incluso puedes incluir otro enum !
Además, los tipos de biblioteca estándar a menudo no son mucho más complicados de lo
que podrías idear.
Ten en cuenta que aunque la biblioteca estándar contiene una definición para IpAddr , aún
podemos crear y usar nuestra propia definición sin conflicto porque no hemos traído la
definición de la biblioteca estándar a nuestro contexto de ejecución. Hablaremos más sobre
cómo traer tipos al contexto de ejecución en el Capítulo 7.
Veamos otro ejemplo de una enumeración en el Listing 6-2: este tiene una amplia variedad
de tipos incrustados en sus variantes.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Listing 6-2: Un enum Message cuyas variantes almacenan diferentes cantidades y tipos de valores
Definiendo un enum con variantes como las del Listing 6-2 es similar a definir diferentes
tipos de definiciones de struct, excepto que el enum no use la palabra clave struct y todas
las variantes están agrupadas juntas bajo el tipo Message . Los siguientes structs podrían
contener los mismos datos que las variantes de enum anteriores:
https://book.rustlang-es.org/print 137/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Pero si usamos los diferentes structs, cada uno de los 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 con el enum Message definido en el Listing 6-2, que es un solo
tipo.
Hay una similitud entre enums y structs que puede ser útil de recordar: al igual que puedes
definir métodos en structs usando impl , puedes definir métodos en enums. Aquí hay un
método llamado call que podemos definir en nuestro enum Message :
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 usaría self para obtener el valor en el que llamamos el método. En
este ejemplo, hemos creado una variable m que tiene el valor
Message::Write(String::from("hello")) , y eso es lo que será self en el cuerpo del
método call cuando se ejecute m.call() .
Veamos otro enum en la librería estándar que es muy común y útil: Option .
Esta sección explora un caso de estudio de Option , que es otro enum definido por la
biblioteca estándar. El tipo Option codifica el escenario muy común en el que un valor
podría ser algo o podría ser nada.
Por ejemplo, si solicita el primer elemento de una lista no vacía, obtendría un valor. Si
solicita el primer elemento de una lista vacía, no obtendría nada. Expresar este concepto en
términos del sistema de tipos significa que el compilador puede verificar si ha manejado
todos los casos que debería estar manejando; esta funcionalidad puede prevenir errores
que son extremadamente comunes en otros lenguajes de programación.
https://book.rustlang-es.org/print 138/656
12/7/24, 22:37 El Lenguaje de Programación Rust
también. Rust no tiene la característica de null que muchos otros lenguajes tienen. Null es
un valor que significa que no hay ningún valor allí. En los lenguajes con null, las variables
siempre pueden estar en uno de dos estados: null o no null.
En su presentación del 2009 “Null References: The Billion Dollar Mistake”, Tony Hoare, el
inventor de null, tiene esto que decir:
El problema con los valores null es que si intentas utilizar un valor null como un valor no
null, obtendrás un error de algún tipo. Debido a que esta propiedad nula o no nula es
omnipresente, es extremadamente fácil cometer este tipo de error.
Sin embargo, el concepto que null está tratando de expresar sigue siendo útil: un null es un
valor que es actualmente inválido o ausente por alguna razón.
enum Option<T> {
None,
Some(T),
}
El enum Option<T> es tan útil que incluso está incluido en el prelude; no necesitas traerlo al
contexto de ejecución explícitamente. Sus variantes también están incluidas en el prelude:
puedes usar Some y None directamente sin el prefijo Option:: . El enum Option<T> es aún
un enum regular, y Some(T) y None son aún variantes de tipo Option<T> .
La sintaxis <T> es una característica de Rust que aún no hemos hablado. Es un parámetro
de tipo genérico, y cubriremos los genéricos en más detalle en el Capítulo 10. Por ahora,
todo lo que necesitas saber es que <T> significa que la variante Some del enum Option
puede contener una pieza de datos de cualquier tipo, y que cada tipo concreto que se usa
en lugar de T hace que el tipo Option<T> general sea un tipo diferente. Aquí hay algunos
ejemplos de usar valores Option para contener tipos de números y tipos de strings:
https://book.rustlang-es.org/print 139/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cuando tenemos un valor Some , sabemos que un valor está presente y el valor se mantiene
dentro del Some . Cuando tenemos un valor None , en cierto sentido significa lo mismo que
null: no tenemos un valor válido. Entonces, ¿por qué tener Option<T> es mejor que tener
null?
En resumen, porque Option<T> y T (donde T puede ser cualquier tipo) son tipos
diferentes, el compilador no nos permitirá usar un valor Option<T> como si fuera
definitivamente un valor válido. Por ejemplo, este código no se compilará, porque está
tratando de agregar un i8 a 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>`:
<i8 as Add>
<i8 as Add<&i8>>
<&'a i8 as Add<i8>>
<&i8 as Add<&i8>>
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 agregar un
i8 y un Option<i8> , porque son tipos diferentes. Cuando tenemos un valor de un tipo
https://book.rustlang-es.org/print 140/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En otras palabras, tienes que convertir un Option<T> a un T antes de que puedas realizar
operaciones T con él. Generalmente, esto ayuda a detectar uno de los errores más
comunes con null: asumiendo que algo no es null cuando realmente lo es.
Eliminar el riesgo de asumir incorrectamente un valor no null ayuda a tener más confianza
en su código. Para tener un valor que posiblemente pueda ser null, debe optar
explícitamente por hacer que el tipo de ese valor sea Option<T> . Entonces, cuando use ese
valor, se le requerirá expresar explícitamente el caso cuando el valor es null. Siempre que
un valor tenga un tipo que no sea Option<T> , se puede asumir con seguridad que el valor
no es null. Esta fue una decisión deliberada del diseño de Rust para limitar la omnipresencia
de nulls y aumentar la seguridad del código de Rust.
Entonces ¿cómo obtienes el valor T de un Some cuando tienes un valor de tipo Option<T>
para que puedas usar ese valor? El enum Option<T> tiene una gran cantidad de métodos
que son útiles en una variedad de situaciones; puedes verlos en su documentación.
Familiarizarse con los métodos en Option<T> será extremadamente útil en su viaje con
Rust.
En general, para usar un valor Option<T> , querrás tener código que maneje cada variante.
Quieres tener algún código que se ejecute solo cuando tienes un valor Some(T) , y este
código está permitido de usar el T interno. Quieres tener algún otro código que se ejecute
solo si tienes un valor None , y ese código no tiene un valor T disponible. La expresión
match es una construcción de flujo de control que hace exactamente esto cuando se usa
con enums: ejecutará diferente código dependiendo de la variante del enum que tenga, y
ese código puede usar los datos dentro del valor que coincida.
https://book.rustlang-es.org/print 141/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Piensa en una expresión match como una máquina de clasificación de monedas: las
monedas deslizan a lo largo de una pista con orificios de diversos tamaños a lo largo de ella,
y cada moneda cae a través del primer orificio que encuentra que se ajusta a ella. De la
misma manera, los valores pasan a través de cada patrón en un match , y en el primer
patrón en el que el valor “se ajusta”, el valor cae en el bloque de código asociado para ser
utilizado durante la ejecución.
Hablando de monedas, ¡usémoslas como un ejemplo usando match ! Podemos escribir una
función que tome una moneda desconocida de los Estados Unidos y, de una manera similar
a la máquina de conteo, determine qué moneda es y devuelva su valor en centavos, como
se muestra en el Listing 6-3.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
Listing 6-3: Una expresión enum y match que tiene las variantes del enum como sus patrones
https://book.rustlang-es.org/print 142/656
12/7/24, 22:37 El Lenguaje de Programación Rust
A continuación, dentro de las llaves de match , hay un número de Opciones. Una Opción
tiene dos partes: un patrón y algún código. La primera Opción aquí tiene un patrón que es el
valor Coin::Penny y luego el operador => que separa el patrón y el código a ejecutar. El
código en este caso es solo el valor 1 . Cada Opción está separado del siguiente con una
coma.
Cuando la expresión match se ejecuta, compara el valor resultante contra el patrón de cada
Opción, en orden. Si un patrón coincide con el valor, se ejecuta el código asociado con ese
patrón. Si ese patrón no coincide con el valor, la ejecución continúa en la siguiente Opción,
como en una máquina de clasificación de monedas. Podemos tener tantas Opciones como
necesitemos: en el Listado 6-3, nuestro match tiene cuatro Opciones.
El código asociado con cada Opción es una expresión, y el valor resultante de la expresión
en la Opción coincidente es el valor que se devuelve para la expresión match completa.
Otra característica útil de las Opciones de match es que pueden vincularse a las partes del
valor que coinciden con el patrón. Esto es cómo podemos extraer valores de las variantes
de enum.
https://book.rustlang-es.org/print 143/656
12/7/24, 22:37 El Lenguaje de Programación Rust
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
Listing 6-4: Un enum Coin en el cual la variante Quarter también contiene un valor UsState
Imaginemos que tenemos un amigo que está tratando de coleccionar todas las monedas de
50 estados. Mientras clasificamos nuestra moneda suelta por tipo de moneda, también
llamaremos al nombre del estado asociado con cada moneda de 50 centavos para que si es
uno que no tiene, pueda agregarlo a su colección.
https://book.rustlang-es.org/print 144/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Digamos que queremos escribir una función que tome un Option<i32> y, si hay un valor
dentro, agregue 1 a ese valor. Si no hay un valor dentro, la función debe devolver el valor
None y no intentar realizar ninguna operación.
Esta función es muy fácil de escribir, gracias a match , y se verá como el Listing 6-5.
Listing 6-5: Una función que usa una expresión match en un Option<i32>
El valor Some(5) no coincide con el patrón None , por lo que seguimos a la siguiente Opción:
¿Coincide Some(5) con Some(i) ? ¡Lo hace! Tenemos la misma variante. Él i se vincula al
valor contenido en Some , por lo que i toma el valor 5 . Luego se ejecuta el código en la
Opción de match, por lo que agregamos 1 al valor de i y creamos un nuevo valor Some con
nuestro total 6 dentro.
https://book.rustlang-es.org/print 145/656
12/7/24, 22:37 El Lenguaje de Programación Rust
¡Coincide! No hay valor para agregar, por lo que el programa se detiene y devuelve el valor
None en el lado derecho de => . Debido a que la primera Opción coincidió, no se comparan
otras Opciones.
Combinando match y enums es útil en muchas situaciones. Verás este patrón mucho en el
código Rust: match contra un enum, vincula una variable a los datos internos y luego
ejecuta el código en función de él. Es un poco complicado al principio, pero una vez que te
acostumbras, desearás tenerlo en todos los lenguajes. Es consistentemente un favorito de
los usuarios.
Hay otro aspecto de match que debemos discutir: los patrones de las Opciones deben
cubrir todas las posibilidades. Considera esta versión de nuestra función plus_one , que
tiene un error y no se compila:
No manejamos el caso None , por lo que este código causará un error. Afortunadamente, es
un error que Rust sabe cómo detectar. Si intentamos compilar este código, obtendremos
este error:
https://book.rustlang-es.org/print 146/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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 {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
-->
/rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/option.rs:572:
1
:::
/rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/option.rs:576:
5
|
= note: 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!
Los matches en Rust son exhaustivos: debemos agotar todas las posibilidades para que el
código sea válido. Especialmente en el caso de Option<T> , cuando Rust nos impide olvidar
manejar el caso None , nos protege de asumir que tenemos un valor cuando podríamos
tener nulo, haciendo así imposible el error de mil millones de dólares discutido
anteriormente.
Usando enums, también podemos tomar acciones especiales para algunos valores
particulares, pero para todos los demás valores, tomar una acción predeterminada. Imagina
que estamos implementando un juego donde, si sacas un 3 en un lanzamiento de dados, tu
jugador no se mueve, sino que obtiene un nuevo sombrero elegante. Si sacas un 7, tu
jugador pierde un sombrero elegante. Para todos los demás valores, tu jugador se mueve
esa cantidad de espacios en el tablero de juego. Aquí hay un match que implementa esa
lógica, con el resultado del lanzamiento de dados codificado en lugar de un valor aleatorio, y
toda la lógica representada por funciones sin cuerpos porque implementarlas realmente
está fuera del alcance de este ejemplo:
https://book.rustlang-es.org/print 147/656
12/7/24, 22:37 El Lenguaje de Programación Rust
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
Para las primeras dos Opciones, los patrones son los valores literales 3 y 7 . Para la última
Opción que cubre cualquier otro valor posible, el patrón es la variable que hemos elegido
para nombrar other . El código que se ejecuta para la Opción other usa la variable
pasándola a la función move_player .
Este código compila, aunque no hemos enumerado todos los posibles valores que puede
tener un u8 , porque el último patrón coincidirá con todos los valores no especificados
específicamente. Este patrón de captura cumple con el requisito de que match debe ser
exhaustivo. Ten en cuenta que tenemos que poner la Opción de captura al final porque los
patrones se evalúan en orden. Si ponemos la Opción de captura antes, las otras Opciones
nunca se ejecutarían, por lo que Rust nos advertirá si agregamos Opciones después de un
catch-all!
Rust también tiene un patrón que podemos usar cuando queremos un catch-all, pero no
queremos usar el valor en el patrón catch-all: _ es un patrón especial que coincide con
cualquier valor y no se vincula a ese valor. Esto le dice a Rust que no vamos a usar el valor,
por lo que Rust no nos advertirá sobre una variable no utilizada.
Vamos a cambiar las reglas del juego. Ahora, si sacas un número diferente de un 3 o un 7
debes tirar de nuevo. Ya no necesitamos usar el valor general, por lo que puede 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() {}
https://book.rustlang-es.org/print 148/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Finalmente, cambiaremos las reglas del juego una vez más para que nada más ocurra en tu
turno si sacas algo que no sea un 3 o un 7. Podemos expresar eso usando el valor de unidad
(el tipo de tupla vacía que mencionamos en “El tipo de tupla” sección) como el código que va
con la Opción _ :
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 usar ningún otro valor que no
coincida con un patrón en una Opción anterior, y no queremos ejecutar ningún código en
este caso.
Hay más sobre patrones y coincidencias que cubriremos en el Capítulo 18. Por ahora, vamos
a pasar a la sintaxis if let que puede ser útil en situaciones en las que la expresión match
es un poco larga.
https://book.rustlang-es.org/print 149/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Listado 6-6: Un match que solo se preocupa por ejecutar código cuando el valor es Some
En su lugar, podríamos escribir esto de una manera más corta usando if let . El siguiente
código se comporta de la misma manera que el match en el Listado 6-6:
La sintaxis if let toma un patrón y una expresión separados por un signo igual. Funciona
de la misma manera que un match , donde la expresión se da al match y el patrón es su
primer brazo. En este caso, el patrón es Some(max) , y el max se vincula al valor dentro del
Some . Luego podemos usar max en el cuerpo del bloque if let de la misma manera que
usamos max en el brazo match correspondiente. El código en el bloque if let no se
ejecuta si el valor no coincide con el patrón.
Usar if let significa menos escritura, menos indentación y menos código repetitivo. Sin
embargo, pierdes la verificación exhaustiva que hace cumplir match . Elegir entre match e
if let depende de lo que estés haciendo en tu situación particular y de si ser más conciso
a cambio de la verificación exhaustiva es un intercambio adecuado.
En otras palabras, puedes pensar en if let como una sintaxis dulce para un match que
ejecuta código cuando el valor coincide con un patrón y luego ignora todos los demás
valores.
https://book.rustlang-es.org/print 150/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Podemos incluir un else con un if let . El bloque de código que va con el else es el
mismo que el bloque de código que iría con el caso _ en la expresión match que es
equivalente al if let y else . Recuerda la definición de Coin en el Listado 6-4, donde la
variante Quarter también tenía un valor UsState . Si quisiéramos contar todas las
monedas que no son cuartos que vemos mientras también anunciamos el estado de los
cuartos, podríamos hacerlo con una expresión match , como esta:
Resumen
Ahora hemos cubierto cómo usar enums para crear tipos personalizados que pueden ser
uno de un conjunto de valores enumerados. Hemos mostrado cómo el tipo Option<T> de
la biblioteca estándar te ayuda a usar el sistema de tipos para prevenir errores. Cuando los
valores de enum tienen datos dentro de ellos, podemos usar match o if let para extraer
y usar esos valores, dependiendo de cuántos casos necesites manejar.
Tus programas Rust ahora pueden expresar conceptos en tu dominio usando structs y
enums. Crear tipos personalizados para usar en tu API asegura la seguridad de tipos: el
compilador se asegurará de que tus funciones solo obtengan valores del tipo que cada
función espera.
En orden de proveer una API bien organizada a tus usuarios que sea sencilla de usar y solo
exponga exactamente lo que tus usuarios necesitarán, ahora vamos a ver los módulos de
Rust.
https://book.rustlang-es.org/print 151/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los programas que hemos escrito hasta ahora han estado en un módulo en un archivo. A
medida que un proyecto crece, debes organizar el código dividiéndolo en múltiples módulos
y luego en múltiples archivos. Un paquete puede contener múltiples crates binarios y
opcionalmente un crate de biblioteca. A medida que un paquete crece, puedes extraer
partes en crates separados que se convierten en dependencias externas. Este capítulo cubre
todas estas técnicas. Para proyectos muy grandes que comprenden un conjunto de
paquetes interrelacionados que evolucionan juntos, Cargo proporciona workspaces, que
cubriremos en la sección “Cargo Workspaces” en el Capítulo 14.
Rutas: Una forma de nombrar un elemento, como una estructura, función o módulo
https://book.rustlang-es.org/print 153/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Paquetes y Crates
Las primeras partes del sistema de módulos que cubriremos son los paquetes y los crates.
Un crate es la cantidad más pequeña de código que el compilador Rust considera a la vez.
Incluso si ejecutas rustc en lugar de cargo y pasas un solo archivo de código fuente (como
lo hicimos en la sección “Escribir y Ejecutar un Programa Rust” del Capítulo 1), el compilador
considera que ese archivo es un crate. Los crates pueden contener módulos, y los módulos
pueden definirse en otros archivos que se compilan con el crate, como veremos en las
próximas secciones.
Un crate puede venir en una de dos formas: un crate binario o un crate de biblioteca. Los
crates binarios son programas que puedes compilar a un ejecutable que puedes ejecutar,
como un programa de línea de comandos o un servidor. Cada uno debe tener una función
llamada main que defina lo que sucede cuando se ejecuta el ejecutable. Todos los crates
que hemos creado hasta ahora han sido crates binarios.
El crate root es un archivo fuente que el compilador Rust comienza y forma el módulo raíz
de tu crate (explicaremos los módulos en profundidad en la sección “Definir Módulos para
Controlar el Alcance y la Privacidad”).
Veamos qué sucede cuando creamos un paquete. Primero, ingresamos el comando cargo
new :
https://book.rustlang-es.org/print 154/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Después de ejecutar cargo new my-project , usamos ls para ver lo que crea Cargo. En el
directorio del proyecto, hay un archivo Cargo.toml, que nos da un paquete. También hay un
directorio src que contiene main.rs. Abre Cargo.toml en tu editor de texto, y observa que no
hay mención de src/main.rs. Cargo sigue una convención de que src/main.rs es la raíz del
crate de un crate binario con el mismo nombre que el paquete. Del mismo modo, Cargo
sabe que si el directorio del paquete contiene src/lib.rs, el paquete contiene un crate de
biblioteca con el mismo nombre que el paquete, y src/lib.rs es su raíz del crate. Cargo pasa
los archivos raíz del crate a rustc para compilar la biblioteca o el binario.
Aquí, tenemos un paquete que solo contiene src/main.rs, lo que significa que solo contiene
un crate binario llamado my-project . Si un paquete contiene src/main.rs y src/lib.rs, tiene
dos crates: un binario y una biblioteca, ambos con el mismo nombre que el paquete. Un
paquete puede tener múltiples crates binarios colocando archivos en el directorio src/bin:
cada archivo será un crate binario separado.
https://book.rustlang-es.org/print 155/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Primero, vamos a empezar con una lista de reglas para tener a mano cuando estés
organizando tu código en el futuro. Luego explicaremos cada una de las reglas en detalle.
Antes nosotros debemos obtener los detalles de los módulos y las rutas, aquí te
proporcionamos una referencia rápida sobre cómo funcionan los módulos, las rutas, la
palabra clave use y la palabra clave pub en el compilador, y cómo la mayoría de los
desarrolladores organizan su código. Vamos a ir tratando ejemplos de cada una de estas
reglas a lo largo de este capítulo, pero esta es una buena referencia para tener a mano
cuando necesites recordar cómo funcionan los módulos.
https://book.rustlang-es.org/print 156/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Aquí, crearemos un crate binario llamado backyard que ilustra estas reglas. El directorio del
crate, también llamado backyard , contiene estos archivos y directorios:
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rs
Filename: src/main.rs
use crate::garden::vegetables::Asparagus;
fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
}
La línea mod garden; le dice al compilador que incluya el código que encuentra en
src/garden.rs, que es:
Filename: src/garden.rs
https://book.rustlang-es.org/print 157/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[derive(Debug)]
pub struct Asparagus {}
Los módulos nos permiten organizar el código dentro de un crate para facilitar su lectura y
reutilización. También nos permiten controlar la privacidad de los elementos, ya que el
código dentro de un módulo es privado por defecto. Los elementos privados son detalles de
la implementación interna que no están disponibles para su uso externo. Podemos elegir
hacer públicos los módulos y los elementos que contienen para exponerlos y permitir que el
código externo los use y dependa de ellos.
Para estructurar nuestro crate de esta manera, podemos organizar sus funciones dentro de
módulos anidados. Crea una nueva librería llamada restaurant ejecutando cargo new
restaurant --lib . Luego ingresa el código en el Listado 7-1 para definir algunos módulos y
firmas de funciones. Aquí está la sección front of house:
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
https://book.rustlang-es.org/print 158/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Listado 7-1: Un módulo front_of_house que contiene otros módulos que luego contienen funciones
Definimos un módulo con la palabra clave mod seguida del nombre del módulo (en este
caso, front_of_house ). El cuerpo del módulo va dentro de llaves. Dentro de los módulos,
podemos colocar otros módulos, como en este caso con los módulos hosting y serving .
Los módulos también pueden contener definiciones de otros elementos, como structs,
enums, constantes, traits, y como en la Lista 7-1—funciones.
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
Este árbol muestra como algunos de los módulos se anidan dentro de otros módulos; por
ejemplo, hosting se anida dentro de front_of_house . El árbol también muestra que
algunos módulos son hermanos entre sí, lo que significa que están definidos en el mismo
módulo; hosting y serving son 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 el hijo del
módulo B y que el módulo B es el padre del módulo A. Nota que el árbol de módulos
completo está enraizado bajo el módulo implícito llamado crate .
https://book.rustlang-es.org/print 159/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Una ruta absoluta es la ruta completa que comienza desde la raíz de un crate ; para el
código de un crate externo, la ruta absoluta comienza con el nombre del crate , y
para el código del crate actual, comienza con el crate literal.
Una ruta relativa comienza desde el módulo actual y utiliza self , super , o un
identificador del módulo actual.
Tanto las rutas absolutas como las relativas están seguidas por uno o más identificadores
separados por dos puntos dobles ( :: ).
Volviendo al listado 7-1, digamos que queremos llamar a la función add_to_waitlist desde
la función eat_at_restaurant definida en el crate root. Este es el mismo que preguntar:
¿cuál es la ruta de la función add_to_waitlist ? El listado 7-3 contiene el listado 7-1 con
algunos de los módulos y funciones removidas.
Mostraremos dos formas de llamar a la función add_to_waitlist desde una nueva función,
eat_at_restaurant , definida en el crate de la raíz. Estas rutas son correctas, pero hay otro
problema que impide que este ejemplo compile tal cual. Explicaremos por qué en un
momento.
La función eat_at_restaurant es parte de la API pública del crate de nuestra librería, así
que la marcamos con la palabra clave pub . En la sección “Exponiendo Rutas con la palabra
clave pub ”, iremos en más detalle sobre pub .
Filename: src/lib.rs
https://book.rustlang-es.org/print 160/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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();
}
Elegir si usar una ruta relativa o absoluta es una decisión que tomarás basado en tu
proyecto, y depende de si es más probable que muevas la definición de un item de código
separadamente o junto con el código que usa el item. Por ejemplo, si movemos el módulo
front_of_house y la función eat_at_restaurant 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 función
eat_at_restaurant separadamente a un módulo llamado dining , la ruta absoluta a la
llamada de add_to_waitlist seguiría siendo la misma, pero la ruta relativa necesitaría ser
actualizada. Nuestra preferencia en general es especificar rutas absolutas porque es más
probable que queramos mover definiciones de código y llamadas de items
independientemente.
¡Intentemos compilar el listado 7-3 y averigüemos por qué aún no compila! Los errores que
obtenemos se muestran en el listado 7-4.
https://book.rustlang-es.org/print 161/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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 de compilación al hacer building del código del listado 7-3
El mensaje de error dice que el módulo hosting es privado. En otras palabras, tenemos las
rutas correctas para el módulo hosting y la función add_to_waitlist , pero Rust no nos
deja usarlos porque no tiene acceso a las secciones privadas. En Rust, todos los items
(funciones, métodos, structs, enums, módulos, y constantes) son privados a los módulos
padres por defecto. Si quieres hacer un item como una función o struct privado, lo pones en
un módulo.
Los elementos en un módulo privado no pueden ser accedidos por una ruta externa
absoluta, porque el módulo padre no puede ver dentro de los módulos privados de sus
hijos. El módulo padre puede ver el contenido de sus módulos hijos porque los módulos
hijos están dentro del módulo padre. Para continuar con nuestra metáfora, piensa en las
reglas de privacidad como la oficina de atrás de un restaurante: lo que pasa ahí es privado
para los clientes del restaurante, pero los gerentes de la oficina pueden ver y hacer todo en
el restaurante que operan.
https://book.rustlang-es.org/print 162/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Rust elige tener el sistema de módulos funcionando de esta forma para que ocultar detalles
de implementación internos sea lo predeterminado. De esta forma, sabes qué partes del
código interno puedes cambiar sin romper el código externo. Sin embargo, Rust te da la
opción de exponer partes internas del código de los módulos hijos a los módulos ancestros
externos usando la palabra clave pub para hacer un item público.
Volviendo al error en el listado 7-4 que nos dijo que el módulo hosting es privado,
queremos que la función eat_at_restaurant en el módulo padre tenga acceso a la función
add_to_waitlist en el módulo hijo, así que marcamos el módulo hosting con la palabra
clave pub , como se muestra en el listado 7-5.
Filename: src/lib.rs
mod front_of_house {
pub 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-5: Declarando el módulo hosting como pub para usarlo desde eat_at_restaurant
https://book.rustlang-es.org/print 163/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
Listado 7-6: Errores de compilación al hacer building del código del listado 7-5
¿Qué pasó? Agregar la palabra clave pub al frente del módulo hosting hace que el módulo
sea público. Con este cambio, si podemos acceder a front_of_house , podemos acceder a
hosting . Pero el contenido de hosting sigue siendo privado; hacer el módulo público no
hace que su contenido sea público. La palabra clave pub en un módulo solo permite que el
código en sus módulos ancestros se refiera a él, no acceder a su código interno. Debido a
que los módulos son contenedores, no hay mucho que podamos hacer solo haciendo que el
módulo sea público; necesitamos ir más allá y elegir hacer que uno o más de los items
dentro del módulo sean públicos también.
El error en el listado 7-6 dicen que la función add_to_waitlist es privada. Las reglas de
privacidad se aplican a structs, enums, funciones, y métodos, así como a módulos.
Para hacer que la función add_to_waitlist sea pública, necesitamos agregar la palabra
clave pub antes de su definición, como se muestra en el listado 7-7.
Filename: src/lib.rs
https://book.rustlang-es.org/print 164/656
12/7/24, 22:37 El Lenguaje de Programación Rust
mod front_of_house {
pub mod hosting {
pub 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-7: Agregar la keyword pub a mod hosting y fn add_to_waitlist nos permite llamar a la función
desde eat_at_restaurant
¡Ahora el código compilará! Para ver por qué agregar la palabra clave pub nos permite usar
estas rutas en eat_at_restaurant con respecto a las reglas de privacidad, veamos las rutas
absolutas y relativas.
En la ruta absoluta, comenzamos con crate , la raíz del árbol de módulos de nuestro crate.
El módulo front_of_house está definido en la raíz del crate . Si bien front_of_house no
es público, porque la función eat_at_restaurant está definida en el mismo módulo que
front_of_house (es decir, eat_at_restaurant y front_of_house son hermanos),
podemos referirnos a front_of_house desde eat_at_restaurant . A continuación está el
módulo hosting marcado con pub . Podemos acceder al módulo padre de hosting , por lo
que podemos acceder a hosting . ¡Finalmente, la función add_to_waitlist está marcada
con pub y podemos acceder a su módulo padre, por lo que está llamada a función
funciona!
En la ruta relativa, la lógica es la misma que la ruta absoluta, excepto por el primer paso: en
lugar de comenzar desde la raíz del crate , la ruta comienza desde front_of_house . El
módulo front_of_house está definido dentro del mismo módulo que eat_at_restaurant ,
por lo que la ruta relativa que comienza desde el módulo en el que se define
eat_at_restaurant funciona. Luego, porque hosting y add_to_waitlist están marcados
con pub , el resto de la ruta funciona, ¡y está llamada a función es válida!
Si planeas compartir tu biblioteca crate para que otros proyectos puedan usar tu código, tu
API pública es tu contrato con los usuarios de tu crate que determina cómo pueden
interactuar con tu código. Hay muchas consideraciones sobre cómo administrar los cambios
en tu API pública para que sea más fácil que la gente dependa de tu crate. Estas
consideraciones están fuera del alcance de este libro; si estás interesado en este tema,
consulta The Rust API Guidelines.
https://book.rustlang-es.org/print 165/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Mencionamos que un paquete puede contener tanto un binario src/main.rs como una
biblioteca src/lib.rs, y ambos tendrán el nombre del paquete de forma predeterminada.
Típicamente, los paquetes con este patrón de contener tanto una biblioteca como un
binario tendrán solo el código suficiente en el binario para iniciar un ejecutable que
llame al código con la biblioteca. Esto permite que otros proyectos se beneficien de
que la mayor funcionalidad que proporciona el paquete, porque el código de la
biblioteca se puede compartir.
El árbol de módulos debería ser definido en src/lib.rs. Luego, cualquier item público
puede ser usado en el binario comenzando las rutas con el nombre del paquete. El
binario se convierte en un usuario de la biblioteca de la misma forma que un crate
completamente externo usaría la biblioteca: solo puede usar la API pública. Esto te
ayuda a diseñar una buena API; no solo eres el autor, ¡también eres un cliente!
Considere el código en el listado 7-8 que modela la situación en la que un chef arregla un
pedido incorrecto y lo trae personalmente al cliente. La función fix_incorrect_order
definida en el módulo back_of_house llama a la función deliver_order definida en el
módulo padre especificando la ruta a deliver_order , comenzando con super .
Filename: src/lib.rs
https://book.rustlang-es.org/print 166/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
Listado 7-8: Llamar a una función usando una ruta relativa que comienza con super
También podemos usar pub para designar structs y enums como públicos, pero hay
algunos detalles adicionales para el uso de pub con structs y enums. Si usamos pub antes
de una definición de struct, hacemos que el struct sea público, pero los campos del struct
seguirán siendo privados. Podemos hacer que cada campo sea público o no caso por caso.
En el listado 7-9, hemos definido un struct back_of_house::Breakfast público con un
campo toast público pero un campo seasonal_fruit privado. Esto modela el caso en un
restaurante donde el cliente puede elegir el tipo de pan que viene con una comida, pero el
chef decide qué fruta acompaña la comida según lo que está en temporada y en stock. La
fruta disponible cambia rápidamente, por lo que los clientes no pueden elegir la fruta o
incluso ver qué fruta obtendrán.
Filename: src/lib.rs
https://book.rustlang-es.org/print 167/656
12/7/24, 22:37 El Lenguaje de Programación Rust
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
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");
}
Listado 7-9: Un struct con algunos campos públicos y algunos campos privados
Debido a que el campo toast es público, podemos cambiar el valor de toast en una
instancia de Breakfast en la función eat_at_restaurant en el listado 7-10. Ten en cuenta
que no podemos usar el campo seasonal_fruit en eat_at_restaurant , porque
seasonal_fruit es privado. ¡Intenta descomentar la línea que modifica el valor del campo
seasonal_fruit para ver qué error obtiene!
Por el contrario, si hacemos un enum público, todos sus variantes son públicas. Solo
necesitamos el pub antes de la palabra clave enum , como se muestra en el listado 7-10.
Filename: src/lib.rs
https://book.rustlang-es.org/print 168/656
12/7/24, 22:37 El Lenguaje de Programación Rust
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
Listado 7-10: Designar un enum como público hace que todas sus variantes sean públicas
Debido a que hicimos el enum Appetizer público, podemos usar las variantes
Appetizer::Soup y Appetizer::Salad en eat_at_restaurant .
Los Enums no son muy útiles a menos que sus variantes sean públicas; sería molesto tener
que anotar todas las variantes de enum con pub en todos los casos, por lo que el valor
predeterminado para las variantes de enum es ser público. Los structs a menudo son útiles
sin que sus campos sean públicos, por lo que los campos de struct siguen la regla general
de que todo es privado por defecto a menos que se anote con pub .
Hay una situación más relacionada con pub que no hemos cubierto, y es nuestra última
característica del sistema de módulos: la palabra clave use . Cubriremos use por sí solo
primero, y luego mostraremos cómo combinar pub y use .
https://book.rustlang-es.org/print 169/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Agregar use y una ruta en un ámbito es similar a crear un enlace simbólico en el sistema de
archivos. Al agregar use crate::front_of_house::hosting en la raíz del crate, hace que
hosting sea ahora un nombre válido en ese ámbito, como si el módulo hosting hubiera
sido definido en la raíz del crate. Las rutas traídas al ámbito con use también verifican la
privacidad, como cualquier otra ruta.
Ten en cuenta que use solo crea el atajo para el ámbito particular en el que ocurre él use .
El Listado 7-12 mueve la función eat_at_restaurant a un nuevo módulo hijo llamado
customer , que es entonces un ámbito diferente al de la sentencia use , por lo que el cuerpo
de la función no compilará.
Filename: src/lib.rs
https://book.rustlang-es.org/print 170/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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: La sentencia use solo aplica en el ámbito donde se encuentra declarado
El error del compilador muestra que el acceso directo ya no se aplica dentro del módulo del
customer :
$ 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
Observe que también hay una advertencia de que él use ya no se utiliza en su ámbito. Para
solucionar este problema, mueva también él use dentro del módulo customer , o haga
referencia al acceso directo en el módulo padre con super::hosting dentro del módulo
hijo customer .
https://book.rustlang-es.org/print 171/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting::add_to_waitlist;
pub fn eat_at_restaurant() {
add_to_waitlist();
}
Listado 7-13: Incorporando la función add_to_waitlist en el ámbito con use , que no es idiomático
Aunque el Listado 7-11 y 7-13 logran la misma tarea, el Listado 7-11 es la forma idiomática
de traer una función al ámbito con use . Traer el módulo padre de la función al ámbito con
use significa que tenemos que especificar el módulo padre cuando llamamos a la función.
Especificar el módulo padre cuando llamamos a la función hace que quede claro que la
función no está definida localmente, al tiempo que minimiza la repetición de la ruta
completa. El código en el Listado 7-13 no es claro en cuanto a dónde se define
add_to_waitlist .
Por otro lado, cuando traemos structs, enums y otros items con use , es idiomático
especificar la ruta completa. El Listado 7-14 muestra la forma idiomática de traer la struct
HashMap de la biblioteca estándar al ámbito de un crate binario.
Filename: src/main.rs
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
No hay una razón fuerte detrás de este idioma: es solo la convención que ha surgido, y la
gente se ha acostumbrado a leer y escribir código Rust de esta manera.
https://book.rustlang-es.org/print 172/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La excepción a este idioma es si estamos trayendo dos elementos con el mismo nombre al
ámbito con declaraciones use , porque Rust no lo permite. El Listado 7-15 muestra cómo
traer dos tipos Result al ámbito que tienen el mismo nombre pero módulos padres
diferentes, y cómo referirse a ellos.
Filename: src/lib.rs
use std::fmt;
use std::io;
Listado 7-15: Incorporando dos tipos con el mismo nombre en el mismo ámbito requiere el uso de sus módulos
principales.
Como puedes ver, usar los módulos padres distingue los dos tipos Result . Sí, en cambio,
especificamos use std::fmt::Result y use std::io::Result , tendríamos dos tipos
Result en el mismo ámbito, y Rust no sabría a cuál nos referimos cuando usamos Result .
Hay otra solución a este problema de traer dos elementos con el mismo nombre al ámbito
con use : después de la ruta, podemos especificar as y un nuevo nombre local, o alias, para
el tipo. El Listado 7-16 muestra otra forma de escribir el código en el Listado 7-15
renombrando uno de los dos tipos Result usando as .
Filename: src/lib.rs
use std::fmt::Result;
use std::io::Result as IoResult;
Listado 7-16: Cambiando el nombre de un tipo cuando se introduce en el ámbito con la keyword as
https://book.rustlang-es.org/print 173/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cuando traemos un nombre al ámbito con la keyword use , el nombre está disponible en
ese ámbito de forma privada. Si queremos que el nombre esté disponible para que el
código que llama a nuestro código lo use, podemos combinar pub y use . Esta técnica se
llama re-exporting porque estamos trayendo un elemento al ámbito, pero también haciendo
que ese elemento esté disponible para que otros lo traigan a su ámbito.
El listado 7-17 muestra el código del listado 7-11 con use en el módulo front_of_house
cambiado a pub use .
Filename: src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Listado 7-17: Hacer que un nombre esté disponible para que lo use cualquier código desde un nuevo ámbito
con pub use
Antes de este cambio, el código externo tendría que llamar a la función add_to_waitlist
usando la ruta restaurant::front_of_house::hosting::add_to_waitlist() , el cual
también debería tener requerido el modulo front_of_house para ser marcado como pub .
Ahora que este pub use ha reexportado el módulo hosting desde el módulo raíz, el
código externo puede usar la ruta restaurant::hosting::add_to_waitlist() en su lugar.
Filename: Cargo.toml
rand = "0.8.5"
Añadir rand como dependencia en Cargo.toml le dice a Cargo que descargue el paquete
rand y cualquier dependencia de crates.io y haga que rand esté disponible para nuestro
proyecto.
Luego, para llevar las definiciones de rand al ámbito de nuestro paquete, agregamos una
línea use que comienza con el nombre del paquete, rand , y enumera los items que
queremos traer al ámbito. Recuerda que en la sección “Generando un número aleatorio” del
Capítulo 2, traíamos el trait Rng al ámbito y llamábamos a la función rand::thread_rng :
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}
Ten en cuenta que la biblioteca estándar std también es una crate externa a nuestro
paquete. Debido a que la biblioteca estándar se envía con el lenguaje Rust, no necesitamos
cambiar Cargo.toml para incluir std . Pero sí necesitamos referirnos a él con use para traer
items de allí al ámbito de nuestro paquete. Por ejemplo, con HashMap usaríamos esta línea:
use std::collections::HashMap;
Esta es una ruta absoluta que comienza con std , el nombre del crate de la biblioteca
estándar. También podríamos escribir este use como:
https://book.rustlang-es.org/print 175/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/main.rs
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--
En su lugar, podemos utilizar rutas anidadas para incluir los mismos elementos en una sola
línea. Hacemos esto especificando la parte común de la ruta, seguida de dos puntos y luego
entre llaves los elementos, como se muestra en el Listado 7-18.
Filename: src/main.rs
// --snip--
use std::{cmp::Ordering, io};
// --snip--
Listado 7-18: Especificación de una ruta anidada para incluir en el ámbito varios elementos con el mismo prefijo
En programas más grandes, traer muchos items al ámbito desde el mismo crate o módulo
usando rutas anidadas puede reducir la cantidad de declaraciones use necesarias en gran
medida.
Podemos usar una ruta anidada en cualquier nivel de una ruta, lo que es útil cuando
combinamos dos sentencias use que comparten una sub-ruta. Por ejemplo, el Listado 7-19
muestra dos sentencias use: una que trae std::io al ámbito y otra que trae
std::io::Write al ámbito.
Filename: src/lib.rs
use std::io;
use std::io::Write;
Listado 7-19: Dos sentencias use donde una es una sub-ruta de la otra
La parte común de estas dos rutas es std::io , así que podemos usar una ruta anidada
para traer ambos al ámbito en una línea, como se muestra en el Listado 7-20.
Filename: src/lib.rs
Listado 7-20: Combinando las rutas del listado 7-19 en una sentencia use
Si queremos incluir al ámbito todos los elementos públicos definidos en una ruta, podemos
especificar esa ruta seguido del operador glob * :
use std::collections::*;
Esta sentencia use trae todos los elementos públicos definidos en std::collections al
ámbito actual. Tenga cuidado al utilizar el operador glob . El operador glob puede hacer
más difícil saber qué elementos están en el ámbito y dónde se definió un elemento que este
siendo utilizado en su programa.
El operador glob se utiliza a menudo cuando se realizan pruebas para llevar todo lo que se
está probando al módulo de pruebas ; hablaremos de ello en la sección "Cómo escribir
pruebas" del capítulo 11. El operador glob también se utiliza a veces como parte del patrón
prelude: consulte la documentación de la biblioteca estándar para obtener más información
sobre ese patrón.
https://book.rustlang-es.org/print 177/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Por ejemplo, comencemos desde el código en el listado 7-17 que tenía múltiples módulos
de restaurante. Extraeremos los módulos en archivos en lugar de tener todos los módulos
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.
Filename: src/lib.rs
mod front_of_house;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Luego, coloca el código que estaba entre las llaves en un nuevo archivo llamado
src/front_of_house.rs, como se muestra en el Listado 7-22. El compilador sabe que debe
buscar en este archivo porque se encontró con la declaración del módulo en la raíz del crate
con el nombre front_of_house .
Filename: src/front_of_house.rs
Ten en cuenta que solo necesitas cargar un archivo usando una declaración mod una vez en
tu árbol de módulos. Una vez que el compilador sabe que el archivo es parte del proyecto (y
https://book.rustlang-es.org/print 178/656
12/7/24, 22:37 El Lenguaje de Programación Rust
sabe en qué parte del árbol de módulos reside el código debido a dónde has puesto la
declaración mod ), otros archivos en tu proyecto deben hacer referencia al código del archivo
cargado usando una ruta que indique donde se declaró, como se cubre en la sección “Rutas
para referirse a un elemento en el árbol de módulos”. En otras palabras, mod no es una
operación de “incluir” que puede haber visto en otros lenguajes de programación.
Filename: src/front_of_house.rs
Filename: src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}
Hasta ahora hemos cubierto las rutas de archivos más idiomáticas que utiliza 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_house declarado en la raíz del crate, el
compilador buscará el código del módulo en:
Si usas ambos estilos para el mismo módulo, obtendrás un error del compilador. Usar
una mezcla de ambos estilos para diferentes módulos en el mismo proyecto está
permitido, pero podría ser confuso para las personas que navegan por tu proyecto.
La principal desventaja del estilo que usa archivos llamados mod.rs es que tu proyecto
puede terminar con muchos archivos llamados mod.rs, lo que puede ser confuso
cuando los tienes abiertos en tu editor al mismo tiempo.
Resumen
Rust te permite dividir un paquete en múltiples crates y un crate en módulos para que
puedas referirte a elementos definidos en un módulo desde otro módulo. Puedes hacer
esto especificando rutas absolutas o relativas. Estas rutas se pueden traer al ámbito con una
declaración use para que puedas usar una ruta más corta para múltiples usos del
elemento en ese ámbito. El código de los módulos es privado por defecto, pero puedes
hacer que las definiciones sean públicas agregando la palabra clave pub .
https://book.rustlang-es.org/print 180/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Colecciones comunes
La biblioteca estándar de Rust incluye una serie de estructuras de datos muy útiles llamadas
colecciones. La mayoría de los otros tipos de datos representan un valor específico, pero las
colecciones pueden contener varios valores. A diferencia de los tipos de datos built-in array
y tupla, los datos a los que apuntan estas colecciones se almacenan en el heap, lo que
significa que la cantidad de datos no necesita conocerse en el momento de la compilación y
puede crecer o disminuir a medida que se ejecuta el programa. Cada tipo de colección tiene
diferentes capacidades y costos, y elegir uno apropiado para su situación actual es una
habilidad que desarrollará con el tiempo. En este capítulo, discutiremos tres colecciones que
se usan muy a menudo en los programas Rust:
Un vector le permite almacenar un número variable de valores uno al lado del otro.
Un string es una colección de caracteres. Hemos mencionado el tipo String
anteriormente, pero en este capítulo hablaremos de él en profundidad.
Un hash map le permite asociar un valor con una clave especifica. Es una
implementación particular de la estructura de datos más general llamada map.
Para aprender sobre los otros tipos de colecciones proporcionados por la biblioteca
estándar, consulte la documentación.
Discutiremos cómo crear y actualizar vectores, strings y hash maps, así como lo que hace
que cada uno sea especial.
https://book.rustlang-es.org/print 181/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para crear un nuevo vector vacío, llamamos a la función Vec::new , como se muestra en el
listado 8-1.
Listado 8-1: Creando un nuevo vector vacío para mantener valores de tipo i32
Ten en cuenta que agregamos una anotación de tipo aquí. Como no estamos insertando
ningún valor en este vector, Rust no sabe qué tipo de elementos queremos almacenar. Este
es un punto importante. Los vectores se implementan usando genéricos; cubriremos cómo
usar genéricos con tus propios tipos en el Capítulo 10. Por ahora, sepa que el tipo Vec<T>
proporcionado por la biblioteca estándar puede contener cualquier tipo. Cuando creamos
un vector para contener un tipo específico, podemos especificar el tipo dentro de corchetes
angulares. En el listado 8-1, le hemos dicho a Rust que el Vec<T> en v contendrá
elementos del tipo i32 .
A menudo, crearás un Vec<T> con valores iniciales y Rust inferirá el tipo de valor que
deseas almacenar, por lo que rara vez necesitarás hacer esta anotación de tipo. Rust
proporciona convenientemente la macro vec! , que creará un nuevo vector que contenga
los valores que le des. El listado 8-2 crea un nuevo Vec<i32> que contiene los valores 1 , 2
y 3 . El tipo entero es i32 porque ese es el tipo entero predeterminado, como discutimos
en la sección "Tipos de datos" del Capítulo 3.
Debido a que hemos dado valores iniciales i32 , Rust puede inferir que el tipo de v es
Vec<i32> , y la anotación de tipo no es necesaria. A continuación, veremos cómo modificar
un vector.
https://book.rustlang-es.org/print 182/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Actualizando un vector
Para crear un vector y luego agregar elementos a él, podemos usar el método push , como
se muestra en el listado 8-3.
v.push(5);
v.push(6);
v.push(7);
v.push(8);
Como con cualquier variable, si queremos poder cambiar su valor, necesitamos hacerlo
mutable usando la palabra clave mut , como se discutió en el Capítulo 3. Los números que
colocamos dentro son todos del tipo i32 , y Rust infiere esto de los datos, por lo que no
necesitamos la anotación Vec<i32> .
En el listado 8-4 se muestran ambos métodos de acceso a un valor en un vector, con sintaxis
de indexación y el método get .
Listado 8-4: Usando la sintaxis de indexación o el método get accediendo a un objeto en un vector
Ten en cuenta algunos detalles aquí. Usamos el valor de índice 2 para obtener el tercer
elemento porque los vectores se indexan por número, comenzando en cero. Usar & y []
nos da una referencia al elemento en el índice. Cuando usamos el método get con el índice
pasado como argumento, obtenemos un Option<&T> que podemos usar con match .
La razón por la que Rust proporciona estas dos formas de hacer referencia a un elemento
es para que puedas elegir cómo se comporta el programa cuando intentas usar un valor de
https://book.rustlang-es.org/print 183/656
12/7/24, 22:37 El Lenguaje de Programación Rust
índice fuera del rango de elementos existentes. Como ejemplo, veamos qué sucede cuando
tenemos un vector de cinco elementos y luego intentamos acceder a un elemento en el
índice 100 con cada técnica, como se muestra en el listado 8-5.
Listado 8-5: Intentando acceder al elemento en el índice 100 en un vector que contiene 5 elementos
Cuando ejecutamos este código, el primer método [] causará que el programa falle
porque intenta acceder a un elemento que no existe. Este método es mejor usarlo cuando
quieres que tu programa se bloquee si hay un intento de acceder a un elemento más allá
del final del vector.
Cuando el método get se pasa un índice que está fuera del rango del vector, simplemente
devuelve None sin entrar en pánico. Tendrías que usar este método si acceder a un
elemento más allá del rango del vector puede suceder con frecuencia en circunstancias
normales. Tu código tendrá entonces la lógica necesaria para gestionar la presencia de
Some(&element) o None, tal y como se explica en el capítulo 6. Por ejemplo, el índice podría
provenir de una persona que ingresa un número. Si ingresan accidentalmente un número
que es demasiado grande y el programa obtiene un valor None , podrías decirle al usuario
cuántos elementos hay en el vector actual y darle otra oportunidad de ingresar un valor
válido. Eso sería más amigable para el usuario que bloquear el programa debido a un error
tipográfico.
Cuando el programa tiene una referencia válida, el borrow checker hace cumplir las reglas
de ownership y borrowing (cubiertas en el Capítulo 4) para asegurar que esta referencia y
cualquier otra referencia a los contenidos del vector permanezcan válidas. Recuerda la regla
que establece que no puedes tener referencias mutables e inmutables en el mismo ámbito.
Esa regla se aplica en el listado 8-6, donde tenemos una referencia inmutable al primer
elemento en un vector e intentamos agregar un elemento al final. Este programa no
funcionará si también intentamos referirnos a ese elemento más adelante en la función.
v.push(6);
Listado 8-6: Intentando agregar un elemento a un vector mientras se mantiene una referencia a un elemento
https://book.rustlang-es.org/print 184/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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 en el listado 8-6 podría parecer que debería funcionar: ¿por qué una referencia al
primer elemento se preocuparía por los cambios al final del vector? Este error se debe a la
forma en que funcionan los vectores: porque los vectores colocan los valores uno al lado del
otro en la memoria, agregar un nuevo elemento al final del vector puede requerir asignar
nueva memoria y copiar los elementos antiguos al nuevo espacio, si no hay suficiente
espacio para poner todos los elementos uno al lado del otro donde se almacena el vector
actualmente. En ese caso, la referencia al primer elemento apuntaría a la memoria
desasignada. Las reglas de borrowing evitan que los programas terminen en esa situación.
Nota: Para más información sobre los detalles de implementación del tipo Vec<T> ,
véase "The Rustonomicon".
Para acceder a cada elemento en un vector a su vez, iteramos a través de todos los
elementos, en lugar de usar índices para acceder a uno a la vez. El listado 8-7 muestra cómo
usar un bucle for para obtener referencias inmutables a cada elemento en un vector de
valores i32 e imprimirlos.
Listado 8-7: Imprimiendo cada elemento en un vector iterando sobre los elementos usando un ciclo for
https://book.rustlang-es.org/print 185/656
12/7/24, 22:37 El Lenguaje de Programación Rust
También podemos iterar sobre referencias mutables a cada elemento en un vector mutable,
lo que nos permite cambiar los valores en un vector en el lugar. El código en el listado 8-8
agregará 50 a cada elemento en un vector.
Para cambiar el valor al que se refiere la referencia mutable, tenemos que usar el operador
de desreferencia * para llegar al valor en i antes de poder usar el operador += .
Hablaremos más sobre el operador de desreferencia en la sección “Siguiendo el puntero al
valor con el operador de desreferencia” del Capítulo 15.
Iterando sobre un vector, ya sea inmutable o mutable, es seguro debido a las reglas del
borrow checker. Si intentáramos insertar o eliminar elementos en los cuerpos del ciclo for
en el listado 8-7 y el listado 8-8, obtendríamos un error del compilador similar al que
obtuvimos con el código en el listado 8-6. La referencia al vector que el ciclo for contiene
evita la modificación simultánea de todo el vector.
Los vectores solo pueden almacenar valores del mismo tipo. Esto puede ser inconveniente;
definitivamente hay casos de uso para necesitar almacenar una lista de elementos de
diferentes tipos. Afortunadamente, las variantes de un enum se definen bajo el mismo tipo
de enum , por lo que cuando necesitamos que un tipo represente elementos de diferentes
tipos, ¡podemos definir y usar un enum !
Por ejemplo, digamos que queremos almacenar en una lista los elementos de una tabla de
hoja de cálculo: algunas columnas pueden contener números, y otras cadenas de texto.
Podemos definir un enum cuyas variantes contendrán los diferentes tipos de datos, y todas
las variantes se considerarán del mismo tipo: el del enum . Luego podemos crear un vector
para contener ese enum y, por lo tanto, en última instancia, contener diferentes tipos.
Hemos demostrado esto en el listado 8-9.
https://book.rustlang-es.org/print 186/656
12/7/24, 22:37 El Lenguaje de Programación Rust
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
Listado 8-9: Definiendo un enum para almacenar valores de diferentes tipos en un vector
Rust necesita saber qué tipos habrá 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 qué tipos están permitidos en este vector. Si Rust
permitiera que un vector contenga cualquier tipo, existiría la posibilidad de que uno o más
de los tipos causaran errores con las operaciones realizadas en los elementos del vector.
Usar un enum más una expresión match significa que Rust se asegurará en tiempo de
compilación de que se maneje cada caso posible, como se discutió en el Capítulo 6.
Ahora que hemos discutido algunas de las formas más comunes de usar vectores,
asegúrese de revisar la documentación de la API para todos los muchos métodos útiles
definidos en Vec<T> por la biblioteca estándar. Por ejemplo, además de push , un método
pop elimina y devuelve el último elemento.
Como cualquier otro struct , un vector se libera cuando sale del ámbito, como se anota 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
Cuando se libera el vector, también se libera todo su contenido, lo que significa que se
limpiarán los enteros que contiene. El borrow checker garantiza que cualquier referencia al
contenido de un vector solo se utilice mientras el vector en sí sea válido.
https://book.rustlang-es.org/print 187/656
12/7/24, 22:37 El Lenguaje de Programación Rust
https://book.rustlang-es.org/print 188/656
12/7/24, 22:37 El Lenguaje de Programación Rust
¿Qué es un string?
Bien primero definamos lo que queremos decir con el término string. Rust solo tiene un tipo
de string en el lenguaje principal, que es el string slice str que generalmente se ve en su
forma prestada &str . En el Capítulo 4, hablamos sobre string slices, que son referencias a
algunos datos de cadena codificados en UTF-8 almacenados en otro lugar. Las literales de
cadena, por ejemplo, se almacenan en el binario del programa y, por lo tanto, son trozos de
cadena.
Muchas de las mismas operaciones disponibles con Vec<T> también están disponibles con
String , ya que String se implementa en realidad como un envoltorio alrededor de un
vector de bytes con algunas garantías, restricciones y capacidades adicionales. Un ejemplo
de una función que funciona de la misma manera con Vec<T> y String es la función new
para crear una instancia, que se muestra en el listado 8-11.
https://book.rustlang-es.org/print 189/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Esta línea crea un nuevo String vacío llamado s , el cual podemos luego cargar con datos.
A menudo, tendremos algunos datos iniciales que queremos comenzar en el string. Para
eso, usamos el método to_string , que está disponible en cualquier tipo que implemente el
trait Display , como lo hacen los String Literals. El listado 8-12 muestra dos ejemplos.
let s = data.to_string();
Listado 8-12: Usando el método to_string para crear un String a partir de un string literal
Podemos también usar la función String::from para crear un String a partir de un string
literal. El código en el listado 8-13 es equivalente al código del listado 8-12 que usa
to_string .
Listado 8-13: Usando la función String::from para crear un String a partir de un string literal
Debido a que los strings se usan para muchas cosas, podemos usar muchas APIs genéricas
diferentes para strings, lo que nos proporciona muchas opciones. Algunos de ellos pueden
parecer redundantes, ¡pero todos tienen su lugar! En este caso, String::from y to_string
hacen lo mismo, por lo que elegir depende del estilo y la legibilidad.
Recuerda que los strings son UTF-8 codificados, por lo que podemos incluir cualquier dato
codificado correctamente en ellos, Como se muestra en el listado 8-14.
Actualizando un String
Podemos hacer crecer un String usando el método push_str para agregar un string slice,
como se muestra en el listado 8-15.
Después de estas dos líneas, s contendrá foobar . El método push_str toma un string
slice porque no necesariamente queremos tomar posesión del parámetro. Por ejemplo, en
el código del listado 8-16, queremos poder usar s2 después de agregar su contenido a s1 .
El método push toma un solo carácter como parámetro y lo agrega al String . El listado 8-
17 agrega la letra l a un String usando el método push .
https://book.rustlang-es.org/print 191/656
12/7/24, 22:37 El Lenguaje de Programación Rust
A veces, necesitarás combinar dos strings. Sin embargo, no es tan simple como usar el
operador + con dos referencias a String . El código en el listado 8-18 no compilará:
Listado 8-18: Usando el operador + para combinar dos valores String en un nuevo valor String
En la biblioteca estándar, verás add definido usando genéricos y tipos asociados. Aquí,
hemos sustituido tipos concretos, que es lo que sucede cuando llamamos a este método
con valores String . Discutiremos los genéricos en el Capítulo 10. Esta firma nos da las
pistas que necesitamos para entender las partes complicadas del operador + .
Primero, s2 tiene un & , lo que significa que estamos agregando una referencia del
segundo string al primer string. Esto se debe al parámetro s en la función add : solo
podemos agregar un &str a un String ; no podemos agregar dos valores String juntos.
Pero espera, el tipo de &s2 es &String , no &str , como se especifica en el segundo
parámetro de add . ¿Entonces por qué compila el listado 8-18?
La razón por la que podemos usar s2 en la llamada a add es que el compilador puede
convertir el argumento &String en un &str . Cuando
llamamos al método add , Rust usa una coerción de dereferencia, que aquí convierte &s2 en
&s2[..] . Discutiremos la coerción de dereferencia con más detalle en el Capítulo 15.
Debido a que add no toma posesión del parámetro s , s2 seguirá siendo un String
válido después de esta operación.
En segundo lugar, podemos ver en la firma que add toma el ownership de self , porque
self no tiene un & . Esto significa que s1 en el listado 8-18 se moverá a la llamada de add
y ya no será válido después de eso. Entonces, aunque let s3 = s1 + &s2; parece que
copiará ambos strings y creará uno nuevo, esta declaración realmente toma posesión de
s1 , agrega una copia del contenido de s2 y luego devuelve la propiedad del resultado. En
otras palabras, parece que está haciendo muchas copias, pero no lo está; la implementación
es más eficiente que copiar.
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
En este punto, s contendrá tic-tac-toe . Con todos los caracteres + y " es difícil ver qué
está pasando. Para una combinación de cadenas más complicada, podemos usar la macro
format! :
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
Indexando en Strings
let s1 = String::from("hello");
let h = s1[0];
https://book.rustlang-es.org/print 193/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `str` cannot be indexed by `{integer}`
--> src/main.rs:3:16
|
3 | let h = s1[0];
| ^ string indices are ranges of `usize`
|
= help: the trait `SliceIndex<str>` is not implemented for `{integer}`, which
is required by `String: Index<_>`
= note: you can use `.chars().nth()` or `.bytes().nth()`
for more information, see chapter 8 in The Book: <https://doc.rust-
lang.org/book/ch08-02-strings.html#indexing-into-strings>
= help: the trait `SliceIndex<[_]>` is implemented for `usize`
= help: for that trait implementation, expected `[_]`, found `str`
= note: required for `String` to implement `Index<{integer}>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` (bin "collections") due to 1 previous
error
El error y la nota cuentan la historia: los strings de Rust no admiten indexación. Pero, ¿por
qué no? Para responder a esa pregunta, necesitamos discutir cómo Rust almacena los
strings en la memoria.
Representación Interna
En este caso, len será 4 , lo que significa que el vector que almacena el string “Hola” tiene
4 bytes de largo. Cada una de estas letras toma un byte cuando se codifica en UTF-8. La
siguiente línea, sin embargo, puede sorprenderte. (Nota que este string comienza con la
letra cirílica Ze mayúscula, no con el número árabe 3.)
Si tu te preguntas que tan largo es el string, podrías decir 12. De hecho, la respuesta de Rust
es 24: ese es el número de bytes que se necesitan para codificar “Здравствуйте” en UTF-8,
porque cada valor escalar Unicode en ese string toma 2 bytes de almacenamiento. Por lo
tanto, un índice en los bytes del string no siempre se correlacionará con un valor escalar
Unicode válido. Para demostrarlo, considera este código inválido de Rust:
https://book.rustlang-es.org/print 194/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Tu Ahora sabes que answer no será З , la primera letra. Cuando codificado en UTF-8, el
primer byte de З es 208 y el segundo es 151 , por lo que parecería que answer debería
ser 208 , pero 208 no es un carácter válido por sí solo. Devolver 208 probablemente no
sea lo que un usuario querría si pidieran la primera letra de esta cadena; sin embargo, esos
son los únicos datos que Rust tiene en el índice de bytes 0. Los usuarios generalmente no
quieren que se devuelva el valor de byte, incluso si la cadena contiene solo letras latinas: si
&"hello"[0] fuera un código válido que devolviera el valor de byte, devolvería 104 , no h .
La respuesta, entonces, es que para evitar devolver un valor inesperado y causar errores
que podrían no descubrirse de inmediato, Rust no compila este código en absoluto y evita
malentendidos al comienzo del proceso de desarrollo.
Otro punto sobre UTF-8 es que hay tres formas relevantes de ver las cadenas desde la
perspectiva de Rust: como bytes, valores escalares y grupos de grafemas (lo más parecido a
lo que llamaríamos letras).
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
Eso es 18 bytes y es como las computadoras almacenan los datos. Si los observamos como
valores escalares Unicode, que es lo que es el tipo char de Rust, esos bytes se ven así:
Aquí hay seis valores char , pero el cuarto y el sexto no son letras: son diacríticos que no
tienen sentido por sí mismos. Finalmente, si los miramos como grupos de grafemas,
obtendríamos lo que una persona llamaría las cuatro letras que componen la palabra hindi:
Rust proporciona diferentes formas de interpretar los datos de string sin procesar que las
computadoras almacenan para que cada programa pueda elegir la interpretación que
necesita, sin importar en qué idioma humano estén los datos.
Una última razón por la que Rust no permite indexar en un String para obtener un
carácter es que se espera que las operaciones de indexación siempre tomen tiempo
constante (O(1)). Pero no es posible garantizar ese rendimiento con un String , porque Rust
tendría que recorrer el contenido desde el principio hasta el índice para determinar cuántos
caracteres válidos había.
https://book.rustlang-es.org/print 195/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Slicing Strings
La indexación en un String suele ser una mala idea porque no está claro cuál debería ser
el tipo de retorno de la operación de indexación de string: un valor de byte, un carácter, un
grupo de grafemas o una rebanada de string. Si realmente necesita usar índices para crear
rebanadas de string, por lo tanto, Rust le pide que sea más específico.
En lugar de indexar usando [] con un solo número, puede usar [] con un rango para
crear un string slice conteniendo bytes particulares:
let s = &hello[0..4];
Aquí, s será un &str que contiene los primeros cuatro bytes del string. Antes,
mencionamos que cada uno de estos caracteres era de dos bytes, lo que significa que s
será Зд .
Si intentáramos hacer un slice con solo una parte de los bytes de un carácter, algo como
&hello[0..1] , Rust entraría en pánico en 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`
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
Debemos tener cuidado cuando creamos string slices, porque hacerlo puede bloquear su
programa.
La mejor manera de operar en partes de strings es ser explícito sobre si desea caracteres o
bytes. Para valores escalares Unicode individuales, use el método chars . Llamar a chars
en “Зд” separa y devuelve dos valores de tipo char , y puede iterar sobre el resultado para
acceder a cada elemento:
for c in "Зд".chars() {
println!("{c}");
}
https://book.rustlang-es.org/print 196/656
12/7/24, 22:37 El Lenguaje de Programación Rust
З
д
Alternativamente, el método bytes devuelve cada byte sin procesar, que puede ser
apropiado para su dominio:
for b in "Зд".bytes() {
println!("{b}");
}
208
151
208
180
Pero asegúrate de recordar que los valores escalares de Unicode válidos pueden estar
compuestos por más de un byte.
Obtener grupos de grafemas a partir de cadenas, como en el caso del alfabeto Devanagari,
es complejo, por lo que esta funcionalidad no está proporcionada por la biblioteca estándar.
Hay paquetes disponibles en crates.io si necesitas esta funcionalidad.
Para resumir, los strings son complicados. Los diferentes lenguajes de programación hacen
diferentes elecciones sobre cómo presentar esta complejidad al programador. Rust ha
elegido hacer que el manejo correcto de los datos String sea el comportamiento
predeterminado para todos los programas de Rust, lo que significa que los programadores
tienen que pensar más en el manejo de datos UTF-8 por adelantado. Este compromiso
expone más de la complejidad de las cadenas de lo que parece en otros lenguajes de
programación, pero evita que tenga que manejar errores que involucran caracteres no ASCII
más adelante en su ciclo de vida de desarrollo.
https://book.rustlang-es.org/print 197/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los hash maps son útiles cuando desea buscar datos no usando un índice, como puede
hacerlo con vectores, sino usando una clave que puede ser de cualquier tipo. Por ejemplo,
en un juego, podría realizar un seguimiento de la puntuación de cada equipo en un hash
map en el que cada clave es el nombre de un equipo y los valores son la puntuación de cada
equipo. Dado un nombre de equipo, puede recuperar su puntuación.
Repasaremos la API básica de los hash maps en esta sección, pero muchas más cosas
buenas se esconden en las funciones definidas en HashMap<K, V> por la biblioteca
estándar. Como siempre, consulte la documentación de la biblioteca estándar para obtener
más información.
Una forma de crear un hash map vacío es usar new y agregar elementos con insert . En el
Listado 8-20, estamos realizando un seguimiento de las puntuaciones de dos equipos cuyos
nombres son Blue y Yellow. El equipo Blue comienza con 10 puntos y el equipo Yellow
comienza con 50.
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Listado 8-20: Creando un nuevo hash map e insertando algunas claves y valores
Al igual que los vectores, los hash maps almacenan sus datos en el heap. Este HashMap tiene
claves de tipo String y valores de tipo i32 . Al igual que los vectores, los hash maps son
https://book.rustlang-es.org/print 198/656
12/7/24, 22:37 El Lenguaje de Programación Rust
homogéneos: todas las claves deben tener el mismo tipo entre sí y todos los valores deben
tener el mismo tipo.
Podemos obtener un valor de un hash map proporcionando su clave al método get como
se muestra en el listado 8-21.
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Listado 8-21: Acceso al puntaje para el equipo Blue almacenado en el hash map
Aquí, score tendrá el valor que está asociado con el equipo Blue, y el resultado será 10 . El
método get devuelve un Option<&V> ; si no hay un valor para ese clave en el hash map,
get devolverá None . Este programa maneja un Option llamando a copied para obtener
un Option<i32> en lugar de un Option<&i32> , luego unwrap_or para establecer score en
cero si scores no tiene una entrada para la clave.
Podemos iterar sobre cada par clave-valor en un hash map de manera similar a como lo
hacemos con vectores, usando un ciclo for :
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Yellow: 50
Blue: 10
https://book.rustlang-es.org/print 199/656
12/7/24, 22:37 El Lenguaje de Programación Rust
HashMaps y Ownership
Para los tipos que implementan el trait Copy , como i32 , los valores se copian en el hash
map. Para valores de propiedad como String , los valores se moverán y el hash map será el
propietario de esos valores, como se demuestra en el listado 8-22.
use std::collections::HashMap;
Listado 8-22: Mostrando que claves y valores son propiedad del hash map una vez que se insertan
Si insertamos referencias a valores en el hash map, los valores no se moverán al hash map.
Los valores a los que apuntan las referencias deben ser válidos al menos mientras el hash
map sea válido. Hablaremos más sobre estos problemas en la sección “Validando
referencias con Lifetimes” en el Capítulo 10.
Actualizando un HashMap
Aunque la cantidad de pares clave/valor es creciente, cada clave única solo puede tener un
valor asociado con ella a la vez (pero no viceversa: por ejemplo, el equipo Blue y el equipo
Yellow podrían tener el valor 10 almacenados en el hash map scores ).
Cuando queremos cambiar los datos en un hash map, tenemos que decidir cómo manejar
el caso en el que una clave ya tiene un valor asignado. Podrías reemplazar el valor antiguo
por el nuevo valor, ignorando completamente el valor antiguo. Podrías mantener el valor
antiguo e ignorar el nuevo valor, agregando el nuevo valor solo si la clave no tiene ya un
valor. O podrías combinar el valor antiguo y el nuevo valor. ¡Veamos cómo hacer cada una
de estas!
Reemplazando un valor
Si insertamos una clave y un valor en un hash map y luego insertamos esa misma clave con
un valor diferente, el valor asociado con esa clave se reemplazará. Aunque el código en el
listado 8-23 llama a insert dos veces, el hash map solo contendrá un par clave-valor
porque estamos insertando el valor para la clave del equipo Blue dos veces.
https://book.rustlang-es.org/print 200/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{scores:?}");
Es común verificar si una clave en particular ya existe en el hash map con un valor y luego
realizar las siguientes acciones: si la clave existe en el hash map, el valor existente debe
permanecer tal como está; si la clave no existe, insertarla junto con su valor.
Los hash maps tienen una API especial para esto llamada entry que toma la clave que
desea verificar como parámetro. El valor de retorno del método entry es un enum llamado
Entry que representa un valor que puede o no existir. Digamos que queremos verificar si
la clave para el equipo Yellow tiene un valor asociado. Si no lo tiene, queremos insertar el
valor 50 , y lo mismo para el equipo Blue. Usando la API entry , el código se ve como el
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: Usando el método entry para insertar solo si la clave aún no tiene un valor
El método or_insert en Entry está definido para devolver una referencia mutable al valor
correspondiente a la clave Entry si esa clave existe, y si no, inserta el parámetro como el
nuevo valor para esta clave y devuelve una referencia mutable al nuevo valor. Esta técnica
es mucho más limpia que escribir la lógica nosotros mismos y, además, juega mejor con el
borrow checker.
Ejecutar el código en el listado 8-24 imprimirá {"Yellow": 50, "Blue": 10} . La primera
llamada a entry insertará la clave para el equipo Yellow con el valor 50 porque el equipo
Yellow no tiene un valor todavía. La segunda llamada a entry no cambiará el hash map
porque el equipo Blue ya tiene el valor 10 .
https://book.rustlang-es.org/print 201/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Otro caso común para los hash maps es buscar un valor para una clave y luego actualizar
ese valor en función del valor anterior. Por ejemplo, el listado 8-25 muestra un código que
cuenta cuántas veces aparece cada palabra en algún texto. Usamos un hash map con las
palabras como claves y aumentamos el valor para mantener un recuento de cuántas veces
hemos visto esa palabra. Si es la primera vez que vemos una palabra, primero insertaremos
el valor 0 .
use std::collections::HashMap;
println!("{map:?}");
Listado 8-25: Contando ocurrencias de palabras usando un hash map que almacena palabras y cuenta
Funciones de Hashing
Por defecto, HashMap usa una función de hashing llamada SipHash que puede proporcionar
resistencia a ataques de Denegación de Servicio (DoS) que involucran tablas hash1. Este no
es el algoritmo de hashing más rápido disponible, pero el compromiso por una mejor
seguridad que viene con la caída en el rendimiento vale la pena. Si perfilas tu código y
encuentras que la función de hash predeterminada es demasiado lenta para tus propósitos,
puedes cambiar a otra función especificando un hasher diferente. Un hasher es un tipo que
implementa el trait BuildHasher . Hablaremos sobre traits y cómo implementarlos en el
Capítulo 10. No necesariamente tienes que implementar tu propio hasher desde cero;
https://book.rustlang-es.org/print 202/656
12/7/24, 22:37 El Lenguaje de Programación Rust
crates.io tiene bibliotecas compartidas por otros usuarios de Rust que proporcionan hashes
que implementan muchos algoritmos de hashing comunes.
1 https://en.wikipedia.org/wiki/SipHash
Resumen
Los vectores, los strings y los hash maps proporcionan una funcionalidad importante que
necesitarás cuando quieras almacenar, acceder y modificar datos. Aquí hay algunos
ejercicios que ahora deberías estar equipado para resolver:
1. Dada una lista de enteros, usa un vector y devuelve la mediana (cuando se ordena, el
valor en la posición media) y la moda (el valor que ocurre con más frecuencia; un hash
map será útil aquí) de la lista.
2. Convierte strings a pig latin. La primera consonante de cada palabra se mueve al final
de la palabra y se agrega "ay", por lo que "primero" se convierte en "rimepay". Sin
embargo, si la palabra comienza con una vocal, simplemente agregue "hay" al final de
la palabra ("manzanaay"). ¡Ten en cuenta las reglas de UTF-8!
3. Usando un hash map y vectores, cree un texto de interfaz para permitir que un
usuario agregue nombres de empleados a un departamento en una empresa. Por
ejemplo, "Agregar Sally a Ingeniería" o "Agregar Amir a Ventas". Luego, permita que el
usuario recupere una lista de todas las personas en un departamento o todas las
personas en la empresa por departamento, ordenadas alfabéticamente.
Nos estamos adentrando en programas más complejos en los que las operaciones pueden
fallar, por lo que es un momento perfecto para discutir el manejo de errores. ¡Haremos eso
a continuación!
https://book.rustlang-es.org/print 203/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Manejo de Errores
Los errores son un hecho de la vida en el software, por lo que Rust tiene una serie de
características para manejar situaciones en las que algo sale mal. En muchos casos, Rust te
obliga a reconocer la posibilidad de un error y tomar alguna acción antes de que tu código
se compile. ¡Este requisito hace que su programa sea más robusto al garantizar que
descubrirá errores y los manejará adecuadamente antes de implementar su código en
producción!
Rust agrupa los errores en dos categorías principales: errores recuperables e irrecuperables.
Para un error recuperable, como un error de archivo no encontrado, lo más probable es que
solo queramos informar el problema al usuario y volver a intentar la operación. Los errores
irreversibles siempre son síntomas de errores, como intentar acceder a una ubicación más
allá del final de un arreglo, por lo que queremos detener inmediatamente el programa.
La mayoría de los lenguajes no distinguen entre estos dos tipos de errores y los manejan de
la misma manera, utilizando mecanismos como excepciones. Rust no tiene excepciones. En
cambio, tiene el tipo Result<T, E> para errores recuperables y el macro panic! que
detiene la ejecución cuando el programa encuentra un error irrecuperable. Este capítulo
cubre primero la llamada a panic! y luego habla sobre la devolución de valores Result<T,
E> . Además, exploraremos consideraciones al decidir si intentar recuperarse de un error o
detener la ejecución.
https://book.rustlang-es.org/print 204/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La memoria que el programa estaba usando deberá ser limpiada por el sistema
operativo. Si en su proyecto necesita hacer que el binario resultante sea lo más
pequeño posible, puede cambiar de deshacer el programa a abortarlo al producir un
pánico agregando panic = 'abort' a las secciones [profile] apropiadas en su
archivo Cargo.toml. Por ejemplo, si desea abortar en caso de pánico en el modo de
lanzamiento, agregue esto:
[profile.release]
panic = 'abort'
Filename: src/main.rs
fn main() {
panic!("crash and burn");
}
https://book.rustlang-es.org/print 205/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
La llamada a panic! causa el mensaje de error contenido en las dos últimas líneas. La
primera línea muestra nuestro mensaje de panic y el lugar en nuestro código fuente donde
ocurrió el panic: src/main.rs:2:5 indica que es la segunda línea, quinto carácter de nuestro
archivo src/main.rs.
En este caso, la línea indicada es parte de nuestro código, y si vamos a esa línea, vemos la
llamada a la macro panic! . En otros casos, la llamada a panic! podría estar en el código
que nuestro código llama, y el nombre de archivo y el número de línea informados por el
mensaje de error serán el código de otra persona donde se llama a la macro panic! , no la
línea de nuestro código que finalmente condujo a la llamada a panic! . Podemos usar el
backtrace de las funciones de las que provino la llamada a panic! para determinar la parte
de nuestro código que está causando el problema. Discutiremos el backtrace en más detalle
a continuación.
Veamos otro ejemplo de cómo es cuando una llamada a panic! proviene de una biblioteca
debido a un error en nuestro código en lugar de que nuestro código llame directamente a la
macro. El listado 9-1 tiene algún código que intenta acceder a un índice en un vector más
allá del rango de índices válidos.
Filename: src/main.rs
fn main() {
let v = vec![1, 2, 3];
v[99];
}
Listado 9-1: Intentando acceder a un elemento más allá del fin de un vector, que provocará una llamada a
panic!
Aquí, estamos intentando acceder al elemento 100 de nuestro vector (que está en el índice
99 porque el indexado comienza en cero), pero el vector solo tiene 3 elementos. En esta
situación, Rust entrará en pánico. Usar [] se supone que devuelve un elemento, pero si
pasa un índice no válido, no hay ningún elemento que Rust podría devolver aquí que sea
correcto.
https://book.rustlang-es.org/print 206/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En C, intentar leer más allá del final de una estructura de datos es un undefined. Podría
obtener lo que está en la ubicación de memoria que correspondería a ese elemento en la
estructura de datos, aunque la memoria no pertenece a esa estructura. Esto se llama buffer
overread y puede provocar vulnerabilidades de seguridad si un atacante puede manipular el
índice de tal manera que lea datos que no debería estar permitido que se almacenen
después de la estructura de datos.
$ 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
Este error apunta a la línea 4 de nuestro main.rs donde intentamos acceder al índice 99. La
siguiente línea de nota nos dice que podemos establecer la variable de entorno
RUST_BACKTRACE para obtener el backtrace de exactamente lo que sucedió para causar el
error. El Backtrace es una lista de todas las funciones que se han llamado para llegar a este
punto. El backtrace en Rust funciona como lo hacen en otros lenguajes: la clave para leer el
backtrace es comenzar desde la parte superior y leer hasta que vea archivos que escribió.
Ese es el lugar donde se originó el problema. Las líneas por encima de ese punto son el
código que su código ha llamado; las líneas a continuación son el código que llamó a su
código. Estas líneas antes y después pueden incluir código de Rust core, código de biblioteca
estándar o crates que estés usando. Intentemos obtener el backtrace estableciendo la
variable de entorno RUST_BACKTRACE a cualquier valor excepto 0. El listado 9-2 muestra una
salida similar a la que verás.
https://book.rustlang-es.org/print 207/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Listado 9-2: El backtrace generado por una llamada a panic! se muestra cuando la variable de entorno
RUST_BACKTRACE está configurada
¡Eso es mucho resultado! La salida exacta que vea puede ser diferente según su sistema
operativo y la versión de Rust. Para obtener el backtrace con esta información, deben estar
habilitados los símbolos de depuración. Los símbolos de depuración están habilitados de
forma predeterminada cuando se usa cargo build o cargo run sin el indicador --
release , como tenemos aquí.
En la salida en el listado 9-2, la línea 6 del backtrace apunta a la línea en nuestro proyecto
que está causando 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
señalada por la primera línea que menciona un archivo que escribimos. En la listado 9-1,
donde escribimos deliberadamente un código que entraría en pánico, la forma de
solucionar el pánico es no solicitar un elemento más allá del rango de los índices del vector.
https://book.rustlang-es.org/print 208/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cuando su código entra en pánico en el futuro, deberá averiguar 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.
https://book.rustlang-es.org/print 209/656
12/7/24, 22:37 El Lenguaje de Programación Rust
T y E son parámetros de tipo genérico: hablaremos de los genéricos con más detalle en el
Capítulo 10. Lo que necesita saber ahora es que T representa el tipo del valor que será
devuelto en un caso de éxito dentro de la variante Ok , y E representa el tipo del error que
será devuelto en un caso de fallo dentro de la variante Err . Debido a que Result tiene
estos parámetros de tipo genérico, podemos usar el tipo Result y las funciones definidas
en él en muchas situaciones diferentes donde el valor de éxito y el valor de error que
queremos devolver pueden diferir.
Llamemos a una función que devuelve un valor Result porque la función podría fallar. En
el listado 9-3 intentamos abrir un archivo.
Filename: src/main.rs
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
}
Filename: src/main.rs
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
Listado 9-4: Usando una expresión match para manejar las variantes Result que podrían devolverse
Ten en cuenta que, al igual que el enum Option , el enum Result y sus variantes se han
traído al ámbito por el prelude, por lo que no necesitamos especificar Result:: antes de
las variantes Ok y Err en las opciones de match .
Cuando el result es Ok , este código devolverá el valor interno file fuera de la variante Ok ,
y luego asignaremos ese valor de manejador de archivo a la variable greeting_file .
Después del match , podemos usar el manejador de archivo para leer o escribir.
La otra opción en el match es Err , que significa que el File::open ha fallado y el valor
interno err de la variante Err contendrá información sobre cómo o por qué falló
File::open . En este caso, llamamos a la función panic! y pasamos el valor err al
panic! . Esto causa que nuestro programa se bloquee y muestre el mensaje de error que
panic! proporciona. Si ejecutamos este código, obtendremos el siguiente mensaje de
error:
https://book.rustlang-es.org/print 211/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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 de costumbre, esta salida nos dice exactamente qué ha salido mal.
El código en el listado 9-4 será panic! no importa por qué File::open falló. Sin embargo,
queremos tomar diferentes acciones para diferentes razones de falla: si File::open falló
porque el archivo no existe, queremos crear el archivo y devolver el manejador del nuevo
archivo. Si File::open falló por cualquier otra razón, por ejemplo, porque no teníamos
permiso para abrir el archivo, todavía queremos que el código dispare el panic! de la
misma manera que lo hizo en el listado 9-4. Para esto agregamos una expresión match
interna, que se muestra en el listado 9-5.
Filename: src/main.rs
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
El tipo de valor que File::open devuelve dentro de la variante Err es io::Error , que es
una estructura proporcionada por la biblioteca estándar. Esta estructura tiene un método
kind que podemos llamar para obtener un valor io::ErrorKind . El enum io::ErrorKind
es proporcionado por la biblioteca estándar y tiene variantes que representan los diferentes
https://book.rustlang-es.org/print 212/656
12/7/24, 22:37 El Lenguaje de Programación Rust
tipos de errores que podrían resultar de una operación io . La variante que queremos usar
es ErrorKind::NotFound , que indica que el archivo que estamos tratando de abrir aún no
existe. Así que hacemos coincidir en greeting_file_result , pero también tenemos una
coincidencia interna en error.kind() .
¡Eso es mucho match ! La expresión match es útil, pero también es bastante verbosa.
En el Capítulo 13 aprenderás sobre los closures, que se usan con muchos de los
métodos definidos en Result<T, E> . Estos métodos pueden ser más concisos que
usar match al manejar valores Result<T, E> en tu código.
Por ejemplo, aquí hay otra forma de escribir la misma lógica que se muestra en el
listado 9-5, esta vez usando closures y el método unwrap_or_else :
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
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:?}");
}
});
}
Aunque este código tiene el mismo comportamiento que el listado 9-5, no contiene
ninguna expresión match y es más fácil de leer. Vuelve a este ejemplo después de leer
el Capítulo 13, y busca el método unwrap_or_else en la documentación de la
biblioteca estándar. Muchos más de estos métodos pueden limpiar enormes
expresiones match anidadas cuando se trata de errores.
https://book.rustlang-es.org/print 213/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Usando match funciona bastante bien, pero puede ser un poco verboso y no siempre
comunica bien la intención. El tipo Result<T, E> tiene muchos métodos auxiliares
definidos en él para hacer varias tareas más específicas. El método unwrap es un método
de atajo implementado exactamente como la expresión match que escribimos en el listado
9-4. Si el valor Result es la variante Ok , unwrap devolverá el valor dentro de Ok . Si el
Result es la variante Err , unwrap llamará a la macro panic! por nosotros. Aquí hay un
ejemplo de unwrap en acción:
Filename: src/main.rs
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
Del mismo modo, el método expect nos permite elegir el mensaje de error de panic! .
Usando expect en lugar de unwrap y proporcionando buenos mensajes de error puede
transmitir tu intención y facilitar el seguimiento de la fuente de un pánico. La sintaxis de
expect se ve así:
Filename: src/main.rs
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
Nosotros usamos expect de la misma manera que unwrap : para devolver el manejo de
archivo o llamar a la macro panic! . El mensaje de error utilizado por expect en su llamada
a panic! será el parámetro que pasamos a expect , en lugar del mensaje predeterminado
de panic! que usa unwrap . Así es como se ve:
https://book.rustlang-es.org/print 214/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En producción, la mayoría de los Rustaceans eligen expect en lugar de unwrap y dan más
contexto sobre por qué se espera que la operación siempre tenga éxito. De esa manera, si
tus suposiciones se demuestran incorrectas, tienes más información para usar en la
depuración.
Propagación de errores
Cuando escribes una función cuyo cuerpo puede generar un error, en lugar de manejar el
error dentro de la función, puedes devolver el error al código que llamó la función. Esto se
conoce como propagación del error y le da más control al código que llama, donde puede
haber más información o lógica que dicte cómo se debe manejar el error que la que tienes
disponible en el contexto de tu 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 llamó a la función.
Filename: src/main.rs
use std::fs::File;
use std::io::{self, Read};
Listado 9-6: Una función que devuelve errores al código llamado usando match
Esta función se puede escribir de una manera mucho más corta, pero vamos a empezar por
hacer mucho de ella manualmente para explorar el manejo de errores; al final,
mostraremos la forma más corta. Veamos primero el tipo de retorno de la función:
Result<String, io::Error> . Esto significa que la función está devolviendo un valor del
tipo Result<T, E> donde el parámetro genérico T se ha rellenado con el tipo concreto
String , y el tipo genérico E se ha rellenado con el tipo concreto io::Error .
Si esta función tiene éxito sin ningún problema, el código que llama a esta función recibirá
un valor Ok que contiene una String - el nombre de usuario que esta función leyó del
https://book.rustlang-es.org/print 215/656
12/7/24, 22:37 El Lenguaje de Programación Rust
archivo. Si esta función encuentra algún problema, el código que llama recibirá un valor
Err que contiene una instancia de io::Error que contiene más información sobre cuáles
fueron los problemas. Elegimos io::Error como el tipo de retorno de esta función porque
eso sucede que es el tipo del valor de error devuelto de ambas operaciones que estamos
llamando en el cuerpo de esta función que podrían fallar: la función File::open y el
método read_to_string .
El código que llama a este código se encargará de obtener un valor Ok que contiene un
nombre de usuario o un valor Err que contiene un io::Error . Es responsabilidad del
código que llama decidir qué hacer con esos valores. Si el código que llama obtiene un valor
Err , podría llamar a panic! y bloquear el programa, usar un nombre de usuario
predeterminado o buscar el nombre de usuario en algún lugar que no sea un archivo, por
ejemplo. No tenemos suficiente información sobre lo que el código que llama realmente
está tratando de hacer, por lo que propagamos toda la información de éxito o error hacia
arriba para que la maneje apropiadamente.
Este patrón de propagación de errores es tan común en Rust que Rust proporciona el
operador de interrogación ? para hacer esto más fácil.
Filename: src/main.rs
https://book.rustlang-es.org/print 216/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::fs::File;
use std::io::{self, Read};
Listado 9-7: Una función que devuelve errores al código llamado usando el operador ?
Hay una diferencia entre lo que hace la expresión match del listado 9-6 y lo que hace el
operador ? : los valores de error que tienen el operador ? llamado en ellos pasan a través
de la función from , definida en el trait From en la biblioteca estándar, que se usa para
convertir valores de un tipo a otro. Cuando el operador ? llama a la función from , el tipo
de error recibido se convierte en el tipo de error definido en el tipo de retorno de la función
actual. Esto es útil cuando una función devuelve un tipo de error para representar todas las
formas en que una función podría fallar, incluso si las partes podrían fallar por muchas
razones diferentes.
Filename: src/main.rs
https://book.rustlang-es.org/print 217/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::fs::File;
use std::io::{self, Read};
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
Hemos movido la creación del nuevo String en username al principio de la función; esa
parte no ha cambiado. En lugar de crear una variable username_file , hemos encadenado
la llamada a read_to_string directamente sobre el resultado de
File::open("hello.txt")? . Todavía tenemos un ? al final de la llamada a
read_to_string , y todavía devolvemos un valor Ok que contiene username cuando tanto
File::open como read_to_string tienen éxito en lugar de devolver errores. La
funcionalidad es nuevamente la misma que en el listado 9-6 y el listado 9-7; esta es solo una
forma diferente y más ergonómica de escribirla.
El listado 9-9 muestra una forma de hacer esto aún más conciso usando
fs::read_to_string .
Filename: src/main.rs
use std::fs;
use std::io;
Leer un archivo en un String es una operación bastante común, por lo que la biblioteca
estándar proporciona la conveniente función fs::read_to_string que abre el archivo, crea
un nuevo String , lee el contenido del archivo, coloca el contenido en ese String y lo
devuelve. Por supuesto, usar fs::read_to_string no nos da la oportunidad de explicar
todo el manejo de errores, por lo que lo hicimos de la manera más larga primero.
El operador ? solo puede usarse en funciones cuyo tipo de retorno sea compatible con el
valor que se usa con el operador ? . Porque el operador ? está definido para realizar una
devolución temprana de un valor de la función, de la misma manera que la expresión
match que definimos en el listado 9-6. En el listado 9-6, el match estaba usando un valor
https://book.rustlang-es.org/print 218/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/main.rs
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
Este código abre un archivo, que puede fallar. El operador ? sigue el valor Result devuelto
por File::open , pero esta función main tiene el tipo de retorno de () , no Result .
Cuando compilamos este código, obtenemos el siguiente mensaje de error:
$ 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 `()`
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 podemos usar el operador ? en una función que devuelve
Result o Option o en cualquier otro tipo que implemente FromResidual .
Para corregir el error, tienes dos opciones. Una opción es cambiar el tipo de retorno de tu
función para que sea compatible con el valor que estás usando el operador ? mientras no
tengas restricciones que lo impidan. La otra técnica es usar un match o uno de los métodos
Result<T, E> para manejar el Result<T, E> de la manera que sea apropiada.
El mensaje de error también menciona que el operador ? también se puede usar con
valores Option<T> . Al igual que con el uso de ? en Result , solo puedes usar ? en Option
en una función que devuelve Option . El comportamiento del operador ? cuando se llama
https://book.rustlang-es.org/print 219/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Esta función devuelve Option<char> porque es posible que haya un carácter allí, pero
también es posible que no lo haya. Este código toma el argumento de string slice text y
llama al método lines en él, que devuelve un iterador sobre las líneas en el string. Debido
a que esta función quiere examinar la primera línea, llama a next en el iterador para
obtener el primer valor del iterador. Si text es un string vacío, esta llamada a next
devolverá None , en cuyo caso usamos ? para detener y devolver None desde
last_char_of_first_line . Si text no es un string vacío, next devolverá un valor Some
que contiene un string slice de la primera línea en text .
El ? extrae el string slice, y podemos llamar a chars en ese string slice para obtener un
iterador de sus caracteres. Estamos interesados en el último carácter en esta primera línea,
por lo que llamamos a last para devolver el último elemento en el iterador. Esto es un
Option porque es posible que la primera línea sea el string vacío, por ejemplo, si text
comienza 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 variante Some .
El operador ? en el medio nos da una forma concisa de expresar esta lógica, lo que nos
permite implementar la función en una línea. Si no pudiéramos usar el operador ? en
Option , tendríamos que implementar esta lógica usando más llamadas de método o una
expresión match .
Ten en cuenta que puedes usar el operador ? en una función que devuelve Result y
puedes usar el operador ? en una función que devuelve Option , pero no puedes mezclar y
combinar. El operador ? no convertirá automáticamente un Result en un Option o
viceversa; en esos casos, puedes usar métodos como el método ok en Result o el método
ok_or en Option para hacer la conversión explícitamente.
Hasta ahora, todas las funciones main que hemos usado devuelven () . La función main
es especial porque es el punto de entrada y salida de los programas ejecutables, y hay
restricciones sobre cuál puede ser su tipo de retorno para que los programas se comporten
como se espera.
Por suerte, main también puede devolver un Result<(), E> . El Listado 9-12 tiene el código
del listado 9-10, pero hemos cambiado el tipo de retorno de main para que sea Result<(),
https://book.rustlang-es.org/print 220/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Box<dyn Error>> y hemos agregado un valor de retorno Ok(()) al final. Este código ahora
se compilará:
use std::error::Error;
use std::fs::File;
Ok(())
}
Listado∂ 9-12: Cambiando main devuelve Result<(), E> permitiendo el uso del operador ? en valores Result
El Box<dyn Error> tipo es un trait object, que hablaremos en la sección “Usando Trait
Objects que permiten valores de diferentes tipos” en el Capítulo 17. Por ahora, puedes leer
Box<dyn Error> para significar “cualquier tipo de error”. Usar ? en un valor Result en
una función main con el tipo de error Box<dyn Error> está permitido, porque permite que
cualquier valor Err se devuelva temprano. A pesar de que el cuerpo de esta función main
solo devolverá errores de tipo std::io::Error , al especificar Box<dyn Error> , esta firma
seguirá siendo correcta incluso si se agrega más código que devuelva otros errores al
cuerpo de main .
Cuando una función main devuelve un Result , el ejecutable puede salir con un valor de 0
si main devuelve Ok(()) y saldrá con un valor distinto de 0 si main devuelve un Err . Los
ejecutables escritos en C devuelven enteros cuando salen: los programas que salen con
éxito devuelven el entero 0 , y los programas que devuelven un error devuelven algún
entero distinto de 0 . Rust también devuelve enteros de ejecutables para ser compatibles
con esta convención.
Ahora que hemos discutido los detalles de llamar a panic! o devolver Result , volvamos al
tema de cómo decidir cuál es apropiado usar en qué casos.
https://book.rustlang-es.org/print 221/656
12/7/24, 22:37 El Lenguaje de Programación Rust
panic! o no panic!
Entonces, ¿cómo decides cuándo debes llamar a panic! y cuándo debes devolver Result ?
Cuando el código entra en panic, no hay forma de recuperarse. Podrías llamar a panic!
para cualquier situación de error, ya sea que haya una forma posible de recuperarse o no,
pero entonces estás tomando la decisión de que una situación es irreparable en nombre del
código que llama. Cuando eliges devolver un valor Result , le das al código que llama
opciones. El código que llama podría elegir intentar recuperarse de una manera que sea
apropiada para su situación, o podría decidir que un valor Err en este caso es irreparable,
por lo que puede llamar a panic! y convertir su error recuperable en uno irreparable. Por
lo tanto, devolver Result es una buena opción predeterminada cuando estás definiendo
una función que podría fallar.
Cuando estás escribiendo un ejemplo para ilustrar algún concepto, también incluir código
de manejo de errores robusto puede hacer que el ejemplo sea menos claro. En los
ejemplos, se entiende que una llamada a un método como unwrap que podría entrar en
panic se entiende como un marcador de posición para la forma en que desea que su
aplicación maneje los errores, que puede diferir según lo que el resto de su código está
haciendo.
De manera similar, los métodos unwrap y expect son muy útiles cuando se prototipa,
antes de que estés listo para decidir cómo manejar los errores. Dejan marcadores claros en
tu código para cuando estés listo para hacer que tu programa sea más robusto.
Si una llamada a un método falla en una prueba, querrás que toda la prueba falle, incluso si
ese método no es la funcionalidad en prueba. Debido a que panic! es la forma en que una
prueba se marca como fallida, llamar a unwrap o expect es exactamente lo que debería
suceder.
https://book.rustlang-es.org/print 222/656
12/7/24, 22:37 El Lenguaje de Programación Rust
También sería apropiado llamar a unwrap o expect cuando tienes alguna otra lógica que
garantiza que el Result tendrá un valor Ok , pero la lógica no es algo que el compilador
entiende. Aún tendrás un valor Result que debes manejar: la operación que estás
llamando aún tiene la posibilidad de fallar en general, incluso si es lógicamente imposible en
tu situación particular. Si puedes asegurar inspeccionando manualmente el código que
nunca tendrás una variante Err , es perfectamente aceptable llamar a unwrap , e incluso
mejor documentar la razón por la que crees que nunca tendrás una variante Err en el
texto de expect . Aquí hay un ejemplo:
use std::net::IpAddr;
Aquí estamos creando una instancia IpAddr analizando una cadena codificada. Podemos
ver que 127.0.0.1 es una dirección IP válida, por lo que es aceptable usar expect aquí. Sin
embargo, tener una cadena válida codificada no cambia el tipo de retorno del método
parse : aún obtenemos un valor Result , y el compilador aún nos hará manejar el Result
como si la variante Err fuera una posibilidad porque el compilador no es lo
suficientemente inteligente como para ver que esta cadena es siempre una dirección IP
válida. Si la cadena de dirección IP proviniera de un usuario en lugar de estar codificada en
el programa y, por lo tanto, tuviera una posibilidad de falla, definitivamente querríamos
manejar el Result de una manera más robusta en su lugar. Mencionar la suposición de
que esta dirección IP está codificada nos indicará que cambiemos expect a un mejor
código de manejo de errores si en el futuro necesitamos obtener la dirección IP de otra
fuente.
Es aconsejable que tu código entre en panic cuando sea posible que tu código termine en
un estado incorrecto. En este contexto, un estado incorrecto es cuando se ha roto alguna
suposición, garantía, contrato o invariante, como cuando se pasan valores no válidos,
valores contradictorios o valores faltantes a tu código, más uno o más de los siguientes:
https://book.rustlang-es.org/print 223/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Si alguien llama a tu código y pasa valores que no tienen sentido, es mejor devolver un error
si puedes para que el usuario de la biblioteca pueda decidir qué hacer en ese caso. Sin
embargo, en los casos en que continuar podría ser inseguro o dañino, la mejor opción
podría ser llamar a panic! y alertar a la persona que usa tu biblioteca sobre el error en su
código para que puedan solucionarlo durante el desarrollo. De manera similar, panic! a
menudo es apropiado si estás llamando a un código externo que está fuera de tu control y
devuelve un estado no válido que no tienes forma de solucionar.
Sin embargo, cuando se espera que falle, es más apropiado devolver un Result que hacer
una llamada a panic! . Los ejemplos incluyen un analizador que recibe datos con formato
incorrecto o una solicitud HTTP que devuelve un estado que indica que has alcanzado un
límite de velocidad. En estos casos, devolver un Result indica que el fallo es una posibilidad
esperada que el código llamado decidida cómo manejarlo.
Cuando tu código realiza una operación que podría poner a un usuario en riesgo si se llama
con valores no válidos, tu código debe verificar primero que los valores sean válidos y entrar
en panic si los valores no son válidos. Esto es principalmente por 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á a panic! si intentas un acceso a
memoria fuera de los límites: intentar acceder a la 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 con requisitos
particulares. Entrar en panic cuando se viola el contrato tiene sentido porque una violación
del contrato siempre indica un error del lado del llamador y no es un tipo de error que
deseas que el código llamado tenga que manejar explícitamente. De hecho, no hay una
manera razonable para que el código de llamada se recupere; los programadores que
llaman deben corregir el código. Los contratos para una función, especialmente cuando una
violación causará un panic, deben explicarse en la documentación de la API de la función.
Sin embargo, tener muchas comprobaciones de errores en todas tus funciones sería
verboso y molesto. Afortunadamente, puedes usar el sistema de tipos de Rust (y, por lo
tanto, la comprobación de tipos realizada por el compilador) para hacer muchas de las
comprobaciones por ti. Si tu función tiene un tipo particular como parámetro, puedes
proceder con la lógica de tu código sabiendo que el compilador ya se ha asegurado de que
tengas un valor válido. Por ejemplo, si tienes un tipo en lugar de un Option , tu programa
espera tener algo en lugar de nada. Tu código entonces no tiene que manejar dos casos
para las variantes Some y None : solo tendrá un caso para tener definitivamente un valor. El
código que intenta pasar nada a tu función ni siquiera se compilará, por lo que tu función
no tiene que verificar ese caso en tiempo de ejecución. Otro ejemplo es usar un tipo de
entero sin signo como u32 , que garantiza que el parámetro nunca sea negativo.
https://book.rustlang-es.org/print 224/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Tomemos la idea de usar el sistema de tipos de Rust para garantizar que tengamos un valor
válido un paso más allá y veamos cómo crear un tipo personalizado para validación.
Recuerda el juego de adivinanzas en el Capítulo 2 en el que nuestro código le pidió al
usuario que adivinara un número entre 1 y 100. Nunca validamos que la suposición del
usuario estuviera entre esos números antes de verificarla con nuestro número secreto; solo
validamos que la suposición fuera positiva. En este caso, las consecuencias no fueron muy
graves: nuestra salida de “Demasiado alto” o “Demasiado bajo” seguiría siendo correcta.
Pero sería una mejora útil guiar al usuario hacia suposiciones válidas y tener un
comportamiento diferente cuando un usuario adivina un número que está fuera del rango
en comparación con cuando un usuario escribe, por ejemplo, letras en su lugar.
Una forma de hacer esto sería analizar la suposición como un i32 en lugar de solo un u32
para permitir números potencialmente negativos, y luego agregar una verificación de que el
número esté en el rango, de esta manera:
loop {
// --snip--
match guess.cmp(&secret_number) {
// --snip--
}
La expresión if verifica si nuestro valor está fuera del rango, le dice al usuario sobre el
problema y llama a continue para iniciar la siguiente iteración del ciclo y pedir otra
suposición. Después de la expresión if , podemos continuar con las comparaciones entre
guess y el número secreto sabiendo que guess está entre 1 y 100.
Sin embargo, esta no es una solución ideal: si fuera absolutamente crítico que el programa
solo operara en valores entre 1 y 100, y tuviera muchas funciones con este requisito, tener
una verificación como esa en cada función sería tedioso (y podría afectar el rendimiento).
En su lugar, podemos crear un nuevo tipo y poner las verificaciones en una función para
crear una instancia del tipo en lugar de repetir las verificaciones en cada función. De esa
manera, es seguro que las funciones utilicen el nuevo tipo en sus firmas y utilicen los
valores que reciben con confianza. El Listado 9-13 muestra una forma de definir un tipo
Guess que solo creará una instancia de Guess si la función new recibe un valor entre 1 y
100.
https://book.rustlang-es.org/print 225/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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 }
}
Listing 9-13: Un tipo Guess que solo continuará con valores entre 1 y 100
Primero, definimos un struct llamado Guess que tiene un campo llamado value que
contiene un i32 . Aquí es donde se almacenará el número.
Luego implementamos una función asociada llamada new en Guess que crea instancias de
valores Guess . La función new está definida para tener un parámetro llamado value de
tipo i32 y para devolver un Guess . El código en el cuerpo de la función new prueba value
para asegurarse de que esté entre 1 y 100. Si value no pasa esta prueba, hacemos una
llamada panic! , que alertará al programador que está escribiendo el código de llamada
que tiene un error que debe corregir, porque crear un Guess con un value fuera de este
rango violaría el contrato en el que Guess::new se basa. Las condiciones en las que
Guess::new podría entrar en pánico deben discutirse en la documentación de la API de cara
al público; cubriremos las convenciones de documentación que indican la posibilidad de un
panic! en la documentación de la API que creas en el Capítulo 14. Si value pasa la prueba,
creamos un nuevo Guess con su campo value establecido en el value y devolvemos el
Guess .
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 un Guess en lugar de un i32 y no
https://book.rustlang-es.org/print 226/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Resumen
Las características de manejo de errores de Rust están diseñadas para ayudarte a escribir
un código más robusto. La macro panic! indica que tu programa está en un estado que no
puede manejar y te permite indicarle al proceso que se detenga en lugar de intentar
continuar con valores no válidos o incorrectos. El enum Result usa el sistema de tipos de
Rust para indicar que las operaciones pueden fallar de una manera que tu código podría
recuperar. Puedes usar Result para decirle al código que llama a tu código que necesita
manejar el éxito o el error de manera potencial. Usar panic! y Result en las situaciones
apropiadas hará que tu código sea más confiable ante los problemas inevitables.
Ahora que has visto formas útiles en que la biblioteca estándar usa generics con los enums
Option y Result , hablaremos sobre cómo funcionan los generics y cómo puedes usarlos
en tu código.
https://book.rustlang-es.org/print 227/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Las funciones pueden tomar parámetros de algún tipo genérico, en lugar de un tipo
concreto como i32 o String , de la misma manera que una función toma parámetros con
valores desconocidos para ejecutar el mismo código en múltiples valores concretos. De
hecho, ya hemos usado genéricos en el Capítulo 6 con Option<T> , Capítulo 8 con Vec<T> y
HashMap<K, V> , y Capítulo 9 con Result<T, E> . ¡En este capítulo, explorará cómo definir
sus propios tipos, funciones y métodos con genéricos!
Primero, revisaremos cómo extraer una función para reducir la duplicación de código.
Luego usaremos la misma técnica para hacer una función genérico a partir de dos funciones
que difieren solo en los tipos de sus parámetros. También explicaremos cómo usar tipos
genéricos en definiciones de structs y enums.
Entonces aprenderás cómo usar traits para definir el comportamiento de una manera
genérica. Puedes combinar traits con tipos genéricos para restringir un tipo genérico para
que acepte solo aquellos tipos que tienen un comportamiento particular, en lugar de
cualquier tipo.
Comenzamos con un corto programa en el listado 10-1 que encuentra el número más
grande en una lista.
https://book.rustlang-es.org/print 228/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/main.rs
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
Ahora se nos ha encargado encontrar el número más grande en dos listas de números. Para
hacerlo, podemos duplicar el código en el listado 10-1 y usar la misma lógica en dos lugares
diferentes en el programa, como se muestra en el listado 10-2.
Filename: src/main.rs
https://book.rustlang-es.org/print 229/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
Listado 10-2: Código para encontrar el mayor número en dos listas de números
Aunque este código funciona, duplicar el código es tedioso y propenso a errores. También
tenemos que recordar actualizar el código en varios lugares cuando queremos cambiarlo.
Para eliminar esta duplicación, crearemos una abstracción definiendo una función que
opera en cualquier lista de enteros que se pase en un parámetro. Esta solución hace que
nuestro código sea más claro y nos permite expresar el concepto de encontrar el número
más grande en una lista de forma abstracta.
En el listado 10-3, extraemos el código que encuentra el mayor número en una función
llamada largest . Luego llamamos a la función para encontrar el mayor número en las dos
listas del listado 10-2. También podríamos usar la función en cualquier otra lista de valores
i32 que podríamos tener en el futuro.
Filename: src/main.rs
https://book.rustlang-es.org/print 230/656
12/7/24, 22:37 El Lenguaje de Programación Rust
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
Listado 10-3: Código abstracto para encontrar el número mayor en dos listas
La función largest tiene un parámetro llamado list , que representa cualquier slice de
valores ì32 que podríamos pasar a la función. Como resultado, cuando llamamos a la
función, el código se ejecuta en los valores específicos que pasamos.
En resumen, estos son los pasos que tomamos para cambiar el código del listado 10-2 al
listado 10-3:
A continuación, usaremos estos mismos pasos con los genéricos para reducir la duplicación
de código. De la misma manera que el cuerpo de la función puede operar en una list
abstracta en lugar de valores específicos, los genéricos permiten que el código opere en
tipos abstractos.
Por ejemplo, digamos que teníamos dos funciones: una que encuentra el mayor elemento
en un slices de valores i32 y otra que encuentra el mayor elemento en un slice de valores
char . ¿Cómo eliminaríamos esa duplicación? ¡Averigüémoslo!
https://book.rustlang-es.org/print 231/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Definiciones in function
Al definir una función que usa genéricos, colocamos los genéricos en la firma de la función
donde normalmente especificaríamos los tipos de datos de los parámetros y el valor de
retorno. Hacerlo hace que nuestro código sea más flexible y brinda más funcionalidad a los
llamadores de nuestra función al tiempo que evita la duplicación de código.
Continuando con nuestra función largest , el listado 10-4 muestra dos funciones que
encuentran el valor más grande en un slice. Luego combinaremos estos en una sola función
que usa genéricos.
Filename: src/main.rs
https://book.rustlang-es.org/print 232/656
12/7/24, 22:37 El Lenguaje de Programación Rust
largest
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
Listado 10-4: Dos funciones que difieren solo en sus nombres y los tipos en sus firmas
La función largest_i32 es la que extrajimos en el listado 10-3 que encuentra el i32 más
grande en un slice. La función largest_char encuentra el char más grande en un slice. Los
cuerpos de las funciones tienen el mismo código, así que eliminemos la duplicación
introduciendo un parámetro de tipo generic en una sola función.
Para parametrizar los tipos en una nueva función única, necesitamos nombrar el parámetro
de tipo, tal como lo hacemos para los parámetros de valor de una función. Pero usaremos
T porque, por convención, los nombres de los parámetros de tipo en Rust son cortos, a
menudo solo una letra, y la convención de nomenclatura de tipo de Rust es
UpperCamelCase. Abreviatura de "tipo", T es la opción predeterminada de la mayoría de
los programadores de Rust.
función, tenemos que declarar el nombre del parámetro de tipo antes de usarlo. Para
definir la función genérico largest , coloque las declaraciones de nombre de tipo dentro de
corchetes angulares, <> , entre el nombre de la función y la lista de parámetros, así:
Leemos esta definición como: la función largest es genérico sobre algún tipo T . Esta
función tiene un parámetro llamado list , que es un slice de valores de tipo T . La función
largest devolverá una referencia a un valor del mismo tipo T .
Filename: src/main.rs
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
Listing 10-5: La función largest está usando parámetros de tipo genérico; esto aún no se compila
https://book.rustlang-es.org/print 234/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> 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 In Struct
También podemos definir structs para usar tipos genéricos en uno o más campos usando la
sintaxis <> . El listado 10-6 define un struct Point<T> para contener valores x e y de
cualquier tipo T .
Filename: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
https://book.rustlang-es.org/print 235/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La sintaxis para usar genéricos en las definiciones de structs es similar a la que se usa en las
definiciones de funciones. Primero, declaramos el nombre del parámetro de tipo dentro de
corchetes angulares, justo después del nombre del struct. Luego, usamos el tipo genérico
en la definición del struct donde especificaríamos tipos de datos concretos.
Ten en cuenta que porque hemos usado un solo tipo genérico para definir Point<T> , esta
definición dice que el struct Point<T> es genérico sobre algún tipo T , y los campos x e y
son ambos ese mismo tipo, sea cual sea ese tipo. Si creamos una instancia de un Point<T>
que tenga valores de diferentes tipos, como en el listado 10-7, nuestro código no se
compilará.
Filename: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
Listing 10-7: Los campos x e y deben ser del mismo tipo porque ambos tienen el mismo tipo de dato 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 un struct Point donde x e y son ambos genéricos pero podrían tener
diferentes tipos, podemos usar múltiples parámetros de tipo genérico. Por ejemplo, en el
listado 10-8, cambiamos la definición de Point para que sea generic sobre los tipos T y U
donde x es de tipo T y y es de tipo U .
Filename: src/main.rs
https://book.rustlang-es.org/print 236/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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 para que x e y puedan ser valores de tipos diferentes
¡Ahora todas las instancias de Point que se muestran se permiten! Puede usar tantos
parámetros de tipo genérico en una definición como desee, pero usar más de unos pocos
hace que su código sea difícil de leer. Si encuentra que necesita muchos tipos genérico en
su código, podría indicar que su código necesita reestructurarse en piezas más pequeñas.
Definiciones In Enum
Como hicimos con structs, podemos definir enums para contener tipos genérico en sus
variantes. Echemos otro vistazo al enum Option<T> que la biblioteca estándar proporciona,
que usamos en el Capítulo 6:
enum Option<T> {
Some(T),
None,
}
Esta definición debería tener más sentido para ti ahora. Como puede ver, el enum
Option<T> es genérico sobre el tipo T y tiene dos variantes: Some , que contiene un valor
de tipo T , y None , que no contiene ningún valor. Al usar el enum Option<T> , podemos
expresar el concepto abstracto de un valor opcional, y porque Option<T> es genérico,
podemos usar esta abstracción sin importar el tipo del valor opcional.
Los enums también pueden usar múltiples tipos genérico. La definición del enum Result
que usamos en el Capítulo 9 es un ejemplo:
El enum Result es un genérico en dos tipos, T y E . Tiene dos variantes: Ok , que contiene
un valor de tipo T , y Err , que contiene un valor de tipo E . Esta definición es apropiada
porque el significado de Result es que uno de estos dos tipos, T o E , será el tipo del valor
que se devuelve cuando se produce un error o cuando se tiene éxito (devolviendo un valor
https://book.rustlang-es.org/print 237/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cuando reconoces situaciones en tu código con múltiples definiciones de struct o enum que
difieren solo en los tipos de los valores que contienen, puedes evitar la duplicación usando
tipos genérico en su lugar.
Definiciones In Method
Filename: src/main.rs
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
Listado 10-9: Implementando un método llamado x en el struct Point<T> que devolverá una referencia al
campo x de tipo T
Aquí, hemos definido un método llamado x en Point<T> que devuelve una referencia a la
data en el campo x .
Ten en cuenta que tenemos que declarar T justo después de impl para que podamos usar
T para especificar que estamos implementando métodos en el tipo Point<T> . Al declarar
T como un tipo genérico después de impl , Rust puede identificar que el tipo en los
corchetes angulares en Point es un tipo generic en lugar de un tipo concreto. Podríamos
haber elegido un nombre diferente para este parámetro genérico que el parámetro
genérico declarado en la definición del struct, pero usar el mismo nombre es convencional.
Los métodos escritos dentro de un impl que declara el tipo genérico se definirán en
https://book.rustlang-es.org/print 238/656
12/7/24, 22:37 El Lenguaje de Programación Rust
cualquier instancia del tipo, sin importar qué tipo concreto termine sustituyendo al tipo
genérico.
Filename: src/main.rs
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Listado 10-10: Un bloque impl que solo aplica a un struct con un tipo concreto particular para el parámetro del
tipo genérico T
Los parámetros de tipo genérico en una definición de struct no siempre son los mismos que
los que usas en las firmas de métodos de ese mismo struct. El listado 10-11 usa los tipos
genérico X1 e Y1 para el struct Point y X2 Y2 para la firma del método mixup para
hacer el ejemplo más claro. El método crea una nueva instancia de Point con el valor x del
self Point (de tipo X1 ) y el valor y del Point pasado (de tipo Y2 ).
Filename: src/main.rs
https://book.rustlang-es.org/print 239/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
Listado 10-11: Un método que usa diferentes tipos genérico de la definición de su struct
En main , hemos definido un Point que tiene un i32 para x (con valor 5 ) y un f64 para
y (con valor 10.4 ). La variable p2 es un Point struct que tiene un string slice para x (con
valor "Hello" ) y un char para y (con valor c ). Llamar a mixup en p1 con el argumento
p2 nos da p3 , que tendrá un i32 para x , porque x vino de p1 . La variable p3 tendrá un
char para y , porque y vino de p2 . La llamada al macro println! imprimirá p3.x = 5,
p3.y = c .
Rust logra esto realizando monomorfización del código usando genéricos en tiempo de
compilación. Monomorfización es el proceso de convertir código genérico en código
específico llenando los tipos concretos que se usan cuando se compila. En este proceso, el
compilador hace lo contrario de los pasos que usamos para crear la función genérica en el
https://book.rustlang-es.org/print 240/656
12/7/24, 22:37 El Lenguaje de Programación Rust
listado 10-5: el compilador mira todos los lugares donde se llama el código genérico y
genera código para los tipos concretos con los que se llama el código genérico.
Veamos como funciona esto usando el enum genérico de la biblioteca estándar Option<T> :
Cuando Rust compila este código, realiza monomorfización. Durante ese proceso, el
compilador lee los valores que se han usado en las instancias de Option<T> e identifica dos
tipos de Option<T> : uno es i32 y el otro es f64 . Como tal, expande la definición genérica
de Option<T> en dos definiciones especializadas a i32 y f64 , reemplazando así la
definición genérica con las específicas.
La versión monomorfizada del código se ve similar al siguiente (el compilador usa nombres
diferentes a los que estamos usando aquí para ilustración):
Filename: src/main.rs
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
https://book.rustlang-es.org/print 241/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Nota: Los traits son similares a una característica a menudo llamada interfaces en otros
lenguajes, aunque con algunas diferencias. En español también se les conoce como
rasgos pero en el libro intentaremos mantener la palabra clave sin traducir, no
obstante creamos esta encuesta para futuras revisiones.
Definiendo un Trait
El comportamiento de un tipo consiste en los métodos que podemos llamar en ese tipo.
Diferentes tipos comparten el mismo comportamiento si podemos llamar los mismos
métodos en todos esos tipos. Las definiciones de traits son una manera de agrupar firmas
de métodos para definir un conjunto de comportamientos necesarios para lograr algún
propósito.
Por ejemplo, digamos que tenemos múltiples structs que contienen varios tipos y
cantidades de texto: un struct NewsArticle que contiene una historia de noticias archivada
en una ubicación particular y un Tweet que puede tener como máximo 280 caracteres
junto con metadatos que indican si es un nuevo tweet, un retweet, o una respuesta a otro
tweet.
Queremos hacer una biblioteca de agregación de medios llamada aggregator que puede
mostrar resúmenes de datos que podrían estar almacenados en una instancia de
NewsArticle o Tweet . Para hacer esto, necesitamos un resumen de cada tipo, y
solicitaremos ese resumen llamando un método summarize en una instancia. El listado 10-
12 muestra la definición de un trait Summary público que expresa este comportamiento.
Filename: src/lib.rs
Listado 10-12: Un trait Summary que consiste en el comportamiento proporcionado por un método summarize
https://book.rustlang-es.org/print 242/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Aquí, declaramos un trait usando la palabra clave trait y luego el nombre del trait, que en
este caso es Summary . También hemos declarado el trait como pub para que los crates que
dependen de este crate puedan hacer uso de este trait también, como veremos en algunos
ejemplos. Dentro de las llaves curvas, declaramos las firmas de los métodos que describen
los comportamientos de los tipos que implementan este trait, que en este caso es fn
summarize (&self) -> String .
Un trait 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 un punto y coma.
Ahora que hemos definido el trait Summary , podemos implementarlo en los tipos en
nuestro agregador de medios. El listado 10-13 muestra una implementación del trait
Summary en el struct NewsArticle que usa el encabezado, el autor y la ubicación para crear
el valor de retorno de summarize . Para el struct Tweet , definimos summarize como el
nombre de usuario seguido del texto completo del tweet, asumiendo que el contenido del
tweet ya está limitado a 280 caracteres.
Filename: src/lib.rs
https://book.rustlang-es.org/print 243/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Listado 10-13: Implementación del trait Summary en los tipos NewsArticle y Tweet
https://book.rustlang-es.org/print 244/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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,
};
Otros crates que dependen de nuestro crate aggregator pueden usar el trait Summary en
el ámbito para implementar Summary en sus propios tipos. Una restricción a tener en
cuenta es que podemos implementar un trait en un tipo solo si al menos uno de los trait o
el tipo es local a nuestro crate. Por ejemplo, podemos implementar traits de la biblioteca
estándar como Display en un tipo personalizado como Tweet como parte de nuestra
funcionalidad de crate aggregator , porque el tipo Tweet es local a nuestro crate
aggregator . También podemos implementar Summary en Vec<T> en nuestro crate
aggregator , porque el trait Summary es local a nuestro crate aggregator .
Pero no podemos implementar traits externos en tipos externos. Por ejemplo, digamos que
queremos implementar Display en Vec<T> como parte de nuestra funcionalidad de crate
aggregator . Esto no es posible porque tanto Display como Vec<T> están definidos en la
biblioteca estándar y no son locales a nuestro crate aggregator . La restricción de
implementar un trait en un tipo solo si uno de ellos es local a nuestro crate es parte de una
propiedad llamada coherencia, y más específicamente la regla huérfana, así llamada porque
el tipo padre no está presente. Esta regla asegura que el código de otras personas no pueda
romper su código y viceversa. Sin la regla, dos crates podrían implementar el mismo trait
para el mismo tipo, y Rust no sabría qué implementación usar.
Implementaciones predeterminadas
A veces es útil tener un comportamiento predeterminado para algunos o todos los métodos
en un trait en lugar de requerir implementaciones para todos los métodos en cada tipo.
Luego, a medida que implementamos el trait en un tipo particular, podemos mantener o
anular el comportamiento predeterminado para cada método.
https://book.rustlang-es.org/print 245/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
Listado 10-14: Definición de un trait Summary con un valor predeterminado implementado del método
summarize
https://book.rustlang-es.org/print 246/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para usar esta version de Summary , solo necesitamos definir summarize_author cuando
implementamos el trait en un tipo:
Ahora que sabes cómo definir y implementar traits, podemos explorar cómo usar traits para
definir funciones que aceptan muchos tipos diferentes. Usaremos el trait Summary que
implementamos en los tipos NewsArticle y Tweet en el listado 10-13 para definir una
función notify que llama al método summarize en su parámetro item , que es de algún
tipo que implementa el trait Summary . Para hacer esto, usamos la sintaxis impl Trait ,
como esto:
https://book.rustlang-es.org/print 247/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La sintaxis impl Trait funciona para casos sencillos, pero en realidad es azúcar sintáctico
para una forma más larga conocida como trait bound; se ve así:
Esta forma más larga es equivalente al ejemplo en la sección anterior pero más detallado.
Colocamos los límites de los traits con la declaración del parámetro generic después de dos
puntos y dentro de corchetes angulares.
La sintaxis impl Trait es conveniente y hace que el código sea más conciso en casos
simples, mientras que la sintaxis de trait bound más completa puede expresar más
complejidad en otros casos. Por ejemplo, podemos tener dos parámetros que implementan
Summary . Hacerlo con la sintaxis impl Trait se ve así:
Usando impl Trait es apropiado si queremos que esta función permita que item1 y
item2 tengan tipos diferentes (siempre que ambos tipos implementen Summary ). Sin
embargo, si queremos forzar que ambos parámetros tengan el mismo tipo, debemos usar
un trait bound, como esto:
El tipo generic T especificado como el tipo de los parámetros item1 e item2 restringe la
función de tal manera que el tipo concreto del valor pasado como argumento para item1 e
item2 debe ser el mismo.
https://book.rustlang-es.org/print 248/656
12/7/24, 22:37 El Lenguaje de Programación Rust
También podemos especificar más de un trait bound. Digamos que queremos que notify
use la representación de cadena de un tipo que implementa Summary en el cuerpo de la
función. Para hacer esto, necesitamos que el parámetro item implemente tanto Display
como Summary . Podemos hacerlo usando la sintaxis + :
Con los dos trait bounds especificados, el cuerpo de notify puede llamar a summarize y
usar {} para formatear item .
Usar demasiados trait bounds tiene sus inconvenientes. Cada generic tiene sus propios trait
bounds, por lo que las funciones con múltiples parámetros de tipo generic pueden contener
mucha información de trait bound entre el nombre de la función y su lista de parámetros, lo
que hace que la firma de la función sea difícil de leer. Por esta razón, Rust tiene una sintaxis
alternativa para especificar los trait bounds dentro de una cláusula where después de la
firma de la función. Así que en lugar de escribir esto:
También podemos usar la sintaxis impl Trait en el tipo de retorno de una función para
devolver un valor de algún tipo que implementa un trait, como se muestra aquí:
https://book.rustlang-es.org/print 249/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Sin embargo, no puedes usar impl Trait si la función devuelve más de un tipo. Por
ejemplo, este código que devuelve un NewsArticle o un Tweet con el tipo de retorno
especificado como impl Summary no compilaría:
https://book.rustlang-es.org/print 250/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Al usar un trait bound con un bloque impl que usa parámetros de tipo generic, podemos
implementar métodos condicionalmente para tipos que implementan los traits
especificados. Por ejemplo, el tipo Pair<T> en el listado 10-15 siempre implementa la
función new para devolver una nueva instancia de Pair<T> (recuerda de la sección
“Definiendo métodos” del Capítulo 5 que Self es un alias de tipo para el tipo del bloque
impl , que en este caso es Pair<T> ). Pero en el siguiente bloque impl , Pair<T> solo
implementa el método cmp_display si su tipo interno T implementa el trait PartialOrd
que permite la comparación y el trait Display que permite la impresión.
Filename: src/lib.rs
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
Listado 10-15: Implementación condicional de métodos en un tipo generic dependiendo de los trait bounds
Debido a que la biblioteca estándar tiene esta implementación, podemos llamar al método
to_string definido por el trait ToString en cualquier tipo que implemente el trait
Display . Por ejemplo, podemos convertir enteros en sus valores String correspondientes
de esta manera porque los enteros implementan Display :
let s = 3.to_string();
Traits y trait bounds nos permiten usar genéricos para reducir la duplicación de código, pero
también para especificar a el compilador que queremos que un tipo generic tenga un
comportamiento particular. El compilador puede usar la información de los trait bounds
para verificar que todos los tipos concretos que usamos con nuestro código proporcionan el
comportamiento correcto. En lenguajes de tipado dinámico, obtendríamos un error en
tiempo de ejecución si llamamos a un método en un tipo que no define el método. Pero
Rust mueve estos errores al tiempo de compilación, por lo que estamos obligados a corregir
los problemas 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 porque ya hemos
verificado en tiempo de compilación. Hacerlo mejora el rendimiento sin tener que renunciar
a la flexibilidad de los generics.
https://book.rustlang-es.org/print 252/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El objetivo principal de los lifetimes es prevenir referencias colgantes, que hacen que un
programa haga referencia a datos que no son los que se pretende referenciar. Considere el
programa en el listado 10-16, que tiene un scope externo y un scope interno.
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
Listado 10-16: Un intento de usar una referencia cuyo valor ha quedado fuera del scope
Nota: Los ejemplos en los Listados 10-16, 10-17 y 10-23 declaran variables sin darles
un valor inicial, por lo que el nombre de la variable existe en el scope externo. A
primera vista, esto podría parecer estar en conflicto con el hecho de que Rust no tiene
valores nulos. Sin embargo, si intentamos usar una variable antes de darle un valor,
https://book.rustlang-es.org/print 253/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El scope externo declara una variable llamada r sin valor inicial, y el scope interno declara
una variable llamada x con el valor inicial de 5. Dentro del scope interno, intentamos
establecer el valor de r como una referencia a x . Luego, el scope interno termina, e
intentamos imprimir el valor en r . Este código no se compilará porque el valor al que se
refiere r ha quedado fuera del scope antes de que intentemos usarlo. Aquí está el mensaje
de error:
$ 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
La variable x no “vive lo suficiente”. La razón es que x estará fuera del scope cuando el
scope interno termine en la línea 7. Pero r todavía es válido para el scope externo; porque
su scope es más grande, decimos que “vive más tiempo”. Si Rust permitiera que este código
funcionara, r estaría referenciando memoria que se desasignó cuando x quedó fuera del
scope, y cualquier cosa que intentemos hacer con r no funcionaría correctamente. ¿Cómo
determina Rust que este código es inválido? Utiliza el borrow checker.
El Borrow Checker
El compilador de Rust tiene un borrow checker que compara scopes para determinar si todos
los borrows son válidos. El listado 10-17 muestra el mismo código que el listado 10-16, pero
con anotaciones que muestran los lifetimes de las variables.
https://book.rustlang-es.org/print 254/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
Aquí, hemos anotado el lifetime de r con 'a y el lifetime de x con 'b . Como puede ver, el
bloque interno 'b es mucho más pequeño que el bloque externo 'a . En tiempo de
compilación, Rust compara el tamaño de los dos lifetimes y ve que r tiene un lifetime de
'a pero que se refiere a la memoria con un lifetime de 'b . El programa es rechazado
porque 'b es más corto que 'a : el sujeto de la referencia no vive tanto como la referencia.
El listado 10-18 corrige el código para que no tenga una referencia pendiente 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 un lifetime más largo que la referencia
Aquí, x tiene el lifetime 'b que en este caso es más grande que 'a . Esto significa que r
puede hacer referencia a x porque Rust sabe que la referencia en r siempre será válida
mientras x sea válida.
Ahora que sabemos dónde están los lifetimes de las referencias y cómo Rust analiza los
lifetimes para garantizar que las referencias siempre sean válidas, exploraremos los
lifetimes genéricos de los parámetros y valores de retorno en el contexto de las funciones.
Escribiremos una función que devuelva el más largo de dos string slices. Esta función tomará
dos string slices y devolverá un solo string slice. Después de haber implementado la función
longest , el código en el listado 10-19 debería imprimir The longest string is abcd .
Filename: src/main.rs
https://book.rustlang-es.org/print 255/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
Listado 10-19: Una función main que llama a la función longest para encontrar el más largo de dos string
slices
Ten en cuenta que queremos que la función tome string slices, que son referencias, en lugar
de strings, porque no queremos que la función longest tome posesión de sus parámetros.
Consulta la sección “String Slices as Parameters” 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
queremos.
Filename: src/main.rs
Listado 10-20: Una implementación de la función longest que devuelve el más largo de dos string slices pero
aún no compila
https://book.rustlang-es.org/print 256/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
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 3 previous errors
El texto de ayuda revela que el tipo de retorno necesita un parámetro de lifetime generic en
él porque Rust no puede decir si la referencia que se devuelve se refiere a x o y . De hecho,
nosotros tampoco lo sabemos, porque el bloque if en el cuerpo de esta función devuelve
una referencia a x y el bloque else devuelve una referencia a y !
Cuando estamos definiendo esta función, no sabemos los valores concretos que se pasarán
a esta función, por lo que no sabemos si se ejecutará el caso if o el caso else . Tampoco
conocemos los lifetimes concretos de las referencias que se pasarán, por lo que no
podemos mirar los scopes como lo hicimos en los Listados 10-17 y 10-18 para determinar si
la referencia que devolvemos siempre será válida. El borrow checker tampoco puede
determinar esto, porque no sabe cómo se relacionan los lifetimes de x e y con el lifetime
del valor de retorno. Para corregir este error, agregaremos parámetros de lifetime generics
que definan la relación entre las referencias para que el borrow checker pueda realizar su
análisis.
https://book.rustlang-es.org/print 257/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Las anotaciones de los lifetimes no cambian cuánto tiempo viven las referencias. En cambio,
describen las relaciones de los lifetimes de múltiples referencias entre sí sin afectar los
lifetimes. Al igual que las funciones pueden aceptar cualquier tipo cuando la firma especifica
un parámetro de tipo genérico, las funciones pueden aceptar referencias con cualquier
lifetime especificando un parámetro de lifetime generic.
Las anotaciones de los lifetimes tienen una sintaxis ligeramente inusual: los nombres de los
parámetros de los lifetimes deben comenzar con un apóstrofe ( ' ) y generalmente son
todos en minúsculas y muy cortos, como los tipos generics. La mayoría de la gente usa el
nombre 'a para la primera anotación de lifetime. Colocamos las anotaciones de los
parámetros de los lifetimes después del & de una referencia, usando un espacio para
separar la anotación del tipo de referencia.
Estos son algunos ejemplos: una referencia a un i32 sin un parámetro de lifetime, una
referencia a un i32 que tiene un parámetro de lifetime llamado 'a , y una referencia
mutable a un i32 que también tiene el lifetime 'a .
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
Una anotación de lifetime en si misma no tiene mucho significado, porque las anotaciones
están destinadas a decirle a Rust cómo los parámetros de lifetime generics de múltiples
referencias se relacionan entre sí. Examinemos cómo las anotaciones de los lifetimes se
relacionan entre sí en el contexto de la función longest .
Para usar anotaciones de los lifetimes en las firmas de las funciones, primero necesitamos
declarar los parámetros de los lifetimes generic dentro de los corchetes angulares entre el
nombre de la función y la lista de parámetros, como lo hicimos con los parámetros de tipo
generic.
Queremos que la firma exprese la siguiente restricción: la referencia devuelta será válida
siempre que ambos parámetros sean válidos. Esta es la relación entre los lifetimes de los
parámetros y el valor de retorno. Nombraremos al lifetime 'a y luego lo agregaremos a
cada referencia, como se muestra en el listado 10-21.
Filename: src/main.rs
https://book.rustlang-es.org/print 258/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Listado 10-21: La definición de la función longest que especifica que todas las referencias en la firma deben
tener el mismo lifetime 'a
Este código debe compilar y producir el resultado que queremos cuando lo usamos con la
función main en el listado 10-19.
La firma de la función ahora le dice a Rust que durante el lifetime 'a , la función toma dos
parámetros, ambos los cuales son string slices que viven al menos tanto como el lifetime
'a . La firma de la función también le dice a Rust que el string slice devuelto también vivirá
al menos tanto como el lifetime 'a . En la práctica, significa que el lifetime de la referencia
devuelta por la función longest es el mismo que el más pequeño de los lifetimes de los
valores a los que se refieren los argumentos de la función. Estas relaciones son lo que
queremos que Rust use al analizar este código.
Recuerda, cuando especificamos los parámetros de los lifetimes en la firma de esta función,
no estamos cambiando los lifetimes de ninguna de las referencias que se pasan en o se
devuelven. En cambio, estamos especificando que el borrow checker debería rechazar
cualquier valor que no cumpla con estas restricciones. Ten en cuenta que la función
longest no necesita saber exactamente cuánto tiempo vivirán x e y , solo que algún
scope puede sustituirse por 'a que satisfará esta firma.
https://book.rustlang-es.org/print 259/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Veamos cómo las anotaciones de los lifetimes restringen la función longest pasando
referencias que tienen diferentes lifetimes concretos. El listado 10-22 es un ejemplo sencillo.
Filename: src/main.rs
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {result}");
}
}
Listado 10-22: Usando la función longest con referencias a valores String que tienen diferentes lifetimes
concretos
En este ejemplo, string1 es válida hasta el final del scope externo, string2 es válida hasta
el final del scope interno, y result referencia algo que es válido hasta el final del scope
interno. Ejecuta este código, y verás que el borrow checker lo aprueba; se compilará e
imprimirá The longest string is long string is long .
Filename: src/main.rs
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: Intentando utilizar result después de que string2 haya quedado fuera del scope
https://book.rustlang-es.org/print 260/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
El error muestra que para que result sea válido para la instrucción println! , string2
tendría que ser válido hasta el final del scope externo. Rust sabe esto porque anotamos los
lifetimes de los parámetros de la función y los valores de retorno usando el mismo
parámetro de lifetime 'a .
Como humanos, podemos mirar este código y ver que string1 es más larga que string2
y por lo tanto result contendrá una referencia a string1 . Debido a que string1 aún no
ha quedado fuera del scope, una referencia a string1 todavía será válida para la
instrucción println! . Sin embargo, el compilador no puede ver que la referencia sea válida
en este caso. Le hemos dicho a Rust que el lifetime de la referencia devuelta por la función
longest es el mismo que el más pequeño de los lifetimes de las referencias pasadas. Por lo
tanto, el borrow checker rechaza el código del listado 10-23 como posiblemente conteniendo
una referencia no válida.
Intenta diseñar más experimentos que varíen los valores y los lifetimes de las referencias
que se pasan a la función longest y cómo se usa la referencia devuelta. Haz hipótesis
sobre si tus experimentos pasarán el borrow checker antes de compilar; luego comprueba si
tienes razón!
La forma en que necesitas especificar los parámetros de los lifetimes depende de lo que tu
función esté haciendo. Por ejemplo, si cambiamos la implementación de la función longest
para que siempre devuelva el primer parámetro en lugar de la referencia a la cadena más
larga, no necesitaríamos especificar un lifetime en el parámetro y . El siguiente código
compilará:
Filename: src/main.rs
https://book.rustlang-es.org/print 261/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cuando se devuelve una referencia desde una función, el parámetro del lifetime para el tipo
de retorno debe coincidir con el parámetro del lifetime 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 esa función. Sin embargo, esto sería una referencia colgante porque el valor
quedará fuera del scope al final de la función. Considera esta implementación intentada de
la función longest que no se compilará:
Filename: src/main.rs
Aquí, aunque hemos especificado un parámetro de lifetime 'a para el tipo de retorno, esta
implementación no se compilará porque el lifetime del valor retornado no está relacionado
en absoluto con el lifetime de los parámetros. Este es el mensaje de error que obtenemos:
$ 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()
| ------^^^^^^^^^
| |
| 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
El problema es que result sale del scope y se limpia al final de la función longest .
También estamos tratando de devolver una referencia a result desde la función. No hay
forma de especificar parámetros de lifetime que cambien la referencia colgante, y Rust no
nos permitirá crear una referencia colgante. En este caso, la mejor solución sería devolver
un tipo de dato propiedad en lugar de una referencia para que la función que llama sea
responsable de limpiar el valor.
Hasta ahora, los structs que hemos definido contienen tipos de ownership. Podemos definir
structs que contengan referencias, pero en ese caso necesitamos agregar una anotación de
lifetime en cada referencia en la definición del struct. El listado 10-24 tiene un struct llamado
ImportantExcerpt que contiene una string slice.
Filename: src/main.rs
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a
'.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
Listado 10-24: Un struct que contiene una referencia, lo que requiere una annotation de lifetime
Este struct tiene el campo part que contiene un string slice, que es una referencia. Como
con los tipos de datos generics, declaramos el nombre del parámetro de lifetime genérico
dentro de corchetes angulares después del nombre del struct para que podamos usar el
parámetro de lifetime en el cuerpo de la definición del struct. Esta anotación significa que
una instancia de ImportantExcerpt no puede sobrevivir más allá de la referencia que
contiene en su campo part .
La función main aquí crea una instancia del struct ImportantExcerpt que contiene una
referencia a la primera oración de la variable novel . La data en novel existe antes de que
se cree la instancia de ImportantExcerpt . Además, novel no sale del scope hasta después
de que la instancia de ImportantExcerpt sale del scope, por lo que la referencia en la
instancia de ImportantExcerpt es válida.
Omisión de lifetime
Has aprendido que cada referencia tiene un lifetime y que debes especificar parámetros de
lifetime para las funciones o structs que usan referencias. Sin embargo, en el Capítulo 4,
https://book.rustlang-es.org/print 263/656
12/7/24, 22:37 El Lenguaje de Programación Rust
tuvimos una función en el listado 4-9, que se muestra nuevamente en el listado 10-25, que
se compiló sin anotaciones de lifetime.
Filename: src/lib.rs
&s[..]
}
Listado 10-25: Una función que definimos en el listado 4-9 que compiló sin anotaciones de lifetime, a pesar de
que el parámetro y el tipo de retorno son referencias
La razón por la que esta función se compila sin anotaciones de lifetime es histórica: en las
primeras versiones (pre-1.0) de Rust, este código no se compilaría porque cada referencia
necesitaba un lifetime explícito. En ese momento, la firma de la función se habría escrito así:
Después de escribir mucho código en Rust, el equipo de Rust descubrió que los
programadores de Rust estaban ingresando las mismas anotaciones de lifetime una y otra
vez en situaciones particulares. Estas situaciones eran predecibles y seguían algunos
patrones deterministas. Los desarrolladores programaron estos patrones en el código del
compilador para que el borrow checker pudiera inferir los lifetimes en estas situaciones y no
necesitara anotaciones explícitas.
https://book.rustlang-es.org/print 264/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los lifetime en los parámetros de una función o método se llaman lifetime de entrada y los
lifetime en los valores de retorno se llaman output lifetimes.
El compilador usa tres reglas para determinar los lifetime de las referencias cuando no hay
anotaciones explícitas. La primera regla se aplica a los lifetime de entrada, y la segunda y
tercera regla se aplican a los output lifetimes. Si el compilador llega al final de las tres reglas y
aún hay referencias para las que no puede determinar los lifetime, el compilador mostrará
un error. Estas reglas se aplican tanto a las definiciones de fn como a los bloques impl .
La segunda regla es que, si hay un parámetro de input de lifetime, ese lifetime se asigna a
todos los parámetros de output de lifetime: fn foo<'a>(x: &'a i32) -> &'a i32 .
La tercera regla es que si hay múltiples parámetros de input de lifetime, pero uno de ellos es
&self o &mut self porque este es un método, el lifetime de self se asigna a todos los
parámetros de output de lifetime. Esto hace que los métodos sean mucho más agradables
de leer y escribir porque se necesitan menos símbolos.
Imaginemos que somos el compilador. Aplicaremos estas reglas para descubrir los lifetime
de las referencias en la firma de la función first_word en el listado 10-25. La firma
comienza sin ningún lifetime asociado con las referencias:
Luego el compilador aplica la primera regla, que especifica que cada parámetro tiene su
propio lifetime. Como de costumbre, llamaremos a este lifetime 'a , por lo que ahora la
firma es la siguiente:
La segunda regla aplica porque hay exactamente un parámetro de input con lifetime. Este
segundo conjunto establece que el lifetime del único parámetro de input se asigna a todos
los parámetros de output, por lo que la firma de la función se convierte en la siguiente:
Ahora todas las referencias en esta firma de función tienen lifetime, y el compilador puede
continuar su análisis sin necesidad de que el programador anote los lifetime en esta firma
de función.
https://book.rustlang-es.org/print 265/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Veamos otro ejemplo, esta vez usando la función longest que no tenía parámetros de
lifetime cuando comenzamos a trabajar con ella en el listado 10-20:
Aplicamos la primera regla: cada parámetro obtiene su propio lifetime. Esta vez tenemos
dos parámetros en lugar de uno, por lo que tenemos dos lifetimes:
Podemos ver que la segunda regla no se aplica porque hay más de un input lifetime. La
tercera regla tampoco se aplica porque longest es una función en lugar de un método, por
lo que no hay un parámetro de self . Después de trabajar a través de las tres reglas,
todavía no hemos descubierto cuál es el lifetime de retorno. Es por eso que obtuvimos un
error al intentar compilar el código en el listado 10-20: el compilador trabajó a través de las
reglas de omisión de lifetime, pero aún no pudo descubrir todos los lifetime de las
referencias en la firma.
Dado que la tercera regla solo se aplica realmente en las firmas de los métodos, veremos
los lifetime en ese contexto a continuación para ver por qué la tercera regla significa que no
tenemos que anotar los lifetime en las firmas de los métodos con mucha frecuencia.
Cuando implementamos métodos en un struct con lifetimes, usamos la misma sintaxis que
la de los parámetros de tipo generic que se muestra en el listado 10-11. Donde declaramos
y usamos los parámetros de lifetime depende de si están relacionados con los campos del
struct o con los parámetros y valores de retorno del método.
Los nombres de lifetime para los campos de una estructura siempre deben declararse
después de la palabra clave impl y luego usarse después del nombre del struct, porque
esos lifetime son parte del tipo del struct.
En las firmas de los métodos dentro del bloque impl , las referencias pueden estar
vinculadas a los lifetime de los campos del struct, o pueden ser independientes. Además, las
reglas de omisión de lifetime a menudo hacen que no sean necesarias las anotaciones de
lifetime en las firmas de los métodos. Veamos algunos ejemplos usando el struct llamado
ImportantExcerpt que definimos en el listado 10-24.
En primer lugar, usaremos un método llamado level cuyo parámetro es una referencia a
self , y cuyo valor de retorno es un i32 , que no es una referencia a nada:
https://book.rustlang-es.org/print 266/656
12/7/24, 22:37 El Lenguaje de Programación Rust
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
La declaración del parámetro de lifetime después de impl y su uso después del nombre del
struct son requeridos, pero no estamos obligados a anotar el lifetime de la referencia a
self porque se aplica la primera regla de omisión.
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}
Hay dos input lifetimes, por lo que Rust aplica la primera regla de omisión de lifetime y les
da a &self y announcement sus propios lifetimes. Luego, debido a que uno de los
parámetros es &self , el tipo de retorno obtiene el lifetime de &self , y todos los lifetimes
han sido contabilizados.
El lifetime static
Un lifetime especial que necesitamos discutir es 'static , que denota que la referencia
afectada puede vivir durante toda la duración del programa. Todos los string literals tienen
el lifetime 'static , que podemos anotar de la siguiente manera:
El texto de este string se almacena directamente en el programa binario, que siempre está
disponible. Por lo tanto, la duración de todos los string literals es 'static .
Es posible que veas sugerencias para usar el lifetime 'static en mensajes de error. Pero
antes de especificar 'static como el lifetime para una referencia, piensa si la referencia
que tienes realmente vive durante toda la duración de tu programa o no, y si quieres que lo
haga. La mayoría de las veces, un mensaje de error que sugiere el lifetime 'static resulta
de intentar crear una referencia colgante o una falta de coincidencia de los lifetimes
disponibles. En tales casos, la solución es corregir esos problemas, no especificar el lifetime
'static .
https://book.rustlang-es.org/print 267/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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
}
}
Esta es la función longest del listado 10-21 que devuelve el string más largo de dos string
slices. Pero ahora tiene un parámetro adicional llamado ann del tipo generic T , que puede
llenarse con cualquier tipo que implemente el trait Display como se especifica en la
cláusula where . Este parámetro adicional se imprimirá con {} , por lo que es necesario el
trait bound Display . Debido a que los lifetimes son un tipo de generic, las declaraciones
del parámetro de lifetime 'a y el parámetro de tipo generic T van en la misma lista dentro
de los corchetes angulares después del nombre de la función.
Resumen
¡Hemos cubierto mucho en este capítulo! Ahora que conoces los parámetros de tipo
generic, los traits y los trait bounds, y los parámetros de lifetime generic, estás listo para
escribir código sin repetición que funcione en muchas situaciones diferentes. Los
parámetros de tipo generic te permiten aplicar el código a diferentes tipos. Los traits y los
trait bounds garantizan que, aunque los tipos son generic, tendrán el comportamiento que
el código necesita. Aprendiste cómo usar las anotaciones de lifetime para garantizar que
este código flexible no tendrá referencias colgantes. ¡Y todo este análisis ocurre en tiempo
de compilación, lo que no afecta el rendimiento en tiempo de ejecución!
Aunque no lo creas, hay mucho más que aprender sobre los temas que discutimos en este
capítulo: el Capítulo 17 discute los trait objects, que son otra forma de usar traits. También
hay escenarios más complejos que involucran anotaciones de lifetime que solo necesitarás
https://book.rustlang-es.org/print 268/656
12/7/24, 22:37 El Lenguaje de Programación Rust
en escenarios muy avanzados; para esos, debes leer la Referencia de Rust. Pero a
continuación, aprenderás cómo escribir pruebas en Rust para que puedas asegurarte de
que tu código funcione como debería.
https://book.rustlang-es.org/print 269/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Digamos que escribimos una función add_two que suma 2 a cualquier número que se le
pase. La firma de esta función acepta un entero como parámetro y devuelve un entero
como resultado. Cuando implementamos y compilamos esa función, Rust hace toda la
comprobación de tipos y de préstamos que has aprendido hasta ahora para asegurarse de
que, por ejemplo, no estamos pasando un valor String o una referencia no válida a esta
función. Pero Rust no puede comprobar que esta función haga precisamente lo que
pretendemos, que es devolver el parámetro más 2 en lugar de, por ejemplo, el parámetro
más 10 o el parámetro menos 50! Ahí es donde entran los tests.
Podemos escribir tests que afirmen, por ejemplo, que cuando pasamos 3 a la función
add_two , el valor devuelto es 5 . Podemos ejecutar estos tests siempre que hagamos
cambios en nuestro código para asegurarnos de que cualquier comportamiento correcto
existente no haya cambiado.
El Testing es una habilidad compleja: aunque no podemos cubrir todos los detalles sobre
cómo escribir buenos tests en un capítulo, discutiremos los mecanismos de las instalaciones
de testing de Rust. Hablaremos sobre las anotaciones y macros disponibles para ti cuando
escribas tus tests, el comportamiento predeterminado y las opciones proporcionadas para
ejecutar tus tests, y cómo organizar los tests en tests unitarios y tests de integración.
https://book.rustlang-es.org/print 270/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Veamos las características que Rust provee específicamente para escribir tests que incluyen
el atributo test , algunas macros, y el atributo should_panic .
En su forma más simple, un test en Rust es una función que está anotada con el atributo
test . Los atributos son metadatos sobre piezas de código Rust; un ejemplo es el atributo
derive que usamos con structs en el Capítulo 5. Para cambiar una función en una función
de test, agrega #[test] en la línea antes de fn . Cuando ejecutas tus tests con el comando
cargo test , Rust construye un binario que corre las funciones anotadas y reporta si cada
función de test pasa o falla.
Creemos un nuevo proyecto de librería llamado adder que sume dos números:
El contenido del archivo src/lib.rs en tu librería adder debería verse como el Listado 11-
1.
Filename: src/lib.rs
https://book.rustlang-es.org/print 271/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Listing 11-1: El módulo test y la función generada automáticamente por cargo new
Por ahora, ignoremos las dos primeras líneas y nos enfoquemos solamente en la función
it_works() . Nota la anotación #[test] : este atributo indica que esta es una función de
test, así que el test runner sabe que tratar esta función como un test. También podríamos
tener funciones no-test en el módulo tests para ayudar a configurar escenarios comunes
o realizar operaciones comunes, así que siempre necesitamos indicar qué funciones son
tests.
El cuerpo de la función de test llama a la macro assert_eq! , que verifica que dos valores
sean iguales. Si los valores no son iguales, assert_eq! falla y el test falla. Si son iguales, no
pasa nada y el test pasa.
El comando cargo test ejecuta todos los tests en tu proyecto de librería, Como puedes ver
en el Listado 11-2.
$ 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 (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_works ... ok
Doc-tests adder
running 0 tests
https://book.rustlang-es.org/print 272/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cargo compila y ejecuta el test. Vemos la línea running 1 test . La siguiente línea muestra
el nombre de la función de test generada, llamada it_works , y que el resultado de ejecutar
ese test es ok . El resumen general test result: ok. significa que todos los tests pasaron,
y la porción que lee 1 passed; 0 failed totaliza el número de tests que pasaron o fallaron.
Es posible marcar un test como ignorado para que no se ejecute en una particular instancia;
cubriremos eso en la sección “Ignorando algunos tests a menos que sean específicamente
requeridos” más tarde en este capítulo. Porque no hemos hecho eso aquí, el resumen
muestra 0 ignored . También podemos pasar un argumento al comando cargo test para
ejecutar solo tests cuyo nombre coincida con un string; esto se llama filtrado y lo cubriremos
en la sección “Ejecutando un subconjunto de tests por nombre”. Tampoco hemos filtrado los
tests que se ejecutan, así que el final del resumen muestra 0 filtered out .
La estadística 0 measured es para tests de benchmark que miden performance. Los tests
de benchmark, al momento de escribir esto, solo están disponibles en Rust nightly. Ver la
documentación sobre tests de benchmark para aprender más.
La siguiente parte del output de test, comenzando con Doc-tests adder , es para los
resultados de cualquier test de documentación. No tenemos tests de documentación aún,
pero Rust puede compilar cualquier ejemplo de código que aparezca en nuestra
documentación de API. ¡Esta característica ayuda a mantener tus docs y tu código en
sincronía! Discutiremos cómo escribir tests de documentación en la sección
“Documentación como tests” del Capítulo 14. Por ahora, ignoraremos el output Doc-tests .
Filename: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Entonces ejecutamos cargo test de nuevo. El output ahora muestra exploration en lugar
de it_works :
https://book.rustlang-es.org/print 273/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.59s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::exploration ... ok
Doc-tests adder
running 0 tests
Ahora agregaremos otro test, ¡pero esta vez haremos un test que falle! Los tests fallan
cuando algo en la función de test hace panic. Cada test se ejecuta en un nuevo thread, y
cuando el thread principal ve que un thread de test ha muerto, el test se marca como
fallido. En el Capítulo 9, hablamos sobre cómo la forma más simple de hacer panic es llamar
a la macro panic! . Ingresa el nuevo test como una función llamada another , así que tu
archivo src/lib.rs se ve como el Listado 11-3.
Filename: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn another() {
panic!("Make this test fail");
}
}
Listing 11-3: Agregando un segundo test que fallará porque llamamos a la macro panic!
Volvemos a ejecutar los tests usando cargo test . El output debería verse como el Listado
11-4, que muestra que nuestro test exploration pasó y another falló.
https://book.rustlang-es.org/print 274/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
Listing 11-4: Resultados del test cuando un test pasa y el otro falla
Ahora que has visto cómo se ven los resultados de tests en diferentes escenarios, veamos
algunas macros que son útiles en tests que no sean panic! .
https://book.rustlang-es.org/print 275/656
12/7/24, 22:37 El Lenguaje de Programación Rust
falle. Usar la macro assert! nos ayuda a verificar que nuestro código esté funcionando de
la forma que queremos.
Filename: src/lib.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
El método can_hold devuelve un valor booleano, lo que significa que es un caso de uso
perfecto para la macro assert! . En el Listado 11-6, escribimos un test que ejercita el
método can_hold creando una instancia de Rectangle que tiene un ancho de 8 y una
altura de 7 y afirmando que puede contener otra instancia de Rectangle que tiene un
ancho de 5 y una altura de 1.
Filename: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[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));
}
}
https://book.rustlang-es.org/print 276/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Listing 11-6: Un test para can_hold que verifica si un rectángulo más grande puede contener un rectángulo
más pequeño
Observa que hemos agregado una nueva línea dentro del módulo tests : use super::*; .
El módulo tests es un módulo regular que sigue las reglas de visibilidad habituales que
cubrimos en el Capítulo 7 en la sección “Paths para referirse a un item en el árbol de
módulos”. Como el módulo tests es un módulo interno, necesitamos traer el código bajo
test en el módulo externo al alcance del módulo interno. Usamos un asterisco aquí para que
cualquier cosa que definamos en el módulo externo esté disponible para este módulo
tests .
$ 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
Doc-tests rectangle
running 0 tests
¡Pasó! Ahora agreguemos otro test, esta vez afirmando que un rectángulo más pequeño no
puede contener un rectángulo más grande:
Filename: src/lib.rs
https://book.rustlang-es.org/print 277/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[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)
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
Doc-tests rectangle
running 0 tests
¡Dos tests que pasan! Ahora veamos qué sucede con nuestros resultados de test cuando
introducimos un bug en nuestro código. Cambiaremos la implementación del método
can_hold reemplazando el signo mayor que con un signo menor que cuando compara los
anchos:
https://book.rustlang-es.org/print 278/656
12/7/24, 22:37 El Lenguaje de Programación Rust
// --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
Una manera común de verificar la funcionalidad es probar la igualdad entre el resultado del
código bajo test y el valor que esperamos que el código devuelva. Podrías hacer esto
usando la macro assert! y pasándole una expresión usando el operador == . Sin embargo,
este es un test tan común que la biblioteca estándar provee un par de macros —
assert_eq! y assert_ne! — para realizar este test de manera más conveniente. Estas
macros comparan dos argumentos por igualdad o desigualdad, respectivamente. También
imprimirán los dos valores si la aserción falla, lo que hace más fácil ver por qué falló el test;
conversamente, la macro assert! solo indica que obtuvo un valor false para la expresión
== , sin imprimir los valores que llevaron al valor false .
https://book.rustlang-es.org/print 279/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En el Listado 11-7, escribimos una función llamada add_two que suma 2 a su parámetro,
luego testeamos esta función usando la macro assert_eq! .
Filename: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
}
$ 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
Doc-tests adder
running 0 tests
Vamos a introducir un error en nuestro código para ver cómo se ve assert_eq! cuando
falla. Cambiaremos la implementación de la función add_two para que en su lugar añada
3:
https://book.rustlang-es.org/print 280/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
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
¡Nuestro test atrapó el bug! El test it_adds_two falló, y el mensaje nos dice que la aserción
que falló fue assertion `left == right` failed y cuáles son los valores de left y
right . Este mensaje nos ayuda a comenzar a debuggear: el argumento left fue 4 pero el
argumento right , donde llamamos a add_two(2) , fue 5 . Puedes imaginar que esto sería
especialmente útil cuando tenemos muchos tests en marcha.
Cabe señalar que en algunos lenguajes y frameworks de test, los parámetros de las
funciones de aserción de igualdad se llaman expected y actual , y el orden en que
especificamos los argumentos importa. Sin embargo, en Rust, se llaman left y right , y el
orden en que especificamos el valor que esperamos y el valor que el código produce no
importa. Podríamos escribir la aserción en este test como assert_eq!(add_two(2), 4) , lo
que resultaría en el mismo mensaje de error que muestra assertion failed: `(left ==
right)` .
La macro assert_ne! pasará si los dos valores que le proporcionamos no son iguales. Esta
macro es más útil en casos en los que no estamos seguros de cuál será el valor, pero
sabemos que el valor definitivamente no debería ser. Por ejemplo, si estamos testeando una
función que está garantizada de cambiar su entrada de alguna manera, pero la forma en
https://book.rustlang-es.org/print 281/656
12/7/24, 22:37 El Lenguaje de Programación Rust
que la entrada cambia depende del día de la semana en que ejecutamos nuestros tests, lo
mejor sería afirmar que el output de la función no es igual al input.
También puedes agregar un mensaje personalizado a ser impreso con el mensaje de fallo
como argumentos opcionales a las macros assert! , assert_eq! y assert_ne! . Cualquier
argumento especificado después de los argumentos requeridos se pasa a la macro
format! (discutida en el Capítulo 8 en la sección “Concatenación con el operador + o la
macro format! ”), por lo que puedes pasar una format string que contenga marcadores de
posición {} y valores para ir en esos marcadores de posición. Los mensajes personalizados
son útiles para documentar lo que significa una aserción; cuando un test falla, tendrás una
mejor idea de cuál es el problema con el código.
Por ejemplo, supongamos que tenemos una función que saluda a las personas por nombre
y queremos probar que el nombre que pasamos a la función aparece en el output:
Filename: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
https://book.rustlang-es.org/print 282/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Las especificaciones para este programa aún no se han acordado, y estamos bastante
seguros de que el texto Hello al comienzo del saludo cambiará. Decidimos que no
queremos tener que actualizar el test cuando cambien los requisitos, por lo que en lugar de
verificar la igualdad exacta con el valor devuelto de la función greeting , solo afirmaremos
que el output contiene el texto del parámetro de entrada.
Ahora introduciremos un bug en este código cambiando greeting para excluir el name y
veremos cómo se ve el fallo de test predeterminado:
$ 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:
tests::greeting_contains_name
https://book.rustlang-es.org/print 283/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[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
Podemos ver el valor que realmente obtuvimos en el output del test, lo que nos ayudaría a
debuggear lo que sucedió en lugar de lo que esperábamos que sucediera.
Además de verificar los valores de retorno, es importante verificar que nuestro código
maneje las condiciones de error como esperamos. Por ejemplo, considera el tipo Guess
que creamos en el Listado 9-13 del Capítulo 9. Otro código que usa Guess depende de la
garantía de que las instancias de Guess contendrán solo valores entre 1 y 100. Podemos
escribir un test que asegure que al intentar crear una instancia de Guess con un valor fuera
de ese rango, se produzca un panic.
https://book.rustlang-es.org/print 284/656
12/7/24, 22:37 El Lenguaje de Programación Rust
hace un panic.
El Listado 11-8 muestra un test que verifica que las condiciones de error de Guess::new
sucedan cuando esperamos que sucedan.
Filename: src/lib.rs
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() {
Guess::new(200);
}
}
https://book.rustlang-es.org/print 285/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
Doc-tests guessing_game
running 0 tests
¡Se ve bien! Ahora introduzcamos un bug en nuestro código eliminando la condición de que
la función new hará un panic si el valor es mayor que 100:
// --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 }
}
}
https://book.rustlang-es.org/print 286/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
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
No obtenemos un mensaje muy útil en este caso, pero cuando miramos la función de test,
vemos que está anotada con #[should_panic] . El fallo que obtuvimos significa que el
código en la función de test no causó un panic.
Los tests que usan should_panic pueden ser imprecisos. Un test should_panic pasaría
incluso si el test hace un panic por una razón diferente a la que esperábamos. Para hacer
que los tests should_panic sean más precisos, podemos agregar un parámetro opcional
expected al atributo should_panic . El test harness se asegurará de que el mensaje de
error contenga el texto proporcionado. Por ejemplo, considera el código modificado para
Guess en el Listado 11-9 donde la función new hace un panic con mensajes diferentes
dependiendo de si el valor es demasiado pequeño o demasiado grande.
Filename: src/lib.rs
https://book.rustlang-es.org/print 287/656
12/7/24, 22:37 El Lenguaje de Programación Rust
// --snip--
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 {
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);
}
}
Listing 11-9: Testeando un panic! con un mensaje panic que contiene un substring específico
Este test fallará porque el valor que pusimos en el parámetro expected del atributo
should_panic es un substring del mensaje que genera la función Guess::new . Podríamos
haber especificado todo el mensaje de excepción que esperamos, que en este caso sería
Guess value must be less than or equal to 100, got 200. . Lo que elijas especificar
depende de cuánto del mensaje de excepción es único o dinámico y de cuán preciso quieras
que sea tu test. En este caso, un substring del mensaje de excepción es suficiente para
asegurar que el código en la función de test ejecuta el caso else if value > 100 .
Para ver que sucede cuando un test should_panic con un mensaje expected falla,
introduzcamos un bug en nuestro código al intercambiar los cuerpos de los bloques if
value < 1 y else if value > 100 :
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}."
);
}
https://book.rustlang-es.org/print 288/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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 el test falló con un error como esperábamos, pero el
mensaje de panic no incluyó el string esperado less than or equal to 100 . El mensaje de
panic que obtuvimos en este caso fue Guess value must be greater than or equal to 1,
got 200. . ¡Ahora podemos empezar a descubrir dónde está nuestro bug!
Hasta ahora, todos nuestros tests entran en panic cuando fallan. ¡También podemos
escribir tests que usen Result<T, E> ! Aquí está el test del Listado 11-1, reescrito para usar
Result<T, E> y devolver un Err en lugar de hacer un panic:
https://book.rustlang-es.org/print 289/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
Escribir tests que devuelvan un Result<T, E> te permite usar el operador ? en el cuerpo
de los tests, lo que puede ser una forma conveniente de escribir tests que fallarán si
cualquier operación dentro de ellos devuelve una variante Err .
No puedes utilizar la anotación #[should_panic] en tests que usen Result<T, E> . Para
asegurar que una operación devuelve una variante Err , no uses el operador ? en el valor
Result<T, E> . En su lugar, usa assert!(value.is_err()) .
Ahora que conoces varias formas de escribir tests, veamos qué sucede cuando ejecutamos
nuestros tests y exploremos las diferentes opciones que podemos usar con cargo test .
https://book.rustlang-es.org/print 290/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Algunas opciones de línea de comandos van a cargo test , y otras van al binario de test
resultante. Para separar estos dos tipos de argumentos, debes listar los argumentos que
van a cargo test seguidos del separador -- y luego los que van al binario de test. Ejecutar
cargo test --help muestra las opciones que puedes usar con cargo test , y ejecutar
cargo test -- --help muestra las opciones que puedes usar después del separador.
Cuando ejecutas múltiples tests, por defecto estos se ejecutan en paralelo usando hilos, lo
que significa que terminan de ejecutarse más rápido y obtienes feedback más rápido.
Debido a que los tests se ejecutan al mismo tiempo, debes asegurarte que tus tests no
dependan entre sí o de cualquier estado compartido, incluyendo un entorno compartido,
como el directorio de trabajo actual o las variables de entorno.
Por ejemplo, digamos que cada uno de tus tests ejecuta código que crea un archivo en disco
llamado test-output.txt y escribe algunos datos en ese archivo. Luego cada test lee los datos
en ese archivo y aserta que el archivo contiene un valor particular, el cual es diferente en
cada test. Debido a que los tests se ejecutan al mismo tiempo, un test podría sobreescribir
el archivo en el tiempo entre que otro test escribe y lee el archivo. El segundo test fallará, no
porque el código sea incorrecto, sino porque los tests han interferido entre sí mientras se
ejecutaban en paralelo. Una solución es asegurarte que cada test escriba en un archivo
diferente; otra solución es ejecutar los tests uno a la vez.
Si no deseas ejecutar los tests en paralelo o si deseas tener un control más fino sobre el
número de hilos usados, puedes enviar la bandera --test-threads y el número de hilos
que deseas usar al binario de test. Echa un vistazo al siguiente ejemplo:
https://book.rustlang-es.org/print 291/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Por defecto, si un test pasa, la librería de tests de Rust captura cualquier cosa impresa en la
salida estándar. Por ejemplo, si llamamos a println! en un test y el test pasa, no veremos
la salida de println! en la terminal; solo veremos la línea que indica que el test pasó. Si un
test falla, veremos lo que sea que se haya impreso en la salida estándar junto con el resto
del mensaje de falla.
Como ejemplo, el Listado 11-10 tiene una función tonta que imprime el valor de su
parámetro y retorna 10, así como un test que pasa y un test que falla.
Filename: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(10, value);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(5, value);
}
}
Cuando ejecutamos estos tests con cargo test , vemos el siguiente output:
https://book.rustlang-es.org/print 292/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
failures:
failures:
tests::this_test_will_fail
Nota que en ninguna parte de este output vemos I got the value 4 , que es lo que se
imprime cuando el test que pasa se ejecuta. Ese output ha sido capturado. El output del test
que falla, I got the value 8 , aparece en la sección del resumen de tests, que también
muestra la causa de la falla del test.
Si queremos ver los valores impresos por los tests que pasan también, podemos decirle a
Rust que muestre el output de los tests exitosos con --show-output .
Cuando ejecutamos los tests en el Listado 11-10 nuevamente con el flag --show-output ,
vemos el siguiente output:
https://book.rustlang-es.org/print 293/656
12/7/24, 22:37 El Lenguaje de Programación Rust
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
successes:
successes:
tests::this_test_will_pass
failures:
failures:
tests::this_test_will_fail
A veces, ejecutar un conjunto completo de tests puede tomar mucho tiempo. Si estás
trabajando en código en un área particular, podrías querer ejecutar solo los tests que
pertenecen a ese código. Puedes elegir qué tests ejecutar pasándole a cargo test el
nombre o nombres del test(s) que quieres ejecutar como argumento.
Para demostrar cómo ejecutar un subset de tests, primero crearemos tres tests para
nuestra función add_two , como se muestra en el Listado 11-11, y elegiremos cuáles
ejecutar.
Filename: src/lib.rs
https://book.rustlang-es.org/print 294/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
assert_eq!(4, add_two(2));
}
#[test]
fn add_three_and_two() {
assert_eq!(5, add_two(3));
}
#[test]
fn one_hundred() {
assert_eq!(102, add_two(100));
}
}
Si ejecutamos los tests sin pasar ningún argumento, como vimos anteriormente, todos los
tests se ejecutarán en paralelo:
$ 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
Doc-tests adder
running 0 tests
https://book.rustlang-es.org/print 295/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Podemos pasar el nombre de cualquier función de test a cargo test para ejecutar solo ese
test:
running 1 test
test tests::one_hundred ... ok
Solo se ejecutó el test con el nombre one_hundred ; los otros dos tests no coincidieron con
ese nombre. El output de los tests nos indica que tenemos más tests que no se ejecutaron
al mostrar 2 filtered out al final.
No podemos especificar los nombres de varios tests de esta manera; solo se usará el primer
valor dado a cargo test . Pero hay una manera de ejecutar varios tests.
Podemos especificar parte de un nombre de test y cualquier test cuyo nombre coincida con
ese valor se ejecutará. Por ejemplo, como dos de nuestros tests tienen add en el nombre,
podemos ejecutar esos dos ejecutando cargo test add :
running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
Este comando ejecutó todos los test con add en el nombre y filtró el test con el nombre
one_hundred . También nota que el módulo en el que aparece un test se convierte en parte
del nombre del test, por lo que podemos ejecutar todos los tests en un módulo filtrando por
el nombre del módulo.
https://book.rustlang-es.org/print 296/656
12/7/24, 22:37 El Lenguaje de Programación Rust
A veces, algunos tests específicos pueden ser muy lentos para ejecutarse, por lo que puede
que quieras excluirlos en la mayoría de las ejecuciones de cargo test . En lugar de listar
como argumentos todos los tests que quieres ejecutar, puedes anotar los tests que
consumen mucho tiempo usando el atributo ignore para excluirlos, como se muestra aquí:
Filename: src/lib.rs
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
Después de #[test] agregamos la línea #[ignore] al test que queremos excluir. Ahora
cuando ejecutamos nuestros tests, it_works se ejecuta, pero expensive_test no:
$ 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)
running 2 tests
test expensive_test ... ignored
test it_works ... ok
Doc-tests adder
running 0 tests
Esta función expensive_test está listada como ignored . Si queremos ejecutar solo los
tests ignorados, podemos usar cargo test -- -- ignored :
https://book.rustlang-es.org/print 297/656
12/7/24, 22:37 El Lenguaje de Programación Rust
running 1 test
test expensive_test ... ok
Doc-tests adder
running 0 tests
Controlando que tests se ejecutan, puedes asegurarte de que los resultados de cargo test
serán rápidos. Cuando estés en un punto en el que tenga sentido verificar los resultados de
los tests ignorados y tengas tiempo para esperar los resultados, puedes ejecutar cargo
test -- --ignored en su lugar. Si quieres ejecutar todos los tests, ignorados o no, puedes
ejecutar cargo test -- --include-ignored .
https://book.rustlang-es.org/print 298/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Escribir ambos tipos de tests es importante para asegurar que las piezas de tu biblioteca
están haciendo lo que esperas, separada y conjuntamente.
Tests Unitarios
El propósito de los tests unitarios es probar cada unidad de código en aislamiento del resto
del código para rápidamente identificar donde el código está y no está funcionando como
se espera. Pondrás los tests unitarios en el directorio src en cada archivo con el código que
están testeando. La convención es crear un módulo llamado tests en cada archivo para
contener las funciones de test y anotar el módulo con cfg(test) .
Recuerda que cuando generamos el nuevo proyecto adder en la primera sección de este
capítulo, Cargo generó este código para nosotros:
Filename: src/lib.rs
https://book.rustlang-es.org/print 299/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Hay debate dentro de la comunidad de testing sobre si las funciones privadas deberían ser
testeables directamente, y otros lenguajes hacen difícil o imposible testear funciones
privadas. Independientemente de la ideología de testing a la que te adhieras, las reglas de
privacidad de Rust te permiten testear funciones privadas. Considera el código en el Listado
11-12 con la función privada internal_adder .
Filename: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}
https://book.rustlang-es.org/print 300/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Nota que la función internal_adder no está marcada como pub . Los tests son solo código
Rust, y el módulo tests es solo otro módulo. Como discutimos en la sección “Paths for
Referring to an Item in the Module Tree”, items en módulos hijos pueden usar los items en
sus ancestros. En este test, traemos todos los items del padre del módulo tests al alcance
con use super::* , y entonces el test puede llamar a internal_adder . Si no piensas que las
funciones privadas deberían ser testeables, no hay nada en Rust que te obligue a hacerlo.
Tests de Integración
El directorio tests
Se crea un directorio llamado tests en el nivel superior del directorio de nuestro proyecto, al
lado de src. Cargo sabe buscar archivos de test de integración en este directorio. Podemos
crear tantos archivos de test como queramos en este directorio, y Cargo compilará cada
archivo como un crate individual.
Creemos un test de integración. Con el código en el Listado 11-12 aún en el archivo src/lib.rs,
crea un directorio tests y crea un nuevo archivo llamado tests/integration_test.rs. Tu
estructura de directorios debería verse así:
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
Filename: tests/integration_test.rs
https://book.rustlang-es.org/print 301/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use adder::add_two;
#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
Cada archivo en el directorio tests es un crate separado, así que necesitamos importar
nuestra biblioteca en el scope de cada crate de test. Por esa razón, agregamos use
adder::add_two al inicio del código, lo cual no necesitamos en los tests unitarios.
$ 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
running 1 test
test it_adds_two ... ok
Doc-tests adder
running 0 tests
Las tres secciones de output incluyen los tests unitarios, el test de integración y los tests de
documentación. Nota que si algún test en una sección falla, las siguientes secciones no
serán ejecutadas. Por ejemplo, si falla un test unitario, no habrá ningún output para los tests
de integración y de documentación porque esos tests solo serán ejecutados si todos los
tests unitarios pasan.
https://book.rustlang-es.org/print 302/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La primera sección es para los tests unitarios es la misma que hemos visto: una línea para
cada test unitario (uno llamado internal que agregamos en el Listado 11-12) y luego una
línea de resumen para los tests unitarios.
Cada archivo de test de integración tiene su propia sección, así que si agregamos más
archivos en el directorio tests, habrá más secciones de tests de integración.
running 1 test
test it_adds_two ... ok
En la medida en que se agregan más tests de integración, es posible que quieras crear más
archivos en el directorio tests para ayudar a organizarlas; por ejemplo, puedes agrupar las
funciones de test por la funcionalidad que están probando. Como se mencionó
anteriormente, cada archivo en el directorio tests es compilado como un crate separado, lo
cual es útil para crear scopes separados para imitar más de cerca la manera en que los
usuarios finales usarán tu crate. Sin embargo, esto significa que los archivos en el directorio
tests no comparten el mismo comportamiento que los archivos en src, como aprendiste en
el Capítulo 7 sobre cómo separar el código en módulos y archivos.
https://book.rustlang-es.org/print 303/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: tests/common.rs
pub fn setup() {
// setup code specific to your library's tests would go here
}
Cuando volvemos a ejecutar los tests, veremos una sección en el output de los tests para el
archivo common.rs, aunque este archivo no contiene ninguna función de test ni hemos
llamada a la función setup desde ningún lado:
$ 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
running 0 tests
running 1 test
test it_adds_two ... ok
Doc-tests adder
running 0 tests
Tener common apareciendo en los resultados de los tests con running 0 tests mostrado
para él no es lo que queríamos. Solo queríamos compartir algo de código con los otros
archivos de test de integración.
https://book.rustlang-es.org/print 304/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para evitar que common aparezca en el output de los tests, en lugar de crear
tests/common.rs, crearemos tests/common/mod.rs. El directorio del proyecto ahora se ve así:
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
Filename: tests/integration_test.rs
use adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}
Nota que la declaración mod common; es la misma que la declaración de módulo que
demostramos en el Listado 7-21. Luego, en la función de test, podemos llamar a la función
common::setup() .
Si nuestro proyecto es un crate binario que solo contiene un archivo src/main.rs y no tiene
un archivo src/lib.rs, no podemos crear tests de integración en el directorio tests y traer
funciones definidas en el archivo src/main.rs al scope con una declaración use . Solo los
crates de librería exponen funciones que otros crates pueden usar; los crates binarios están
destinados a ser ejecutados por sí mismos.
https://book.rustlang-es.org/print 305/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Esta es una de las razones por las que los proyectos Rust que proveen un binario tienen un
archivo src/main.rs que llama a la lógica que vive en el archivo src/lib.rs. Usando esa
estructura, los tests de integración pueden probar el crate de la librería con use para hacer
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 ese pequeño
código no necesita ser testeado.
Resumen
Las características de testing de Rust proveen una manera de especificar cómo el código
debería funcionar para asegurarse de que continúe funcionando como esperas, incluso
mientras haces cambios. Los tests unitarios ejercitan diferentes partes de una librería por
separado y pueden testear detalles de implementación privados. Los tests de integración
chequean que muchas partes de la librería funcionen juntas correctamente, y usan la API
pública de la librería para testear el código de la misma manera que el código externo lo
usará. Aunque el sistema de tipos y las reglas de ownership de Rust ayudan a prevenir
algunos tipos de bugs, los tests son todavía importantes para reducir bugs de lógica que
tienen que ver con cómo se espera que tu código se comporte.
https://book.rustlang-es.org/print 306/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En el camino, mostraremos cómo hacer que nuestra herramienta de línea de comandos use
las características del terminal que muchas otras herramientas de línea de comandos usan.
Leeremos el valor de una variable de entorno para permitir que el usuario configure el
comportamiento de nuestra herramienta. También imprimiremos mensajes de error a 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 exitosa a un archivo y seguir viendo los
mensajes de error en la pantalla.
Nuestro proyecto grep combinará una serie de conceptos que has aprendido hasta ahora:
También presentaremos brevemente los closures, iterators, y trait objects, que los capítulos
13 y 17 cubrirán en detalle.
https://book.rustlang-es.org/print 307/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La primera tarea es hacer que minigrep acepte sus dos argumentos de línea de comandos:
la ruta del archivo y una cadena para buscar. Es decir, queremos poder ejecutar nuestro
programa con cargo run , dos guiones para indicar que los siguientes argumentos son para
nuestro programa en lugar de para cargo , una cadena para buscar y una ruta a un archivo
para buscar, así:
En este momento, el programa generado por cargo new no puede procesar los
argumentos que le damos. Algunas bibliotecas existentes en crates.io pueden ayudar a
escribir un programa que acepte argumentos de línea de comandos, pero como estás
aprendiendo este concepto, implementemos esta capacidad nosotros mismos.
Para permitir que minigrep lea los valores de los argumentos de línea de comandos que le
pasamos, necesitaremos la función std::env::args proporcionada en la biblioteca
estándar de Rust. Esta función devuelve un iterador de los argumentos de línea de
comandos pasados a minigrep . Cubriremos los iteradores completamente en el capítulo
13. Por ahora, solo necesitas saber dos detalles sobre los iteradores: los iteradores
producen una serie de valores, y podemos llamar al método collect en un iterador para
convertirlo en una colección, como un vector, que contiene todos los elementos que el
iterador produce.
El código en el Listado 12-1 permite que tu programa minigrep lea cualquier argumento de
línea de comandos que se le pase y luego recoja los valores en un vector.
Filename: src/main.rs
https://book.rustlang-es.org/print 308/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
Primero, traemos el módulo std::env al alcance con una declaración use para que
podamos usar su función args . Ten en cuenta que la función std::env::args está
anidada en dos niveles de módulos. Como discutimos en el capítulo 7, en los casos en que la
función deseada está anidada en más de un módulo, hemos elegido traer el módulo padre
al alcance en lugar de la función. Al hacerlo, podemos usar fácilmente otras funciones de
std::env . También es menos ambiguo que agregar use std::env::args y luego llamar a
la función con solo args , porque args podría confundirse fácilmente con una función
definida en el módulo actual.
$ 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",
]
https://book.rustlang-es.org/print 309/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/main.rs
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
Listing 12-2: Creando variables para contener el argumento de consulta y la ruta de archivo
Como vimos cuando imprimimos en el vector, el nombre del programa ocupa el primer
valor del vector en args[0] , por lo que estamos comenzando a leer los argumentos en el
índice 1 . El primer argumento minigrep que toma es la cadena que estamos buscando,
por lo que ponemos una referencia al primer argumento en la variable query . El segundo
argumento será la ruta del archivo, por lo que ponemos una referencia al segundo
argumento en la variable file_path .
https://book.rustlang-es.org/print 310/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Temporalmente, imprimimos los valores de estas variables para demostrar que el código
está funcionando como pretendemos. Ejecutemos el programa nuevamente con los
argumentos test y sample.txt :
¡Genial, el programa está funcionando! Los valores de los argumentos que necesitamos se
están guardando en las variables correctas. Más adelante agregaremos un manejo de
errores para tratar ciertas situaciones erróneas potenciales, como cuando el usuario no
proporciona argumentos; por ahora, ignoraremos esa situación y trabajaremos en agregar
capacidades de lectura de archivos en su lugar.
https://book.rustlang-es.org/print 311/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Leyendo un archivo
Ahora agregaremos funcionalidad para leer el archivo especificado en el argumento
file_path . Primero, necesitamos un archivo de muestra para probarlo: ¡usaremos un
archivo con una pequeña cantidad de texto en varias líneas con algunas palabras repetidas!
¡El Listado 12-3 tiene un poema de Emily Dickinson que funcionará bien! Cree un archivo
llamado poem.txt en el nivel raíz de su proyecto e ingrese el poema "¡Soy nadie! ¿Quién eres
tú?"
Filename: poem.txt
Listing 12-3: Un poema de Emily Dickinson sirve como buen caso de test
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.
Filename: src/main.rs
use std::env;
use std::fs;
fn main() {
// --snip--
println!("In file {file_path}");
println!("With text:\n{contents}");
}
Listing 12-4: Leyendo el contenido del archivo especificado por el segundo argumento
Primero, importamos la parte relevante de la biblioteca estándar con una sentencia use :
necesitamos std::fs para manejar archivos.
https://book.rustlang-es.org/print 312/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Luego, nuevamente agregamos una declaración println! temporal que imprime el valor
de contents después de que se lee el archivo, para que podamos verificar que el programa
está funcionando hasta ahora.
Ejecutemos este código con cualquier string como primer argumento de la línea de
comandos (porque aún no hemos implementado la parte de búsqueda) y el archivo poem.txt
como segundo argumento:
¡Genial! El código leyó el archivo y luego imprimió el contenido del archivo. Pero el código
tiene algunas fallas. En este momento, la función main tiene múltiples responsabilidades:
en general, las funciones son más claras y más fáciles de mantener si cada función es
responsable de una sola idea. El otro problema es que no estamos manejando los errores
tan bien como podríamos. El programa todavía es pequeño, por lo que estas fallas no son
un gran problema, pero a medida que el programa crece, será más difícil corregirlos de
manera limpia. Es una buena práctica comenzar a refactorizar desde el principio al
desarrollar un programa, porque es mucho más fácil refactorizar cantidades menores de
código. Haremos eso a continuación.
https://book.rustlang-es.org/print 313/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Este problema también está relacionado con el segundo problema: aunque query y
file_path son variables de configuración para nuestro programa, variables como
contents se utilizan para realizar la lógica del programa. Cuanto más largo sea main , más
variables necesitaremos para traer al alcance; Cuantas más variables tengamos en el
alcance, más difícil será realizar un seguimiento del propósito de cada una. Es mejor
agrupar las variables de configuración en una estructura para que su propósito quede claro.
El tercer problema es que hemos usado expect para imprimir un mensaje de error cuando
falla la lectura del archivo, pero el mensaje de error solo imprime Should have been able
to read the file . La lectura de un archivo puede fallar de varias maneras: por ejemplo, el
archivo podría faltar, o podríamos no tener permiso para abrirlo. En este momento,
independientemente de la situación, imprimiríamos el mismo mensaje de error para todo,
¡lo que no le daría al usuario ninguna información!
https://book.rustlang-es.org/print 314/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Las responsabilidades que quedan en la función main después de este proceso deberían
limitarse a lo siguiente:
Este patrón se trata de separar las preocupaciones: main.rs maneja la ejecución del
programa, y lib.rs maneja toda la lógica de la tarea en cuestión. Debido a que no puede
probar la función main directamente, esta estructura le permite probar toda la lógica de su
programa moviéndola a funciones en lib.rs. El código que permanece en main.rs será lo
suficientemente pequeño como para verificar su corrección leyéndolo. Rehagamos nuestro
programa siguiendo este proceso.
Extraeremos la funcionalidad para analizar los argumentos en una función que main
llamará para prepararse para mover la lógica de análisis de línea de comandos a src/lib.rs.
La lista 12-5 muestra el nuevo inicio de main que llama a una nueva función parse_config ,
que definiremos en src/main.rs por el momento.
Filename: src/main.rs
fn main() {
let args: Vec<String> = env::args().collect();
// --snip--
}
(query, file_path)
}
Esta reorganización puede parecer excesiva para nuestro pequeño programa, pero estamos
refactorizando en pequeños pasos incrementales. Después de hacer este cambio, ejecute el
programa nuevamente para verificar que el análisis de argumentos aún funcione. Es bueno
verificar su progreso con frecuencia, para ayudar a identificar la causa de los problemas
cuando ocurren.
Podemos dar otro pequeño paso para mejorar aún más la función parse_config . En este
momento, estamos devolviendo una tupla, pero luego rompemos esa tupla en partes
individuales nuevamente. Esto es una señal de que tal vez no tenemos la abstracción
correcta todavía.
Otro indicador que muestra que hay margen de mejora es la parte config de
parse_config , que implica que los dos valores que devolvemos están relacionados y
ambos son parte de un valor de configuración. Actualmente, no estamos transmitiendo este
significado en el struct de los datos que no sea agrupar los dos valores en una tupla; en su
lugar, pondremos los dos valores en un struct y daremos a cada uno de los campos del
struct un nombre significativo. Hacerlo hará que sea más fácil para los futuros
mantenedores de este código comprender cómo se relacionan los diferentes valores entre
sí y cuál es su propósito.
Filename: src/main.rs
https://book.rustlang-es.org/print 316/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let args: Vec<String> = env::args().collect();
// --snip--
}
struct Config {
query: String,
file_path: String,
}
Listing 12-6: Refactorizando parse_config para que devuelva una instancia de un struct Config
Hemos agregado un struct llamado Config definido para tener campos llamados query y
file_path . La firma de parse_config ahora índica que devuelve un valor Config . En el
cuerpo de parse_config , donde solíamos devolver rebanadas de cadena que hacen
referencia a valores String en args , ahora definimos Config para contener valores
String de propiedad. La variable args en main es el propietario de los valores de
argumento y solo permite que la función parse_config los pida prestados, lo que significa
que violaríamos las reglas de préstamo de Rust si Config intentara tomar posesión de los
valores en args .
Hay varias formas de administrar los datos de String ; la más fácil, aunque algo ineficiente,
es llamar al método clone en los valores. Esto hará una copia completa de los datos para
que la instancia de Config posea, lo que toma más tiempo y memoria que almacenar una
referencia a los datos de string. Sin embargo, clonar los datos también hace que nuestro
código sea muy sencillo porque no tenemos que administrar los lifetimes de las referencias;
en estas circunstancias, renunciar a un poco de rendimiento para ganar simplicidad es un
intercambio válido.
https://book.rustlang-es.org/print 317/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Hay una tendencia entre muchos Rustaceans a evitar usar clone para solucionar
problemas de propiedad debido a su costo de tiempo de ejecución. En el capítulo 13,
aprenderá a usar métodos más eficientes en este tipo de situaciones. Pero por ahora,
está bien copiar algunas cadenas para seguir progresando porque solo harás estas
copias una vez y tu ruta de archivo y cadena de consulta son muy pequeñas. Es mejor
tener un programa que funcione un poco ineficiente que intentar hiperoptimizar el
código en tu primer paso. 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 .
Hemos actualizado main para que coloque la instancia de Config devuelta por
parse_config en una variable llamada config , y hemos actualizado el código que
anteriormente usaba las variables separadas query y file_path para que ahora use los
campos en el struct Config en su lugar.
Ahora nuestro código transmite más claramente que query y file_path están
relacionados y que su propósito es configurar cómo funcionará el programa. Cualquier
código que use estos valores sabe que debe buscarlos en la instancia config en los campos
nombrados por su propósito.
Hasta ahora, hemos extraído la lógica responsable de analizar los argumentos de la línea de
comandos de main y la hemos colocado en la función parse_config . Hacerlo nos ayudó a
ver que los valores query y file_path estaban relacionados y que esa relación debería
transmitirse en nuestro código. Luego agregamos un struct Config para nombrar el
propósito relacionado de query y file_path y poder devolver los nombres de los valores
como nombres de campo de struct desde la función parse_config .
Así que ahora el propósito de la función parse_config es crear una instancia de Config ,
podemos cambiar parse_config de una función normal a una función llama new que es
asociada con Config . que esté asociada con el struct Config . Haciendo este cambio, el
código será más idiomático. Podemos crear instancias de tipos en la biblioteca estándar,
como String , llamando a String::new . De manera similar, al cambiar parse_config a
una función asociada con Config , podremos crear instancias de Config llamando a
Config::new . El listado 12-7 muestra los cambios que debemos hacer.
Filename: src/main.rs
https://book.rustlang-es.org/print 318/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let args: Vec<String> = env::args().collect();
// --snip--
}
// --snip--
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Hemos actualizado main donde estábamos llamando a parse_config para que en su lugar
llame a Config::new . Hemos cambiado el nombre de parse_config a new y lo hemos
movido dentro de un bloque impl , que asocia la función new con Config . Intenta compilar
este código nuevamente para asegurarte de que funciona.
$ 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
La línea index out of bounds: the len is 1 but the index is 1 es un mensaje de error
destinado a los programadores. No ayudará a nuestros usuarios finales a comprender lo
que deben hacer en su lugar. Arreglemos eso ahora.
https://book.rustlang-es.org/print 319/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En el Listado 12-8, agregamos una verificación en la función new que verificará que el slice
sea lo suficientemente largo antes de acceder al índice 1 y 2. Si el slice no es lo
suficientemente largo, el programa entra en pánico y muestra un mensaje de error mejor.
Filename: src/main.rs
// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("not enough arguments");
}
// --snip--
Este código es similar a la función Guess::new que escribimos en el Listado 9-13, donde
llamamos a panic! cuando el argumento value estaba fuera del rango de valores válidos.
En lugar de verificar un rango de valores aquí, estamos verificando que la longitud de args
sea al menos 3 y el resto de la función puede operar bajo la suposición de que esta
condición se ha cumplido. Si args tiene menos de tres elementos, esta condición será
verdadera y llamaremos a la macro panic! para finalizar el programa inmediatamente.
Con estas pocas líneas de código adicionales en new , ejecutemos el programa sin ningún
argumento nuevamente para ver cómo se ve el error 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
Este output es mejor: ahora tenemos un mensaje de error razonable. Sin embargo, también
tenemos información superflua que no queremos dar a nuestros usuarios. Quizás usar la
técnica que usamos en el Listado 9-13 no es la mejor para usar aquí: una llamada a panic!
es más apropiada para un problema de programación que para un problema de uso, como
se discutió en el Capítulo 9. En su lugar, usaremos la otra técnica que aprendiste en el
Capítulo 9: devolver un Result que indique el éxito o un error.
https://book.rustlang-es.org/print 320/656
12/7/24, 22:37 El Lenguaje de Programación Rust
new nunca fallen. Cuando Config::build se comunique con main , podemos usar el tipo
Result para señalar que hubo un problema. Luego podemos cambiar main para convertir
una variante Err en un error más práctico para nuestros usuarios sin el texto circundante
sobre thread 'main' y RUST_BACKTRACE que una llamada a panic! provoca.
El Listado 12-9 muestra los cambios que debemos hacer en el valor de retorno de la función
que ahora llamamos Config::build y el cuerpo de la función necesario para devolver un
Result . Ten en cuenta que esto no se compilará hasta que actualicemos main también, lo
cual haremos en el siguiente listado.
Filename: src/main.rs
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
Nuestra función build devuelve un Result con una instancia de Config en el caso de
éxito y una referencia a un string en el caso de error. Nuestros valores de error siempre
serán string literals que tengan el lifetime 'static .
Hemos hecho dos cambios en el cuerpo de la función: en lugar de llamar a panic! cuando
el usuario no pasa suficientes argumentos, ahora devolvemos un valor Err , y hemos
envuelto el valor de retorno Config en un Ok . Estos cambios hacen que la función se
ajuste a su nueva firma de tipo.
Devolviendo un valor Err desde Config::build permite que la función main maneje el
Result devuelto por la función build y salga del proceso de manera más limpia en el caso
de error.
Para manejar el caso de error e imprimir un mensaje amigable para el usuario, necesitamos
actualizar main para manejar el Result que devuelve Config::build , como se muestra en
el Listado 12-10. También tomaremos la responsabilidad de salir de la herramienta de línea
de comandos con un código de error distinto de cero de panic! e implementarlo a mano.
Un estado de salida distinto de cero es una convención para señalar al proceso que llamó a
nuestro programa que el programa salió con un estado de error.
https://book.rustlang-es.org/print 321/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/main.rs
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
// --snip--
Listing 12-10: Saliendo con un código de error si falla la construcción de una Config
En este listado, hemos usado un método que aún no hemos cubierto en detalle:
unwrap_or_else , que está definido en Result<T, E> por la biblioteca estándar. Usar
unwrap_or_else nos permite definir un manejo de errores personalizado que no sea
panic! . Si el Result es un valor Ok , el comportamiento de este método es similar a
unwrap : devuelve el valor interno que Ok está envolviendo. Sin embargo, si el valor es un
valor Err , este método llama al código en el closure, que es una función anónima que
definimos y pasamos como argumento a unwrap_or_else . Cubriremos los closures con
más detalle en el Capítulo 13. Por ahora, solo necesitas saber que unwrap_or_else pasará
el valor interno del Err , que en este caso es el string estático "not enough arguments" que
agregamos en el Listado 12-9, a nuestro closure en el argumento err que aparece entre las
barras verticales | . El código en el closure imprime el valor de err cuando se ejecuta.
Hemos agregado una nueva línea use para traer process de la biblioteca estándar al
alcance. El código en el closure que se ejecutará en el caso de error es solo de dos líneas:
imprimimos el valor de err y luego llamamos a process::exit . La función process::exit
detendrá el programa inmediatamente y devolverá el número que se pasó como código de
estado de salida. Esto es similar al manejo basado en panic! que usamos en el Listado 12-
8, pero ya no obtenemos todo el output extra. ¡Probémoslo!
$ 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
extraeremos una función llamada run que contendrá toda la lógica actualmente en la
función main que no está involucrada con la configuración o el manejo de errores. Cuando
terminemos, main será conciso y fácil de verificar por inspección, y podremos escribir
pruebas para toda la otra lógica.
El Listado 12-11 muestra la función run extraída. Por ahora, solo estamos haciendo la
pequeña mejora incremental de extraer la función. Todavía estamos definiendo la función
en src/main.rs.
Filename: src/main.rs
fn main() {
// --snip--
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--
Listing 12-11: Extracting a run function containing the rest of the program logic
La función run ahora contiene toda la lógica restante de main , comenzando desde la
lectura del archivo. La función run toma la instancia de Config como argumento.
Con la lógica del programa restante separada en la función run , podemos mejorar el
manejo de errores, como hicimos con Config::build en el Listado 12-9. En lugar de
permitir que el programa entre en pánico llamando a expect , la función run devolverá un
Result<T, E> cuando algo salga mal. Esto nos permitirá consolidar aún más la lógica que
rodea el manejo de errores en main de una manera amigable para el usuario. El Listado 12-
12 muestra los cambios que debemos hacer en la firma y el cuerpo de run .
Filename: src/main.rs
https://book.rustlang-es.org/print 323/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::error::Error;
// --snip--
println!("With text:\n{contents}");
Ok(())
}
Hemos realizado tres cambios significativos aquí. Primero, cambiamos el tipo de retorno de
la función run a Result<(), Box<dyn Error>> . Esta función anteriormente devolvía el tipo
unitario, () , y lo mantenemos como el valor devuelto en el caso Ok .
Para el tipo de error, usamos el trait object Box<dyn Error> (y hemos traído
std::error::Error al alcance con una declaración use en la parte superior). Cubriremos
los trait objects en el Capítulo 17. Por ahora, solo sepa que Box<dyn Error> significa que la
función devolverá un tipo que implementa el trait Error , pero no tenemos que especificar
qué tipo particular será el valor de retorno. Esto nos da flexibilidad para devolver valores de
error que pueden ser de diferentes tipos en diferentes casos de error. La palabra clave dyn
es corta para “dynamic”.
Segundo, hemos eliminado la llamada a expect en favor del operador ? , como hablamos
en el Capítulo 9. En lugar de panic! en un error, ? devolverá el valor de error de la función
actual para que el llamador lo maneje.
Tercero, la función run ahora devuelve un valor Ok en caso de éxito. Hemos declarado con
éxito la función run como () en la firma, lo que significa que necesitamos envolver el valor
unitario en el valor Ok . Esta sintaxis Ok(()) puede parecer un poco extraña al principio,
pero usar () de esta manera es la forma idiomática de indicar que estamos llamando a
run solo por sus efectos secundarios; no devuelve un valor que necesitamos.
https://book.rustlang-es.org/print 324/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Rust nos dice que nuestro código ignoró el valor Result y el valor Result podría indicar
que ocurrió un error. Pero no estamos comprobando si hubo un error o no, ¡y el compilador
nos recuerda que probablemente quisimos tener algo de código de manejo de errores aquí!
Corrijamos ese problema ahora.
Comprobaremos los errores y los manejaremos usando una técnica similar a la que usamos
con Config::build en el Listado 12-10, pero con una ligera diferencia:
Filename: src/main.rs
https://book.rustlang-es.org/print 325/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
// --snip--
Usamos if let en lugar de unwrap_or_else para verificar si run devuelve un valor Err y
llamar a process::exit(1) si lo hace. La función run no devuelve un valor que queremos
unwrap de la misma manera que Config::build devuelve la instancia de Config . Debido
a que run devuelve () en el caso de éxito, solo nos importa detectar un error, por lo que
no necesitamos que unwrap_or_else devuelva el valor desempaquetado, que solo sería
() .
Los cuerpos de las funciones if let y unwrap_or_else son los mismos en ambos casos:
imprimimos el error y salimos.
Nuestro proyecto minigrep se ve bien hasta ahora. Ahora dividiremos el archivo src/main.rs
y pondremos parte del código en el archivo src/lib.rs. De esa manera podemos probar el
código y tener un archivo src/main.rs con menos responsabilidades.
Vamos a mover todo el código que no sea la función main de src/main.rs a src/lib.rs:
La función run
Las declaraciones use relevantes
La definición de Config
La función Config::build
Filename: src/lib.rs
https://book.rustlang-es.org/print 326/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::error::Error;
use std::fs;
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
// --snip--
}
}
Hemos hecho uso de la palabra clave pub : en Config , en sus campos y en su método
build , y en la función run . ¡Ahora tenemos un crate de biblioteca que tiene una API
pública que podemos probar!.
Ahora necesitamos traer el código que movimos a src/lib.rs al scope del crate binario en
src/main.rs, como se muestra en el Listado 12-14.
Filename: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
// --snip--
if let Err(e) = minigrep::run(config) {
// --snip--
}
}
Agregamos una línea use minigrep::Config para traer el tipo Config desde el crate de
biblioteca al scope del crate binario, y agregamos el prefijo minigrep:: a la llamada a run .
Ahora toda la funcionalidad debería estar conectada y debería funcionar. Ejecuta el
programa con cargo run y asegúrate de que todo funcione correctamente.
¡Uf! Eso fue mucho trabajo, pero nos hemos preparado para el éxito en el futuro. Ahora es
mucho más fácil manejar errores, y hemos hecho que el código sea más modular. Casi todo
nuestro trabajo se hará en src/lib.rs a partir de ahora.
https://book.rustlang-es.org/print 327/656
12/7/24, 22:37 El Lenguaje de Programación Rust
¡Aprovechemos esta nueva modularidad haciendo algo que habría sido difícil con el código
antiguo, pero es fácil con el nuevo código: escribiremos algunas pruebas!
https://book.rustlang-es.org/print 328/656
12/7/24, 22:37 El Lenguaje de Programación Rust
1. Escriba un test que falle y ejecútala para asegurarse de que falla por la razón que
espera.
2. Escribe o modifica solo el código suficiente para que el nuevo test pase.
3. Refactoriza el código que acabas de agregar o cambiar y asegúrate de que los tests
sigan pasando.
4. ¡Repite desde el paso 1!
Aunque es solo una de las muchas formas de escribir software, TDD puede ayudar a
impulsar el diseño del código. Escribir la prueba antes de escribir el código que hace que la
prueba pase ayuda a mantener una alta cobertura de prueba durante todo el proceso.
Filename: src/lib.rs
https://book.rustlang-es.org/print 329/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
Listing 12-15: Creando un test fallido para la función search que deseamos tener
Este test busca el string "duct" . El texto que estamos buscando son tres líneas, solo una de
las cuales contiene "duct" (Tenga en cuenta que la barra invertida después de la comilla
doble de apertura le dice a Rust que no ponga un carácter de nueva línea al comienzo del
contenido de esta cadena literal). Afirmamos que el valor devuelto de la función search
contiene solo la línea que esperamos.
Aún no podemos ejecutar este test y verlo fallar porque el test ni siquiera se compila: ¡la
función search aún no existe! De acuerdo con los principios de TDD, agregaremos solo el
código suficiente para que la prueba se compile y se ejecute agregando una definición de la
función search que siempre devuelve un vector vacío, como se muestra en el listado 12-16.
Luego, la prueba debería compilar y fallar porque un vector vacío no coincide con un vector
que contiene la línea "safe, fast, productive." .
Filename: src/lib.rs
Listing 12-16: Definiendo solo lo necesario de la función search para que nuestro test compile
Observa que necesitamos definir un lifetime explícito 'a en la firma de search y usar ese
lifetime con el argumento contents y el valor de retorno. Recuerde en Capítulo 10 que los
parámetros de lifetime especifican qué lifetime de argumento está conectado al lifetime del
valor de retorno. En este caso, indicamos que el vector devuelto debe contener string slices
que hagan referencia a slices del argumento contents (en lugar del argumento query ).
En otras palabras, le decimos a Rust que los datos devueltos por la función search vivirán
tanto tiempo como los datos pasados a la función search en el argumento contents . ¡Esto
es importante! Los datos a los que hace referencia un slice deben ser válidos para que la
https://book.rustlang-es.org/print 330/656
12/7/24, 22:37 El Lenguaje de Programación Rust
referencia sea válida; si el compilador asume que estamos haciendo string slices de query
en lugar de contents , hará sus comprobaciones de seguridad incorrectamente.
$ 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 qué argumento de los dos necesitamos, por lo que debemos decirle
explícitamente. Debido a que contents es el argumento que contiene todo nuestro texto y
queremos devolver las partes de ese texto que coincidan, sabemos que contents es el
argumento que debe estar conectado al valor de retorno usando la sintaxis de lifetime.
https://book.rustlang-es.org/print 331/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
failures:
failures:
tests::one_result
¡Genial, el test falla, exactamente como esperábamos! ¡Vamos a hacer que el test pase!
Actualmente, nuestro test falla porque siempre devolvemos un vector vacío. Para solucionar
eso e implementar search , nuestro programa debe seguir estos pasos:
Rust tiene un método útil para manejar la iteración línea por línea de strings,
convenientemente llamado lines , que funciona como se muestra en el listado 12-17.
Tenga en cuenta que esto aún no se compilará.
Filename: src/lib.rs
https://book.rustlang-es.org/print 332/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
Listing 12-18: Agregando funcionalidad para verificar si la línea contiene el string en query
En este punto, estamos construyendo funcionalidad. Para que compile, debemos devolver
un valor del cuerpo como indicamos en la firma de la función.
Para terminar esta función, necesitamos una forma de almacenar las líneas coincidentes
que queremos devolver. Para eso, podemos hacer un vector mutable antes del bucle for y
llamar al método push para almacenar una line en el vector. Después del bucle for ,
devolvemos el vector, como se muestra en el listado 12-19.
Filename: src/lib.rs
https://book.rustlang-es.org/print 333/656
12/7/24, 22:37 El Lenguaje de Programación Rust
results
}
Listing 12-19: Almacenando las líneas que coinciden para poder devolverlas
Ahora la función search debería devolver solo las líneas que contienen query , y nuestro
test debería pasar. Ejecutemos el test:
$ 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
running 0 tests
Doc-tests minigrep
running 0 tests
https://book.rustlang-es.org/print 334/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Ahora que la función search funciona y está probada, necesitamos llamar a search desde
nuestra función run . Necesitamos pasar el valor de config.query y el contents que run
lee del archivo a la función search . Luego, run imprimirá cada línea devuelta por search :
Filename: src/lib.rs
Ok(())
}
Todavía estamos usando un bucle for para devolver cada línea de search e imprimirla.
Ahora todo el programa debería funcionar. Probémoslo con una palabra que debería
devolver exactamente una línea del poema de Emily Dickinson, "frog":
¡Funciona! Ahora intentemos que coincida con varias líneas, como "body":
¡Excelente! Hemos construido nuestra propia versión de una herramienta clásica y hemos
aprendido mucho sobre cómo estructurar aplicaciones. También hemos aprendido un poco
sobre input y output de archivos, lifetimes, testing y análisis de líneas de comandos.
https://book.rustlang-es.org/print 335/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para completar nuestro proyecto, demostraremos brevemente cómo trabajar con variables
de entorno y cómo imprimir en el error estándar, ambas son útiles cuando se escriben
programas de línea de comandos.
https://book.rustlang-es.org/print 336/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
https://book.rustlang-es.org/print 337/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
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)
);
}
}
Listing 12-20: Agregando un nuevo test fallido para la función insensible a mayúsculas y minúsculas que
estamos a punto de agregar
Ten en cuenta que hemos editado el contents del viejo test también. Hemos agregado una
nueva línea con el texto "Duct tape." usando una D mayúscula que no debería coincidir
con la consulta "duct" cuando estamos buscando de manera sensible a mayúsculas y
minúsculas. Cambiar el viejo test de esta manera ayuda a asegurar que no rompamos
accidentalmente la funcionalidad de búsqueda sensible a mayúsculas y minúsculas que ya
hemos implementado. Este test debería pasar ahora y debería continuar pasando mientras
trabajamos en la búsqueda insensible a mayúsculas y minúsculas.
El nuevo test para la búsqueda insensible a mayúsculas y minúsculas usa "rUsT" como su
consulta. En la función search_case_insensitive que estamos a punto de agregar, la
consulta "rUsT" debería coincidir con la línea que contiene "Rust:" con una R mayúscula
y coincidir con la línea "Trust me." aunque ambas tienen diferente capitalización que la
consulta. Este es nuestro test fallido, y fallará al compilar porque aún no hemos definido la
función search_case_insensitive . Siéntete libre de agregar una implementación esqueleto
que siempre devuelva un vector vacío, similar a la forma en que lo hicimos para la función
search en el Listado 12-16 para ver el test compilar y fallar.
https://book.rustlang-es.org/print 338/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
results
}
Listing 12-21: Definiendo la función search_case_insensitive para convertir a minúsculas tanto la consulta
como la línea antes de compararlas
Nota que query ahora es un String en lugar de un string slice, porque llamar a
to_lowercase crea nuevos datos en lugar de referenciar datos existentes. Digamos que la
consulta es "rUsT" , como un ejemplo: ese string slice no contiene una u o t en minúscula
para que podamos usar, así que tenemos que asignar un nuevo String que contenga
"rust" . Cuando pasamos query como un argumento al método contains ahora,
necesitamos agregar un ampersand porque la firma de contains está definida para tomar
un string slice.
https://book.rustlang-es.org/print 339/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
running 0 tests
Doc-tests minigrep
running 0 tests
Filename: src/lib.rs
Filename: src/lib.rs
https://book.rustlang-es.org/print 340/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Ok(())
}
Finalmente, necesitamos verificar la variable de entorno. Las funciones para trabajar con
variables de entorno están en el módulo env en la biblioteca estándar, así que traemos ese
módulo al alcance en la parte superior de src/lib.rs. Luego usaremos la función var del
módulo env para verificar si se ha establecido algún valor para una variable de entorno
llamada IGNORE_CASE , como se muestra en el Listado 12-23.
Filename: src/lib.rs
use std::env;
// --snip--
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
Listing 12-23: Comprobando si existe algún valor en una variable de entorno llamada IGNORE_CASE
Aquí, creamos una nueva variable ignore_case . Para establecer su valor, llamamos a la
función env::var y le pasamos el nombre de la variable de entorno IGNORE_CASE . La
https://book.rustlang-es.org/print 341/656
12/7/24, 22:37 El Lenguaje de Programación Rust
función env::var devuelve un Result que será la variante Ok exitosa que contiene el
valor de la variable de entorno si la variable de entorno está configurada con algún valor.
Devolverá la variante Err si la variable de entorno no está configurada.
¡Parece que aún funciona! Ahora, ejecutemos el programa con IGNORE_CASE establecido en
1 pero con la misma consulta to .
Esto hará que IGNORE_CASE persista durante el resto de la sesión de tu shell. Puede
desestablecerse con el comando Remove-Item :
Deberíamos obtener líneas que contengan "to" que podrían tener letras mayúsculas:
https://book.rustlang-es.org/print 342/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Excelente, ¡también obtuvimos líneas que contienen "To"! Nuestro programa minigrep
ahora puede hacer búsquedas insensibles a mayúsculas y minúsculas controladas por una
variable de entorno. Ahora sabes cómo administrar las opciones establecidas mediante
argumentos de línea de comandos o variables de entorno.
El módulo std::env contiene muchas más funciones útiles para trabajar con variables de
entorno: consulta su documentación para ver qué está disponible.
https://book.rustlang-es.org/print 343/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La macro println! solo es capaz de imprimir en el output estándar, así que tenemos que
usar algo más para imprimir en el error estándar.
Primero, observemos como el contenido impreso por minigrep está siendo escrito en el
output estándar, incluyendo cualquier mensaje de error que queramos escribir en el error
estándar. Haremos eso redirigiendo el output estándar a un archivo mientras causamos un
error intencionalmente. No redirigiremos el error estándar, así que cualquier contenido
enviado al error estándar continuará mostrándose en la pantalla.
Los programas de línea de comandos se espera que envíen mensajes de error al error
estándar así que podemos ver los mensajes de error en la pantalla incluso si redirigimos el
output estándar a un archivo. Nuestro programa no se está comportando bien: estamos a
punto de ver que guarda los mensajes de error en un archivo en su lugar!
Para demostrar este comportamiento, ejecutaremos el programa con > y la ruta del
archivo, output.txt, al que queremos redirigir el output estándar. No pasaremos ningún
argumento, lo que debería causar un error:
La sintaxis > le dice a la shell que escriba el contenido del output estándar en output.txt en
lugar de la pantalla. No vimos el mensaje de error que esperábamos impreso en la pantalla,
así que eso significa que debe haber terminado en el archivo. Esto es lo que contiene
output.txt:
Sí, nuestro mensaje de error está siendo impreso en el output estándar. Es mucho más útil
para mensajes de error como este ser impresos en el error estándar así que solo los datos
de una ejecución exitosa terminen en el archivo. Cambiaremos eso.
https://book.rustlang-es.org/print 344/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Usaremos el código en el Listado 12-24 para cambiar como los mensajes de error son
impresos. Debido al refactor que hicimos anteriormente en este capítulo, todo el código que
imprime mensajes de error está en una función, main . La librería estándar provee la macro
eprintln! que imprime en el flujo de error estándar, así que cambiaremos los dos lugares
donde estábamos llamando println! para imprimir errores usando eprintln! en su
lugar.
Filename: src/main.rs
fn main() {
let args: Vec<String> = env::args().collect();
Listing 12-24: Escribiendo mensajes de error en el error estándar en lugar del output estándar utilizando
eprintln!
Ahora, ejecutaremos el programa de la misma manera que antes, sin pasar ningún
argumento y redirigiendo el output estándar con > :
Ahora podemos ver el mensaje de error en la pantalla y output.txt no contiene nada, que es
el comportamiento que esperamos de los programas de línea de comandos.
Ejecutemos el programa otra vez con argumentos que no causen un error pero aun así
redirigiendo el output estándar a un archivo, como así:
Filename: output.txt
https://book.rustlang-es.org/print 345/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Esto demuestra que ahora estamos usando el output estándar para output exitoso y el
error estándar para output de error como es apropiado.
Resumen
En este capítulo repasó algunos de los conceptos principales que has aprendido hasta ahora
y cubrió como realizar operaciones de I/O comunes en Rust. Al usar argumentos de línea de
comandos, archivos, variables de ambiente, y la macro eprintln! para imprimir errores,
ahora estás preparado para escribir aplicaciones de línea de comandos. Combinado con los
conceptos de capítulos anteriores, tu código estará bien organizado, almacenará datos
efectivamente en las estructuras de datos apropiadas, manejará errores de manera
agradable, y estará bien testeado.
https://book.rustlang-es.org/print 346/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Closures, una construcción similar a una función que puede almacenarse en una
variable
Iteradores, una forma de procesar una serie de elementos
Cómo usar closures e iterators para mejorar el proyecto I/O en el Capítulo 12
¡El rendimiento de los closures e iteradores (Spoiler alert: son más rápidos de lo que
podrías pensar!)
Ya hemos cubierto algunas otras características de Rust, como el pattern matching y los
enums, que también están influenciados por el estilo funcional. Debido a que dominar los
closures e iteradores es una parte importante de escribir código Rust idiomático y rápido,
dedicaremos todo este capítulo a ellos.
https://book.rustlang-es.org/print 347/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Primero examinaremos cómo podemos usar closures para capturar valores del entorno en
el que están definidos para su uso posterior. Aquí está el escenario: Cada cierto tiempo,
nuestra compañía de camisetas regala una camiseta exclusiva y de edición limitada a
alguien en nuestra lista de correo como promoción. Las personas en la lista de correo
pueden agregar opcionalmente su color favorito a su perfil. Si la persona elegida para una
camiseta gratis tiene su color favorito establecido, obtienen esa camiseta de color. Si la
persona no ha especificado un color favorito, obtienen el color que la compañía tiene
actualmente en mayor cantidad.
Hay muchas formas de implementar esto. Para este ejemplo, vamos a usar un enum
llamado ShirtColor que tiene las variantes Red y Blue (limitando el número de colores
disponibles para simplificar). Representamos el inventario de la compañía con un struct
Inventory que tiene un campo llamado shirts que contiene un Vec<ShirtColor> que
representa los colores de camisetas actualmente en stock. El método giveaway definido en
Inventory obtiene la preferencia opcional de color de camiseta del ganador de la camiseta
gratis, y devuelve el color de camiseta que la persona obtendrá. Esta configuración se
muestra en el Listado 13-1:
Filename: src/main.rs
https://book.rustlang-es.org/print 348/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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],
};
https://book.rustlang-es.org/print 349/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El almacén definido en main tiene dos camisetas azules y una camiseta roja restante para
distribuir para esta promoción de edición limitada. Llamamos al método giveaway para un
usuario con preferencia por una camiseta roja y un usuario sin ninguna preferencia.
Otra vez, este código podría implementarse de muchas maneras, y aquí, para centrarnos en
los closures, nos hemos adherido a los conceptos que ya has aprendido, excepto por el
cuerpo del método giveaway que usa un closure. En el método giveaway , obtenemos la
preferencia del usuario como un parámetro de tipo Option<ShirtColor> y llamamos al
método unwrap_or_else en user_preference . El método unwrap_or_else en Option<T>
Closure sin ningún argumento que devuelve un valor T (el mismo tipo almacenado en la
variante Some de la Option<T> , en este caso ShirtColor ). Si la Option<T> es la variante
Some , unwrap_or_else devuelve el valor de dentro de Some . Si la Option<T> es la variante
None , unwrap_or_else llama al closure y devuelve el valor devuelto por el closure.
$ 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 closures. Los closures no suelen requerir que
anotes los tipos de los parámetros o el valor de retorno como lo hacen las funciones fn .
Las anotaciones de tipo son necesarias en las funciones porque los tipos son parte de una
interfaz explícita expuesta a tus usuarios. Definir esta interfaz rígidamente es importante
https://book.rustlang-es.org/print 350/656
12/7/24, 22:37 El Lenguaje de Programación Rust
para garantizar que todos estén de acuerdo en qué tipos de valores usa y devuelve una
función. Los closures, por otro lado, no se usan en una interfaz expuesta como esta: se
almacenan en variables y se usan sin nombrarlos y exponerlos a los usuarios de nuestra
biblioteca.
Los closures típicamente son cortos y relevantes solo dentro de un contexto estrecho en
lugar de en cualquier escenario arbitrario. Dentro de estos contextos limitados, el
compilador puede inferir los tipos de los parámetros y el tipo de retorno, similar a cómo
puede inferir los tipos de la mayoría de las variables (hay casos raros en los que el
compilador también necesita anotaciones de tipo de closure).
Como con las variables, podemos agregar anotaciones de tipo opcionales si queremos
aumentar la explicitud y la claridad a costa de ser más verbosos de lo estrictamente
necesario. La anotación de tipos para un closure se vería como la definición que se muestra
en el Listado 13-2. En este ejemplo, estamos definiendo un closure y almacenándolo en una
variable en lugar de definir el closure en el lugar donde lo pasamos como argumento como
lo hicimos en el Listado 13-1.
Filename: src/main.rs
Listing 13-2: Agregando anotaciones de tipo opcionales para los tipos de parámetros y valor de retorno en el
closure
Con la anotación de tipo agregada, la sintaxis de los closures se parece más a la sintaxis de
las funciones. Aquí definimos una función que agrega 1 a su parámetro y un closure que
tiene el mismo comportamiento, para comparación. Hemos agregado algunos espacios para
alinear las partes relevantes. Esto ilustra cómo la sintaxis de los closures es similar a la
sintaxis de las funciones, excepto por el uso de tuberías y la cantidad de sintaxis que es
opcional:
La primera línea muestra una definición de función, y la segunda línea muestra una
definición de closure completamente anotada. En la tercera línea, quitamos las anotaciones
de tipo de la definición de closure. En la cuarta línea, quitamos los corchetes, que son
opcionales porque el cuerpo del closure tiene solo una expresión. Estas son todas
definiciones válidas que producirán el mismo comportamiento cuando se llamen. Las líneas
add_one_v3 y add_one_v4 requieren que los closures se evalúen para poder compilar
https://book.rustlang-es.org/print 351/656
12/7/24, 22:37 El Lenguaje de Programación Rust
porque los tipos se inferirán a partir de su uso. Esto es similar a let v = Vec::new(); que
necesita anotaciones de tipo o valores de algún tipo para insertar en el Vec para que Rust
pueda inferir el tipo.
Para las definiciones de closure, el compilador infiere 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 closure corto que solo devuelve el valor que recibe como parámetro. Este closure no es
muy útil, excepto para los propósitos de este ejemplo. Tenga en cuenta que no hemos
agregado ninguna anotación de tipo a la definición. Debido a que no hay anotaciones de
tipo, podemos llamar al closure con cualquier tipo, lo que hemos hecho aquí con String la
primera vez. Si luego intentamos llamar a example_closure con un entero, obtendremos
un error.
Filename: src/main.rs
let s = example_closure(String::from("hello"));
let n = example_closure(5);
Listing 13-3: Intentando llamar a un closure cuyos tipos se infieren con dos tipos diferentes
https://book.rustlang-es.org/print 352/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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`
--> 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
La primera vez que llamamos a example_closure con el valor String , el compilador infiere
el tipo de x y el tipo de retorno del closure como String . Esos tipos se bloquean en el
closure en example_closure , y obtenemos un error de tipo cuando intentamos usar un tipo
diferente con el mismo closure.
Los closures pueden valores desde su entorno de tres maneras, que se mapean
directamente a las tres formas en que una función puede tomar un parámetro: borrowing
inmutable, borrowing mutable y tomando ownership. El closure decidirá cuál de estos usar
en función de lo que haga el cuerpo de la función con los valores capturados.
En el Listado 13-4, definimos un closure que captura una referencia inmutable al vector
list ya que solo necesita una referencia inmutable para imprimir el valor:
Filename: src/main.rs
https://book.rustlang-es.org/print 353/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
Listing 13-4: Definiendo y llamando a un closure que captura una referencia inmutable
Este ejemplo también ilustra que una variable puede vincularse a una definición de closure,
y luego podemos llamar al closure usando el nombre de la variable y paréntesis como si el
nombre de la variable fuera un nombre de función.
Debido a que podemos tener múltiples referencias inmutables a list al mismo tiempo,
list sigue siendo accesible desde el código antes de la definición del closure, después de
la definición del closure, pero antes de que se llame al closure, y después de que se llame al
closure. Este código se compila, se ejecuta e imprime:
$ cargo run
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]
Luego, en el Listado 13-5, cambiamos el cuerpo del closure para que agregue un elemento
al vector list . El closure ahora captura una referencia mutable:
Filename: src/main.rs
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
borrows_mutably();
println!("After calling closure: {list:?}");
}
Listing 13-5: Definiendo y llamando a un closure que captura una referencia mutable
https://book.rustlang-es.org/print 354/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
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]
Si deseas forzar al closure para que tome ownership de los valores que usa en el entorno,
incluso cuando el cuerpo del closure no los necesite, puedes usar la palabra clave move
antes de la lista de parámetros.
Esta técnica es principalmente útil cuando se pasa un closure a un nuevo hilo para mover
los datos para que sean propiedad del nuevo hilo. Discutiremos los hilos y por qué querrías
usarlos en detalle en el Capítulo 16 cuando hablemos sobre la concurrencia, pero por ahora,
exploremos brevemente cómo generar un nuevo hilo usando un closure que necesita la
palabra clave move . El Listado 13-6 muestra el Listado 13-4 modificado para imprimir el
vector en un nuevo hilo en lugar de en el hilo principal:
Filename: src/main.rs
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
Listing 13-6: Usando move para forzar que el closure del thread tome el ownership de list
Iniciamos un nuevo hilo, dando al hilo un closure para ejecutar como argumento. El cuerpo
del closure imprime la lista. En el Listado 13-4, el closure solo capturó list usando una
referencia inmutable porque esa es la menor cantidad de acceso a list necesaria para
imprimirla. En este ejemplo, aunque el cuerpo del closure todavía solo necesita una
referencia inmutable, debemos especificar que list debe moverse al closure poniendo la
palabra clave move al comienzo de la definición del closure. El nuevo hilo podría terminar
antes de que el resto del hilo principal termine, o el hilo principal podría terminar primero.
https://book.rustlang-es.org/print 355/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Si el hilo principal mantuviera la propiedad de list pero terminara antes de que lo hiciera
el nuevo hilo y dejara caer list , la referencia inmutable en el hilo sería inválida. Por lo
tanto, el compilador requiere que list se mueva al closure dado al nuevo hilo para que la
referencia sea válida. ¡Intente eliminar la palabra clave move o usar list en el hilo principal
después de que se defina el closure para ver qué errores del compilador obtiene!
Una vez que un closure ha capturado una referencia o capturado el ownership de un valor
del entorno donde se define el closure (afectando así lo que, si cualquier cosa, se mueve
dentro del closure), el código en el cuerpo del closure define lo que sucede con las
referencias o valores cuando el closure se evalúa más tarde (afectando así lo que, si
cualquier cosa, se mueve fuera del closure). El cuerpo de un closure puede hacer cualquiera
de las siguientes acciones: mover un valor capturado fuera del closure, mutar el valor
capturado, ni mover ni mutar el valor, o no capturar nada del entorno para comenzar.
La forma en que un closure captura y maneja los valores del entorno afecta qué traits
implementa el closure, y los traits son cómo las funciones y los structs pueden especificar
qué tipos de closures pueden usar. Los closures implementarán automáticamente uno, dos
o los tres de estos traits Fn , de manera aditiva, dependiendo de cómo el cuerpo del closure
maneje los valores:
1. FnOnce se aplica los closures que pueden ser llamados una vez. Todos los closures
implementan al menos este trait, porque todos los closures pueden ser llamados. Un
closure que mueve valores capturados fuera de su cuerpo solo implementará FnOnce
y ninguno de los otros traits Fn , porque solo puede ser llamado una vez.
2. FnMut se aplica a los closures que no mueven valores capturados fuera de su cuerpo,
pero que podrían mutar los valores capturados. Estos closures pueden ser llamados
más de una vez.
3. Fn se aplica a los closures que no mueven valores capturados fuera de su cuerpo y
que no mutan los valores capturados, así como los closures que no capturan nada de
su entorno. Estos closures pueden ser llamados más de una vez sin mutar su entorno,
lo cual es importante en casos como llamar a un closure múltiples veces
concurrentemente.
https://book.rustlang-es.org/print 356/656
12/7/24, 22:37 El Lenguaje de Programación Rust
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
Recuerda que T es el tipo generic que representa el tipo del valor en la variante Some de
un Option . Ese tipo T también es el tipo de retorno de la función unwrap_or_else : el
código que llama a unwrap_or_else en un Option<String> , por ejemplo, obtendrá un
String .
Luego, observe que el método unwrap_or_else tiene el parámetro de tipo generic adicional
F . El tipo F es el tipo del parámetro llamado f , que es el closure que proporcionamos al
llamar a unwrap_or_else .
El trait bound especificado en el tipo generic F es FnOnce() -> T , lo que significa que F
debe poder ser llamado una vez para producir un valor del tipo T . Usar FnOnce en el trait
bound expresa la restricción de que unwrap_or_else solo va a llamar a f como máximo
una vez. En el cuerpo de unwrap_or_else , podemos ver que si el Option es Some , f no se
llamará. Si el Option es None , f se llamará una vez. Debido a que todos los closures
implementan FnOnce , unwrap_or_else acepta todos esos tipos de closures y es tan flexible
como puede ser.
Nota: Las funciones también pueden implementar los tres traits Fn , FnMut y FnOnce .
Si lo que queremos hacer no requiere capturar un valor del entorno, podemos usar el
nombre de una función en lugar de un closure donde necesitamos algo que
implemente uno de los traits Fn . Por ejemplo, en un valor Option<Vec<T>> ,
podríamos llamar a unwrap_or_else(Vec::new) para obtener un nuevo vector vacío si
el valor es None .
Ahora veamos el método de la biblioteca estándar sort_by_key definido en slices, para ver
cómo difiere de unwrap_or_else y por qué sort_by_key utiliza FnMut en lugar de FnOnce
para el trait bound. El closure recibe un argumento en forma de referencia al elemento
actual en el slice que se está considerando, y devuelve un valor de tipo K que se puede
ordenar. Esta función es útil cuando desea ordenar un slice por un atributo particular de
cada elemento. En el Listado 13-7, tenemos una lista de instancias de Rectangle y usamos
sort_by_key para ordenarlas por su atributo width de menor a mayor:
Filename: src/main.rs
https://book.rustlang-es.org/print 357/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[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| r.width);
println!("{list:#?}");
}
$ 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,
},
]
La razón por la que sort_by_key está definido para tomar un closure FnMut es que llama
al closure varias veces: una vez por cada elemento en el slice. El closure |r| r.width no
captura, muta ni mueve nada de su entorno, por lo que cumple con los requisitos de los
trait bound.
En contraste, El Listado 13-8 muestra un ejemplo de un closure que implementa solo el trait
FnOnce , porque mueve un valor fuera del entorno. El compilador no nos permitirá usar este
closure con sort_by_key :
Filename: src/main.rs
https://book.rustlang-es.org/print 358/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[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:#?}");
}
Esto es un ejemplo artificial y complicado (que no funciona) para tratar de contar la cantidad
de veces que se llama a sort_by_key llama a la closure al ordenar list . Este código
intenta hacer este conteo empujando value —un String del entorno del closure—en el
vector sort_operations . El closure captura value y luego mueve value fuera del closure
transfiriendo la propiedad de value al vector sort_operations . Este closure puede ser
llamado una vez; tratar de llamarlo una segunda vez no funcionaría porque value ya no
estaría en el entorno para ser empujado a sort_operations nuevamente. Por lo tanto, este
closure solo implementa FnOnce . Cuando intentamos compilar este código, obtenemos
este error de que value no se puede mover fuera del closure porque el closure debe
implementar FnMut :
https://book.rustlang-es.org/print 359/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous
error
El error señala la línea en el cuerpo del closure que mueve value fuera del entorno. Para
solucionar esto, debemos cambiar el cuerpo del closure para que no mueva valores fuera
del entorno. Para contar la cantidad de veces que se llama a la closure, mantener un
contador en el entorno e incrementar su valor en el cuerpo del closure es una forma más
directa de calcular eso. El closure en el Listado 13-9 funciona con sort_by_key porque solo
está capturando una referencia mutable al contador num_sort_operations y, por lo tanto,
puede ser llamado más de una vez:
#[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 },
];
https://book.rustlang-es.org/print 360/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los Fn traits son importantes al definir o usar funciones o tipos que hacen uso de closures.
En la siguiente sección, discutiremos los iteradores. Muchos métodos de iteradores toman
argumentos de closure, ¡así que tenga en cuenta estos detalles de closure a medida que
continuamos!
https://book.rustlang-es.org/print 361/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En rust, los iterators son lazy, lo que significa que no tienen efecto hasta que llamas a
métodos que consumen el iterador para usarlo. Por ejemplo, el código en el Listado 13-10
crea un iterador sobre los elementos del vector v1 llamando al método iter definido en
Vec<T> . Este código por sí solo no hace nada útil.
El iterador es almacenado en la variable v1_iter . Una vez que hemos creado un iterator,
podemos usarlo de varias maneras. En el Listado 3-5 del Capítulo 3, iteramos sobre un array
usando un bucle for para ejecutar algún código en cada uno de sus elementos. Bajo el
capó, esto crea e implícitamente consume un iterator, pero pasamos por alto cómo
funciona exactamente hasta ahora.
En el ejemplo del Listado 13-11, separamos la creación del iterador del uso del iterador en el
bucle for . Cuando el bucle for es llamado usando el iterator en v1_iter , cada elemento
en el iterador es usado en una iteración del bucle, lo que imprime cada valor.
En lenguajes que no tienen iterators provistos por sus bibliotecas estándar, probablemente
escribirías esta misma funcionalidad comenzando una variable en el índice 0, usando esa
variable para indexar en el vector para obtener un valor, e incrementando el valor de la
variable en un bucle hasta que alcanzara el número total de elementos en el vector.
Los iterators manejan toda esa lógica por ti, reduciendo el código repetitivo que podrías
potencialmente arruinar. Los iterators te dan más flexibilidad para usar la misma lógica con
https://book.rustlang-es.org/print 362/656
12/7/24, 22:37 El Lenguaje de Programación Rust
muchos tipos diferentes de secuencias, no solo estructuras de datos en las que puedes
indexar, como los vectores. Examinemos cómo los iterators hacen eso.
Todos los iterators implementan un trait llamado Iterator que está definido en la
biblioteca estándar. La definición del trait se ve así:
Observa que esta definición usa una nueva sintaxis: type Item y Self::Item , que definen
un associated type con este trait. Hablaremos sobre los associated types en profundidad en
el Capítulo 19. Por ahora, todo lo que necesitas saber es que este código dice que
implementar el trait Iterator requiere que también definas un tipo Item , y este tipo Item
es usado en el tipo de retorno del método next . En otras palabras, el tipo Item será el tipo
retornado del iterator.
El trait Iterator solo requiere que los implementadores definan un método: el método
next , que retorna un item del iterador a la vez envuelto en Some y, cuando la iteración ha
terminado, retorna None .
Podemos llamar al método next en los iterators directamente; el Listado 13-12 demuestra
qué valores son retornados de llamadas repetidas a next en el iterador creado desde el
vector.
Filename: src/lib.rs
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
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);
}
https://book.rustlang-es.org/print 363/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Nota que necesitamos hacer v1_iter mutable: llamar al método next en un iterador
cambia el estado interno que el iterador usa para mantenerse al tanto de dónde está en la
secuencia. En otras palabras, este código consume, o usa, el iterator. Cada llamada a next
consume un item del iterator. No necesitamos hacer v1_iter mutable cuando usamos un
bucle for porque el bucle toma posesión de v1_iter y lo hace mutable detrás de escena.
También debemos tener en cuenta que los valores que obtenemos de las llamadas a next
son referencias inmutables a los valores en el vector. El método iter produce un iterador
sobre referencias inmutables. Si queremos crear un iterator que tome posesión de v1 y
retorne valores poseídos, podemos llamar a into_iter en lugar de iter . De manera
similar, si queremos iterar sobre referencias mutables, podemos llamar a iter_mut en
lugar de iter .
Los métodos que llaman a next se llaman consuming adaptors, porque consumen el
iterador llamando a next . Un ejemplo es el método sum , que toma posesión del iterador y
lo itera a través de los items llamando a next , así consumiendo el iterator. A medida que
itera a través de ellos, agrega cada item a un total en ejecución y retorna el total cuando la
iteración está completa. El Listado 13-13 tiene una prueba que ilustra el uso del método
sum :
Filename: src/lib.rs
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
assert_eq!(total, 6);
}
Listing 13-13: Llamando al método sum para obtener el total de todos los items en el iterator
No se nos permite usar v1_iter después de la llamada a sum porque sum toma el
ownership del iterador en el que lo llamamos.
https://book.rustlang-es.org/print 364/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Iterator adaptors son métodos definidos en el trait Iterator que no consumen el iterator.
En cambio, producen diferentes iterators cambiando algún aspecto del iterador original.
El Listado 13-14 muestra un ejemplo de llamar al método adaptador de iterator map que
toma un closure para llamar en cada item y produce un nuevo iterator. El método map
retorna un nuevo iterador que ejecuta el closure que le pasamos en cada item y produce los
items resultantes. El closure aquí crea un nuevo iterador en el que cada item del vector será
incrementado en 1:
Filename: src/main.rs
v1.iter().map(|x| x + 1);
Listing 13-14: Llamando al iterador adaptor map para crear un nuevo iterator
$ 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 hace nada; el closure que hemos especificado nunca es
llamado. El warning nos recuerda por qué: los iterador adaptors son perezosos, y
necesitamos consumir el iterador aquí.
Para solucionar este warning y consumir el iterator, usaremos el método collect , que
usamos en el Capítulo 12 con env::args en el Listado 12-1. Este método consume el
iterador y colecciona los valores resultantes en un tipo de colección.
En el Listado 13-15, recolectamos los resultados de iterar sobre el iterator que es retornado
de la llamada a map en un vector. Este vector terminará conteniendo cada item del vector
https://book.rustlang-es.org/print 365/656
12/7/24, 22:37 El Lenguaje de Programación Rust
original incrementado en 1.
Filename: src/main.rs
Listing 13-15: Llamando al método map para crear un nuevo iterador y luego llamando al método collect para
Debido a que map toma un closure, podemos especificar cualquier operación que
queramos realizar en cada item. Este es un gran ejemplo de cómo los closures te permiten
personalizar algún comportamiento mientras reutilizas el comportamiento de iteración que
el trait Iterator provee.
Puedes encadenar múltiples llamadas a iterador adaptors para realizar acciones complejas
de una manera legible. Pero debido a que todos los iterators son perezosos, tienes que
llamar a uno de los métodos adaptadores consumidores para obtener resultados de las
llamadas a iterador adaptors.
Muchos de los iterador adaptors toman closures como argumentos, y comúnmente los
closures que especificaremos como argumentos a iterador adaptors capturarán su entorno.
Para este ejemplo, usaremos el método filter definido en el trait Iterator , que toma un
closure que toma un item y retorna un bool . Si el closure retorna true , el valor será
incluido en el iterador producido. Si el closure retorna false , el valor no será incluido en el
iterador producido.
En el Listado 13-16, usamos filter con un closure que captura la variable shoe_size de
su entorno para iterar sobre una colección de instancias de la estructura Shoe . Retornará
solo los zapatos que sean del tamaño especificado.
Filename: src/lib.rs
https://book.rustlang-es.org/print 366/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
#[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")
},
]
);
}
}
Listing 13-16: Usando el método filter con un closure que captura shoe_size
https://book.rustlang-es.org/print 367/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El closure captura el parámetro shoe_size del entorno y compara el valor con el tamaño de
cada zapato, manteniendo solo los zapatos del tamaño especificado. Finalmente, llamando
a collect recolectamos los valores retornados por el iterador adaptado en un vector que
es retornado por la función.
https://book.rustlang-es.org/print 368/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En el Listado 12-6, agregamos código que tomó un slice de valores String y creó una
instancia del struct Config indexando en el slice y clonando los valores, permitiendo que el
struct Config posea esos valores. En el Listado 13-17, hemos reproducido la
implementación de la función Config::build tal como estaba en el Listado 12-23:
Filename: src/lib.rs
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
En ese momento, dijimos que no nos preocupáramos por las llamadas ineficientes a clone
porque las eliminaríamos en el futuro. ¡Bueno, ese momento es ahora!
https://book.rustlang-es.org/print 369/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Con nuestro nuevo conocimiento sobre iteradores, podemos cambiar la función build
para tomar propiedad de un iterator como su argumento en lugar de tomar prestado un
slice. Usaremos la funcionalidad del iterator en lugar del código que verifica la longitud del
slice e indexa en ubicaciones específicas. Esto aclarará lo que la función Config::build
está haciendo porque el iterator accederá a los valores.
Una vez que Config::build tome ownership del iterator y deje de usar operaciones de
indexación que toman borrowing, podemos mover los valores String del iterator dentro
de Config en lugar de llamar a clone y hacer una nueva asignación.
Filename: src/main.rs
fn main() {
let args: Vec<String> = env::args().collect();
// --snip--
}
Filename: src/main.rs
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
}
¡La función env::args retorna un iterator! En lugar de recolectar los valores del iterator en
un vector y luego pasar un slice a Config::build , ahora estamos pasando ownership del
iterator retornado por env::args directamente a Config::build .
https://book.rustlang-es.org/print 370/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
Debido a que estamos tomando ownership de args y estaremos mutando args por
iterarlo, podemos agregar la palabra clave mut en la especificación del parámetro args
para hacerlo mutable.
Luego, necesitamos actualizar el cuerpo de Config::build para usar los métodos del trait
Iterator en lugar de indexar en el slice. En el Listado 13-20 hemos actualizado el código
del Listado 12-23 para usar el método next :
Filename: src/lib.rs
https://book.rustlang-es.org/print 371/656
12/7/24, 22:37 El Lenguaje de Programación Rust
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
También podemos aprovechar los iterators en la función search de nuestro proyecto I/O, el
cual se reproduce aquí en el Listado 13-21 como estaba en el Listado 12-19:
Filename: src/lib.rs
https://book.rustlang-es.org/print 372/656
12/7/24, 22:37 El Lenguaje de Programación Rust
results
}
Podemos escribir este código de una manera más concisa usando los métodos adaptor del
iterator. Hacerlo también nos permite evitar tener un vector intermedio mutable results .
El estilo de programación funcional prefiere minimizar la cantidad de estado mutable para
hacer el código más claro. Remover el estado mutable podría permitir una mejora futura
para hacer que la búsqueda ocurra en paralelo, porque no tendríamos que manejar el
acceso concurrente al vector results . El Listado 13-22 muestra este cambio:
Filename: src/lib.rs
Recuerda que el propósito de la función search es retornar todas las líneas en contents
que contengan query . Similar al ejemplo de filter en el Listado 13-16, este código usa el
adaptador filter para mantener solo las líneas que retornan true para
line.contains(query) . Luego recolectamos las líneas que coinciden en otro vector con
collect . ¡Mucho más simple! Siéntete libre de hacer el mismo cambio para usar los
métodos del iterator en la función search_case_insensitive también.
La siguiente pregunta lógica es qué estilo deberías escoger en tu propio código y por qué: la
implementación original en el Listado 13-21 o la versión usando iterators en el Listado 13-
22. La mayoría de los programadores Rust prefieren usar el estilo de iterators. Es un poco
más difícil de entender al principio, pero una vez que obtienes una idea de los varios
adaptadores de iterators y lo que hacen, los iterators pueden ser más fáciles de entender.
En lugar de manipular los varios bits de los loops y construir nuevos vectores, el código se
https://book.rustlang-es.org/print 373/656
12/7/24, 22:37 El Lenguaje de Programación Rust
enfoca en el objetivo de alto nivel del loop. Esto abstrae un poco del código común para que
sea más fácil ver los conceptos que son únicos a este código, como la condición de filtrado
que cada elemento en el iterator debe pasar.
¿Pero son las dos implementaciones realmente equivalentes? La suposición intuitiva podría
ser que el loop más bajo nivel será más rápido. Hablemos de performance.
https://book.rustlang-es.org/print 374/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La versión del iterator fue ligeramente más rápida! No explicaremos el código del
benchmark aquí, porque el punto no es probar que las dos versiones son equivalentes, sino
obtener una idea general de cómo estas dos implementaciones se comparan en términos
de performance.
Para un benchmark más completo, deberías verificar usando varios textos de varios
tamaños como el contents , diferentes palabras y palabras de diferentes longitudes como
el query , y todo tipo de otras variaciones. El punto es este: los iterators, aunque son una
abstracción de alto nivel, se compilan a aproximadamente el mismo código que si hubieras
escrito el código de más bajo nivel tú mismo. Los iterators son una de las abstracciones de
costo cero de Rust, por lo que queremos decir que el uso de la abstracción no impone
ningún costo adicional en tiempo de ejecución. Esto es análogo a cómo Bjarne Stroustrup, el
diseñador e implementador original de C++, define cero costo en “Foundations of C++”
(2012):
https://book.rustlang-es.org/print 375/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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 a través de cada uno de los 12
valores en coefficients y usa el método zip para emparejar los valores de los
coeficientes con los 12 valores anteriores en buffer . Luego, para cada par, multiplicamos
los valores juntos, sumamos todos los resultados y desplazamos los bits en la suma
qlp_shift bits a la derecha.
Todos los coeficientes se almacenan en registros, lo que significa que acceder a los valores
es muy rápido. No hay verificaciones de límites en el acceso al array en tiempo de ejecución.
Todas estas optimizaciones que Rust es capaz de aplicar hacen que el código resultante sea
extremadamente eficiente. Ahora que sabes esto, ¡puedes usar iterators y closures sin
miedo! Hacen que el código parezca de más alto nivel, pero no imponen una penalización
de performance en tiempo de ejecución por hacerlo.
Resumen
Los closures e iterators son características de Rust inspiradas en ideas de lenguajes de
programación funcionales. Contribuyen a la capacidad de Rust de expresar claramente
ideas de alto nivel a bajo nivel de performance. Las implementaciones de closures e
iterators son tales que el performance en tiempo de ejecución no se ve afectado. Esto es
parte de la meta de Rust de esforzarse por proveer abstracciones de costo cero.
https://book.rustlang-es.org/print 376/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cargo puede hacer aún más que la funcionalidad que cubrimos en este capítulo, así que
para una explicación completa de todas sus características, consulte la documentación.
https://book.rustlang-es.org/print 377/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cargo tiene dos perfiles principales: el perfil dev que Cargo usa cuando ejecutas cargo
build y el perfil release que Cargo usa cuando ejecutas cargo build --release . El perfil
dev está definido con buenos valores predeterminados para el desarrollo, y el perfil
release tiene buenos valores predeterminados para las compilaciones de lanzamiento.
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
$ cargo build --release
Finished release [optimized] target(s) in 0.0s
El perfil dev y release son estos perfiles diferentes utilizados por el compilador.
Cargo tiene valores predeterminados para cada uno de los perfiles que se aplican cuando
no has agregado explícitamente ninguna sección [profile.*] en el archivo Cargo.toml del
proyecto. Al agregar secciones [profile.*] para cualquier perfil que desees personalizar,
anularás cualquier subconjunto de los valores predeterminados. Por ejemplo, aquí están los
valores predeterminados para la configuración opt-level para los perfiles dev y release :
Filename: Cargo.toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
https://book.rustlang-es.org/print 378/656
12/7/24, 22:37 El Lenguaje de Programación Rust
se ejecuta más rápido. Es por eso que el opt-level predeterminado para el perfil release
es 3 .
Filename: Cargo.toml
[profile.dev]
opt-level = 1
https://book.rustlang-es.org/print 379/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Rust y Cargo tienen características que hacen que tu paquete publicado sea más fácil de
encontrar y usar. Hablaremos sobre algunas de estas características a continuación y luego
explicaremos cómo publicar un paquete.
Documentar adecuadamente tus paquetes ayudará a otros usuarios a saber cómo y cuándo
usarlos, por lo que vale la pena invertir el tiempo para escribir documentación. En el
Capítulo 3, discutimos cómo comentar el código Rust usando dos barras diagonales, // .
Rust también tiene un tipo particular de comentario para la documentación, conocido
convenientemente como un comentario de documentación, que generará documentación
HTML. El HTML muestra el contenido de los comentarios de documentación para los
elementos de API públicos destinados a programadores interesados en saber cómo usar tu
paquete en oposición a cómo se implementa tu paquete.
Los comentarios de documentación usan tres barras diagonales, /// , en lugar de dos y
admiten la notación Markdown para formatear el texto. Coloca los comentarios de
documentación justo antes del elemento que están documentando. El Listado 14-1 muestra
comentarios de documentación para una función add_one en un crate llamado my_crate .
Filename: src/lib.rs
https://book.rustlang-es.org/print 380/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Aquí, damos una descripción de lo que hace la función add_one , comenzamos una sección
con el encabezado Examples y luego proporcionamos código que demuestra cómo usar la
función add_one . Podemos generar la documentación HTML de este comentario de
documentación ejecutando cargo doc . Este comando ejecuta la herramienta rustdoc
distribuida con Rust y coloca la documentación HTML generada en el directorio target/doc.
Por conveniencia, ejecutar cargo doc --open generará el HTML para la documentación de
tu crate actual (así como la documentación para todas las dependencias de tu crate) y abrirá
el resultado en un navegador web. Navega hasta la función add_one y verás cómo se
renderiza el texto en los comentarios de documentación, como se muestra en la Figura 14-1:
Hemos usado el encabezado de Markdown # Examples en el Listado 14-1 para crear una
sección en el HTML con el título "Examples". Aquí hay algunas otras secciones que los
autores de crates comúnmente usan en su documentación:
Panics: Los escenarios en los que la función documentada podría entrar en panic. Los
llamadores de la función que no quieren que sus programas entren en panic deben
asegurarse de no llamar a la función en estas situaciones.
Errores: Si la función devuelve un Result , describir los tipos de errores que podrían
ocurrir y qué condiciones podrían hacer que esos errores se devuelvan puede ser útil
para los llamadores para que puedan escribir código para manejar los diferentes tipos
de errores de diferentes maneras.
https://book.rustlang-es.org/print 381/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Doc-tests my_crate
running 1 test
test src/lib.rs - add_one (line 5) ... ok
El estilo de comentario de doc //! agrega documentación al item que contiene los
comentarios en lugar de a los items que siguen a los comentarios. Normalmente, usamos
estos comentarios de documentación dentro del archivo raíz del crate (src/lib.rs por
convención) o dentro de un módulo para documentar el crate o el módulo en su conjunto.
Por ejemplo, para agregar documentación que describe el propósito del crate my_crate
que contiene la función add_one , agregamos comentarios de documentación que
comienzan con //! al principio del archivo src/lib.rs, como se muestra en el Listado 14-2:
https://book.rustlang-es.org/print 382/656
12/7/24, 22:37 El Lenguaje de Programación Rust
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
Observa que no hay ningún código después de la última línea que comienza con //! .
Debido a que comenzamos los comentarios con //! en lugar de /// , estamos
documentando el item que contiene este comentario en lugar de un item que sigue a este
comentario. En este caso, ese item es el archivo src/lib.rs, que es el crate root. Estos
comentarios describen todo el crate.
Cuando ejecutamos cargo doc --open ahora, veremos la documentación para el crate
my_crate en lugar de la documentación para la función add_one , como se muestra en la
Figura 14-2:
Figura 14-2: Documentación renderizada para my_crate , incluido el comentario que describe el crate como un
todo
Los comentarios de documentación dentro de los items son útiles para describir crates y
módulos en particular. Úsalos para explicar el propósito general del contenedor para
ayudar a tus usuarios a comprender la organización del crate.
https://book.rustlang-es.org/print 383/656
12/7/24, 22:37 El Lenguaje de Programación Rust
tener dificultades para encontrar las piezas que desean usar si tu crate tiene una gran
jerarquía de módulos.
En el Capítulo 7, cubrimos cómo hacer que los items sean públicos usando la palabra clave
pub y traer items a un scope con la palabra clave use . Sin embargo, la estructura que tiene
sentido para ti mientras desarrollas un crate puede que no sea muy conveniente para tus
usuarios. Es posible que desees organizar tus structs en una jerarquía que contenga varios
niveles, pero luego las personas que desean usar un tipo que has definido profundamente
en la jerarquía podrían tener problemas para descubrir que ese tipo existe. También
podrían estar molestos por tener que ingresar use
my_crate::some_module::another_module::UsefulType; en lugar de use
my_crate::UsefulType; .
Las buenas noticias son que si la estructura no es conveniente para que otros la usen desde
otra biblioteca, no tienes que reorganizar tu organización interna: en su lugar, puedes
reexportar items para hacer una estructura pública que sea diferente de tu estructura
privada usando pub use . Reexportar toma un item público en una ubicación y lo hace
público en otra ubicación, como si se definiera en la otra ubicación en su lugar.
Por ejemplo, supongamos que creamos una biblioteca llamada art para modelar
conceptos artísticos. Dentro de esta biblioteca hay dos módulos: un módulo kinds que
contiene dos enums llamados PrimaryColor y SecondaryColor y un módulo utils que
contiene una función llamada mix , como se muestra en el Listado 14-3:
https://book.rustlang-es.org/print 384/656
12/7/24, 22:37 El Lenguaje de Programación Rust
//! # Art
//!
//! A library for modeling artistic concepts.
Listado 14-3: Una biblioteca llamada art con items organizados en los módulos kinds y utils
La Figura 14-3 muestra cómo se vería la página frontal de la documentación para este crate
generada por cargo doc :
https://book.rustlang-es.org/print 385/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Figure 14-3: Página principal de la documentación de art que enumera los módulos kinds y utils
Nota que los tipos PrimaryColor y SecondaryColor no están listados en la página principal.
Tampoco lo está la función mix . Para verlos, tendríamos que hacer clic en kinds y utils .
Otro crate que depende de esta biblioteca necesitaría declarar un use que traigan los items
de art al scope, especificando la estructura de módulos actualmente definida. El Listado
14-4 muestra un ejemplo de un crate que usa los items PrimaryColor y mix del crate art :
use art::kinds::PrimaryColor;
use art::utils::mix;
fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
Listado 14-4: Un crate que utiliza los items del crate art con su estructura interna exportada
El autor del código en el Listado 14-4, que usa el crate art , tuvo que averiguar que
PrimaryColor está en el módulo kinds y mix está en el módulo utils . La estructura de
módulos del crate art es más relevante para los desarrolladores que trabajan en el crate
art que para aquellos que lo usan. La estructura interna no contiene ninguna información
útil para alguien que intenta comprender cómo usar el crate art , sino que causa confusión
porque los desarrolladores que lo usan tienen que averiguar dónde buscar y deben
especificar los nombres de módulo en las declaraciones use .
Para remover la estructura interna de la API pública, podemos modificar el código del crate
art en el Listado 14-3 para agregar declaraciones pub use para reexportar los items en el
nivel superior, como se muestra en el Listado 14-5:
https://book.rustlang-es.org/print 386/656
12/7/24, 22:37 El Lenguaje de Programación Rust
//! # Art
//!
//! A library for modeling artistic concepts.
La documentación de la API que cargo doc genera para este crate ahora listará y enlazará
los reexports en la página principal, como se muestra en la Figura 14-4, haciendo que los
tipos PrimaryColor y SecondaryColor y la función mix sean más fáciles de encontrar.
Figura 14-4: La página principal de la documentación para art que lista las re-exportaciones
Los usuarios del crate art aún pueden ver y usar la estructura interna del Listado 14-3
como se demuestra en el Listado 14-4, o pueden usar la estructura más conveniente del
https://book.rustlang-es.org/print 387/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use art::mix;
use art::PrimaryColor;
fn main() {
// --snip--
}
Listado 14-6: Un programa que utiliza los items reexportados del crate art
En casos donde hay muchos módulos anidados, reexportar los tipos en el nivel superior con
pub use puede hacer una diferencia significativa en la experiencia de las personas que
usan el crate. Otro uso común de pub use es reexportar definiciones de una dependencia
en el crate actual para hacer que las definiciones de ese crate sean parte de la API pública
de su crate.
Crear una estructura de API pública es más un arte que una ciencia, y puedes iterar para
encontrar la API que funcione mejor para tus usuarios. Elegir pub use te da flexibilidad en
cómo estructuras tu crate internamente y desacopla esa estructura interna de lo que
presentas a tus usuarios. Mira algo del código de los crates que has instalado para ver si su
estructura interna difiere de su API pública.
Antes de que puedas publicar cualquier crate, necesitas crear una cuenta en crates.io y
obtener un token de API. Para hacerlo, visita la página de inicio en crates.io e inicia sesión a
través de una cuenta de GitHub. (La cuenta de GitHub es actualmente un requisito, pero el
sitio podría admitir otras formas de crear una cuenta en el futuro). Una vez que hayas
iniciado sesión, visita la configuración de tu cuenta en https://crates.io/me/ y recupera tu
clave de API. Luego ejecuta el comando cargo login y pega tu clave de la API, cuando se
solicitad, como se muestra a continuación:
$ cargo login
abcdefghijklmnopqrstuvwxyz012345
https://book.rustlang-es.org/print 388/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Supongamos que tienes un crate que deseas publicar. Antes de publicarlo, necesitarás
agregar algunos metadatos en la sección [package] del archivo Cargo.toml del crate.
[package]
name = "guessing_game"
Incluso si has elegido un nombre único, cuando ejecutes cargo publish para publicar el
crate en este punto, obtendrás una advertencia y luego un error:
$ 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: missing or empty metadata fields:
description, license. Please see https://doc.rust-
lang.org/cargo/reference/manifest.html for how to upload metadata
Estos errores se deben a que te faltan algunos datos cruciales: se requiere una descripción y
una licencia para que las personas sepan qué hace tu crate y bajo qué términos pueden
usarlo. En Cargo.toml, agrega una descripción que sea solo una o dos oraciones, porque
aparecerá con tu crate en los resultados de búsqueda. Para el campo license , debes dar
un valor de identificador de licencia. La Linux Foundation’s Software Package Data Exchange
(SPDX) enumera los identificadores que puedes usar para este valor. Por ejemplo, para
especificar que has licenciado tu crate usando la Licencia MIT, agrega el identificador MIT :
[package]
name = "guessing_game"
license = "MIT"
https://book.rustlang-es.org/print 389/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Si tu deseas especificar una licencia que no aparece en el SPDX, necesitas colocar el texto de
esa licencia en un archivo, incluir el archivo en tu proyecto y luego usar license-file para
especificar el nombre de ese archivo en lugar de usar la key license .
La orientación sobre qué licencia es apropiada para tu proyecto está fuera del alcance de
este libro. Muchas personas en la comunidad de Rust licencian sus proyectos de la misma
manera que Rust, usando una licencia dual de MIT OR Apache-2.0 . Esta práctica demuestra
que también puedes especificar múltiples identificadores de licencia separados por OR para
tener múltiples licencias para tu proyecto.
Con un nombre único, la versión, una descripción y una licencia agregados, el archivo
Cargo.toml para un proyecto que está listo para publicar podría verse así:
[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]
Publicando en Crates.io
Ahora que has creado una cuenta, guardado tu token de API, elegido un nombre para tu
crate y especificado los metadatos requeridos, ¡estás listo para publicar! Publicar un crate
carga una versión específica en crates.io para que otros la usen.
Ten cuidado, porque una publicación es permanente. La versión nunca se puede sobrescribir
y el código no se puede eliminar. Uno de los principales objetivos de crates.io es actuar
como un archivo permanente de código para que las compilaciones de todos los proyectos
que dependen de crates de crates.io sigan funcionando. Permitir la eliminación de versiones
haría imposible cumplir ese objetivo. Sin embargo, no hay límite en la cantidad de versiones
de crate que puedes publicar.
Ejecuta el comando cargo publish otra vez. Esta vez, debería tener éxito:
https://book.rustlang-es.org/print 390/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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 [unoptimized + debuginfo] target(s) in 0.19s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
¡Felicidades! Ahora has compartido tu código con la comunidad de Rust y cualquiera puede
agregar tu crate como una dependencia de su proyecto.
Cuando hayas realizado cambios en tu crate y estés listo para publicar una nueva versión,
cambia el valor version especificado en tu archivo Cargo.toml y vuelve a publicar. Usa las
reglas de versionado semántico para decidir cuál es el siguiente número de versión
apropiado en función de los tipos de cambios que hayas realizado. Luego, ejecuta cargo
publish para cargar la nueva versión.
Aunque no puedes eliminar versiones anteriores de un crate, puedes evitar que cualquier
proyecto futuro las agregue como una nueva dependencia. Esto es útil cuando una versión
de crate está rota por una razón u otra. En tales situaciones, Cargo admite yanking una
versión de crate.
Hacer un yank a una versión impide que nuevos proyectos dependan de esa versión, pero
permite que todos los proyectos existentes que dependen de ella continúen. Esencialmente,
un yank significa que todos los proyectos con un Cargo.lock no se romperán y que cualquier
Cargo.lock futuro generado no usará la versión yanked.
Para hacer un yank de una versión de un crate, en el directorio del crate que has publicado
previamente, ejecuta cargo yank y especifica qué versión deseas yank. Por ejemplo, si
hemos publicado un crate llamado guessing_game versión 1.0.1 y queremos yank la
versión, en el directorio del proyecto para guessing_game ejecutaríamos:
Al agregar --undo al comando, también puedes deshacer un yank y permitir que los
proyectos vuelvan a depender de una versión:
https://book.rustlang-es.org/print 391/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Un yank no borra ningún código. No puede, por ejemplo, eliminar secretos cargados
accidentalmente. Si eso sucede, debes restablecer esos secretos inmediatamente.
https://book.rustlang-es.org/print 392/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cargo Workspaces
En el Capítulo 12, construimos un paquete que incluía un crate binario y un crate de
biblioteca. A medida que su proyecto se desarrolle, es posible que encuentre que el crate de
biblioteca continúa creciendo y que desea dividir su paquete aún más en múltiples crate de
biblioteca. Cargo ofrece una característica llamada workspaces que puede ayudar a
administrar múltiples paquetes relacionados que se desarrollan en tándem.
Creando un Workspace
$ mkdir add
$ cd add
Filename: Cargo.toml
[workspace]
members = [
"adder",
]
A continuación, crearemos el crate binario adder ejecutando cargo new dentro del
directorio add:
https://book.rustlang-es.org/print 393/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En este punto, podemos construir el workspace ejecutando cargo build . Los archivos en su
directorio add deberían verse así:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
El workspace tiene un directorio target en el nivel superior que contendrá los artefactos
compilados. El paquete adder no tiene su propio directorio target. Incluso si ejecutáramos
cargo build desde dentro del directorio adder, los artefactos compilados aún terminarían
en add/target en lugar de add/adder/target. Cargo estructura el directorio target en un
workspace de esta manera porque los crate en un workspace están destinados a dependerse
entre sí. Si cada crate tuviera su propio directorio target, cada crate tendría que volver a
compilar cada uno de los otros crate en el workspace para colocar los artefactos en su propio
directorio target. Al compartir un directorio target, los crate pueden evitar la reconstrucción
innecesaria.
Filename: Cargo.toml
[workspace]
members = [
"adder",
"add_one",
]
https://book.rustlang-es.org/print 394/656
12/7/24, 22:37 El Lenguaje de Programación Rust
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
Filename: add_one/src/lib.rs
Ahora podemos hacer que el paquete adder con nuestro binario dependa del paquete
add_one con nuestra biblioteca. Primero, necesitaremos agregar una dependencia de ruta
en adder/Cargo.toml.
Filename: adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
Cargo no asume que los crates en un workspace dependerán entre sí, por lo que
necesitamos ser explícitos sobre las relaciones de dependencia.
A continuación, usaremos la función add_one (del crate add_one ) en el crate adder . Abra el
archivo adder/src/main.rs y agregue una línea use en la parte superior para traer el nuevo
crate de biblioteca add_one al alcance. Luego cambie la función main para llamar a la
función add_one , como en el Listado 14-7.
Filename: adder/src/main.rs
use add_one;
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
https://book.rustlang-es.org/print 395/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.68s
Para ejecutar el crate binario desde el directorio add, podemos especificar qué paquete en
el workspace queremos ejecutar con el argumento -p y el nombre del paquete con cargo
run :
Observa que el workspace tiene solo un archivo Cargo.lock en el nivel superior, en lugar de
tener un Cargo.lock en cada directorio de crate. Esto asegura que todos los crate estén
usando la misma versión de todas las dependencias. Si agregamos el paquete rand al
Cargo.toml de adder y add_one, Cargo resolverá ambos a una versión de rand y lo registrará
en el único Cargo.lock. Hacer que todos los crate en el workspace usen las mismas
dependencias significa que los crate siempre serán compatibles entre sí. Agreguemos el
crate rand a la sección [dependencies] en el archivo add_one/Cargo.toml para que
podamos usar el crate rand en el crate add_one :
Filename: add_one/Cargo.toml
[dependencies]
rand = "0.8.5"
https://book.rustlang-es.org/print 396/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo build
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
$ 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`
Para solucionar esto, edita el archivo Cargo.toml del paquete adder e indica que rand es
una dependencia para él también. Construir el paquete adder agregará rand a la lista de
dependencias para adder en Cargo.lock, pero no se descargarán copias adicionales de
rand . Cargo se asegurara de que cada crate en cada paquete en el workspace que usa el
paquete rand estará usando la misma versión siempre y cuando se especifiquen como
versiones compatibles de rand , ahorrándonos espacio y asegurando que los crate en el
workspace serán compatibles entre sí.
Para otra mejora, agreguemos una prueba de la función add_one::add_one dentro del crate
add_one :
Filename: add_one/src/lib.rs
https://book.rustlang-es.org/print 397/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(3, add_one(2));
}
}
Ahora ejecutamos cargo test en el directorio superior add para ejecutar los tests una
estructura de workspace como esta ejecutará los tests para todos los crates en el
workspace:
$ cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.27s
Running unittests src/lib.rs (target/debug/deps/add_one-f0253159197f7841)
running 1 test
test tests::it_works ... ok
running 0 tests
Doc-tests add_one
running 0 tests
La primera sección del output muestra que el test it_works en el crate add_one pasó. La
siguiente sección muestra que no se encontraron tests en el crate adder , y luego la última
sección muestra que no se encontraron tests de documentación en el crate add_one .
https://book.rustlang-es.org/print 398/656
12/7/24, 22:37 El Lenguaje de Programación Rust
running 1 test
test tests::it_works ... ok
Doc-tests add_one
running 0 tests
Este output muestra que cargo test solo ejecutó los tests para el crate add_one y no
ejecutó los tests del crate adder .
Para practicar aún más, agrega un crate add_two a este workspace de manera similar al
crate add_one !
https://book.rustlang-es.org/print 399/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Todos los binarios instalados con cargo install se almacenan en la carpeta raíz de
instalación de bin. Si instalaste Rust usando rustup.rs y no tienes configuraciones
personalizadas, este directorio será $HOME/.cargo/bin. Asegúrese de que el directorio de
instalación esté en su $PATH para poder ejecutar los programas que ha instalado con cargo
install .
Por ejemplo, en el Capítulo 12, mencionamos que hay una implementación de Rust de la
herramienta grep llamada ripgrep para buscar archivos. Para instalar ripgrep , podemos
ejecutar lo siguiente:
La penúltima línea de la salida muestra la ubicación y el nombre del binario instalado, que
en el caso de ripgrep es rg . Mientras el directorio de instalación esté en su $PATH , como
se mencionó anteriormente, puede ejecutar rg --help y comenzar a usar una herramienta
más rápida y oxidada para buscar archivos!
https://book.rustlang-es.org/print 400/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Resumen
Compartir código con Cargo y crates.io es parte de lo que hace que el ecosistema de Rust
sea útil para muchas tareas diferentes. La biblioteca estándar de Rust es pequeña y estable,
pero los crates son fáciles de compartir, usar y mejorar en una línea de tiempo diferente a la
del lenguaje. ¡No seas tímido al compartir código que te sea útil en crates.io; es probable
que también sea útil para otra persona!
https://book.rustlang-es.org/print 401/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Smart Pointers
Un puntero es un concepto general para una variable que contiene una dirección en
memoria. Esta dirección se refiere, o “apunta a,” algún otro dato. El tipo más común de
puntero en Rust es una referencia, la cual aprendiste en el Capítulo 4. Las referencias son
indicadas por el símbolo & y toman prestado el valor al que apuntan. No tienen ninguna
capacidad especial más allá de referirse a datos, y no tienen sobrecarga.
Los Smart pointers, por otro lado, son estructuras de datos que actúan como un puntero,
pero también tienen metadatos y capacidades adicionales. El concepto de smart pointers no
es único de Rust: los smart pointers se originaron en C++ y existen en otros lenguajes
también. Rust tiene una variedad de smart pointers definidos en la biblioteca estándar que
proveen funcionalidad más allá de la proveída por las referencias. Para explorar el concepto
general, veremos un par de ejemplos diferentes de smart pointers, incluyendo un tipo de
puntero reference counting. Este puntero te permite permitir que los datos tengan múltiples
propietarios al mantener un registro del número de propietarios y, cuando no hay
propietarios restantes, limpiar los datos.
Rust, con su concepto de propiedad y préstamo, tiene una diferencia adicional entre
referencias y smart pointers: mientras que las referencias solo toman prestado los datos, en
muchos casos, los smart pointers son dueños de los datos a los que apuntan.
Aunque no los llamamos así en ese momento, ya hemos encontrado algunos smart pointers
en este libro, incluyendo String y Vec<T> en el Capítulo 8. Ambos estos tipos cuentan
como smart pointers porque poseen algo de memoria y te permiten manipularla. También
tienen metadatos y capacidades o garantías adicionales. String , por ejemplo, almacena su
capacidad como metadato y tiene la capacidad adicional de asegurar que sus datos siempre
serán UTF-8 válidos.
Dado que el patrón de smart pointer es un patrón de diseño general usado frecuentemente
en Rust, este capítulo no cubrirá todos los smart pointers existentes. Muchas bibliotecas
tienen sus propios smart pointers, e incluso puedes escribir los tuyos. Cubriremos los smart
pointers más comunes en la biblioteca estándar:
https://book.rustlang-es.org/print 402/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Además, cubriremos el patrón interior mutability donde un tipo inmutable expone una API
para mutar un valor interior. También discutiremos reference cycles: cómo pueden fugar
memoria y cómo prevenirlos.
https://book.rustlang-es.org/print 403/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los boxes no tienen overhead de performance, más allá de almacenar sus datos en el heap
en lugar del stack. Pero tampoco tienen muchas capacidades adicionales. Los usarás más
frecuentemente en estas situaciones:
Cuando tienes un tipo cuyo tamaño no puede ser conocido en tiempo de compilación
y quieres usar un valor de ese tipo en un contexto que requiere un tamaño exacto
Cuando tienes una gran cantidad de datos y quieres transferir el ownership, pero
asegurarte de que los datos no serán copiados cuando hagas eso
Cuando quieres ser dueño de un valor y solo te importa que sea un tipo que
implemente un trait en particular, en lugar de ser de un tipo específico
El Listado 15-1 muestra cómo usar un box para almacenar un valor i32 en el heap:
Filename: src/main.rs
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
https://book.rustlang-es.org/print 404/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Declaramos una variable b para tener el valor de un Box que apunta al valor 5 , el cual
está almacenado en el heap. Este programa imprimirá b = 5 ; en este caso, podemos
acceder a los datos en el box de forma similar a como lo haríamos en el stack. Como
cualquier valor owned, cuando un box sale del scope, como b hace al final de main , será
desasignado. La desasignación ocurre tanto para el box (almacenado en el stack) como para
los datos a los que apunta (almacenados en el heap).
Colocar un solo valor en el heap no es muy útil, así que no usarás boxes por sí solos de esta
forma muy seguido. Tener valores como un solo i32 en el stack, donde son almacenados
por defecto, es más apropiado en la mayoría de situaciones. Veamos un caso donde los
boxes nos permiten definir tipos que no podríamos definir si no tuviéramos boxes.
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, el anidamiento de valores de tipos
recursivos podría teóricamente continuar infinitamente, así que Rust no puede saber cuánto
espacio necesita el valor. Como los boxes tienen un tamaño conocido, podemos habilitar
tipos recursivos insertando un box en la definición del tipo recursivo.
Como ejemplo de un tipo recursivo, exploremos la cons list. Este es un tipo de dato
comúnmente encontrado en lenguajes de programación funcionales. El tipo de cons list que
definiremos es sencillo excepto por la recursión; por lo tanto, los conceptos en el ejemplo
con el que trabajaremos serán útiles en cualquier situación más compleja que involucre
tipos recursivos.
Una cons list es una estructura de datos que viene del lenguaje de programación Lisp y sus
dialectos y está compuesta de pares anidados, y es la versión de Lisp de una lista enlazada.
Su nombre viene de la función cons (abreviatura de “construct function” o “función de
construcción”) en Lisp que construye un nuevo par a partir de sus dos argumentos. Al llamar
cons en un par que consiste en un valor y otro par, podemos construir cons lists hechas de
pares recursivos.
Por ejemplo, aquí tenemos una representación de pseudocódigo de una cons list que
contiene la lista 1, 2, 3 con cada par en paréntesis:
Cada item en una cons list contiene dos elementos: el valor del item actual y el siguiente
item. El último item en la lista contiene solo un valor llamado Nil sin un siguiente item. Una
cons list es producida llamando recursivamente la función cons . El nombre canónico para
https://book.rustlang-es.org/print 405/656
12/7/24, 22:37 El Lenguaje de Programación Rust
denotar el caso base de la recursión es Nil . Nota que esto no es lo mismo que el concepto
de “null” o “nil” en el Capítulo 6, el cual es un valor inválido o ausente.
La cons list no es un tipo de dato que se use comúnmente en Rust. La mayoría de las veces
cuando tienes una lista de items en Rust, Vec<T> es una mejor opción. Otros tipos de datos
recursivos más complejos son útiles en varias situaciones, pero al comenzar con la cons list
en este capítulo, podemos explorar cómo los boxes nos permiten definir un tipo de dato
recursivo sin mucha distracción.
El Listado 15-2 contiene una definición de enum para una cons list. Nota que este código no
compilará aún porque el tipo List no tiene un tamaño conocido, lo cual demostraremos.
Filename: src/main.rs
enum List {
Cons(i32, List),
Nil,
}
Listado 15-2: El primer intento de definir un enum para representar una estructura de datos de lista de cons
con valores i32
Nota: Estamos implementando una const list que solo contiene valores i32 con el
propósito de este ejemplo. Podríamos haberla implementado usando genéricos, como
discutimos en el Capítulo 10, para definir un tipo de cons list que pueda almacenar
valores de cualquier tipo.
Usando el tipo List para almacenar la lista 1, 2, 3 se vería como el código en el Listado
15-3:
Filename: src/main.rs
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
El primer valor Cons contiene 1 y otro valor List . Este valor List es otro valor Cons que
contiene 2 y otro valor List . Este valor List es otro valor Cons que contiene 3 y un
valor List , que es finalmente es Nil , la variante no recursiva que señala el final de la lista.
https://book.rustlang-es.org/print 406/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
El error muestra que este tipo “tiene un tamaño infinito”. La razón es que hemos definido
List con una variante que es recursiva: contiene otro valor de sí mismo directamente.
Como resultado, Rust no puede averiguar cuánto espacio necesita para almacenar un valor
de List . Veamos por qué obtenemos este error. Primero, veremos cómo Rust decide
cuánto espacio necesita para almacenar un valor de un tipo no recursivo.
Recuerda el enum Message que definimos en el Listado 6-2 cuando discutimos definiciones
de enum en el Capítulo 6:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Para determinar cuánto espacio necesita almacenar un valor de tipo Message , Rust
comienza con la variante que necesita la mayor cantidad de espacio. Rust observa que
https://book.rustlang-es.org/print 407/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Compara esto con lo que sucede cuando Rust intenta determinar cuánto espacio necesita
un valor de un tipo recursivo como el enum List en el Listado 15-2. El compilador
comienza mirando la variante Cons , la cual contiene un valor de tipo i32 y un valor de tipo
List . Por lo tanto, Cons necesita una cantidad de espacio igual al tamaño de un i32 más
el tamaño de un List . Para averiguar cuánto espacio necesita el tipo List , el compilador
mira las variantes, comenzando con la variante Cons . La variante Cons contiene un valor
de tipo i32 y un valor de tipo List , y este proceso continúa infinitamente, como se
muestra en la Figura 15-1.
Cons
Cons
Cons
i32 Cons
i32
i32 Cons
i32
i32 ∞
Debido a que Rust no puede determinar cuánto espacio necesita asignar para tipos
definidos recursivamente, el compilador muestra un error con una sugerencia util:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
Debido a que Box<T> es un tipo de puntero, Rust siempre sabe cuánto espacio necesita un
Box<T> : el tamaño de un puntero no cambia en función de la cantidad de datos que está
almacenando. Esto significa que podemos poner un Box<T> dentro de la variante Cons en
lugar de otro valor List directamente. El Box<T> apuntará al siguiente valor List que
https://book.rustlang-es.org/print 408/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Podemos cambiar la definición del enum List en el Listado 15-2 y el uso de List en el
Listado 15-3 al código del Listado 15-5, el cual compilará:
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Listado 15-5: Definición de List que utiliza Box<T> para tener un tamaño conocido
La variante Cons necesita el tamaño de un i32 más el espacio para almacenar los datos
del puntero. La variante Nil no almacena ningún valor, por lo que necesita menos espacio
que la variante Cons . Ahora sabemos que cualquier valor de List ocupará el tamaño de
un i32 más el tamaño de los datos del puntero de un Box. Al usar un Box, hemos roto la
cadena infinita y recursiva, por lo que el compilador puede averiguar el tamaño que
necesita para almacenar un valor de List . La Figura 15-2 muestra cómo se ve la variante
Cons ahora.
Cons
Box
i32
usize
Figura 15-2: Una List que no tiene un tamaño infinito porque Cons contiene una Box
https://book.rustlang-es.org/print 409/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los Boxes proporcionan indirección y asignación de heap; no tienen ninguna otra capacidad
especial, como veremos con los otros tipos de smart pointers. Tampoco tienen la
sobrecarga de rendimiento que estas capacidades especiales incurran, por lo que pueden
ser útiles en casos como la lista cons donde la indirección es la única característica que
necesitamos. También veremos más casos de uso para los boxes en el Capítulo 17.
El tipo Box<T> es un tipo de puntero inteligente porque implementa el trait Deref , que
permite que los valores de Box<T> se traten como referencias normales. Cuando un
Box<T> sale del scope, los datos en el heap que apunta se limpian también porque
implementa el trait Drop . Estos dos traits serán aún más importantes para la funcionalidad
proporcionada por los otros tipos de smart pointers que discutiremos en el resto de este
capítulo. Exploraremos estos dos traits en más detalle.
https://book.rustlang-es.org/print 410/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Nota: Hay una gran diferencia entre el tipo MyBox<T> que estamos a punto de
construir y el tipo Box<T> real: nuestra versión no almacenará sus datos en el heap.
Nos estamos enfocando en este ejemplo en Deref , por lo que dónde se almacenan
los datos es menos importante que el comportamiento similar al de un puntero.
Una referencia regular es un tipo de puntero, y una forma de pensar en un puntero es como
una flecha a un valor almacenado en otro lugar. En el Listado 15-6, creamos una referencia
a un valor i32 y luego usamos el operador de desreferencia para seguir la referencia al
valor:
Filename: src/main.rs
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
Listing 15-6: Utilizando el operador de dereferencia para seguir una referencia a un valor i32
https://book.rustlang-es.org/print 411/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1
previous error
Comparar un número y una referencia a un número no está permitido porque son tipos
diferentes. Debemos usar el operador de desreferencia para seguir la referencia al valor al
que apunta.
Podemos reescribir el código del Listado 15-6 para usar un Box<T> en lugar de una
referencia; el operador de desreferencia usado en el Box<T> en el Listado 15-7 funciona de
la misma manera que el operador de desreferencia usado en la referencia en el Listado 15-
6:
Filename: src/main.rs
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
https://book.rustlang-es.org/print 412/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La principal diferencia entre el Listado 15-7 y el Listado 15-6 es que aquí definimos y como
una instancia de Box<T> apuntando a una copia del valor de x en lugar de ser una
referencia que apunta al valor de x . En la última afirmación, podemos usar el operador de
desreferencia para seguir el puntero del Box<T> de la misma manera que lo hicimos
cuando y era una referencia. A continuación, exploraremos que es lo especial de Box<T>
que nos permite el uso del operador de desreferencia al definir nuestro propio tipo.
El tipo Box<T> es finalmente definido como una tupla struct con un elemento, por lo que el
Listado 15-8 define un tipo MyBox<T> de la misma manera. También definiremos una
función new para que coincida con la función new definida en Box<T> .
Filename: src/main.rs
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
Vamos a intentar añadir la función main del Listado 15-7 al Listado 15-8 y cambiarla para
usar el tipo MyBox<T> que hemos definido en lugar de Box<T> . El código en el Listado 15-9
no se compilará porque Rust no sabe cómo desreferenciar MyBox .
Filename: src/main.rs
https://book.rustlang-es.org/print 413/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Listing 15-9: Intentando usar MyBox<T> de la misma manera en que usamos referencias y Box<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
Nuestro tipo MyBox<T> no puede ser desreferenciado porque no hemos implementado esa
capacidad en nuestro tipo. Para habilitar la desreferencia con el operador * ,
implementamos el trait Deref .
Filename: src/main.rs
use std::ops::Deref;
https://book.rustlang-es.org/print 414/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La sintaxis type Target = T; define un tipo asociado que será utilizado por el trait Deref .
Los tipos asociados son una forma ligeramente diferente de declarar un parámetro
genérico, pero no necesitas preocuparte por ellos por ahora; los cubriremos con más detalle
en el Capítulo 19.
Rellenamos el cuerpo del método deref con &self.0 para que deref devuelva una
referencia al valor al que queremos acceder con el operador * . Recordemos de la sección
“Usando Tuplas Structs sin Campos Nombrados para Crear Diferentes Tipos” del Capítulo 5
que .0 accede al primer valor en una tupla struct. ¡La función main en el Listado 15-9 que
llama a * en el valor MyBox<T> ahora compila, y las afirmaciones pasan!
Sin el trait Deref , el compilador no sabe cómo desreferenciar referencias & . El método
deref le da al compilador la capacidad de tomar un valor de cualquier tipo que implemente
Deref y llamar al método deref para obtener una referencia & que sabe cómo
desreferenciar.
*(y.deref())
Rust sustituye el operador * con una llamada al método deref , y luego realiza una
desreferenciación directa, por lo que no tenemos que pensar si necesitamos llamar al
método deref . Esta característica de Rust nos permite escribir código que funciona de
manera idéntica si tenemos una referencia regular o un tipo que implementa Deref .
La razón por la cual el método deref devuelve una referencia a un valor, y por qué la
desreferenciación simple fuera de los paréntesis en *(y.deref()) todavía es necesaria,
tiene que ver con el sistema de propiedad. Si el método deref devolviera el valor
directamente en lugar de una referencia al valor, el valor se movería fuera de self . No
queremos tomar posesión del valor interno dentro de MyBox<T> en este caso o en la
mayoría de los casos en los que usamos el operador de desreferencia.
Nota que el operador * es reemplazado con una llamada al método deref y luego una
llamada al operador * solo una vez, cada vez que usamos un * en nuestro código. Debido
a que la sustitución del operador * no se repite infinitamente, terminamos con datos de
tipo i32 , que coincide con el 5 en assert_eq! en el Listado 15-9.
La coerción Deref convierte una referencia a un tipo que implementa el trait Deref en una
referencia a otro tipo. Por ejemplo, la coerción Deref puede convertir &String en &str
porque String implementa el trait Deref de manera que devuelve &str . La coerción
Deref es una conveniencia que Rust realiza en los argumentos de las funciones y métodos, y
https://book.rustlang-es.org/print 415/656
12/7/24, 22:37 El Lenguaje de Programación Rust
solo funciona en tipos que implementan el trait Deref . Sucede automáticamente cuando
pasamos una referencia al valor de un tipo particular como argumento a una función o
método que no coincide con el tipo de parámetro en la definición de la función o método.
Una secuencia de llamadas al método deref convierte el tipo que proporcionamos en el
tipo que necesita el parámetro.
La coerción Deref se agregó a Rust para que los programadores que escriben llamadas a
funciones y métodos no necesiten agregar tantas referencias y desreferencias explícitas con
& y * . La característica de coerción Deref también nos permite escribir más código que
puede funcionar para referencias o smart pointers.
Para ver la coerción Deref en acción, usemos el tipo MyBox<T> que definimos en el Listado
15-8 y la implementación de Deref que agregamos en el Listado 15-10. El Listado 15-11
muestra la definición de una función que tiene un parámetro de tipo string slice:
Filename: src/main.rs
fn hello(name: &str) {
println!("Hello, {name}!");
}
Listing 15-11: Una función hello que tiene el parámetro name de tipo &str
Filename: src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
Listing 15-12: Llamando a hello con una referencia a un valor MyBox<String> , lo cual funciona debido a la
coerción deref
Aquí estamos llamando a la función hello con el argumento &m , que es una referencia a
un valor MyBox<String> . Debido a que implementamos el trait Deref en MyBox<T> en el
Listado 15-10, Rust puede convertir &MyBox<String> en &String llamando a deref . La
biblioteca estándar proporciona una implementación de Deref en String que devuelve
una cadena de texto, y esto está en la documentación de la API de Deref . Rust llama a
deref nuevamente para convertir el &String en &str , que coincide con la definición de la
función hello .
https://book.rustlang-es.org/print 416/656
12/7/24, 22:37 El Lenguaje de Programación Rust
&MyBox<String> .
Filename: src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
Listing 15-13: El código que tendríamos que escribir si Rust no tuviera deref coerción
Cuando el trait Deref está definido para el tipo involucrado, Rust analizará los tipos y usará
Deref::deref tantas veces como sea necesario para obtener una referencia que coincida
con el tipo del parámetro. El número de veces que Deref::deref necesita ser insertado se
resuelve en tiempo de compilación, por lo que no hay penalización en tiempo de ejecución
por aprovechar la coerción Deref!
Similar a cómo usas el trait Deref para anular el operador * en referencias inmutables,
puedes usar el trait DerefMut para anular el operador * en referencias mutables.
Rust realiza la coerción Deref cuando encuentra tipos e implementaciones de traits en tres
casos:
Los dos primeros casos son iguales entre sí, excepto que el segundo implementa
mutabilidad. El primer caso establece que si tienes un &T , y T implementa Deref a algún
tipo U , puedes obtener un &U de forma transparente. El segundo caso establece que la
misma coerción de desreferencia ocurre para referencias mutables.
El tercer caso es más complicado. Rust también convertirá una referencia mutable en una
inmutable. Pero lo contrario no es posible: una referencia inmutable nunca se puede
convertir en una referencia mutable. Debido a las reglas de borrowing, si tienes una
referencia mutable, esa referencia debe ser la única referencia a ese dato (de lo contrario, el
programa no se compilaría). Convertir una referencia mutable a una inmutable nunca
romperá las reglas de borrowing. Convertir una referencia inmutable a una mutable
https://book.rustlang-es.org/print 417/656
12/7/24, 22:37 El Lenguaje de Programación Rust
requeriría que la referencia inmutable inicial sea la única referencia inmutable a esos datos,
pero las reglas de borrowing no garantizan eso. Por lo tanto, Rust no puede hacer la
suposición de que convertir una referencia inmutable en una mutable es posible.
https://book.rustlang-es.org/print 418/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En algunos lenguajes, para algunos tipos, el programador debe llamar código para liberar
memoria o recursos cada vez que terminan de usar una instancia de esos tipos. Ejemplos
incluyen manejadores de archivos, sockets, o locks. Si se olvidan, el sistema podría
sobrecargarse y colapsar. En Rust, puedes especificar que un pedazo particular de código
sea ejecutado cada vez que un valor sale del scope, y el compilador insertará este código
automáticamente. Como resultado, no necesitas ser cuidadoso sobre colocar código de
limpieza en todos lados en un programa que una instancia de un tipo particular está
terminada con él—¡aún no se fugarán recursos!
Puedes especificar el código a ejecutar cuando un valor sale del scope implementando el
trait Drop . El trait Drop requiere que implementes un método llamado drop que toma una
referencia mutable a self . Para ver cuándo Rust llama a drop , implementemos drop con
declaraciones println! por ahora.
Filename: src/main.rs
https://book.rustlang-es.org/print 419/656
12/7/24, 22:37 El Lenguaje de Programación Rust
struct CustomSmartPointer {
data: String,
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}
Listing 15-14: Un struct CustomSmartPointer que implementa el trait Drop donde colocaríamos nuestro código
de limpieza
El trait Drop está incluido en el prelude, así que no necesitamos traerlo al scope.
Implementamos el trait Drop en CustomSmartPointer y proveemos una implementación
para el método drop que llama a println! . El cuerpo de la función drop es donde
colocarías cualquier lógica que quisieras correr cuando una instancia de tu tipo sale del
scope. Estamos imprimiendo un texto aquí para demostrar visualmente cuándo Rust
llamará a drop .
$ 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`!
Rust automáticamente llamó a drop para nosotros cuando nuestras instancias salieron del
scope, llamando al código que especificamos. Las variables son dropeadas en el orden
inverso a su creación, así que d fue dropeada antes que c . El propósito de este ejemplo es
https://book.rustlang-es.org/print 420/656
12/7/24, 22:37 El Lenguaje de Programación Rust
darte una guía visual de cómo funciona el método drop ; usualmente especificarías el
código de limpieza que tu tipo necesita correr en lugar de un mensaje de impresión.
Si intentamos llamar manualmente al método drop del trait Drop modificando la función
main del Listado 15-14, como se muestra en el Listado 15-15, obtendremos un error del
compilador:
Filename: src/main.rs
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
c.drop();
println!("CustomSmartPointer dropped before the end of main.");
}
Listing 15-15: Intento de llamar manualmente al método drop del trait Drop para limpiar de forma anticipada
$ 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
https://book.rustlang-es.org/print 421/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Este mensaje de error indica que no se nos permite llamar a drop explícitamente. El
mensaje de error usa el término destructor, que es el término general de programación para
una función que limpia una instancia. Un destructor es análogo a un constructor, que crea
una instancia. La función drop en Rust es un destructor particular.
Rust no nos deja llamar a drop explícitamente porque Rust llamaría automáticamente a
drop en el valor al final de main . Esto causaría un error de double free porque Rust
intentaría limpiar el mismo valor dos veces.
No podemos desactivar la inserción automática de drop cuando un valor sale del scope, y
no podemos llamar explícitamente al método drop . Así que, si necesitamos forzar a un
valor a ser limpiado temprano, usamos la función std::mem::drop .
Filename: src/main.rs
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
Listing 15-16: Llamando a std::mem::drop para eliminar explícitamente un valor antes de que salga del scope
$ 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.
El texto Dropping CustomSmartPointer with data `some data`! es impreso entre el texto
CustomSmartPointer created. y CustomSmartPointer dropped before the end of main. ,
mostrando que el código del método drop es llamado para dropear c en ese punto.
Puedes utilizar código especificado en una implementación del trait Drop de varias
maneras para hacer la limpieza conveniente y segura: por ejemplo, ¡podrías usarlo para
crear tu propio allocator de memoria! Con el trait Drop y el sistema de ownership de Rust,
no tienes que recordar limpiar porque Rust lo hace automáticamente.
https://book.rustlang-es.org/print 422/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Tampoco tienes que preocuparte por problemas que surjan de limpiar accidentalmente
valores que aún están en uso: el sistema de ownership que asegura que las referencias
siempre sean válidas también asegura que drop sea llamado solo una vez cuando el valor
ya no está siendo usado.
Ahora que hemos examinado Box<T> y algunas de las características de los smart pointers,
veamos algunos otros smart pointers definidos en la librería estándar.
https://book.rustlang-es.org/print 423/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Debes habilitar el ownership múltiple explícitamente usando el tipo de Rust Rc<T> , el cual
es una abreviación para reference counting. El tipo Rc<T> lleva la cuenta del número de
referencias a un valor para determinar si el valor aún está en uso. Si hay cero referencias a
un valor, el valor puede ser limpiado sin que ninguna referencia se vuelva inválida.
Imagina Rc<T> como una TV en una sala familiar. Cuando una persona entra a ver TV, la
enciende. Otros pueden entrar a la sala y ver la TV. Cuando la última persona sale de la sala,
apaga la TV porque ya no está siendo usada. Si alguien apaga la TV mientras otros aún la
están viendo, ¡habría un alboroto de los televidentes restantes!
Usamos el tipo Rc<T> cuando queremos asignar algunos datos en el heap para que
múltiples partes de nuestro programa puedan leer y no podemos determinar en tiempo de
compilación cuál parte terminará usando los datos por último. Si supiéramos cuál parte
terminaría de último, podríamos hacer que esa parte sea dueña de los datos, y las reglas
normales de ownership aplicadas en tiempo de compilación tomarían efecto.
Nota que Rc<T> es solo para uso en escenarios de un solo hilo. Cuando discutamos
concurrencia en el Capítulo 16, cubriremos cómo hacer conteo de referencias en programas
multihilo.
Volvamos a nuestro ejemplo de la lista cons en el Listado 15-5. Recuerda que lo definimos
usando Box<T> . Esta vez, crearemos dos listas que comparten ownership de una tercera
lista. Conceptualmente, esto se ve similar a la Figura 15-3:
https://book.rustlang-es.org/print 424/656
12/7/24, 22:37 El Lenguaje de Programación Rust
b 3
a 5 10 Nil
c 4
Crearemos la lista a que contiene 5 y luego 10. Luego haremos dos listas más: b que
comienza con 3 y c que comienza con 4. Ambas listas b y c luego continuarán a la
primera lista a que contiene 5 y 10. En otras palabras, ambas listas compartirán la primera
lista que contiene 5 y 10.
Intentar implementar este escenario usando nuestra definición de List con Box<T> no
funcionará, como se muestra en el Listado 15-17:
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
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: Demostrando que no se nos permite tener dos listas que usen Box<T> y traten de compartir
https://book.rustlang-es.org/print 425/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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 variantes Cons poseen los datos que contienen, así que cuando creamos la lista b , a
es movida a b y b posee a . Luego, cuando intentamos usar a nuevamente cuando
creamos c , no se nos permite porque a ha sido movida.
Podríamos cambiar la definición de Cons para que contenga referencias en su lugar, pero
entonces tendríamos que especificar parámetros de lifetime. Al especificar parámetros de
lifetime, estaríamos especificando que cada elemento en la lista vivirá al menos tanto como
la lista entera. Este es el caso para los elementos y listas en el Listado 15-17, pero no en
todos los escenarios.
En su lugar, cambiaremos nuestra definición de List para usar Rc<T> en lugar de Box<T> ,
como se muestra en el Listado 15-18. Cada variante Cons ahora contendrá un valor y un
Rc<T> apuntando a una List . Cuando creamos b , en lugar de tomar ownership de a ,
clonaremos el Rc<List> que a está sosteniendo, aumentando así el número de
referencias de uno a dos y permitiendo que a y b compartan ownership de los datos en
ese Rc<List> . También clonaremos a cuando creamos c , aumentando el número de
referencias de dos a tres. Cada vez que llamamos a Rc::clone , el conteo de referencias a
los datos dentro del Rc<List> aumentará, y los datos no serán limpiados a menos que no
haya referencias a él.
https://book.rustlang-es.org/print 426/656
12/7/24, 22:37 El Lenguaje de Programación Rust
enum List {
Cons(i32, Rc<List>),
Nil,
}
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));
}
Necesitamos agregar una declaración use para traer Rc<T> al scope porque no está en el
prelude. En main , creamos la lista que contiene 5 y 10 y la almacenamos en un nuevo
Rc<List> en a . Luego cuando creamos b y c , llamamos a la función Rc::clone y
pasamos una referencia al Rc<List> en a como argumento.
Vamos a modificar nuestro ejemplo de trabajo en el Listado 15-18 para que podamos ver
los conteos de referencias cambiando a medida que creamos y descartamos referencias al
Rc<List> en a .
En el Listado 15-19, cambiaremos main para que tenga un scope interno alrededor de la
lista c ; luego podemos ver cómo el conteo de referencias cambia cuando c sale del scope.
https://book.rustlang-es.org/print 427/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&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
Podemos ver que el Rc<List> en a tiene un recuento de referencias de 1; luego, cada vez
que llamamos a clone el recuento aumenta en 1. Cuando c sale del scope, el recuento
disminuye en 1. No tenemos que llamar a una función para disminuir el recuento de
referencias como tenemos que llamar a Rc::clone para aumentar el recuento de
referencias: la implementación del trait Drop disminuye el recuento de referencias
automáticamente cuando un valor Rc<T> sale del scope.
Lo que no podemos ver en este ejemplo es que cuando b y luego a salen del scope al final
de main , el recuento es entonces 0, y el Rc<List> se limpia completamente. Usando
Rc<T> permite que un solo valor tenga múltiples propietarios, y el recuento asegura que el
valor permanezca válido siempre que cualquiera de los propietarios aún exista.
A través de referencias inmutables, Rc<T> permite que comparta datos entre múltiples
partes de su programa para lectura solamente. Si Rc<T> le permitiera tener múltiples
referencias mutables también, podría violar una de las reglas de préstamo discutidas en el
Capítulo 4: múltiples préstamos mutables al mismo lugar pueden causar carreras de datos e
inconsistencias. ¡Pero poder mutar datos es muy útil! En la siguiente sección, discutiremos el
https://book.rustlang-es.org/print 428/656
12/7/24, 22:37 El Lenguaje de Programación Rust
patrón de mutabilidad interior y el tipo RefCell<T> que puede usar en conjunto con un
Rc<T> para trabajar con esta restricción de inmutabilidad.
https://book.rustlang-es.org/print 429/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Podemos utilizar tipos que utilizan el patrón de mutabilidad interna solo cuando podemos
asegurar que las reglas de borrowing se seguirán en tiempo de ejecución, aunque el
compilador no pueda garantizarlo. El código unsafe involucrado se envuelve entonces en
una API segura, y el tipo externo sigue siendo inmutable.
Vamos a explorar este concepto al examinar el tipo RefCell<T> que sigue el patrón de
mutabilidad interna.
A diferencia de Rc<T> , el tipo RefCell<T> representa un único ownership sobre los datos
que contiene. Entonces, ¿qué hace que RefCell<T> sea diferente de un tipo como Box<T> ?
Recuerda las reglas de borrowing que aprendiste en el Capítulo 4:
En cualquier momento dado, puedes tener o bien una referencia mutable o bien
cualquier número de referencias inmutables.
Las referencias siempre deben ser válidas.
Con referencias y Box<T> , las invariantes de las reglas de borrowing se hacen cumplir en
tiempo de compilación. Con RefCell<T> , estas invariantes se hacen cumplir en tiempo de
ejecución. Con referencias, si rompes estas reglas, obtendrás un error de compilación. Con
RefCell<T> , si rompes estas reglas, tu programa entrará en panic y saldrá.
https://book.rustlang-es.org/print 430/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Debido a que algunos análisis son imposibles, si el compilador de Rust no puede estar
seguro de que el código cumple con las reglas de ownership, podría rechazar un programa
correcto; de esta manera, es conservador. Si Rust aceptara un programa incorrecto, los
usuarios no podrían confiar en las garantías que Rust hace. Sin embargo, si Rust rechaza un
programa correcto, el programador se verá perjudicado, pero no puede ocurrir nada
catastrófico. El tipo RefCell<T> es útil cuando estás seguro de que tu código sigue las
reglas de borrowing, pero el compilador no puede entenderlo y garantizarlo.
Similar a Rc<T> , RefCell<T> solo se usa en escenarios de un solo hilo y te dará un error de
tiempo de compilación si intentas usarlo en un contexto multihilo. Hablaremos de cómo
obtener la funcionalidad de RefCell<T> en un programa multihilo en el Capítulo 16.
Aquí tienes un resumen de las razones para elegir Box<T> , Rc<T> o RefCell<T> :
Una consecuencia de las reglas de borrowing es que cuando tienes un valor inmutable, no
puedes pedir prestado una referencia mutable a través de ese valor. Por ejemplo, este
código no compilará:
fn main() {
let x = 5;
let y = &mut x;
}
https://book.rustlang-es.org/print 431/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
Sin embargo, hay situaciones en las que sería útil que un valor se mute a sí mismo en sus
métodos, pero parezca inmutable para otro código. El código fuera de los métodos del valor
no podría mutar el valor. Usar RefCell<T> es una forma de obtener la capacidad de tener
mutabilidad interior, pero RefCell<T> no evita las reglas de borrowing por completo: el
comprobador de préstamos en el compilador permite esta mutabilidad interior, y las reglas
de borrowing se comprueban en tiempo de ejecución en su lugar. Si violas las reglas,
obtendrás un panic! en lugar de un error del compilador.
Vamos a trabajar a través de un ejemplo práctico donde podemos usar RefCell<T> para
mutar un valor inmutable y ver por qué es útil.
A veces durante el testing, un programador usará un tipo en lugar de otro para observar un
comportamiento particular y afirmar que se implementa correctamente. Este tipo de
marcador de posición se llama test double. Piensa en ello en el sentido de un "doble de
riesgo" en la realización de películas, donde una persona entra y sustituye a un actor para
hacer una escena particularmente difícil. Los test doubles se sustituyen por otros tipos
cuando se ejecutan las pruebas. Los objetos simulados son tipos específicos de test doubles
que registran lo que sucede durante una prueba para que puedas afirmar que se
produjeron las acciones correctas.
Rust no tiene objetos en el mismo sentido que otros lenguajes tienen objetos, y Rust no
tiene funcionalidad de objetos simulados integrada en la biblioteca estándar como lo hacen
otros lenguajes. Sin embargo, definitivamente puedes crear una struct que sirva para los
mismos propósitos que un objeto simulado.
Aquí está el escenario que vamos a probar: crearemos una biblioteca que realiza un
seguimiento de un valor en relación con un valor máximo, y envía mensajes en función de la
proximidad del valor actual al valor máximo. Esta biblioteca podría usarse para realizar un
seguimiento de la cuota de un usuario para el número de llamadas a la API que se le
permite realizar, por ejemplo.
https://book.rustlang-es.org/print 432/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
Listing 15-20: Una biblioteca para realizar un seguimiento de qué tan cerca está un valor a su valor máximo y
emitir advertencias cuando el valor alcanza ciertos niveles.
https://book.rustlang-es.org/print 433/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Una parte importante de este código es el trait Messenger , que tiene un método llamado
send que toma una referencia inmutable a self y el texto del mensaje. Este trait es la
interfaz que nuestro objeto simulado necesita implementar para que el simulado se pueda
usar de la misma manera que un objeto real. La otra parte importante es que queremos
probar el comportamiento del método set_value en el LimitTracker . Podemos cambiar
lo que pasamos para el parámetro value , pero set_value no devuelve nada para que
podamos hacer afirmaciones. Queremos poder decir que si creamos un LimitTracker con
algo que implemente el trait Messenger y un valor particular para max , cuando pasemos
diferentes números para value , se le dice al mensajero que envíe los mensajes apropiados.
Filename: src/lib.rs
https://book.rustlang-es.org/print 434/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[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);
}
}
Listing 15-21: Un intento de implementar un MockMessenger que no es permitido por el borrow checker
Este código de test define un struct MockMessenger que tiene un campo sent_messages
que es una Vec de String valores. Definimos una función asociada new para que sea
conveniente crear nuevos valores MockMessenger que comiencen con una lista vacía de
mensajes. Luego implementamos el trait Messenger para MockMessenger para que
podamos darle un MockMessenger a un LimitTracker . En la definición del método send ,
tomamos el mensaje pasado como parámetro y lo almacenamos en la lista MockMessenger
de sent_messages .
En el test, estamos testeando qué sucede cuando el LimitTracker se le dice que establezca
value en algo que es más del 75 por ciento del valor max . En primer lugar, creamos un
nuevo MockMessenger , que comenzará con una lista vacía de mensajes. Luego creamos un
nuevo LimitTracker y le damos una referencia al nuevo MockMessenger y un valor max de
100. Llamamos al método set_value en el LimitTracker con un valor de 80, que es más
del 75 por ciento de 100. Luego afirmamos que la lista de mensajes que el MockMessenger
está realizando un seguimiento debería tener ahora un mensaje en ella.
Sin embargo, hay un problema con este test, como se muestra aquí:
https://book.rustlang-es.org/print 435/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
|
2 | fn send(&mut self, msg: &str);
| ~~~~~~~~~
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
Esta es una situación en la que la mutabilidad interior puede ayudar. Almacenaremos los
sent_messages dentro de un RefCell<T> , y luego el método send podrá modificar
sent_messages para almacenar los mensajes que hemos visto. El listado 15-22 muestra
cómo se ve eso:
Filename: src/lib.rs
https://book.rustlang-es.org/print 436/656
12/7/24, 22:37 El Lenguaje de Programación Rust
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
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);
}
}
Listing 15-22: Usando RefCell<T> para mutar un valor interno mientras el valor externo se considera
inmutable.
La última modificación que debemos hacer está en la afirmación: para ver cuántos
elementos hay en el vector interno, llamamos a borrow en el RefCell<Vec<String>> para
obtener una referencia inmutable al vector.
Ahora que has visto cómo usar RefCell<T> , ¡profundicemos en cómo funciona!
https://book.rustlang-es.org/print 437/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Si intentamos romper estas reglas, en lugar de obtener un error del compilador como lo
haríamos con las referencias, la implementación de RefCell<T> se bloqueará en tiempo de
ejecución. El listado 15-23 muestra una modificación de la implementación de send en el
listado 15-22. Estamos tratando deliberadamente de crear dos borrowing mutables activos
para el mismo scope para ilustrar que RefCell<T> nos impide hacer esto en tiempo de
ejecución.
Filename: src/lib.rs
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
Listing 15-23: Creando dos referencias mutables en el mismo scope para ver que RefCell<T> lanzará un panic
Creamos una variable one_borrow para el smart pointer RefMut<T> devuelto desde
borrow_mut . Luego creamos otro borrowing mutable de la misma manera en la variable
two_borrow . Esto hace dos referencias mutables en el mismo scope, lo cual no está
permitido. Cuando ejecutamos los tests para nuestra librería, el código en el listado 15-23 se
compilará sin errores, pero el test fallará:
https://book.rustlang-es.org/print 438/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
Observa que el código entró en panic con el mensaje already borrowed: BorrowMutError .
Así es como RefCell<T> maneja las violaciones de las reglas de borrowing en tiempo de
ejecución.
Una forma común de usar RefCell<T> es en combinación con Rc<T> . Recuerda que Rc<T>
te permite tener múltiples propietarios de algunos datos, pero solo te da acceso inmutable
a esos datos. Si tienes un Rc<T> que contiene un RefCell<T> , puedes obtener un valor que
puede tener múltiples propietarios y que puedes mutar.
https://book.rustlang-es.org/print 439/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Por ejemplo, recuerda el ejemplo de la lista de cons en el Listado 15-18 donde usamos
Rc<T> para permitir que múltiples listas compartan propiedad de otra lista. Debido a que
Rc<T> contiene solo valores inmutables, no podemos cambiar ninguno de los valores en la
lista una vez que los hemos creado. Agreguemos RefCell<T> para obtener la capacidad de
cambiar los valores en las listas. El listado 15-24 muestra que al usar un RefCell<T> en la
definición de Cons , podemos modificar el valor almacenado en todas las listas:
Filename: src/main.rs
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
fn main() {
let value = Rc::new(RefCell::new(5));
*value.borrow_mut() += 10;
Listing 15-24: Usando Rc<RefCell<i32>> para crear una List que podemos modificar.
Envolvemos la lista a en un Rc<T> para que cuando creemos las listas b y c , ambas
puedan referirse a a , que es lo que hicimos en el listado 15-18.
https://book.rustlang-es.org/print 440/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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))
¡Esta técnica es bastante genial! Al usar RefCell<T> , tenemos un valor List externamente
inmutable. Pero podemos usar los métodos en RefCell<T> que proporcionan acceso a su
mutabilidad interior para que podamos modificar nuestros datos cuando sea necesario. Las
comprobaciones en tiempo de ejecución de las reglas de borrowing nos protegen de las
condiciones de carrera en los datos y, a veces, vale la pena intercambiar un poco de
velocidad por esta flexibilidad en nuestras estructuras de datos. ¡Ten en cuenta que
RefCell<T> no funciona para código multihilo! Mutex<T> es la versión segura para hilos de
RefCell<T> y discutiremos Mutex<T> en el capítulo 16.
https://book.rustlang-es.org/print 441/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Vamos a ver cómo podría ocurrir una referencia circular y cómo prevenirla, comenzando
con la definición del enum List y un método tail en el Listado 15-25:
Filename: src/main.rs
#[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() {}
Listing 15-25: Una definición de lista enlazada que contiene un RefCell<T> para poder modificar a que se
Estamos usando otra variación de la definición de List del Listado 15-5. El segundo
elemento en la variante Cons es ahora RefCell<Rc<List>> , lo que significa que en lugar de
tener la capacidad de modificar el valor i32 como lo hicimos en el Listado 15-24, queremos
https://book.rustlang-es.org/print 442/656
12/7/24, 22:37 El Lenguaje de Programación Rust
modificar el valor List al que una variante Cons está apuntando. También estamos
agregando un método tail para que sea conveniente para nosotros acceder al segundo
elemento si tenemos una variante Cons .
En el Listado 15-26, estamos agregando una función main que usa las definiciones en el
Listado 15-25. Este código crea una lista en a y una lista en b que apunta a la lista en a .
Luego modifica la lista en a para que apunte a b , creando un ciclo de referencia. Hay
declaraciones println! a lo largo del camino para mostrar cuáles son los recuentos de
referencia en varios puntos de este proceso.
Filename: src/main.rs
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
Listing 15-26: Creando un ciclo de referencia de dos valores List que se apuntan mutuamente.
Creamos una instancia Rc<List> que contiene un valor List en la variable a con una lista
inicial de 5, Nil . Luego creamos una instancia Rc<List> que contiene otro valor List en
la variable b que contiene el valor 10 y apunta a la lista en a .
Modificamos a para que apunte a b en lugar de Nil , creando un ciclo. Hacemos eso
usando el método tail para obtener una referencia al RefCell<Rc<List>> en a , que
ponemos en la variable link . Luego usamos el método borrow_mut en el
RefCell<Rc<List>> para cambiar el valor interno de un Rc<List> que contiene un valor
Nil al Rc<List> en b .
https://book.rustlang-es.org/print 443/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
a
5 10
En comparación con un programa del mundo real, las consecuencias de crear un ciclo de
referencia en este ejemplo no son muy graves: justo después de crear el ciclo de referencia,
el programa termina. Sin embargo, si un programa más complejo asignara mucha memoria
en un ciclo y la mantuviera durante mucho tiempo, el programa usaría más memoria de la
https://book.rustlang-es.org/print 444/656
12/7/24, 22:37 El Lenguaje de Programación Rust
que necesitaba y podría abrumar el sistema, causando que se quede sin memoria
disponible.
Crear ciclos de referencia no es algo fácil de hacer, pero tampoco es imposible. Si tienes
valores RefCell<T> que contienen valores Rc<T> o combinaciones similares de tipos con
mutabilidad interior y recuento de referencias anidados, debes asegurarte de no crear
ciclos; no puedes confiar en Rust para atraparlos. Crear un ciclo de referencia sería un error
de lógica en tu programa que deberías usar pruebas automatizadas, revisiones de código y
otras prácticas de desarrollo de software para minimizar.
Otra solución para evitar ciclos de referencia es reorganizar tus estructuras de datos para
que algunas referencias expresen propiedad y algunas referencias no expresen ownership.
Como resultado, puedes tener ciclos compuestos por algunas relaciones de ownership y
algunas relaciones de no ownership, y solo las relaciones de ownership afectan si un valor
puede ser descartado. En el Listado 15-25, siempre queremos que las variantes Cons
posean su lista, por lo que no es posible reorganizar la estructura de datos. Veamos un
ejemplo usando gráficos compuestos por nodos padres y nodos hijos para ver cuándo las
relaciones de no ownership son una forma apropiada de evitar ciclos de referencia.
Hasta ahora, hemos demostrado que llamar a Rc::clone aumenta el strong_count de una
instancia Rc<T> , y una instancia Rc<T> solo se limpia si su strong_count es 0. También
puedes crear una referencia débil al valor dentro de una instancia Rc<T> llamando a
Rc::downgrade y pasando una referencia a la Rc<T> . Las referencias fuertes son cómo
puedes compartir el ownership de una instancia Rc<T> . Las referencias débiles no expresan
una relación de ownership, y su recuento no afecta cuándo se limpia una instancia Rc<T> .
No causarán un ciclo de referencia porque cualquier ciclo que involucre algunas referencias
débiles se romperá una vez que el recuento de referencias fuertes de los valores
involucrados sea 0.
Dado que el valor al que apunta Weak<T> puede haber sido eliminado, para hacer cualquier
cosa con el valor al que apunta un Weak<T> , debes asegurarte de que el valor aún exista.
Haz esto llamando al método upgrade en una instancia Weak<T> , que devolverá un
Option<Rc<T>> . Si el valor Rc<T> aún no se ha eliminado, upgrade devolverá Some , y si el
valor Rc<T> se ha eliminado, upgrade devolverá None . Porque upgrade devuelve un
https://book.rustlang-es.org/print 445/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Option<Rc<T>> , Rust se asegurará de que se manejen los casos Some y None , y no habrá
un puntero no válido.
Como ejemplo, en lugar de usar una lista cuyos elementos solo conocen al siguiente
elemento, crearemos un árbol cuyos elementos conocen a sus elementos hijos y a sus
elementos padres.
Para comenzar, construiremos un árbol con nodos que conocen a sus nodos hijos.
Crearemos una estructura llamada Node que contenga su propio valor i32 así como
referencias a sus nodos hijos Node :
Filename: src/main.rs
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
Queremos que un Node sea propietario de sus hijos, y queremos compartir ese ownership
con variables para que podamos acceder a cada Node en el árbol directamente. Para hacer
esto, definimos los elementos Vec<T> para ser valores de tipo Rc<Node> . También
queremos modificar qué nodos son hijos de otro nodo, por lo que tenemos un RefCell<T>
en children alrededor del Vec<Rc<Node>> .
Filename: src/main.rs
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
Listing 15-27: Creando un nodo leaf sin hijos y un nodo branch con leaf como uno de sus hijos
https://book.rustlang-es.org/print 446/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para hacer que el nodo hijo sea consciente de su padre, necesitamos agregar un campo
parent a nuestra definición de struct Node . El problema está en decidir qué tipo de
parent debería ser. Sabemos que no puede contener un Rc<T> , porque eso crearía un
ciclo de referencia con leaf.parent apuntando a branch y branch.children apuntando a
leaf , lo que haría que sus valores strong_count nunca fueran 0.
Si pensamos en las relaciones de otra manera, un nodo padre debería ser propietario de
sus nodos hijos: si se elimina un nodo padre, sus nodos 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 aún debería existir. ¡Este es un caso para las referencias débiles!
Entonces en lugar de Rc<T> , usaremos Weak<T> como tipo de dato para parent ,
específicamente RefCell<Weak<Node>> . Ahora nuestra definición de struct Node se ve así:
Filename: src/main.rs
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á referirse a su nodo padre, pero no será propietario de él. En el Listado 15-
28, actualizamos main para usar esta nueva definición, por lo que el nodo leaf tendrá una
forma de referirse a su nodo padre, branch :
Filename: src/main.rs
https://book.rustlang-es.org/print 447/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
Listing 15-28: Un nodo leaf con una referencia débil a su nodo padre branch
La creación del nodo leaf se ve similar al Listado 15-27 con la excepción del campo
parent : leaf comienza sin un padre, por lo que creamos una nueva instancia de
referencia Weak<Node> vacía.
En este punto, cuando intentamos obtener una referencia al padre de leaf usando el
método upgrade , obtenemos un valor None . Vemos esto en el output de la primera
instrucción println! :
Cuando creamos el nodo branch , también tendrá una nueva referencia Weak<Node> en el
campo parent , porque branch no tiene un nodo padre. Todavía tenemos leaf como uno
de los hijos de branch . Una vez que tenemos la instancia Node en branch , podemos
modificar leaf para darle una referencia Weak<Node> a su padre. Usamos el método
borrow_mut en el RefCell<Weak<Node>> en el campo parent de leaf , y luego usamos la
función Rc::downgrade para crear una referencia Weak<Node> a branch desde el
Rc<Node> en branch .
Cuando imprimimos el padre de leaf nuevamente, esta vez obtendremos una variante
Some que contiene branch : ¡ahora leaf puede acceder a su padre! Cuando imprimimos
leaf , también evitamos el ciclo que eventualmente terminó en un desbordamiento de pila
como teníamos en el Listado 15-26; las referencias Weak<Node> se imprimen como (Weak) :
https://book.rustlang-es.org/print 448/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La falta de output infinito indica que este código no creó un ciclo de referencia. También
podemos decir esto mirando los valores que obtenemos al llamar a Rc::strong_count y
Rc::weak_count .
Veamos cómo cambian los valores strong_count y weak_count de las instancias Rc<Node>
al crear y modificar branch . El Listado 15-29 muestra el código que usamos para crear
branch en un nuevo scope interno y examinar los valores de referencia strong_count y
weak_count . Al hacerlo, podemos ver qué sucede cuando se crea branch y luego se elimina
cuando sale del scope. Las modificaciones se muestran en el Listado 15-29:
Filename: src/main.rs
https://book.rustlang-es.org/print 449/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
{
let branch = Rc::new(Node {
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),
);
}
Listing 15-29: Creando branch en un scope interno y examinando los recuentos de referencias fuertes y débiles
https://book.rustlang-es.org/print 450/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cuando el scope interno termina, branch sale del scope y el recuento fuerte del Rc<Node>
en branch se reduce a 0, por lo que su Node se elimina. El recuento débil de 1 de
leaf.parent no tiene ninguna consecuencia sobre si se elimina o no Node , ¡así que no
obtenemos fugas de memoria!
Si intentamos acceder al padre de leaf después del final del scope, obtendremos None
nuevamente. Al final del programa, el Rc<Node> en leaf tiene un recuento fuerte de 1 y un
recuento débil de 0, porque la variable leaf es nuevamente la única referencia al
Rc<Node> .
Toda la lógica que gestiona los recuentos y la eliminación de valores está incorporada en
Rc<T> y Weak<T> y sus implementaciones del trait Drop . Al especificar que la relación de
un hijo con su padre debe ser una referencia Weak<T> en la definición de Node , puede
tener nodos padres que apunten a nodos hijos y viceversa sin crear un ciclo de referencia y
fugas de memoria.
Resumen
Este capítulo cubrió cómo usar smart pointers para hacer diferentes garantías y
compensaciones de las que Rust hace de forma predeterminada con referencias regulares.
El tipo Box<T> tiene un tamaño conocido y apunta a datos asignados en el heap. El tipo
Rc<T> realiza un seguimiento del número de referencias a los datos en el heap para que los
datos puedan tener múltiples propietarios. El tipo RefCell<T> con su mutabilidad interior
nos da un tipo que podemos usar cuando necesitamos un tipo inmutable, pero necesitamos
cambiar un valor interno de ese tipo; también hace cumplir las reglas de borrowing en
tiempo de ejecución en lugar de en tiempo de compilación.
También se discutieron los traits Deref , Drop ,que habilitan gran parte de la funcionalidad
de los smart pointers. Exploramos los ciclos de referencia que pueden causar fugas de
memoria y cómo prevenirlos usando Weak<T> .
Si este capítulo ha despertado tu interés y quieres implementar tus propios smart pointers,
consulta “The Rustonomicon” para obtener más información útil.
https://book.rustlang-es.org/print 451/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Nota: Para simplificar, nos referiremos a muchos de los problemas como concurrentes
en lugar de ser más precisos al decir concurrentes y/o paralelos. Si este libro tratara
sobre concurrencia y/o paralelismo, seríamos más específicos. Para este capítulo, por
favor sustituya mentalmente concurrente y/o paralelo cada vez que usemos
concurrente.
Muchos lenguajes son dogmáticos sobre las soluciones que ofrecen para manejar
problemas concurrentes. Por ejemplo, Erlang tiene una funcionalidad elegante para la
concurrencia de paso de mensajes, pero solo tiene formas oscuras de compartir estado
entre hilos. Soportar solo un subconjunto de soluciones posibles es una estrategia
razonable para los lenguajes de más alto nivel, porque un lenguaje de más alto nivel
promete beneficios al renunciar a cierto control para obtener abstracciones. Sin embargo,
se espera que los lenguajes de nivel inferior 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 una variedad de herramientas para modelar problemas de la
manera que sea apropiada para su situación y requisitos.
https://book.rustlang-es.org/print 452/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Cómo crear hilos para ejecutar múltiples piezas de código al mismo tiempo
Message-passing concurrencia, donde los canales envían mensajes entre hilos
Shared-state concurrencia, donde múltiples hilos tienen acceso a alguna pieza de datos
Los traits Sync y Send , que extienden las garantías de concurrencia de Rust a los
tipos definidos por el usuario, así como a los tipos proporcionados por la biblioteca
estándar
https://book.rustlang-es.org/print 453/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Race conditions, donde los hilos están accediendo a datos o recursos en un orden
inconsistente
Deadlocks, donde dos hilos están esperando el uno al otro, evitando que ambos hilos
continúen
Bugs que ocurren solo en ciertas situaciones y son difíciles de reproducir y arreglar de
manera confiable
Rust intenta mitigar los efectos negativos de usar hilos, pero la programación en un
contexto multihilo aún requiere un pensamiento cuidadoso y requiere una estructura de
código que sea diferente de la de los programas que se ejecutan en un solo hilo.
Filename: src/main.rs
https://book.rustlang-es.org/print 454/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::thread;
use std::time::Duration;
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));
}
}
Listing 16-1: Creando un nuevo hilo para imprimir una cosa mientras el hilo principal imprime algo más
Nota que cuando el hilo principal de un programa Rust se completa, todos los hilos creados
se apagan, independientemente de si han terminado de ejecutarse o no. La salida de este
programa podría ser un poco diferente cada vez, pero se verá similar a lo siguiente:
Las llamadas a thread::sleep fuerzan a un hilo a detener su ejecución durante una corta
duración, permitiendo que se ejecute un hilo diferente. Los hilos probablemente se
turnarán, pero eso no está garantizado: depende de cómo su sistema operativo programe
los hilos. En esta ejecución, el hilo principal imprimió primero, a pesar de que la instrucción
de impresión del hilo creado aparece primero en el código. Y aunque le dijimos al hilo
creado que imprimiera hasta que i sea 9, solo llegó a 5 antes de que el hilo principal se
apagara.
Si ejecutas este código y solo ves el output del hilo principal, o no ves ninguna
superposición, intenta aumentar los números en los rangos para crear más oportunidades
para que el sistema operativo cambie entre los hilos.
El código en el Listado 16-1 no solo detiene el hilo creado prematuramente la mayoría de las
veces debido a que el hilo principal termina, sino que debido a que no hay garantía sobre el
https://book.rustlang-es.org/print 455/656
12/7/24, 22:37 El Lenguaje de Programación Rust
orden en que se ejecutan los hilos, ¡tampoco podemos garantizar que el hilo creado se
ejecute en absoluto!
Filename: src/main.rs
use std::thread;
use std::time::Duration;
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));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
Listing 16-2: Guardando un JoinHandle devuelto por thread::spawn para garantizar que el hilo se ejecute
hasta completarse
Llamar a join en el handle bloquea el hilo que está actualmente en ejecución hasta que el
hilo representado por el handle termine. Bloquear un hilo significa que ese hilo se impide
realizar un trabajo o salir. Debido a que hemos puesto la llamada a join después del bucle
for del hilo principal, ejecutar el Listado 16-2 debería producir una salida similar a esta:
https://book.rustlang-es.org/print 456/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los dos hilos continúan alternándose, pero el hilo principal espera debido a la llamada a
handle.join() y no termina hasta que el hilo creado haya terminado.
Pero veamos que sucede cuando movemos la llamada a handle.join() antes del bucle
for en main , como esto:
Filename: src/main.rs
use std::thread;
use std::time::Duration;
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();
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
El hilo principal ahora espera a que el hilo creado termine antes de comenzar su bucle for ,
para que el output no se intercale más. La salida ahora se verá así:
https://book.rustlang-es.org/print 457/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Pequeños detalles, como dónde se llama a join , pueden afectar si sus hilos se ejecutan al
mismo tiempo.
Filename: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
handle.join().unwrap();
}
Listing 16-3: Intentando usar un vector creado por el hilo principal en otro hilo
https://book.rustlang-es.org/print 458/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El closure usa v , por lo que capturará v y lo hará parte del entorno del closure. Debido a
que thread::spawn ejecuta este closure en un nuevo hilo, deberíamos poder acceder a v
dentro de ese nuevo hilo. Pero cuando compilamos este ejemplo, obtenemos el siguiente
error:
$ 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
Rust infiere cómo capturar v , y porque println! solo necesita una referencia a v , el
closure intenta pedir prestado v . Sin embargo, hay un problema: Rust no puede decir
cuánto tiempo se ejecutará el hilo creado, por lo que no sabe si la referencia a v siempre
será válida.
El Listado 16-4 proporciona un escenario que es más probable que tenga una referencia a
v que no sea válida:
Filename: src/main.rs
https://book.rustlang-es.org/print 459/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::thread;
fn main() {
let v = vec![1, 2, 3];
drop(v); // oh no!
handle.join().unwrap();
}
Listing 16-4: Un hilo con un closure que intenta capturar una referencia a v desde un hilo principal que deja de
tener v
Si Rust nos permitiera ejecutar este código, existe la posibilidad de que el hilo creado se
ponga inmediatamente en segundo plano sin ejecutarse en absoluto. El hilo creado tiene
una referencia a v dentro, pero el hilo principal inmediatamente deja caer v , usando la
función drop que discutimos en el Capítulo 15. Luego, cuando el hilo creado comienza a
ejecutarse, v ya no es válido, por lo que una referencia a él también es inválida. ¡Oh no!
Para solucionar el error en el Listado 16-3, podemos seguir el consejo 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 || {
| ++++
Al agregar la keyword move antes del closure, forzamos al closure a tomar ownership de los
valores que está usando en lugar de permitir que Rust infiera que debería pedir prestado
los valores. La modificación al Listado 16-3 que se muestra en el Listado 16-5 se compilará y
ejecutará como lo pretendemos:
Filename: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
handle.join().unwrap();
}
Listing 16-5: Usando la keyword move para forzar a un closure a tomar ownership de los valores que utiliza
https://book.rustlang-es.org/print 460/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Podríamos sentir la tentación de intentar lo mismo para arreglar el código en el Listado 16-4
donde el hilo principal llamó a drop usando un closure move . Sin embargo, esta solución
no funcionará porque lo que el Listado 16-4 está intentando hacer está prohibido por una
razón diferente. Si agregáramos move al closure, moveríamos v al entorno del closure, y ya
no podríamos llamar a drop en 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 ownership de Rust nos han salvado de nuevo! Obtenemos un error del código
en el Listado 16-3 porque Rust es conservador y solo pide prestado v para el hilo, lo que
significa que el hilo principal podría teóricamente invalidar la referencia del hilo creado. Al
decirle a Rust que mueva la propiedad de v al hilo creado, le garantizamos a Rust que el
hilo principal no usará v nunca más. Si cambiamos el Listado 16-4 de la misma manera,
entonces estamos violando las reglas de ownership cuando intentamos usar v en el hilo
principal. La keyword move anula la conservadora predeterminada de Rust de pedir
prestado; no nos permite violar las reglas de ownership.
Con una comprensión básica de los hilos y la API de hilos, veamos qué podemos hacer con
los hilos.
https://book.rustlang-es.org/print 461/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Aquí, iremos desarrollando un programa que tiene un hilo para generar valores y enviarlos
por un canal, y otro hilo que recibirá los valores e imprimirá por pantalla. Enviaremos
valores simples entre hilos usando un canal para ilustrar la característica. Una vez que esté
familiarizado con la técnica, podría usar canales para cualquier hilo que necesite
comunicarse entre sí, como un sistema de chat o un sistema donde muchos hilos realizan
partes de un cálculo y envían las partes a un hilo que agrega los resultados.
Primero, en el Listado 16-6, crearemos un canal pero no haremos nada con él. Tenga en
cuenta que esto aún no se compilará porque Rust no puede determinar qué tipo de valores
queremos enviar por el canal.
Filename: src/main.rs
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
https://book.rustlang-es.org/print 462/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Movamos el extremo de envío a un hilo generado y hagamos que envíe un string para que
el hilo generado se comunique con el hilo principal, como se muestra en el Listado 16-7.
Esto es como poner un patito de goma en el río aguas arriba o enviar un mensaje de chat de
un hilo a otro.
Filename: src/main.rs
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();
});
}
Nuevamente, estamos usando thread::spawn para crear un nuevo hilo y luego usando
move para mover tx al cierre para que el hilo generado posea tx . El hilo generado
necesita poseer el transmisor para poder enviar mensajes a través del canal. El transmisor
tiene un método send que toma el valor que queremos enviar. El método send devuelve
un tipo Result<T, E> , por lo que si el receptor se ha eliminado y no hay ningún lugar para
enviar un valor, la operación de envío devolverá un error. En este ejemplo, estamos
llamando a unwrap para que se produzca un pánico en caso de error. Pero en una
aplicación real, lo manejaríamos correctamente: vuelva al Capítulo 9 para revisar las
estrategias para el manejo adecuado de errores.
https://book.rustlang-es.org/print 463/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En el Listado 16-8, recibiremos el valor enviado en el hilo principal. Esto es como recibir el
patito de goma en el río aguas abajo o recibir un mensaje de chat.
Filename: src/main.rs
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();
});
El receptor tiene dos métodos útiles: recv y try_recv . Estamos usando recv , abreviatura
de receive (recibir), que bloqueará la ejecución del hilo principal y esperará hasta que se
envíe un valor por el canal. Una vez que se envía un valor, recv lo devolverá en un
Result<T, E> . Cuando el transmisor se cierra, recv devolverá un error para indicar que no
se enviarán más valores.
Hemos usado recv en este ejemplo por simplicidad; no tenemos otro trabajo para que
haga el hilo principal que esperar mensajes, por lo que bloquear el hilo principal es
apropiado.
Cuando ejecutamos el código en el Listado 16-8, veremos el valor impreso desde el hilo
principal:
Got: hi
¡Perfecto!
https://book.rustlang-es.org/print 464/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Las reglas de ownership juegan un papel vital en el envío de mensajes porque ayudan a
escribir código concurrente seguro. Prevenir errores en la programación concurrente es la
ventaja de pensar en el ownership en todos sus programas Rust. Hagamos un experimento
para mostrar cómo los canales y el ownership funcionan juntos para evitar problemas:
intentaremos usar un valor val en el hilo generado después de haberlo enviado por el
canal. Intente compilar el código en el Listado 16-9 para ver por qué este código no está
permitido:
Filename: src/main.rs
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}");
});
Listing 16-9: Attempting to use val after we’ve sent it down the channel
Aquí, intentamos imprimir val después de haberlo enviado por el canal a través de
tx.send . Permitir esto sería una mala idea: una vez que el valor se ha enviado a otro hilo,
ese hilo podría modificarlo o eliminarlo antes de que intentemos usar el valor nuevamente.
Potencialmente, las modificaciones de otro hilo podrían causar errores o resultados
inesperados debido a datos inconsistentes o inexistentes. Sin embargo, Rust nos da un
error si intentamos compilar el código en el Listado 16-9:
https://book.rustlang-es.org/print 465/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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)
help: consider cloning the value if the performance cost is acceptable
|
9 | tx.send(val.clone()).unwrap();
| ++++++++
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 en el Listado 16-8 compiló y se ejecutó, pero no nos mostró claramente que dos
hilos separados estaban hablando entre sí a través del canal. En el Listado 16-10 hemos
realizado algunas modificaciones que demostrarán que el código en el Listado 16-8 se está
ejecutando concurrentemente: el hilo generado ahora enviará varios mensajes y se pausará
durante un segundo entre cada mensaje.
Filename: src/main.rs
https://book.rustlang-es.org/print 466/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
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}");
}
}
Esta vez, el hilo generado tiene un vector de strings que queremos enviar al hilo principal.
Iteramos sobre ellos, enviando cada uno individualmente, y pausamos entre cada uno
llamando a la función thread::sleep con un valor Duration 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
Debido a que no tenemos ningún código que pause o retrase el bucle for en el hilo
principal, podemos decir que el hilo principal está esperando recibir valores del hilo
generado.
https://book.rustlang-es.org/print 467/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/main.rs
// --snip--
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--
Esta vez, antes de crear el primer hilo generado, llamamos a clone en el transmisor, lo que
nos dará un nuevo transmisor que podemos pasar al primer hilo generado. Pasamos el
https://book.rustlang-es.org/print 468/656
12/7/24, 22:37 El Lenguaje de Programación Rust
transmisor original a un segundo hilo generado. Esto nos da dos hilos, cada uno enviando
mensajes diferentes al receptor.
Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you
Es posible que veas los valores en otro orden según tu sistema. Esto es lo que hace que la
concurrencia sea tan interesante como difícil. Si experimentas con thread::sleep , dándole
varios valores en los diferentes hilos, cada ejecución será más no determinista y creará una
salida diferente cada vez.
Ahora que hemos visto cómo funcionan los canales, veamos un método diferente de
concurrencia.
https://book.rustlang-es.org/print 469/656
12/7/24, 22:37 El Lenguaje de Programación Rust
¿Qué significaría comunicarse compartiendo memoria? Además, ¿por qué los entusiastas
del paso de mensajes advierten que no se debe usar el intercambio de memoria?
Mutex es una abreviatura de exclusión mutua, como en, un mutex permite que solo un hilo
acceda a algunos datos en un momento dado. Para acceder a los datos en un mutex, un hilo
primero debe señalar que desea acceso solicitando adquirir el lock del mutex. El lock es una
estructura de datos que forma parte del mutex que realiza un seguimiento de quién tiene
actualmente acceso exclusivo a los datos. Por lo tanto, el mutex se describe como
guardando los datos que contiene a través del sistema de bloqueo.
Los Mutexes tienen la reputación de ser difíciles de usar porque debes recordar dos reglas:
Para una metáfora del mundo real para un mutex, imagina un panel de discusión en una
conferencia con un solo micrófono. Antes de que un panelista pueda hablar, debe
preguntar o señalar que desea usar el micrófono. Cuando obtienen el micrófono, pueden
hablar todo el tiempo que quieran y luego entregar el micrófono al siguiente panelista que
solicite hablar. Si un panelista olvida entregar el micrófono cuando haya terminado con él,
nadie más puede hablar. Si la administración del micrófono compartido sale mal, ¡el panel
no funcionará como estaba previsto!
https://book.rustlang-es.org/print 470/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La gestión de mutexes puede ser increíblemente difícil de hacer bien, razón por la cual tanta
gente está entusiasmada con los canales. Sin embargo, gracias al sistema de tipos y las
reglas de ownership de Rust, no puedes bloquear y desbloquear incorrectamente.
La API de Mutex<T>
Filename: src/main.rs
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println!("m = {m:?}");
}
Listing 16-12: Explorando la API de Mutex<T> en un contexto de un solo hilo para simplificar
Como con muchos tipos, creamos un Mutex<T> usando la función asociada new . Para
acceder a los datos dentro del mutex, usamos el método lock para adquirir el bloqueo.
Esta llamada bloqueará el hilo actual para que no pueda hacer ningún trabajo hasta que sea
nuestro turno de tener el bloqueo.
La llamada a lock fallaría si otro hilo que tiene el bloqueo se bloquea. En ese caso, nadie
nunca podría obtener el bloqueo, por lo que hemos elegido unwrap y hacer que este hilo se
bloquee si estamos en esa situación.
Después de que hayamos adquirido el bloqueo, podemos tratar el valor de retorno llamado
num en este caso, como una referencia mutable a los datos internos. El sistema de tipos
garantiza que adquirimos un bloqueo antes de usar el valor en m . El tipo de m es
Mutex<i32> , no i32 , por lo que debemos llamar a lock para poder usar el valor i32
interno. No podemos olvidar; el sistema de tipos no nos permitirá acceder al i32 interno
de otra manera.
bloqueo y bloquear el mutex para que otros hilos no puedan usarlo, porque la liberación del
bloqueo ocurre automáticamente.
Después de eliminar el bloqueo, podemos imprimir el valor mutex y ver que pudimos
cambiar el valor interno i32 a 6.
Ahora, intentemos compartir un valor entre múltiples hilos usando Mutex<T> . Activaremos
10 hilos y haremos que cada uno incremente un valor de contador en 1, por lo que el
contador va de 0 a 10. El siguiente ejemplo en el Listado 16-13 tendrá un error del
compilador, y usaremos ese error para aprender más sobre el uso de Mutex<T> y cómo
Rust nos ayuda a usarlo correctamente.
Filename: src/main.rs
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);
}
Listing 16-13: Diez hilos cada uno incrementa un contador custodiado por un Mutex<T>
Creamos una variable counter para contener un i32 dentro de un Mutex<T> , como
hicimos en el Listado 16-12. A continuación, creamos 10 hilos iterando sobre un rango de
números. Usamos thread::spawn y damos a todos los hilos el mismo closure: uno que
mueve el contador al hilo, adquiere un bloqueo en el Mutex<T> llamando al método lock ,
y luego agrega 1 al valor en el mutex. Cuando un hilo termina de ejecutar su closure, num
saldrá del scope y liberará el bloqueo para que otro hilo pueda adquirirlo.
En el hilo principal, recopilamos todos los identificadores de unión. Luego, como hicimos en
el Listado 16-2, llamamos a join en cada identificador para asegurarnos de que todos los
https://book.rustlang-es.org/print 472/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
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
...
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
For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous
error
El mensaje de error indica que el valor de counter se movió en la anterior iteración del
bucle. El compilador nos está diciendo que no podemos mover la propiedad de counter a
múltiples hilos. Arreglemos el error del compilador con un método de múltiples propietarios
que discutimos en el Capítulo 15.
En el capítulo 15, le dimos a un valor múltiples dueños al usar el smart pointer Rc<T> para
crear un valor de recuento de referencia. Hagamos lo mismo aquí y veamos qué sucede.
Envolveremos el Mutex<T> en Rc<T> en el Listado 16-14 y clonaremos el Rc<T> antes de
mover el ownership al hilo.
Filename: src/main.rs
https://book.rustlang-es.org/print 473/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
Listing 16-14: Intentando usar Rc<T> para permitir múltiples hilos para poseer Mutex<T>
Una vez más, compilamos y obtenemos... ¡diferentes errores! El compilador nos está
enseñando mucho.
https://book.rustlang-es.org/print 474/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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`
-->
/rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/thread/mod.rs:6
77:1
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous
error
Wow, ¡ese mensaje de error es muy extenso! Aquí está la parte importante en la que
debemos enfocarnos: `Rc<Mutex<i32>>` cannot be sent between threads safely . El
compilador también nos está diciendo la razón por la que: the trait `Send` is not
implemented for `Rc<Mutex<i32>>` . Hablaremos de Send en la siguiente sección: es uno
de los traits que asegura que los tipos que usamos con hilos están destinados a su uso en
situaciones concurrentes.
https://book.rustlang-es.org/print 475/656
12/7/24, 22:37 El Lenguaje de Programación Rust
para más detalles. En este punto, solo necesita saber que los
Átomos funcionan como tipos primitivos, pero son seguros para compartir entre hilos.
Entonces podrías preguntarte por qué todos los tipos primitivos no son atómicos y por qué
los tipos de biblioteca estándar no se implementan para usar Arc<T> de forma
predeterminada. La razón es que la seguridad de los hilos conlleva una penalización de
rendimiento que solo desea pagar cuando realmente lo necesita. Si solo está realizando
operaciones en valores dentro de un solo hilo, su código puede ejecutarse más rápido si no
tiene que hacer cumplir las garantías que proporcionan los átomos.
Volvamos a nuestro ejemplo: Arc<T> y Rc<T> tienen la misma API, por lo que arreglamos
nuestro programa cambiando la línea use , la llamada a new y la llamada a clone . El
código en el Listado 16-15 finalmente se compilará y ejecutará:
Filename: src/main.rs
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);
}
Listing 16-15: Usando un Arc<T> para envolver Mutex<T> para poder compartir el ownership a través de
múltiples hilos
https://book.rustlang-es.org/print 476/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Result: 10
¡Lo hicimos! Contamos de 0 a 10, lo que puede no parecer muy impresionante, pero nos
enseñó mucho sobre Mutex<T> y la seguridad de los hilos. También podría usar la
estructura de este programa para realizar operaciones más complicadas que simplemente
incrementar un contador. Usando esta estrategia, puede dividir un cálculo en partes
independientes, dividir esas partes en hilos y luego usar un Mutex<T> para que cada hilo
actualice el resultado final con su parte.
Nota que si estás haciendo operaciones numéricas simples, hay tipos más simples que los
tipos Mutex<T> proporcionados por el std::sync::atomic módulo de la biblioteca
estándar. Estos tipos proporcionan acceso seguro y concurrente a tipos primitivos. Elegimos
usar Mutex<T> con un tipo primitivo para este ejemplo para que pudiéramos concentrarnos
en cómo funciona Mutex<T> .
Es posible que hayas notado que counter es inmutable, pero podríamos obtener una
referencia mutable al valor dentro de él; esto significa que Mutex<T> proporciona
mutabilidad interior, como lo hace la familia Cell . De la misma manera que usamos
RefCell<T> en el Capítulo 15 para permitirnos mutar contenidos dentro de un Rc<T> ,
usamos Mutex<T> para mutar contenidos dentro de un Arc<T> .
Un detalle a tener en cuenta es que Rust no puede protegerte de todos los errores lógicos al
usar Mutex<T> . Recuerda en el Capítulo 15 que usar Rc<T> venía con el riesgo de crear
ciclos de referencia, donde dos valores Rc<T> se refieren entre sí, causando fugas de
memoria. De manera similar, Mutex<T> viene con el riesgo de crear deadlocks. Estos
ocurren cuando una operación necesita bloquear dos recursos y dos hilos han adquirido
cada uno de los bloqueos, lo que los hace esperar el uno al otro para siempre. Si está
interesado en los deadlocks, intente crear un programa Rust que tenga un deadlock; luego
investigue las estrategias de mitigación de deadlock para mutexes en cualquier lenguaje y
pruebe implementarlas en Rust. La documentación de la API de la biblioteca estándar para
Mutex<T> y MutexGuard ofrece información útil.
Terminaremos este capítulo hablando sobre los traits Send y Sync y cómo podemos
usarlos con tipos personalizados.
https://book.rustlang-es.org/print 477/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Sin embargo, dos conceptos de concurrencia están integrados en el lenguaje: los traits
Sync y Send de std::marker .
El trait Send indica que la propiedad de un valor se puede transferir entre hilos. Casi todos
los tipos son Send , con algunas excepciones notables, como Rc<T> , que no es Send
porque si clonara un valor de Rc<T> y tratara de transferir la propiedad del clon a otro hilo,
ambos hilos podrían actualizar el recuento de referencias al mismo tiempo. Por esta razón,
Rc<T> está implementado para su uso en situaciones de un solo hilo donde no desea pagar
la penalización de rendimiento segura para subprocesos.
Por lo tanto, el sistema de tipos y los límites de los traits de Rust garantizan que nunca
pueda enviar accidentalmente un valor Rc<T> a través de hilos de forma insegura. Cuando
intentamos hacer esto en el Listado 16-14, obtuvimos el error the trait Send is not
implemented for Rc<Mutex<i32>> . Cuando cambiamos a Arc<T> , que es Send , el código se
compiló.
El trait Sync indica que es seguro que el tipo que implementa Sync se referencie desde
múltiples hilos. En otras palabras, cualquier tipo T es Sync si &T (una referencia inmutable
a T ) es Send , lo que significa que la referencia se puede enviar de forma segura a otro hilo.
De manera similar a Send , los tipos primitivos son Sync , y los tipos compuestos
enteramente de tipos que son Sync también son Sync .
El smart pointer Rc<T> tampoco es Sync por las mismas razones por las que no es Send .
El tipo RefCell<T> (del que hablamos en el Capítulo 15) y la familia de tipos relacionados
https://book.rustlang-es.org/print 478/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Debido a que los tipos que están compuestos de los traits Send y Sync se automatizan
también Send y Sync , no tenemos que implementar esos traits manualmente. Como
marcadores de traits, ni siquiera tienen ningún método para implementar. Son útiles para
hacer cumplir invariantes relacionados con la concurrencia.
Resumen
No es la última vez que verás la concurrencia en este libro: el proyecto del Capítulo 20 usará
los conceptos de este capítulo en una situación más realista que los ejemplos más
pequeños que se discuten aquí.
Como se mencionó anteriormente, debido a que muy poco de cómo Rust maneja la
concurrencia es parte del lenguaje, muchas soluciones de concurrencia se implementan
como cajones. Estos evolucionan más rápido que la biblioteca estándar, así que asegúrese
de buscar en línea las cajas actuales de última generación para usar en situaciones de
múltiples subprocesos.
https://book.rustlang-es.org/print 479/656
12/7/24, 22:37 El Lenguaje de Programación Rust
discutiremos cómo los ideales de Rust se relacionan con los que puede estar familiarizado
con la programación orientada a objetos.
https://book.rustlang-es.org/print 480/656
12/7/24, 22:37 El Lenguaje de Programación Rust
https://book.rustlang-es.org/print 481/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Usando esta definición, Rust es orientado a objetos: los structs y los enums tienen datos, y
los bloques impl proporcionan métodos en structs y enums. Aunque los structs y los
enums con métodos no se llaman objetos, proporcionan la misma funcionalidad, según la
definición de objetos del Gang of Four’s.
Otro aspecto comúnmente asociado con OOP es la idea de encapsulación, que significa que
los detalles de implementación de un objeto no son accesibles al código que usa ese objeto.
Por lo tanto, la única forma de interactuar con un objeto es a través de su API pública; el
código que usa el objeto no debería poder acceder a los detalles internos del objeto y
cambiar los datos o el comportamiento directamente. Esto permite al programador cambiar
y refactorizar los detalles internos de un objeto sin necesidad de cambiar el código que usa
el objeto.
struct también puede tener un campo que contiene el promedio de los valores en el vector,
lo que significa que el promedio no tiene que calcularse a pedido cada vez que alguien lo
necesite. En otras palabras, AveragedCollection almacenará en caché el promedio
calculado para nosotros. El Listado 17-1 tiene la definición del struct AveragedCollection :
Filename: src/lib.rs
Listing 17-1: Un struct AveragedCollection que mantiene una lista de enteros y el promedio de los elementos
en la colección
El struct está marcado como pub para que otro código pueda usarlo, pero los campos
dentro del struct permanecen privados. Esto es importante en este caso porque queremos
asegurarnos de que cada vez que se agrega o elimina un valor de la lista, el promedio
también se actualiza. Hacemos esto implementando los métodos públicos add , remove y
average en el struct, como se muestra en el Listado 17-2:
Filename: src/lib.rs
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
Listing 17-2: Implementaciones de los métodos públicos add , remove , y average en AveragedCollection
https://book.rustlang-es.org/print 483/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los métodos públicos add , remove , y average son las únicas formas de acceder o
modificar los datos en una instancia de AveragedCollection . Cuando se agrega un
elemento a list usando el método add o se elimina usando el método remove , las
implementaciones de cada uno llaman al método privado update_average que maneja la
actualización del campo average también.
Dejamos los campos list y average privados para que no haya forma de que el código
externo agregue o elimine elementos de list directamente; de lo contrario, el campo
average podría quedar fuera de sincronización cuando list cambia. El método average
devuelve el valor en el campo average , permitiendo que el código externo lea el average
pero no lo modifique.
Si se considera que un lenguaje debe tener herencia para ser un lenguaje orientado a
objetos, entonces Rust no cumple con esta definición. No existe una forma de definir un
struct que herede los campos y las implementaciones de métodos de un struct padre sin
usar una macro.
Elegirías la herencia por dos razones principales. Una es reutilizar el código: puedes
implementar un comportamiento particular para un tipo, y la herencia te permite reutilizar
esa implementación para un tipo diferente. Puedes hacer esto de una manera limitada en el
código Rust usando implementaciones de métodos predeterminados de un trait, que viste
en el Listado 10-14 cuando agregamos una implementación predeterminada del método
https://book.rustlang-es.org/print 484/656
12/7/24, 22:37 El Lenguaje de Programación Rust
summarize en el trait Summary . Cualquier tipo que implemente el trait Summary tendría el
método summarize disponible sin ningún código adicional. Esto es similar a una clase padre
que tiene una implementación de un método y una clase hija heredada que también tiene la
implementación del método. También podemos anular la implementación predeterminada
del método summarize cuando implementamos el trait Summary , lo que es similar a una
clase hija anulando la implementación de un método heredado de una clase padre.
La otra razón para usar la herencia está relacionada con el sistema de tipos: permitir que un
tipo hijo se use en los mismos lugares que el tipo padre. Esto es también llamado
polimorfismo, lo que significa que puedes sustituir múltiples objetos entre sí en tiempo de
ejecución si comparten ciertas características.
Polimorfismo
En cambio, Rust utiliza generics para abstraerse sobre diferentes tipos posibles y los
trait bounds para imponer restricciones sobre lo que esos tipos deben proporcionar.
Esto se llama a veces polimorfismo paramétrico acotado.
Por estas razones, Rust toma un enfoque diferente utilizando trait objects en lugar de
herencia. Veamos cómo los trait objects permiten el polimorfismo en Rust.
https://book.rustlang-es.org/print 485/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Sin embargo, a veces queremos que los usuarios de nuestra biblioteca puedan ampliar el
conjunto de tipos que pueden almacenar en una estructura de datos. Para mostrar cómo
podríamos lograr esto, crearemos una herramienta de interfaz gráfica de usuario (GUI) de
ejemplo que itera a través de una lista de elementos, llamando a un método draw en cada
uno para dibujarlo en la pantalla, una técnica común para las herramientas de GUI.
Crearemos una caja de biblioteca llamada gui que contiene la estructura de una biblioteca
GUI. Esta caja podría incluir algunos tipos para que las personas los usen, como Button o
TextField . Además, los usuarios de gui querrán crear sus propios tipos que se puedan
dibujar: por ejemplo, un programador podría agregar una Image y otro podría agregar un
SelectBox .
Para hacer esto en un lenguaje con herencia, podríamos definir una clase llamada
Component que tenga un método llamado draw en ella. Las otras clases, como Button ,
Image y SelectBox , heredarían de Component y, por lo tanto, heredarían el método draw .
Cada uno podría anular el método draw para definir su comportamiento personalizado,
pero el marco podría tratar todos los tipos como si fueran instancias de Component y llamar
a draw en ellos. Pero como Rust no tiene herencia, necesitamos otra forma de estructurar
la biblioteca gui para permitir a los usuarios extenderla con nuevos tipos.
https://book.rustlang-es.org/print 486/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para implementar el comportamiento que queremos que tenga gui , definiremos un trait
llamado Draw que tendrá un método llamado draw . Luego podemos definir un vector que
tome un objeto de trait. Un objeto de trait apunta tanto a una instancia de un tipo que
implementa nuestro trait especificado como a una tabla utilizada para buscar métodos de
trait en ese tipo en tiempo de ejecución. Creamos un objeto de trait especificando algún tipo
de puntero, como una referencia & o un puntero inteligente Box<T> , luego la palabra clave
dyn y luego especificando el trait relevante. (Hablaremos sobre la razón por la que los
objetos de trait deben usar un puntero en el Capítulo 19 en la sección “Tipos de tamaño
dinámico y el trait Sized .”) Podemos usar objetos de trait en lugar de un tipo genérico o
concreto. Donde sea que usemos un objeto de trait, el sistema de tipos de Rust se
asegurará en tiempo de compilación que cualquier valor utilizado en ese contexto
implemente el trait del objeto de trait. En consecuencia, no necesitamos conocer todos los
tipos posibles en tiempo de compilación.
Hemos mencionado que, en Rust, nos abstenemos de llamar a los structs y enums “objetos”
para distinguirlos de los objetos de otros lenguajes. En un struct o enum, los datos en los
campos del struct y el comportamiento en los bloques impl están separados, mientras que
en otros lenguajes, los datos y el comportamiento combinados en un solo concepto a
menudo se etiquetan como un objeto. Sin embargo, los objetos de trait son más como
objetos en otros lenguajes en el sentido de que combinan datos y comportamiento. Pero los
objetos de trait difieren de los objetos tradicionales en que no podemos agregar datos a un
objeto de trait. Los objetos de trait no son tan útiles en general como los objetos en otros
lenguajes: su propósito específico es permitir la abstracción a través del comportamiento
común.
El Listado 17-3 muestra cómo definir un trait llamado Draw con un método llamado draw :
Filename: src/lib.rs
Esta sintaxis debería verse familiar de nuestras discusiones sobre cómo definir traits en el
Capítulo 10. A continuación viene una sintaxis nueva: el Listado 17-4 define un struct
llamado Screen que contiene un vector llamado components . Este vector es de tipo
Box<dyn Draw> , que es un objeto de trait; es un sustituto de cualquier tipo dentro de una
Box que implementa el trait Draw .
Filename: src/lib.rs
https://book.rustlang-es.org/print 487/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Listing 17-4: Definición del struct Screen con un campo components que contiene un vector de trait objects que
implementan el trait Draw
En el struct Screen hemos definido un método llamado run que llamará al método draw
en cada uno de sus components , como se muestra en el Listado 17-5:
Filename: src/lib.rs
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
Listing 17-5: Un método run en Screen que llama al método draw en cada componente
Esto funciona de manera diferente a la definición de un struct que usa un parámetro de tipo
generic con trait bound. Un parámetro de tipo generic solo se puede sustituir con un tipo
concreto a la vez, mientras que los trait objects permiten que varios tipos concretos llenen
el trait object en tiempo de ejecución. Por ejemplo, podríamos haber definido el struct
Screen usando un parámetro de tipo generic y un trait bound como en el Listado 17-6:
Filename: src/lib.rs
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
Listing 17-6: Una implementación alternativa del struct Screen y su método run usando generics y trait
bounds
Esto nos restringe a una instancia de Screen que tiene una lista de componentes de tipo
Button o de tipo TextField . Si solo tendrá colecciones homogéneas, usar generics y trait
https://book.rustlang-es.org/print 488/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Por otro lado, con el método que utiliza trait objects, una instancia de Screen puede
contener un Vec<T> que contiene una Box<Button> así como una Box<TextField> .
Veamos cómo funciona esto, y luego hablaremos sobre las implicaciones de rendimiento en
tiempo de ejecución.
Implementando el trait
Ahora agregaremos algunos tipos que implementen el trait Draw . Proporcionaremos el tipo
Button . Nuevamente, implementar una biblioteca GUI está más allá del alcance de este
libro, por lo que el método draw no tendrá ninguna implementación útil en su cuerpo. Para
imaginar cómo podría ser la implementación, un struct Button podría tener campos para
width , height y label , como se muestra en el Listado 17-7:
Filename: src/lib.rs
Los campos width , height y label en Button serán diferentes de los campos en otros
componentes; por ejemplo, un tipo TextField podría tener esos mismos campos más un
campo placeholder . Cada uno de los tipos que queremos dibujar en la pantalla
implementará el trait Draw pero usará código diferente en el método draw para definir
cómo dibujar ese tipo particular, como lo hace Button aquí (sin el código GUI real, como se
mencionó). El tipo Button , por ejemplo, podría tener un bloque impl adicional que
contenga métodos relacionados con lo que sucede cuando un usuario hace clic en el botón.
Este tipo de métodos no se aplicarán a tipos como TextField .
Si alguien que utiliza nuestra biblioteca decide implementar un struct SelectBox que tiene
campos width , height y options , también implementará el trait Draw en el tipo
SelectBox , como se muestra en el Listado 17-8:
Filename: src/main.rs
https://book.rustlang-es.org/print 489/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
Listing 17-8: Otro crate usando gui e implementando el trait Draw en un struct SelectBox
El usuario de nuestra biblioteca ahora puede escribir su función main para crear una
instancia de Screen . A la instancia de Screen , pueden agregar un SelectBox y un Button
colocando cada uno en una Box<T> para convertirse en un trait object. Luego pueden
llamar al método run en la instancia de Screen , que llamará a draw en cada uno de los
componentes. El Listado 17-9 muestra esta implementación:
Filename: src/main.rs
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();
}
Listing 17-9: Usando trait objects para almacenar valores de diferentes tipos que implementan el mismo trait
Cuando escribimos la biblioteca, no sabíamos que alguien podría agregar el tipo SelectBox ,
pero nuestra implementación de Screen pudo operar en el nuevo tipo y dibujarlo porque
https://book.rustlang-es.org/print 490/656
12/7/24, 22:37 El Lenguaje de Programación Rust
SelectBox implementa el trait Draw , lo que significa que implementa el método draw .
Este concepto, de preocuparnos solo por los mensajes a los que responde un valor en lugar
del tipo concreto del valor, es similar al concepto de duck typing en lenguajes de tipado
dinámico: si camina como un pato y grazna como un pato, ¡entonces debe ser un pato! En la
implementación de run en Screen en el Listado 17-5, run no necesita saber cuál es el tipo
concreto de cada componente. No verifica si un componente es una instancia de un Button
o de un SelectBox , simplemente llama al método draw en el componente. Al especificar
Box<dyn Draw> como el tipo de los valores en el vector components , hemos definido que
Screen necesita valores a los que podamos llamar el método draw .
La ventaja de utilizar trait objects y el sistema de tipos de Rust para escribir código similar al
código que utiliza duck typing es que nunca tenemos que verificar si un valor implementa
un método en particular en tiempo de ejecución o preocuparnos por obtener errores si un
valor no implementa un método, pero lo llamamos de todos modos. Rust no compilará
nuestro código si los valores no implementan los traits que necesitan los trait objects.
Por ejemplo, el Listado 17-10 muestra lo que sucede si intentamos crear una Screen con un
String como componente:
Filename: src/main.rs
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
Listing 17-10: Intentando utilizar un tipo que no implementa the trait del trait object
$ 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
https://book.rustlang-es.org/print 491/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Este error nos indica que o bien estamos pasando algo a Screen que no queríamos pasar y,
por lo tanto, deberíamos pasar un tipo diferente o deberíamos implementar Draw en
String para que Screen pueda llamar a draw en él.
Recuerda que en la sección “Performance of Code Using Generics” del Capítulo 10 hablamos
sobre el proceso de monomorfización que realiza el compilador cuando usamos trait bounds
en los genéricos: el compilador genera implementaciones no genéricas de funciones y
métodos para cada tipo concreto que usamos en lugar de un parámetro de tipo genérico. El
código que resulta de la monomorfización está realizando static dispatch, que es cuando el
compilador sabe qué método estás llamando en tiempo de compilación. Esto se opone al
dynamic dispatch, que es cuando el compilador no puede decir en tiempo de compilación
qué método estás llamando. En los casos de dynamic dispatch, el compilador emite código
que en tiempo de ejecución determinará qué método llamar.
Cuando usamos trait objects, Rust debe usar dynamic dispatch. El compilador no conoce
todos los tipos que podrían usarse con el código que está llamando a trait objects, por lo
que no sabe qué método implementado en qué tipo llamar. En cambio, en tiempo de
ejecución, Rust usa los punteros dentro del trait object para saber qué método llamar. Esta
búsqueda incurre en un costo de tiempo de ejecución que no ocurre con el static dispatch.
Dynamic dispatch también evita que el compilador elija la opción de inline del código de un
método, lo que a su vez evita algunas optimizaciones. Sin embargo, obtuvimos flexibilidad
adicional en el código que escribimos en el Listado 17-5 y pudimos admitir en el Listado 17-
9, por lo que es un compromiso a considerar.
https://book.rustlang-es.org/print 492/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los state objects comparten funcionalidad: en Rust, por supuesto, usamos structs y traits en
lugar de objetos y herencia. Cada state object es responsable de su propio comportamiento
y de gobernar cuándo debe cambiar a otro estado. El valor que contiene un state object no
sabe nada sobre el comportamiento diferente de los estados o cuándo hacer la transición
entre estados.
La ventaja de usar el state pattern es que, cuando los requisitos comerciales del programa
cambian, no necesitaremos cambiar el código del valor que contiene el estado o el código
que usa el valor. Solo necesitaremos actualizar el código dentro de uno de los state objects
para cambiar sus reglas o quizás agregar más state objects.
Primero, vamos a implementar el state pattern de una manera más tradicional orientada a
objetos, luego usaremos un enfoque que es un poco más natural en Rust. Vamos a
profundizar en la implementación incremental de un flujo de trabajo de publicación de blog
usando el state pattern.
Cualquier otro cambio que se intente realizar en una publicación no debería tener ningún
efecto. Por ejemplo, si intentamos aprobar un borrador de blog antes de haber solicitado
una revisión, la publicación debería seguir siendo un borrador no publicado.
El Listado 17-11 muestra este flujo de trabajo en forma de código: este es un ejemplo de uso
de la API que implementaremos en una crate de biblioteca llamada blog . Esto aún no se
compilará porque no hemos implementado el crate de biblioteca blog .
Filename: src/main.rs
https://book.rustlang-es.org/print 493/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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());
}
Listing 17-11: Código que demuestra el comportamiento deseado que queremos que tenga nuestro crate blog
Queremos permitir que el usuario cree una nueva publicación de blog en borrador con
Post::new . Queremos permitir que se agregue texto a la publicación del blog. Si
intentamos obtener el contenido de la publicación inmediatamente, antes de la aprobación,
no deberíamos obtener ningún texto porque la publicación sigue siendo un borrador.
Hemos agregado assert_eq! en el código con fines de demostración. Una excelente
prueba unitaria para esto sería afirmar que una publicación de blog en borrador devuelve
un string vacío del método content , pero no vamos a escribir pruebas para este ejemplo.
Observa que el único tipo con el que estamos interactuando desde el crate es el tipo Post .
Este tipo utilizará el state pattern y contendrá un valor que será uno de los tres state objects
que representan los diversos estados en los que puede estar una publicación: borrador,
esperando revisión o publicado. El cambio de un estado a otro se administrará
internamente dentro del tipo Post . Los estados cambian en respuesta a los métodos
llamados por los usuarios de nuestra biblioteca en la instancia Post , pero no tienen que
administrar los cambios de estado directamente. Además, los usuarios no pueden cometer
un error con los estados, como publicar una publicación antes de que se revise.
https://book.rustlang-es.org/print 494/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Luego, Post contendrá un trait object de Box<dyn State> dentro de un campo privado
llamado state para mantener el state object. Verás por qué Option<T> es necesario en un
momento.
Filename: src/lib.rs
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}
trait State {}
struct Draft {}
Listing 17-12: Definición de un struct Post y una función new que crea una nueva instancia de Post , un trait
State , y un struct Draft
El trait State define el comportamiento compartido por los diferentes estados de una
publicación. Los state objects son Draft , PendingReview y Published , y todos
implementarán el trait State . Por ahora, el trait no tiene ningún método, y comenzaremos
definiendo solo el estado Draft porque ese es el estado en el que queremos que comience
una publicación.
Cuando creamos un nuevo Post , estableceremos su campo state como un valor Some
que contiene un Box que apunta a una nueva instancia del struct Draft . Esto asegura que
cada vez que creemos una nueva instancia de Post , comenzará como un borrador. Debido
a que el campo state de Post es privado, ¡no hay forma de crear un Post en ningún otro
estado! En la función Post::new , establecemos el campo content en un nuevo String
vacío.
Vimos en el Listado 17-11 que queremos poder llamar a un método llamado add_text y
pasarle un &str que luego se agregará como el contenido de texto de la publicación del
blog. Implementaremos esto como un método, en lugar de exponer el campo content
como pub , para que más tarde podamos implementar un método que controlará cómo se
https://book.rustlang-es.org/print 495/656
12/7/24, 22:37 El Lenguaje de Programación Rust
lee el campo content . El método add_text es bastante sencillo, así que agreguemos la
implementación en el Listado 17-13 al bloque impl Post :
Filename: src/lib.rs
impl Post {
// --snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Listing 17-13: Implementando el método add_text para agregar texto al campo content de una publicación
El método add_text toma una referencia mutable a self porque estamos cambiando la
instancia de Post en la que estamos llamando add_text . Luego llamamos a push_str en
el String en content y pasamos el argumento text para agregar al content guardado.
Este comportamiento no depende del estado en el que se encuentre la publicación, por lo
que no es parte del state pattern. El método add_text no interactúa con el campo state
en absoluto, pero es parte del comportamiento que queremos admitir.
Incluso después de que hayamos llamado add_text y agregado algún contenido a nuestra
publicación, todavía queremos que el método content devuelva un slice de string vacío
porque la publicación todavía está en el estado de borrador, como se muestra en la línea 7
del Listado 17-11. Por ahora, implementemos el método content con lo más simple que
cumplirá con este requisito: siempre devolver un string slice vacío. Lo cambiaremos más
tarde una vez que implementemos la capacidad de cambiar el estado de una publicación
para que pueda publicarse. Hasta ahora, las publicaciones solo pueden estar en el estado
de borrador, por lo que el contenido de la publicación siempre debe estar vacío. El Listado
17-14 muestra esta implementación de marcador de posición:
Filename: src/lib.rs
impl Post {
// --snip--
pub fn content(&self) -> &str {
""
}
}
Listing 17-14: Agregando una implementación provisional para el método content en Post que siempre
devuelve un string slice vacío
Con este método content añadido, todo en el Listado 17-11 hasta la línea 7 funciona como
se pretendía.
https://book.rustlang-es.org/print 496/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
impl Post {
// --snip--
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
struct PendingReview {}
Agregamos un método público llamado request_review a Post que toma una referencia
mutable a self . Luego llamamos a un método interno request_review en el estado actual
de Post , y este segundo método request_review consume el estado actual y devuelve un
nuevo estado.
Agregamos el método request_review al trait State ; todos los tipos que implementan el
trait ahora deberán implementar el método request_review . Tenga en cuenta que en lugar
de tener self , &self o &mut self como el primer parámetro del método, tenemos self:
Box<Self> . Esta sintaxis significa que el método solo es válido cuando se llama en un Box
que contiene el tipo. Esta sintaxis toma posesión de Box<Self> , invalidando el estado
anterior para que el valor de estado de Post pueda transformarse en un nuevo estado.
https://book.rustlang-es.org/print 497/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para consumir el antiguo estado, el método request_review debe tomar ownership del
valor de estado. Aquí es donde entra en juego la Option en el campo state de Post :
llamamos al método take para sacar el valor Some del campo state y dejar un None en
su lugar, porque Rust no nos permite tener campos no poblados en los structs. Esto nos
permite mover el valor state fuera de Post en lugar de pedir borrowing. Luego
estableceremos el valor state de la publicación en el resultado de esta operación.
Ahora podemos comenzar a ver las ventajas del state pattern: el método request_review
en Post es el mismo sin importar su valor state . Cada estado es responsable de sus
propias reglas.
Dejaremos el método content en Post tal como está, devolviendo un string slice vacío.
Ahora podemos tener un Post en el estado PendingReview así como en el estado Draft ,
pero queremos el mismo comportamiento en el estado PendingReview . ¡El Listado 17-11
ahora funciona hasta la línea 10!
Filename: src/lib.rs
https://book.rustlang-es.org/print 498/656
12/7/24, 22:37 El Lenguaje de Programación Rust
impl Post {
// --snip--
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
struct PendingReview {}
struct Published {}
https://book.rustlang-es.org/print 499/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Ahora debemos actualizar el método content en Post . Queremos que el valor devuelto
por content dependa del estado actual de Post , por lo que vamos a hacer que Post
delegue a un método content definido en su state , como se muestra en el Listado 17-17:
Filename: src/lib.rs
impl Post {
// --snip--
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
// --snip--
}
Listing 17-17: Actualizando el método content en Post para delegar en un método content en State
Debido a que el objetivo es mantener todas estas reglas dentro de los structs que
implementan State , llamamos a un método content en el valor en state y pasamos la
instancia de publicación (es decir, self ) como argumento. Luego devolvemos el valor
devuelto del uso del método content en el valor state .
Luego llamamos al método unwrap , el cual sabemos que nunca generará un error, porque
los métodos en Post aseguran que state siempre contendrá un valor Some cuando esos
métodos finalicen. Este es uno de los casos que mencionamos en la sección “Casos en los
que tienes más información que el compilador” del Capítulo 9 cuando sabemos que un valor
None nunca es posible, aunque el compilador no puede entender eso.
Filename: src/lib.rs
https://book.rustlang-es.org/print 500/656
12/7/24, 22:37 El Lenguaje de Programación Rust
trait State {
// --snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
// --snip--
struct Published {}
¡Y hemos terminado! ¡Todo lo que se muestra en el Listado 17-11 ahora funciona! Hemos
implementado el patrón de estado con las reglas del flujo de trabajo de la publicación de
blog. La lógica relacionada con las reglas vive en los objetos de estado en lugar de estar
dispersa en Post .
Puede que te hayas preguntado por qué no usamos un enum con los diferentes
estados posibles de la publicación como variantes. Esa es ciertamente una solución
posible, ¡pruébala y compara los resultados finales para ver cuál prefieres! Una
desventaja de usar un enum es que cada lugar que verifica el valor del enum
necesitará una expresión match o similar para manejar cada variante posible. Esto
podría ser más repetitivo que esta solución de trait object.
https://book.rustlang-es.org/print 501/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Hemos demostrado que Rust es capaz de implementar el State Pattern orientado a objetos
para encapsular los diferentes tipos de comportamiento que un post debería tener en cada
estado. Los métodos en Post no saben nada sobre los diferentes comportamientos. La
forma en que organizamos el código, solo tenemos que mirar en un solo lugar para conocer
las diferentes formas en que un post publicado puede comportarse: la implementación del
trait State en el struct Published .
Con el State Pattern, los métodos Post y los lugares donde usamos Post no necesitan
expresiones match , y para agregar un nuevo estado, solo necesitaríamos agregar un nuevo
struct e implementar los métodos del trait en ese struct.
Un inconveniente del State Pattern es que, debido a que los estados implementan las
transiciones entre estados, algunos de los estados están acoplados entre sí. Si agregamos
otro estado entre PendingReview y Published , como Scheduled , tendríamos que cambiar
el código en PendingReview para hacer la transición a Scheduled en su lugar. Sería menos
trabajo si PendingReview no necesitara cambiar con la adición de un nuevo estado, pero
eso significaría cambiar a otro patrón de diseño.
Otro inconveniente es que hemos duplicado algo de lógica. Para eliminar parte de la
duplicación, podríamos intentar hacer implementaciones predeterminadas para los
métodos request_review y approve en el trait State que devuelvan self ; sin embargo,
esto violaría la seguridad del objeto, porque el trait no sabe exactamente cuál será el self
concreto. Queremos poder usar State como un objeto de trait, por lo que sus métodos
deben ser seguros para el objeto.
https://book.rustlang-es.org/print 502/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Vamos a mostrarte cómo replantear el State Pattern para obtener un conjunto diferente de
compensaciones. En lugar de encapsular los estados y las transiciones por completo para
que el código externo no tenga conocimiento de ellos, codificaremos los estados en
diferentes tipos. En consecuencia, el sistema de verificación de tipos de Rust evitará los
intentos de usar publicaciones borradores donde solo se permiten publicaciones publicadas
emitiendo un error del compilador.
Filename: src/main.rs
fn main() {
let mut post = Post::new();
Filename: src/lib.rs
https://book.rustlang-es.org/print 503/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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);
}
}
Listing 17-19: Un Post con un método content y un DraftPost sin un método content
Tanto los structs Post como DraftPost tienen un campo privado content que almacena
el texto de la publicación del blog. Los structs ya no tienen el campo state porque estamos
moviendo la codificación del estado a los tipos de los structs. El struct Post representará
una publicación publicada, y tiene un método content que devuelve el content .
Todavía tenemos una función Post::new , pero en lugar de devolver una instancia de Post ,
devuelve una instancia de DraftPost . Debido a que content es privado y no hay funciones
que devuelvan Post , no es posible crear una instancia de Post en este momento.
El struct DraftPost tiene un método add_text , por lo que podemos agregar texto al
campo content como antes. Sin embargo, ten en cuenta que DraftPost no tiene un
método content definido. Entonces, ahora el programa garantiza que todas las
publicaciones comienzan como publicaciones en borrador, y las publicaciones en borrador
no tienen su contenido disponible para mostrar. Cualquier intento de evitar estas
restricciones dará como resultado un error del compilador.
Entonces, ¿cómo obtenemos una publicación publicada? Queremos hacer cumplir la regla
de que una publicación en borrador debe ser revisada y aprobada antes de que pueda
publicarse. Una publicación en el estado de revisión pendiente todavía no debe mostrar
ningún contenido. Implementemos estas restricciones agregando otro struct,
https://book.rustlang-es.org/print 504/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
impl DraftPost {
// --snip--
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
content: self.content,
}
}
}
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
Los métodos request_review y approve toman ownership de self , consumiendo así las
instancias de DraftPost y PendingReviewPost y transformándolas en un
PendingReviewPost y un Post publicado, respectivamente. De esta manera, no tendremos
ninguna instancia de DraftPost persistente después de haber llamado a request_review
en ellas, y así sucesivamente. El struct PendingReviewPost no tiene un método content
definido en él, por lo que intentar leer su contenido da como resultado un error del
compilador, como con DraftPost . Debido a que la única forma de obtener una instancia de
Post publicada que tiene un método content definido es llamar al método approve en un
PendingReviewPost , y la única forma de obtener un PendingReviewPost es llamar al
método request_review en un DraftPost , ahora hemos codificado el workflow de la
publicación del blog en el sistema de tipos.
Pero también debemos hacer algunos cambios pequeños en main . Los métodos
request_review y approve devuelven nuevas instancias en lugar de modificar el struct en
el que se llaman, por lo que debemos agregar más asignaciones de sombreado let post =
para guardar las instancias devueltas. Tampoco podemos tener las afirmaciones sobre el
contenido de las publicaciones en borrador y revisión pendiente sean strings vacíos, ni los
necesitamos: ya no podemos compilar el código que intenta usar el contenido de las
https://book.rustlang-es.org/print 505/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/main.rs
use blog::Post;
fn main() {
let mut post = Post::new();
Listing 17-21: Modificaciones a main para usar la nueva implementación del workflow de la publicación del blog
Las modificaciones que hicimos a main para reasignar post significan que esta
implementación ya no sigue el patrón de estado orientado a objetos: las transformaciones
entre los estados ya no están encapsuladas completamente dentro de la implementación de
Post . Sin embargo, nuestra ganancia es que los estados inválidos ahora son imposibles
debido al sistema de tipos y la comprobación de tipos que ocurre en tiempo de compilación.
Esto garantiza que ciertos errores, como la visualización del contenido de una publicación
no publicada, se descubrirán antes de que lleguen a producción.
Prueba las tareas sugeridas al comienzo de esta sección en el crate blog tal como está
después del Listado 17-21 para evaluar el diseño de esta versión del código. Ten en cuenta
que es posible que algunas de las tareas ya estén completadas en este diseño.
Hemos visto que aunque Rust es capaz de implementar patrones de diseño orientados a
objetos, también están disponibles en Rust otros patrones, como la codificación del estado
en el sistema de tipos. Estos patrones tienen diferentes compensaciones. Aunque es posible
que estés muy familiarizado con los patrones orientados a objetos, repensar el problema
para aprovechar las características de Rust puede proporcionar beneficios, como prevenir
algunos errores en tiempo de compilación. Los patrones orientados a objetos no siempre
serán la mejor solución en Rust debido a ciertas características, como el ownership, que los
lenguajes orientados a objetos no tienen.
Resumen
Sin importar si consideras a Rust como un lenguaje orientado a objetos después de leer este
capítulo, ahora sabes que puedes usar objetos de tipo trait para obtener algunas
https://book.rustlang-es.org/print 506/656
12/7/24, 22:37 El Lenguaje de Programación Rust
A continuación, veremos los patterns, que son otra de las características de Rust que
permiten mucha flexibilidad. Hemos visto brevemente los patterns a lo largo del libro, pero
aún no hemos visto su capacidad total. ¡Vamos allá!
https://book.rustlang-es.org/print 507/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Literales
Arrays, Enums, Structs, o Tuplas desestructuradas
Variables
Wildcards
Placeholders
Para usar un patrón, lo comparamos con algún valor. Si el patrón coincide con el valor,
usamos las partes de valor en nuestro código. Recuerde las expresiones match en el
Capítulo 6 que usaron patrones, como el ejemplo de la máquina clasificadora de monedas.
Si el valor se ajusta a la forma del patrón, podemos usar las piezas con nombre. Si no lo
hace, el código asociado con el patrón no se ejecutará.
Este capítulo es una referencia sobre todo lo relacionado con los patrones. Cubriremos los
lugares válidos para usar patrones, la diferencia entre patrones refutables e irrefutables, y
los diferentes tipos de sintaxis de patrones que puede ver. Al final del capítulo, sabrá cómo
usar patrones para expresar muchos conceptos de una manera clara.
https://book.rustlang-es.org/print 508/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Opciones de match
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
Por ejemplo, aquí está la expresión match del Listado 6-5 que coincide con un valor
Option<i32> en la variable x :
match x {
None => None,
Some(i) => Some(i + 1),
}
Los patterns en esta expresión match son el None y el Some(i) a la izquierda de cada
flecha.
Un requisito para las expresiones match es que deben ser exhaustivas en el sentido de que
todas las posibilidades para el valor en la expresión match deben tenerse en cuenta. Una
forma de asegurarse de haber cubierto todas las posibilidades es tener un patrón de
captura para el último brazo: por ejemplo, un nombre de variable que coincida con
cualquier valor nunca puede fallar y, por lo tanto, cubre todos los casos restantes.
El patrón específico _ coincidirá con cualquier cosa, pero nunca se une a una variable, por
lo que a menudo se usa en la última opción de coincidencia. El patrón _ puede ser útil
cuando desea ignorar cualquier valor no especificado, por ejemplo. Cubriremos el patrón _
con más detalle en la sección “Ignorar valores en un patrón” más adelante en este capítulo.
https://book.rustlang-es.org/print 509/656
12/7/24, 22:37 El Lenguaje de Programación Rust
En el capítulo 6 discutimos cómo usar expresiones if let principalmente como una forma
más corta de escribir el equivalente de un match que solo coincide con un caso.
Opcionalmente, if let puede tener un else correspondiente que contenga código para
ejecutar si el patrón en el if let no coincide.
El Listado 18-1 muestra que también es posible mezclar y combinar expresiones if let ,
else if y else if let . Hacerlo nos da más flexibilidad que una expresión match 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 let se
relacionen entre sí.
El código en el Listado 18-1 determina de qué color hacer su fondo en función de una serie
de comprobaciones para varias condiciones. Para este ejemplo, hemos creado variables con
valores codificados que un programa real podría recibir de la entrada del usuario.
Filename: src/main.rs
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
Si el usuario especifica un color favorito, ese color se usa como fondo. Si no se especifica un
color favorito y hoy es martes, el color de fondo es verde. De lo contrario, si el usuario
especifica su edad como una cadena y podemos analizarla como un número con éxito, el
color es púrpura o naranja dependiendo del valor del número. Si ninguna de estas
condiciones se aplica, el color de fondo es azul.
Una estructura condicional nos permite cumplir con requisitos complejos. Con los valores
codificados que tenemos aquí, este ejemplo imprimirá Using purple as the background
color .
https://book.rustlang-es.org/print 510/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Puedes ver que if let también puede introducir variables con shadowing de la misma
manera que lo hacen las opciones match : la línea if let Ok(age) = age introduce una
nueva variable age que contiene el valor dentro de la variante Ok . Esto significa que
necesitamos colocar la condición if age > 30 dentro de ese bloque: no podemos
combinar estas dos condiciones en if let Ok (age) = age && age > 30 . El age
sombreado que queremos comparar con 30 no es válido hasta que comience el nuevo
alcance con la llave de apertura.
Similar en su construcción a if let , el bucle condicional while let permite que un bucle
while se ejecute mientras un patrón continúe coincidiendo. En el Listado 18-2 codificamos
un bucle while let que usa un vector como una pila e imprime los valores en el vector en
el orden opuesto en el que se pusieron.
stack.push(1);
stack.push(2);
stack.push(3);
Listing 18-2: Utilizando un bucle while let para imprimir valores mientras stack.pop() devuelva Some
Este ejemplo imprime 3, 2 y luego 1. El método pop toma el último elemento del vector y
devuelve Some(value) . Si el vector está vacío, pop devuelve None . El bucle while continúa
ejecutando el código en su bloque siempre que pop devuelva Some . Cuando pop devuelve
None , el bucle se detiene. Podemos usar while let para sacar todos los elementos de
nuestra pila.
Bucles for
En un bucle for , el valor que sigue directamente a la palabra clave for es un pattern. Por
ejemplo, en for x in y el x es el pattern. El Listado 18-3 demuestra cómo usar un pattern
en un bucle for para destruir, o romper, una tupla como parte del bucle for .
https://book.rustlang-es.org/print 511/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Listing 18-3: Usando un pattern en un bucle for para desestructurar una tupla
$ 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
b is at index 1
c is at index 2
Adaptamos un iterator usando el método enumerate para que produzca un valor y el índice
de ese valor, colocado en una tupla. El primer valor producido es la tupla (0, 'a') . Cuando
este valor se corresponde con el pattern (index, value) , index será 0 y value será
'a' , imprimiendo la primera línea del output.
Sentencias let
Antes de este capítulo, solo habíamos discutido explícitamente el uso de patterns con
match e if let , pero de hecho, también hemos usado patterns en otros lugares,
incluyendo en las sentencias let . Por ejemplo, considera esta asignación de variable
directa con let :
let x = 5;
Cada vez que has utilizado una declaración let como esta, has estado usando patterns,
aunque es posible que no te hayas dado cuenta. Más formalmente, una sentencia let se
ve así:
https://book.rustlang-es.org/print 512/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Listing 18-4: Usando un pattern para desestructurar una tupla y crear tres variables a la vez
Aquí, emparejamos una tupla con un pattern. Rust compara el valor (1, 2, 3) con el
pattern (x, y, z) y ve que el valor y el pattern coinciden, por lo que Rust asigna 1 a x , 2
a y y 3 a z . Puedes pensar que este pattern de tupla anida tres patterns de variable
individuales dentro de él.
Listing 18-5: Al construir incorrectamente un pattern cuyas variables no coinciden con el número de elementos
en la tupla
$ 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 solucionar el error, podríamos ignorar uno o más valores en la tupla utilizando _ o
.. , como verás en la sección “Ignorando valores en un pattern”. Si el problema es que
tenemos demasiadas variables en el pattern, 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 en
la tupla.
https://book.rustlang-es.org/print 513/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Parámetros de función
Los parámetros de función también pueden ser patterns. El código del Listado 18-6, que
declara una función llamada foo que toma un parámetro llamado x de tipo i32 , debería
ser familiar a estas alturas.
fn foo(x: i32) {
// code goes here
}
Listing 18-6: La firma de una función que utiliza patterns en los parámetros
¡La parte x es un pattern! Como hicimos con let , podríamos hacer coincidir una tupla en
los argumentos de una función con el pattern. El Listado 18-7 divide los valores en una tupla
a medida que la pasamos a una función.
Filename: src/main.rs
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
Listing 18-7: Una función con parámetros que desetructura una tupla
Este código imprime Current location: (3, 5) . El valor (3, 5) coincide con el pattern
(x, y) , por lo que x es 3 y y es 5 .
Hasta ahora, has visto varias formas de usar patrones, pero los patrones no funcionarán de
la misma manera en todos los lugares donde podemos usarlos. En algunos casos, los
patrones deben ser irrefutables; en otras circunstancias, pueden ser refutables.
Discutiremos estos dos conceptos a continuación.
https://book.rustlang-es.org/print 514/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los parámetros de funciones, las declaraciones let y los bucles for solo pueden aceptar
patterns irrefutables, porque el programa no puede hacer nada significativo cuando los
valores no coinciden. Las expresiones if let y while let aceptan patterns refutables e
irrefutables, pero el compilador advierte contra los patterns irrefutables porque, por
definición, están destinados a manejar posibles fallas: la funcionalidad de una condicional
está en su capacidad de realizar de manera diferente dependiendo del éxito o el fracaso.
Veamos un ejemplo de lo que sucede cuando intentamos usar un pattern refutable donde
Rust requiere un pattern irrefutable y viceversa. El Listado 18-8 muestra una declaración
let , pero para el pattern hemos especificado Some(x) , un pattern refutable. Como puede
imaginar, este código no se compilará.
https://book.rustlang-es.org/print 515/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
Debido a que no hemos cubierto (¡y no pudimos cubrir!) Cada valor válido con el pattern
Some(x) , Rust produce un error del compilador.
Listing 18-9: Usando if let y un bloque con patterns refutables en lugar de let
¡Le hemos dado una solución al código! Este código es perfectamente válido ahora. Sin
embargo, significa que no podemos usar un pattern irrefutable sin recibir un error. Si le
damos a if let un pattern que siempre coincidirá, como x , como se muestra en el
Listado 18-10, el compilador dará una advertencia.
if let x = 5 {
println!("{x}");
};
Rust se queja de que no tiene sentido usar if let con un pattern irrefutable:
https://book.rustlang-es.org/print 516/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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, las opciones del match deben usar patterns refutables, excepto por la última
opción, que debe coincidir con cualquier valor restante con un pattern irrefutable. Rust nos
permite usar un pattern irrefutable en un match con solo un brazo, pero esta sintaxis no es
particularmente útil y podría reemplazarse con una declaración let más simple.
Ahora que sabes dónde usar patterns y la diferencia entre patterns refutables e irrefutables,
cubramos toda la sintaxis que podemos usar para crear patterns.
https://book.rustlang-es.org/print 517/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Como viste en el Capítulo 6, puedes hacer coincidir patterns contra literales directamente. El
siguiente código da algunos ejemplos:
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
Este código imprime one porque el valor en x es 1. Esta sintaxis es útil cuando quieres que
tu código tome una acción si obtiene un valor concreto particular.
Las variables nombradas son patterns irrefutables que coinciden con cualquier valor, y las
hemos usado muchas veces en el libro. Sin embargo, hay una complicación cuando usas
variables nombradas en expresiones match . Debido a que match inicia un nuevo alcance,
las variables declaradas como parte de un pattern dentro de la expresión match ocultarán
aquellas con el mismo nombre fuera del constructo match , como es el caso de todas las
variables. En el Listado 18-11, declaramos una variable llamada x con el valor Some(5) y
una variable y con el valor 10 . Luego creamos una expresión match en el valor x . Mira
los patterns en las opciones match y println! al final, e intenta averiguar qué imprimirá el
código antes de ejecutar este código o leer más.
Filename: src/main.rs
https://book.rustlang-es.org/print 518/656
12/7/24, 22:37 El Lenguaje de Programación Rust
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {x:?}"),
}
Listing 18-11: Una expresión match con una opción que introduce una variable sombreada (shadowing) y
El pattern en la segunda opción de match introduce una nueva variable llamada y que
coincidirá con cualquier valor dentro de un valor Some . Debido a que estamos en un nuevo
scope dentro de la expresión match , esta es una nueva variable y , no la que declaramos al
principio con el valor 10. Este nuevo enlace y coincidirá con cualquier valor dentro de un
Some , que es lo que tenemos en x . Por lo tanto, este nuevo y se vincula al valor interno de
Some en x . Ese valor es 5 , por lo que la expresión para esa opción se ejecuta e imprime
Matched, y = 5 .
Si x hubiera sido un None en lugar de Some(5) , los patterns en las dos primeras opciones
no habrían coincidido, por lo que el valor habría coincidido con el guion bajo. No
introdujimos la variable x en el pattern de la opción del guion bajo, por lo que el x en la
expresión sigue siendo el x externo que no ha sido sombreado. En este caso hipotético, el
match imprimiría Default case, x = None .
Cuando la expresión match termina, su scope termina, y también lo hace el scope del y
interno. El último println! produce at the end: x = Some(5), y = 10 .
Para crear una expresión match que compare los valores del x e y externos en lugar de
introducir una variable sombreada, necesitaríamos usar una condición de guardia de
match . Hablaremos de las guardias de match más adelante en la sección “Condicionales
adicionales con match guards”
Múltiples Patterns
En las expresiones match , puedes coincidir con múltiples patrones usando la sintaxis | ,
que es el operador or del pattern. Por ejemplo, en el siguiente código hacemos coincidir el
valor de x con las opciones de match , el primero de los cuales tiene una opción or, lo que
significa que si el valor de x coincide con cualquiera de los valores en esa opción, se
ejecutará el código de esa opción:
https://book.rustlang-es.org/print 519/656
12/7/24, 22:37 El Lenguaje de Programación Rust
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
La sintaxis ..= nos permite emparejar un rango inclusivo de valores. En el siguiente código,
cuando un patrón coincide con cualquiera de los valores dentro del rango dado, esa opción
se ejecutará:
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
El compilador verifica que el rango no esté vacío en tiempo de compilación, y debido a que
los únicos tipos para los que Rust puede decir si un rango está vacío o no son los valores
numéricos y char , los rangos solo están permitidos con valores numéricos o char .
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
Rust puede determinar que 'c' se encuentra dentro del rango especificado en el primer
pattern y se muestra por pantalla early ASCII letter .
https://book.rustlang-es.org/print 520/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Podemos usar patterns para desestructurar structs, enums y tuplas para utilizar diferentes
partes de estos valores. Veamos cada uno de ellos.
Desestructurando Structs
El Listado 18-12 muestra un struct Point con dos campos, x e y , que podemos
desestructurar usando un pattern con una declaración let .
Filename: src/main.rs
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
Este código crear las variables a y b que coinciden con los valores de los campos x e y
del struct p . Este ejemplo muestra que los nombres de las variables en el pattern no tienen
que coincidir con los nombres de los campos del struct. Sin embargo, es común que los
nombres de las variables coincidan con los nombres de los campos para facilitar recordar
qué variables provienen de qué campos. Debido a este uso común, y porque escribir let
Point { x: x, y: y } = p; contiene mucha duplicación, Rust tiene una abreviatura para
los patterns que coinciden con los campos de los structs: solo necesitas listar el nombre del
campo del struct, y las variables creadas a partir del pattern tendrán los mismos nombres.
El Listado 18-13 se comporta de la misma manera que el código del Listado 18-12, pero las
variables creadas en el pattern let son x e y en lugar de a y b .
Filename: src/main.rs
https://book.rustlang-es.org/print 521/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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);
}
Listing 18-13: Desestructurando los campos de un struct utilizando la forma abreviada de los campos struct
Este código crea las variables x e y que coinciden con los campos x e y del struct p . El
resultado es que las variables x e y contienen los valores de los campos x e y del struct.
También podemos desestructurar y con valores literales como parte del pattern del struct
en lugar de crear variables para todos los campos. Hacerlo nos permite probar algunos de
los campos para valores particulares mientras creamos variables para desestructurar los
otros campos.
En el Listado 18-14, tenemos una expresión match que separa los valores de Point en tres
casos: puntos que se encuentran directamente en el eje x (lo cual es cierto cuando y = 0 ),
en el eje y ( x = 0 ), o ninguno.
Filename: src/main.rs
fn main() {
let p = Point { x: 0, y: 7 };
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})");
}
}
}
El primer bloque coincidirá con cualquier punto que se encuentre en el eje x especificando
que el campo y debe coincidir con el valor 0 . El pattern aún crea una variable x que
podemos usar en el código de este bloque.
https://book.rustlang-es.org/print 522/656
12/7/24, 22:37 El Lenguaje de Programación Rust
del campo y . El tercer bloque no especifica literales, por lo que coincide con cualquier otro
Point y crea variables para ambos campos x e y .
En este ejemplo, el valor p coincide con el segundo bloque debido a que x contiene un 0 ,
por lo que este código imprimirá On the y axis at 7 .
Recuerda que una expresión match detiene la verificación de los bloques una vez que ha
encontrado el primer patrón que coincide, por lo que, aunque Point { x: 0, y: 0 } está
en el eje x y en el eje y , este código solo imprimirá On the x axis at 0 .
Desestructurando Enums
Hemos desestructurado enums en este libro (por ejemplo, el Listado 6-5 en el Capítulo 6),
pero aún no hemos discutido explícitamente que el pattern para desestructurar un enum
corresponde a la forma en que se define los datos almacenados dentro del enum. Como
ejemplo, en el Listado 18-15 usamos el enum Message del Listado 6-2 y escribimos un
match con patterns que desestructuran cada valor interno.
Filename: src/main.rs
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
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}")
}
}
}
Listing 18-15: Desestructurando variantes enum que contienen diferentes tipos de valores
Este código imprimirá Change the color to red 0, green 160, and blue 255 . Prueba
cambiar el valor de msg para ver el código de las otras opciones.
https://book.rustlang-es.org/print 523/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para variantes de enum sin ningún dato, como Message::Quit , no podemos desestructurar
el valor más allá. Solo podemos coincidir con el valor literal Message::Quit , y no hay
variables en ese pattern.
Para variantes de enum similares a structs, como Message::Move , podemos usar un pattern
similar al que especificamos para coincidir con structs. Después del nombre de la variante,
colocamos llaves y luego enumeramos los campos con variables para que desarmemos las
piezas para usar en el código de esta opción. Aquí usamos la forma abreviada como lo
hicimos en el Listado 18-13.
Para variantes de enum similares a tuplas, como Message::Write que contiene una tupla
con un elemento y Message::ChangeColor que contiene una tupla con tres elementos, el
pattern es similar al pattern que especificamos para coincidir con tuplas. El número de
variables en el pattern debe coincidir con el número de elementos en la variante que
estamos coincidiendo.
So far, our examples have all been matching structs or enums one level deep, but matching
can work on nested items too! For example, we can refactor the code in Listing 18-15 to
support RGB and HSV colors in the ChangeColor message, as shown in Listing 18-16.
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));
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}")
}
_ => (),
}
}
https://book.rustlang-es.org/print 524/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Podemos mezclar, combinar y anidar los patrones de desestructuración de formas aún más
complejas. El siguiente ejemplo muestra una desestructuración complicada donde
anidamos structs y tuplas dentro de una tupla y desestructuramos todos los valores
primitivos:
Este código nos permite descomponer 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 pattern, como en la última opción de un
match , para obtener una opción que no hace nada, pero que abarca todos los posibles
valores restantes. Hay varias formas de ignorar valores completos o partes en un pattern:
usando el pattern _ (que has visto), usando el pattern _ dentro de otro pattern, usando un
nombre que comienza con un guion bajo y usando .. para ignorar las partes restantes de
un valor. Exploraremos cómo y por qué usar cada uno de estos patterns.
Hemos utilizado el guion bajo como un pattern comodín que coincide con cualquier valor
pero no se enlaza con él. Esto es especialmente útil como la última opción en una expresión
match , pero también podemos usarlo en cualquier pattern, incluyendo los parámetros de
una función, como se muestra en el Listado 18-17.
Filename: src/main.rs
https://book.rustlang-es.org/print 525/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
foo(3, 4);
}
En este caso, el pattern _ se utiliza dentro de otro pattern para ignorar solo una parte del
valor. Esto puede ser útil cuando queremos probar solo una parte del valor, pero no
tenemos uso para las otras partes en el código correspondiente que queremos ejecutar. El
Listado 18-18 muestra un código encargado de gestionar el valor de una configuración. Los
requisitos son que el usuario no debe poder sobrescribir una personalización existente de
una configuración, pero puede eliminar la configuración y asignarle un valor si actualmente
no está establecida.
println!("setting is {setting_value:?}");
Listing 18-18: Utilizando un guion bajo dentro de patterns que coinciden con variantes Some cuando no
https://book.rustlang-es.org/print 526/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Este código imprimirá setting is None y luego setting is Some(5) . En la primera opción
de match , no necesitamos hacer coincidir ni usar los valores dentro de ninguna de las
variantes Some , pero si necesitamos comprobar en el caso en el que tanto setting_value
como new_setting_value son la variante Some . En ese caso, imprimimos la razón por la
que no cambiamos setting_value , y no lo cambiamos.
En todos los demás casos (si setting_value o new_setting_value son None ) expresados
por el pattern _ en la segunda opción, queremos permitir que new_setting_value se
convierta en setting_value .
También podemos usar guiones bajos en múltiples lugares dentro de un solo pattern para
ignorar valores particulares. El Listado 18-19 muestra un ejemplo de ignorar el segundo y
cuarto valores en una tupla de cinco elementos.
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}")
}
}
Si creas una variable, pero no la utilizas en ningún lugar, Rust generalmente emitirá una
advertencia porque una variable no utilizada podría causar un bug. Sin embargo, a veces es
útil poder crear una variable que aún no se utilizará, como cuando estás prototipando o
simplemente comenzando un proyecto. En esta situación, puedes decirle a Rust que no te
advierta sobre la variable no utilizada comenzando el nombre de la variable con un guion
bajo. En el Listado 18-20, creamos dos variables no utilizadas, pero cuando compilamos este
código, solo deberíamos obtener una advertencia sobre una de ellas.
Filename: src/main.rs
fn main() {
let _x = 5;
let y = 10;
}
Listing 18-20: Comenzar el nombre de una variable con un guion bajo para evitar recibir advertencias de
variables no utilizadas
https://book.rustlang-es.org/print 527/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Aquí recibimos una advertencia sobre no utilizar la variable y , pero no recibimos una
advertencia sobre no utilizar _x .
Es importante destacar que hay una diferencia sutil entre usar solo _ y usar un nombre
que comienza con un guion bajo. La sintaxis _x todavía enlaza el valor a la variable,
mientras que _ no enlaza en absoluto. Para mostrar un caso en el que esta distinción
importa, el Listado 18-21 nos proporcionará un error.
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
Listing 18-21: Una variable no utilizada que comienza con un guion bajo aún vincula el valor, lo que puede
tomar ownership del valor
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
Con los valores que tiene muchas partes, podemos usar la sintaxis .. para usar partes
específicas e ignorar el resto, evitando la necesidad de enumerar guiones bajos para cada
valor ignorado. El pattern .. ignora cualquier parte de un valor que no hayamos coincidido
explícitamente en el resto del pattern. En el Listado 18-23, tenemos un struct Point que
contiene una coordenada en el espacio tridimensional. En la expresión match , queremos
operar solo en la coordenada x e ignorar los valores en los campos y y z .
https://book.rustlang-es.org/print 528/656
12/7/24, 22:37 El Lenguaje de Programación Rust
struct Point {
x: i32,
y: i32,
z: i32,
}
match origin {
Point { x, .. } => println!("x is {x}"),
}
Listing 18-23: Ignorando todos los campos de un Point excepto x mediante el uso de ..
Listamos el valor x y luego simplemente incluimos el pattern .. . Esto es más rápido que
tener que listar y: _ y z: _ , particularmente cuando estamos trabajando con structs que
tienen muchos campos en situaciones en las que solo uno o dos campos son relevantes.
La sintaxis .. se expandirá a tantos valores como sea necesario. El Listado 18-24 muestra
cómo usar .. con una tupla.
Filename: src/main.rs
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
Listing 18-24: Coincidir solo con el primer y último valor en una tupla e ignorar todos los demás valores
En este código, el primer y último valor se coinciden con first y last . El .. coincidirá con
cualquier número de valores entre el primero y el último.
Sin embargo, el uso de .. debe ser inequívoco. Si no está claro qué valores deben coincidir
y cuáles deben ignorarse, Rust nos dará un error. El Listado 18-25 muestra un ejemplo de
usar .. de manera ambigua, por lo que no se compilará.
Filename: src/main.rs
https://book.rustlang-es.org/print 529/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
$ 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
Es imposible para Rust determinar cuántos valores en la tupla ignorar antes de hacer
coincidir un valor con second y luego cuántos valores más ignorar después. Este código
podría significar que queremos ignorar 2 , vincular second a 4 y luego ignorar 8 , 16 y
32 ; o que queremos ignorar 2 y 4 , vincular second a 8 y luego ignorar 16 y 32 ; y así
sucesivamente. El nombre de la variable second no significa nada especial para Rust, por lo
que obtenemos un error del compilador porque usar .. en dos lugares como este es
ambiguo.
Un match guard es una condición adicional if , especificada después del pattern en una
opción match , que también debe coincidir para que se elija esa opción. Los match guards
son útiles para expresar ideas más complejas que las que permite un pattern solo.
https://book.rustlang-es.org/print 530/656
12/7/24, 22:37 El Lenguaje de Programación Rust
match num {
Some(x) if x % 2 == 0 => println!("The number {x} is even"),
Some(x) => println!("The number {x} is odd"),
None => (),
}
Este ejemplo imprimirá The number 4 is even . Cuando num se compara con el pattern en
la primera opción, coincide, porque Some(4) coincide con Some(x) . Luego, el match guard
verifica si el resto de dividir x por 2 es igual a 0, y porque lo es, se selecciona la primera
opción.
Si num hubiera sido Some(5) , el match guard en la primera opción habría sido falso porque
el resto de 5 dividido por 2 es 1, que no es igual a 0. Rust entonces pasaría a la segunda
opción, que coincidiría porque la segunda opción no tiene un match guard y, por lo tanto,
coincide con cualquier variante Some .
En el Listado 18-11, mencionamos que podríamos usar match guards para resolver nuestro
problema de shadowing de pattern. Recordemos que creamos una nueva variable dentro
del pattern en la expresión match en lugar de usar la variable fuera del match . Esa nueva
variable significaba que no podíamos probar contra el valor de la variable externa. El Listado
18-27 muestra cómo podemos usar un match guard para solucionar este problema.
Filename: src/main.rs
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {n}"),
_ => println!("Default case, x = {x:?}"),
}
Listing 18-27: Utilizando un match guard para probar la igualdad con una variable externa
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
(4 | 5 | 6) if y => ...
en lugar de esto:
4 | 5 | (6 if y) => ...
https://book.rustlang-es.org/print 532/656
12/7/24, 22:37 El Lenguaje de Programación Rust
@ Bindings
El operador @ , conocido como at, nos permite crear una variable que almacena un valor al
mismo tiempo que lo comprobamos para una coincidencia de pattern. En el Listado 18-29,
queremos probar que el campo id de un Message::Hello está dentro del rango 3..=7 .
También queremos vincular el valor a la variable id_variable para poder usarlo en el
código asociado con la opción. Podríamos nombrar esta variable id , igual 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}"),
}
Listing 18-29: Usando @ para enlazar un valor en un pattern mientras también lo testeamos
En la última opción, donde hemos especificado una variable sin un rango, sí tenemos el
valor disponible para usar en el código de la opción en una variable llamada id . La razón es
que hemos usado la sintaxis de campo de struct shorthand. Pero no hemos aplicado
ninguna prueba al valor en el campo id en esta opción, como hicimos con las dos primeras
opciones: cualquier valor coincidiría con este pattern.
Usar @ nos permite probar un valor y guardarlo en una variable dentro de un mismo
pattern.
https://book.rustlang-es.org/print 533/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Resumen
Los patterns en Rust son muy útiles para distinguir entre diferentes tipos de datos. Cuando
se usan en expresiones match , Rust garantiza que tus patterns cubran todos los valores
posibles, o tu programa no se compilará. Los patterns en las declaraciones let y en los
parámetros de las funciones hacen que esos constructos sean más útiles, permitiendo la
deconstrucción de valores en partes más pequeñas al mismo tiempo que se asignan a
variables. Podemos crear patterns simples o complejos para adaptarse a nuestras
necesidades.
https://book.rustlang-es.org/print 534/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Características Avanzadas
Por ahora, has aprendido las partes más comúnmente usadas del lenguaje de
programación Rust. Antes de hacer otro proyecto en el Capítulo 20, veremos algunos
aspectos del lenguaje que podrías encontrar de vez en cuando, pero que no usarás todos
los días. Puede usar este capítulo como referencia para cuando encuentre algo
desconocido. Las características cubiertas aquí son útiles en situaciones muy específicas.
Aunque es posible que no alcances a menudo, queremos asegurarnos de que tengas un
dominio de todas las características que Rust tiene para ofrecer.
Unsafe Rust: cómo optar por algunas de las garantías de Rust y asumir la
responsabilidad de mantener manualmente esas garantías
Traits avanzados: tipos asociados, parámetros de tipo predeterminados, sintaxis
completamente calificada, supertraits y el patrón newtype en relación con los traits
Tipos avanzados: más sobre el pattern newtype, tipo alias, el tipo never y tipos de
tamaño dinámico
Funciones y closures avanzados: punteros a funciones y devolución de closures
Macros: formas de definir código que define más código en tiempo de compilación
¡Es una panoplia de características de Rust con algo para todos! ¡Vamos a sumergirnos!
https://book.rustlang-es.org/print 535/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Unsafe Rust
Todo el código que hemos discutido hasta ahora ha tenido las garantías de seguridad de
memoria de Rust aplicadas en tiempo de compilación. Sin embargo, Rust tiene un segundo
lenguaje oculto dentro de él que no hace cumplir estas garantías de seguridad de memoria:
se llama unsafe Rust y funciona como Rust regular, pero nos da superpoderes adicionales.
Unsafe Rust existe porque, por naturaleza, el análisis estático es conservador. Cuando el
compilador intenta determinar si el código cumple o no con las garantías, es mejor que
rechace algunos programas válidos que aceptar algunos programas no válidos. Aunque el
código podría estar bien, si el compilador de Rust no tiene suficiente información para estar
seguro, rechazará el código. En estos casos, puede usar código inseguro para decirle al
compilador: "Confía en mí, sé lo que estoy haciendo". Sin embargo, debes tener cuidado, ya
que el uso de Unsafe Rust conlleva riesgos: si usas código inseguro de manera incorrecta,
pueden ocurrir problemas debido a la inseguridad de la memoria, como la
desreferenciación de puntero nulo.
Otra razón por la que Rust tiene un alter ego inseguro es que el hardware informático
subyacente es inherentemente inseguro. Si Rust no le permitiera realizar operaciones
inseguras, no podría realizar ciertas tareas. Rust necesita permitirle realizar programación
de sistemas de bajo nivel, como interactuar directamente con el sistema operativo o incluso
escribir su propio sistema operativo. Trabajar con programación de sistemas de bajo nivel
es uno de los objetivos del lenguaje. Veamos qué podemos hacer con Rust inseguro y cómo
hacerlo.
Superpoderes Unsafe
Para cambiar a Unsafe Rust, use la palabra clave unsafe y luego comience un nuevo bloque
que contenga el código inseguro. Puede tomar cinco acciones en Rust inseguro que no
puede en Rust seguro, que llamamos superpoderes Unsafe. Esos superpoderes incluyen la
capacidad de:
https://book.rustlang-es.org/print 536/656
12/7/24, 22:37 El Lenguaje de Programación Rust
luego no son verificadas por el compilador para la seguridad de la memoria. Aún obtendrá
cierto grado de seguridad dentro de un bloque inseguro.
Además, unsafe no 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, se asegurará de que el código dentro de un bloque unsafe acceda a la
memoria de una manera válida.
Las personas son falibles y pueden cometer errores, pero al requerir que estas cinco
operaciones inseguras estén dentro de bloques anotados con unsafe , sabrá que cualquier
error relacionado con la seguridad de la memoria debe estar dentro de un bloque unsafe .
Mantenga los bloques unsafe pequeños; lo agradecerá más tarde cuando investigue bugs
de memoria.
Para aislar el código inseguro tanto como sea posible, es mejor encerrar el código inseguro
dentro de una abstracción segura y proporcionar una API segura, que discutiremos más
adelante en el capítulo cuando examinemos las funciones y métodos inseguros. Partes de la
biblioteca estándar se implementan como abstracciones seguras sobre código inseguro que
ha sido auditado. Envolver el código inseguro en una abstracción segura evita que los usos
de unsafe se filtren en todos los lugares que usted o sus usuarios puedan querer usar la
funcionalidad implementada con código unsafe , porque usar una abstracción segura es
seguro.
Veamos cada uno de los cinco superpoderes unsafe a su vez. También veremos algunas
abstracciones que proporcionan una interfaz segura al código inseguro.
mencionamos que el compilador garantiza que las referencias siempre son válidas.
Unsafe Rust tiene dos nuevos tipos llamados punteros crudos que son similares a las
referencias. Al igual que con las referencias, los punteros crudos pueden ser inmutables o
mutables y se escriben como *const T y *mut T , respectivamente. El asterisco no es el
operador de desreferencia; es parte del nombre del tipo. En el contexto de los punteros
crudos, inmutable significa que el puntero no se puede asignar directamente después de ser
desreferenciado.
Son permitidos ignorar las reglas de borrowing al tener tanto punteros inmutables
como mutables o múltiples punteros mutables al mismo lugar
No se garantiza que apunten a una memoria válida
Se les permite ser nulos
No implementan ninguna limpieza automática
https://book.rustlang-es.org/print 537/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Al optar por no hacer que Rust haga cumplir 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 Listing 19-1 muestra cómo crear un puntero crudo inmutable y mutable a partir de
referencias.
Observa que no incluimos la palabra clave unsafe en este código. Podemos crear punteros
crudos en código seguro; simplemente no podemos desreferenciar punteros crudos fuera
de un bloque unsafe , como verás en un momento.
Hemos creado punteros crudos utilizando as para convertir una referencia inmutable y
una mutable en sus tipos de puntero crudo correspondientes. Como los creamos
directamente a partir de referencias garantizadas como válidas, sabemos que estos
punteros crudos particulares son válidos, pero no podemos hacer esa suposición sobre
cualquier puntero crudo.
Para demostrar esto, a continuación crearemos un puntero crudo cuya validez no podemos
estar tan seguros. El Listado 19-2 muestra cómo crear un puntero crudo a una ubicación
arbitraria en la memoria. Intentar usar memoria arbitraria es indefinido: puede haber datos
en esa dirección o no, el compilador puede optimizar el código para que no haya acceso a la
memoria, o el programa puede generar un error con un fallo de segmentación. Por lo
general, no hay una buena razón para escribir código como este, pero es posible.
Recuerda que podemos crear punteros crudos en código seguro, pero no podemos
desreferenciar punteros crudos y leer la memoria a la que apuntan fuera de un bloque
unsafe .
https://book.rustlang-es.org/print 538/656
12/7/24, 22:37 El Lenguaje de Programación Rust
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
Crear un puntero no causa daño; solo cuando intentamos acceder al valor al que apunta
que podríamos terminar tratando con un valor no válido.
También ten en cuenta que en los Listados 19-1 y 19-3, creamos *const i32 y *mut i32
punteros crudos que apuntaban a la misma ubicación de memoria, donde se almacena
num . Si en su lugar intentáramos crear una referencia inmutable y mutable a num , el código
no se compilaría porque las reglas de ownership de Rust no permiten una referencia
mutable al mismo tiempo que cualquier referencia inmutable. Con punteros crudos,
podemos crear un puntero mutable y un puntero inmutable a la misma ubicación y cambiar
los datos a través del puntero mutable, potencialmente creando una carrera de datos. ¡Ten
cuidado!
Con todos estos peligros, ¿por qué usarías punteros crudos? Un caso de uso importante es
cuando se interactúa con código C, como verás en la siguiente sección, “Llamando a una
función o método inseguro”. Otro caso es cuando se construyen abstracciones seguras que
el borrow checker no entiende. Presentaremos funciones inseguras y luego veremos un
ejemplo de una abstracción segura que usa código inseguro.
El segundo tipo de operación que solo se puede realizar en un bloque unsafe es llamar a
una función o método inseguro. Podemos crear funciones inseguras y métodos inseguros
que se ven exactamente como funciones y métodos regulares, pero tienen un unsafe
adicional antes del resto de la definición. La palabra clave unsafe en este contexto indica
que la función tiene requisitos que debemos cumplir cuando llamamos a esta función
porque Rust no puede garantizar que hayamos cumplido con estos requisitos. Al llamar a
una función insegura dentro de un bloque unsafe , estamos diciendo que hemos leído la
documentación de esta función y asumimos la responsabilidad de cumplir con los contratos
de la función.
Aquí hay un ejemplo de una función insegura llamada dangerous que no hace nada en su
cuerpo:
https://book.rustlang-es.org/print 539/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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
|
= 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
Con el bloque unsafe , le estamos indicando a Rust que hemos leído la documentación de la
función, entendemos cómo usarla correctamente y hemos verificado que estamos
cumpliendo con el contrato de la función.
Los cuerpos de las funciones unsafe son similares a los bloques unsafe , por lo que para
realizar otras operaciones unsafe dentro de una función unsafe , no necesitamos agregar
otro bloque unsafe .
Solo porque una función contiene código inseguro no significa que debamos marcar toda la
función como insegura. De hecho, envolver el código inseguro en una función segura es una
abstracción común. Como ejemplo, estudiemos la función split_at_mut de la biblioteca
estándar, que requiere algo de código inseguro. Exploraremos cómo podríamos
implementarlo. Este método seguro está definido en slices mutables: toma un slice y lo
divide en dos al dividir el slice en el índice dado como argumento. El Listado 19-4 muestra
cómo usar split_at_mut .
https://book.rustlang-es.org/print 540/656
12/7/24, 22:37 El Lenguaje de Programación Rust
No podemos implementar esta función utilizando solo Rust seguro. Un intento podría ser
algo como el Listado 19-5, que no se compilará. Para simplificar, implementaremos
split_at_mut como una función en lugar de un método y solo para slices de valores i32
en lugar de para un tipo genérico T .
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
Esta función primero obtiene la longitud total del slice. Luego verifica si el índice dado como
parámetro está dentro del slice al verificar si es menor o igual a la longitud. La aserción
significa que si pasamos un índice que es mayor que la longitud para dividir el slice, la
función entrará en panic antes de intentar usar ese índice.
Luego, devolvemos dos slices mutables en una tupla: uno desde el inicio del slice original
hasta el índice mid y otro desde mid hasta el final del slice.
https://book.rustlang-es.org/print 541/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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`
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1
previous error
El borrow checker de Rust no puede entender que estamos tomando prestado diferentes
partes del slice; solo sabe que estamos tomando prestado el mismo slice dos veces. Tomar
prestadas diferentes partes de un slice es fundamentalmente correcto porque los dos slices
no se superponen, pero Rust no es lo suficientemente inteligente como para saber esto.
Cuando sabemos que el código está bien, pero Rust no lo sabe, es hora de recurrir al código
inseguro.
El Listado 19-6 muestra cómo usar un bloque unsafe , un puntero sin procesar y algunas
llamadas a funciones inseguras para hacer que la implementación de split_at_mut
funcione.
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),
)
}
}
Recordemos la sección “The Slice Type” del Capítulo 4 que los slices son un puntero a
algunos datos y la longitud del slice. Usamos el método len para obtener la longitud del
slice y el método as_mut_ptr para acceder al puntero sin procesar de un slice. En este caso,
https://book.rustlang-es.org/print 542/656
12/7/24, 22:37 El Lenguaje de Programación Rust
porque tenemos un slice mutable a valores i32 , as_mut_ptr devuelve un puntero sin
procesar con el tipo *mut i32 , que hemos almacenado en la variable ptr .
Mantenemos la afirmación de que el índice mid está dentro del slice. Luego llegamos al
código inseguro: la función slice::from_raw_parts_mut toma un puntero sin procesar y
una longitud, y crea un slice. Usamos esta función para crear un slice que comienza desde
ptr y es mid elementos de largo. Luego llamamos al método add en ptr con mid como
argumento para obtener un puntero sin procesar que comienza en mid , y creamos un slice
usando ese puntero y el número restante de elementos después de mid como la longitud.
use std::slice;
A veces, tu código en Rust necesita interactuar con código escrito en otro lenguaje. Para
esto, Rust tiene la palabra clave extern que facilita la creación y el uso de una Foreign
https://book.rustlang-es.org/print 543/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Function Interface (FFI). Una FFI es una forma para que un lenguaje de programación defina
funciones y permita que un lenguaje de programación diferente (extranjero) llame a esas
funciones.
El Listado 19-8 demuestra cómo configurar una integración con la función abs de la
biblioteca estándar de C. Las funciones declaradas dentro de bloques extern siempre son
inseguras de llamar desde el código Rust. La razón es que otros lenguajes no hacen cumplir
las reglas y garantías de Rust, y Rust no puede verificarlas, por lo que la responsabilidad
recae en el programador para garantizar la seguridad.
Filename: src/main.rs
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
Listing 19-8: Declarando y llamando a una función extern definida en otro lenguaje
Dentro del bloque extern "C" en el Listado 19-8, enumeramos los nombres y las firmas de
las funciones externas que queremos llamar. El nombre y la firma de la función abs se
definen en el estándar C y son parte de la biblioteca estándar de C. La firma de la función
abs es int abs(int) , lo que significa que toma un argumento int y devuelve un int . La
función abs devuelve el valor absoluto de su argumento.
También podemos usar extern para crear una interfaz que permita que otros
lenguajes llamen funciones Rust. En lugar de crear un bloque extern , podemos
agregar la palabra clave extern y especificar la ABI a usar justo antes de la palabra
clave fn para la función relevante. También necesitamos agregar una anotación #
[no_mangle] para decirle al compilador de Rust que no cambie el nombre de esta
función. Mangling es cuando un compilador cambia el nombre que le hemos dado a
una función a un nombre diferente que contiene más información para otras partes
del proceso de compilación para consumir, , pero es menos legible para los humanos.
Cada compilador de lenguaje de programación mangla los nombres de manera
ligeramente diferente, por lo que para que una función Rust sea nombrable por otros
lenguajes, debemos deshabilitar el mangling del compilador de Rust.
#[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 variables globales, las cuales Rust admite, pero
pueden ser problemáticas con las reglas de ownership de Rust. Si dos hilos acceden a la
misma variable global mutable, puede causar una condición de carrera.
En Rust, las variables globales son llamadas variables static. El Listado 19-9 muestra un
ejemplo de declaración y uso de una variable static con un string slice como valor.
Filename: src/main.rs
fn main() {
println!("name is: {HELLO_WORLD}");
}
Las static variables son similares a las constantes, que discutimos en la sección "Diferencias
entre variables y constantes" en el Capítulo 3. Los nombres de las variables static están en
SCREAMING_SNAKE_CASE por convención. Las variables static solo pueden almacenar
referencias con el lifetime 'static , lo que significa que el compilador de Rust puede
calcular el lifetime y no estamos obligados a anotarlo explícitamente. Acceder a una variable
static inmutable es seguro.
Una diferencia sutil entre constantes y variables static inmutables es que los valores en una
variable static tienen una dirección fija en la memoria. Usar el valor siempre accederá a los
mismos datos. Las constantes, por otro lado, pueden duplicar sus datos cada vez que se
usan. Otra diferencia es que las variables static pueden ser mutables. Acceder y modificar
variables static mutables es inseguro. El Listado 19-10 muestra cómo declarar, acceder y
modificar una variable static mutable llamada COUNTER .
Filename: src/main.rs
https://book.rustlang-es.org/print 545/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {COUNTER}");
}
}
Como con las variables regulares, especificamos la mutabilidad usando la palabra clave
mut . Cualquier código que lea o escriba desde COUNTER debe estar dentro de un bloque
unsafe . Este código se compila e imprime COUNTER: 3 como esperaríamos porque es de
un solo hilo. Tener múltiples hilos accediendo a COUNTER , probablemente habría
condiciones de carrera.
Con datos mutables que son accesibles globalmente, es difícil asegurarse de que no haya
carreras de datos, por lo que Rust considera que las variables static mutables son inseguras.
Cuando sea posible, es preferible usar las técnicas de concurrencia y los smart pointers
seguros para los hilos que discutimos en el Capítulo 16, para que el compilador verifique
que los datos a los que se accede desde diferentes hilos se hagan de manera segura.
Podemos usar unsafe para implementar un trait inseguro. Un trait se considera inseguro
cuando al menos uno de sus métodos tiene algún invariante que el compilador no puede
verificar. Declaramos que un trait es unsafe agregando la palabra clave unsafe antes de
trait y marcando la implementación del trait como unsafe también, como se muestra en
el Listado 19-11.
fn main() {}
Al utilizar unsafe impl , estamos prometiendo que mantendremos las invariantes que el
compilador no puede verificar.
Como ejemplo, recordemos los marcadores de traits Sync y Send que discutimos en la
sección "Concurrencia extensible con los traits Sync y Send " en el Capítulo 16: el
compilador implementa estos traits automáticamente si nuestros tipos se componen
únicamente de tipos Send y Sync . Si implementamos un tipo que contiene un tipo que no
es Send o Sync , como punteros crudos, y queremos marcar ese tipo como Send o Sync ,
debemos usar unsafe . Rust no puede verificar que nuestro tipo cumpla con las garantías
de que se puede enviar seguramente a través de hilos o acceder desde múltiples hilos; por
lo tanto, debemos hacer esas comprobaciones manualmente e indicarlo con unsafe .
La última acción que solo se puede realizar con unsafe es acceder a los campos de una
union. Una union es similar a una struct , pero solo un campo declarado se usa en una
instancia particular en un momento dado. Las unions se usan principalmente para
interactuar con unions en código C. Acceder a los campos de la union es inseguro porque
Rust no puede garantizar el tipo de los datos que se almacenan actualmente en la instancia
de la union. Puedes aprender más sobre las uniones en la Referencia de Rust.
Utilizar unsafe para llevar a cabo una de las cinco acciones (superpoderes) que se acaban
de mencionar no está mal ni se desaconseja. Sin embargo, es más difícil obtener código
unsafe correcto porque el compilador no puede ayudar a mantener la seguridad de la
memoria. Cuando tengas una razón para usar código unsafe , puedes hacerlo, y tener la
anotación unsafe explícita hace que sea más fácil rastrear la fuente de los problemas
cuando ocurren.
https://book.rustlang-es.org/print 547/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Traits Avanzados
Primero cubrimos los traits en la sección "Traits: Defining Shared Behavior" del Capítulo 10,
pero no discutimos los detalles más avanzados. Ahora que conoces más Rust, podemos
entrar en los detalles más minuciosos.
Los tipos asociados conectan un marcador de tipo con un trait de modo que los métodos de
definición de trait puedan usar estos marcadores de tipo en sus firmas. El implementador
de un trait especificará el tipo concreto que se utilizará en lugar del tipo de marcador para la
implementación particular. De esa manera, podemos definir un trait que use algunos tipos
sin necesidad de saber exactamente cuáles son esos tipos hasta que se implemente el trait.
Hemos descrito la mayoría de las características avanzadas en este capítulo como poco
necesarias. Los tipos asociados están en algún lugar en el medio: se utilizan con menos
frecuencia que las características explicadas en el resto del libro, pero con más frecuencia
que muchas de las otras características discutidas en este capítulo.
Un ejemplo de un trait con un tipo asociado es el trait Iterator que la biblioteca estándar
proporciona. El tipo asociado se llama Item y representa el tipo de los valores que el tipo
que implementa el trait Iterator está iterando. La definición del trait Iterator es como
se muestra en el Listado 19-12.
Listing 19-12: La definición del trait Iterator que tiene un tipo asociado Item
El tipo Item es un marcador de tipo, y la definición del método next muestra que
devolverá valores del tipo Option<Self::Item> . Los implementadores del trait Iterator
especificarán el tipo concreto para Item , y el método next devolverá una Option que
contiene un valor de ese tipo concreto.
Los tipos asociados pueden parecer un concepto similar a los generics, ya que estos últimos
nos permiten definir una función sin especificar qué tipos puede manejar. Para examinar la
diferencia entre los dos conceptos, veremos una implementación del trait Iterator en un
tipo llamado Counter que especifica que el tipo Item es u32 :
https://book.rustlang-es.org/print 548/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
Esta sintaxis parece comparable a la de los generics. Entonces, ¿por qué no definir
simplemente el trait Iterator con generics, como se muestra en el Listado 19-13?
Listing 19-13: Una definición hipotética del trait Iterator usando generics
La diferencia es que cuando usamos generics, como en el Listado 19-13, debemos anotar
los tipos en cada implementación; porque también podemos implementar
Iterator<String> for Counter o cualquier otro tipo, podríamos tener múltiples
implementaciones de Iterator para Counter . En otras palabras, cuando un trait tiene un
parámetro genérico, puede implementarse para un tipo múltiples veces, cambiando los
tipos concretos de los parámetros genéricos de tipo cada vez. Cuando usamos el método
next en Counter , tendríamos que proporcionar anotaciones de tipo para indicar qué
implementación de Iterator queremos usar.
Con los tipos asociados, no necesitamos anotar los tipos porque no podemos implementar
un trait en un tipo múltiples veces. En el Listado 19-12 con la definición que usa tipos
asociados, solo podemos elegir cuál será el tipo de Item una vez, porque solo puede haber
un impl Iterator for Counter . No tenemos que especificar que queremos un iterador de
valores u32 en todas partes que llamamos a next en Counter .
Los tipos asociados también forman parte del contrato del trait: los implementadores del
trait deben proporcionar un tipo para que se use en lugar del marcador de tipo. Los tipos
asociados a menudo tienen un nombre que describe cómo se usará el tipo, y documentar el
tipo asociado en la documentación de la API es una buena práctica.
https://book.rustlang-es.org/print 549/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Un ejemplo excelente de una situación en la que esta técnica es útil es con la sobrecarga de
operadores, en la que personalizas el comportamiento de un operador (como + ) en
situaciones particulares.
Rust no te permite crear tus propios operadores o sobrecargar operadores arbitrarios. Pero
puedes sobrecargar las operaciones y los traits correspondientes enumerados en std::ops
implementando los traits asociados con el operador. Por ejemplo, en el Listado 19-14
sobrecargamos el operador + para agregar dos instancias de Point juntas. Hacemos esto
implementando el trait Add en un struct Point :
Filename: src/main.rs
use std::ops::Add;
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
Listing 19-14: Implementando el trait Add para sobrecargar el operador + para instancias Point
El método add suma los valores x de dos instancias Point y los valores y de dos
instancias Point para crear una nueva instancia Point . El trait Add tiene un tipo asociado
llamado Output que determina el tipo devuelto desde el método add .
El tipo generic predeterminado en este código está dentro del trait Add . Aquí está su
definición:
https://book.rustlang-es.org/print 550/656
12/7/24, 22:37 El Lenguaje de Programación Rust
trait Add<Rhs=Self> {
type Output;
Este código debería resultar familiar en general: un trait con un método y un tipo asociado.
La nueva parte es Rhs=Self : esta sintaxis se llama parámetros de tipo predeterminados. El
parámetro de tipo generic Rhs (abreviatura de “lado derecho”) define el tipo del parámetro
rhs en el método add . Si no especificamos un tipo concreto para Rhs cuando
implementamos el trait Add , el tipo de Rhs será predeterminado a Self , que será el tipo
en el que estamos implementando Add .
Cuando implementamos Add para Point , utilizamos el valor predeterminado para Rhs
porque queremos agregar dos Point instancias. Veamos un ejemplo de implementación
del trait Add donde queremos personalizar el tipo Rhs en lugar de usar el predeterminado.
Filename: src/lib.rs
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
Listing 19-15: Implementando el trait Add en Millimeters para sumar Millimeters a Meters
https://book.rustlang-es.org/print 551/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Nada en Rust impide que un trait tenga un método con el mismo nombre que el método de
otro trait, ni Rust te impide implementar ambos traits en un solo tipo. También es posible
implementar un método directamente en el tipo con el mismo nombre que los métodos de
los traits.
Cuando llamas a métodos con el mismo nombre, necesitarás decirle a Rust cuál quieres
usar. Considera el código en el Listado 19-16 donde hemos definido dos traits, Pilot y
Wizard , que ambos tienen un método llamado fly . Luego implementamos ambos traits
en un tipo Human que ya tiene un método llamado fly implementado en él. Cada método
fly hace algo diferente.
Filename: src/main.rs
https://book.rustlang-es.org/print 552/656
12/7/24, 22:37 El Lenguaje de Programación Rust
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
Listing 19-16: Se definen dos traits para tener un método fly y se implementan en el tipo Human , además se
implementa directamente un método fly en Human
Cuando llamamos al método fly en 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 19-17.
Filename: src/main.rs
fn main() {
let person = Human;
person.fly();
}
Ejecutando este código imprimirá *waving arms furiously* , mostrando que Rust llamó al
método fly implementado directamente en Human .
Para llamar a los métodos fly de los traits Pilot o Wizard , necesitamos usar una sintaxis
más explícita para especificar cuál método fly queremos llamar. El Listado 19-18
demuestra esta sintaxis.
Filename: src/main.rs
https://book.rustlang-es.org/print 553/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
Listing 19-18: Especificando qué método fly del trait queremos llamar
Especificar el nombre del trait antes del nombre del método aclara a Rust qué
implementación del método fly queremos llamar. También podríamos escribir
Human::fly(&person) ; esto es equivalente a person.fly() , pero es un poco más largo de
escribir 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*
Debido a que el método fly toma un parámetro self , si tuviéramos dos tipos que
implementan el mismo trait, Rust podría determinar cuál implementación del trait utilizar en
función del tipo de self .
Sin embargo, las funciones asociadas que no son métodos no tienen un parámetro self .
Cuando hay múltiples tipos o traits que definen funciones no métodos con el mismo
nombre de función, Rust no siempre sabe a qué tipo te refieres a menos que uses sintaxis
completamente calificada. Por ejemplo, en el Listado 19-19 creamos un trait para un refugio
de animales que quiere nombrar a todos los perros bebés Spot. Creamos un trait Animal
con una función no método asociada baby_name . El trait Animal se implementa para la
estructura Dog , en la que también proporcionamos una función no método asociada
baby_name directamente.
Filename: src/main.rs
https://book.rustlang-es.org/print 554/656
12/7/24, 22:37 El Lenguaje de Programación Rust
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
Listing 19-19: Un trait con una función asociada y un tipo con una función asociada del mismo nombre que
también implementa el trait
Implementamos el código para nombrar a todos los cachorros Spot en la función asociada
baby_name que se define en Dog . El tipo Dog también implementa el trait Animal , que
describe las características que todos los animales tienen. Los cachorros de perro se llaman
cachorros, y eso se expresa en la implementación del trait Animal en Dog en la función
baby_name asociada con el trait Animal .
$ 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
Filename: src/main.rs
https://book.rustlang-es.org/print 555/656
12/7/24, 22:37 El Lenguaje de Programación Rust
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
Listing 19-20: Al intentar llamar a la función baby_name del trait Animal , Rust no sabe qué implementación usar
Debido a que Animal::baby_name no tiene un parámetro self y podría haber otros tipos
que implementen el trait Animal , Rust no puede averiguar qué implementación de
Animal::baby_name queremos. Obtendremos este error de compilación:
$ 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
Para desambiguar y decirle a Rust que queremos usar la implementación de Animal para
Dog en lugar de la implementación de Animal para algún otro tipo, necesitamos usar la
sintaxis completamente calificada. El Listado 19-21 demuestra cómo usar la sintaxis
completamente calificada.
Filename: src/main.rs
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
Listing 19-21: Usando la sintaxis completamente calificada para especificar que queremos llamar a la función
baby_name del trait Animal implementado en Dog
Estamos proporcionando a Rust una anotación de tipo dentro de los corchetes angulares, lo
que indica que queremos llamar al método baby_name del trait Animal implementado en
Dog diciendo que queremos tratar el tipo Dog como un Animal para esta llamada de
función. Este código ahora imprimirá lo que queremos:
https://book.rustlang-es.org/print 556/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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 son métodos, no habría un receiver : solo habría una
lista de otros argumentos. Podrías usar la sintaxis completamente calificada en todas partes
donde llames a funciones o métodos. Sin embargo, se te permite omitir cualquier parte de
esta sintaxis que Rust pueda deducir de otra información en el programa. Solo necesitas
usar esta sintaxis más verbosa en casos en los que haya múltiples implementaciones que
usen el mismo nombre y Rust necesite ayuda para identificar qué implementación quieres
llamar.
A veces, es posible que desees escribir una definición de trait que dependa de otro trait:
para que un tipo implemente el primer trait, quieres exigir que este tipo también
implemente el segundo trait. Esto se hace para que la definición de tu trait pueda hacer uso
de los elementos asociados del segundo trait. El trait en el que se basa la definición de tu
trait se llama supertrait de tu trait.
Por ejemplo, supongamos que queremos crear un trait llamado OutlinePrint con un
método outline_print que imprima un valor dado enmarcado entre asteriscos. Es decir,
dado un struct Point que implementa el trait de la biblioteca estándar Display para que
el resultado sea (x, y) , cuando llamemos a outline_print en una instancia de Point
que tenga 1 para x y 3 para y , debería imprimir lo siguiente:
**********
* *
* (1, 3) *
* *
**********
Filename: src/main.rs
use std::fmt;
Dado que hemos especificado que OutlinePrint requiere el trait Display , el utilizar la
función to_string que se implementa automáticamente para cualquier tipo que
implemente Display está bien. Si intentáramos usar to_string sin agregar dos puntos y
especificar el trait Display después del nombre del trait, obtendríamos un error diciendo
que no se encontró ningún método llamado to_string para el tipo &Self en el scope
actual.
Veamos qué sucede cuando intentamos usar OutlinePrint en un tipo que no implementa
Display , como el struct Point :
Filename: src/main.rs
struct Point {
x: i32,
y: i32,
}
Obtenemos un error que indica que se requiere implementar Display , pero no está
implementado:
https://book.rustlang-es.org/print 558/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
Filename: src/main.rs
use std::fmt;
https://book.rustlang-es.org/print 559/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Como ejemplo, supongamos que queremos implementar Display en Vec<T> , lo cual nos
impide hacerlo directamente debido a regla de los "orphan rules", ya que el trait Display y
el tipo Vec<T> están definidos fuera de nuestro crate. Podemos hacer un struct llamado
Wrapper que contenga una instancia de Vec<T> . Luego podemos implementar Display en
Wrapper y usar el valor de Vec<T> , como se muestra en el Listado 19-23.
Filename: src/main.rs
use std::fmt;
struct Wrapper(Vec<String>);
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {w}");
}
Listing 19-23: Crear un tipo Wrapper alrededor de Vec<String> para implementar Display
https://book.rustlang-es.org/print 560/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La desventaja de usar esta técnica es que Wrapper es un nuevo tipo, por lo que no tiene los
métodos del valor que contiene. Tendríamos que implementar todos los métodos de
Vec<T> directamente en Wrapper de tal manera que los métodos deleguen a self.0 , lo
que nos permitiría tratar a Wrapper exactamente como un Vec<T> . Si quisiéramos que el
nuevo tipo tenga todos los métodos del tipo interno, implementar el trait Deref (discutido
en el capítulo 15 en la sección “Tratando a los smart pointers como referencias regulares
con el trait Deref ”) en Wrapper para devolver el tipo interno sería una solución. Si no
queremos que el tipo Wrapper tenga todos los métodos del tipo interno, por ejemplo, para
restringir el comportamiento del tipo Wrapper , tendríamos que implementar manualmente
solo los métodos que queremos.
El pattern newtype también es útil incluso cuando no se involucran traits. Ahora cambiemos
de enfoque y exploremos algunas formas avanzadas de interactuar con el sistema de tipos
de Rust.
https://book.rustlang-es.org/print 561/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Tipos Avanzados
El sistema de tipos de Rust tiene algunas características que hemos mencionado hasta
ahora, pero que aún no hemos discutido. Comenzaremos discutiendo los newtypes en
general mientras examinamos por qué los newtypes son útiles como tipos. Luego
pasaremos a los type aliases, una característica similar a los newtypes pero con semántica
ligeramente diferente. También discutiremos el tipo ! y los tipos de tamaño dinámico.
Nota: Esta sección asume que has leído la sección anterior “Usando el Pattern Newtype
para Implementar Traits Externos en Tipos Externos.”
El newtype pattern también es útil para tareas más allá de las que hemos discutido hasta
ahora, incluyendo hacer cumplir estáticamente que los valores nunca se confundan e
indicar las unidades de un valor. Viste un ejemplo de usar newtypes para indicar unidades
en el Listado 19-15: recuerda que los structs Millimeters y Meters envolvieron valores
u32 en un newtype. Si escribiéramos una función con un parámetro de tipo Millimeters ,
no podríamos compilar un programa que accidentalmente intentara llamar a esa función
con un valor de tipo Meters o un u32 simple.
Los newtypes también pueden ocultar la implementación interna. Por ejemplo, podríamos
proporcionar un tipo People para envolver un HashMap<i32, String> que almacena el ID
de una persona asociado con su nombre. El código que usa People solo interactuaría con la
API pública que proporcionamos, como un método para agregar un string de nombre a la
colección People ; ese código no necesitaría saber que asignamos un ID i32 a los nombres
internamente. El newtype pattern es una forma ligera de lograr la encapsulación para
ocultar los detalles de implementación, que discutimos en la sección “Encapsulación que
Oculta los Detalles de Implementación” del Capítulo 17.
Rust proporciona la capacidad de declarar un type alias para darle a un tipo existente otro
nombre. Para esto usamos la palabra clave type . Por ejemplo, podemos crear el alias
Kilometers a i32 de la siguiente manera:
https://book.rustlang-es.org/print 562/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Ahora, el alias Kilometers es un sinónimo para i32 ; a diferencia de los tipos Millimeters
y Meters que creamos en el Listado 19-15, Kilometers no es un tipo nuevo y separado.
Los valores que tienen el tipo Kilometers se tratarán de la misma manera que los valores
del tipo i32 :
let x: i32 = 5;
let y: Kilometers = 5;
Debido a que Kilometers e i32 son el mismo tipo, podemos agregar valores de ambos
tipos y podemos pasar valores Kilometers a funciones que toman parámetros i32 . Sin
embargo, usando este método, no obtenemos los beneficios de verificación de tipos que
obtenemos del newtype pattern discutido anteriormente. En otras palabras, si mezclamos
valores Kilometers e i32 en algún lugar, el compilador no nos dará un error.
El caso de uso principal para los sinónimos de tipo es reducir la repetición. Por ejemplo,
podríamos tener un tipo largo como este:
Escribir este tipo extenso en firmas de función y como anotaciones de tipo en todo el código
puede ser tedioso y propenso a errores. Imagina tener un proyecto lleno de código como el
del Listado 19-24.
Un type alias hace que este código sea más manejable al reducir la repetición. En el Listado
19-25, hemos introducido un alias llamado Thunk para el tipo extenso y podemos
reemplazar todos los usos del tipo con el alias más corto Thunk .
https://book.rustlang-es.org/print 563/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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 es una palabra para
código que se evaluará en un momento posterior, por lo que es un nombre apropiado para
un cierre que se almacena).
Los type alias también se utilizan con frecuencia con el tipo Result<T, E> para reducir la
repetición. Considera el módulo std::io en la biblioteca estándar. Las operaciones de E/S
a menudo devuelven un Result<T, E> para manejar situaciones en las que las operaciones
no funcionan. Esta biblioteca tiene una estructura std::io::Error que representa todos
los posibles errores de E/S. Muchas de las funciones en std::io devolverán Result<T, E>
donde E es std::io::Error , como estas funciones en el trait Write :
use std::fmt;
use std::io::Error;
Él Result<..., Error> se repite mucho. Como tal, std::io tiene esta declaración de alias
de tipo:
Dado que esta declaración está en el módulo std::io , podemos usar el alias
completamente calificado std::io::Result<T> ; es decir, un Result<T, E> con E lleno
como std::io::Error . Las firmas de las funciones del trait Write terminan viéndose así:
https://book.rustlang-es.org/print 564/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El type alias ayuda de dos maneras: hace que el código sea más fácil de escribir y nos da una
interfaz consistente en todo std::io . Debido a que es un alias, es solo otro Result<T, E> ,
lo que significa que podemos usar cualquier método que funcione en Result<T, E> con él,
así como la sintaxis especial como el operador ? .
Rust tiene un tipo especial llamado ! , conocido en la jerga de la teoría de tipos como tipo
vacío porque no tiene valores. Preferimos llamarlo tipo never porque se encuentra en el
lugar del tipo de retorno cuando una función nunca retornará. Aquí hay un ejemplo:
fn bar() -> ! {
// --snip--
}
Este código se lee como “la función bar devuelve never”. Las funciones que devuelven
never se llaman funciones divergentes. No podemos crear valores del tipo ! por lo que bar
nunca puede devolver.
Pero, ¿qué utilidad tiene un tipo del que nunca se pueden crear valores? Recuerda el código
del Juego de Adivinar el Número mostrado en el Listado 2-5; hemos reproducido parte de él
aquí en el Listado 19-26.
En ese momento, omitimos algunos detalles en este código. En el Capítulo 6 en la sección “El
operador de control de flujo match ” discutimos que las opciones de match deben devolver
todos el mismo tipo. Por lo tanto, por ejemplo, el siguiente código no funciona:
https://book.rustlang-es.org/print 565/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El tipo de guess en este código tendría que ser un entero y un string, y Rust requiere que
guess tenga solo un tipo. Entonces, ¿qué devuelve continue ? ¿Cómo se nos permitió
devolver un u32 de una opción y tener otra opción que termina con continue en el Listado
19-26?
Como habrás adivinado, continue tiene un valor ! . Es decir, cuando Rust calcula el tipo de
guess , mira ambas opciones de match , el primero con un valor de u32 y el segundo con
un valor de ! . Debido a que ! nunca puede tener un valor, Rust decide que el tipo de
guess es u32 .
El tipo never también es útil con la macro panic! . Recordemos la función unwrap que
llamamos en valores Option<T> para producir un valor o generar un panic 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"),
}
}
}
en este código, ocurre lo mismo que en el match del Listado 19-26: Rust ve que val tiene el
tipo T y panic! tiene el tipo ! , por lo que el resultado de la expresión match es T . Este
código funciona porque panic! no produce un valor; termina el programa. En el caso de
None , no devolveremos un valor de unwrap , por lo que este código es válido.
print!("forever ");
loop {
print!("and ever ");
}
Aquí, el bucle nunca termina, por lo que ! es el valor de la expresión. Sin embargo, esto no
sería cierto si incluyéramos un break , porque el bucle terminaría cuando llegara al break .
https://book.rustlang-es.org/print 566/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Rust necesita conocer ciertos detalles sobre sus tipos, como la cantidad de espacio para
asignar a un valor de un tipo particular. Esto deja una esquina de su sistema de tipos un
poco confusa al principio: el concepto de tipos de tamaño dinámico. A veces se refiere como
DST o tipos no dimensionados, estos tipos nos permiten escribir código usando valores cuyo
tamaño solo podemos conocer en tiempo de ejecución.
Profundicemos en los detalles de un tipo de tamaño dinámico llamado str , que hemos
estado usando a lo largo del libro. Así es, no &str , sino str por sí solo, es un DST. No
podemos saber cuánto tiempo es la cadena hasta el tiempo de ejecución, lo que significa
que no podemos crear una variable de tipo str , ni podemos tomar un argumento de tipo
str . Considera el siguiente código, que no funciona:
Rust necesita conocer cuánta memoria asignar para cualquier valor de un tipo particular, y
todos los valores de un tipo deben usar la misma cantidad de memoria. Si Rust nos
permitiera escribir este código, estos dos valores str necesitarían ocupar el mismo
espacio. Pero tienen longitudes diferentes: s1 necesita 12 bytes de almacenamiento y s2
necesita 15. Es por eso que no es posible crear una variable que contenga un tipo de
tamaño dinámico.
Entonces, ¿qué hacemos en este caso? En este caso, como ya sabes, la solución es hacer que
los tipos de s1 y s2 sean &str en lugar de str . Recuerda de la sección “String Slices” del
Capítulo 4 que la estructura de datos de slice solo almacena la posición de inicio y la
longitud del slice. Por lo tanto, aunque un &T es un solo valor que almacena la dirección de
memoria de donde se encuentra el T , un &str son dos valores: la dirección del str y su
longitud. Como tal, podemos conocer el tamaño de un valor &str en tiempo de
compilación: es dos veces la longitud de un usize . Es decir, siempre conocemos el tamaño
de un &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 debemos envolverlos en algún tipo de puntero.
Podemos combinar str con todo tipo de punteros: por ejemplo, Box<str> o Rc<str> . De
hecho, ya has visto esto antes, pero con un tipo de tamaño dinámico diferente: los traits.
Cada trait es un tipo de tamaño dinámico al que podemos referirnos usando el nombre del
trait. En el Capítulo 17 en la sección “Usando trait objects que permiten valores de
diferentes tipos”, mencionamos que para usar traits como objetos de trait, debemos
ponerlos detrás de un puntero, como &dyn Trait o Box<dyn Trait> ( Rc<dyn Trait>
también funcionaría).
Para trabajar con DST, Rust proporciona el trait Sized para determinar si el tamaño de un
tipo es conocido en tiempo de compilación o no. Este trait se implementa automáticamente
https://book.rustlang-es.org/print 567/656
12/7/24, 22:37 El Lenguaje de Programación Rust
para todo lo cuyo tamaño es conocido en tiempo de compilación. Además, Rust agrega
implícitamente un límite en Sized a cada función generic. Es decir, una definición de
función generic como esta:
fn generic<T>(t: T) {
// --snip--
}
fn generic<T: Sized>(t: T) {
// --snip--
}
De forma predeterminada, las funciones generic solo funcionarán en tipos que tienen un
tamaño conocido en tiempo de compilación. Sin embargo, puede usar la siguiente sintaxis
especial para relajar esta restricción:
Un trait bound en ?Sized significa “ T puede o no ser Sized ” y esta notación anula el valor
predeterminado de que los tipos generic deben tener un tamaño conocido en tiempo de
compilación. La sintaxis ?Trait con este significado solo está disponible para Sized , no
para ningún otro trait.
También debes tener en cuenta que hemos cambiado el tipo del parámetro t de T a &T .
Debido a que el tipo puede no ser Sized , necesitamos usarlo detrás de algún tipo de
puntero. En este caso, hemos elegido una referencia.
https://book.rustlang-es.org/print 568/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Function Pointers
Hemos hablado de cómo pasar closures a funciones; ¡también puedes pasar funciones
regulares a funciones! Esta técnica es útil cuando quieres pasar una función que ya has
definido en lugar de definir un nuevo closure. Las funciones se coercen al tipo fn (con una f
minúscula), no confundir con el trait de cierre Fn . El tipo fn se llama puntero a función.
Pasar funciones con punteros a función te permitirá usar funciones como argumentos para
otras funciones.
Filename: src/main.rs
fn main() {
let answer = do_twice(add_one, 5);
Listing 19-27: Usando el tipo fn para aceptar un puntero a function como un argumento
Este código imprime The answer is: 12 . Especificamos que el parámetro f en do_twice
es un fn que toma un parámetro de tipo i32 y devuelve un i32 . Luego podemos llamar a
f en el cuerpo de do_twice . En main , podemos pasar el nombre de la función add_one
como el primer argumento a do_twice .
https://book.rustlang-es.org/print 569/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los punteros a funciones implementan los tres closure traits ( Fn , FnMut y FnOnce ), lo que
significa que siempre puedes pasar un puntero a función como un argumento para una
función que espera un closure. Es mejor escribir funciones usando un tipo generic y uno de
los closure traits para que tus funciones puedan aceptar funciones o closures.
Como ejemplo de dónde podrías usar un closure definido en línea o una función nombrada,
veamos un uso del método map proporcionado por el trait Iterator en la biblioteca
estándar. Para usar la función map para convertir un vector de números en un vector de
strings, podríamos usar un closure, como este:
O podríamos nombrar una función como argumento para map en lugar del closure, como
este:
Ten en cuenta que debemos utilizar la sintaxis completamente calificada que mencionamos
anteriormente en la sección “Traits avanzados”
Aquí, estamos usando la función to_string definida en el trait ToString , que la biblioteca
estándar ha implementado para cualquier tipo que implemente Display .
Recuerda la sección “Valores de Enum” del Capítulo 6, que el nombre de cada variante de
enum que definimos también se convierte en una función inicializadora. Podemos usar
estas funciones inicializadoras como punteros a función que implementan los closure traits,
lo que significa que podemos especificar las funciones inicializadoras como argumentos
para los métodos que toman closures, como se muestra a continuación:
https://book.rustlang-es.org/print 570/656
12/7/24, 22:37 El Lenguaje de Programación Rust
enum Status {
Value(u32),
Stop,
}
Aquí creamos instancias de Status::Value usando cada valor u32 en el rango en el que se
llama a map usando la función inicializadora de Status::Value . A algunas personas les
gusta este estilo, y a otras les gusta usar closures. Compilan al mismo código, así que usa el
estilo que sea más claro para ti.
Retornando Closures
Los closures se representan mediante traits, lo que significa que no puedes devolver
closures directamente. En la mayoría de los casos en los que podrías querer devolver un
trait, puedes usar en su lugar el tipo concreto que implementa el trait como el valor de
retorno de la función. Sin embargo, no puedes hacer eso con los closures porque no tienen
un tipo concreto que se pueda devolver; no se te permite usar el puntero a función fn
como un tipo de retorno, por ejemplo.
https://book.rustlang-es.org/print 571/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
--> src/lib.rs:1:25
|
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^ doesn't have a size known at
compile-time
|
help: return an `impl Trait` instead of a `dyn Trait`, if all returned values
are the same type
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ~~~~
help: box the return type, and wrap all of the returned values in `Box::new`
|
1 ~ fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
2 ~ Box::new(|x| x + 1)
|
For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` (lib) due to 1 previous error
¡El error hace referencia nuevamente al trait Sized ! Rust no sabe cuánto espacio necesitará
para almacenar el closure. Vimos una solución a este problema anteriormente. Podemos
usar un trait object:
Este código se compilará correctamente. Para obtener más información sobre los trait
objects, consulta la sección “Usando trait objects que permiten valores de diferentes tipos”
en el Capítulo 17.
https://book.rustlang-es.org/print 572/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Macros
Hemos utilizado macros como 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 declarativas con macro_rules! y tres tipos de
macros procedurales:
Hablaremos de cada uno de estos a su vez, pero primero, veamos por qué necesitamos
macros cuando ya tenemos funciones.
Fundamentalmente, las macros son una forma de escribir código que escribe otro código, lo
que se conoce como metaprogramación. En el Apéndice C, discutimos el atributo derive ,
que genera una implementación de varios traits para ti. También hemos usado las macros
println! y vec! a lo largo del libro. Todas estas macros se expanden para producir más
código que el código que has escrito manualmente.
La metaprogramación es útil para reducir la cantidad de código que tienes que escribir y
mantener, que también es uno de los roles de las funciones. Sin embargo, las macros tienen
algunos poderes adicionales que las funciones no tienen.
Una función debe declarar el número y el tipo de parámetros que tiene la función. Las
macros, por otro lado, pueden tomar un número variable de parámetros: podemos llamar a
println!("hello") con un argumento o println!("hello {}", name) con dos
argumentos. Además, las macros se expanden antes de que el compilador interprete el
significado del código, por lo que una macro puede, por ejemplo, implementar un trait en
un tipo dado. Una función no puede, porque se llama en tiempo de ejecución y un trait debe
implementarse en tiempo de compilación.
La desventaja de implementar una macro en lugar de una función es que las definiciones de
macros son más complejas que las definiciones de funciones porque estás escribiendo
código Rust que escribe código Rust. Debido a esta indirección, las definiciones de macros
generalmente son más difíciles de leer, entender y mantener que las definiciones de
funciones.
https://book.rustlang-es.org/print 573/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Otra diferencia importante entre las macros y las funciones es que debes definir macros o
traerlas al scope antes de llamarlas en un archivo, a diferencia de las funciones que puedes
definir en cualquier lugar y llamar en cualquier lugar.
Para definir una macro, usas el constructor macro_rules! . Exploremos cómo usar
macro_rules! mirando cómo se define la macro vec! . El Capítulo 8 cubrió cómo podemos
usar la macro vec! para crear un nuevo vector con valores particulares. Por ejemplo, la
siguiente macro crea un nuevo vector que contiene tres enteros:
También podemos usar la macro vec! para crear un vector de dos enteros o un vector de
cinco string slices. No podríamos usar una función porque no conoceríamos el número o el
tipo de valores.
Filename: src/lib.rs
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
https://book.rustlang-es.org/print 574/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Nota: La definición real de la macro vec! en la biblioteca estándar incluye código para
preasignar la cantidad correcta de memoria por adelantado. Ese código es una
optimización que no incluimos aquí para hacer el ejemplo más simple.
La anotación #[macro_export] indica que esta macro debe estar disponible siempre que la
biblioteca en la que se define la macro se traiga al scope. Sin esta anotación, la macro no se
puede traer al scope.
La sintaxis válida del pattern en una macro es diferente de la sintaxis de los patterns
cubiertos en el Capítulo 18 porque los patterns de macro se comparan con la estructura del
código Rust en lugar de con valores. Recorramos lo que significan las piezas del pattern en
el Listado 19-28; para obtener la sintaxis completa del pattern de macro, consulte la
Referencia de Rust.
Primero, usamos un conjunto de paréntesis para englobar todo el patrón. Usamos el signo
de dólar ( $ ) para declarar una variable en el sistema de macros que contendrá el código
Rust que coincida con el patrón. El signo de dólar hace que quede claro que esta es una
variable de macro en lugar de una variable regular de Rust. A continuación, viene un
conjunto de paréntesis que captura los valores que coinciden con el patrón dentro de los
paréntesis para su uso en el código de reemplazo. Dentro de $() está $x:expr , que
coincide con cualquier expresión de Rust y le da el nombre $x .
La coma que sigue a $() índica que opcionalmente podría aparecer un carácter de coma
separador literal después del código que coincide con el código en $() . Él * especifica que
el patrón coincide cero o más veces con lo que precede al * .
Cuando llamamos a esta macro con vec![1, 2, 3]; , el patrón $x coincide tres veces con
las tres expresiones 1 , 2 y 3 .
Ahora veamos el pattern en el cuerpo del código asociado con esta opción:
temp_vec.push($x); . Dentro de $()* se genera para cada parte que coincide con $() en
el patrón cero o más veces dependiendo de cuántas veces coincida el patrón. Él $x se
reemplaza con cada expresión que coincida. Cuando llamamos a esta macro con vec![1,
2, 3]; , el código generado que reemplaza esta llamada a la macro será el siguiente:
https://book.rustlang-es.org/print 575/656
12/7/24, 22:37 El Lenguaje de Programación Rust
{
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.
Para obtener más información sobre cómo escribir macros, consulta la documentación en
línea u otros recursos, como “The Little Book of Rust Macros” iniciado por Daniel Keep y
continuado por Lukas Wirth.
Las macros procedurales, que es la segunda forma de macros, actúan más como una
función (y son un tipo de procedimiento). Las macros procedurales aceptan código como
entrada, operan en ese código y producen código como salida en lugar de coincidir con
patrones y reemplazar el código por otro código como lo hacen las macros declarativas. Los
tres tipos de macros procedurales son derivaciones personalizadas, atributos y funciones, y
todas funcionan de manera similar.
Al crear macros procedurales, las definiciones deben residir en su propio crate con un tipo
de crate especial. Esto se debe a razones técnicas complejas que esperamos eliminar en el
futuro. En el Listado 19-29 se muestra cómo se define una macro procedural, donde
some_attribute es un marcador de posición para usar un tipo específico de macro.
Filename: src/lib.rs
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
La función que define una macro procedural tome un TokenStream como entrada y
devuelve un TokenStream como salida. El TokenStream tipo es definido por el proc_macro
crate que se incluye con Rust y representa una secuencia de tokens. Esta es la base de la
macro: el código fuente en el que la macro está operando constituye la entrada
TokenStream , y el código que la macro produce es el TokenStream de salida. La función
también tiene un atributo adjunto que especifica qué tipo de macro procedural estamos
creando. Podemos tener varios tipos de macros procedurales en el mismo crate.
https://book.rustlang-es.org/print 576/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Veamos los diferentes tipos de macros procedurales. Comenzaremos con una derivación
personalizada y luego explicaremos las pequeñas diferencias que hacen que las otras
formas sean diferentes.
Creemos un crate llamado hello_macro que defina un trait llamado HelloMacro con una
función asociada llamada hello_macro . En lugar de hacer que nuestros usuarios
implementen el trait HelloMacro para cada uno de sus tipos, proporcionaremos una macro
procedural para que los usuarios puedan anotar sus tipos con #[derive(HelloMacro)]
para obtener una implementación predeterminada de la función hello_macro . La
implementación predeterminada imprimirá Hello, Macro! My name is TypeName! , donde
TypeName es el nombre del tipo en el que se ha definido este trait. En otras palabras,
escribiremos un crate que permita a otro programador escribir código como el Listado 19-
30 usando nuestro crate.
Filename: src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
Listing 19-30: El código que un usuario de nuestro crate podrá escribir cunado se use nuestra macro procedural
Este código imprimirá Hello, Macro! My name is Pancakes! cuando hayamos terminado.
El primer paso es hacer un nuevo crate de biblioteca, así:
Filename: src/lib.rs
Tenemos un trait y su función. En este punto, nuestro usuario de crate podría implementar
el trait para lograr la funcionalidad deseada, así:
https://book.rustlang-es.org/print 577/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use hello_macro::HelloMacro;
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
Sin embargo, tendrían que escribir el bloque de implementación para cada tipo que
quisieran usar con hello_macro ; queremos evitar que tengan que hacer este trabajo.
El siguiente paso es definir la macro procedural. En el momento de escribir esto, las macros
procedurales deben estar en su propio crate. Eventualmente, esta restricción podría ser
levantada. La convención para estructurar crates y macro crates es la siguiente: para un
crate llamado foo , un crate de macro procedural de derivación personalizada se llama
foo_derive . Creemos un nuevo crate llamado hello_macro_derive dentro de nuestro
proyecto hello_macro :
Nuestros dos crates están estrechamente relacionados, por lo que creamos el crate de
macro procedural dentro del directorio de nuestro crate hello_macro . Si cambiamos la
definición del trait en hello_macro , también tendremos que cambiar la implementación de
la macro procedural en hello_macro_derive . Los dos crates deberán publicarse por
separado, y los programadores que usen estos crates deberán agregar ambos como
dependencias y traerlos a ambos al scope. En su lugar, podríamos hacer que el crate
hello_macro use hello_macro_derive como una dependencia y vuelva a exportar el
código de la macro procedural. Sin embargo, la forma en que hemos estructurado el
proyecto hace posible que los programadores usen hello_macro incluso si no quieren la
funcionalidad derive .
Filename: hello_macro_derive/Cargo.toml
https://book.rustlang-es.org/print 578/656
12/7/24, 22:37 El Lenguaje de Programación Rust
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
Para comenzar a definir la macro procedural, coloca el código del Listado 19-31 en tu
archivo src/lib.rs para el crate hello_macro_derive . Ten en cuenta que este código no se
compilará hasta que agreguemos una definición para la función impl_hello_macro .
Filename: hello_macro_derive/src/lib.rs
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();
Listing 19-31: Código que la mayoría de los crates de macros procedurales requerirán para procesar código Rust
Hemos introducido tres nuevos crates: proc_macro , syn , y quote . El crate proc_macro
viene con Rust, por lo que no necesitamos agregarlo a las dependencias en Cargo.toml. El
crate proc_macro es la API del compilador que nos permite leer y manipular código Rust
desde nuestro código.
El crate syn analiza el código Rust desde un string en una estructura de datos en la que
podemos realizar operaciones. El crate quote convierte las estructuras de datos de syn
nuevamente en código Rust. Estos crates hacen que sea mucho más simple analizar
cualquier tipo de código Rust que deseemos manipular: escribir un analizador completo
para el código Rust no es una tarea sencilla.
https://book.rustlang-es.org/print 579/656
12/7/24, 22:37 El Lenguaje de Programación Rust
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
Listing 19-32: La instancia DeriveInput que obtenemos al analizar el código que tiene el atributo de la macro
en el Listado 19-30
Los campos de este struct muestran que el código Rust que hemos analizado es un struct
unitario con el ident (identificador, es decir, el nombre) de Pancakes . Hay más campos en
este struct para describir todo tipo de código Rust; consulta la documentación de syn para
DeriveInput para obtener más información.
Es posible que hayas notado que estamos usando unwrap para hacer que la función
hello_macro_derive genere un panic si la llamada a la función syn::parse falla. Es
necesario que nuestra macro procedural genere un panic en caso de error porque las
funciones proc_macro_derive deben devolver TokenStream en lugar de Result para
cumplir con la API de las macros procedurales. Hemos simplificado este ejemplo usando
unwrap ; en código de producción, debes proporcionar mensajes de error más específicos
sobre lo que salió mal usando panic! o expect .
https://book.rustlang-es.org/print 580/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Ahora que tenemos el código para convertir el código de Rust anotado de un TokenStream
a una instancia DeriveInput , generemos el código que implementa el trait HelloMacro en
el tipo anotado, como se muestra en el Listado 19-33.
Filename: hello_macro_derive/src/lib.rs
Obtenemos una instancia del struct DeriveInput que contiene el nombre (identificador) del
tipo anotado usando ast.ident . El struct en el Listado 19-32 muestra que cuando
ejecutamos la función impl_hello_macro en el código del Listado 19-30, el ident que
obtenemos tendrá el campo ident con un valor de "Pancakes" . Por lo tanto, la variable
name en el Listado 19-33 contendrá una instancia del struct Ident que, cuando se imprima,
será la cadena "Pancakes" , el nombre del struct en el Listado 19-30.
La macro quote! nos permite construir el código que queremos devolver. El compilador
espera algo diferente al resultado directo de la ejecución de la macro quote! , por lo que
debemos convertirlo a un TokenStream . Hacemos esto llamando al método into , que
consume esta representación intermedia y devuelve un valor del tipo TokenStream
requerido.
Queremos que nuestra macro procedural genere una implementación de nuestro trait
HelloMacro para el tipo que el usuario ha anotado, lo cual podemos lograr utilizando
#name . La implementación del trait tiene la función hello_macro , cuyo cuerpo contiene la
funcionalidad que queremos proporcionar: imprimir Hello, Macro! My name is y luego el
nombre del tipo anotado.
La macro stringify! utilizada aquí está incorporada en Rust. Toma una expresión de Rust
como 1 + 2 y en tiempo de compilación convierte la expresión en un literal de string como
"1 + 2" . Esto es diferente a format! o println! , macros que evalúan la expresión y
luego convierten el resultado en un String . Existe la posibilidad de que la entrada #name
https://book.rustlang-es.org/print 581/656
12/7/24, 22:37 El Lenguaje de Programación Rust
sea una expresión para imprimir literalmente, por lo que usamos stringify! . El uso de
stringify! también ahorra una asignación al convertir #name en un literal de string en
tiempo de compilación.
Coloca el código del Listado 19-30 en src/main.rs y ejecuta cargo run : debería imprimir
Hello, Macro! My name is Pancakes! La implementación del trait HelloMacro de la
macro procedural se incluyó sin que el crate pancakes tuviera que implementarlo; la macro
#[derive(HelloMacro)] agregó la implementación del trait.
A continuación, vamos a explorar cómo los otros tipos de macros procedurales difieren de
las macros derive personalizadas.
Las macros similares a atributos son similares a las macros derivadas personalizadas, pero
en lugar de generar código para el atributo derive , permiten crear nuevos atributos.
También son más flexibles: derive solo funciona para structs y enums; los atributos se
pueden aplicar a otros items también, como funciones. Aquí hay un ejemplo de uso de una
macro similar a un atributo: digamos que tienes un atributo llamado route que anota
funciones cuando se usa un framework de aplicación web:
#[route(GET, "/")]
fn index() {
El atributo #[route] será definido por el framework como una macro procedural. La firma
de la función de definición de la macro se vería así:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
Aquí, tenemos dos parámetros de tipo TokenStream . El primero es para el contenido del
atributo: la parte GET, "/" . El segundo es el cuerpo del item al que se adjunta el atributo:
en este caso, fn index() {} y el resto del cuerpo de la función.
https://book.rustlang-es.org/print 582/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Aparte de eso, las macros similares a atributos funcionan de la misma manera que las
macros derivadas personalizadas: creas un crate con el tipo proc-macro y defines una
función que genera el código que deseas.
Las macros tipo función definen macros que se ven como llamadas a funciones. De manera
similar a las macros macro_rules! , son más flexibles que las funciones; por ejemplo,
pueden tomar un número desconocido de argumentos. Sin embargo, las macros
macro_rules! solo se pueden definir usando la sintaxis similar a la de los patterns que
discutimos en la sección “Macros declarativas con macro_rules! para metaprogramación
general” anteriormente. Las macros tipo función toman un parámetro TokenStream y su
definición manipula ese TokenStream usando código Rust como los otros dos tipos de
macros procedurales. Un ejemplo de una macro tipo función es una macro sql! que
podría ser llamada así:
Esta macro analizaría la declaración SQL dentro de ella y verificaría que sea sintácticamente
correcta, lo cual es un procesamiento mucho más complejo de lo que una macro
macro_rules! puede hacer. La macro sql! se definiría así:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
Resumen
¡Uf! Ahora que tienes algunas características de Rust en tu caja de herramientas que
probablemente no usarás a menudo, pero sabrás que están disponibles en circunstancias
muy particulares. Hemos introducido varios temas complejos para que cuando los
encuentres en sugerencias de mensajes de error o en el código de otras personas, puedas
reconocer estos conceptos y sintaxis. Usa este capítulo como referencia para guiarte hacia
soluciones.
¡A continuación, pondremos en práctica todo lo que hemos discutido a lo largo del libro y
haremos un proyecto más!
https://book.rustlang-es.org/print 583/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para nuestro proyecto final, haremos un servidor web que diga "hola" y se vea como la
Figura 20-1 en un navegador web.
https://book.rustlang-es.org/print 584/656
12/7/24, 22:37 El Lenguaje de Programación Rust
básico y el thread pool manualmente para que puedas aprender las ideas y técnicas
generales detrás de los crates que podrías usar en el futuro.
https://book.rustlang-es.org/print 585/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Los dos protocolos principales involucrados en los servidores web son Hypertext Transfer
Protocol (HTTP) y Transmission Control Protocol (TCP). Ambos protocolos son protocolos de
solicitud-respuesta, lo que significa que un cliente inicia solicitudes y un servidor escucha las
solicitudes y proporciona una respuesta al cliente. El contenido de esas solicitudes y
respuestas está definido por los protocolos.
TCP es el protocolo de nivel inferior que describe los detalles de cómo la información pasa
de un servidor a otro, pero no especifica qué es esa información. HTTP se basa en TCP
definiendo el contenido de las solicitudes y respuestas. Técnicamente, es posible usar HTTP
con otros protocolos, pero en la gran mayoría de los casos, HTTP envía sus datos a través de
TCP. Trabajaremos con los bytes sin procesar de las solicitudes y respuestas de TCP y HTTP.
Nuestro servidor web debe escuchar una conexión TCP, por lo que esa es la primera parte
en la que trabajaremos. La biblioteca estándar ofrece un módulo std::net que nos
permite hacer esto. Hagamos un nuevo proyecto de la manera habitual:
Ahora agreguemos el código en el Listado 20-1 en src/main.rs para comenzar. Este código
escuchará en la dirección local 127.0.0.1:7878 para flujos TCP entrantes. Cuando recibe
un flujo entrante, imprimirá ¡Conexión establecida! .
Filename: src/main.rs
https://book.rustlang-es.org/print 586/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
println!("Connection established!");
}
}
Listing 20-1: Escuchar transmisiones entrantes e imprimir un mensaje cuando recibimos una transmisión
La función bind en este escenario funciona como la función new en que devolverá una
nueva instancia de TcpListener . La función se llama bind porque, en redes, conectarse a
un puerto para escuchar se conoce como “enlazar a un puerto”.
La función bind devuelve un Result<T, E> , que indica que es posible que el enlace falle.
Por ejemplo, conectarse al puerto 80 requiere privilegios de administrador (los no
administradores solo pueden escuchar en puertos superiores a 1023), por lo que si
intentáramos conectarnos al puerto 80 sin ser un administrador, el enlace no funcionaría. El
enlace tampoco funcionaría, por ejemplo, si ejecutáramos dos instancias de nuestro
programa y, por lo tanto, tuvimos dos programas escuchando el mismo puerto. Debido a
que estamos escribiendo un servidor básico solo con fines de aprendizaje, no nos
preocuparemos por manejar este tipo de errores; en su lugar, usamos unwrap para detener
el programa si ocurren errores.
Por ahora, nuestro manejo del flujo consiste en llamar a unwrap para terminar nuestro
programa si el flujo tienen algún error; si no hay errores, el programa imprime un mensaje.
Agregaremos más funcionalidad para el caso de éxito en el siguiente listado. La razón por la
https://book.rustlang-es.org/print 587/656
12/7/24, 22:37 El Lenguaje de Programación Rust
que podríamos recibir errores del método incoming cuando un cliente se conecta al
servidor es que en realidad no iteramos sobre las conexiones. En cambio, iteramos sobre
intentos de conexión. La conexión podría no tener éxito por una serie de razones, muchas de
ellas específicas del sistema operativo. Por ejemplo, muchos sistemas operativos tienen un
límite para el número de conexiones abiertas simultáneas que pueden admitir; los nuevos
intentos de conexión más allá de ese número producirán un error hasta que algunas de las
conexiones abiertas se cierren.
¡Intentemos ejecutar este código! Invoca cargo run en la terminal y luego carga
127.0.0.1:7878 en un navegador web. El navegador debería mostrar un mensaje de error
como “Conexión restablecida”, porque el servidor no está enviando ningún dato
actualmente. ¡Pero cuando miras tu terminal, deberías ver varios mensajes que se
imprimieron cuando el navegador se conectó al servidor!
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
A veces, verás múltiples mensajes impresos para una solicitud del navegador; la razón
podría ser que el navegador está haciendo una solicitud para la página además de una
solicitud para otros recursos, como el icono favicon.ico que aparece en la pestaña del
navegador.
También podría ser que el navegador esté intentando conectarse al servidor varias veces
porque el servidor no está respondiendo con ningún dato. Cuando stream sale del scope y
se descarta al final del bucle, la conexión se cierra como parte de la implementación de
drop . Los navegadores a veces tratan con conexiones cerradas volviendo a intentar, porque
el problema podría ser temporal. ¡El factor importante es que hemos obtenido con éxito un
controlador para una conexión TCP!
Recuerda detener el programa presionando ctrl-c cuando hayas terminado de ejecutar una
versión particular del código. Luego reinicia el programa invocando el comando cargo run
después de haber hecho cambios de código para asegurarte de que estás ejecutando el
código más nuevo.
Leyendo la solicitud
¡Vamos a implementar la funcionalidad para leer la solicitud del navegador!. Para separar
las preocupaciones de obtener primero una conexión y luego tomar alguna acción con la
conexión, iniciaremos una nueva función para procesar conexiones. En esta nueva función
handle_connection , leeremos datos del flujo TCP e imprimiremos para que podamos ver
los datos que se envían desde el navegador. Cambia el código para que se vea como el
Listado 20-2.
https://book.rustlang-es.org/print 588/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/main.rs
use std::{
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
handle_connection(stream);
}
}
println!("Request: {http_request:#?}");
}
Creamos una variable llamada http_request para recopilar las líneas de la solicitud que el
navegador envía a nuestro servidor. Indicamos que queremos recopilar estas líneas en un
vector agregando la anotación de tipo Vec<_> .
https://book.rustlang-es.org/print 589/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El navegador señala el final de una solicitud HTTP enviando dos caracteres de nueva línea
seguidos, por lo que para obtener una solicitud del flujo, tomamos líneas hasta que
obtenemos una línea que es el string vacío. Una vez que hemos recopilado las líneas en el
vector, las imprimimos usando el formato de depuración bonito para que podamos echar
un vistazo a las instrucciones que el navegador web está enviando a nuestro servidor.
¡Probemos este código! Inicia el programa y luego carga realiza una solicitud en un
navegador web nuevamente. Ten en cuenta que aún obtendremos una página de error en
el navegador, pero la salida del programa en la terminal se verá similar a esto:
$ 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",
"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",
]
Dependiendo de tu navegador, podrías obtener una salida ligeramente diferente. Ahora que
estamos imprimiendo los datos de la solicitud, podemos ver por qué obtenemos múltiples
conexiones desde una solicitud del navegador al mirar la ruta después de GET en la
primera línea de la solicitud. Si las conexiones repetidas están solicitando /, sabemos que el
navegador está tratando de obtener / repetidamente porque no está obteniendo una
respuesta de nuestro programa.
https://book.rustlang-es.org/print 590/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La primera línea es la línea de solicitud que contiene información sobre lo que el cliente está
solicitando. La primera parte de la línea de solicitud indica el método que se está utilizando,
como GET o POST , que describe cómo el cliente está haciendo esta solicitud. Nuestro
cliente usó una solicitud GET , lo que significa que está solicitando información.
La siguiente parte de la línea de solicitud es /, que indica el Uniform Resource Identifier (URI)
que el cliente está solicitando: un URI es casi, pero no exactamente, lo mismo que un
Uniform Resource Locator (URL). La diferencia entre URIs y URLs no es importante para
nuestros propósitos en este capítulo, pero la especificación HTTP usa el término URI, por lo
que podemos simplemente sustituir mentalmente URL por URI aquí.
La última parte es la versión de HTTP que utiliza el cliente, y luego la línea de solicitud
termina en una secuencia CRLF. (CRLF significa carriage return y line feed, que son términos
de los días de la máquina de escribir!) La secuencia CRLF también se puede escribir como
\r\n , donde \r es un retorno de carro y \n es un avance de línea. La secuencia CRLF
separa la línea de solicitud del resto de los datos de la solicitud. Tenga en cuenta que
cuando se imprime el CRLF, vemos que comienza una nueva línea en lugar de \r\n .
Al examinar los datos de la línea de solicitud que hemos recibido al ejecutar nuestro
programa hasta ahora, vemos que GET es el método, / es el URI de solicitud y HTTP/1.1 es
la versión.
Después de la línea de solicitud, las líneas restantes a partir de Host: en adelante son
encabezados. Las solicitudes GET no tienen cuerpo.
Intenta hacer una solicitud desde un navegador diferente o solicitar una dirección diferente,
como 127.0.0.1:7878/test, para ver cómo cambian los datos de la solicitud.
Ahora que sabemos lo que el navegador está solicitando, ¡enviemos algunos datos de
vuelta!
Vamos a implementar el envío de datos en respuesta a una solicitud del cliente. Las
respuestas tienen el siguiente formato:
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
https://book.rustlang-es.org/print 591/656
12/7/24, 22:37 El Lenguaje de Programación Rust
de motivo que proporciona una descripción textual del código de estado. Después de la
secuencia CRLF hay encabezados, otra secuencia CRLF y el cuerpo de la respuesta.
Aquí hay un ejemplo de respuesta que usa la versión HTTP 1.1, tiene un código de estado
200, una frase de motivo OK, no tiene encabezados y no tiene cuerpo:
El código de estado 200 es la respuesta de éxito estándar. El texto es una respuesta HTTP
exitosa. ¡Escribamos esto en el flujo como nuestra respuesta a una solicitud exitosa! Desde
la función handle_connection , elimine el println! que estaba imprimiendo los datos de la
solicitud y reemplácelo con el código en el Listado 20-3.
Filename: src/main.rs
stream.write_all(response.as_bytes()).unwrap();
}
Listing 20-3: Escribiendo una pequeña respuesta HTTP exitosa en el flujo de datos
El primer cambio introduce la variable response , que contiene los datos del mensaje de
éxito. Luego, llamamos a as_bytes en nuestra response para convertir los datos de string
en bytes. El método write_all en stream toma un &[u8] y envía esos bytes directamente
por la conexión. Debido a que la operación write_all podría fallar, usamos unwrap en
cualquier resultado de error como antes. Nuevamente, en una aplicación real agregarías
manejo de errores aquí.
Con estos cambios, ejecutemos nuestro código y hagamos una solicitud. Como ya no
estamos imprimiendo ningún dato en la terminal, no veremos ninguna salida aparte de la
salida generada por Cargo. Cuando cargues 127.0.0.1:7878 en un navegador web, deberías
ver una página en blanco en lugar de un error. ¡Acabas de codificar a mano la recepción de
una solicitud HTTP y el envío de una respuesta!
Vamos a implementar la funcionalidad para devolver algo más que una página en blanco.
Crea el nuevo archivo hello.html en la raíz de tu directorio del proyecto, no en el directorio
https://book.rustlang-es.org/print 592/656
12/7/24, 22:37 El Lenguaje de Programación Rust
src. Puedes introducir cualquier HTML que quieras; el Listado 20-4 muestra una posibilidad.
Filename: hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Hello!</h1>
<p>Hi from Rust</p>
</body>
</html>
Esto es un documento HTML5 mínimo con un encabezado y un poco de texto. Para devolver
esto desde el servidor cuando se recibe una solicitud, modificaremos handle_connection
como se muestra en el Listado 20-5 para leer el archivo HTML, agregarlo a la respuesta
como un cuerpo y enviarlo.
Filename: src/main.rs
use std::{
fs,
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
};
// --snip--
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
https://book.rustlang-es.org/print 593/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Hemos agregado fs a la declaración use para traer el módulo del sistema de archivos de
la biblioteca estándar al scope. El código para leer el contenido de un archivo a una cadena
debería ser familiar; lo usamos en el Capítulo 12 cuando leímos el contenido de un archivo
para nuestro proyecto de I/O en el Listado 12-4.
A continuación, utilizamos format! para agregar el contenido del archivo como el cuerpo
de la respuesta de éxito. Para asegurar una respuesta HTTP válida, agregamos el
encabezado Content-Length que se establece en el tamaño del cuerpo de nuestra
respuesta, en este caso el tamaño de hello.html .
Ejecuta este código con cargo run y carga 127.0.0.1:7878 en tu navegador; ¡Deberías ver tu
HTML renderizado!
En este momento, nuestro servidor web devolverá el HTML del archivo sin importar lo que
el cliente haya solicitado. Agreguemos funcionalidad para verificar que el navegador esté
solicitando / antes de devolver el archivo HTML y devolver un error si el navegador solicita
cualquier otra cosa. Para esto necesitamos modificar handle_connection , como se muestra
en el Listado 20-6. Este nuevo código verifica el contenido de la solicitud recibida contra lo
que sabemos que se parece una solicitud para / y agrega bloques if y else para tratar las
solicitudes de manera diferente.
Filename: src/main.rs
https://book.rustlang-es.org/print 594/656
12/7/24, 22:37 El Lenguaje de Programación Rust
// --snip--
stream.write_all(response.as_bytes()).unwrap();
} else {
// some other request
}
}
Listing 20-6: Tratar las solicitudes a / de manera diferente a las demás solicitudes
Solo vamos a analizar la primera línea de la solicitud HTTP, por lo que en lugar de leer toda
la solicitud en un vector, estamos llamando a next para obtener el primer elemento del
iterator. El primer unwrap se encarga de la Option y detiene el programa si el iterator no
tiene elementos. El segundo unwrap maneja el Result y tiene el mismo efecto que el
unwrap que estaba en el map agregado en el Listado 20-2.
Ejecuta este código ahora y solicita 127.0.0.1:7878; deberías ver el HTML en hello.html. Si
haces cualquier otra solicitud, como 127.0.0.1:7878/something-else, obtendrás un error de
conexión como los que viste al ejecutar el código en el Listado 20-1 y el Listado 20-2.
Ahora agreguemos el código del Listado 20-7 al bloque else para devolver una respuesta
con el código de estado 404, que indica que el contenido de la solicitud no se encontró.
También devolveremos un poco de HTML para una página que se renderizará en el
navegador indicando la respuesta al usuario final.
Filename: src/main.rs
https://book.rustlang-es.org/print 595/656
12/7/24, 22:37 El Lenguaje de Programación Rust
// --snip--
} else {
let status_line = "HTTP/1.1 404 NOT FOUND";
let contents = fs::read_to_string("404.html").unwrap();
let length = contents.len();
stream.write_all(response.as_bytes()).unwrap();
}
Listing 20-7: Respondiendo con el código de estado 404 y una página de error si se solicita 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 en el archivo 404.html.
Necesitarás crear un archivo 404.html junto a hello.html para la página de error;
nuevamente, siéntete libre de usar cualquier HTML que desees o usa el HTML de ejemplo en
el Listado 20-8.
Filename: 404.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Oops!</h1>
<p>Sorry, I don't know what you're asking for.</p>
</body>
</html>
Listing 20-8: Contenido de ejemplo para la página que se enviará como respuesta en cualquier caso de error
404
Un toque de refactorización
En este momento, los bloques if y else tienen mucha repetición: ambos están leyendo
archivos y escribiendo el contenido de los archivos en el stream. Las únicas diferencias son
la línea de estado y el nombre del archivo. Hagamos que el código sea más conciso
extrayendo esas diferencias en líneas if y else separadas que asignarán los valores de la
línea de estado y el nombre del archivo a variables; luego podemos usar esas variables
https://book.rustlang-es.org/print 596/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/main.rs
// --snip--
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
Listing 20-9: Refactorizando los bloques if y else para que contengan solo el código que difiere entre los dos
casos
Ahora los bloques if y else solo devuelven los valores apropiados para la línea de estado
y el nombre de archivo en una tupla; luego usamos la destructuración para asignar estos
dos valores a status_line y filename usando un patrón en la declaración let , como se
discutió en el Capítulo 18.
El código previamente duplicado ahora está fuera de los bloques if y else y usa las
variables status_line y filename . Esto hace que sea más fácil ver la diferencia entre los
dos casos, y significa que solo tenemos un lugar para actualizar el código si queremos
cambiar la forma en que funciona la lectura de archivos y la escritura de respuestas. El
comportamiento del código en el Listado 20-9 será el mismo que el del Listado 20-7.
Actualmente, nuestro servidor se ejecuta en un solo hilo, lo que significa que solo puede
atender una solicitud a la vez. Analicemos cómo esto puede ser un problema al simular
algunas solicitudes lentas. Luego lo arreglaremos para que nuestro servidor pueda manejar
múltiples solicitudes a la vez.
https://book.rustlang-es.org/print 597/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para simular una solicitud lenta, podemos hacer que el servidor duerma durante un tiempo
antes de responder. Veremos cómo una solicitud de procesamiento lento puede afectar a
otras solicitudes realizadas a nuestra implementación actual del servidor. El listado 20-10
implementa el manejo de una solicitud a /sleep con una respuesta lenta simulada que hará
que el servidor duerma durante 5 segundos antes de responder.
Filename: src/main.rs
use std::{
fs,
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
// --snip--
// --snip--
}
Hemos cambiado de if a match ahora que tenemos tres casos. Necesitamos hacer
coincidir explícitamente con un slice de request_line para hacer coincidir con los valores
literales de string; match no hace referencia automática y desreferenciación como el
método de igualdad.
La primera opción es la misma que el bloque if del Listado 20-9. La segunda opción
coincide con una solicitud a /sleep. Cuando se recibe esa solicitud, el servidor dormirá
durante 5 segundos antes de representar la página HTML correcta. La tercera opción es la
misma que el bloque else del Listado 20-9.
Puedes ver cómo nuestro servidor es primitivo: ¡las bibliotecas reales manejarían el
reconocimiento de múltiples solicitudes de una manera mucho menos verbosa!
Iniciamos el servidor con cargo run . Luego abrimos dos ventanas del navegador: una para
http://127.0.0.1:7878/ y la otra para http://127.0.0.1:7878/sleep. Si ingresas la URI / varias
veces, como antes, verás que responde rápidamente. Pero si ingresas /sleep y luego cargas /,
verás que / espera hasta que sleep haya dormido durante sus 5 segundos completos antes
de cargarse.
Existen varias técnicas que podríamos usar para evitar que las solicitudes se acumulen
detrás de una solicitud lenta; la que implementaremos es un pool de hilos.
Un pool de hilos es un grupo de hilos generados que están esperando y listos para manejar
una tarea. Cuando el programa recibe una nueva tarea, asigna uno de los hilos del grupo a
la tarea, y ese hilo procesará la tarea. Los hilos restantes en el grupo están disponibles para
manejar cualquier otra tarea que llegue mientras el primer hilo está procesando. Cuando el
primer hilo termina de procesar su tarea, se devuelve al grupo de hilos inactivos, listo para
manejar una nueva tarea. Un pool de hilos le permite procesar conexiones de forma
concurrente, aumentando el rendimiento de su servidor.
En lugar de crear un nuevo hilo para cada solicitud, crearemos un grupo de hilos que
actuarán como un pool de hilos. Cuando llega una solicitud, el servidor enviará la solicitud al
pool de hilos. El pool de hilos mantendrá una cola de solicitudes entrantes. Cada uno de los
hilos en el pool sacará una solicitud de esta cola, manejará la solicitud y luego pedirá a la
cola otra solicitud. Con este diseño, podemos procesar hasta N solicitudes
simultáneamente, donde N es el número de hilos. Si cada hilo responde a una solicitud de
larga duración, las solicitudes posteriores aún pueden acumularse en la cola, pero hemos
https://book.rustlang-es.org/print 599/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Esta técnica es solo una de las muchas formas de mejorar el rendimiento de un servidor
web. Otras opciones que puede explorar son el modelo fork / join, el modelo de I / O
asincrónico de un solo hilo o el modelo de I / O asincrónico de múltiples hilos. Si está interesado
en este tema, puedes leer más sobre otras soluciones e intentar implementarlas; con un
lenguaje de bajo nivel como Rust, todas estas opciones son posibles.
Antes de comenzar a implementar un pool de hilos, hablemos sobre cómo debería verse el
uso del pool. Cuando intentas diseñar código, escribir la interfaz del cliente primero puede
ayudar a guiar tu diseño. Escribe la API del código para que esté estructurado de la manera
en que deseas llamarlo; luego implementa la funcionalidad dentro de esa estructura en
lugar de implementar la funcionalidad y luego diseñar la API pública.
Primero, exploremos cómo podría lucir nuestro código si creáramos un nuevo hilo para
cada conexión. Como se mencionó anteriormente, este no es nuestro plan final debido a los
problemas con la posibilidad de generar un número ilimitado de hilos, pero es un punto de
partida para obtener un servidor web multihilo. Luego agregaremos el pool de hilos como
una mejora, y contrastar las dos soluciones será más fácil. El Listado 20-11 muestra los
cambios que debe realizar en main para crear un nuevo hilo para manejar cada flujo dentro
del bucle for .
Filename: src/main.rs
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
thread::spawn(|| {
handle_connection(stream);
});
}
}
https://book.rustlang-es.org/print 600/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Como aprendiste en el Capítulo 16, thread::spawn creará un nuevo hilo y luego ejecutará
el código en el cierre en el nuevo hilo. Si ejecutas este código y cargas /sleep en tu
navegador, luego / en otras dos pestañas del navegador, verás que las solicitudes a / no
tienen que esperar a que /sleep termine. Sin embargo, como mencionamos, esto
eventualmente abrumará el sistema porque estarías creando nuevos hilos sin ningún límite.
Queremos que nuestro pool de hilos funcione de manera similar y familiar, de modo que
cambiar de hilos a un pool de hilos no requiera grandes cambios en el código que usa
nuestra API. El Listado 20-12 muestra la interfaz hipotética para un struct ThreadPool que
queremos usar en lugar de thread::spawn .
Filename: src/main.rs
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
pool.execute(|| {
handle_connection(stream);
});
}
}
Utilizamos ThreadPool::new para crear un nuevo pool de hilos con un número configurable
de hilos, en este caso cuatro. Luego, en el bucle for , pool.execute tiene una interfaz
similar a thread::spawn en que toma un cierre que el pool debe ejecutar para cada flujo.
Necesitamos implementar pool.execute para que tome el cierre y se lo dé a un hilo en el
pool para que lo ejecute. Este código aún no se compilará, pero lo intentaremos para que el
compilador pueda guiarnos en cómo solucionarlo.
Realiza los cambios en el Listado 20-12 a src/main.rs, y luego usemos los errores del
compilador de cargo check para impulsar nuestro desarrollo. Aquí está el primer error que
obtenemos:
https://book.rustlang-es.org/print 601/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
¡Eso es genial! Este error nos dice que necesitamos un tipo o módulo ThreadPool , así que lo
construiremos ahora. Nuestra implementación de ThreadPool será independiente del tipo
de trabajo que nuestro servidor web está haciendo. Entonces, cambiemos el crate de hello
de un crate binario a un crate de biblioteca para contener nuestra implementación de
ThreadPool . Después de cambiar a un crate de biblioteca, también podríamos usar la
biblioteca de pool de hilos separada para cualquier trabajo que queramos hacer usando un
pool de hilos y no solo para servir solicitudes web.
Crea un src/lib.rs que contenga lo siguiente, que es la definición más simple de un struct
ThreadPool que podemos tener por ahora:
Filename: src/lib.rs
Luego edita el archivo main.rs para traer ThreadPool al scope del crate desde el crate de la
biblioteca agregando el siguiente código en la parte superior de src/main.rs:
Filename: src/main.rs
use hello::ThreadPool;
Este código aún no funcionará, pero verifiquémoslo nuevamente para obtener el siguiente
error que debemos abordar:
$ 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
https://book.rustlang-es.org/print 602/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Este error indica que a continuación debemos crear una función asociada llamada new para
ThreadPool . También sabemos que new debe tener un parámetro que pueda aceptar 4
como argumento y debe devolver una instancia de ThreadPool . Implementemos la función
new más simple que tendrá esas características:
Filename: src/lib.rs
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
}
Elegimos usize como el tipo del parámetro size , porque sabemos que un número
negativo de hilos no tiene sentido. También sabemos que usaremos este 4 como el
número de elementos en una colección de hilos, que es para lo que se usa el tipo usize ,
como se discutió en la sección “Tipos de enteros” del Capítulo 3.
$ 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
https://book.rustlang-es.org/print 603/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El trait FnOnce también tiene un trait bound Send y un lifetime bound 'static , que son
útiles en nuestra situación: necesitamos Send para transferir el closure de un hilo a otro y
'static porque no sabemos cuánto tiempo tomará el hilo para ejecutarse. Creemos un
método execute en ThreadPool que tomará un parámetro genérico de tipo F con estos
bounds:
Filename: src/lib.rs
impl ThreadPool {
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
Aún usamos () después de FnOnce porque este FnOnce representa un closure que no
toma parámetros y devuelve el tipo de unidad () . Al igual que las definiciones de
funciones, el tipo de retorno se puede omitir de la firma, pero incluso si no tenemos
parámetros, todavía necesitamos los paréntesis.
Una vez más, esta es la implementación más simple del método execute : no hace nada,
pero estamos tratando de que nuestro código compile. Verifiquemos nuevamente:
$ 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 run y haces una solicitud en el
navegador, verás los errores en el navegador que vimos al comienzo del capítulo. ¡Nuestra
biblioteca aún no está llamando al closure pasado a execute !
https://book.rustlang-es.org/print 604/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Nota: Una frase que podrías escuchar sobre lenguajes con compiladores estrictos,
como Haskell y Rust, es “si el código se compila, funciona”. Pero esta frase no es
universalmente cierta. Nuestro proyecto se compila, ¡pero no hace absolutamente
nada! Si estuviéramos construyendo un proyecto real y completo, este sería un buen
momento para comenzar a escribir pruebas unitarias para verificar que el código se
compile y tenga el comportamiento que queremos.
No estamos haciendo nada con los parámetros a new y execute . Implementemos los
cuerpos de estas funciones con el comportamiento que queremos. Para comenzar,
pensemos en new . Anteriormente, elegimos un tipo sin signo para el parámetro size ,
porque un pool con un número negativo de hilos no tiene sentido. Sin embargo, un pool con
cero hilos tampoco tiene sentido, pero cero es un usize perfectamente válido.
Agregaremos código para verificar que size es mayor que cero antes de devolver una
instancia de ThreadPool y hacer que el programa se bloquee si recibe un cero usando el
macro assert! , como se muestra en el Listado 20-13.
Filename: src/lib.rs
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);
ThreadPool
}
// --snip--
}
En lugar de agregar la macro assert! como lo hicimos aquí, podríamos cambiar new a
build y devolver un Result como lo hicimos con Config::build en el proyecto I/O en el
https://book.rustlang-es.org/print 605/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Listado 12-9. Pero hemos decidido en este caso que intentar crear un pool de hilos sin
ningún hilo debería ser un error irrecuperable. Si te sientes ambicioso, intenta escribir una
función llamada build con la siguiente firma para comparar con la función new :
Ahora que tenemos una forma de saber que tenemos un número válido de hilos para
almacenar en el pool, podemos crear esos hilos y almacenarlos en el struct ThreadPool
antes de devolver el struct. Pero, ¿cómo “almacenamos” un hilo? Echemos otro vistazo a la
firma de thread::spawn :
El código en el Listado 20-14 se compilará, pero aún no creará ningún hilo. Hemos cambiado
la definición de ThreadPool para contener un vector de instancias de
thread::JoinHandle<()> , inicializado el vector con una capacidad de size , configurado un
bucle for que ejecutará algún código para crear los hilos y devuelto una instancia de
ThreadPool que los contiene.
Filename: src/lib.rs
https://book.rustlang-es.org/print 606/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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--
}
Listing 20-14: Creando un vector para que ThreadPool contenga los hilos
Una vez que se recibe un tamaño válido, nuestro ThreadPool crea un nuevo vector que
puede contener size elementos. La función with_capacity realiza la misma tarea que
Vec::new , pero con una diferencia importante: se asigna espacio en el vector. Debido a que
sabemos que necesitamos almacenar size elementos en el vector, hacer esta asignación
por adelantado es ligeramente más eficiente que usar Vec::new , que se redimensiona a sí
mismo a medida que se insertan elementos.
https://book.rustlang-es.org/print 607/656
12/7/24, 22:37 El Lenguaje de Programación Rust
pooling. El Worker recoge el código que debe ejecutarse y ejecuta el código en el hilo del
Worker. Piensa en las personas que trabajan en la cocina de un restaurante: los
trabajadores esperan hasta que lleguen los pedidos de los clientes, y luego son
responsables de tomar esos pedidos y cumplirlos.
Si estás listo para un desafío, intenta implementar estos cambios por ti mismo antes de ver
el código en el Listado 20-15.
¿Listo? Aquí está el Listado 20-15 con una forma de hacer las modificaciones
Filename: src/lib.rs
https://book.rustlang-es.org/print 608/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::thread;
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 {
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});
Listing 20-15: Modificando ThreadPool para contener instancias de Worker en lugar de contener hilos
directamente
El código externo (como nuestro servidor en src/main.rs) no necesita conocer los detalles de
implementación con respecto al uso de un struct Worker dentro de ThreadPool , por lo que
hacemos que el struct Worker y su función new sean privadas. La función Worker::new
utiliza el id que le damos y almacena una instancia JoinHandle<()> que se crea al generar
un nuevo hilo usando un closure vacío.
https://book.rustlang-es.org/print 609/656
12/7/24, 22:37 El Lenguaje de Programación Rust
servidor entre en panic, incluso si la creación de algunos hilos tiene éxito. Por
simplicidad, este comportamiento está bien, pero en una implementación de grupo de
hilos de producción, es probable que desee usar std::thread::Builder y su método
spawn que devuelve Result en su lugar.
El siguiente problema que abordaremos es que los closures que se pasan a tread::spawn
no hacen absolutamente nada. Actualmente, obtenemos el closure que queremos ejecutar
en el método execute . Pero necesitamos darle a thread::spawn un closure para ejecutar
cuando creamos cada Worker durante la creación del ThreadPool .
Queremos que los structs Worker que acabamos de crear obtengan el código a ejecutar
desde una cola mantenida en ThreadPool y envíen ese código a su hilo para su ejecución.
Los canales que aprendimos en el Capítulo 16, una forma simple de comunicarse entre dos
hilos, serían perfectos para este caso de uso. Usaremos un canal para funcionar como la
cola de trabajos, y execute enviará un trabajo desde el ThreadPool a las instancias
Worker , que enviarán el trabajo a su hilo. Aquí está el plan:
Filename: src/lib.rs
https://book.rustlang-es.org/print 610/656
12/7/24, 22:37 El Lenguaje de Programación Rust
struct Job;
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
for id in 0..size {
workers.push(Worker::new(id));
}
Listing 20-16: Modificando ThreadPool para almacenar el emisor de un canal que transmite instancias Job
Intentemos pasar un receptor del canal a cada trabajador mientras el pool de hilos crea el
canal. Sabemos que queremos usar el receptor en el hilo que los trabajadores generan, por
lo que haremos referencia al parámetro receiver en el closure. El código en el Listado 20-
17 aún no se compilará.
Filename: src/lib.rs
https://book.rustlang-es.org/print 611/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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;
});
https://book.rustlang-es.org/print 612/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ 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
For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello` (lib) due to 1 previous error
Además, quitar un trabajo de la cola del canal implica modificar el receiver , por lo que los
hilos necesitan una forma segura de compartir y modificar el receiver ; de lo contrario,
podríamos obtener condiciones de carrera (como se explicó en el Capítulo 16).
Recuerda los smart pointers thread-safe discutidos en el Capítulo 16: para compartir la
propiedad entre varios hilos y permitir que los hilos muten el valor, necesitamos usar
Arc<Mutex<T>> . El tipo Arc permitirá que varios trabajadores sean propietarios del
receptor, y Mutex garantizará que solo un trabajador obtenga un trabajo del receptor a la
vez. El Listado 20-18 muestra los cambios que debemos hacer.
Filename: src/lib.rs
https://book.rustlang-es.org/print 613/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use std::{
sync::{mpsc, Arc, Mutex},
thread,
};
// --snip--
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--
}
}
Listing 20-18: Compartiendo el receptor entre los trabajadores usando Arc y Mutex
Filename: src/lib.rs
https://book.rustlang-es.org/print 614/656
12/7/24, 22:37 El Lenguaje de Programación Rust
// --snip--
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--
Listing 20-19: Creando un alias de tipo Job para un Box que contenga cada closure y luego enviamos el trabajo
por el canal
Después de crear una nueva instancia de Job usando el closure que obtenemos en
execute , enviamos ese trabajo por el extremo de envío del canal. Estamos llamando a
unwrap en send para el caso de que el envío falle. Esto podría suceder si, por ejemplo,
detenemos todos nuestros hilos de ejecución, lo que significa que el extremo receptor ha
dejado de recibir nuevos mensajes. En este momento, no podemos detener que nuestros
hilos se ejecuten: nuestros hilos continúan ejecutándose mientras exista el pool. La razón
por la que usamos unwrap es que sabemos que el caso de falla no sucederá, pero el
compilador no sabe eso.
Filename: src/lib.rs
https://book.rustlang-es.org/print 615/656
12/7/24, 22:37 El Lenguaje de Programación Rust
// --snip--
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 a lock en el receiver para adquirir el mutex, y luego llamamos a
unwrap para que el hilo actual se bloquee en caso de que ocurra algún error. Adquirir un
bloqueo puede fallar si el mutex está en un estado envenenado, lo que puede suceder si
algún otro hilo se bloqueó mientras sostenía el bloqueo en lugar de liberar el bloqueo. En
esta situación, llamar a unwrap para que este hilo se bloquee es la acción correcta a tomar.
Siéntase libre de cambiar este unwrap a un expect con un mensaje de error que sea
significativo para ti.
La llamada a recv bloquea, por lo que si aún no hay un trabajo, el hilo actual esperará
hasta que haya un trabajo disponible. El Mutex<T> garantiza que solo un hilo Worker a la
vez está tratando de solicitar un trabajo.
¡Nuestro pool de hilos ahora está en un estado funcional! Ejecuta cargo run y haz algunas
solicitudes:
https://book.rustlang-es.org/print 616/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
warning: field is never read: `workers`
--> src/lib.rs:7:5
|
7 | workers: Vec<Worker>,
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
¡Éxito! Ahora tenemos un pool de hilos que ejecuta conexiones de forma asincrónica. Nunca
hay más de cuatro hilos creados, por lo que nuestro sistema no se sobrecargará si el
servidor recibe muchas solicitudes. Si hacemos una solicitud a /sleep, el servidor podrá
atender otras solicitudes haciendo que otro hilo las ejecute.
Después de aprender sobre el bucle while let en el Capítulo 18, es posible que te
preguntes por qué no escribimos el código del hilo del trabajador como se muestra en el
Listado 20-21.
Filename: src/lib.rs
https://book.rustlang-es.org/print 617/656
12/7/24, 22:37 El Lenguaje de Programación Rust
// --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();
}
});
https://book.rustlang-es.org/print 618/656
12/7/24, 22:37 El Lenguaje de Programación Rust
A continuación, implementaremos el trait Drop para llamar a join en cada uno de los
threads del pool para que puedan terminar las requests en las que están trabajando antes
de cerrar. Luego implementaremos una forma de decirle a los threads que deben dejar de
aceptar nuevas requests y cerrarse. Para ver este código en acción, modificaremos nuestro
servidor para que acepte solo dos requests antes de cerrar el pool de threads
correctamente.
Filename: src/lib.rs
worker.thread.join().unwrap();
}
}
}
Listing 20-22: Uniendo cada thread cuando el thread pool se sale del scope
Primero, iteramos a través de cada uno de los workers del pool de threads. Usamos &mut
para esto porque self es una referencia mutable, y también necesitamos poder mutar
worker . Para cada worker, imprimimos un mensaje diciendo que este worker en particular
se está cerrando, y luego llamamos a join en el thread de ese worker. Si la llamada a join
falla, usamos unwrap para que Rust entre en pánico y haga una salida poco elegante.
$ 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`
-->
/rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/thread/mod.rs:1
657:17
For more information about this error, try `rustc --explain E0507`.
error: could not compile `hello` (lib) due to 1 previous error
El error nos dice que no podemos llamar a join porque solo tenemos un mutable borrow
de cada worker y join toma el ownership de su argumento. Para solucionar este
problema, necesitamos mover el thread fuera de la instancia de Worker que posee thread
para que join pueda consumir el thread. Hicimos esto en el Listing 17-15: si Worker tiene
un Option<thread::JoinHandle<()>> en su lugar, podemos llamar al método take en el
Option para mover el valor fuera de la variante Some y dejar una variante None en su
lugar. En otras palabras, un Worker que se está ejecutando tendrá una variante Some en
thread , y cuando queramos limpiar un Worker , reemplazaremos Some con None para que
el Worker no tenga un thread para ejecutar.
Filename: src/lib.rs
struct Worker {
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
Ahora usemos el compilador para encontrar los otros lugares que necesitan cambiar. Al
verificar este código, obtenemos dos errores:
https://book.rustlang-es.org/print 620/656
12/7/24, 22:37 El Lenguaje de Programación Rust
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no method named `join` found for enum `Option` in the current
scope
--> src/lib.rs:52:27
|
52 | worker.thread.join().unwrap();
| ^^^^ method not found in
`Option<JoinHandle<()>>`
|
note: the method `join` exists on the type `JoinHandle<()>`
-->
/rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/thread/mod.rs:1
657:5
help: consider using `Option::expect` to unwrap the `JoinHandle<()>` value,
panicking if the value is an `Option::None`
|
52 | worker.thread.expect("REASON").join().unwrap();
| +++++++++++++++++
Filename: src/lib.rs
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
// --snip--
Worker {
id,
thread: Some(thread),
}
}
}
https://book.rustlang-es.org/print 621/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Filename: src/lib.rs
Como discutimos en el Capítulo 17, el método take en Option toma la variante Some y
deja None en su lugar. Estamos usando if let para deconstruir el Some y obtener el
thread; luego llamamos a join en el thread. Si el thread de un worker ya es None , sabemos
que ese worker ya ha tenido su thread limpiado, por lo que en ese caso no sucede nada.
Con todos los cambios que hemos hecho, nuestro código se compila sin advertencias. Sin
embargo, las malas noticias son que este código aún no funciona de la manera que
queremos. La clave es la lógica en los closures ejecutados por los threads de las instancias
de Worker : en este momento, llamamos a join , pero eso no detendrá los threads porque
se ejecutan en un loop para siempre buscando jobs. Si intentamos dejar caer nuestro
ThreadPool con nuestra implementación actual de drop , el thread principal se bloqueará
para siempre esperando a que el primer thread termine.
Filename: src/lib.rs
https://book.rustlang-es.org/print 622/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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();
}
}
Listing 20-23: Dejar caer explícitamente sender antes de unirse a los threads del worker
Soltar sender cierra el canal, lo que indica que no se enviarán más mensajes. Cuando eso
sucede, todas las llamadas a recv que los workers hacen en el loop infinito devolverán un
error. En el Listing 20-24, cambiamos el loop de Worker para salir del loop con gracia en ese
caso, lo que significa que los hreads terminarán cuando la implementación de drop de
ThreadPool llame a join en ellos.
Filename: src/lib.rs
https://book.rustlang-es.org/print 623/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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),
}
}
}
Listing 20-24: Saliendo explícitamente del loop cuando recv devuelve un error
Para ver este código en acción, modifiquemos main para aceptar solo dos requests antes
de cerrar el servidor con gracia, como se muestra en el Listing 20-25.
Filename: src/main.rs
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.");
}
Listing 20-25: Apagando el servidor después de servir dos requests saliendo del loop
No querríamos que un servidor web del mundo real se apague después de servir solo dos
requests. Este código solo demuestra que el apagado y la limpieza con gracia funcionan.
https://book.rustlang-es.org/print 624/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El método take es definido en el trait Iterator y limita la iteración de los primeros dos
items como máximo. El ThreadPool saldrá del scope al final de main y la implementación
drop correrá.
Iniciamos el servidor con cargo run y hacemos tres requests. La tercera request debería
fallar, y en su terminal debería ver una salida similar a esta:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 1.0s
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
Es posible que vea un orden diferente de workers y mensajes impresos. Podemos ver cómo
funciona este código a partir de los mensajes: los workers 0 y 3 obtuvieron las dos primeras
requests. El servidor dejó de aceptar conexiones después de la segunda conexión, y la
implementación Drop en ThreadPool comienza a ejecutarse antes de que el worker 3
comience su trabajo. Al soltar sender desconecta a todos los workers y les dice que se
apaguen. Los workers imprimen un mensaje cuando se desconectan, y luego el pool de
threads llama a join para esperar a que cada thread worker termine.
¡Enhorabuena! Hemos completado nuestro proyecto; tenemos un servidor web básico que
usa un pool de threads para responder de forma asíncrona. Podemos realizar un apagado
con gracia del servidor, que limpia todos los threads del pool.
Filename: src/main.rs
https://book.rustlang-es.org/print 625/656
12/7/24, 22:37 El Lenguaje de Programación Rust
use hello::ThreadPool;
use std::{
fs,
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
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();
}
Filename: src/lib.rs
https://book.rustlang-es.org/print 626/656
12/7/24, 22:37 El Lenguaje de Programación Rust
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();
}
}
https://book.rustlang-es.org/print 627/656
12/7/24, 22:37 El Lenguaje de Programación Rust
thread.join().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í hay algunas ideas:
Resumen
¡Bien hecho! ¡Has llegado al final del libro! Queremos agradecerte por unirte a nosotros en
este tour de Rust. Ahora estás listo para implementar tus propios proyectos en Rust y
ayudar con los proyectos de otras personas. Ten en cuenta que hay una comunidad
https://book.rustlang-es.org/print 628/656
12/7/24, 22:37 El Lenguaje de Programación Rust
acogedora de otros Rustaceans que estarían encantados de ayudarte con cualquier desafío
que encuentres en tu viaje con Rust.
https://book.rustlang-es.org/print 629/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Apéndice
Las siguientes secciones contienen material de referencia que le puede ser útil en su viaje a
Rust.
https://book.rustlang-es.org/print 630/656
12/7/24, 22:37 El Lenguaje de Programación Rust
La siguiente lista contiene las palabras clave actuales en uso, con su funcionalidad descrita.
as - realiza una conversión primitiva, elimina la ambigüedad del trait específico que
contiene un elemento, o cambiar el nombre de los elementos en las declaraciones
use y extern crate
async - retornar un Future en lugar de bloquear el hilo actual
await - suspender la ejecución hasta que el resultado de un Future esté listo
break - salir de un bucle inmediatamente
const - define elementos constantes o punteros crudos constantes
continue - continuar con la siguiente iteración del bucle
crate - en un camino de módulo, se refiere a la raíz del módulo
dyn - despacho dinámico a un objeto de rasgo
else - alternativa para las construcciones de flujo de control if y if let
enum - define una enumeración
extern - enlaza una función o variable externa
false - literal booleano falso
fn - define una función o el tipo de puntero de función
for - bucle sobre elementos de un iterador, implementa un rasgo, o especifica una
vida más alta
if - ramificación basada en el resultado de una expresión condicional
impl - implementa funcionalidad propia o de rasgo
in - parte de la sintaxis del bucle for
let - vincula una variable
loop - bucle sin condición
match - combina un valor con patrones
mod - define un módulo
move - hace que una función clausura tome posesión de todos sus capturas
mut - denota mutabilidad en referencias, punteros crudos o vinculaciones de patrones
pub - denota visibilidad pública en campos de estructuras, bloques impl o módulos
ref - vincula por referencia
https://book.rustlang-es.org/print 631/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Las siguientes palabras clave no tienen aún ninguna funcionalidad, pero están reservadas
por Rust para un uso potencial en el futuro.
abstract
become
box
do
final
macro
override
priv
try
typeof
unsized
virtual
yield
Identificadores brutos
Identificadores brutos son la sintaxis que le permite usar palabras clave donde normalmente
no se permitirían. Usted usa un identificador bruto prefijando una palabra clave con r# .
Por ejemplo, match es una palabra clave. Si intenta compilar la siguiente función que usa
match como su nombre:
https://book.rustlang-es.org/print 632/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El error muestra que no se puede usar la palabra clave match como identificador de
función. Para usar match como nombre de función, necesita usar la sintaxis de identificador
bruto, como esta:
fn main() {
assert!(r#match("foo", "foobar"));
}
Los identificadores brutos permiten usar cualquier palabra como identificador, incluso si
esa palabra es una palabra clave. Esto nos da más libertad para elegir nombres de
identificadores, así como nos permite integrarnos con programas escritos en un lenguaje
donde estas palabras no son palabras clave. Además, los identificadores brutos nos
permiten usar bibliotecas escritas en una edición de Rust diferente a la de su crate. Por
ejemplo, try no es una palabra clave en la edición 2015, pero lo es en la edición 2018. Si
depende de una biblioteca que está escrita usando la edición 2015 y tiene una función try ,
necesitará usar la sintaxis de identificador bruto para llamar a esa función desde su código
de la edición 2018. Vea Apéndice E para obtener más información sobre las ediciones.
https://book.rustlang-es.org/print 633/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Operadores
La tabla B-1 contiene los operadores en Rust, un ejemplo de cómo aparecería el operador
en contexto, una breve explicación y si ese operador es sobrecargable. Si un operador es
sobrecargable, se lista el rasgo relevante para sobrecargar ese operador.
&expr , &mut
& Préstamo
expr
&type , &mut
type , &'a Préstamo del puntero del
&
type , &'a mut tipo
type
https://book.rustlang-es.org/print 634/656
12/7/24, 22:37 El Lenguaje de Programación Rust
*const type ,
* Puntero
*mut type
trait +
Restricción de tipo
+ trait , 'a +
compuesta
trait
Separador de argumentos
, expr, expr
y elementos
- - expr Aritmético de Negación Neg
Aritmético de sustracción y
-= var -= expr SubAssign
asignación
fn(...) ->
Tipo de retorno en
-> type , |...| ->
funciones y clausuras
type
División aritmética y
/= var /= expr DivAssign
asignación
pat: type ,
: Restricciones
ident: type
https://book.rustlang-es.org/print 635/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Terminador de declaración
; expr;
y elemento
Parte de la sintaxis de
; [...; len]
arreglos de tamaño fijo
Desplazamiento a la
<< expr << expr Shl
izquierda
Desplazamiento a la
<<= var <<= expr ShlAssign
izquierda y asignación
< expr < expr Comparador de menor que PartialOrd
Comparador de menor o
<= expr <= expr PartialOrd
igual que
var = expr ,
= Asignación/equivalencia
ident = type
Parte de la sintaxis de
=> pat => expr
match
> expr > expr Comparador de mayor que PartialOrd
Comparador de mayor o
>= expr >= expr PartialOrd
igual que
Desplazamiento a la
>> expr >> expr Shr
derecha
Desplazamiento a la
>>= var >>= expr ShrAssign
derecha y asignación
@ ident @ pat Patrón de enlace
^ expr ^ expr Operador bit a bit XOR BitXor
https://book.rustlang-es.org/print 636/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Simbolos no operadores
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.
Tabla B-2 muestra los símbolos que aparecen por sí mismos y son válidos en una variedad
de ubicaciones.
Símbolos Explicación
'ident Lifetime nombrado o etiqueta de bucle
...u8 , ...i32 , ...f64 ,
Literal numérico de un tipo especifico
...usize , etc.
Tabla B-3 muestra los símbolos que aparecen en el contexto de un camino a través del
módulo de la jerarquía para un elemento.
Símbolos Explicación
ident::ident Namespace path
Path relative to the crate root (i.e., an explicitly
::path
absolute path)
Path relative to the current module (i.e., an
self::path
explicitly relative path)
super::path Path relative to the parent of the current module
type::ident , <type as Associated constants, functions, and types
https://book.rustlang-es.org/print 637/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Símbolos Explicación
trait>::ident
Tabla B-4 muestra los símbolos que aparecen en el contexto de usar parámetros de tipo
genérico.
Símbolos Explicación
Especifica parámetros de tipo genérico en un tipo (por
path<...>
ejemplo, Vec<u8> )
Especifica parámetros de tipo genérico, función o método
path::<...> ,
en una expresión; a menudo se refiere como pez espada
method::<...>
(por ejemplo, "42".parse::<i32>() )
fn ident<...> ... Define una función genérica
struct ident<...>
Define una estructura genérica
...
enum ident<...>
Define una enumeración genérica
...
Tabla B-5 muestra los símbolos que aparecen en el contexto de restringir parámetros de
tipo genérico con límites de tipo.
Simbolos Explicación
Parámetro de tipo genérico T restringido a tipos que
T: U
implementan U
Tipo genérico T debe sobrevivir al tiempo de vida 'a (es decir,
T: 'a el tipo no puede contener de forma transitiva referencias con
tiempos de vida más cortos que 'a )
https://book.rustlang-es.org/print 638/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Simbolos Explicación
Tipo genérico T no contiene referencias prestadas, excepto las
T: 'static
de 'static
Tiempo de vida genérico 'b debe sobrevivir al tiempo de vida
'b: 'a
'a
Tabla B-6 muestra los símbolos que aparecen en el contexto de llamar o definir macros y
especificar atributos en un elemento.
Símbolos Explicación
#[meta] Atributo externo
#![meta] Atributo interno
$ident Sustitución de macro
$ident:kind Captura de macro
$(...)... Repetición de macro
ident!(...) , ident!{...} , ident![...] Invocación de macro
Símbolos Explicación
// Comentario de línea
//! Comentario de línea de documentación interna
/// Comentario de línea de documentación externa
/*...*/ Comentario de bloque
/*!...*/ Comentario de bloque de documentación interna
/**...*/ Comentario de bloque de documentación externa
Tabla B-8 muestra los símbolos que aparecen en el contexto de usar tuplas.
Símbolos Explicación
Tupla vacía (también conocida como unidad), tanto literal como
()
tipo
https://book.rustlang-es.org/print 639/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Símbolos Explicación
(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
expr(expr, Expresión de llamada de función; también se usa para inicializar
...) struct s de tupla y variantes de enum de tupla
expr.0 ,
Índice de tupla
expr.1 , etc.
Tabla B-9 muestra los contextos en los que se usan las llaves.
Contexto Explicación
{...} Expresión de bloque
Type {...} Literal de struct
Tabla B-10 muestra los contextos en los que se usan los corchetes.
Contexto Explicación
[...] Expresión de arreglo
[type; expr] Arreglo de tipo y tamaño
expr[expr] Índice de colección. Sobrecargable ( Index , IndexMut )
Índice de colección fingiendo ser recortes de colección,
expr[..] , expr[a..] ,
usando Range , RangeFrom , RangeTo , o RangeFull
expr[..b] , expr[a..b]
como el “índice”
https://book.rustlang-es.org/print 640/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Estos traits enumerados aquí son los únicos definidos por la biblioteca estándar que se
pueden implementar en sus tipos usando derive . Otros traits definidos en la biblioteca
estándar no tienen un comportamiento predeterminado sensato, por lo que depende de
usted implementarlos de la manera que tenga sentido para lo que está tratando de lograr.
Un ejemplo de un trait que no se puede derivar es Display , que maneja el formateo para
los usuarios finales. Siempre debe considerar la forma apropiada de mostrar un tipo a un
usuario final. ¿Qué partes del tipo puede ver un usuario final? ¿Qué partes encontrarían
relevantes? ¿Qué formato de los datos sería más relevante para ellos? El compilador Rust no
tiene esta idea, por lo que no puede proporcionar un comportamiento predeterminado
apropiado para usted.
https://book.rustlang-es.org/print 641/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El trait Debug te permite imprimir instancias de un tipo con fines de depuración, para que
tú y otros programadores que usen tu tipo puedan inspeccionar una instancia en un punto
particular de la ejecución de un programa.
El trait Debug es necesario, por ejemplo, en el uso de la macro assert_eq! . Esta macro
imprime los valores de las instancias dadas como argumentos si la aserción de igualdad
falla, por lo que los programadores pueden ver por qué las dos instancias no eran iguales.
El trait PartialEq es necesario, por ejemplo, con el uso de la macro assert_eq! , que
necesita poder comparar dos instancias de un tipo para la igualdad.
El trait Eq no tiene métodos. Su propósito es señalar que para cada valor del tipo anotado,
el valor es igual a sí mismo. El trait Eq solo se puede aplicar a tipos que también
implementan PartialEq , aunque no todos los tipos que implementan PartialEq pueden
implementar Eq . Un ejemplo de esto son los tipos de números de punto flotante: la
implementación de los números de punto flotante establece que dos instancias del valor no
es un número ( NaN ) no son iguales entre sí.
Un ejemplo de cuando se necesita Eq es para las claves en un HashMap<K, V> para que el
HashMap<K, V> pueda decir si dos claves son iguales.
https://book.rustlang-es.org/print 642/656
12/7/24, 22:37 El Lenguaje de Programación Rust
El trait PartialOrd es necesario, por ejemplo, para el método gen_range del crate rand
que genera un valor aleatorio en el rango especificado por una expresión de rango.
El trait Ord permite saber que para cualquier dos valores del tipo anotado, existirá un
orden válido. El trait Ord implementa el método cmp , que devuelve un Ordering en lugar
de un Option<Ordering> porque siempre será posible un orden válido. Solo puede aplicar
el trait Ord a tipos que también implementan PartialOrd y Eq (y Eq requiere
PartialEq ). Cuando se deriva en structs y enums, cmp se comporta de la misma manera
que la implementación derivada para partial_cmp con PartialOrd .
El trait Clone te permite crear explícitamente una copia profunda de un valor, y el proceso
de duplicación puede implicar la ejecución de código arbitrario y la copia de datos de la pila.
Consulte la sección “Ways Variables and Data Interact: Clone” en el Capítulo 4 para obtener
más información sobre Clone .
Derivar Clone implementa el método clone , que cuando se implementa para todo el tipo,
llama a clone en cada una de las partes del tipo. Esto significa que todos los campos o
valores en el tipo también deben implementar Clone para derivar Clone .
El trait Copy te permite duplicar un valor copiando solo los bits almacenados en la pila; no
es necesario ningún código arbitrario. Consulte la sección “Stack-Only Data: Copy” en el
Capítulo 4 para obtener más información sobre Copy .
El trait Copy no define ningún método para evitar que los programadores sobrecarguen
esos métodos y violen la suposición de que no se está ejecutando código arbitrario. De esa
manera, todos los programadores pueden asumir que copiar un valor será muy rápido.
Puede derivar Copy en un tipo solo si todas las partes del tipo implementan Copy . Un tipo
que implementa Copy también debe implementar Clone , porque un tipo que implementa
https://book.rustlang-es.org/print 643/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Copy tiene una implementación trivial de Clone que realiza la misma tarea que Copy .
El trait Copy es rara vez requerido; los tipos que implementan Copy tienen optimizaciones
disponibles, lo que significa que no tiene que llamar a clone , lo que hace que el código sea
más conciso.
Todo lo posible con Copy también se puede lograr con Clone , pero el código podría ser
más lento o tener que usar clone en lugares.
El trait Hash te permite tomar una instancia de un tipo de tamaño arbitrario y asignar esa
instancia a un valor de tamaño fijo usando una función hash. Derivar Hash implementa el
método hash . La implementación derivada del método hash combina el resultado de
llamar a hash en cada una de las partes del tipo, lo que significa que todos los campos o
valores también deben implementar Hash para derivar Hash .
El trait Default te permite crear un valor predeterminado para un tipo. Derivar Default
implementa la función default . La implementación derivada de la función default llama a
la función default en cada parte del tipo, lo que significa que todos los campos o valores
en el tipo también deben implementar Default para derivar Default .
https://book.rustlang-es.org/print 644/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Este comando le da rustfmt y cargo-fmt , similar a cómo Rust le da rustc y cargo . Para
formatear cualquier proyecto de carga útil, ingrese lo siguiente:
$ cargo fmt
Ejecutando este comando reformatea todo el código Rust en la carga útil actual. Esto solo
debería cambiar el estilo de código, no la semántica del código. Para más información sobre
rustfmt , vea su documentación.
Filename: src/main.rs
fn do_something() {}
fn main() {
for i in 0..100 {
do_something();
}
}
https://book.rustlang-es.org/print 645/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Aquí, estamos llamando a la función do_something 100 veces, pero nunca usamos la
variable i en el cuerpo del bucle for . Rust nos advierte sobre eso:
$ cargo build
Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: unused variable: `i`
--> src/main.rs:4:9
|
4 | for i in 0..100 {
| ^ help: consider using `_i` instead
|
= note: #[warn(unused_variables)] on by default
Esta advertencia sugiere que usemos _i como nombre en su lugar: el guión bajo indica que
pretendemos que esta variable no se use. Podemos aplicar automáticamente esa
sugerencia usando la herramienta rustfix ejecutando el comando cargo fix :
$ 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
Cuando volvemos a mirar src/main.rs, veremos que cargo fix ha cambiado el código:
Filename: src/main.rs
fn do_something() {}
fn main() {
for _i in 0..100 {
do_something();
}
}
También puede usar cargo fix para transformar su código entre diferentes ediciones de
Rust. Las ediciones se tratan en el Apéndice E.
La herramienta clippy es una colección de lints para analizar su código para que pueda
detectar errores comunes y mejorar su código Rust.
https://book.rustlang-es.org/print 646/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para ejecutar los lints de Clippy en cualquier proyecto de carga útil, ingrese lo siguiente:
$ cargo clippy
Por ejemplo, digamos que escribe un programa que usa una aproximación de una
constante matemática, como pi, como lo hace este programa:
Filename: src/main.rs
fn main() {
let x = 3.1415;
let r = 8.0;
println!("the area of the circle is {}", x * r * r);
}
Este error le informa que Rust ya tiene una constante PI más precisa definida y que su
programa sería más correcto si usara la constante en su lugar. Luego cambiaría su código
para usar la constante PI . El siguiente código no produce ningún error ni advertencia de
Clippy:
Filename: src/main.rs
fn main() {
let x = std::f64::consts::PI;
let r = 8.0;
println!("the area of the circle is {}", x * r * r);
}
https://book.rustlang-es.org/print 647/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Para ayudar a la integración del IDE, la comunidad Rust recomienda usar rust-analyzer .
Esta herramienta es un conjunto de utilidades centradas en el compilador que habla el
Protocolo del servidor de lenguaje
https://book.rustlang-es.org/print 648/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Apéndice E - Ediciones
En el Capítulo 1, viste que cargo new agrega un poco de metadatos a tu archivo Cargo.toml
sobre una edición. ¡Este apéndice habla sobre lo que eso significa!
Cada dos o tres años, el equipo de Rust produce una nueva edición de Rust. Cada edición
reúne las características que han aterrizado en un paquete claro con documentación y
herramientas completamente actualizadas. Las nuevas ediciones se envían como parte del
proceso de lanzamiento habitual de seis semanas.
Para los usuarios activos de Rust, una nueva edición reúne los cambios incrementales
en un paquete fácil de entender.
Para los no usuarios, una nueva edición señala que se han realizado algunos avances
importantes, lo que podría hacer que Rust valga la pena volver a mirar.
Para aquellos que desarrollan Rust, una nueva edición proporciona un punto de
reunión para todo el proyecto.
En el momento de escribir esto, hay tres ediciones de Rust disponibles: Rust 2015, Rust 2018
y Rust 2021. Este libro está escrito utilizando los modismos de la edición Rust 2021.
La clave edition en Cargo.toml indica qué edición debe usar el compilador para su código.
Si la clave no existe, Rust usa 2015 como el valor de la edición por razones de
compatibilidad con versiones anteriores.
Cada proyecto puede optar por una edición que no sea la edición predeterminada 2015. Las
ediciones pueden contener cambios incompatibles, como incluir una nueva palabra clave
que entra en conflicto con los identificadores en el código. Sin embargo, a menos que opte
por esos cambios, su código seguirá compilando incluso cuando actualice la versión del
compilador Rust que usa.
Todas las versiones del compilador Rust admiten cualquier edición que existía antes del
lanzamiento de ese compilador, y pueden vincular cajas de cualquier edición compatible
entre sí. Los cambios de edición solo afectan la forma en que el compilador analiza
inicialmente el código. Por lo tanto, si está usando Rust 2015 y una de sus dependencias usa
https://book.rustlang-es.org/print 649/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Rust 2018, su proyecto se compilará y podrá usar esa dependencia. La situación opuesta,
donde su proyecto usa Rust 2018 y una dependencia usa Rust 2015, también funciona.
Para ser claros: la mayoría de las características estarán disponibles en todas las ediciones.
Los desarrolladores que usen cualquier edición de Rust seguirán viendo mejoras a medida
que se realicen nuevos lanzamientos estables. Sin embargo, en algunos casos,
principalmente cuando se agregan nuevas palabras clave, algunas nuevas características
pueden estar disponibles solo en ediciones posteriores. Deberá cambiar de edición si desea
aprovechar dichas características.
Para más detalles, la Guía de edición es un libro completo sobre ediciones que enumera las
diferencias entre ediciones y explica cómo actualizar automáticamente su código a una
nueva edición a través de cargo fix .
https://book.rustlang-es.org/print 650/656
12/7/24, 22:37 El Lenguaje de Programación Rust
https://book.rustlang-es.org/print 651/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Como lenguaje, Rust se preocupa mucho por la estabilidad de tu código. Queremos que Rust
sea una base sólida sobre la que puedas construir, y si las cosas cambian constantemente,
eso sería imposible. Al mismo tiempo, si no podemos experimentar con nuevas
características, es posible que no descubramos fallos importantes hasta después de su
lanzamiento, cuando ya no podamos cambiar las cosas.
El desarrollo de Rust funciona con un horario de trenes. Es decir, todo el desarrollo se hace
en la rama master del repositorio de Rust. Las versiones siguen un modelo de tren de
lanzamiento de software, que ha sido utilizado por Cisco IOS y otros proyectos de software.
Hay tres canales de lanzamiento para Rust:
Nightly
Beta
Stable
https://book.rustlang-es.org/print 652/656
12/7/24, 22:37 El Lenguaje de Programación Rust
nightly: * - - * - - *
Cada seis semanas, es hora de preparar un nuevo lanzamiento! La rama beta del
repositorio de Rust se ramifica de la rama master utilizada por nightly. Ahora, hay dos
lanzamientos:
nightly: * - - * - - *
|
beta: *
La mayoría de los usuarios de Rust no utilizan las versiones beta activamente, pero prueban
contra beta en su sistema CI para ayudar a Rust a descubrir posibles regresiones. Mientras
tanto, hay un lanzamiento nightly cada noche:
nightly: * - - * - - * - - * - - *
|
beta: *
Digamos que se encuentra una regresión. ¡Qué bueno que tuvimos algo de tiempo para
probar la versión beta antes de que la regresión se colara en una versión estable! La
solución se aplica a master , de modo que nightly se arregla, y luego la solución se vuelve a
aplicar a la rama beta , y se produce una nueva versión de beta:
nightly: * - - * - - * - - * - - * - - *
|
beta: * - - - - - - - - *
Seis semanas después de que se creó la primera beta, ¡es hora de un lanzamiento estable!
La rama stable se produce a partir de la rama beta :
nightly: * - - * - - * - - * - - * - - * - * - *
|
beta: * - - - - - - - - *
|
stable: *
¡Hurra! ¡Rust 1.5 está listo! Sin embargo, nos hemos olvidado de una cosa: porque han
pasado las seis semanas, también necesitamos una nueva beta de la siguiente versión de
Rust, 1.6. Así que después de que stable se ramifica de beta , la siguiente versión de beta
se ramifica de nightly de nuevo:
nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: * - - - - - - - - * *
|
stable: *
https://book.rustlang-es.org/print 653/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Esto se llama el “modelo de tren” porque cada seis semanas, un lanzamiento “sale de la
estación”, pero aún tiene que hacer un viaje a través del canal beta antes de llegar como un
lanzamiento estable.
Rust se lanza cada seis semanas, como un reloj. Si conoces la fecha de un lanzamiento de
Rust, puedes conocer la fecha del siguiente: es seis semanas después. Un aspecto agradable
de tener lanzamientos programados cada seis semanas es que el próximo tren está
llegando pronto. Si una característica llega a perder un lanzamiento en particular, no hay
necesidad de preocuparse: ¡otro está sucediendo en un corto tiempo! Esto ayuda a reducir
la presión para colar posiblemente características poco pulidas cerca de la fecha límite de
lanzamiento.
Tiempo de Mantenimiento
El proyecto Rust admite la versión estable más reciente. Cuando una nueva versión estable
es lanzada, la versión anterior llega al final de su vida útil (EOL). Esto significa que cada
versión tiene soporte durante seis semanas.
Características inestables
Hay alo más con este modelo de lanzamiento: características inestables. Rust utiliza una
técnica llamada “indicadores de características” para determinar qué características están
habilitadas en un lanzamiento dado. Si una nueva característica está en desarrollo activo,
aterriza en master , y por lo tanto, en nightly, pero detrás de un indicador de característica.
Si, como usuario, desea probar la característica en progreso, puede hacerlo, pero debe estar
utilizando una versión nightly de Rust y anotar su código fuente con el indicador apropiado
para optar por ello.
Si está utilizando una versión beta o estable de Rust, no puede utilizar indicadores de
características. Esta es la clave que nos permite obtener un uso práctico con nuevas
características antes de declararlas estables para siempre. Aquellos que deseen optar por el
borde sangrante pueden hacerlo, y aquellos que deseen una experiencia sólida pueden
quedarse con estable y saber que su código no se romperá. Estabilidad sin estancamiento.
Este libro sólo contiene información sobre características estables, ya que las características
en progreso aún están cambiando, y seguramente serán diferentes entre cuando se escribió
https://book.rustlang-es.org/print 654/656
12/7/24, 22:37 El Lenguaje de Programación Rust
También puede ver todas las herramientas (versiones de Rust y componentes asociados) que
tiene instaladas con rustup . Aquí hay un ejemplo en uno de los autores de Windows:
$ cd ~/projects/needs-nightly
$ rustup override set nightly
Ahora, cada vez que llame a rustc o cargo dentro de ~/projects/needs-nightly, rustup se
asegurará de que esté utilizando Rust nocturno, en lugar de su estable predeterminado.
¡Esto es útil cuando tienes muchos proyectos de Rust!
Cualquiera puede escribir RFC para mejorar Rust, y las propuestas son revisadas y
discutidas por el equipo de Rust, que está compuesto por muchos subequipos de temas.
Hay una lista completa de los equipos en el sitio web de Rust, que incluye equipos para cada
área del proyecto: diseño de lenguaje, implementación de compilador, infraestructura,
https://book.rustlang-es.org/print 655/656
12/7/24, 22:37 El Lenguaje de Programación Rust
Después de algún tiempo, una vez que los desarrolladores de Rust que utilizan las versiones
nightly han podido probar la nueva característica, los miembros del equipo discutirán la
característica, cómo ha funcionado en nightly, y decidirán si debe o no hacerlo en Rust
estable. Si la decisión es seguir adelante, la puerta de la característica se elimina, ¡y la
característica ahora se considera estable! Monta los trenes en una nueva versión estable de
Rust.
https://book.rustlang-es.org/print 656/656