0% encontró este documento útil (0 votos)
6 vistas719 páginas

Libro Rust

El libro 'El Lenguaje de Programación Rust' es una guía introductoria que busca empoderar a los programadores a través del uso del lenguaje Rust, destacando su capacidad para escribir código seguro y eficiente. Rust es accesible para desarrolladores de diversos niveles y se utiliza en una amplia gama de aplicaciones, desde sistemas operativos hasta herramientas web. El texto incluye instrucciones sobre la instalación, conceptos fundamentales y proyectos prácticos para facilitar el aprendizaje del lenguaje.

Cargado por

santander050809
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como DOCX, PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
6 vistas719 páginas

Libro Rust

El libro 'El Lenguaje de Programación Rust' es una guía introductoria que busca empoderar a los programadores a través del uso del lenguaje Rust, destacando su capacidad para escribir código seguro y eficiente. Rust es accesible para desarrolladores de diversos niveles y se utiliza en una amplia gama de aplicaciones, desde sistemas operativos hasta herramientas web. El texto incluye instrucciones sobre la instalación, conceptos fundamentales y proyectos prácticos para facilitar el aprendizaje del lenguaje.

Cargado por

santander050809
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como DOCX, PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 719

Rust

jueves, 22 de mayo de 2025 : Página 1 de 719

Lenguaje de Programación Rust


por Steve Klabnik, Carol Nichols y Chris Krycho, y contribuciones de la comunidad
Rust.

Esta versión del texto asume que usas Rust 1.82.0 (publicado el 17/10/2024) o
posterior. Consulta la sección "Instalación" del Capítulo 1 para instalar o actualizar
Rust.

El formato HTML está disponible en línea


en https://doc.rust-lang.org/stable/book/ y fuera de línea con instalaciones de Rust
realizadas con rustup; ejecutar rustup doc --bookpara abrir.

También están disponibles varias traducciones comunitarias.

Este texto está disponible en formato de libro de bolsillo y libro electrónico en No


Starch Press .

¿Quieres una experiencia de aprendizaje más interactiva? Prueba una


versión diferente del Rust Book, que incluye: cuestionarios, resaltado,
visualizaciones y más : https://rust-book.cs.brown.edu

Prefacio

No siempre fue tan claro, pero el lenguaje de programación Rust se trata


fundamentalmente de empoderamiento : no importa qué tipo de código estés
escribiendo ahora, Rust te permite llegar más lejos y programar con confianza en
una variedad de dominios más amplia que antes.

Tomemos, por ejemplo, el trabajo a nivel de sistemas que aborda los detalles
básicos de la gestión de memoria, la representación de datos y la concurrencia.
Tradicionalmente, este ámbito de la programación se considera arcano, accesible
solo para unos pocos que han dedicado los años necesarios al aprendizaje para
evitar sus infames trampas. E incluso quienes lo practican lo hacen con cautela,
para evitar que su código esté expuesto a vulnerabilidades, fallos o corrupción.

Rust derriba estas barreras eliminando las antiguas dificultades y proporcionando


un conjunto de herramientas intuitivas y perfeccionadas para ayudarte en el
proceso. Los programadores que necesiten profundizar en el control de bajo nivel
pueden hacerlo con Rust, sin correr el riesgo habitual de fallos o vulnerabilidades
Rust
jueves, 22 de mayo de 2025 : Página 2 de 719

de seguridad, y sin tener que aprender los detalles de una cadena de


herramientas inestable. Mejor aún, el lenguaje está diseñado para guiarte de
forma natural hacia un código fiable y eficiente en términos de velocidad y uso de
memoria.

Los programadores que ya trabajan con código de bajo nivel pueden usar Rust
para ampliar sus horizontes. Por ejemplo, introducir paralelismo en Rust es una
operación de bajo riesgo: el compilador detectará automáticamente los errores
clásicos. Además, pueden realizar optimizaciones más agresivas en su código con
la confianza de no introducir accidentalmente fallos ni vulnerabilidades.

Pero Rust no se limita a la programación de sistemas de bajo nivel. Es lo


suficientemente expresivo y ergonómico como para que escribir aplicaciones
CLI, servidores web y muchos otros tipos de código sea muy agradable;
encontrarás ejemplos sencillos de ambos más adelante en el libro.

Trabajar con Rust te permite desarrollar habilidades que se transfieren de un


dominio a otro; puedes aprender Rust escribiendo una aplicación web y luego
aplicar esas mismas habilidades a tu Raspberry Pi.

Este libro aprovecha al máximo el potencial de Rust para empoderar a sus


usuarios. Es un texto amigable y accesible que te ayudará a mejorar no solo tus
conocimientos de Rust, sino también tu alcance y confianza como programador
en general. ¡Sumérgete, prepárate para aprender y bienvenido a la comunidad de
Rust!

— Nicholas Matsakis y Aaron Turon

Introducción

Nota: Esta edición del libro es la misma que The Rust Programming
Language, disponible en formato impreso y de libro electrónico en No Starch
Press .

Bienvenido a "El Lenguaje de Programación Rust" , un libro introductorio sobre


Rust. El lenguaje de programación Rust te ayuda a escribir software más rápido y
confiable. La ergonomía de alto nivel y el control de bajo nivel suelen estar en
conflicto en el diseño de lenguajes de programación; Rust desafía este conflicto.
Al equilibrar una potente capacidad técnica con una excelente experiencia de
desarrollo, Rust te brinda la opción de controlar detalles de bajo nivel (como el
Rust
jueves, 22 de mayo de 2025 : Página 3 de 719

uso de memoria) sin las complicaciones que tradicionalmente conlleva dicho


control.

Para quién es Rust

Rust es ideal para muchas personas por diversas razones. Analicemos algunos de
los grupos más importantes.

Equipos de desarrolladores

Rust está demostrando ser una herramienta productiva para la colaboración entre
grandes equipos de desarrolladores con distintos niveles de conocimiento en
programación de sistemas.

El código de bajo nivel es propenso a diversos errores sutiles, que en la mayoría


de los demás lenguajes solo se pueden detectar mediante pruebas exhaustivas y
una revisión minuciosa del código por parte de desarrolladores experimentados.
En Rust, el compilador desempeña un papel de guardián al evitar compilar código
con estos errores elusivos, incluyendo errores de concurrencia. Al trabajar junto
con el compilador, el equipo puede centrarse en la lógica del programa en lugar
de buscar errores.

Rust también aporta herramientas de desarrollo contemporáneas al mundo de la


programación de sistemas:

 Cargo, el administrador de dependencias y herramienta de compilación


incluidos, hace que agregar, compilar y administrar dependencias sea
sencillo y consistente en todo el ecosistema Rust.
 La herramienta de formato Rustfmt garantiza un estilo de codificación
consistente para todos los desarrolladores.
 El analizador de óxido potencia la integración del entorno de desarrollo
integrado (IDE) para completar código y enviar mensajes de error en línea.

Al utilizar estas y otras herramientas en el ecosistema Rust, los desarrolladores


pueden ser productivos mientras escriben código a nivel de sistemas.

Estudiantes

Rust está dirigido a estudiantes y a quienes estén interesados en aprender sobre


conceptos de sistemas. Gracias a Rust, muchas personas han aprendido sobre
Rust
jueves, 22 de mayo de 2025 : Página 4 de 719

temas como el desarrollo de sistemas operativos. La comunidad es muy


acogedora y está dispuesta a responder las preguntas de los estudiantes.
Mediante iniciativas como este libro, los equipos de Rust buscan que los
conceptos de sistemas sean más accesibles para más personas, especialmente
para quienes se inician en la programación.

Empresas

Cientos de empresas, grandes y pequeñas, utilizan Rust en producción para una


variedad de tareas, incluidas herramientas de línea de comandos, servicios web,
herramientas DevOps, dispositivos integrados, 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.

Desarrolladores de código abierto

Rust es para quienes desean desarrollar el lenguaje de programación Rust, su


comunidad, herramientas de desarrollo y bibliotecas. Nos encantaría que
contribuyeras al lenguaje Rust.

Personas que valoran la velocidad y la estabilidad

Rust es para quienes buscan velocidad y estabilidad en un lenguaje. Por


velocidad, nos referimos tanto a la velocidad con la que se ejecuta el código de
Rust como a la velocidad con la que Rust permite escribir programas. Las
comprobaciones del compilador de Rust garantizan la estabilidad mediante la
incorporación de características y la refactorización. Esto contrasta con el frágil
código heredado de los lenguajes sin estas comprobaciones, que los
desarrolladores suelen temer modificar. Al buscar abstracciones de coste cero,
características de alto nivel que se compilan a código de bajo nivel tan rápido
como el código escrito manualmente, Rust se esfuerza por que el código seguro
sea también código rápido.

El lenguaje Rust también espera dar soporte a muchos otros usuarios; los
mencionados aquí son solo algunos de los principales interesados. En general, la
mayor ambición de Rust es eliminar las desventajas que los programadores han
aceptado durante décadas, ofreciendo seguridad , productividad,
velocidad y ergonomía. Prueba Rust y comprueba si sus opciones te funcionan.
Rust
jueves, 22 de mayo de 2025 : Página 5 de 719
Para quién es este libro

Este libro asume que has escrito código en otro lenguaje de programación, pero
no presupone cuál. Hemos intentado que el material sea accesible para personas
con diversos antecedentes en programación. No dedicamos mucho tiempo a
hablar sobre qué es la programación ni cómo pensar en ella. Si eres totalmente
nuevo en programación, te conviene más leer un libro que ofrezca una
introducción específica a la programación.

Cómo utilizar este libro

En general, este libro asume que se lee en secuencia, de principio a fin. Los
capítulos posteriores amplían los conceptos de los capítulos anteriores, y estos
últimos podrían no profundizar en los detalles de un tema en particular, sino que
lo retoman en un capítulo posterior.

En este libro encontrarás dos tipos de capítulos: capítulos conceptuales y


capítulos de proyecto. En los capítulos conceptuales, aprenderás sobre un
aspecto de Rust. En los capítulos de proyecto, crearemos pequeños programas
juntos, aplicando lo aprendido hasta ahora. Los capítulos 2, 12 y 21 son capítulos
de proyecto; el resto son capítulos conceptuales.

El Capítulo 1 explica cómo instalar Rust, cómo escribir un programa "¡Hola,


mundo!" y cómo usar Cargo, el gestor de paquetes y la herramienta de
compilación de Rust.

El Capítulo 2 es una introducción práctica a la escritura de programas en Rust,


donde crearás un juego de adivinanzas. Aquí cubrimos conceptos a un alto nivel,
y los 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,.

El Capítulo 4 aprenderás sobre el sistema de propiedad de Rust.

Si eres un estudiante particularmente meticuloso que prefiere aprender cada


detalle antes de pasar al siguiente, es posible que quieras saltarte el Capítulo 2 e
ir directamente al Capítulo 3, volviendo al Capítulo 2 cuando quieras trabajar en
un proyecto aplicando los detalles que has aprendido.
Rust
jueves, 22 de mayo de 2025 : Página 6 de 719

El capítulo 5 trata sobre estructuras y métodos.

El capítulo 6 sobre enumeraciones, match expresiones y la if let construcción del


flujo de control. Usarás estructuras y enumeraciones para crear tipos
personalizados en Rust.

En el Capítulo 7, aprenderá sobre el sistema de módulos de Rust y las reglas de


privacidad para organizar su código y su Interfaz de Programación de Aplicaciones
(API) pública.

El Capítulo 8 analiza algunas estructuras de datos de colección comunes que


proporciona la biblioteca estándar, como vectores, cadenas y mapas hash.

El Capítulo 9 explora la filosofía y las técnicas de gestión de errores de Rust.

El capítulo 10 profundiza en los genéricos, los rasgos y los tiempos de vida, lo


que permite definir código aplicable a múltiples tipos.

El capítulo 11 se centra en las pruebas, que, incluso con las garantías de


seguridad de Rust, son necesarias para garantizar la correcta lógica del
programa.

En el capítulo 12, crearemos nuestra propia implementación de un subconjunto


de funcionalidades de la grep herramienta de línea de comandos que busca texto
en archivos. Para ello, utilizaremos muchos de los conceptos que abordamos en
los capítulos anteriores.

El capítulo 13 explora los cierres e iteradores: características de Rust


provenientes de lenguajes de programación funcionales.

En el capítulo 14, analizaremos Cargo con más profundidad y hablaremos sobre


las mejores prácticas para compartir bibliotecas.

El capítulo 15 analiza los punteros inteligentes que proporciona la biblioteca


estándar y las características que habilitan su funcionalidad.

En el capítulo 16, analizaremos diferentes modelos de programación


concurrente y explicaremos cómo Rust te ayuda a programar en múltiples hilos
sin problemas.
Rust
jueves, 22 de mayo de 2025 : Página 7 de 719

En el capítulo 17, profundizaremos en este tema explorando la sintaxis async y


await de Rust, así como el modelo de concurrencia ligero que admiten.

El capítulo 18 analiza cómo los modismos de Rust se comparan con los


principios de programación orientada a objetos con los que quizás esté
familiarizado.

El capítulo 19 es una referencia sobre patrones y su coincidencia, que son


formas eficaces de expresar ideas en los programas Rust.

El capítulo 20 contiene una amplia gama de temas avanzados de interés,


incluyendo Rust inseguro, macros y más información sobre tiempos de vida,
rasgos, tipos, funciones y cierres.

En el Capítulo 21, completaremos un proyecto en el que implementaremos un


servidor web multiproceso de bajo nivel.

Finalmente, algunos apéndices contienen información útil sobre el lenguaje en un


formato más similar a una referencia.

El Apéndice A abarca las palabras clave de Rust.

El Apéndice B sus operadores y símbolos.

El Apéndice C los rasgos derivables proporcionados por la biblioteca estándar.

El Apéndice D algunas herramientas de desarrollo útiles.

El Apéndice E explica las ediciones de Rust.

El Apéndice F encontrará traducciones del libro.

El Apéndice G explicaremos cómo se crea Rust y qué es Rust nocturno.

No hay una forma incorrecta de leer este libro: si quieres adelantar algo, ¡hazlo!
Quizás tengas que volver a capítulos anteriores si te confundes. Pero haz lo que te
funcione.

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 un código funcional. Por ello, proporcionaremos muchos ejemplos que no
Rust
jueves, 22 de mayo de 2025 : Página 8 de 719

compilan, junto con el mensaje de error que el compilador mostrará en cada


situación. Ten en cuenta que si introduces y ejecutas un ejemplo aleatorio,
¡podría no compilar! Asegúrate de leer el texto circundante para ver si el ejemplo
que intentas ejecutar está diseñado para generar errores. Ferris también te
ayudará a distinguir el código que no está diseñado para funcionar:

Ferri
Significado
s

¡Este código no se compila!

¡Este código causa pánico!

Este código no produce el comportamiento


deseado.

En la mayoría de las situaciones, lo guiaremos a la versión correcta de cualquier


código que no se compile.

Código fuente

Los archivos fuente a partir de los cuales se genera este libro se pueden
encontrar en GitHub .

Empezando

¡Comencemos tu aventura en Rust! Hay mucho que aprender, pero todo viaje
empieza en algún punto. En este capítulo, hablaremos sobre:

 Instalación de Rust en Linux, macOS y Windows


 Escribir un programa que imprimeHello, world!
 Usando cargo el administrador de paquetes y el sistema de compilación de
Rust

Instalación
Rust
jueves, 22 de mayo de 2025 : Página 9 de 719

El primer paso es instalar Rust. Lo descargaremos mediante rustup, una


herramienta de línea de comandos para gestionar las versiones de Rust y las
herramientas asociadas. Necesitará conexión a internet para la descarga.

Nota: Si prefiere no usarlo rustup por algún motivo, consulte la página Otros
métodos de instalación de Rust para obtener más opciones.

Los siguientes pasos instalan la última versión estable del compilador de Rust. Las
garantías de estabilidad de Rust garantizan que todos los ejemplos del libro que
compilan seguirán compilando con versiones más recientes de Rust. El resultado
puede variar ligeramente entre versiones, ya que Rust suele mejorar los mensajes
de error y las advertencias. En otras palabras, cualquier versión estable más
reciente de Rust que instale siguiendo estos pasos debería funcionar
correctamente con el contenido de este libro.

Notación de línea de comandos

En este capítulo y a lo largo del libro, mostraremos algunos comandos utilizados


en la terminal. Las líneas que debe introducir en una terminal empiezan por $. No
es necesario escribir el $carácter; es el símbolo del sistema el que indica el inicio
de cada comando. Las líneas que no empiezan por, $generalmente, muestran la
salida del comando anterior. Además, los ejemplos específicos de PowerShell
usarán >en lugar de $.

Instalación rustupen Linux o macOS

Si usa Linux o macOS, abra una terminal e ingrese el siguiente comando:

$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

El comando descarga un script e inicia la instalación de la rustup herramienta,


que instala la última versión estable de Rust. Es posible que se le solicite su
contraseña. Si la instalación se realiza correctamente, aparecerá la siguiente
línea:

Rust is installed now. Great!

También necesitarás un enlazador , un programa que Rust usa para unir las
salidas compiladas en un solo archivo. Probablemente ya tengas uno. Si recibes
errores del enlazador, deberías instalar un compilador de C, que normalmente
Rust
jueves, 22 de mayo de 2025 : Página 10 de 719

incluye un enlazador. El compilador de C también es útil porque algunos paquetes


comunes de Rust dependen del código C y lo requieren.

En macOS, puedes obtener un compilador de C ejecutando:

$ xcode-select --install

Los usuarios de Linux generalmente deberían instalar GCC o Clang, según la


documentación de su distribución. Por ejemplo, si usan Ubuntu, pueden instalar
el build-essentialpaquete.

Instalación rustupen Windows

En Windows, vaya a https://www.rust-lang.org/tools/install y siga las instrucciones


para instalar Rust. En algún momento de la instalación, se le solicitará que instale
Visual Studio. Este proporciona un enlazador y las bibliotecas nativas necesarias
para compilar programas. Si necesita más ayuda con este paso,
consulte https://rust-lang.github.io/rustup/installation/windows-msvc.html

El resto de este libro utiliza comandos que funcionan tanto en cmd.exe como en
PowerShell. Si existen diferencias específicas, explicaremos cuál usar.

Solución de problemas

Para comprobar si tienes Rust instalado correctamente, abre un shell e introduce


esta línea:

$ rustc --version

Debería ver el número de versión, el hash de confirmación y la fecha de


confirmación de la última versión estable que se ha publicado, en el siguiente
formato:

rustc x.y.z (abcabcabc yyyy-mm-dd)

Si ves esta información, ¡has instalado Rust correctamente! Si no la ves,


comprueba que Rust esté en %PATH%la variable de tu sistema como se indica a
continuación.

En Windows CMD, utilice:

> echo %PATH%


Rust
jueves, 22 de mayo de 2025 : Página 11 de 719

En PowerShell, utilice:

> echo $env:Path

En Linux y macOS, utilice:

$ echo $PATH

Si todo esto es correcto y Rust sigue sin funcionar, hay varios lugares donde
puedes obtener ayuda. Descubre cómo contactar con otros Rustáceos (un apodo
gracioso con el que nos llamamos) en la página de la comunidad .

Actualización y desinstalación

Una vez instalado Rust mediante [nombre del archivo] rustup, actualizar a una
versión nueva es fácil. Desde tu shell, ejecuta el siguiente script de actualización:

$ rustup update

Para desinstalar Rust y rustup, ejecute el siguiente script de desinstalación desde


su shell:

$ rustup self uninstall

Documentación local

La instalación de Rust también incluye una copia local de la documentación para


que puedas consultarla sin conexión. Ejecuta rustup doc para abrir la
documentación local en tu navegador.

Cada vez que la biblioteca estándar proporciona un tipo o una función y no está
seguro de qué hace o cómo usarlo, utilice la documentación de la interfaz de
programación de aplicaciones (API) para averiguarlo.

Editores de texto y entornos de desarrollo integrados

Este libro no presupone qué herramientas se utilizan para crear código en Rust.
¡Casi cualquier editor de texto servirá! Sin embargo, muchos editores de texto y
entornos de desarrollo integrados (IDE) son compatibles con Rust. Siempre
puedes encontrar una lista bastante actualizada de editores e IDE en la página de
herramientas del sitio web de Rust.
Rust
jueves, 22 de mayo de 2025 : Página 12 de 719
¡Hola Mundo!

Ahora que has instalado Rust, es hora de escribir tu primer programa en Rust. Al
aprender un nuevo lenguaje, es tradicional escribir un pequeño programa que
imprima el texto Hello, world! en pantalla, ¡así que haremos lo mismo aquí!

Nota: Este libro presupone un conocimiento básico de la línea de comandos. Rust


no exige requisitos específicos sobre la edición, las herramientas ni la ubicación
del código, así que si prefiere usar un entorno de desarrollo integrado (IDE) en
lugar de la línea de comandos, puede usar su IDE preferido. Muchos IDE ya
ofrecen cierto grado de compatibilidad con Rust; consulte la documentación del
IDE para obtener más información. El equipo de Rust se ha centrado en ofrecer
una excelente compatibilidad con IDE mediante [insertar código] rust-analyzer.
Consulte el Apéndice D para obtener más información.

Creación de un directorio de proyectos

Comenzarás creando un directorio para almacenar tu código de Rust. A Rust no le


importa dónde se encuentre tu código, pero para los ejercicios y proyectos de
este libro, te sugerimos crear un directorio de proyectos en tu directorio personal
y guardar todos tus proyectos allí.

Abra una terminal e ingrese los siguientes comandos para crear un directorio de
proyectos y un directorio para el proyecto “Hola, mundo!” dentro del directorio de
proyectos .

Para Linux, macOS y PowerShell en Windows, ingrese esto:

$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world

Para Windows CMD, ingrese esto:

> fn main
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world

Cómo escribir y ejecutar un programa en Rust


Rust
jueves, 22 de mayo de 2025 : Página 13 de 719

A continuación, crea un nuevo archivo fuente y llámalo main.rs. Los archivos de


Rust siempre terminan con la extensión .rs . Si usas más de una palabra en el
nombre del archivo, la convención es usar un guion bajo para separarlas. Por
ejemplo, usa hello_world.rs en lugar de helloworld.rs. .

Ahora abra el archivo main.rs que acaba de crear e ingrese el código del Listado
1-1.

Nombre del archivo: main.rs

fn main() {
println!("Hello, world!");
}

Listado 1-1: Un programa que imprime Hello, world!

Guarde el archivo y vuelva a la ventana de terminal en el


directorio ~/projects/hello_world . En Linux o macOS, introduzca los siguientes
comandos para compilar y ejecutar el archivo:

$ rustc main.rs
$ ./main
Hello, world!

En Windows, ingrese el comando .\main.exe en lugar de ./main:

> rustc main.rs


> .\main.exe
Hello, world!

Independientemente de su sistema operativo, la cadena Hello, world! debería


imprimirse en la terminal. Si no ve este resultado, consulte la sección "Solución de
problemas" de la sección de Instalación para obtener ayuda.

Si Hello, world! lo imprimiste, ¡felicidades! Ya escribiste un programa en Rust. Eso


te convierte en programador en Rust. ¡Bienvenido!

Anatomía de un programa Rust

Repasemos este programa "¡Hola mundo!" en detalle. Aquí está la primera pieza
del rompecabezas:
Rust
jueves, 22 de mayo de 2025 : Página 14 de 719
fn main() {

Estas líneas definen una función llamada main. Esta mainfunción es especial:
siempre es el primer código que se ejecuta en cada programa ejecutable de Rust.
Aquí, la primera línea declara una función llamada mainque no tiene parámetros y
no devuelve nada. Si hubiera parámetros, irían entre paréntesis ().

El cuerpo de la función se encierra en {}. Rust requiere llaves alrededor de todos


los cuerpos de las funciones. Es recomendable colocar la llave de apertura en la
misma línea que la declaración de la función, con un espacio entre ellas.

Nota: Si desea mantener un estilo estándar en todos sus proyectos de Rust,


puede usar una herramienta de formateo automática llamada "formateador
automático" rustfmtpara formatear su código con un estilo específico (más
información rustfmten el Apéndice D ). El equipo de Rust ha incluido esta
herramienta con la distribución estándar de Rust, tal rustccual, por lo que ya
debería estar instalada en su ordenador.

El cuerpo de la mainfunción contiene el siguiente código:

println!("Hello, world!");

Esta línea realiza todo el trabajo en este pequeño programa: imprime texto en la
pantalla. Hay cuatro detalles importantes a tener en cuenta.

Primero, println! llama a una macro de Rust. Si hubiera llamado a una función, se
habría ingresado como println(sin el !). Analizaremos las macros de Rust con más
detalle en el Capítulo 20. Por ahora, solo necesita saber que usar un ! significa
que está llamando a una macro en lugar de a una función normal y que las
macros no siempre siguen las mismas reglas que las funciones.

En segundo lugar, se ve la "Hello, world!" cadena. Pasamos esta cadena como


argumento a [<string> println!] y se imprime en pantalla.

En tercer lugar, terminamos la línea con un punto y coma ( ;), lo que indica que
esta expresión ha terminado y que la siguiente está lista para comenzar. La
mayoría de las líneas de código de Rust terminan con punto y coma.

Compilar y ejecutar son pasos separados


Rust
jueves, 22 de mayo de 2025 : Página 15 de 719

Acaba de ejecutar un programa recién creado, así que examinemos cada paso del
proceso.

Antes de ejecutar un programa Rust, debes compilarlo usando el compilador Rust


ingresando el rustccomando y pasándole el nombre de tu archivo fuente, de esta
manera:

$ rustc main.rs

Si tienes conocimientos de C o C++, notarás que esto es similar a gcc o clang.


Tras compilar correctamente, Rust genera un ejecutable binario.

En Linux, macOS y PowerShell en Windows, puedes ver el ejecutable ingresando


el lscomando en tu shell:

$ ls
main main.rs

En Linux y macOS, verás dos archivos. Con PowerShell en Windows, verás los
mismos tres archivos que con CMD. Con CMD en Windows, debes escribir lo
siguiente:

> dir /B %= the /B option says to only show the file names =%
main.exe
main.pdb
main.rs

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 demás plataformas) y,
en Windows, un archivo con información de depuración con la extensión .pdb .
Desde aquí, se ejecuta el archivo main o main.exe , así:

$ ./main # or .\main.exe on Windows

Si su archivo main.rs es su programa “¡Hola, mundo!”, esta línea se


imprime Hello, world!en su terminal.

Si estás más familiarizado con un lenguaje dinámico, como Ruby, Python o


JavaScript, quizás no estés acostumbrado a compilar y ejecutar un programa
como pasos separados. Rust es un lenguaje compilado con antelación , lo que
significa que puedes compilar un programa y entregar el ejecutable a otra
persona, quien podrá ejecutarlo incluso sin tener Rust instalado. Si le das a
Rust
jueves, 22 de mayo de 2025 : Página 16 de 719

alguien un archivo .rb , .py o .js , necesita tener instalada una implementación de
Ruby, Python o JavaScript (respectivamente). Pero en esos lenguajes, solo
necesitas un comando para compilar y ejecutar tu programa. Todo es cuestión de
equilibrio en el diseño del lenguaje.

Compilar con rustces suficiente para programas sencillos, pero a medida que tu
proyecto crezca, querrás gestionar todas las opciones y facilitar la compartición
de tu código. A continuación, te presentaremos la herramienta Cargo, que te
ayudará a escribir programas reales en Rust.

¡Hola, Cargo!

Cargo es el sistema de compilación y el gestor de paquetes de Rust. La mayoría


de los rustáceos usan esta herramienta para gestionar sus proyectos de Rust, ya
que Cargo gestiona muchas tareas por ti, como compilar tu código, descargar las
bibliotecas de las que depende y compilar dichas bibliotecas. (Las bibliotecas que
tu código necesita se denominan dependencias). ").

Los programas Rust más sencillos, como el que hemos escrito hasta ahora, no
tienen dependencias. Si hubiéramos creado el proyecto "¡Hola mundo!" con
Cargo, solo usaría la parte de Cargo que se encarga de la creación del código. A
medida que escribas programas Rust más complejos, irás añadiendo
dependencias, y si empiezas un proyecto con Cargo, añadir dependencias será
mucho más fácil.

Dado que la gran mayoría de los proyectos de Rust usan Cargo, el resto de este
libro asume que usted también lo usa. Cargo viene instalado con Rust si utilizó los
instaladores oficiales mencionados en la sección "Instalación" . Si instaló Rust por
otros medios, compruebe si Cargo está instalado introduciendo lo siguiente en su
terminal:

$ cargo --version

Si ves un número de versión, ¡la tienes! Si ves un error, como command not
found, consulta la documentación de tu método de instalación para determinar
cómo instalar Cargo por separado.

Creando un proyecto con Cargo


Rust
jueves, 22 de mayo de 2025 : Página 17 de 719

Creemos un nuevo proyecto con Cargo y veamos en qué se diferencia de nuestro


proyecto original "¡Hola mundo!". Regresa al directorio de proyectos (o donde
hayas guardado el código). Luego, en cualquier sistema operativo, ejecuta lo
siguiente:

$ cargo new hello_cargo


$ cd hello_cargo

El primer comando crea un nuevo directorio y proyecto llamado hello_cargo .


Llamamos a nuestro proyecto hello_cargo y Cargo crea sus archivos en un
directorio con el mismo nombre.

Accede al directorio hello_cargo y lista los archivos. Verás que Cargo ha generado
dos archivos y un directorio: un archivo Cargo.toml y un directorio src con
un archivo main.rs.

También se ha inicializado un nuevo repositorio Git junto con un


archivo .gitignore . Los archivos Git no se generarán si se ejecuta cargo
newdentro de un repositorio Git existente; puede anular este comportamiento
usando cargo new --vcs=git.

Nota: Git es un sistema de control de versiones común. Puedes cargo newusar un


sistema de control de versiones diferente o ninguno usando la --vcsopción
"Ejecutar" cargo new --helppara ver las opciones disponibles.

Abra Cargo.toml en su editor de texto preferido. Debería ser similar al código del
Listado 1-2.

Nombre del archivo: Cargo.toml


[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at


https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
Listado 1-2: Contenido de Cargo.toml generado porcargo new

Este archivo está en formato TOML ( Tom's Obvious, Minimal Language ), que es
el formato de configuración de Cargo.
Rust
jueves, 22 de mayo de 2025 : Página 18 de 719

La primera línea, [package], es un encabezado de sección que indica que las


siguientes instrucciones configuran un paquete. A medida que agreguemos más
información a este archivo, añadiremos otras secciones.

Las siguientes 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 de la editionclave en el Apéndice E. el

La última línea, [dependencies], marca el inicio de una sección donde puedes


listar las dependencias de tu proyecto. En Rust, los paquetes de código se
denominan crates . No necesitaremos otros crates para este proyecto, pero sí los
necesitaremos en el primer proyecto del Capítulo 2, así que usaremos esta
sección de dependencias.

Ahora abre src/main.rs y echa un vistazo:

Nombre de archivo: src/main.rs

fn main() {
println!("Hello, world!");
}

Cargo ha generado un programa "¡Hola mundo!" para ti, igual al que escribimos
en el Listado 1-1. Hasta ahora, las diferencias entre nuestro proyecto y el que
generó Cargo son que Cargo colocó el código en el directorio src y tenemos un
archivo de configuración Cargo.toml en el directorio superior.

Cargo espera que tus archivos fuente se encuentren en el directorio src . El


directorio principal del proyecto es solo para archivos README, información de
licencia, archivos de configuración y cualquier otro elemento no relacionado con
tu código. Usar Cargo te ayuda a organizar tus proyectos. Hay un lugar para todo,
y cada cosa está en su sitio.

Si iniciaste un proyecto que no usa Cargo, como hicimos con el proyecto "¡Hola
mundo!", puedes convertirlo en uno que sí lo use. Mueve el código del proyecto
al directorio src y crea un archivo Cargo.toml apropiado . Una forma sencilla de
obtenerlo es ejecutar , que lo creará automáticamente.cargo init

Construcción y gestión de un proyecto de carga


Rust
jueves, 22 de mayo de 2025 : Página 19 de 719

Ahora veamos qué cambia al compilar y ejecutar el programa "¡Hola mundo!" con
Cargo. Desde el directorio hello_cargo , compila tu proyecto con el siguiente
comando:

$ cargo build
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs

Este comando crea un archivo ejecutable en target/debug/hello_cargo (o target\


debug\hello_cargo.exe en Windows) en lugar de en el directorio actual. Dado que
la compilación predeterminada es de depuración, Cargo guarda el archivo binario
en un directorio llamado debug . Puede ejecutar el ejecutable con este comando:

$ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows


Hello, world!

Si todo va bien, Hello, world!debería imprimirse en la terminal. cargo buildLa


primera ejecución también provoca que Cargo cree un nuevo archivo en el nivel
superior: Cargo.lock. . Este archivo registra 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 modificar este archivo
manualmente; Cargo gestiona su contenido por ti.

Acabamos de crear un proyecto con cargo buildy lo ejecutamos


con ./target/debug/hello_cargo, pero también podemos usar cargo runpara
compilar el código y luego ejecutar el ejecutable resultante, todo en un solo
comando:

$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/hello_cargo`
Hello, world!

Usarlo cargo runes más conveniente que tener que recordar ejecutar cargo buildy
luego usar la ruta completa al binario, por eso la mayoría de los desarrolladores
usan cargo run.

Tenga en cuenta que esta vez no vimos ningún resultado que indicara que Cargo
estaba compilando hello_cargo. Cargo detectó que los archivos no habían
cambiado, por lo que no recompiló, sino que simplemente ejecutó el binario. Si
Rust
jueves, 22 de mayo de 2025 : Página 20 de 719

hubiera modificado el código fuente, Cargo habría recompilado el proyecto antes


de ejecutarlo y habría visto este resultado:

$ 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


revisa rápidamente el código para asegurarse de que se compile, pero no genera
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 checkes mucho más


rápido cargo buildporque omite el paso de producirlo. Si revisas constantemente
tu trabajo mientras escribes el código, usar [nombre del archivo] cargo
checkacelerará el proceso de avisarte si tu proyecto aún se está compilando. Por
eso, muchos rustáceos ejecutan [nombre del archivo] cargo checkperiódicamente
mientras escriben su programa para asegurarse de que compila. Luego, lo
ejecutan cargo buildcuando están listos para usar el ejecutable.

Resumamos lo que hemos aprendido hasta ahora sobre Cargo:

 Podemos crear un proyecto usando cargo new.


 Podemos construir un proyecto usando cargo build.
 Podemos construir y ejecutar un proyecto en un solo paso usando cargo
run.
 Podemos construir un proyecto sin producir un binario para verificar errores
usando cargo check.
 En lugar de guardar el resultado de la compilación en el mismo directorio
que nuestro código, Cargo lo almacena en el directorio target/debug .

Una ventaja adicional de usar Cargo es que los comandos son los mismos
independientemente del sistema operativo. Por lo tanto, por ahora, ya no
proporcionaremos instrucciones específicas para Linux y macOS frente a
Windows.
Rust
jueves, 22 de mayo de 2025 : Página 21 de 719
Construyendo para la liberación

Cuando tu proyecto esté listo para su lanzamiento, puedes cargo build --


releasecompilarlo con optimizaciones. Este comando creará un ejecutable
en target/release en lugar de target/debug . Las optimizaciones aceleran la
ejecución de tu código en Rust, pero activarlas alarga el tiempo de compilación.
Por eso existen dos perfiles: uno para desarrollo, cuando quieres reconstruir
rápidamente y con frecuencia, y otro para compilar el programa final que
entregarás a un usuario, que no se reconstruirá repetidamente y se ejecutará lo
más rápido posible. Si estás evaluando el tiempo de ejecución de tu código,
asegúrate de ejecutarlo cargo build --releasey realizar la evaluación comparativa
con el ejecutable en target/release .

La carga como convención

Con proyectos simples, Cargo no aporta mucho valor en comparación con


simplemente usarlo rustc , pero demostrará su eficacia a medida que tus
programas se vuelvan más complejos. Cuando los programas crecen hasta ocupar
varios archivos o necesitan una dependencia, es mucho más fácil dejar que Cargo
coordine la compilación.

Aunque el hello_cargoproyecto es simple, ahora utiliza gran parte de las


herramientas 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 extraer el código con Git, acceder al directorio del proyecto y compilar:

$ git clone example.org/someproject


$ cd someproject
$ cargo build

Para obtener más información sobre Cargo, consulte su documentación .

Resumen

¡Tu experiencia en Rust ya ha empezado con buen pie! En este capítulo, has
aprendido a:

 Instale la última versión estable de Rust usandorustup


 Actualizar a una versión más nueva de Rust
 Abrir la documentación instalada localmente
 Escriba y ejecute un programa “¡Hola, mundo!” usando rustcdirectamente
Rust
jueves, 22 de mayo de 2025 : Página 22 de 719

 Cree y ejecute un nuevo proyecto utilizando las convenciones de Cargo

Este es un buen momento para crear un programa más completo y familiarizarse


con la lectura y escritura de código Rust. Por eso, en el Capítulo 2, crearemos un
programa de adivinanzas. Si prefieres empezar aprendiendo cómo funcionan los
conceptos comunes de programación en Rust, consulta el Capítulo 3 y luego
vuelve al Capítulo 2.

Programando un juego de adivinanzas

¡Adentrémonos en Rust trabajando juntos en un proyecto práctico! Este capítulo


te presenta algunos conceptos comunes de Rust y te muestra cómo usarlos en un
programa real. Aprenderás sobre letlos matchmétodos, las funciones asociadas,
los crates externos y mucho más. En los siguientes capítulos, exploraremos estas
ideas con más detalle. En este capítulo, solo practicarás los fundamentos.

Implementaremos un problema clásico de programación para principiantes: un


juego de adivinanzas. Funciona así: el programa generará un entero aleatorio
entre 1 y 100. A continuación, solicitará al jugador que introduzca una suposición.
Tras introducirla, el programa indicará si es demasiado baja o demasiado alta. Si
la suposición es correcta, el juego imprimirá un mensaje de felicitación y
finalizará.

Configurar un nuevo proyecto

Para configurar un nuevo proyecto, vaya al directorio de proyectos que creó en el


Capítulo 1 y cree un nuevo proyecto usando Cargo, de la siguiente manera:

$ cargo new guessing_game


$ cd guessing_game

El primer comando, cargo new, toma el nombre del proyecto ( guessing_game)


como primer argumento. El segundo comando cambia al directorio del nuevo
proyecto.

Mira el archivo Cargo.toml generado :

Nombre del archivo: Cargo.toml

[package]
name = "guessing_game"
Rust
jueves, 22 de mayo de 2025 : Página 23 de 719
version = "0.1.0"
edition = "2021"

[dependencies]

Como viste en el Capítulo 1, cargo newgenera un programa "¡Hola mundo!".


Consulta el archivo src/main.rs :

Nombre de archivo: src/main.rs

fn main() {
println!("Hello, world!");
}

Ahora compilemos este programa “¡Hola, mundo!” y ejecútelo en el mismo paso


usando el cargo runcomando:

$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.20s
Running `target/debug/guessing_game`
Hello, world!

El runcomando resulta útil cuando necesitas iterar rápidamente en un proyecto,


como haremos en este juego, probando rápidamente cada iteración antes de
pasar a la siguiente.

Vuelve a abrir el archivo src/main.rs . Escribirás todo el código en este archivo.

Procesando una conjetura

La primera parte del programa del juego de adivinanzas solicitará la entrada del
usuario, la procesará y comprobará que esté en el formato esperado. Para
empezar, permitiremos que el jugador introduzca una suposición. Introduzca el
código del Listado 2-1 en src/main.rs .

Nombre de archivo: src/main.rs


use std::io;

fn main() {
println!("Guess the number!");

println!("Please input your guess.");


Rust
jueves, 22 de mayo de 2025 : Página 24 de 719
let mut guess = String::new();

io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");

println!("You guessed: {}", guess);


}
Listado 2-1: Código que obtiene una suposición del usuario y la imprime

Este código contiene mucha información, así que vamos a analizarlo línea por
línea. Para obtener la entrada del usuario e imprimir el resultado como salida,
necesitamos incluir la iobiblioteca de entrada/salida en el alcance.
Esta iobiblioteca proviene de la biblioteca estándar, conocida como std:

use std::io;

Por defecto, Rust tiene un conjunto de elementos definidos en la biblioteca


estándar que se incorpora al alcance de cada programa. Este conjunto se
denomina preludio y puede consultarse su contenido completo en la
documentación de la biblioteca estándar .

Si un tipo que desea usar no está en el preludio, debe incluirlo explícitamente en


el ámbito mediante una usedeclaración. Usar la std::iobiblioteca le ofrece diversas
funciones útiles, como la posibilidad de aceptar entradas del usuario.

Como viste en el Capítulo 1, la mainfunción es el punto de entrada al programa:

fn main() {

La fnsintaxis declara una nueva función; los paréntesis, (), indican que no hay
parámetros; y la llave, {, inicia el cuerpo de la función.

Como también aprendiste en el Capítulo 1, println!es una macro que imprime una
cadena en la pantalla:

println!("Guess the number!");

println!("Please input your guess.");

Este código imprime un mensaje indicando de qué se trata el juego y solicitando


información al usuario.
Rust
jueves, 22 de mayo de 2025 : Página 25 de 719
Almacenamiento de valores con variables

A continuación, crearemos una variable para almacenar la entrada del usuario,


como esta:

let mut guess = String::new();

¡Ahora el programa se pone interesante! Hay mucho que hacer en esta pequeña
línea. Usamos la letinstrucción para crear la variable. Aquí hay otro ejemplo:

let apples = 5;

Esta línea crea una nueva variable llamada applesy la vincula al valor 5. En Rust,
las variables son inmutables por defecto, lo que significa que una vez que les
asignamos un valor, este no cambia. Analizaremos este concepto en detalle en la
sección "Variables y Mutabilidad" del Capítulo 3. Para que una variable sea
mutable, añadimos mutantes del nombre de la variable:

let apples = 5; // immutable


let mut bananas = 5; // mutable

Nota: La //sintaxis inicia un comentario que continúa hasta el final de la línea.


Rust ignora todo lo que contiene el comentario. Analizaremos los comentarios con
más detalle en el Capítulo 3 .

Volviendo al programa del juego de adivinanzas, ahora sabes que let mut
guessintroducirá una variable mutable llamada guess. El signo igual ( =) indica a
Rust que queremos asociar algo a la variable. A la derecha del signo igual se
encuentra el valor guessal que se asocia, que es el resultado de llamar
a String::new, una función que devuelve una nueva instancia de
un String. String es un tipo de cadena proporcionado por la biblioteca estándar
que consiste en un fragmento de texto ampliable y codificado en UTF-8.

La ::sintaxis de la ::newlínea indica que newes una función asociada


del Stringtipo. Una función asociada es una función implementada en un tipo, en
este caso String. Esta newfunción crea una nueva cadena vacía. Encontrarás
una newfunción en muchos tipos porque es un nombre común para una función
que crea un nuevo valor de algún tipo.
Rust
jueves, 22 de mayo de 2025 : Página 26 de 719

En su totalidad, la let mut guess = String::new();línea ha creado una variable


mutable que actualmente está vinculada a una nueva instancia vacía de a String.
¡Ufff!

Recibir entrada del usuario

Recuerde que incluimos la función de entrada/salida de la biblioteca estándar use


std::io;en la primera línea del programa. Ahora, llamaremos a la stdinfunción
desde el iomódulo, lo que nos permitirá gestionar la entrada del usuario:

io::stdin()
.read_line(&mut guess)

Si no hubiéramos importado la iobiblioteca use std::io;al principio del programa,


podríamos usar la función escribiendo esta llamada como std::io::stdin.
La stdinfunción devuelve una instancia de std::io::Stdin, que es un tipo que
representa un identificador de la entrada estándar de la terminal.

A continuación, la línea .read_line(&mut guess)llama al read_linemétodo en el


manejador de entrada estándar para obtener la información del usuario. También
pasamos &mut guesscomo argumento a read_linepara indicarle en qué cadena
almacenar la información del usuario. La función de read_linees tomar lo que el
usuario escribe en la entrada estándar y añadirlo a una cadena (sin sobrescribir
su contenido), por lo que pasamos esa cadena como argumento. El argumento de
cadena debe ser mutable para que el método pueda cambiar su contenido.

El &[nombre del argumento] indica que este argumento es una referencia , lo que
permite que varias partes del código accedan a un dato sin necesidad de copiarlo
en memoria varias veces. Las referencias son una función compleja, y una de las
principales ventajas de Rust es su seguridad y facilidad de uso. No necesitas
conocer muchos detalles para completar este programa. Por ahora, solo necesitas
saber que, al igual que las variables, las referencias son inmutables por defecto.
Por lo tanto, necesitas escribir &mut guessen lugar de &guesshacerlas mutables.
(El capítulo 4 explicará las referencias con más detalle).

Manejo de posibles fallos conResult

Seguimos trabajando en esta línea de código. Ahora estamos analizando una


tercera línea de texto, pero tenga en cuenta que sigue formando parte de una
sola línea lógica de código. La siguiente parte es este método:
Rust
jueves, 22 de mayo de 2025 : Página 27 de 719
.expect("Failed to read line");

Podríamos haber escrito este código así:

io::stdin().read_line(&mut guess).expect("Failed to read line");

Sin embargo, una línea larga es difícil de leer, por lo que es mejor dividirla. Suele
ser recomendable introducir un salto de línea y otros espacios en blanco para
dividir líneas largas al llamar a un método con la .method_name()sintaxis. Ahora
veamos qué hace esta línea.

Como se mencionó anteriormente, read_lineintroduce lo que el usuario introduce


en la cadena que le pasamos, pero también devuelve un Resultvalor. ResultEs
una enumeración , a menudo llamada enum , que es un tipo que puede estar en
uno de varios estados posibles. Cada estado posible se denomina variante .

El capítulo 6 abordará las enumeraciones con más detalle. El propósito de


estos Resulttipos es codificar la información de gestión de errores.

ResultLas variantes de son Oky Err. La Okvariante indica que la operación se


realizó correctamente y contiene el valor generado correctamente. La Errvariante
significa que la operación falló y contiene información sobre cómo o por qué falló.

Los valores de este Resulttipo, al igual que los de cualquier otro tipo, tienen
métodos definidos. Una instancia de Resulttiene un expectmétodo al que se
puede llamar. Si esta instancia de Resultes un Errvalor, expect provocará un fallo
del programa y mostrará el mensaje que se pasó como argumento a expect. Si
el read_linemétodo devuelve un Err, probablemente se deba a un error del
sistema operativo subyacente. Si esta instancia de Resultes
un Okvalor, expecttomará el valor de retorno que Okcontiene y le devolverá solo
ese valor para que pueda usarlo. En este caso, ese valor es el número de bytes en
la entrada del usuario.

Si no llamas a expect, el programa se compilará, pero recibirás una advertencia:

$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
Rust
jueves, 22 de mayo de 2025 : Página 28 de 719
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
10 | let _ = io::stdin().read_line(&mut guess);
| +++++++

warning: `guessing_game` (bin "guessing_game") generated 1 warning


Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s

Rust advierte que no has utilizado el Resultvalor devuelto de read_line, lo que


indica que el programa no ha manejado un posible error.

La forma correcta de suprimir la advertencia es escribir código de gestión de


errores, pero en nuestro caso solo queremos bloquear el programa cuando se
produzca un problema, así que podemos usar expect. Aprenderá sobre la
recuperación de errores en el Capítulo 9 .

Impresión de valores con println!marcadores de posición

Aparte de la llave de cierre, solo hay una línea más para discutir en el código
hasta ahora:

println!("You guessed: {}", guess);

Esta línea imprime la cadena que ahora contiene la entrada del usuario.
El {}conjunto de llaves es un marcador de posición: imagínense {}como
pequeñas pinzas de cangrejo que sujetan un valor. Al imprimir el valor de una
variable, el nombre de la variable puede ir dentro de las llaves. Al imprimir el
resultado de la evaluación de una expresión, coloque llaves vacías en la cadena
de formato y, a continuación, una lista de expresiones separadas por comas para
imprimir en cada marcador de posición de llave vacía, en el mismo orden.
Imprimir una variable y el resultado de una expresión en una sola llamada
a println!se vería así:

let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);

Este código se imprimirá x = 5 and y + 2 = 12.


Rust
jueves, 22 de mayo de 2025 : Página 29 de 719
Probando la primera parte

Probemos la primera parte del juego de adivinanzas. Ejecútalo usando cargo run:

$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

En este punto, la primera parte del juego está terminada: recibimos la


información del teclado y luego la imprimimos.

Generando un número secreto

A continuación, necesitamos generar un número secreto que el usuario intentará


adivinar. Este número secreto debe ser diferente cada vez para que el juego sea
divertido de jugar varias veces. Usaremos un número aleatorio entre 1 y 100 para
que el juego no sea demasiado difícil. Rust aún no incluye la funcionalidad de
números aleatorios en su biblioteca estándar. Sin embargo, el equipo de Rust
proporciona una randcaja con dicha funcionalidad.

Usar una caja para obtener más funcionalidad

Recuerda que un crate es una colección de archivos de código fuente de Rust. El


proyecto que hemos estado desarrollando es un crate binario , que es un
ejecutable. El rand crate es un crate de biblioteca , que contiene código diseñado
para usarse en otros programas y no puede ejecutarse por sí solo.

La coordinación de contenedores externos de Cargo es donde Cargo realmente


destaca. Antes de escribir código que use rand, debemos modificar
el archivo Cargo.tomlrand para incluir el contenedor como dependencia. Abra ese
archivo ahora y agregue la siguiente línea al final, debajo
del [dependencies]encabezado de sección que Cargo creó para usted. Asegúrese
de especificar randexactamente como lo hicimos aquí, con este número de
versión, o los ejemplos de código de este tutorial podrían no funcionar:

Nombre del archivo: Cargo.toml


Rust
jueves, 22 de mayo de 2025 : Página 30 de 719
[dependencies]
rand = "0.8.5"

En el archivo Cargo.toml , todo lo que sigue a un encabezado forma parte de esa


sección, que continúa hasta que comienza otra. En [nombre del
archivo], [dependencies]se le indica a Cargo de qué crates externos depende el
proyecto y qué versiones de esos crates se requieren. En este caso, se especifica
el randcrate con el especificador de versión semántica 0.8.5. Cargo entiende el
Versionado Semántico (a veces llamado SemVer ), que es un estándar para
escribir números de versión. El especificador 0.8.5es la abreviatura de [nombre
del archivo] ^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 API públicas compatibles con la
versión 0.8.5, y esta especificación garantiza que recibirá la última versión del
parche, que compilará con el código de este capítulo. No se garantiza que
ninguna versión 0.9.0 o posterior tenga la misma API que la que utilizan los
siguientes ejemplos.

Ahora, sin cambiar nada del código, construyamos el proyecto, como se muestra
en el Listado 2-2.

$ cargo build
Updating crates.io index
Locking 16 packages to latest compatible versions
Adding wasi v0.11.0+wasi-snapshot-preview1 (latest: v0.13.3+wasi-0.2.2)
Adding zerocopy v0.7.35 (latest: v0.8.9)
Adding zerocopy-derive v0.7.35 (latest: v0.8.9)
Downloaded syn v2.0.87
Downloaded 1 crate (278.1 KB) in 0.16s
Compiling proc-macro2 v1.0.89
Compiling unicode-ident v1.0.13
Compiling libc v0.2.161
Compiling cfg-if v1.0.0
Compiling byteorder v1.5.0
Compiling getrandom v0.2.15
Compiling rand_core v0.6.4
Compiling quote v1.0.37
Compiling syn v2.0.87
Compiling zerocopy-derive v0.7.35
Compiling zerocopy v0.7.35
Compiling ppv-lite86 v0.2.20
Compiling rand_chacha v0.3.1
Rust
jueves, 22 de mayo de 2025 : Página 31 de 719
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.69s
Listado 2-2: El resultado de la ejecución cargo builddespués de agregar la caja
rand como dependencia

Es posible que veas diferentes números de versión (¡pero todos serán


compatibles con el código, gracias a SemVer!) y diferentes líneas (dependiendo
del sistema operativo), y las líneas pueden estar en un orden diferente.

Al incluir una dependencia externa, Cargo obtiene las últimas versiones de todo lo
que necesita del registro , que es una copia de los datos de Crates.io . Crates.io es
donde los usuarios del ecosistema Rust publican sus proyectos de código abierto
para que otros los usen.

Tras actualizar el registro, Cargo revisa la [dependencies]sección y descarga los


crates que aún no se hayan descargado. En este caso, aunque solo se
incluyeron randcomo dependencia, Cargo también obtuvo otros crates
que randdependen de él para funcionar. Tras descargar los crates, Rust los
compila y luego compila el proyecto con las dependencias disponibles.

Si lo vuelves a ejecutar inmediatamente cargo buildsin realizar cambios, no


obtendrás ningún resultado aparte de la Finishedlínea. Cargo sabe que ya ha
descargado y compilado las dependencias, y que no has cambiado nada en ellas
en tu archivo Cargo.toml . Cargo también sabe que no has cambiado nada en tu
código, así que tampoco lo recompila. Sin nada que hacer, simplemente sale.

Si abre el archivo src/main.rs , realiza un cambio trivial y luego lo guarda y lo


compila nuevamente, solo verá dos líneas de salida:

$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s

Estas líneas muestran que Cargo solo actualiza la compilación con tu pequeño
cambio en el archivo src/main.rs . Tus dependencias no han cambiado, así que
Cargo sabe que puede reutilizar lo que ya ha descargado y compilado para ellas.

Cómo garantizar compilaciones reproducibles con el archivo Cargo.lock


Rust
jueves, 22 de mayo de 2025 : Página 32 de 719

Cargo cuenta con un mecanismo que garantiza que puedas reconstruir el mismo
artefacto cada vez que tú o cualquier otra persona compile tu código: Cargo usará
solo las versiones de las dependencias que especificaste hasta que indiques lo
contrario. Por ejemplo, supongamos que la semana que viene randsale la versión
0.8.6 del crate, que contiene una corrección importante de errores, pero también
una regresión que dañará tu código. Para solucionar esto, Rust crea el
archivo Cargo.lock la primera vez que ejecutas cargo build, por lo que ahora lo
tenemos en el juego de adivinanzas. .

Al compilar un proyecto por primera vez, Cargo determina todas las versiones de
las dependencias que cumplen los criterios y las escribe en el archivo Cargo.lock .
Al compilar su proyecto en el futuro, Cargo detectará la existencia del
archivo Cargo.lock y usará las versiones especificadas en él, en lugar de tener
que volver a calcularlas. Esto le permite tener una compilación reproducible
automáticamente. En otras palabras, su proyecto se mantendrá en la versión
0.8.5 hasta que actualice explícitamente, gracias al archivo Cargo.lock . Dado que
el archivo Cargo.lock es importante para compilaciones reproducibles, suele
incluirse en el control de código fuente junto con el resto del código del proyecto.

Cómo actualizar una caja para obtener una nueva versión

Cuando desee actualizar un crate, Cargo proporciona el comando update, que


ignorará el archivo Cargo.lock y buscará todas las versiones más recientes que se
ajusten a sus especificaciones en Cargo.toml . Cargo escribirá esas versiones en
el archivo Cargo.lock . En este caso, Cargo solo buscará versiones posteriores a la
0.8.5 y anteriores a la 0.9.0. Si el randcrate ha publicado las dos nuevas versiones
0.8.6 y 0.9.0, verá lo siguiente al ejecutar cargo update:

$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6

Cargo ignora la versión 0.9.0. En este punto, también notará un cambio en su


archivo Cargo.lock , indicando que la versión del randcrate que está usando es la
0.8.6. Para usar randla versión 0.9.0 o cualquier versión de la serie 0.9.x , deberá
actualizar el archivo Cargo.toml para que se vea así:

[dependencies]
rand = "0.9.0"
Rust
jueves, 22 de mayo de 2025 : Página 33 de 719

La próxima vez que ejecute cargo build, Cargo actualizará el registro de cajas
disponibles y reevaluará sus randrequisitos de acuerdo con la nueva versión que
haya especificado.

Hay mucho más que decir sobre Cargo y su ecosistema , que analizaremos en el
Capítulo 14, pero por ahora, eso es todo lo que necesitas saber. Cargo facilita la
reutilización de bibliotecas, lo que permite a los Rustaceans escribir proyectos
más pequeños ensamblados a partir de varios paquetes.

Generando un número aleatorio

Comencemos a usar randpara generar un número que podamos adivinar. El


siguiente paso es actualizar src/main.rs , como se muestra en el Listado 2-3.

Nombre de archivo: src/main.rs


use std::io;
use rand::Rng;

fn main() {
println!("Guess the number!");

let secret_number = rand::thread_rng().gen_range(1..=100);

println!("The secret number is: {secret_number}");

println!("Please input your guess.");

let mut guess = String::new();

io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");

println!("You guessed: {guess}");


}
Listado 2-3: Agregar código para generar un número aleatorio

Primero añadimos la línea use rand::Rng;. El Rngrasgo define los métodos que
implementan los generadores de números aleatorios, y este rasgo debe estar
dentro del alcance para que podamos usarlos. El capítulo 10 tratará los rasgos en
detalle.

A continuación, añadimos dos líneas en el centro. En la primera, llamamos a


la rand::thread_rngfunción que nos proporciona el generador de números
Rust
jueves, 22 de mayo de 2025 : Página 34 de 719

aleatorios que vamos a usar: uno local para el hilo de ejecución actual y generado
por el sistema operativo. Luego, llamamos al gen_range método del generador de
números aleatorios. Este método se define mediante la Rng propiedad que
incorporamos al alcance con la use rand::Rng;instrucción. El gen_rangemétodo
toma una expresión de rango como argumento y genera un número aleatorio
dentro del rango. El tipo de expresión de rango que usamos aquí tiene la
forma start..=endy es inclusivo en los límites inferior y superior, por lo que
debemos especificar 1..=100que se solicite un número entre 1 y 100.

Nota: No solo sabrás qué rasgos usar ni qué métodos y funciones llamar desde un
crate, ya que cada crate incluye documentación con instrucciones para su uso.
Otra característica interesante de Cargo es que al ejecutar el cargo doc --
opencomando, se compilará localmente la documentación proporcionada por
todas tus dependencias y se abrirá en tu navegador. Si te interesan otras
funciones del randcrate, por ejemplo, ejecuta cargo doc --openy haz clic randen la
barra lateral izquierda.

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 tan fácil si el programa imprime la respuesta nada más
iniciarse!

Intente ejecutar el programa varias veces:

$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
Rust
jueves, 22 de mayo de 2025 : Página 35 de 719

Deberías obtener diferentes números aleatorios y todos deberían ser números


entre 1 y 100. ¡Buen trabajo!

Comparando la suposición con el número secreto

Ahora que tenemos la entrada del usuario y un número aleatorio, podemos


compararlos. Este paso se muestra en el Listado 2-4. Tenga en cuenta que este
código aún no se compilará, como explicaremos.

Nombre de archivo: src/main.rs

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
// --snip--

println!("You guessed: {guess}");

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

Primero, añadimos otra usesentencia, incorporando un tipo


llamado std::cmp::Orderingdesde la biblioteca estándar al ámbito de aplicación.
El Orderingtipo es otra enumeración y tiene las variantes Less, Greatery Equal.
Estos son los tres resultados posibles al comparar dos valores.

Luego, añadimos cinco líneas nuevas al final que usan el Orderingtipo.


El cmpmétodo compara dos valores y se puede llamar con cualquier valor
comparable. Toma una referencia a lo que se desea comparar: en este caso, se
compara guesscon [nombre de la variable secret_number]. Luego, devuelve una
variante de la Orderingenumeración que incluimos en el ámbito con
la useinstrucción. Usamos una matchexpresión para decidir qué hacer a
continuación según la variante de [nombre de la variable] Orderingdevuelta al
Rust
jueves, 22 de mayo de 2025 : Página 36 de 719

llamar a [nombre de la variable] cmpcon los valores en [nombre de la


variable] guessy [nombre de la variable secret_number].

Una matchexpresión se compone de brazos . Un brazo consiste en un patrón con


el que comparar y el código que debe ejecutarse si el valor dado a match coincide
con el patrón de ese brazo. Rust toma el valor dado a matchy examina el patrón
de cada brazo uno por uno. Los patrones y la matchconstrucción son potentes
funciones de Rust: permiten expresar diversas situaciones que el código podría
encontrar y garantizan que se gestionen todas. Estas funciones se detallarán en
los capítulos 6 y 19, respectivamente.

Veamos un ejemplo con la matchexpresión que usamos aquí. Supongamos que el


usuario ha adivinado 50 y que el número secreto generado aleatoriamente esta
vez es 38.

Cuando el código compara 50 con 38, el cmpmétodo


retornará Ordering::Greaterporque 50 es mayor que 38. La matchexpresión
obtiene el Ordering::Greatervalor y comienza a verificar el patrón de cada brazo.
Examina el patrón del primer brazo, Ordering::Lessy ve que el
valor Ordering::Greaterno coincide Ordering::Less, por lo que ignora el código de
ese brazo y pasa al siguiente. El patrón del siguiente brazo es Ordering::Greater,
que sí coincide Ordering::Greater. El código asociado en ese brazo se ejecutará e
imprimirá Too big!en pantalla. La match expresión termina después de la primera
coincidencia, por lo que no examinará el último brazo en este caso.

Sin embargo, el código del Listado 2-4 aún no compila. Intentémoslo:

$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
| |
Rust
jueves, 22 de mayo de 2025 : Página 37 de 719
| arguments to this method are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: method defined here
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/
cmp.rs:838:8
|
838 | fn cmp(&self, other: &Self) -> Ordering;
| ^^^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1
previous error

El núcleo del error indica que hay tipos no coincidentes . Rust tiene un sistema de
tipos estático y robusto. Sin embargo, también tiene inferencia de tipos. Cuando
escribimos let mut guess = String::new(), Rust pudo inferir que guessdebería ser
a Stringy no nos obligó a escribir el tipo. El secret_number, por otro lado, es un
tipo numérico. Algunos tipos numéricos de Rust pueden tener un valor entre 1 y
100: i32, un número de 32 bits; u32, un número de 32 bits sin signo; i64, un
número de 64 bits; así como otros. A menos que se especifique lo contrario, Rust
usa de forma predeterminada an i32, que es el tipo de secret_numbera menos
que se añada información de tipo en otro lugar que haría que Rust infiriera un tipo
numérico diferente. El motivo del error es que Rust no puede comparar una
cadena y un tipo numérico.

Finalmente, queremos convertir las Stringlecturas del programa como entrada a


un tipo numérico para poder compararlo numéricamente con el número secreto.
Para ello, añadimos esta línea al maincuerpo de la función:

Nombre de archivo: src/main.rs

// --snip--

let mut guess = String::new();

io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");

let guess: u32 = guess.trim().parse().expect("Please type a number!");

println!("You guessed: {guess}");


Rust
jueves, 22 de mayo de 2025 : Página 38 de 719

match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}

La linea es:

let guess: u32 = guess.trim().parse().expect("Please type a number!");

Creamos una variable llamada guess. Pero, un momento, ¿el programa no tiene
ya una variable llamada guess? Sí, pero Rust nos permite replicar el valor anterior
de guesscon uno nuevo. El replicado nos permite reutilizar el guess nombre de la
variable en lugar de obligarnos a crear dos variables únicas,
como guess_stry guess, por ejemplo. Abordaremos esto con más detalle en el
Capítulo 3 , pero por ahora, recuerda que esta función se usa a menudo cuando
se desea convertir un valor de un tipo a otro.

Vinculamos esta nueva variable a la expresión guess.trim().parse(). El


[ guess nombre de la expresión] se refiere a la variable original guessque
contenía la entrada como cadena. El trimmétodo en una Stringinstancia eliminará
cualquier espacio en blanco al principio y al final, lo cual debemos hacer antes de
poder convertir la cadena en un [nombre de la variable u32], que solo puede
contener datos numéricos. El usuario debe presionar enter para completar la
operación read_linee ingresar su respuesta, lo que agrega un carácter de nueva
línea a la cadena. Por ejemplo, si el usuario escribe 5 y presiona enter , guessse
ve así: 5\n. El \n[nombre de la variable] representa "nueva línea". (En Windows,
presionar enter produce un retorno de carro y una nueva línea, \r\n.)
El trimmétodo elimina \no \r\n, lo que resulta en solo 5.

El parsemétodo en cadenas convierte una cadena a otro tipo. Aquí, lo usamos


para convertir de una cadena a un número. Necesitamos indicar a Rust el tipo de
número exacto que queremos usando let guess: u32. Los dos puntos ( :)
después guessindican a Rust que anotaremos el tipo de la variable. Rust tiene
varios tipos de número integrados; el que u32se ve 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úmero en el Capítulo 3 .
Rust
jueves, 22 de mayo de 2025 : Página 39 de 719

Además, la u32anotación en este programa de ejemplo y la comparación


con secret_numberlos medios de Rust inferirán que
también secret_numberdebería ser a u32. ¡Así que ahora la comparación será
entre dos valores del mismo tipo!

El parsemétodo solo funcionará con caracteres que se puedan convertir


lógicamente en números y, por lo tanto, puedan causar errores fácilmente. Si, por
ejemplo, la cadena contuviera A👍%, no habría forma de convertirla a un número.
Debido a que podría fallar, el parsemétodo devuelve un Resulttipo, de forma
similar a como read_line lo hace el método (discutido anteriormente en "Gestión
de posibles fallos con Result" ). Lo trataremos Resultde la misma manera usando
el expectmétodo de nuevo. Si parse devuelve una Err Resultvariante porque no se
pudo crear un número a partir de la cadena, la expectllamada bloqueará el juego
e imprimirá el mensaje que le enviamos. Si parsepuede convertir correctamente
la cadena a un número, devolverá la Okvariante de Resulty expectdevolverá el
número que buscamos del Okvalor.

Ejecutemos el programa ahora:

$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!

¡Genial! Aunque se añadieron espacios antes de la suposición, el programa


determinó que el usuario había adivinado 76. Ejecute el programa varias veces
para verificar el comportamiento con distintos tipos de entrada: acertar el
número, adivinar un número demasiado alto y adivinar un número demasiado
bajo.

Ya funciona la mayor parte del juego, pero el usuario solo puede adivinar una
cosa. ¡Vamos a cambiar eso añadiendo un bucle!

Permitir múltiples conjeturas mediante bucles


Rust
jueves, 22 de mayo de 2025 : Página 40 de 719

La looppalabra clave crea un bucle infinito. Añadiremos un bucle para que los
usuarios tengan más posibilidades de acertar el número:

Nombre de archivo: src/main.rs

// --snip--

println!("The secret number is: {secret_number}");

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 pueden ver, hemos movido todo, desde la solicitud de entrada de la


suposición, a un bucle. Asegúrense de sangrar las líneas dentro del bucle cuatro
espacios más cada una y ejecuten el programa de nuevo. El programa ahora
solicitará otra suposición indefinidamente, 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 menciona en la parsediscusión en "Comparación de la suposición con el
número secreto" : si el usuario introduce una respuesta que no sea un número, el
programa se bloqueará. Podemos aprovechar esto para permitir que el usuario
salga, como se muestra aquí:

$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Rust
jueves, 22 de mayo de 2025 : Página 41 de 719
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind:
InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Escribir quitcerrará el juego, pero, como notarás, también lo hará cualquier otra
entrada que no sea un número. Esto no es óptimo, como mínimo; queremos que
el juego también se detenga al adivinar el número correcto.

Renunciar después de una suposición correcta

Programemos el juego para que finalice cuando el usuario gane agregando


una breakdeclaración:

Nombre de archivo: src/main.rs

// --snip--

match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}

Añadir la breaklínea después You win!hace que el programa salga del bucle
cuando el usuario acierta el número secreto. Salir del bucle también implica salir
del programa, ya que el bucle es la última parte de main.

Manejo de entradas no válidas


Rust
jueves, 22 de mayo de 2025 : Página 42 de 719

Para refinar aún más el comportamiento del juego, en lugar de bloquear el


programa cuando el usuario introduce un valor distinto de un número, haremos
que el juego lo ignore para que el usuario pueda seguir intentando. Podemos
lograrlo modificando la línea donde guess se convierte de a Stringa a u32, como
se muestra en el Listado 2-5.

Nombre de archivo: src/main.rs


// --snip--

io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");

let guess: u32 = match guess.trim().parse() {


Ok(num) => num,
Err(_) => continue,
};

println!("You guessed: {guess}");

// --snip--
Listado 2-5: Ignorar una suposición que no es un número y solicitar otra
suposición en lugar de bloquear el programa

Cambiamos de una expectllamada a una matchexpresión para pasar de un fallo


ante un error a gestionarlo. Recuerda que parsedevuelve un Result tipo y Resultes
una enumeración con las variantes Oky Err. Aquí usamos una matchexpresión, al
igual que con el Orderingresultado del cmp método.

Si parselogra convertir la cadena en un número, devolverá un Okvalor que


contiene el número resultante. Ese Okvalor coincidirá con el patrón del primer
brazo, y la matchexpresión simplemente devolverá el numvalor parsegenerado y
colocado dentro del Okvalor. Ese número aparecerá exactamente donde lo
queremos en la nueva guessvariable que estamos creando.

Si noparse se puede convertir la cadena en un número, se devolverá un valor con


más información sobre el error. El valor no coincide con el patrón del
primer brazo, pero sí con el del segundo. El guion bajo, , es un valor general; en
este ejemplo, se busca la coincidencia de todos los valores, independientemente
de la información que contengan. Por lo tanto, el programa ejecutará el código del
segundo brazo, , que le indica que pase a la siguiente iteración de y solicite otra
Rust
jueves, 22 de mayo de 2025 : Página 43 de 719

suposición. De esta forma, el programa ignora todos los errores que pueda
encontrar.ErrErrOk(num)matchErr(_)_Errcontinueloopparse

Ahora todo en el programa debería funcionar como se espera. Probémoslo:

$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

¡Genial! Con un pequeño ajuste final, terminaremos el juego de adivinanzas.


Recuerda que el programa sigue imprimiendo el número secreto. Eso funcionó
bien para las pruebas, pero arruina el juego. Eliminemos el println!que muestra el
número secreto. El Listado 2-6 muestra el código final.

Nombre de archivo: src/main.rs


use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
println!("Guess the number!");

let secret_number = rand::thread_rng().gen_range(1..=100);

loop {
println!("Please input your guess.");

let mut guess = String::new();


Rust
jueves, 22 de mayo de 2025 : Página 44 de 719

io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");

let guess: u32 = match guess.trim().parse() {


Ok(num) => num,
Err(_) => continue,
};

println!("You guessed: {guess}");

match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Listado 2-6: Código completo del juego de adivinanzas

Llegados a este punto, has creado el juego de adivinanzas con éxito. ¡Felicidades!

Resumen

Este proyecto fue una forma práctica de presentarte muchos conceptos nuevos
de Rust: letfunciones match, el uso de crates externos y más. En los próximos
capítulos, aprenderás sobre estos conceptos con más detalle. El capítulo 3 abarca
conceptos comunes a la mayoría de los lenguajes de programación, como
variables, tipos de datos y funciones, y muestra cómo usarlos en Rust. El capítulo
4 explora la propiedad, una característica que diferencia a Rust de otros
lenguajes. El capítulo 5 trata sobre las estructuras y la sintaxis de los métodos, y
el capítulo 6 explica el funcionamiento de las enumeraciones.

Conceptos comunes de programación

Este capítulo abarca conceptos presentes en casi todos los lenguajes de


programación y su funcionamiento en Rust. Muchos lenguajes de programación
comparten una base común. Ninguno de los conceptos presentados en este
capítulo es exclusivo de Rust, pero los analizaremos en el contexto de Rust y
explicaremos las convenciones que rigen su uso.
Rust
jueves, 22 de mayo de 2025 : Página 45 de 719

En concreto, aprenderás sobre variables, tipos básicos, funciones, comentarios y


flujo de control. Estos fundamentos estarán presentes en todos los programas de
Rust, y aprenderlos pronto te proporcionará una base sólida para empezar.

Palabras clave

El lenguaje Rust cuenta con un conjunto de palabras clave reservadas para su uso
exclusivo, al igual que en otros lenguajes. Tenga en cuenta que no puede usar
estas palabras como nombres de variables o funciones. La mayoría de las
palabras clave tienen significados especiales y las usará para realizar diversas
tareas en sus programas Rust; algunas no tienen ninguna funcionalidad asociada
actualmente, pero se han reservado para funciones que podrían añadirse a Rust
en el futuro. Puede encontrar una lista de las palabras clave en el Apéndice A.

Variables y mutabilidad

Como se mencionó en la sección "Almacenamiento de valores con variables" , las


variables son inmutables por defecto. Este es uno de los muchos consejos que
Rust te ofrece para escribir tu código aprovechando la seguridad y la
simultaneidad que ofrece. Sin embargo, aún tienes la opción de hacer que tus
variables sean mutables. Exploremos cómo y por qué Rust te anima a priorizar la
inmutabilidad y por qué a veces podrías optar por no hacerlo.

Cuando una variable es inmutable, una vez que un valor se asocia a un nombre,
no se puede cambiar. Para ilustrar esto, genere un nuevo proyecto
llamado variables en el directorio de proyectos usando cargo new variables.

Luego, en el nuevo directorio de variables , abra src/main.rs y reemplace su


código con el siguiente código, que aún no se compilará:

Nombre de archivo: src/main.rs

fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Rust
jueves, 22 de mayo de 2025 : Página 46 de 719

Guarde y ejecute el programa con cargo run. Debería recibir un mensaje de error
sobre un error de inmutabilidad, como se muestra en este resultado:

$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2| let x = 5;
| - first assignment to `x`
3| println!("The value of x is: {x}");
4| x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2| let mut x = 5;
| +++

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error

Este ejemplo muestra cómo el compilador te ayuda a encontrar errores en tus


programas. Los errores de compilación pueden ser frustrantes, pero en realidad
solo significan que tu programa aún no está haciendo lo que quieres;
¡no significan que no seas un buen programador! Los Rustaceans con experiencia
aún tienen errores de compilación.

Recibió el mensaje de error cannot assign twice to immutable variable `x`porque


intentó asignar un segundo valor a la xvariable inmutable.

Es importante que se produzcan errores de compilación al intentar cambiar un


valor designado como inmutable, ya que esta misma situación puede generar
errores. Si una parte de nuestro código asume que un valor nunca cambiará y
otra lo cambia, es posible que la primera parte no cumpla su función. La causa de
este tipo de error puede ser difícil de rastrear posteriormente, especialmente
cuando la segunda parte solo cambia el valor en ocasiones . El compilador de
Rust garantiza que, al indicar que un valor no cambiará, realmente no cambiará,
por lo que no es necesario realizar un seguimiento. De esta forma, el código es
más fácil de entender.
Rust
jueves, 22 de mayo de 2025 : Página 47 de 719

Pero la mutabilidad puede ser muy útil y facilita la escritura del código. Aunque
las variables son inmutables por defecto, puedes hacerlas mutables
añadiendo mutdelante del nombre de la variable, como hiciste en el Capítulo
2. Añadir muttambién transmite la intención a los futuros lectores del código,
indicando que otras partes del código cambiarán el valor de esta variable.

Por ejemplo, cambiemos src/main.rs a lo siguiente:

Nombre de archivo: src/main.rs

fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}

Cuando ejecutamos el programa ahora, obtenemos esto:

$ 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

Podemos cambiar el valor vinculado a xde 5a 6cuando mutse usa. En última


instancia, la decisión de usar mutabilidad o no depende de usted y de lo que
considere más claro en esa situación particular.

Constantes

Al igual que las variables inmutables, las constantes son valores que están
vinculados a un nombre y no pueden cambiar, pero existen algunas diferencias
entre constantes y variables.

En primer lugar, no se permite usar mutcon constantes. Las constantes no solo


son inmutables por defecto, sino que siempre lo son. Se declaran constantes
usando la constpalabra clave en lugar de la letpalabra clave, y el tipo del
valor debe anotarse. Abordaremos los tipos y las anotaciones de tipo en la
siguiente sección, "Tipos de datos" , así que no te preocupes por los detalles
ahora. Simplemente recuerda que siempre debes anotar el tipo.
Rust
jueves, 22 de mayo de 2025 : Página 48 de 719

Las constantes se pueden declarar en cualquier ámbito, incluido el 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 puede calcular en
tiempo de ejecución.

He aquí un ejemplo de una declaración constante:

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

El nombre de la constante es THREE_HOURS_IN_SECONDSy su valor se establece


como el resultado de multiplicar 60 (el número de segundos en un minuto) por 60
(el número de minutos en una hora) por 3 (el número de horas que queremos
contar en este programa). La convención de nombres de Rust para las constantes
es usar mayúsculas con guiones bajos entre las palabras. El compilador puede
evaluar un conjunto limitado de operaciones en tiempo de compilación, lo que nos
permite escribir este valor de forma más fácil de entender y verificar, en lugar de
establecer esta constante en el valor 10,800. Consulta la sección sobre
evaluación de constantes de la Referencia de Rust para obtener más información
sobre las operaciones que se pueden utilizar al declarar constantes.

Las constantes son válidas durante toda la ejecución de un programa, dentro del
ámbito en el que se declararon. Esta propiedad las hace útiles para valores del
dominio de la aplicación que varias partes del programa podrían necesitar
conocer, como el número máximo de puntos que puede obtener un jugador o la
velocidad de la luz.

Nombrar como constantes los valores codificados utilizados en el programa es útil


para transmitir el significado de dicho valor a futuros desarrolladores del código.
También ayuda a tener solo una parte del código que se deba cambiar si el valor
codificado necesita actualizarse en el futuro.

Sombreado

Como viste en el tutorial del juego de adivinanzas del Capítulo 2 , puedes declarar
una nueva variable con el mismo nombre que una variable anterior. Los rustáceos
dicen que la primera variable es eclipsada por la segunda, lo que significa que
esta es lo que el compilador verá al usar su nombre. En efecto, la segunda
variable eclipsa a la primera, apropiándose de cualquier uso del nombre de la
Rust
jueves, 22 de mayo de 2025 : Página 49 de 719

variable hasta que se eclipse o finalice su alcance. Podemos eclipsar una variable
usando el mismo nombre de la variable y repitiendo el uso de la letpalabra clave
de la siguiente manera:

Nombre de archivo: src/main.rs

fn main() {
let x = 5;

let x = x + 1;

{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}

println!("The value of x is: {x}");


}

Este programa primero enlaza xa un valor de 5. Luego, crea una nueva


variable xrepitiendo let x =, tomando el valor original y sumándolo, 1de modo que
el valor de xsea entonces 6. Luego, dentro de un ámbito interno creado con
llaves, la tercera letinstrucción también sombrea xy crea una nueva variable,
multiplicando el valor anterior por , 2para obtener xun valor de 12. Al finalizar ese
ámbito, el sombreado interno finaliza y xvuelve a ser 6. Al ejecutar este
programa, se generará la siguiente salida:

$ 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 sombreado es diferente a marcar una variable como, mutya que obtendremos


un error en tiempo de compilación si intentamos reasignarla accidentalmente sin
usar la letpalabra clave. Al usar `<nombre de la variable` let, podemos realizar
algunas transformaciones en un valor, pero la variable permanece inmutable una
vez completadas.

La otra diferencia entre mutel sombreado y el sombreado es que, dado que


creamos una nueva variable al usar la letpalabra clave de nuevo, podemos
cambiar el tipo del valor pero reutilizar el mismo nombre. Por ejemplo,
Rust
jueves, 22 de mayo de 2025 : Página 50 de 719

supongamos que nuestro programa solicita al usuario que indique cuántos


espacios desea entre un texto introduciendo caracteres de espacio, y luego
queremos almacenar esa entrada como un número:

let spaces = " ";


let spaces = spaces.len();

La primera spacesvariable es de tipo cadena y la segunda spacesde tipo número.


El sombreado nos ahorra tener que crear nombres diferentes,
como spaces_stry spaces_num; en su lugar, podemos reutilizar el spacesnombre
más simple. Sin embargo, si intentamos usar mutpara esto, como se muestra
aquí, obtendremos un error de compilación:

let mut spaces = " ";


spaces = spaces.len();

El error dice que no se nos permite mutar el tipo de una variable:

$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2| let mut spaces = " ";
| ----- expected due to this value
3| spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error

Ahora que hemos explorado cómo funcionan las variables, veamos más tipos de
datos que pueden tener.

Tipos de datos

Cada valor en Rust pertenece a un tipo de dato específico , lo que le indica a Rust
qué tipo de datos se especifican para que sepa cómo trabajar con ellos.
Analizaremos dos subconjuntos de tipos de datos: escalares y compuestos.

Tenga en cuenta que Rust es un lenguaje de tipado estático , lo que significa que
debe conocer los tipos de todas las variables en tiempo de compilación. El
Rust
jueves, 22 de mayo de 2025 : Página 51 de 719

compilador generalmente puede inferir el tipo que queremos usar basándose en


el valor y cómo lo usamos. En los casos en que son posibles varios tipos, como al
convertir "a" Stringa un tipo numérico usando parsela sección "Comparación de la
suposición con el número secreto" del Capítulo 2, debemos agregar una anotación
de tipo, como esta:

let guess: u32 = "42".parse().expect("Not a number!");

Si no agregamos la : u32anotación de tipo que se muestra en el código anterior,


Rust mostrará el siguiente error, lo que significa que el compilador necesita más
información nuestra para saber qué tipo queremos usar:

$ 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

Verá diferentes tipos de anotaciones para otros tipos de datos.

Tipos escalares

Un tipo escalar representa un único valor. Rust tiene cuatro tipos escalares
principales: enteros, números de punto flotante, booleanos y caracteres. Quizás
los reconozcas de otros lenguajes de programación. Veamos cómo funcionan en
Rust.

Tipos enteros

Un entero es un número sin componente fraccionario. En el Capítulo 2, usamos un


tipo entero: el u32tipo. Esta declaración de tipo indica que el valor al que está
Rust
jueves, 22 de mayo de 2025 : Página 52 de 719

asociado debe ser un entero sin signo (los tipos enteros con signo empiezan
por ien lugar de u) que ocupa 32 bits. La Tabla 3-1 muestra los tipos enteros
integrados en Rust. Podemos usar cualquiera de estas variantes para declarar el
tipo de un valor entero.

Tabla 3-1: Tipos de enteros en Rust

Longit Firmad No
ud o firmado

8 bits i8 u8

16 bits i16 u16

32 bits i32 u32

64 bits i64 u64

128 bits i128 u128

arco isize usize

Cada variante puede tener o no signo y un tamaño específico. Con signo y sin
signo se refiere a si el número puede ser negativo; es decir, si necesita un signo
(con signo) o si solo será positivo y, por lo tanto, puede representarse sin signo
(sin signo). Es como escribir números en papel: cuando el signo importa, se
muestra con un signo más o menos; sin embargo, cuando se asume que es
positivo, se muestra sin signo. Los números con signo se almacenan mediante la
representación en complemento a dos .

Cada variante con signo puede almacenar números de -(2 n - 1 ) a 2 n - 1 - 1


inclusive, donde n es el número de bits que utiliza. Por lo tanto, un
[número] i8puede almacenar números de -(2 7 ) a 2 7 - 1, lo que equivale a -128 a
127. Las variantes sin signo pueden almacenar números de 0 a 2 n - 1, por lo que
un [ número] u8puede almacenar números de 0 a 2 8 - 1, lo que equivale a 0 a
255.

Además, los tipos isizey usizedependen de la arquitectura de la computadora en


que se ejecuta el programa, que se denota en la tabla como "arch": 64 bits si está
en una arquitectura de 64 bits y 32 bits si está en una arquitectura de 32 bits.
Rust
jueves, 22 de mayo de 2025 : Página 53 de 719

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 de varios
tipos numéricos permiten un sufijo de tipo, como 57u8, para designar el tipo. Los
literales numéricos también pueden usarse _como separador visual para facilitar
la lectura del número, como 1_000, que tendrá el mismo valor que si se hubiera
especificado 1000.

Tabla 3-2: Literales enteros en Rust

Literales
Ejemplo
numéricos

Decimal 98_222

Maleficio 0xff

Octal 0o77

0b1111_00
Binario
00

Byte
b'A'
( u8solamente)

¿Cómo saber qué tipo de entero usar? Si no estás seguro, los valores
predeterminados de Rust suelen ser un buen punto de partida: los tipos enteros
tienen como valor predeterminado i32. La situación principal en la que
usarías isize`or` usizees al indexar algún tipo de colección.

Desbordamiento de enteros

Supongamos que tienes una variable de tipo u8que admite valores entre 0 y 255.
Si intentas cambiar la variable a un valor fuera de ese rango, como 256, se
producirá un desbordamiento de enteros , lo que puede provocar uno de dos
comportamientos. Al compilar en modo de depuración, Rust incluye
comprobaciones de desbordamiento de enteros que provocan que tu programa
entre en pánico en tiempo de ejecución si ocurre este comportamiento. Rust
utiliza el término "pánico" cuando un programa finaliza con un error; analizaremos
los pánicos con más detalle en la sección "Errores irrecuperables con panic!" del
capítulo 9.
Rust
jueves, 22 de mayo de 2025 : Página 54 de 719

Al compilar en modo de lanzamiento con la --releasebandera, Rust no incluye


comprobaciones de desbordamiento de enteros que causan pánicos. En cambio,
si ocurre un desbordamiento, Rust realiza un ajuste en complemento a dos . En
resumen, los valores mayores que el valor máximo que el tipo puede contener se
ajustan al mínimo de los valores que el tipo puede contener. En el caso de a u8, el
valor 256 se convierte en 0, el valor 257 en 1, y así sucesivamente. El programa
no entrará en pánico, pero la variable tendrá un valor que probablemente no sea
el esperado. Confiar en el comportamiento de ajuste del desbordamiento de
enteros se considera un error.

Para gestionar explícitamente la posibilidad de desbordamiento, puede utilizar


estas familias de métodos proporcionadas por la biblioteca estándar para tipos
numéricos primitivos:

 Envuelva en todos los modos con los wrapping_*métodos,


como wrapping_add.
 Devuelve el Nonevalor si hay desbordamiento con los checked_*métodos.
 Devuelve el valor y un valor booleano que indica si hubo desbordamiento
con los overflowing_*métodos.
 Saturar en los valores mínimo o máximo del valor con
los saturating_* métodos.

Tipos de punto flotante

Rust también tiene dos tipos primitivos para números de punto flotante , que son
números con coma decimal. Los tipos de punto flotante de Rust son f32y f64, que
tienen un tamaño de 32 y 64 bits, respectivamente. El tipo predeterminado
es f64 porque, en las CPU modernas, tiene una velocidad similar a la de , f32pero
mayor precisión. Todos los tipos de punto flotante tienen signo.

A continuación se muestra un ejemplo que muestra números de punto flotante en


acción:

Nombre de archivo: src/main.rs

fn main() {
let x = 2.0; // f64

let y: f32 = 3.0; // f32


}
Rust
jueves, 22 de mayo de 2025 : Página 55 de 719

Los números de punto flotante se representan según el estándar IEEE-754.

Operaciones numéricas

Rust admite las operaciones matemáticas básicas esperadas para todos los tipos
de números: suma, resta, multiplicación, división y resto. La división entera trunca
hacia cero hasta el entero más cercano. El siguiente código muestra cómo usar
cada operación numérica en una letsentencia:

Nombre de archivo: src/main.rs

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;
}

Cada expresión en estas sentencias utiliza un operador matemático y se evalúa


como un único valor, que posteriormente se vincula a una variable. El Apéndice
B contiene una lista de todos los operadores que Rust proporciona.

El tipo booleano

Como en la mayoría de los lenguajes de programación, un tipo booleano en Rust


tiene dos valores posibles: truey false. Los valores booleanos tienen un tamaño de
un byte. El tipo booleano en Rust se especifica mediante bool. Por ejemplo:

Nombre de archivo: src/main.rs

fn main() {
let t = true;
Rust
jueves, 22 de mayo de 2025 : Página 56 de 719
let f: bool = false; // with explicit type annotation
}

La principal forma de usar valores booleanos es mediante condicionales, como


una if expresión. Explicaremos cómo iffuncionan las expresiones en Rust en la
sección "Flujo de control" .

El tipo de personaje

El tipo de Rust chares el tipo alfabético más primitivo del lenguaje. Aquí hay
algunos ejemplos de declaración charde valores:

Nombre de archivo: src/main.rs

fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}

Tenga en cuenta que especificamos charlos literales con comillas simples, a


diferencia de los literales de cadena, que usan comillas dobles. charEl tipo 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 solo ASCII. Las letras acentuadas,
los caracteres chinos, japoneses y coreanos, los emojis y los espacios de ancho
cero son charvalores válidos en Rust. Los valores escalares Unicode van
desde U+0000hasta U+D7FFhasta U+E000inclusive U+10FFFF. Sin embargo, un
"carácter" no es realmente un concepto en Unicode, por lo que su intuición sobre
lo que es un "carácter" puede no coincidir con lo que charsignifica "a" en Rust.
Analizaremos este tema en detalle en "Almacenamiento de texto codificado en
UTF-8 con cadenas" en el capítulo 8.

Tipos de compuestos

Los tipos compuestos pueden agrupar varios valores en un solo tipo. Rust tiene
dos tipos compuestos primitivos: tuplas y arrays.

El tipo tupla

Una tupla es una forma general de agrupar varios valores con diversos tipos en
un tipo compuesto. Las tuplas tienen una longitud fija: una vez declaradas, su
tamaño no puede aumentar ni disminuir.
Rust
jueves, 22 de mayo de 2025 : Página 57 de 719

Creamos una tupla escribiendo una lista de valores separados por comas entre
paréntesis. Cada posición en la tupla tiene un tipo, y los tipos de los diferentes
valores no tienen por qué ser iguales. En este ejemplo, hemos añadido
anotaciones de tipo opcionales:

Nombre de archivo: src/main.rs

fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

La variable tupse vincula a toda la tupla, ya que se considera un único elemento


compuesto. Para extraer los valores individuales de una tupla, podemos usar la
coincidencia de patrones para desestructurar el valor de la tupla, como se
muestra a continuación:

Nombre de archivo: src/main.rs

fn main() {
let tup = (500, 6.4, 1);

let (x, y, z) = tup;

println!("The value of y is: {y}");


}

Este programa primero crea una tupla y la vincula a la variable tup. Luego, usa un
patrón con letpara tomarla tupy convertirla en tres variables
independientes: x, yy z. Esto se denomina desestructuración porque divide la
tupla en tres partes. Finalmente, el programa imprime el valor de y, que es 6.4.

También podemos acceder directamente a un elemento de tupla usando un punto


( .) seguido del índice del valor al que queremos acceder. Por ejemplo:

Nombre de archivo: src/main.rs

fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;

let six_point_four = x.1;


Rust
jueves, 22 de mayo de 2025 : Página 58 de 719
let one = x.2;
}

Este programa crea la tupla xy accede a cada elemento mediante sus respectivos
índices. Como en la mayoría de los lenguajes de programación, el primer índice
de una tupla es 0.

La tupla sin valores tiene un nombre especial, unidad . Este valor y su tipo
correspondiente se escriben ()y representan un valor vacío o un tipo de retorno
vacío. Las expresiones devuelven implícitamente el valor de la unidad si no
devuelven ningún otro valor.

El tipo de matriz

Otra forma de tener una colección de múltiples valores es con un array . A


diferencia de una tupla, todos los elementos de un array deben ser del mismo
tipo. A diferencia de los arrays en otros lenguajes, los arrays en Rust tienen una
longitud fija.

Escribimos los valores en una matriz como una lista separada por comas dentro
de corchetes:

Nombre de archivo: src/main.rs

fn main() {
let a = [1, 2, 3, 4, 5];
}

Los arrays son útiles cuando se desea que los datos se asignen en la pila, al igual
que los otros tipos que hemos visto hasta ahora, en lugar del montón (hablaremos
más sobre la pila y el montón en el Capítulo 4 ) o cuando se desea asegurar un
número fijo de elementos. Sin embargo, un array no es tan flexible como el tipo
vector. Un vector es un tipo de colección similar proporcionado por la biblioteca
estándar, cuyo tamaño puede aumentar o disminuir. Si no está seguro de si usar
un array o un vector, probablemente deba usar un vector. El Capítulo 8 trata los
vectores con más detalle.

Sin embargo, los arrays son más útiles cuando se sabe que el número de
elementos no tendrá que cambiar. Por ejemplo, si se usaran los nombres de los
meses en un programa, probablemente se usaría un array en lugar de un vector,
ya que se sabe que siempre contendrá 12 elementos:
Rust
jueves, 22 de mayo de 2025 : Página 59 de 719
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];

El tipo de una matriz se escribe utilizando corchetes con el tipo de cada elemento,
un punto y coma y luego el número de elementos en la matriz, de la siguiente
manera:

let a: [i32; 5] = [1, 2, 3, 4, 5];

Aquí i32se muestra el tipo de cada elemento. Después del punto y coma, el
número 5 indica que la matriz contiene cinco elementos.

También puede inicializar una matriz para que contenga el mismo valor para cada
elemento especificando el valor inicial, seguido de un punto y coma y luego la
longitud de la matriz entre corchetes, como se muestra aquí:

let a = [3; 5];

La matriz nombrada acontendrá 5elementos que se establecerán 3inicialmente


con el valor. Esto es similar a escribir, let a = [3, 3, 3, 3, 3];pero de forma más
concisa.

Acceso a elementos de matriz

Un array es un fragmento de memoria de tamaño fijo y conocido que se puede


asignar en la pila. Se puede acceder a los elementos de un array mediante
indexación, como se muestra a continuación:

Nombre de archivo: src/main.rs

fn main() {
let a = [1, 2, 3, 4, 5];

let first = a[0];


let second = a[1];
}

En este ejemplo, la variable nombrada firstobtendrá el valor 1porque ese es el


valor en el índice [0]del array. La variable nombrada secondobtendrá el valor 2del
índice [1]del array.
Rust
jueves, 22 de mayo de 2025 : Página 60 de 719
Acceso no válido a elementos de matriz

Veamos qué sucede si intentas acceder a un elemento de un array que está más
allá del final del array. Supongamos que ejecutas este código, similar al juego de
adivinanzas del Capítulo 2, para obtener el índice del array del usuario:

Nombre de archivo: src/main.rs

use std::io;

fn main() {
let a = [1, 2, 3, 4, 5];

println!("Please enter an array index.");

let mut index = String::new();

io::stdin()
.read_line(&mut index)
.expect("Failed to read line");

let index: usize = index


.trim()
.parse()
.expect("Index entered was not a number");

let element = a[index];

println!("The value of the element at index {index} is: {element}");


}

Este código se compila correctamente. Si lo ejecuta usando cargo rune


introduce 0, 1, 2, 3o 4, el programa imprimirá el valor correspondiente en ese
índice del array. Si, en cambio, introduce un número después del final del array,
como 10, obtendrá un resultado como este:

thread 'main' panicked at src/main.rs:19:19:


index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

El programa generó un error en tiempo de ejecución al usar un valor no válido en


la indexación. El programa finalizó con un mensaje de error y no ejecutó
la println!sentencia final. Al intentar acceder a un elemento mediante indexación,
Rust
jueves, 22 de mayo de 2025 : Página 61 de 719

Rust comprobará que el índice especificado sea menor que la longitud del array.
Si el índice es mayor o igual a la longitud, Rust generará un error. Esta
comprobación debe realizarse en tiempo de ejecución, especialmente en este
caso, ya que el compilador no puede saber qué valor introducirá el usuario al
ejecutar el código posteriormente.

Este es un ejemplo de los principios de seguridad de memoria de Rust en acción.


En muchos lenguajes de bajo nivel, este tipo de comprobación no se realiza, y al
proporcionar un índice incorrecto, se puede acceder a memoria no válida. Rust
protege contra este tipo de error saliendo inmediatamente en lugar de permitir el
acceso a la memoria y continuar. El capítulo 9 analiza con más detalle la gestión
de errores de Rust y cómo escribir código legible y seguro que no genere pánico
ni permita accesos no válidos a memoria.

Funciones

Las funciones son comunes en el código de Rust. Ya has visto una de las
funciones más importantes del lenguaje: la mainfunción, que es el punto de
entrada de muchos programas. También has visto la fnpalabra clave, que permite
declarar nuevas funciones.

El código de Rust utiliza el formato Snake Case como estilo convencional para los
nombres de funciones y variables, donde todas las letras están en minúscula y se
subrayan las palabras por separado. A continuación, se muestra un programa con
un ejemplo de definición de función:

Nombre de archivo: src/main.rs

fn main() {
println!("Hello, world!");

another_function();
}

fn another_function() {
println!("Another function.");
}

En Rust, definimos una función introduciendo `enter` fnseguido de su nombre y


un par de paréntesis. Las llaves indican al compilador dónde empieza y termina el
cuerpo de la función.
Rust
jueves, 22 de mayo de 2025 : Página 62 de 719

Podemos llamar a cualquier función que hayamos definido ingresando su nombre


seguido de un par de paréntesis. Dado que another_functionestá definida en el
programa, se puede llamar desde dentro de la mainfunción. Tenga en cuenta que
la definimos another_function después de la mainfunción en el código fuente;
también podríamos haberla definido antes. A Rust no le importa dónde se definan
las funciones, solo que estén definidas en un ámbito visible para quien las llama.

Comencemos un nuevo proyecto binario llamado funciones para explorar las


funciones con más detalle. Coloque el another_functionejemplo en src/main.rs y
ejecútelo. Debería ver el siguiente resultado:

$ 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.

Las líneas se ejecutan en el orden en que aparecen en la mainfunción. Primero se


imprime el mensaje "¡Hola mundo!" y, a continuación, another_functionse llama a
la función y se imprime su mensaje.

Parámetros

Podemos definir funciones con parámetros , que son variables especiales que
forman parte de la firma de una función. Cuando una función tiene parámetros, se
le pueden asignar valores concretos. Técnicamente, estos valores concretos se
denominan argumentos , pero en la conversación informal, se suelen usar los
términos parámetro y argumento indistintamente para referirse tanto a las
variables en la definición de una función como a los valores concretos que se
pasan al llamar a una función.

En esta versión another_functionagregamos un parámetro:

Nombre de archivo: src/main.rs

fn main() {
another_function(5);
}

fn another_function(x: i32) {
println!("The value of x is: {x}");
Rust
jueves, 22 de mayo de 2025 : Página 63 de 719
}

Intente ejecutar este programa; debería obtener el siguiente resultado:

$ 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

La declaración de another_functiontiene un parámetro llamado x. El tipo de xse


especifica como i32. Al pasar 5a another_function, la println!macro indica 5dónde
se encontraba el par de llaves que contiene xen la cadena de formato.

En las firmas de funciones, se debe declarar el tipo de cada parámetro. Esta es


una decisión deliberada del diseño de Rust: exigir anotaciones de tipo en las
definiciones de funciones significa que el compilador casi nunca necesita usarlas
en otra parte del código para determinar el tipo. El compilador también puede
generar mensajes de error más útiles si conoce los tipos que espera la función.

Al definir varios parámetros, separe las declaraciones de parámetros con comas,


de la siguiente manera:

Nombre de archivo: src/main.rs

fn main() {
print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {


println!("The measurement is: {value}{unit_label}");
}

Este ejemplo crea una función print_labeled_measurementcon dos parámetros. El


primer parámetro es `name` valuey es un ` i32`. El segundo es
`name` unit_labely es de tipo ` char`. La función imprime texto que contiene
`` valuey `` unit_label.

Intentemos ejecutar este código. Reemplace el programa que se encuentra


actualmente en el archivo src/main.rs de su proyecto de funciones con el ejemplo
anterior y ejecútelo usando :cargo run

$ cargo run
Rust
jueves, 22 de mayo de 2025 : Página 64 de 719
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h

Debido a que llamamos a la función con 5como valor para valuey 'h'como valor
para unit_label, la salida del programa contiene esos valores.

Declaraciones y expresiones

Los cuerpos de las funciones se componen de una serie de sentencias que,


opcionalmente, terminan en una expresión. Hasta ahora, las funciones que hemos
visto no han incluido una expresión final, pero sí han visto una expresión como
parte de una sentencia. Dado que Rust es un lenguaje basado en expresiones, es
importante comprender esta distinción. 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.

 Las declaraciones son instrucciones que realizan alguna acción y no


devuelven un valor.
 Las expresiones se evalúan como un valor resultante. Veamos algunos
ejemplos.

De hecho, ya hemos usado sentencias y expresiones. Crear una variable y


asignarle un valor con la letpalabra clave es una sentencia. En el Listado 3-1, let y
= 6;es una sentencia.

Nombre de archivo: src/main.rs


fn main() {
let y = 6;
}
Listado 3-1: Una maindeclaración de función que contiene una instrucción

Las definiciones de funciones también son declaraciones; el ejemplo anterior es


en sí mismo una declaración. (Como veremos más adelante, llamar a una función
no es una declaración).

Las sentencias no devuelven valores. Por lo tanto, no se puede asignar


una letsentencia a otra variable, como intenta el siguiente código; se obtendrá un
error:
Rust
jueves, 22 de mayo de 2025 : Página 65 de 719

Nombre de archivo: src/main.rs

fn main() {
let x = (let y = 6);
}

Cuando ejecutes este programa, el error que obtendrás será similar al siguiente:

$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2| let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions

warning: unnecessary parentheses around assigned value


--> src/main.rs:2:13
|
2| let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2- let x = (let y = 6);
2+ let x = let y = 6;
|

warning: `functions` (bin "functions") generated 1 warning


error: could not compile `functions` (bin "functions") due to 1 previous error; 1
warning emitted

La let y = 6instrucción no devuelve un valor, por lo que no hay nada a lo


que xenlazar. Esto difiere de lo que ocurre en otros lenguajes, como C y Ruby,
donde la asignación devuelve el valor de la asignación. En estos lenguajes, se
puede escribir x = y = 6y tener ambos xy ytener el valor 6; esto no ocurre en
Rust.

Las expresiones se evalúan como un valor y constituyen la mayor parte del


código que escribirás en Rust. Considera una operación matemática, como 5 + 6,
Rust
jueves, 22 de mayo de 2025 : Página 66 de 719

que es una expresión que se evalúa como el valor 11. Las expresiones pueden
formar parte de sentencias: en el Listado 3-1, el 6en la sentencia let y = 6;es una
expresión que se evalúa como el valor 6. Llamar a una función es una expresión.
Llamar a una macro es una expresión. Un nuevo bloque de ámbito creado entre
llaves es una expresión, por ejemplo:

Nombre de archivo: src/main.rs

fn main() {
let y = {
let x = 3;
x+1
};

println!("The value of y is: {y}");


}

Esta expresión:

{
let x = 3;
x+1
}

Es un bloque que, en este caso, evalúa como 4. Ese valor se vincula a y como
parte de la letsentencia. Tenga en cuenta que la x + 1línea no tiene punto y coma
al final, a diferencia de la mayoría de las líneas que ha visto hasta ahora. Las
expresiones no incluyen punto y coma al final. Si añade un punto y coma al final
de una expresión, la convierte en una sentencia y, por lo tanto, no devolverá
ningún valor. Tenga esto en cuenta al explorar los valores de retorno de funciones
y expresiones a continuación.

Funciones con valores de retorno

Las funciones pueden devolver valores al código que las invoca. No se nombran
los valores de retorno, pero se debe declarar su tipo después de una flecha ( ->).
En Rust, el valor de retorno de la función es sinónimo del valor de la expresión
final en el bloque del cuerpo de la función. Se puede regresar antes de una
función usando la returnpalabra clave y especificando un valor, pero la mayoría
de las funciones devuelven la última expresión implícitamente. A continuación, se
muestra un ejemplo de una función que devuelve un valor:
Rust
jueves, 22 de mayo de 2025 : Página 67 de 719

Nombre de archivo: src/main.rs

fn five() -> i32 {


5
}

fn main() {
let x = five();

println!("The value of x is: {x}");


}

No hay llamadas a funciones, macros ni letsentencias en la five función, solo el


número 5. Es una función perfectamente válida en Rust. Tenga en cuenta que el
tipo de retorno de la función también se especifica como -> i32. Intente ejecutar
este código; el resultado debería ser similar a esto:

$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5

El 5in fivees el valor de retorno de la función, por lo que el tipo de retorno es i32.
Analicemos esto con más detalle. Hay dos aspectos importantes: primero, la
línea let x = five();muestra que estamos usando el valor de retorno de una
función para inicializar una variable. Dado que la función fivedevuelve un 5, esa
línea es igual a la siguiente:

let x = 5;

En segundo lugar, la fivefunción no tiene parámetros y define el tipo de valor de


retorno, pero el cuerpo de la función es solitario 5sin punto y coma porque es una
expresión cuyo valor queremos devolver.

Veamos otro ejemplo:

Nombre de archivo: src/main.rs

fn main() {
let x = plus_one(5);

println!("The value of x is: {x}");


}
Rust
jueves, 22 de mayo de 2025 : Página 68 de 719

fn plus_one(x: i32) -> i32 {


x+1
}

Al ejecutar este código, se imprimirá The value of x is: 6. Pero si colocamos un


punto y coma al final de la línea que contiene x + 1, convirtiéndola de una
expresión a una sentencia, obtendremos un error:

Nombre de archivo: src/main.rs

fn main() {
let x = plus_one(5);

println!("The value of x is: {x}");


}

fn plus_one(x: i32) -> i32 {


x + 1;
}

Al compilar este código se produce un error como el siguiente:

$ 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 de


este código. La definición de la función plus_oneindica que devolverá un i32, pero
las sentencias no evalúan un valor, que se expresa mediante (), el tipo de unidad.
Por lo tanto, no se devuelve nada, lo que contradice la definición de la función y
genera un error. En esta salida, Rust proporciona un mensaje para posiblemente
Rust
jueves, 22 de mayo de 2025 : Página 69 de 719

ayudar a corregir este problema: sugiere eliminar el punto y coma, lo que


solucionaría el error.

Comentarios

Todos los programadores se esfuerzan por que su código sea fácil de entender,
pero a veces se justifica una explicación adicional. En estos casos, los
programadores dejan comentarios en su código fuente que el compilador
ignorará, pero que quienes lo lean pueden encontrar útiles.

Aquí hay un comentario simple:

// hello, world

En Rust, el estilo de comentario idiomático comienza con dos barras diagonales y


continúa hasta el final de la línea. Para comentarios que se extienden más allá de
una sola línea, deberá incluir //en cada línea, como se muestra a continuación:

// So we’re doing something complicated here, long enough that we need


// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.

Los comentarios también se pueden colocar al final de las líneas que contienen
código:

Nombre de archivo: src/main.rs

fn main() {
let lucky_number = 7; // I’m feeling lucky today
}

Pero con más frecuencia los verás utilizados en este formato, con el comentario
en una línea separada encima del código que está anotando:

Nombre de archivo: src/main.rs

fn main() {
// I’m feeling lucky today
let lucky_number = 7;
}
Rust
jueves, 22 de mayo de 2025 : Página 70 de 719

Rust también tiene otro tipo de comentario, los comentarios de documentación,


que discutiremos en la sección “Publicación de un cajón en Crates.io” del Capítulo
14.

Flujo de control

La capacidad de ejecutar código dependiendo de si se cumple una condición truey


de ejecutarlo repetidamente mientras se cumple una condición trueson
componentes básicos en la mayoría de los lenguajes de programación. Las
construcciones más comunes que permiten controlar el flujo de ejecución del
código de Rust son iflas expresiones y los bucles.

ifExpresiones

Una ifexpresión permite ramificar el código según condiciones. Se proporciona


una condición y se indica: «Si se cumple esta condición, se ejecuta este bloque de
código. Si no se cumple, no se ejecuta este bloque de código».

Cree un nuevo proyecto llamado ramas en su directorio de proyectos para


explorar la ifexpresión. En el archivo src/main.rs , introduzca lo siguiente:

Nombre de archivo: src/main.rs

fn main() {
let number = 3;

if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}

Todas iflas expresiones comienzan con la palabra clave if, seguida de una
condición. En este caso, la condición comprueba si la variable numbertiene un
valor menor que 5. Colocamos el bloque de código que se ejecutará si la
condición aparece trueinmediatamente después de la condición entre llaves. Los
bloques de código asociados con las condiciones en iflas expresiones a veces se
denominan brazos , al igual que los brazos en matchlas expresiones que
analizamos en la sección "Comparación de la suposición con el número
secreto" del Capítulo 2.
Rust
jueves, 22 de mayo de 2025 : Página 71 de 719

Opcionalmente, también podemos incluir una elseexpresión, como hicimos aquí,


para proporcionar al programa un bloque de código alternativo que se ejecutará si
la condición se evalúa como false. Si no se proporciona una elseexpresión y la
condición es false, el programa simplemente omitirá el ifbloque y avanzará al
siguiente fragmento de código.

Intente ejecutar este código; debería ver el siguiente resultado:

$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true

Intentemos cambiar el valor de numbera un valor que cumpla la


condición falsepara ver qué sucede:

let number = 7;

Ejecute el programa nuevamente y observe el resultado:

$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was false

También cabe destacar que la condición en este código debe ser un bool. Si no lo
es bool, se generará un error. Por ejemplo, intente ejecutar el siguiente código:

Nombre de archivo: src/main.rs

fn main() {
let number = 3;

if number {
println!("number was three");
}
}

La ifcondición se evalúa como un valor de 3este tiempo y Rust genera un error:


Rust
jueves, 22 de mayo de 2025 : Página 72 de 719
$ 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 valor, boolpero obtuvo un entero. A


diferencia de lenguajes como Ruby y JavaScript, Rust no intentará convertir
automáticamente tipos no booleanos a booleanos. Debe ser explícito y
proporcionar siempre ifun valor booleano como condición. Si queremos que
el ifbloque de código se ejecute solo cuando un número no sea igual a 0, por
ejemplo, podemos cambiar la if expresión a la siguiente:

Nombre de archivo: src/main.rs

fn main() {
let number = 3;

if number != 0 {
println!("number was something other than zero");
}
}

Al ejecutar este código se imprimirá number was something other than zero.

Manejo de múltiples condiciones conelse if

Puedes usar varias condiciones combinando " ify" elseen una else if expresión. Por
ejemplo:

Nombre de archivo: src/main.rs

fn main() {
let number = 6;

if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
Rust
jueves, 22 de mayo de 2025 : Página 73 de 719
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}

Este programa tiene cuatro rutas posibles. Tras ejecutarlo, debería ver el
siguiente resultado:

$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3

Al ejecutarse, este programa verifica cada ifexpresión por turno y ejecuta el


primer cuerpo cuya condición se evalúa como true. Tenga en cuenta que, aunque
6 es divisible entre 2, no vemos la salida number is divisible by 2ni el number is
not divisible by 4, 3, or 2texto del else bloque. Esto se debe a que Rust solo
ejecuta el bloque para la primera true condición y, una vez que la encuentra, ni
siquiera verifica las demás.

Usar demasiadas else ifexpresiones puede saturar el código, así que si tienes más
de una, quizás quieras refactorizarlo. El capítulo 6 describe una potente
construcción de ramificación de Rust necesaria matchpara estos casos.

Uso ifen una letdeclaración

Como ifes una expresión, podemos usarla en el lado derecho de


una let declaración para asignar el resultado a una variable, como en el Listado 3-
2.

Nombre de archivo: src/main.rs


fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };

println!("The value of number is: {number}");


}
Listado 3-2: Asignación del resultado de una ifexpresión a una variable

La numbervariable se vinculará a un valor según el resultado de la if expresión.


Ejecute este código para ver qué sucede:
Rust
jueves, 22 de mayo de 2025 : Página 74 de 719
$ 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 hasta su última expresión, y los
números por sí mismos también son expresiones. En este caso, el valor de toda
la ifexpresión depende del bloque de código que se ejecute. Esto significa que los
valores que potencialmente podrían ser resultados de cada brazo de [nombre del
grupo] ifdeben ser del mismo tipo; en el Listado 3-2, los resultados de
ambos ifbrazos else eran i32enteros. Si los tipos no coinciden, como en el
siguiente ejemplo, se generará un error:

Nombre de archivo: src/main.rs

fn main() {
let condition = true;

let number = if condition { 5 } else { "six" };

println!("The value of number is: {number}");


}

Al intentar compilar este código, obtendremos un error. Los tipos de valor ifde
los elsebrazos y son incompatibles, y Rust indica exactamente dónde encontrar el
problema en el programa:

$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:44
|
4| let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 75 de 719

La expresión en el ifbloque se evalúa como un entero y la expresión en


el elsebloque se evalúa como una cadena. Esto no funcionará porque las variables
deben tener un solo tipo, y Rust necesita saber en tiempo de compilación de qué
tipo numberes la variable, definitivamente. Conocer el tipo de numberpermite al
compilador verificar que el tipo sea válido en cualquier lugar donde
usemos number. Rust no podría hacerlo si el tipo de numbersolo se determinara
en tiempo de ejecución; el compilador sería más complejo y ofrecería menos
garantías sobre el código si tuviera que registrar múltiples tipos hipotéticos para
cada variable.

Repetición con bucles

Suele ser útil ejecutar un bloque de código más de una vez. Para ello, Rust ofrece
varios bucles que recorren el código dentro del cuerpo del bucle hasta el final y
luego vuelven a empezar inmediatamente desde el principio. Para experimentar
con bucles, creemos un nuevo proyecto llamado loops .

Rust tiene tres tipos de bucles: loop, while, y for. Probemos cada uno.

Código repetitivo conloop

La looppalabra clave le dice a Rust que ejecute un bloque de código una y otra
vez para siempre o hasta que le indiques explícitamente que se detenga.

A modo de ejemplo, cambie el archivo src/main.rs en su directorio de bucles para


que se vea así:

Nombre de archivo: src/main.rs

fn main() {
loop {
println!("again!");
}
}

Al ejecutar este programa, se imprimirá again!una y otra vez hasta que lo


detengamos manualmente. La mayoría de las terminales admiten el atajo de
teclado ctrl " c para interrumpir un programa atascado en un bucle continuo".
Pruébelo:

$ cargo run
Rust
jueves, 22 de mayo de 2025 : Página 76 de 719
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

El símbolo ^Crepresenta dónde presionó ctrl - c . Es posible que vea o no la


palabra again!impresa después de ^C, dependiendo de dónde se encontraba el
código en el bucle cuando recibió la señal de interrupción.

Afortunadamente, Rust también ofrece una forma de salir de un bucle mediante


código. Puedes colocar la breakpalabra clave dentro del bucle para indicar al
programa cuándo detener su ejecución. Recuerda que hicimos esto en el juego de
adivinanzas de la sección "Salir tras un acierto" del Capítulo 2 para salir del
programa cuando el usuario ganó el juego al acertar el número.

También lo usamos continueen el juego de adivinanzas, que en un bucle le dice al


programa que salte cualquier código restante en esta iteración del bucle y pase a
la siguiente iteración.

Devolución de valores desde bucles

Uno de los usos de a loopes reintentar una operación que sabes que podría fallar,
como comprobar si un hilo ha completado su trabajo. También podrías necesitar
pasar el resultado de esa operación fuera del bucle al resto del código. Para ello,
puedes añadir el valor que quieres que se devuelva después de la breakexpresión
que usas para detener el bucle; ese valor se devolverá fuera del bucle para que
puedas usarlo, como se muestra aquí:

fn main() {
let mut counter = 0;

let result = loop {


counter += 1;

if counter == 10 {
break counter * 2;
}
};
Rust
jueves, 22 de mayo de 2025 : Página 77 de 719
println!("The result is {result}");
}

Antes del bucle, declaramos una variable llamada countery la inicializamos


como 0. Luego, declaramos una variable llamada resultpara almacenar el valor
devuelto por el bucle. En cada iteración del bucle, añadimos 1a la countervariable
y comprobamos si counteres igual a 10. Si lo es, usamos la breakpalabra clave
con el valor counter * 2. Después del bucle, usamos un punto y coma para
finalizar la instrucción que asigna el valor a result. Finalmente, imprimimos el
valor en result, que en este caso es 20.

También se puede ejecutar returndesde dentro de un bucle. Si bien breaksolo sale


del bucle actual, returnsiempre sale de la función actual.

Etiquetas de bucle para desambiguar entre múltiples bucles

Si tiene bucles dentro de bucles breaky continuese aplica al bucle más interno en
ese punto, puede especificar opcionalmente una etiqueta de bucle en un bucle
que luego puede usar con breako continuepara especificar que esas palabras
clave se apliquen al bucle etiquetado en lugar del bucle más interno. Las
etiquetas de bucle deben comenzar con una comilla simple. A continuación, se
muestra un ejemplo con dos bucles anidados:

fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;

loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}

count += 1;
}
println!("End count = {count}");
}
Rust
jueves, 22 de mayo de 2025 : Página 78 de 719

El bucle externo tiene la etiqueta 'counting_upy contará de 0 a 2. El bucle interno


sin etiqueta contará de 10 a 9. El primero breakque no especifique una etiqueta
saldrá solo del bucle interno. La break 'counting_up;instrucción saldrá del bucle
externo. Este código imprime:

$ 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

Bucles condicionales conwhile

Un programa a menudo necesitará evaluar una condición dentro de un bucle.


Mientras la condición sea true, el bucle se ejecuta. Cuando la condición deja de
ser true, el programa llama a break, deteniéndolo. Es posible implementar este
comportamiento mediante una combinación de loop, if, elsey break; puedes
probarlo ahora en un programa si lo deseas. Sin embargo, este patrón es tan
común que Rust tiene una construcción de lenguaje integrada para ello,
llamada whilebucle. En el Listado 3-3, usamos whilepara repetir el programa tres
veces, contando regresivamente cada vez, y luego, después del bucle, imprimir
un mensaje y salir.

Nombre de archivo: src/main.rs


fn main() {
let mut number = 3;

while number != 0 {
println!("{number}!");

number -= 1;
}

println!("LIFTOFF!!!");
}
Rust
jueves, 22 de mayo de 2025 : Página 79 de 719
Listado 3-3: Uso de un whilebucle para ejecutar código mientras una condición es
verdadera

Esta construcción elimina gran parte de la anidación que sería necesaria si se


usaran loop, if, else, y break, y es más clara. Si una condición se evalúa
como true, el código se ejecuta; de lo contrario, sale del bucle.

Recorriendo una colección confor

También puede usar la whileconstrucción para recorrer los elementos de una


colección, como un array. Por ejemplo, el bucle del Listado 3-4 imprime cada
elemento del array a.

Nombre de archivo: src/main.rs


fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;

while index < 5 {


println!("the value is: {}", a[index]);

index += 1;
}
}
Listado 3-4: Recorrer cada elemento de una colección usando un whilebucle

Aquí, el código cuenta los elementos del array. Comienza en el índice 0y luego se
repite hasta llegar al último índice del array (es decir, cuando ya index < 5no
es true). Al ejecutar este código, se imprimirán todos los elementos del array:

$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

Los cinco valores de la matriz aparecen en la terminal, como se esperaba.


Aunque index alcanzará un valor de 5en algún momento, el bucle deja de
ejecutarse antes de intentar obtener un sexto valor de la matriz.
Rust
jueves, 22 de mayo de 2025 : Página 80 de 719

Sin embargo, este enfoque es propenso a errores; podríamos provocar que el


programa entre en pánico si el valor del índice o la condición de prueba son
incorrectos. Por ejemplo, si se cambia la definición del aarray a cuatro elementos,
pero se olvida actualizar la condición a while index < 4, el código entrará en
pánico. Además, es lento, ya que el compilador añade código en tiempo de
ejecución para realizar la comprobación condicional de si el índice está dentro de
los límites del array en cada iteración del bucle.

Como alternativa más concisa, puede usar un forbucle y ejecutar código para
cada elemento de una colección. Un forbucle se parece al código del Listado 3-5.

Nombre de archivo: src/main.rs


fn main() {
let a = [10, 20, 30, 40, 50];

for element in a {
println!("the value is: {element}");
}
}
Listado 3-5: Recorrer cada elemento de una colección usando un forbucle

Al ejecutar este código, veremos el mismo resultado que en el Listado 3-4. Más
importante aún, hemos aumentado la seguridad del código y eliminado la
posibilidad de errores que podrían surgir al sobrepasar el final del array o al no ir
lo suficientemente lejos y omitir algunos elementos.

Al utilizar el forbucle, no necesitaría recordar cambiar ningún otro código si


cambiara la cantidad de valores en la matriz, como lo haría con el método
utilizado en el Listado 3-4.

La seguridad y concisión de forlos bucles los convierten en la construcción de


bucles más utilizada en Rust. Incluso en situaciones en las que se desea ejecutar
código un número determinado de veces, como en el ejemplo de cuenta regresiva
que usó un whilebucle en el Listado 3-3, la mayoría de los rustáceos lo usarían for.
La forma de hacerlo sería usar un bucle Range, proporcionado por la biblioteca
estándar, que genera todos los números en secuencia, comenzando con un
número y terminando antes de otro.

Así es como se vería la cuenta regresiva usando un forbucle y otro método del
que aún no hemos hablado, rev, para invertir el rango:
Rust
jueves, 22 de mayo de 2025 : Página 81 de 719

Nombre de archivo: src/main.rs

fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}

Este código es un poco mejor, ¿no?

Resumen

¡Lo lograste! Este capítulo fue extenso: aprendiste sobre variables, tipos de datos
escalares y compuestos, funciones, comentarios, ifexpresiones y bucles. Para
practicar los conceptos de este capítulo, intenta crear programas que hagan lo
siguiente:

 Convierte temperaturas entre Fahrenheit y Celsius.


 Generar el n -ésimo número de Fibonacci.
 Imprime la letra del villancico “Los doce días de Navidad”, aprovechando la
repetición de la canción.

Cuando esté listo para continuar, hablaremos sobre un concepto en Rust que
comúnmente no existe en otros lenguajes de programación: propiedad.

Entendiendo la propiedad

La propiedad es la característica más exclusiva de Rust y tiene profundas


implicaciones para el resto del lenguaje. Permite a Rust garantizar la seguridad de
la memoria sin necesidad de un recolector de basura, por lo que es importante
comprender su funcionamiento. En este capítulo, hablaremos sobre la propiedad,
así como sobre varias características relacionadas: préstamos, porciones y cómo
Rust distribuye los datos en memoria.

¿Qué es la propiedad?

La propiedad es un conjunto de reglas que rigen la gestión de la memoria en un


programa Rust. Todos los programas deben gestionar el uso de la memoria del
ordenador durante su ejecución. Algunos lenguajes utilizan un sistema de
recolección de basura que busca periódicamente memoria en desuso durante la
ejecución del programa; en otros, el programador debe asignar y liberar la
Rust
jueves, 22 de mayo de 2025 : Página 82 de 719

memoria explícitamente. Rust utiliza un tercer enfoque: la memoria se gestiona


mediante un sistema de propiedad con un conjunto de reglas que el compilador
verifica. Si se infringe alguna de estas reglas, el programa no compilará. Ninguna
de las características de la propiedad ralentizará el programa durante su
ejecución.

Dado que la propiedad es un concepto nuevo para muchos programadores, lleva


tiempo acostumbrarse. La buena noticia es que, cuanto más experiencia tengas
con Rust y las reglas del sistema de propiedad, más fácil te resultará desarrollar
código de forma natural, segura y eficiente. ¡Sigue así!

Cuando comprendas la propiedad, tendrás una base sólida para comprender las
características que hacen único a Rust. En este capítulo, aprenderás sobre la
propiedad mediante ejemplos centrados en una estructura de datos muy común:
las cadenas.

La pila y el montón

Muchos lenguajes de programación no requieren que pienses en la pila y el


montón con frecuencia. Pero en un lenguaje de programación de sistemas como
Rust, la ubicación de un valor en la pila o en el montón afecta el comportamiento
del lenguaje y la necesidad de tomar ciertas decisiones. Más adelante en este
capítulo se describirán aspectos de la propiedad en relación con la pila y el
montón, por lo que aquí tienes una breve explicación.

Tanto la pila como el montón son partes de la memoria disponibles para que tu
código las use en tiempo de ejecución, pero están estructuradas de diferentes
maneras. La pila almacena los valores en el orden en que los obtiene y los elimina
en el orden opuesto. Esto se conoce como último en entrar, primero en salir .
Piensa en una pila de platos: cuando agregas más platos, los pones en la parte
superior de la pila y, cuando necesitas un plato, tomas uno de la parte superior.
¡Agregar o quitar platos del medio o de la parte inferior no funcionaría tan bien!
Agregar datos se llama empujar hacia la pila , y quitar datos se llama sacar de la
pila . Todos los datos almacenados en la pila deben tener un tamaño conocido y
fijo. Los datos con un tamaño desconocido en tiempo de compilación o un tamaño
que pueda cambiar se deben almacenar en el montón.

El montón está menos organizado: cuando pones datos en el montón, solicitas


una cierta cantidad de espacio. El asignador de memoria encuentra un lugar vacío
Rust
jueves, 22 de mayo de 2025 : Página 83 de 719

en el montón 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 asignación en el montón y a veces se abrevia simplemente
como asignación (empujar valores a la pila no se considera asignación). Debido a
que el puntero al montón es un tamaño fijo conocido, puedes almacenar el
puntero en la pila, pero cuando quieres los datos reales, debes seguir el puntero.
Imagina estar sentado en un restaurante. Al entrar, dices el número de personas
en tu grupo y el anfitrión encuentra una mesa vacía donde caben todos y te guía
allí. Si alguien en tu grupo llega tarde, puede preguntar dónde te has sentado
para encontrarte.

Subir datos a la pila es más rápido que asignarlos en el montón, ya que el


asignador nunca tiene que buscar un lugar para almacenar nuevos datos; esa
ubicación siempre está en la parte superior de la pila. En comparación, asignar
espacio en el montón requiere más trabajo, ya que el asignador primero debe
encontrar un espacio lo suficientemente grande para albergar los datos y luego
realizar la contabilidad para preparar la siguiente asignación.

Acceder a los datos en el montón es más lento que a los de la pila, ya que se
debe seguir un puntero para llegar a ellos. Los procesadores actuales son más
rápidos si saltan menos en la memoria. Siguiendo con la analogía, considere a un
camarero de un restaurante que toma pedidos de varias mesas. Es más eficiente
recibir todos los pedidos de una mesa antes de pasar a la siguiente. Tomar un
pedido de la mesa A, luego uno de la mesa B, luego otro de la A y luego otro de la
B sería un proceso mucho más lento. Del mismo modo, un procesador puede
funcionar mejor si trabaja con datos cercanos a otros (como en la pila) en lugar de
más alejados (como en el montón).

Cuando tu código llama a una función, los valores pasados a la función


(incluyendo, potencialmente, punteros a datos en el montón) y las variables
locales de la función se apilan en la pila. Al finalizar la función, esos valores se
extraen de la pila.

Monitorear qué partes del código usan qué datos en el montón, minimizar la
cantidad de datos duplicados y limpiar los datos no utilizados para no quedarse
sin espacio son problemas que la propiedad aborda. Una vez que comprenda la
propiedad, no necesitará pensar mucho en la pila ni en el montón, pero saber que
el propósito principal de la propiedad es administrar los datos del montón puede
ayudar a explicar por qué funciona como lo hace.
Rust
jueves, 22 de mayo de 2025 : Página 84 de 719
Reglas de propiedad

Primero, veamos las reglas de propiedad. Tenlas presentes al analizar los


ejemplos que las ilustran:

 Cada valor en Rust tiene un propietario .


 Sólo puede haber un propietario a la vez.
 Cuando el propietario quede fuera del alcance, el valor se eliminará.

Alcance variable

Ahora que ya hemos superado la sintaxis básica de Rust, no incluiremos todo el fn


main() { código en los ejemplos, así que si nos sigues, asegúrate de incluir los
siguientes ejemplos dentro de una mainfunción manualmente. Como resultado,
nuestros ejemplos serán un poco más concisos, lo que nos permitirá centrarnos
en los detalles en lugar de en el código repetitivo.

Como primer ejemplo de propiedad, analizaremos el alcance de algunas variables.


Un alcance es el rango dentro de un programa en el que un elemento es válido.
Consideremos la siguiente variable:

let s = "hello";

La variable sse refiere a un literal de cadena, cuyo valor está codificado en el


texto de nuestro programa. La variable es válida desde su declaración hasta el
final del ámbito actual . El Listado 4-1 muestra un programa con comentarios que
indican dónde ssería válida la variable.

{ // s is not valid here, it’s not yet declared


let s = "hello"; // s is valid from this point forward

// do stuff with s
} // this scope is now over, and s is no longer valid
Listado 4-1: Una variable y el ámbito en el que es válida

En otras palabras, hay dos puntos importantes en el tiempo aquí:

 Cuando sentra en el ámbito de aplicación, es válido.


 Sigue siendo válido hasta que salga de su ámbito de aplicación.
Rust
jueves, 22 de mayo de 2025 : Página 85 de 719

En este punto, la relación entre los ámbitos y la validez de las variables es similar
a la de otros lenguajes de programación. A partir de este conocimiento,
introduciremos el Stringtipo.

El Stringtipo

Para ilustrar las reglas de propiedad, necesitamos un tipo de dato más complejo
que los que vimos en la sección "Tipos de Datos" del Capítulo 3. Los tipos
mencionados anteriormente tienen un tamaño conocido, pueden almacenarse en
la pila y extraerse de ella al final de su alcance, y pueden copiarse rápida y
fácilmente para crear una nueva instancia independiente si otra parte del código
necesita usar el mismo valor en un alcance diferente. Sin embargo, queremos
analizar los datos almacenados en el montón y explorar cómo Rust sabe cuándo
limpiarlos, y este Stringtipo es un excelente ejemplo.

Nos centraremos en las partes Stringrelacionadas con la propiedad. Estos


aspectos también se aplican a otros tipos de datos complejos, ya sean
proporcionados por la biblioteca estándar o creados por el usuario. Lo
analizaremos Stringcon más detalle en el Capítulo 8 .

Ya hemos visto los literales de cadena, donde un valor de cadena se codifica


directamente en nuestro programa. Los literales de cadena son prácticos, pero no
son adecuados para todas las situaciones en las que queramos usar texto. Una
razón es que son inmutables. Otra es que no todos los valores de cadena se
pueden conocer al escribir nuestro código: por ejemplo, ¿qué pasa si queremos
tomar la entrada del usuario y almacenarla? Para estas situaciones, Rust ofrece
un segundo tipo de cadena, String. Este tipo gestiona los datos asignados en el
montón y, por lo tanto, puede almacenar una cantidad de texto desconocida en
tiempo de compilación. Puedes crear un Stringa partir de un literal de cadena
usando la fromfunción, de la siguiente manera:

let s = String::from("hello");

::El operador de dos puntos nos permite crear un espacio de nombres para
esta from función en particular bajo el Stringtipo, en lugar de usar un nombre
como string_from. Analizaremos esta sintaxis con más detalle en la
sección "Sintaxis de métodos" del capítulo 5, y cuando tratemos el espacio de
nombres con módulos en "Rutas para hacer referencia a un elemento en el árbol
de módulos" del capítulo 7.
Rust
jueves, 22 de mayo de 2025 : Página 86 de 719

Este tipo de cadena se puede mutar:

let mut s = String::from("hello");

s.push_str(", world!"); // push_str() appends a literal to a String

println!("{s}"); // This will print `hello, world!`

Entonces, ¿cuál es la diferencia? ¿Por qué se pueden Stringmutar los literales,


pero no? La diferencia radica en cómo estos dos tipos gestionan la memoria.

Memoria y asignación

En el caso de un literal de cadena, conocemos su contenido en tiempo de


compilación, por lo que el texto se codifica directamente en el ejecutable final.
Por eso los literales de cadena son rápidos y eficientes. Sin embargo, estas
propiedades solo se deben a su inmutabilidad. Desafortunadamente, no podemos
añadir un bloque de memoria al binario por cada fragmento de texto cuyo tamaño
se desconoce en tiempo de compilación y cuyo tamaño podría cambiar durante la
ejecución del programa.

Con el Stringtipo, para admitir un fragmento de texto mutable y ampliable,


necesitamos asignar una cantidad de memoria en el montón, desconocida en
tiempo de compilación, para almacenar el contenido. Esto significa:

 La memoria debe solicitarse al asignador de memoria en tiempo de


ejecución.
 Necesitamos una forma de devolver esta memoria al asignador cuando
terminemos con nuestro String.

Nosotros realizamos la primera parte: al llamar a String::from, su implementación


solicita la memoria que necesita. Esto es prácticamente universal en los lenguajes
de programación.

Sin embargo, la segunda parte es diferente. En lenguajes con un recolector de


basura (GC) , este rastrea y limpia la memoria que ya no se usa, sin que
tengamos que preocuparnos por ello. En la mayoría de los lenguajes sin GC, es
nuestra responsabilidad identificar cuándo la memoria ya no se usa y ejecutar
código para liberarla explícitamente, tal como lo hacíamos para solicitarla. Hacer
esto correctamente ha sido históricamente un problema de programación
complejo. Si lo olvidamos, desperdiciaremos memoria. Si lo hacemos demasiado
Rust
jueves, 22 de mayo de 2025 : Página 87 de 719

pronto, tendremos una variable no válida. Si lo hacemos dos veces, también es un


error. Necesitamos emparejar exactamente uno allocatecon exactamente
uno free.

Rust toma un camino diferente: la memoria se devuelve automáticamente cuando


la variable que la contiene sale del ámbito. Aquí hay una versión de nuestro
ejemplo de ámbito del Listado 4-1 usando un Stringliteral en lugar de uno de
cadena:

{
let s = String::from("hello"); // s is valid from this point forward

// do stuff with s
} // this scope is now over, and s is no
// longer valid

Existe un punto natural en el que podemos devolver la memoria


que Stringnecesitamos al asignador: cuando [ sse sale del ámbito]. Cuando una
variable [se sale del ámbito], Rust llama a una función especial. Esta función se
llama [se sale del ámbito] dropy es donde el autor de [ se sale del
ámbito] Stringpuede colocar el código para devolver la memoria. Rust
llama dropautomáticamente al cerrar la llave.

Nota: En C++, este patrón de desasignación de recursos al final de la vida útil de


un elemento se denomina a veces Adquisición de Recursos en Inicialización
(RAII) . Esta dropfunción en Rust le resultará familiar si ha utilizado patrones RAII.

Este patrón tiene un profundo impacto en la forma en que se escribe el código en


Rust. Puede parecer simple ahora, pero el comportamiento del código puede ser
inesperado en situaciones más complejas cuando queremos que múltiples
variables usen los datos asignados en el montón. Exploremos algunas de estas
situaciones.

Variables y datos que interactúan con Move

En Rust, varias variables pueden interactuar con los mismos datos de diferentes
maneras. Veamos un ejemplo con un entero en el Listado 4-2.

let x = 5;
let y = x;
Listado 4-2: Asignación del valor entero de la variable xay
Rust
jueves, 22 de mayo de 2025 : Página 88 de 719

Probablemente podamos adivinar qué hace esto: "vincular el valor 5a x; luego


hacer una copia del valor en xy vincularla a y". Ahora tenemos dos
variables, x y y, y ambas son iguales a 5. Esto es precisamente lo que ocurre,
porque los enteros son valores simples con un tamaño fijo conocido, y estos
dos 5valores se almacenan en la pila.

Ahora veamos la Stringversión:

let s1 = String::from("hello");
let s2 = s1;

Esto parece muy similar, por lo que podríamos suponer que funciona de la misma
manera: es decir, la segunda línea copiaría el valor en s1y lo vincularía a s2. Pero
esto no es exactamente lo que ocurre.

Observe la Figura 4-1 para ver qué sucede en Stringel subconjunto. A Stringse
compone de tres partes, que se muestran a la izquierda: un puntero a la memoria
que contiene el contenido de la cadena, una longitud y una capacidad. Este grupo
de datos se almacena en la pila. A la derecha se encuentra la memoria en el
montón que contiene el contenido.

Figura 4-1: Representación en memoria de un String contenedor que


contiene el valor "hello"ligado as1

La longitud indica la cantidad de memoria, en bytes, que el contenido del


archivo Stringestá utilizando actualmente. La capacidad indica la cantidad total de
memoria, en bytes, que el archivo Stringha recibido del asignador. La diferencia
entre longitud y capacidad es importante, pero no en este contexto, por lo que,
por ahora, se puede ignorar la capacidad.

Al asignar s1a s2, se copian los Stringdatos, es decir, se copia el puntero, la


longitud y la capacidad de la pila. No se copian los datos del montón al que hace
referencia el puntero. En otras palabras, la representación de los datos en
memoria se muestra en la Figura 4-2.
Rust
jueves, 22 de mayo de 2025 : Página 89 de 719

Figura 4-2: Representación en memoria de la variable s2 que tiene copia


del puntero, longitud y capacidad des1

La representación no se parece a la Figura 4-3, que es como se vería la memoria


si Rust también copiara los datos del montón. Si Rust hiciera esto, la operación s2
= s1podría ser muy costosa en términos de rendimiento en tiempo de ejecución si
los datos en el montón fueran grandes.

Figura 4-3: Otra posibilidad de lo que s2 = s1podría pasar si Rust


también copiara los datos del montón

Anteriormente, mencionamos que cuando una variable queda fuera de alcance,


Rust llama automáticamente a la dropfunción y limpia la memoria del montón de
esa variable. Sin embargo, la Figura 4-2 muestra ambos punteros de datos
apuntando a la misma ubicación. Esto representa un problema:
cuando s2y s1quedan fuera de alcance, ambos intentarán liberar la misma
memoria. Esto se conoce como error de doble liberación y es uno de los errores
de seguridad de memoria que mencionamos anteriormente. Liberar memoria dos
veces puede provocar corrupción de memoria, lo que potencialmente puede
generar vulnerabilidades de seguridad.

Para garantizar la seguridad de la memoria, después de la línea let s2 = s1;,


Rust s1la considera inválida. Por lo tanto, no necesita liberar nada al s1salir del
ámbito. Observa lo que ocurre al intentar usar ` s1after` s2; no funcionará.

let s1 = String::from("hello");
let s2 = s1;

println!("{s1}, world!");

Recibirás un error como este porque Rust te impide usar la referencia invalidada:

$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:15
|
2| let s1 = String::from("hello");
Rust
jueves, 22 de mayo de 2025 : Página 90 de 719
| -- move occurs because `s1` has type `String`, which does not implement
the `Copy` trait
3| let s2 = s1;
| -- value moved here
4|
5| println!("{s1}, world!");
| ^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes
from the expansion of the macro `println` (in Nightly builds, run with -Z macro-
backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3| let s2 = s1.clone();
| ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Si has oído los términos " copia superficial" y "copia profunda" al trabajar con
otros lenguajes, el concepto de copiar el puntero, la longitud y la capacidad sin
copiar los datos probablemente te suene a una copia superficial. Sin embargo,
como Rust también invalida la primera variable, en lugar de llamarse copia
superficial, se conoce como un movimiento . En este ejemplo, diríamos
que s1 se movió a s2. Por lo tanto, lo que realmente ocurre se muestra en la
Figura 4-4.

Figura 4-4: Representación en memoria después de s1haber sido


invalidada

¡Eso soluciona nuestro problema! Con solo s2válido, cuando se sale del ámbito,
solo así se liberará la memoria, y listo.

Además, esto implica una decisión de diseño: Rust nunca creará


automáticamente copias "profundas" de tus datos. Por lo tanto, se puede asumir
que cualquier copia automática será económica en términos de rendimiento en
tiempo de ejecución.

Alcance y asignación
Rust
jueves, 22 de mayo de 2025 : Página 91 de 719

Lo contrario también aplica a la relación entre el alcance, la propiedad y la


memoria liberada mediante la dropfunción. Al asignar un valor completamente
nuevo a una variable existente, Rust llamará dropy liberará la memoria del valor
original inmediatamente. Considere este código, por ejemplo:

let mut s = String::from("hello");


s = String::from("ahoy");

println!("{s}, world!");

Inicialmente, declaramos una variable sy la vinculamos a a Stringcon el


valor "hello". Inmediatamente después, creamos una nueva variable Stringcon el
valor "ahoy"y la asignamos a s. En este punto, nada hace referencia al valor
original en el montón.

Figura 4-5: Representación en memoria después de que el valor inicial


ha sido reemplazado en su totalidad.

La cadena original queda inmediatamente fuera del ámbito. Rust ejecutará


la drop función en ella y su memoria se liberará inmediatamente. Al imprimir el
valor al final, será "ahoy, world!".

Variables y datos que interactúan con Clone

Si queremos copiar en profundidad los datos del montón de String, no solo los de
la pila, podemos usar un método común llamado clone. Analizaremos la sintaxis
de los métodos en el capítulo 5, pero como los métodos son una característica
común en muchos lenguajes de programación, probablemente ya los hayas visto.

He aquí un ejemplo del clonemétodo en acción:

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {s1}, s2 = {s2}");

Esto funciona bien y produce explícitamente el comportamiento que se muestra


en la Figura 4-3, donde los datos del montón se copian.
Rust
jueves, 22 de mayo de 2025 : Página 92 de 719

Cuando ves una llamada a clone, sabes que se está ejecutando código arbitrario,
y que ese código puede ser costoso. Es un indicador visual de que algo diferente
está sucediendo.

Datos de solo pila: Copiar

Hay otro detalle que aún no hemos abordado. Este código que usa enteros (parte
del cual se mostró en el Listado 4-2) funciona y es válido:

let x = 5;
let y = x;

println!("x = {x}, y = {y}");

Pero este código parece contradecir lo que acabamos de aprender: no tenemos


una llamada a clone, pero xsigue siendo válido y no se movió a y.

La razón es que tipos como los enteros, que tienen un tamaño conocido en tiempo
de compilación, se almacenan completamente en la pila, por lo que las copias de
los valores reales se crean rápidamente. Esto significa que no hay razón para que
queramos impedir xque sean válidos después de crear la variable y. En otras
palabras, no hay diferencia entre copia profunda y superficial, por lo que la
llamada cloneno tendría ningún efecto diferente a la copia superficial habitual, y
podemos omitirla.

Rust cuenta con una anotación especial llamada Copytrait que podemos aplicar a
tipos almacenados en la pila, como los enteros (hablaremos más sobre traits en el
Capítulo 10 ). Si un tipo implementa el Copy trait, las variables que lo usan no se
mueven, sino que se copian de forma trivial, lo que las hace válidas tras
asignarlas a otra variable.

Rust no nos permite anotar un tipo Copysi este, o alguna de sus partes, ha
implementado la Dropcaracterística. Si el tipo requiere que ocurra algo especial
cuando el valor queda fuera del ámbito y añadimos la Copyanotación,
obtendremos un error de compilación. Para saber cómo añadir la Copyanotación a
tu tipo para implementar la característica, consulta "Características
Derivables" en el Apéndice C.

Entonces, ¿qué tipos implementan el Copyatributo? Puedes consultar la


documentación del tipo dado para asegurarte, pero como regla general, cualquier
Rust
jueves, 22 de mayo de 2025 : Página 93 de 719

grupo de valores escalares simples puede implementar `` Copy, y nada que


requiera asignación o sea algún tipo de recurso puede implementar Copy``. Estos
son algunos de los tipos que implementan Copy``:

 Todos los tipos de enteros, como u32.


 El tipo booleano, bool, con valores truey false.
 Todos los tipos de punto flotante, como f64.
 El tipo de personaje, char.
 Tuplas, si solo contienen tipos que también implementan Copy. Por
ejemplo, (i32, i32)implementa Copy, pero (i32, String)no lo hace.

Propiedad y funciones

La mecánica para pasar un valor a una función es similar a la de asignar un valor


a una variable. Pasar una variable a una función implica mover o copiar, al igual
que la asignación. El Listado 4-3 incluye un ejemplo con algunas anotaciones que
muestran dónde entran y salen del ámbito las variables.

Nombre de archivo: src/main.rs


fn main() {
let s = String::from("hello"); // s comes into scope

takes_ownership(s); // s's value moves into the function...


// ... and so is no longer valid here

let x = 5; // x comes into scope

makes_copy(x); // because i32 implements the Copy trait,


// x does NOT move into the function,
println!("{}", x); // so it's okay to use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope


println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope


println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.
Listado 4-3: Funciones con propiedad y alcance anotados
Rust
jueves, 22 de mayo de 2025 : Página 94 de 719

Si intentáramos usar sdespués de llamar a takes_ownership, Rust generaría un


error de compilación. Estas comprobaciones estáticas nos protegen de errores.
Intenta añadir código a mainesos usos spara xver dónde puedes usarlos y dónde
las reglas de propiedad te lo impiden.

Valores de retorno y alcance

Los valores devueltos también pueden transferir la propiedad. El Listado 4-4


muestra un ejemplo de una función que devuelve un valor, con anotaciones
similares a las del Listado 4-3.

Nombre de archivo: src/main.rs


fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1

let s2 = String::from("hello"); // s2 comes into scope

let s3 = takes_and_gives_back(s2); // s2 is moved into


// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String { // gives_ownership will move its


// return value into the function
// that calls it

let some_string = String::from("yours"); // some_string comes into scope

some_string // some_string is returned and


// moves out to the calling
// function
}

// This function takes a String and returns one


fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
// scope

a_string // a_string is returned and moves out to the calling function


}
Listado 4-4: Transferencia de la propiedad de los valores de retorno

La propiedad de una variable sigue siempre el mismo patrón: al asignar un valor a


otra variable, esta se mueve. Cuando una variable que incluye datos en el montón
Rust
jueves, 22 de mayo de 2025 : Página 95 de 719

queda fuera del alcance, el valor se borrará, dropa menos que la propiedad de los
datos se haya movido a otra variable.

Si bien esto funciona, tomar y devolver la propiedad con cada función es un poco
tedioso. ¿Qué pasa si queremos que una función use un valor, pero no que tome
la propiedad? Es bastante molesto que todo lo que pasamos también tenga que
devolverse si queremos usarlo de nuevo, además de cualquier dato resultante del
cuerpo de la función que queramos devolver.

Rust nos permite devolver múltiples valores usando una tupla, como se muestra
en el Listado 4-5.

Nombre de archivo: src/main.rs


fn main() {
let s1 = String::from("hello");

let (s2, len) = calculate_length(s1);

println!("The length of '{s2}' is {len}.");


}

fn calculate_length(s: String) -> (String, usize) {


let length = s.len(); // len() returns the length of a String

(s, length)
}
Listado 4-5: Devolución de la propiedad de los parámetros

Pero esto es demasiada ceremonia y mucho trabajo para un concepto que debería
ser común. Por suerte, Rust cuenta con una función para usar un valor sin
transferir la propiedad, llamada referencias .

Referencias y préstamos

El problema con el código de tupla del Listado 4-5 es que debemos devolver
`the` Stringa la función que lo llama para poder seguir usando
`the` Stringdespués de llamar a `to` calculate_length, ya que `the` Stringse
movió a calculate_length`. En su lugar, podemos proporcionar una referencia
al Stringvalor. Una referencia es como un puntero, ya que es una dirección que
podemos seguir para acceder a los datos almacenados en ella; esos datos
pertenecen a otra variable. A diferencia de un puntero, se garantiza que una
referencia apuntará a un valor válido de un tipo específico durante su vigencia.
Rust
jueves, 22 de mayo de 2025 : Página 96 de 719

Aquí se explica cómo definiría y utilizaría una calculate_lengthfunción que tiene


una referencia a un objeto como parámetro en lugar de tomar propiedad del
valor:

Nombre de archivo: src/main.rs


fn main() {
let s1 = String::from("hello");

let len = calculate_length(&s1);

println!("The length of '{s1}' is {len}.");


}

fn calculate_length(s: &String) -> usize {


s.len()
}

Primero, observe que se ha eliminado todo el código de tupla en la declaración de


la variable y el valor de retorno de la función. Segundo, observe que
pasamos &s1a calculate_lengthy, en su definición, tomamos &Stringen lugar
de String. Estos símbolos "&" representan referencias y permiten hacer referencia
a un valor sin tomar posesión de él. La Figura 4-6 ilustra este concepto.

Figura 4-6: Diagrama de &String scómo señalarString s1

Nota: Lo opuesto a referenciar mediante "using" &es desreferenciar , lo cual se


logra con el operador de desreferencia. *Veremos algunos usos del operador de
desreferencia en el Capítulo 8 y analizaremos los detalles de la desreferencia en
el Capítulo 15.

Echemos un vistazo más de cerca a la llamada de función aquí:

let s1 = String::from("hello");

let len = calculate_length(&s1);

La &s1sintaxis permite crear una referencia que remite al valor de, s1 pero no lo
posee. Dado que la referencia no lo posee, el valor al que apunta no se eliminará
cuando la referencia deje de usarse.
Rust
jueves, 22 de mayo de 2025 : Página 97 de 719

Asimismo, la firma de la función suele &indicar que el tipo del parámetro ses una
referencia. Añadamos algunas anotaciones explicativas:

fn calculate_length(s: &String) -> usize { // s is a reference to a String


s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
// it refers to, the value is not dropped.

El ámbito de validez de la variable ses el mismo que el de cualquier parámetro de


función, pero el valor al que apunta la referencia no se descarta al sdejar de
usarse, ya que sno tiene propiedad. Cuando las funciones tienen referencias como
parámetros en lugar de los valores reales, no es necesario devolver los valores
para devolver la propiedad, ya que nunca la tuvimos.

A la acción de crear una referencia la llamamos préstamo . Como en la vida real,


si alguien posee algo, puedes pedírselo prestado. Al terminar, debes devolverlo.
No eres el dueño.

Entonces, ¿qué pasa si intentamos modificar algo que tomamos prestado?


Prueben el código del Listado 4-6. ¡Atención, spoiler! ¡No funciona!

Nombre de archivo: src/main.rs

fn main() {
let s = String::from("hello");

change(&s);
}

fn change(some_string: &String) {
some_string.push_str(", world");
}
Listado 4-6: Intento de modificar un valor prestado

Aquí está el error:

$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&`
reference
--> src/main.rs:8:5
|
8| some_string.push_str(", world");
Rust
jueves, 22 de mayo de 2025 : Página 98 de 719
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to
cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Así como las variables son inmutables por defecto, también lo son las referencias.
No podemos modificar algo a lo que tenemos una referencia.

Referencias mutables

Podemos arreglar el código del Listado 4-6 para permitirnos modificar un valor
prestado con solo unos pequeños ajustes que utilizan, en su lugar, una referencia
mutable :

Nombre de archivo: src/main.rs


fn main() {
let mut s = String::from("hello");

change(&mut s);
}

fn change(some_string: &mut String) {


some_string.push_str(", world");
}

Primero, cambiamos sa mut. Luego, creamos una referencia mutable con &mut
sdonde llamamos a la changefunción y actualizamos la firma de la función para
que acepte una referencia mutable con some_string: &mut String. Esto deja claro
que la changefunción mutará el valor que toma prestado.

Las referencias mutables tienen una restricción importante: si se tiene una


referencia mutable a un valor, no se pueden tener otras referencias a ese valor.
Este código, que intenta crear dos referencias mutables a, sfallará:

Nombre de archivo: src/main.rs

let mut s = String::from("hello");


Rust
jueves, 22 de mayo de 2025 : Página 99 de 719
let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

Aquí está el error:

$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4| let r1 = &mut s;
| ------ first mutable borrow occurs here
5| let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6|
7| println!("{}, {}", r1, r2);
| -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Este error indica que este código no es válido porque no podemos tomar
prestado scomo mutable más de una vez. El primer préstamo mutable está en
[nombre del archivo] r1y debe durar hasta que se use en [nombre del
archivo] println!, pero entre la creación de esa referencia mutable y su uso,
intentamos crear otra referencia mutable en [nombre del archivo] r2que toma
prestados los mismos datos que [nombre del archivo] r1.

La restricción que impide múltiples referencias mutables a los mismos datos


simultáneamente permite la mutación, pero de forma muy controlada. Es algo con
lo que los nuevos usuarios de Rust tienen dificultades, ya que la mayoría de los
lenguajes permiten mutar cuando se desee. La ventaja de esta restricción es que
Rust puede evitar las carreras de datos en tiempo de compilación. Una carrera de
datos es similar a una condición de carrera y ocurre cuando se presentan estos
tres comportamientos:

 Dos o más punteros acceden a los mismos datos al mismo tiempo.


 Se está utilizando al menos uno de los punteros para escribir los datos.
 No se utiliza ningún mecanismo para sincronizar el acceso a los datos.
Rust
jueves, 22 de mayo de 2025 : Página 100 de 719

Las carreras de datos causan un comportamiento indefinido y pueden ser difíciles


de diagnosticar y solucionar cuando intentas rastrearlas en tiempo de ejecución.
¡Rust previene este problema al negarse a compilar código con carreras de datos!

Como siempre, podemos usar llaves para crear un nuevo ámbito, lo que permite
múltiples referencias mutables, pero no simultáneas :

let mut s = String::from("hello");

{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no
problems.

let r2 = &mut s;

Rust aplica una regla similar para combinar referencias mutables e inmutables.
Este código genera un error:

let mut s = String::from("hello");

let r1 = &s; // no problem


let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM

println!("{}, {}, and {}", r1, r2, r3);

Aquí está el error:

$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
--> src/main.rs:6:14
|
4| let r1 = &s; // no problem
| -- immutable borrow occurs here
5| let r2 = &s; // no problem
6| let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7|
8| println!("{}, {}, and {}", r1, r2, r3);
| -- immutable borrow later used here
Rust
jueves, 22 de mayo de 2025 : Página 101 de 719
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

¡Uf! Tampoco podemos tener una referencia mutable si tenemos una inmutable al
mismo valor.

Los usuarios de una referencia inmutable no esperan que el valor cambie


repentinamente. Sin embargo, se permiten múltiples referencias inmutables
porque nadie que simplemente lea los datos puede influir en la lectura de los
mismos por parte de otros.

Tenga en cuenta que el alcance de una referencia comienza desde su


introducción y continúa hasta su último uso. Por ejemplo, este código se
compilará porque el último uso de las referencias inmutables está en el println!,
antes de que se introduzca la referencia mutable:

let mut s = String::from("hello");

let r1 = &s; // no problem


let r2 = &s; // no problem
println!("{r1} and {r2}");
// variables r1 and r2 will not be used after this point

let r3 = &mut s; // no problem


println!("{r3}");

Los alcances de las referencias inmutables r1terminan r2después de println! su


último uso, es decir, antes de la r3creación de la referencia mutable. Estos
alcances no se superponen, por lo que este código está permitido: el compilador
puede determinar que la referencia ya no se utiliza antes del final del alcance.

Aunque los errores de préstamo pueden ser frustrantes a veces, recuerda que es
el compilador de Rust el que señala un posible error con antelación (en tiempo de
compilación, no de ejecución) y te muestra exactamente dónde está el problema.
Así, no tendrás que buscar por qué tus datos no son lo que creías.

Referencias colgantes

En lenguajes con punteros, es fácil crear erróneamente un puntero colgante (un


puntero que referencia una ubicación en memoria que podría haber sido asignada
a otra persona) al liberar memoria y conservar un puntero a dicha memoria. En
Rust, por el contrario, el compilador garantiza que las referencias nunca serán
Rust
jueves, 22 de mayo de 2025 : Página 102 de 719

colgantes: si se tiene una referencia a datos, el compilador se asegurará de que


estos no salgan del ámbito antes que la referencia a ellos.

Intentemos crear una referencia colgante para ver cómo Rust los evita con un
error en tiempo de compilación:

Nombre de archivo: src/main.rs

fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String {


let s = String::from("hello");

&s
}

Aquí está el error:

$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no
value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're
returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|

error[E0515]: cannot return reference to local variable `s`


--> src/main.rs:8:5
|
8| &s
Rust
jueves, 22 de mayo de 2025 : Página 103 de 719
| ^^ returns a reference to data owned by the current function

Some errors have detailed explanations: E0106, E0515.


For more information about an error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 2 previous errors

Este mensaje de error se refiere a una característica que aún no hemos abordado:
los tiempos de vida. Los analizaremos en detalle en el capítulo 10. Sin embargo, si
ignoramos la información sobre los tiempos de vida, el mensaje contiene la clave
del problema de este código:

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from

Veamos con más detalle qué sucede exactamente en cada etapa de


nuestro danglecódigo:

Nombre de archivo: src/main.rs

fn dangle() -> &String { // dangle returns a reference to a String

let s = String::from("hello"); // s is a new String

&s // we return a reference to the String, s


} // Here, s goes out of scope, and is dropped. Its memory goes away.
// Danger!

Dado que sse crea dentro de [nombre del archivo] , al finalizar dangleel código de
[ nombre del archivo], se desasignará. Pero intentamos devolver una referencia a
él. Esto significa que esta referencia estaría apuntando a un [nombre del archivo]
no válido . ¡Eso es un error! Rust no nos permite hacerlo.danglesString

La solución aquí es devolverlo Stringdirectamente:

fn no_dangle() -> String {


let s = String::from("hello");

s
}

Esto funciona sin problemas. Se transfiere la propiedad y no se desasigna nada.

Las reglas de referencias


Rust
jueves, 22 de mayo de 2025 : Página 104 de 719

Resumamos lo que hemos discutido sobre las referencias:

 En cualquier momento, puede tener una referencia mutable o cualquier


cantidad de referencias inmutables.
 Las referencias deben ser siempre válidas.

A continuación, veremos un tipo diferente de referencia: las rebanadas.

El tipo de rebanada

Las porciones permiten referenciar una secuencia contigua de elementos de


una colección , en lugar de a toda la colección. Una porción es un tipo de
referencia, por lo que no tiene propiedad.

Aquí hay un pequeño problema de programación: escribe una función que tome
una cadena de palabras separadas por espacios y devuelva la primera palabra
que encuentre en ella. Si la función no encuentra ningún espacio en la cadena, la
cadena completa debe ser una sola palabra, por lo que se debe devolver la
cadena completa.

Analicemos cómo escribiríamos la firma de esta función sin usar porciones, para
comprender el problema que resolverán las porciones:

fn first_word(s: &String) -> ?

La first_wordfunción tiene a &Stringcomo parámetro. No necesitamos propiedad,


así que esto está bien. (En Rust, el lenguaje común, las funciones no toman
propiedad de sus argumentos a menos que lo necesiten, y las razones para ello se
aclararán a medida que avancemos). Pero ¿qué deberíamos devolver? Realmente
no tenemos forma de hablar de una parte de una cadena. Sin embargo,
podríamos devolver el índice del final de la palabra, indicado por un espacio.
Intentémoslo, como se muestra en el Listado 4-7.

Nombre de archivo: src/main.rs


fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {


if item == b' ' {
return i;
}
}
Rust
jueves, 22 de mayo de 2025 : Página 105 de 719

s.len()
}
Listado 4-7: La first_wordfunción que devuelve un valor de índice de byte en
el Stringparámetro

Como necesitamos recorrer Stringelemento por elemento y verificar si un valor es


un espacio, convertiremos nuestro Stringen una matriz de bytes usando
el as_bytesmétodo.

let bytes = s.as_bytes();

A continuación, creamos un iterador sobre la matriz de bytes utilizando


el itermétodo:

for (i, &item) in bytes.iter().enumerate() {

Analizaremos los iteradores con más detalle en el Capítulo 13. Por ahora, sepa
que iteres un método que devuelve cada elemento de una colección y
que enumerateencapsula el resultado de itercada elemento, devolviéndolo como
parte de una tupla. El primer elemento de la tupla devuelta enumeratees el
índice, y el segundo elemento es una referencia al elemento. Esto es un poco más
práctico que calcular el índice nosotros mismos.

Dado que el enumeratemétodo devuelve una tupla, podemos usar patrones para
desestructurarla. Analizaremos los patrones con más detalle en el Capítulo 6. En
el forbucle, especificamos un patrón que tiene i para el índice de la tupla
y &itempara el byte único de la tupla. Dado que obtenemos una referencia al
elemento de [nombre de la tupla] .iter().enumerate(), usamos [nombre de
la &tupla] en el patrón.

Dentro del forbucle, buscamos el byte que representa el espacio usando la


sintaxis de literal de byte. Si encontramos un espacio, devolvemos la posición. De
lo contrario, devolvemos la longitud de la cadena usando s.len().

if item == b' ' {


return i;
}
}

s.len()
Rust
jueves, 22 de mayo de 2025 : Página 106 de 719

Ahora tenemos una forma de averiguar el índice del final de la primera palabra de
la cadena, pero hay un problema. Estamos devolviendo `a` usizepor sí solo, pero
solo es un número significativo en el contexto de ` &String. En otras palabras, al
ser un valor independiente de ` String, no hay garantía de que siga siendo válido
en el futuro. Considere el programa del Listado 4-8 que usa la first_wordfunción
del Listado 4-7.

Nombre de archivo: src/main.rs


fn main() {
let mut s = String::from("hello world");

let word = first_word(&s); // word will get the value 5

s.clear(); // this empties the String, making it equal to ""

// `word` still has the value `5` here, but `s` no longer has any content
// that we could meaningfully use with the value `5`, so `word` is now
// totally invalid!
}
Listado 4-8: Almacenamiento del resultado de llamar a la first_wordfunción y
luego cambiar el Stringcontenido

Este programa se compila sin errores y también lo haría si


usáramos word después de llamar a s.clear(). Dado wordque no está conectado al
estado de s , wordaún contiene el valor 5. Podríamos usar ese valor 5con la
variable spara intentar extraer la primera palabra, pero esto sería un error, ya que
el contenido de sha cambiado desde que lo guardamos 5en word.

Preocuparse de que el índice wordse desincronice con los datos ses tedioso y
propenso a errores. Gestionar estos índices es aún más complejo si escribimos
una second_wordfunción. Su firma debería ser similar a esta:

fn second_word(s: &String) -> (usize, usize) {

Ahora rastreamos un índice inicial y uno final, y tenemos aún más valores
calculados a partir de datos de un estado específico, pero que no están vinculados
a él. Tenemos tres variables no relacionadas que deben mantenerse
sincronizadas.

Afortunadamente, Rust tiene una solución para este problema: segmentos de


cadenas.
Rust
jueves, 22 de mayo de 2025 : Página 107 de 719
Rebanadas de cuerda

Una porción de cadena es una referencia a una parte de una Stringy se ve así:

let s = String::from("hello world");

let hello = &s[0..5];


let world = &s[6..11];

En lugar de una referencia a la totalidad de String, helloes una referencia a una


parte de String, especificada en el [0..5]bit adicional. Creamos segmentos usando
un rango entre corchetes, especificando [starting_index..ending_index],
donde starting_indexes la primera posición del segmento y ending_indexes uno
más que la última posición. Internamente, la estructura de datos del segmento
almacena la posición inicial y la longitud del segmento, que corresponde
a ending_indexmenos starting_index. Por lo tanto, en el caso de let world =
&s[6..11];, worldsería un segmento que contiene un puntero al byte en el índice 6
de scon un valor de longitud de 5.

La figura 4-7 muestra esto en un diagrama.

Figura 4-7: Segmento de cadena que hace referencia a una parte de


una String

Con la sintaxis de rango de Rust .., si desea comenzar en el índice 0, puede omitir
el valor antes de los dos puntos. En otras palabras, son iguales:

let s = String::from("hello");

let slice = &s[0..2];


let slice = &s[..2];

De igual manera, si su segmento incluye el último byte de String, puede omitir el


número final. Esto significa que son iguales:

let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];


Rust
jueves, 22 de mayo de 2025 : Página 108 de 719
let slice = &s[3..];

También puedes omitir ambos valores para tomar una porción de la cadena
completa. Por lo tanto, son iguales:

let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];


let slice = &s[..];

Nota: Los índices de rango de segmentos de cadena deben estar en límites


válidos de caracteres UTF-8. Si intenta crear un segmento de cadena en medio de
un carácter multibyte, el programa cerrará con un error. Para la introducción de
segmentos de cadena, en esta sección se asume únicamente el formato ASCII;
una explicación más detallada del manejo de UTF-8 se encuentra en la
sección "Almacenamiento de texto codificado en UTF-8 con cadenas" del Capítulo
8.

Con toda esta información en mente, reescribamos first_wordpara devolver una


porción. El tipo que significa "porción de cadena" se escribe como &str:

Nombre de archivo: src/main.rs


fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {


if item == b' ' {
return &s[0..i];
}
}

&s[..]
}

Obtenemos el índice del final de la palabra de la misma manera que en el Listado


4-7, buscando la primera ocurrencia de un espacio. Cuando encontramos un
espacio, devolvemos un fragmento de cadena utilizando el inicio de la cadena y el
índice del espacio como índices inicial y final.
Rust
jueves, 22 de mayo de 2025 : Página 109 de 719

Ahora, al llamar a first_word, obtenemos un único valor vinculado a los datos


subyacentes. Este valor se compone de una referencia al punto de inicio del
segmento y el número de elementos que lo componen.

Devolver una porción también funcionaría para una second_wordfunción:

fn second_word(s: &String) -> &str {

Ahora tenemos una API sencilla y mucho más difícil de cometer errores, ya que el
compilador garantizará que las referencias a [insert name] Stringsigan siendo
válidas. ¿Recuerdan el error del programa del Listado 4-8, cuando obtuvimos el
índice hasta el final de la primera palabra, pero luego borramos la cadena, por lo
que nuestro índice no era válido? Ese código era lógicamente incorrecto, pero no
mostró ningún error inmediato. Los problemas aparecerían más adelante si
seguíamos intentando usar el índice de la primera palabra con una cadena vacía.
Las porciones evitan este error y nos avisan mucho antes de que tenemos un
problema con nuestro código. Usar la versión de porciones de [insert
name] first_wordgenerará un error en tiempo de compilación:

Nombre de archivo: src/main.rs

fn main() {
let mut s = String::from("hello world");

let word = first_word(&s);

s.clear(); // error!

println!("the first word is: {word}");


}

Aquí está el error del compilador:

$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
--> src/main.rs:18:5
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
17 |
18 | s.clear(); // error!
Rust
jueves, 22 de mayo de 2025 : Página 110 de 719
| ^^^^^^^^^ mutable borrow occurs here
19 |
20 | println!("the first word is: {word}");
| ------ immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Recuerda que, según las reglas de préstamo, si tenemos una referencia inmutable
a algo, no podemos tomar también una referencia mutable. Dado
que clearnecesita truncarse String, necesita obtener una referencia mutable. El
[nombre de println! la referencia] después de la llamada a clearusa la referencia
en [nombre de la referencia] word, por lo que la referencia inmutable debe seguir
activa en ese momento. Rust impide que la referencia mutable en [nombre de la
referencia] cleary la referencia inmutable en [ wordnombre de la referencia]
existan simultáneamente, y la compilación falla. Rust no solo ha simplificado el
uso de nuestra API, sino que también ha eliminado toda una clase de errores en
tiempo de compilación.

Literales de cadena como porciones

Recordemos que hablamos sobre el almacenamiento de literales de cadena


dentro del binario. Ahora que conocemos las porciones, podemos comprenderlas
correctamente:

let s = "Hello, world!";

El tipo de saquí es &str: es una porción que apunta a ese punto específico del
binario. Esta es también la razón por la que los literales de cadena son
inmutables; &stres una referencia inmutable.

Segmentos de cadena como parámetros

Saber que puedes tomar porciones de literales y Stringvalores nos lleva a una
mejora más en first_word, y es su firma:

fn first_word(s: &String) -> &str {

Un rustáceo con más experiencia escribiría la firma que se muestra en el Listado


4-9 porque nos permite usar la misma función en &Stringvalores y &strvalores.

fn first_word(s: &str) -> &str {


Rust
jueves, 22 de mayo de 2025 : Página 111 de 719
Listado 4-9: Mejora de la first_wordfunción mediante el uso de una porción de
cadena para el tipo de sparámetro

Si tenemos una porción de cadena, podemos pasarla directamente. Si tenemos


un String, podemos pasar una porción de Stringo una referencia a String. Esta
flexibilidad aprovecha las coerciones de desreferencia , una característica que
abordaremos en la sección "Coerciones de desreferencia implícitas con funciones
y métodos" del Capítulo 15.

Definir una función para tomar una porción de cadena en lugar de una referencia
a String hace que nuestra API sea más general y útil sin perder ninguna
funcionalidad:

Nombre de archivo: src/main.rs


fn main() {
let my_string = String::from("hello world");

// `first_word` works on slices of `String`s, whether partial or whole


let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s
let word = first_word(&my_string);

let my_string_literal = "hello world";

// `first_word` works on slices of string literals, whether partial or whole


let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);

// Because string literals *are* string slices already,


// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}

Otras rebanadas

Como puedes imaginar, las porciones de cadena son específicas de las cadenas.
Pero también existe un tipo de porción más general. Considera esta matriz:

let a = [1, 2, 3, 4, 5];

Al igual que podríamos querer referirnos a una parte de una cadena, podríamos
querer referirnos a una parte de un array. Lo haríamos así:
Rust
jueves, 22 de mayo de 2025 : Página 112 de 719
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

Esta porción es de tipo &[i32]. Funciona de forma similar a las porciones de


cadena, almacenando una referencia al primer elemento y una longitud. Utilizará
este tipo de porción para todo tipo de colecciones. Analizaremos estas
colecciones en detalle cuando tratemos los vectores en el Capítulo 8.

Resumen

Los conceptos de propiedad, préstamo y porciones garantizan la seguridad de la


memoria en los programas Rust durante la compilación. Rust te permite controlar
el uso de la memoria de la misma forma que otros lenguajes de programación de
sistemas, pero al permitir que el propietario de los datos los limpie
automáticamente cuando este deja de estar dentro del alcance, evitas escribir y
depurar código adicional para obtener este control.

La propiedad afecta el funcionamiento de muchas otras partes de Rust, por lo que


profundizaremos en estos conceptos a lo largo del libro. Pasemos al capítulo 5 y
veamos cómo agrupar datos en un archivo struct.

Uso de estructuras para estructurar datos relacionados

Un struct , o estructura , es un tipo de dato personalizado que permite


empaquetar y nombrar múltiples valores relacionados que conforman un grupo
significativo. Si está familiarizado con un lenguaje orientado a objetos,
un struct es como los atributos de datos de un objeto. En este capítulo,
compararemos y contrastaremos las tuplas con los structs para ampliar sus
conocimientos y demostrar cuándo los structs son una mejor manera de agrupar
datos.

Demostraremos cómo definir e instanciar estructuras. Explicaremos cómo definir


funciones asociadas, especialmente las llamadas métodos , para especificar el
comportamiento asociado a un tipo de estructura. Las estructuras y las
enumeraciones (discutidas en el Capítulo 6) son los componentes básicos para
crear nuevos tipos en el dominio del programa y aprovechar al máximo la
comprobación de tipos en tiempo de compilación de Rust.
Rust
jueves, 22 de mayo de 2025 : Página 113 de 719
Definición e instanciación de estructuras

Las estructuras son similares a las tuplas, que se describen en la sección "El tipo
de tupla" , ya que ambas contienen múltiples valores relacionados. Al igual que
las tuplas, las partes de una estructura pueden ser de diferentes tipos. A
diferencia de las tuplas, en una estructura se asigna un nombre a cada dato para
que quede claro el significado de los valores. Añadir estos nombres significa que
las estructuras son más flexibles que las tuplas: no es necesario depender del
orden de los datos para especificar o acceder a los valores de una instancia.

Para definir una estructura, introducimos la palabra clave structy le asignamos un


nombre. El nombre de la estructura debe describir la importancia de los datos que
se agrupan. Luego, entre llaves, definimos los nombres y tipos de los datos, que
llamamos campos . Por ejemplo, el Listado 5-1 muestra una estructura que
almacena información sobre una cuenta de usuario.

Nombre de archivo: src/main.rs


struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
Listado 5-1: Una Userdefinición de estructura

Para usar una estructura después de definirla, creamos una instancia de la misma
especificando valores concretos para cada campo. Creamos la instancia indicando
el nombre de la estructura y luego añadimos llaves que contienen pares clave-
valor , donde las claves son los nombres de los campos y los valores los datos que
queremos almacenar en ellos. No es necesario especificar los campos en el
mismo orden en que los declaramos en la estructura. En otras palabras, la
definición de la estructura funciona como una plantilla general para el tipo, y las
instancias la completan con datos específicos para crear valores del tipo. Por
ejemplo, podemos declarar un usuario específico como se muestra en el Listado
5-2.

Nombre de archivo: src/main.rs


fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
Rust
jueves, 22 de mayo de 2025 : Página 114 de 719
sign_in_count: 1,
};
}
Listado 5-2: Creación de una instancia de la Userestructura

Para obtener un valor específico de una estructura, 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
usando la notación de punto y asignándolo a un campo específico. El Listado 5-3
muestra cómo cambiar el valor en el email campo de una Userinstancia mutable.

Nombre de archivo: 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]");
}
Listado 5-3: Cambiar el valor en el emailcampo de una Userinstancia

Tenga en cuenta que toda la instancia debe ser mutable; Rust no permite marcar
solo ciertos campos como mutables. Como con cualquier expresión, podemos
construir una nueva instancia de la estructura como última expresión en el cuerpo
de la función para devolverla implícitamente.

El Listado 5-4 muestra una build_userfunción que devuelve una Userinstancia con
el correo electrónico y el nombre de usuario especificados. El activecampo
obtiene el valor de true, y el sign_in_countobtiene el valor de 1.

Nombre de archivo: src/main.rs


fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
Listado 5-4: Una build_userfunción que toma un correo electrónico y un nombre
de usuario y devuelve una Userinstancia
Rust
jueves, 22 de mayo de 2025 : Página 115 de 719

Tiene sentido nombrar los parámetros de la función con el mismo nombre que los
campos de la estructura, pero tener que repetir los emailnombres usernamede los
campos y las variables es un poco tedioso. Si la estructura tuviera más campos,
repetir cada nombre sería aún más molesto. ¡Por suerte, existe una abreviatura
muy práctica!

Uso de la abreviatura Field Init

Debido a que los nombres de los parámetros y los nombres de los campos de la
estructura son exactamente los mismos en el Listado 5-4, podemos usar la
sintaxis abreviada de field init para reescribir build_userde modo que se comporte
exactamente igual pero no tenga la repetición de usernamey email, como se
muestra en el Listado 5-5.

Nombre de archivo: src/main.rs


fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
Listado 5-5: Una build_userfunción que utiliza la abreviatura de campo init porque
los parámetros usernamey emailtienen el mismo nombre que los campos de
estructura

Aquí, creamos una nueva instancia de la Userestructura, que tiene un campo


llamado email. Queremos establecer el emailvalor del campo con el valor
del emailparámetro de la build_userfunción. Dado que el emailcampo y
el emailparámetro tienen el mismo nombre, solo necesitamos escribir emailen
lugar de email: email.

Creación de instancias a partir de otras instancias con la sintaxis de


actualización de estructura

Suele ser útil crear una nueva instancia de una estructura que incluya la mayoría
de los valores de otra instancia, pero modifique algunos. Puede hacerlo usando la
sintaxis de actualización de estructura .

Primero, en el Listado 5-6 mostramos cómo crear una


nueva Userinstancia user2 regularmente, sin la sintaxis de actualización.
Rust
jueves, 22 de mayo de 2025 : Página 116 de 719

Establecemos un nuevo valor para, emailpero en caso contrario, usamos los


mismos valores user1que creamos en el Listado 5-2.

Nombre de archivo: src/main.rs


fn main() {
// --snip--

let user2 = User {


active: user1.active,
username: user1.username,
email: String::from("[email protected]"),
sign_in_count: user1.sign_in_count,
};
}
Listado 5-6: Creación de una nueva Userinstancia utilizando todos los valores
menos uno deuser1

Usando la sintaxis de actualización de estructura, podemos lograr el mismo efecto


con menos código, como se muestra en el Listado 5-7. La sintaxis ..especifica que
los campos restantes no definidos explícitamente deben tener el mismo valor que
los campos de la instancia dada.

Nombre de archivo: src/main.rs


fn main() {
// --snip--

let user2 = User {


email: String::from("[email protected]"),
..user1
};
}
Listado 5-7: Uso de la sintaxis de actualización de estructura para establecer un
nuevo emailvalor para una Userinstancia pero para usar el resto de los valores
deuser1

El código del Listado 5-7 también crea una instancia en user2que tiene un valor
diferente para, emailpero con los mismos valores para los
campos username, active, y sign_in_countde user1. ..user1Debe aparecer al final
para especificar que los campos restantes deben obtener sus valores de los
campos correspondientes en user1, pero podemos especificar valores para todos
los campos que queramos en cualquier orden, independientemente del orden de
los campos en la definición de la estructura.
Rust
jueves, 22 de mayo de 2025 : Página 117 de 719

Tenga en cuenta que la sintaxis de actualización de estructura se usa =como una


asignación; esto se debe a que mueve los datos, tal como vimos en la
sección "Variables y datos que interactúan con Move" . En este ejemplo, ya no
podemos usar ` user1como un todo` después de crearlo user2porque `` Stringen
el usernamecampo de` user1se movió a `` user2. Si hubiéramos
asignado user2nuevos Stringvalores para `` emaily` username, y por lo tanto solo
hubiéramos usado los valores de active`` y` de` , `` seguiría siendo válido
después de crear ` `. Tanto `` como` son tipos que implementan el atributo, por
lo que se aplicaría el comportamiento que explicamos en la sección "Datos de
solo pila: Copiar" . Todavía podemos usar `` en este ejemplo, ya que su
valor no se movió
fuera.sign_in_countuser1user1user2activesign_in_countCopyuser1.email

Uso de estructuras de tuplas sin campos con nombre para crear


diferentes tipos

Rust también admite estructuras similares a las tuplas, llamadas estructuras de


tupla . Las estructuras de tupla tienen el significado adicional que proporciona el
nombre de la estructura, pero no tienen nombres asociados a sus campos;
simplemente tienen los tipos de estos. Las estructuras de tupla son útiles cuando
se desea asignar un nombre a toda la tupla y que esta tenga un tipo diferente al
de otras tuplas, y cuando nombrar cada campo como en una estructura normal
resultaría demasiado extenso o redundante.

Para definir una estructura de tupla, comience con la structpalabra clave y el


nombre de la estructura, seguidos de los tipos de la tupla. Por ejemplo, aquí
definimos y usamos dos estructuras de tupla llamadas Colory Point:

Nombre de archivo: src/main.rs


struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}

Tenga en cuenta que los valores blacky originson de tipos diferentes porque son
instancias de estructuras de tuplas diferentes. Cada estructura que defina tiene
su propio tipo, aunque los campos dentro de ella puedan tener el mismo tipo. Por
ejemplo, una función que toma un parámetro de tipo Colorno puede tomar
Rust
jueves, 22 de mayo de 2025 : Página 118 de 719

a Pointcomo argumento, aunque ambos tipos estén compuestos por


tres i32 valores. Por lo demás, las instancias de estructuras de tuplas son
similares a las tuplas en que se pueden desestructurar en sus partes individuales
y se puede usar a .seguido del índice para acceder a un valor individual. A
diferencia de las tuplas, las estructuras de tuplas requieren que se indique el tipo
de la estructura al desestructurarlas. Por ejemplo, escribiríamos let Point(x, y, z) =
point.

Estructuras tipo unidad sin campos

También puedes definir estructuras sin campos. Estas se denominan estructuras


tipo unidad porque se comportan de forma similar a (), el tipo de unidad que
mencionamos en la sección "El tipo de tupla" . Las estructuras tipo unidad pueden
ser útiles cuando necesitas implementar un atributo en algún tipo, pero no tienes
datos que quieras almacenar en el propio tipo. Hablaremos de los atributos en el
Capítulo 10. A continuación, se muestra un ejemplo de declaración e instanciación
de una estructura tipo unidad llamada AlwaysEqual:

Nombre de archivo: src/main.rs


struct AlwaysEqual;

fn main() {
let subject = AlwaysEqual;
}

Para definir AlwaysEqual, usamos la structpalabra clave, el nombre deseado y, a


continuación, un punto y coma. ¡No necesitamos llaves ni paréntesis! Luego,
podemos obtener una instancia de AlwaysEqualen la subjectvariable de forma
similar: usando el nombre que definimos, sin llaves ni paréntesis. Imaginemos que
más adelante implementaremos un comportamiento para este tipo de forma que
cada instancia de AlwaysEqualsea siempre igual a cualquier instancia de
cualquier otro tipo, quizás para obtener un resultado conocido para fines de
prueba. ¡No necesitaríamos ningún dato para implementar ese comportamiento!
En el Capítulo 10, veremos cómo definir rasgos e implementarlos en cualquier
tipo, incluyendo estructuras de tipo unidad.

Propiedad de los datos de estructura

En la Userdefinición de la estructura del Listado 5-1, usamos el String tipo propio


en lugar del &strtipo segmento de cadena. Esta es una decisión deliberada, ya
Rust
jueves, 22 de mayo de 2025 : Página 119 de 719

que queremos que cada instancia de esta estructura posea todos sus datos y que
estos sean válidos mientras la estructura completa lo sea.

Las estructuras también pueden almacenar referencias a datos pertenecientes a


otra entidad, pero para ello se requiere el uso de tiempos de vida , una
característica de Rust que analizaremos en el Capítulo 10. Los tiempos de vida
garantizan que los datos referenciados por una estructura sean válidos mientras
la estructura lo sea. Supongamos que intenta almacenar una referencia en una
estructura sin especificar tiempos de vida, como en el siguiente ejemplo; esto no
funcionará:

Nombre de archivo: src/main.rs

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,
};
}

El compilador se quejará de que necesita especificadores de duración:

$ cargo run
Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3| username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2| active: bool,
3~ username: &'a str,
Rust
jueves, 22 de mayo de 2025 : Página 120 de 719
|

error[E0106]: missing lifetime specifier


--> src/main.rs:4:12
|
4| email: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2| active: bool,
3| username: &str,
4~ email: &'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, analizaremos cómo solucionar estos errores para que pueda
almacenar referencias en estructuras, pero por ahora, solucionaremos errores
como estos utilizando tipos propios como Stringen lugar de referencias como &str.

Un programa de ejemplo que utiliza estructuras

Para entender cuándo podríamos necesitar usar estructuras, escribamos un


programa que calcule el área de un rectángulo. Empezaremos usando variables
individuales y luego refactorizaremos el programa hasta que usemos estructuras.

Creemos un nuevo proyecto binario con Cargo llamado rectángulos. Este tomará
el ancho y la altura de un rectángulo especificado en píxeles y calculará su área.
El Listado 5-8 muestra un programa corto con una forma de hacer exactamente
eso en el archivo src/main.rs de nuestro proyecto .

Nombre de archivo: src/main.rs


fn main() {
let width1 = 30;
let height1 = 50;

println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
Rust
jueves, 22 de mayo de 2025 : Página 121 de 719
fn area(width: u32, height: u32) -> u32 {
width * height
}
Listado 5-8: Cálculo del área de un rectángulo especificado por variables de ancho
y alto independientes

Ahora, ejecute este programa usando cargo run:

$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.

Este código logra determinar el área del rectángulo llamando a la areafunción con
cada dimensión, pero podemos hacer más para que este código sea claro y
legible.

El problema con este código es evidente en la firma de area:

fn area(width: u32, height: u32) -> u32 {

Se supone que la areafunción calcula el área de un rectángulo, pero la función


que escribimos tiene dos parámetros, y no queda claro en ningún punto del
programa que estos estén relacionados. Sería más legible y manejable agrupar
anchura y altura. Ya explicamos una forma de hacerlo en la sección "El tipo de
tupla" del capítulo 3: mediante tuplas.

Refactorización con tuplas

El listado 5-9 muestra otra versión de nuestro programa que utiliza tuplas.

Nombre de archivo: src/main.rs


fn main() {
let rect1 = (30, 50);

println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}

fn area(dimensions: (u32, u32)) -> u32 {


dimensions.0 * dimensions.1
}
Rust
jueves, 22 de mayo de 2025 : Página 122 de 719
Listado 5-9: Especificación del ancho y la altura del rectángulo con una tupla

En cierto modo, este programa es mejor. Las tuplas nos permiten añadir algo de
estructura, y ahora solo pasamos un argumento. Pero, por otro lado, esta versión
es menos clara: las tuplas no nombran sus elementos, por lo que debemos
indexar las partes de la tupla, lo que hace que nuestro cálculo sea menos obvio.

Mezclar el ancho y la altura no importaría para el cálculo del área, pero si


queremos dibujar el rectángulo en la pantalla, ¡sí importaría! Tendríamos que
tener en cuenta que widthes el índice de la tupla 0y heightes el índice de la
tupla 1. Esto sería aún más difícil de entender y recordar para alguien más si
usara nuestro código. Como no hemos transmitido el significado de nuestros
datos en el código, ahora es más fácil introducir errores.

Refactorización con estructuras: añadiendo más significado

Usamos estructuras para añadir significado al etiquetar los datos. Podemos


transformar la tupla que usamos en una estructura con un nombre para el
conjunto y nombres para las partes, como se muestra en el Listado 5-10.

Nombre de archivo: src/main.rs


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)
);
}

fn area(rectangle: &Rectangle) -> u32 {


rectangle.width * rectangle.height
}
Listado 5-10: Definición de una Rectangleestructura
Rust
jueves, 22 de mayo de 2025 : Página 123 de 719

Aquí definimos una estructura y la llamamos Rectangle. Dentro de las llaves,


definimos los campos como widthy height, ambos de tipo u32. Luego, en main,
creamos una instancia específica de Rectangle con un ancho de 30y una altura
de 50.

Nuestra areafunción ahora está definida con un parámetro, llamado rectangle,


cuyo tipo es un préstamo inmutable de una Rectangle instancia de struct. Como
se mencionó en el Capítulo 4, queremos tomar prestada la struct en lugar de
tomar posesión de ella. De esta manera, mainconserva su propiedad y puede
seguir usando rect1, razón por la cual usamos &en la firma de la función y donde
la llamamos.

La areafunción accede a los campos width`y` heightde la Rectangle instancia


(tenga en cuenta que acceder a los campos de una instancia de estructura
prestada no mueve los valores de los campos, por lo que a menudo se ven
préstamos de estructuras). Nuestra firma de función,
por ahora, indica areaexactamente lo que queremos decir: calcular el área
de Rectangle` width<sub> height...01

Añadiendo funcionalidad útil con rasgos derivados

Sería útil poder imprimir una instancia de Rectanglemientras depuramos nuestro


programa y ver los valores de todos sus campos. El Listado 5-11 intenta usar
la println!macro como en capítulos anteriores. Sin embargo, esto no funcionará.

Nombre de archivo: src/main.rs

struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

println!("rect1 is {}", rect1);


}
Listado 5-11: Intento de imprimir una Rectangleinstancia
Rust
jueves, 22 de mayo de 2025 : Página 124 de 719

Cuando compilamos este código, obtenemos un error con este mensaje principal:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

La println!macro puede realizar muchos tipos de formato y, por defecto, las llaves
indican println!que se use el formato conocido como Display: output, destinado al
consumo directo del usuario final. Los tipos primitivos que hemos visto hasta
ahora se implementan Displaypor defecto porque solo hay una forma de mostrar
a 1o cualquier otro tipo primitivo a un usuario. Sin embargo, con las estructuras,
la forma println!de formatear la salida es menos clara porque hay más
posibilidades de visualización: ¿Se quieren comas o no? ¿Se quieren imprimir las
llaves? ¿Se deben mostrar todos los campos? Debido a esta ambigüedad, Rust no
intenta adivinar lo que queremos, y las estructuras no tienen una implementación
proporcionada de Displaypara usar con println!y el {}marcador de posición.

Si continuamos leyendo los errores, encontraremos esta nota útil:

= help: the trait `std::fmt::Display` is not implemented for `Rectangle`


= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-
print) instead

¡Probémoslo! La println!llamada a la macro ahora se verá así println!("rect1 is


{rect1:?}");. Al colocar el especificador :?entre llaves, indicamos println!que
queremos usar un formato de salida llamado Debug. Este Debugatributo nos
permite imprimir nuestra estructura de forma útil para los desarrolladores, de
modo que podamos ver su valor mientras depuramos el código.

Compila el código con este cambio. ¡Rayos! Seguimos viendo un error:

error[E0277]: `Rectangle` doesn't implement `Debug`

Pero una vez más, el compilador nos da una nota útil:

= help: the trait `Debug` is not implemented for `Rectangle`


= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for
Rectangle`

Rust incluye la función de imprimir información de depuración, pero debemos


habilitarla explícitamente para que esté disponible en nuestra estructura. Para
ello, añadimos el atributo externo #[derive(Debug)]justo antes de la definición de
la estructura, como se muestra en el Listado 5-12.
Rust
jueves, 22 de mayo de 2025 : Página 125 de 719
Nombre de archivo: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

println!("rect1 is {rect1:?}");
}
Listado 5-12: Agregar el atributo para derivar el Debugrasgo e imprimir
la Rectangleinstancia usando formato de depuración

Ahora, cuando ejecutamos el programa, no obtendremos ningún error y veremos


el siguiente resultado:

$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

¡Genial! No es la salida más atractiva, pero muestra los valores de todos los
campos de esta instancia, lo que sin duda sería útil durante la depuración.
Cuando tenemos estructuras más grandes, es útil tener una salida más legible; en
esos casos, podemos usar ` {:#?}en lugar de` {:?}en la println!cadena. En este
ejemplo, usar el {:#?}estilo generará lo siguiente:

$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}

Otra forma de imprimir un valor usando el Debugformato es usar la dbg! macro ,


que toma propiedad de una expresión (a diferencia de println!, que toma una
referencia), imprime el archivo y el número de línea de donde dbg!ocurre esa
Rust
jueves, 22 de mayo de 2025 : Página 126 de 719

llamada de macro en su código junto con el valor resultante de esa expresión y


devuelve la propiedad del valor.

Nota: Al llamar a la dbg!macro, se imprime en el flujo de consola de error


estándar ( stderr), a diferencia de println!, que se imprime en el flujo de consola
de salida estándar ( stdout). Hablaremos más sobre stderry stdouten la sección
"Escribir mensajes de error en el error estándar en lugar de la salida estándar" del
capítulo 12 .

A continuación se muestra un ejemplo en el que nos interesa el valor que se


asigna al widthcampo, así como el valor de toda la estructura 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);
}

Podemos usar dbg!la expresión 30 * scaley, como dbg! devuelve la propiedad de


su valor, el widthcampo obtendrá el mismo valor que si no se hubiera realizado
la dbg!llamada. No queremos dbg!tomar la propiedad de rect1, así que usamos
una referencia a rect1en la siguiente llamada. Así es como se ve el resultado de
este ejemplo:

$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
Rust
jueves, 22 de mayo de 2025 : Página 127 de 719

Podemos ver que el primer bit de salida proviene de la línea 10 de


src/main.rs, donde estamos depurando la expresión 30 * scale, y su valor
resultante es 60(el Debugformato implementado para enteros es imprimir solo su
valor). La dbg!llamada en la línea 14 de src/main.rs genera el valor de &rect1, que
es la Rectangleestructura. Esta salida usa el Debugformato de Rectangletipo.
¡La dbg!macro puede ser muy útil para entender qué hace tu código!

Además del Debugrasgo, Rust proporciona varios rasgos que podemos usar con
el deriveatributo y que pueden añadir un comportamiento útil a nuestros tipos
personalizados. Estos rasgos y sus comportamientos se listan en el Apéndice
C. En el Capítulo 10, explicaremos cómo implementar estos rasgos con
comportamiento personalizado y cómo crear sus propios rasgos. También existen
muchos otros atributos derive; para más información, consulte la sección
"Atributos" de la Referencia de Rust .

Nuestra areafunción es muy específica: solo calcula el área de rectángulos. Sería


útil vincular este comportamiento más estrechamente con
nuestra Rectangleestructura, ya que no funcionará con ningún otro tipo. Veamos
cómo podemos seguir refactorizando este código convirtiendo la areafunción en
un area método definido en nuestro Rectangletipo.

Sintaxis del método

Los métodos son similares a las funciones: se declaran con la fnpalabra clave y un
nombre, pueden tener parámetros y un valor de retorno, y contienen código que
se ejecuta cuando se invoca el método desde otro lugar. A diferencia de las
funciones, los métodos se definen dentro del contexto de una estructura (o una
enumeración o un objeto de rasgo, que abordamos en los capítulos 6 y 17 ,
respectivamente), y su primer parámetro es siempre self, que representa la
instancia de la estructura en la que se invoca el método.

Definición de métodos

Cambiemos la areafunción que tiene una Rectangleinstancia como parámetro y


en su lugar creemos un areamétodo definido en la Rectangleestructura, como se
muestra en el Listado 5-13.

Nombre de archivo: src/main.rs


#[derive(Debug)]
struct Rectangle {
Rust
jueves, 22 de mayo de 2025 : Página 128 de 719
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()
);
}
Listado 5-13: Definición de un areamétodo en la Rectangleestructura

Para definir la función en el contexto de Rectangle, iniciamos un impl bloque (de


implementación) para Rectangle. Todo dentro de este implbloque se asociará con
el Rectangletipo. Luego, movemos la areafunción dentro de las implllaves y
cambiamos el primer (y en este caso, el único) parámetro para que esté selfen la
firma y en todo el cuerpo. En main, donde llamamos a la areafunción y la
pasamos rect1como argumento, podemos usar la sintaxis de método para llamar
al areamétodo en nuestra Rectangle instancia. La sintaxis de método va después
de una instancia: añadimos un punto seguido del nombre del método, los
paréntesis y los argumentos.

En la firma de area, usamos &selfen lugar de rectangle: &Rectangle. The &selfes


la abreviatura de self: &Self. Dentro de un implbloque, el tipo Selfes un alias del
tipo al que implpertenece el bloque. Los métodos deben tener un parámetro
llamado " selfof type" Selfcomo primer parámetro, por lo que Rust permite
abreviarlo con solo el nombre selfen el primer parámetro. Tenga en cuenta que
aún necesitamos usar "the" &antes de la selfabreviatura para indicar que este
método toma prestada la Selfinstancia, tal como hicimos en rectangle:
&Rectangle. Los métodos pueden tomar posesión de self, tomar
prestada self inmutablemente, como hicimos aquí, o tomar
prestada selfmutablemente, al igual que cualquier otro parámetro.
Rust
jueves, 22 de mayo de 2025 : Página 129 de 719

Elegimos &selfesta opción por la misma razón que &Rectangleen la versión de


función: no queremos tomar posesión de la instancia y solo leer los datos de la
estructura, no escribir en ella. Si quisiéramos cambiar la instancia en la que
hemos llamado al método como parte de su función, usaríamos ``` &mut
selfcomo primer parámetro. Es poco común que un método tome posesión de la
instancia usando ``` selfcomo primer parámetro; esta técnica se suele usar
cuando el método se transforma selfen otra cosa y se desea evitar que quien lo
llama use la instancia original después de la transformación.

La razón principal para usar métodos en lugar de funciones, además de


proporcionar la sintaxis de método y evitar la repetición del tipo de selfen la firma
de cada método, es la organización. Hemos agrupado todas las funciones que
podemos realizar con una instancia de un tipo en un solo implbloque, en lugar de
obligar a los futuros usuarios de nuestro código a buscar las funciones
de Rectangleen diversas partes de la biblioteca que ofrecemos.

Tenga en cuenta que podemos asignar a un método el mismo nombre que uno de
los campos de la estructura. Por ejemplo, podemos definir un método
en Rectangleque también se llame width:

Nombre de archivo: src/main.rs


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í, elegimos que el widthmétodo devuelva truesi el valor del widthcampo de la


instancia es mayor que 0y falsesi el valor es 0: podemos usar un campo dentro de
un método con el mismo nombre para cualquier propósito. En main, cuando se
usan rect1.widthparéntesis después, Rust sabe que nos referimos al
Rust
jueves, 22 de mayo de 2025 : Página 130 de 719

método width. Cuando no se usan paréntesis, Rust sabe que nos referimos al
campo width.

A menudo, pero no siempre, cuando asignamos a un método el mismo nombre


que a un campo, queremos que solo devuelva el valor del campo y no haga nada
más. Este tipo de métodos se denominan getters , y Rust no los implementa
automáticamente para campos de estructura como otros lenguajes. Los getters
son útiles porque permiten hacer que el campo sea privado, pero el método
público, y así habilitar el acceso de solo lectura a ese campo como parte de la API
pública del tipo. En el capítulo 7 , analizaremos qué son "público" y "privado" y
cómo designar un campo o método como público o privado .

¿Dónde está el ->operador?

En C y C++, se utilizan dos operadores diferentes para llamar a métodos: se


usa .si se llama a un método directamente en el objeto y ->si se llama al método
en un puntero al objeto y se necesita desreferenciar primero el puntero. En otras
palabras, si objectes un puntero, object->something()es similar
a (*object).something().

Rust no tiene un equivalente al ->operador; en su lugar, cuenta con una función


llamada referencia y desreferenciación automáticas . La llamada a métodos es
uno de los pocos casos en Rust con este comportamiento.

Así funciona: al llamar a un método con object.something(), Rust añade


automáticamente &, &mut, o *que objectcoincida con la firma del método. En
otras palabras, los siguientes son iguales:

p1.distance(&p2);
(&p1).distance(&p2);

El primero se ve mucho más limpio. Este comportamiento de referencia


automática funciona porque los métodos tienen un receptor claro: el tipo de self.
Dado el receptor y el nombre de un método, Rust puede determinar con certeza si
el método está leyendo ( &self), mutando ( &mut self) o consumiendo ( self). El
hecho de que Rust haga implícito el préstamo para los receptores de métodos
contribuye en gran medida a que la propiedad sea ergonómica en la práctica.

Métodos con más parámetros


Rust
jueves, 22 de mayo de 2025 : Página 131 de 719

Practiquemos el uso de métodos implementando un segundo método en


la Rectangle estructura. En esta ocasión, queremos que una instancia
de Rectangletome otra instancia de Rectangley devuelva truesi la
segunda Rectanglecabe completamente dentro de selfla primera Rectangle; de lo
contrario, debería devolver false. Es decir, una vez definido el can_holdmétodo,
queremos poder escribir el programa que se muestra en el Listado 5-14.

Nombre de archivo: 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,
};

println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));


println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
can_holdListado 5-14: Utilizando el método aún no escrito

El resultado esperado se vería así porque ambas dimensiones de rect2son más


pequeñas que las dimensiones de rect1, pero rect3son más anchas que rect1:

Can rect1 hold rect2? true


Can rect1 hold rect3? false

Sabemos que queremos definir un método, por lo que estará dentro del impl
Rectangle bloque. El nombre del método será can_holdy tomará un préstamo
inmutable de otro Rectanglecomo parámetro. Podemos saber cuál será el tipo del
parámetro mirando el código que llama al
método: rect1.can_hold(&rect2)pasa &rect2, que es un préstamo inmutable
a rect2, una instancia de Rectangle. Esto tiene sentido porque solo necesitamos
leer rect2(en lugar de escribir, lo que significaría que necesitaríamos un préstamo
mutable), y queremos mainconservar la propiedad de rect2para que podamos
usarlo de nuevo después de llamar al can_holdmétodo. El valor de retorno
de can_holdserá un booleano, y la implementación comprobará si el ancho y la
Rust
jueves, 22 de mayo de 2025 : Página 132 de 719

altura de selfson mayores que el ancho y la altura del otro Rectangle,


respectivamente. Agreguemos el nuevo can_holdmétodo al implbloque del Listado
5-13, que se muestra en el Listado 5-15.

Nombre de archivo: src/main.rs


impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}

fn can_hold(&self, other: &Rectangle) -> bool {


self.width > other.width && self.height > other.height
}
}
Listado 5-15: Implementación del can_holdmétodo Rectangleque toma
otra Rectangleinstancia como parámetro

Al ejecutar este código con la mainfunción del Listado 5-14, obtendremos el


resultado deseado. Los métodos pueden aceptar múltiples parámetros que
añadimos a la firma después del selfparámetro, y estos parámetros funcionan
igual que los parámetros de las funciones.

Funciones asociadas

Todas las funciones definidas dentro de un implbloque se denominan funciones


asociadas porque están asociadas al tipo que lleva el nombre de impl. Podemos
definir funciones asociadas que no tengan selfcomo primer parámetro (y, por lo
tanto, no sean métodos) porque no necesitan una instancia del tipo para
funcionar. Ya hemos usado una función como esta: la String::fromfunción definida
en el Stringtipo.

Las funciones asociadas que no son métodos se suelen usar para constructores
que devuelven una nueva instancia de la estructura. Suelen llamarse new,
pero newno es un nombre especial ni está integrado en el lenguaje. Por ejemplo,
podríamos proporcionar una función asociada llamada squareque tuviera un
parámetro de dimensión y lo usara como ancho y alto, facilitando así la creación
de un cuadrado Rectangleen lugar de tener que especificar el mismo valor dos
veces:

Nombre de archivo: src/main.rs

impl Rectangle {
Rust
jueves, 22 de mayo de 2025 : Página 133 de 719
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}

Las Selfpalabras clave en el tipo de retorno y en el cuerpo de la función son alias


para el tipo que aparece después de la implpalabra clave, que en este caso
es Rectangle.

Para llamar a esta función asociada, usamos la ::sintaxis con el nombre de la


estructura; let sq = Rectangle::square(3);es un ejemplo. Esta función tiene un
espacio de nombres definido por la estructura: la ::sintaxis se usa tanto para
funciones asociadas como para espacios de nombres creados por módulos.
Analizaremos los módulos en el Capítulo 7 .

implBloques múltiples

Cada estructura puede tener varios implbloques. Por ejemplo, el Listado 5-15 es
equivalente al código del Listado 5-16, que tiene cada método en su
propio implbloque.

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}

impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Listado 5-16: Reescritura del Listado 5-15 utilizando múltiples implbloques

No hay razón para separar estos métodos en varios implbloques, pero esta
sintaxis es válida. Veremos un caso en el que varios implbloques son útiles en el
Capítulo 10, donde analizamos los tipos y rasgos genéricos.

Resumen
Rust
jueves, 22 de mayo de 2025 : Página 134 de 719

Las estructuras permiten crear tipos personalizados relevantes para el dominio.


Mediante el uso de estructuras, se pueden conectar los datos asociados y
nombrar cada uno para que el código sea claro. En implbloques, se pueden definir
funciones asociadas a un tipo, y los métodos son un tipo de función asociada que
permite especificar el comportamiento de las instancias de las estructuras.

Pero las estructuras no son la única forma de crear tipos personalizados:


recurramos a la función de enumeración de Rust para agregar otra herramienta a
su caja de herramientas.

Enumeraciones y coincidencia de patrones

En este capítulo, analizaremos las enumeraciones , también conocidas


como enums . Las enumeraciones permiten definir un tipo enumerando sus
posibles variantes . Primero, definiremos y usaremos una enumeración para
mostrar cómo puede codificar significado junto con datos. A continuación,
exploraremos una enumeración particularmente útil, llamada Option, que expresa
que un valor puede ser algo o nada. Después, veremos cómo la coincidencia de
patrones en la matchexpresión facilita la ejecución de código diferente para
distintos valores de una enumeración. Finalmente, explicaremos cómo la if
let construcción es otra expresión práctica y concisa disponible para gestionar
enumeraciones en el código.

Definición de una enumeración

Mientras que las estructuras permiten agrupar campos y datos relacionados,


como "a" Rectanglecon sus widthvalores y height, las enumeraciones permiten
indicar que un valor pertenece a un conjunto posible de valores. Por ejemplo,
podríamos indicar que Rectanglepertenece a un conjunto de posibles formas que
también incluye Circlevalores y Triangle. Para ello, Rust permite codificar estas
posibilidades como una enumeración.

Analicemos una situación que podríamos querer expresar en código y veamos por
qué las enumeraciones son útiles y más apropiadas que las estructuras en este
caso. Supongamos que necesitamos trabajar con direcciones IP. Actualmente, se
utilizan dos estándares principales para direcciones IP: la versión cuatro y la
versión seis. Dado que estas son las únicas posibilidades de dirección IP que
nuestro programa encontrará, podemos enumerar todas las variantes posibles, de
ahí el nombre de enumeración.
Rust
jueves, 22 de mayo de 2025 : Página 135 de 719

Cualquier dirección IP puede ser de la versión cuatro o de la versión seis, pero no


ambas a la vez. Esta propiedad de las direcciones IP hace que la estructura de
datos de enumeración sea apropiada, ya que un valor de enumeración solo puede
ser una de sus variantes. Tanto las direcciones de la versión cuatro como de la
versión seis siguen siendo fundamentalmente direcciones IP, por lo que deben
tratarse como del mismo tipo cuando el código gestiona situaciones aplicables a
cualquier tipo de dirección IP.

Podemos expresar este concepto en código definiendo


una IpAddrKindenumeración y listando los posibles tipos de direcciones
IP, V4y V6. Estas son las variantes de la enumeración:

enum IpAddrKind {
V4,
V6,
}

IpAddrKindahora es un tipo de datos personalizado que podemos usar en otras


partes de nuestro código.

Valores de enumeración

Podemos crear instancias de cada una de las dos variantes de IpAddrKindla


siguiente manera:

let four = IpAddrKind::V4;


let six = IpAddrKind::V6;

Tenga en cuenta que las variantes de la enumeración se dividen en dos espacios


de nombres bajo su identificador, y se separan con dos puntos. Esto es útil porque
ahora ambos valores IpAddrKind::V4y IpAddrKind::V6son del mismo
tipo: IpAddrKind. Podemos, por ejemplo, definir una función que tome
cualquier IpAddrKind:

fn route(ip_kind: IpAddrKind) {}

Y podemos llamar a esta función con cualquiera de las variantes:

route(IpAddrKind::V4);
route(IpAddrKind::V6);
Rust
jueves, 22 de mayo de 2025 : Página 136 de 719

Usar enumeraciones ofrece aún más ventajas. Si nos centramos en el tipo de


dirección IP, actualmente no tenemos forma de almacenar los datos de la
dirección IP ; solo sabemos de qué tipo es. Dado que acaba de aprender sobre las
estructuras en el Capítulo 5, podría interesarle abordar este problema con
estructuras, como se muestra en el Listado 6-1.

enum IpAddrKind {
V4,
V6,
}

struct IpAddr {
kind: IpAddrKind,
address: String,
}

let home = IpAddr {


kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};

let loopback = IpAddr {


kind: IpAddrKind::V6,
address: String::from("::1"),
};
Listado 6-1: Almacenamiento de los datos y IpAddrKindla variante de una
dirección IP mediante unstruct

Aquí, hemos definido una estructura IpAddrcon dos campos: un kindcampo de


tipo IpAddrKind(la enumeración que definimos previamente) y un addresscampo
de tipo String. Tenemos dos instancias de esta estructura. La primera es home, y
tiene el valor ` IpAddrKind::V4its` kindcon la dirección asociada ` 127.0.0.1`. La
segunda instancia es loopback, y tiene la otra variante de `` IpAddrKindcomo
su kindvalor, V6``, y tiene la dirección ::1 asociada. Hemos usado una estructura
para agrupar los valores `` kindy `` address , por lo que ahora la variante está
asociada al valor.

Sin embargo, representar el mismo concepto usando solo una enumeración es


más conciso: en lugar de una enumeración dentro de una estructura, podemos
introducir datos directamente en cada variante de la enumeración. Esta nueva
definición de la IpAddrenumeración indica que tanto las variantes V4como V6 las
variantes tendrán Stringvalores asociados:
Rust
jueves, 22 de mayo de 2025 : Página 137 de 719
enum IpAddr {
V4(String),
V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

Adjuntamos datos directamente a cada variante de la enumeración, por lo que no


se necesita una estructura adicional. Aquí también es más fácil ver otro detalle
del funcionamiento de las enumeraciones: el nombre de cada variante de
enumeración que definimos se convierte en una función que construye una
instancia de la enumeración. Es decir, IpAddr::V4()es una llamada a una función
que toma un Stringargumento y devuelve una instancia del IpAddrtipo. Esta
función constructora se define automáticamente al definir la enumeración.

Existe otra ventaja de usar una enumeración en lugar de una estructura: cada
variante puede tener diferentes tipos y cantidades de datos asociados. Las
direcciones IP de la versión cuatro siempre tendrán cuatro componentes
numéricos con valores entre 0 y 255. Si quisiéramos almacenar V4direcciones
como cuatro u8valores, pero expresarlas V6como un solo Stringvalor, no
podríamos hacerlo con una estructura. Las enumeraciones gestionan este caso
fácilmente:

enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

Hemos mostrado varias maneras de definir estructuras de datos para almacenar


direcciones IP de las versiones cuatro y seis. Sin embargo, resulta que almacenar
direcciones IP y codificarlas es tan común que la biblioteca estándar tiene una
definición que podemos usar. Veamos cómo la biblioteca estándar define
[<sub>(<sub>) IpAddr</sub>]: contiene la enumeración y las variantes exactas
que hemos definido y usado, pero integra los datos de las direcciones dentro de
las variantes en forma de dos estructuras diferentes, que se definen de forma
distinta para cada variante:
Rust
jueves, 22 de mayo de 2025 : Página 138 de 719
struct Ipv4Addr {
// --snip--
}

struct Ipv6Addr {
// --snip--
}

enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}

Este código ilustra que se puede incluir cualquier tipo de dato dentro de una
variante de enumeración: cadenas, tipos numéricos o estructuras, por ejemplo.
¡Incluso se puede incluir otra enumeración! Además, los tipos de la biblioteca
estándar no suelen ser mucho más complejos que los que se podrían crear.

Tenga en cuenta que, aunque la biblioteca estándar contiene una definición


para IpAddr, podemos crear y usar nuestra propia definición sin conflicto, ya que
no hemos incluido la definición de la biblioteca estándar en nuestro ámbito.
Hablaremos más sobre cómo incluir tipos en el ámbito en el capítulo 7.

Veamos otro ejemplo de una enumeración en el Listado 6-2: este tiene una
amplia variedad de tipos incorporados en sus variantes.

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Listado 6-2: Una Messageenumeración cuyas variantes almacenan diferentes
cantidades y tipos de valores

Esta enumeración tiene cuatro variantes con diferentes tipos:

 QuitNo tiene ningún dato asociado.


 Movetiene campos nombrados, como lo hace una estructura.
 WriteIncluye un solo String.
 ChangeColorIncluye tres i32valores.
Rust
jueves, 22 de mayo de 2025 : Página 139 de 719

Definir una enumeración con variantes como las del Listado 6-2 es similar a
definir diferentes tipos de definiciones de estructura, salvo que la enumeración no
utiliza la structpalabra clave y todas las variantes se agrupan bajo
el Message tipo. Las siguientes estructuras podrían contener los mismos datos
que las variantes de enumeración anteriores:

struct QuitMessage; // unit struct


struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct

Pero si usáramos diferentes estructuras, cada una de las cuales tiene su propio
tipo, no podríamos definir tan fácilmente una función para tomar cualquiera de
estos tipos de mensajes como podríamos hacerlo con la Messageenumeración
definida en el Listado 6-2, que es de un solo tipo.

Existe otra similitud entre las enumeraciones y las estructuras: así como podemos
definir métodos en estructuras usando impl, también podemos definir métodos en
enumeraciones. Aquí hay un método llamado callque podríamos definir en
nuestra Messageenumeración:

impl Message {
fn call(&self) {
// method body would be defined here
}
}

let m = Message::Write(String::from("hello"));
m.call();

El cuerpo del método se usaría selfpara obtener el valor al que se invocó. En este
ejemplo, hemos creado una variable mcon el
valor Message::Write(String::from("hello")), que será el que selfse incluirá en el
cuerpo del callmétodo al m.call()ejecutarse.

Veamos otra enumeración en la biblioteca estándar que es muy común y


útil: Option.

La Optionenumeración y sus ventajas sobre los valores nulos


Rust
jueves, 22 de mayo de 2025 : Página 140 de 719

Esta sección explora un caso práctico de Option, otra enumeración definida por la
biblioteca estándar. El Optiontipo codifica el escenario muy común en el que un
valor puede ser algo o nada.

Por ejemplo, si solicita el primer elemento de una lista no vacía, obtendrá un


valor. Si solicita el primer elemento de una lista vacía, no obtendrá nada.
Expresar este concepto en términos del sistema de tipos significa que el
compilador puede comprobar si ha gestionado todos los casos que debería
gestionar; esta funcionalidad puede evitar errores muy comunes en otros
lenguajes de programación.

El diseño de un lenguaje de programación suele pensarse en función de las


características que se incluyen, pero las que se excluyen también son
importantes. Rust no cuenta con la característica nula que muchos otros
lenguajes poseen. Nulo significa que no hay ningún valor. En lenguajes con
valores nulos, las variables siempre pueden estar en uno de dos estados: nulo o
no nulo.

En su presentación de 2009 “Referencias nulas: el error de mil millones de


dólares”, Tony Hoare, el inventor de la referencia nula, dice lo siguiente:

Lo llamo mi error de mil millones de dólares. En aquel entonces, estaba diseñando


el primer sistema de tipos completo para referencias en un lenguaje orientado a
objetos. Mi objetivo era garantizar que todo uso de referencias fuera
absolutamente seguro, con comprobación automática realizada por el compilador.
Pero no pude resistir la tentación de incluir una referencia nula, simplemente por
su facilidad de implementación. Esto ha provocado innumerables errores,
vulnerabilidades y fallos del sistema, que probablemente han causado miles de
millones de dólares en daños y perjuicios en los últimos cuarenta años.

El problema con los valores nulos es que, si intentas usar un valor nulo como un
valor no nulo, obtendrás algún tipo de error. Dado que esta propiedad de nulo o
no nulo es omnipresente, es extremadamente fácil cometer este tipo de error.

Sin embargo, el concepto que null intenta expresar sigue siendo útil: un null es un
valor que actualmente no es válido o está ausente por alguna razón.

El problema no radica realmente en el concepto, sino en la implementación


específica. Por lo tanto, Rust no tiene valores nulos, pero sí cuenta con una
Rust
jueves, 22 de mayo de 2025 : Página 141 de 719

enumeración que puede codificar la presencia o ausencia de un valor. Esta


enumeración es y la biblioteca estándarOption<T> la define de la siguiente
manera:

enum Option<T> {
None,
Some(T),
}

La Option<T>enumeración es tan útil que incluso se incluye en el preludio; no es


necesario incluirla explícitamente en el ámbito. Sus variantes también se incluyen
en el preludio: se puede usar " Somey" Nonedirectamente sin el Option:: prefijo.
La Option<T>enumeración sigue siendo una enumeración normal, y
" Some(T)y" Nonesiguen siendo variantes del tipo.Option<T> .

La <T>sintaxis es una característica de Rust que aún no hemos abordado. Se


trata de un parámetro de tipo genérico, y los abordaremos con más detalle en el
Capítulo 10. Por ahora, solo necesita saber que esto <T>significa que
la Somevariante de la Optionenumeración puede contener un dato de cualquier
tipo, y que cada tipo concreto que se usa en su lugar Tconvierte el tipo
general Option<T>en un tipo diferente. Aquí hay algunos ejemplos del uso
de Optionvalores para contener tipos numéricos y char:

let some_number = Some(5);


let some_char = Some('e');

let absent_number: Option<i32> = None;

El tipo de some_numberes Option<i32>. El tipo de some_chares Option<char>,


que es un tipo diferente. Rust puede inferir estos tipos porque hemos especificado
un valor dentro de la Somevariante. Para absent_number, Rust requiere que
anotemos el Optiontipo general: el compilador no puede inferir el tipo
que Somecontendrá la variante correspondiente observando solo un Nonevalor.
Aquí, le indicamos a Rust que queremos que for absent_numbersea del
tipo Option<i32> .

Cuando tenemos un Somevalor, sabemos que existe y que se guarda en el


[nombre del archivo Some]. Tener un Nonevalor, en cierto sentido, significa lo
mismo que tener un valor nulo: no tenemos un valor válido. Entonces, ¿por qué
es Option<T> mejor tener un valor que tener un valor nulo?
Rust
jueves, 22 de mayo de 2025 : Página 142 de 719

En resumen, dado que Option<T>y T(donde Tpuede ser cualquier tipo) son tipos
diferentes, el compilador no nos permitirá usar un Option<T>valor como si fuera
definitivamente válido. Por ejemplo, este código no compila porque intenta
agregar un i8a un Option<i8>:

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

Si ejecutamos este código, obtenemos un mensaje de error como este:

$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5| let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
`&'a i8` implements `Add<i8>`
`&i8` implements `Add<&i8>`
`i8` implements `Add<&i8>`
`i8` implements `Add`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error

¡Intenso! En efecto, este mensaje de error significa que Rust no entiende cómo
sumar an i8y an Option<i8>, ya que son de tipos diferentes. Cuando tenemos un
valor de un tipo, como i8en Rust, el compilador se asegurará de que siempre
tengamos un valor válido. Podemos proceder con confianza sin tener que
comprobar si es nulo antes de usar ese valor. Solo cuando tenemos
an Option<i8>(o cualquier otro tipo de valor con el que estemos trabajando)
debemos preocuparnos por la posibilidad de no tener un valor, y el compilador se
asegurará de que gestionemos ese caso antes de usarlo.

En otras palabras, debes convertir un valor Option<T>en un valor Tantes de


poder realizar Toperaciones con él. Generalmente, esto ayuda a detectar uno de
Rust
jueves, 22 de mayo de 2025 : Página 143 de 719

los problemas más comunes con null: asumir que algo no es nulo cuando en
realidad lo es.

Eliminar el riesgo de asumir incorrectamente un valor distinto de nulo te ayuda a


tener más confianza en tu código. Para tener un valor que pueda ser nulo, debes
aceptarlo explícitamente estableciendo el tipo de ese valor como Option<T>.
Luego, al usar ese valor, debes gestionar explícitamente el caso en que el valor
sea nulo. Siempre que un valor tenga un tipo distinto
de Option<T>, puedes asumir con seguridad que no es nulo. Esta fue una
decisión de diseño deliberada de Rust para limitar la omnipresencia de los valores
nulos y aumentar la seguridad del código de Rust.

Entonces, ¿cómo se obtiene el Tvalor de una Somevariante cuando se tiene un


valor de tipo Option<T>para poder usarlo? La Option<T>enumeración tiene una
gran cantidad de métodos útiles en diversas situaciones; puedes consultarlos
en su documentación . Familiarizarse con los métodos de [enlace faltante]
te Option<T>será extremadamente útil en tu experiencia con Rust.

En general, para usar un Option<T>valor, se necesita código que gestione cada


variante. Se necesita código que se ejecute solo cuando se tenga un Some(T)valor
y este código pueda usar el valor interno T. Se necesita otro código que se
ejecute solo si se tiene un Nonevalor y este código no tiene ningún Tvalor
disponible. La matchexpresión es una construcción de flujo de control que hace
precisamente esto cuando se usa con enumeraciones: ejecutará código diferente
según la variante de la enumeración que tenga, y ese código puede usar los datos
dentro del valor coincidente.

La matchconstrucción del flujo de control

Rust cuenta con una construcción de flujo de control extremadamente potente


llamada "<sup>" matchque permite comparar un valor con una serie de patrones
y luego ejecutar código según el patrón que coincida. Los patrones pueden estar
compuestos por valores literales, nombres de variables, comodines y muchos
otros elementos. El capítulo 19 abarca todos los tipos de patrones y sus
funciones. Su potencia matchreside en la expresividad de los patrones y en que el
compilador confirma que se manejan todos los casos posibles.

Piense en una matchexpresión como si fuera una máquina clasificadora de


monedas: las monedas se deslizan por una pista con agujeros de distintos
Rust
jueves, 22 de mayo de 2025 : Página 144 de 719

tamaños, y cada moneda cae por el primer agujero que encuentra en el que
encaja. De la misma manera, los valores pasan por cada patrón en unmatch , y en
el primer patrón en el que el valor "encaja", este cae en el bloque de código
asociado que se usará durante la ejecución.

Hablando de monedas, tomémoslas como ejemplo usando match!. Podemos


escribir una función que tome una moneda estadounidense desconocida y, de
forma similar a la máquina contadora, determine qué moneda es y devuelva su
valor en centavos, como se muestra en el Listado 6-3.

enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {


match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
Listado 6-3: Una enumeración y una matchexpresión que tiene las variantes de la
enumeración como sus patrones

Analicemos la matchfunción value_in_cents. Primero, listamos la matchpalabra


clave seguida de una expresión, que en este caso es el valor coin. Esto parece
muy similar a una expresión condicional usada con if, pero hay una gran
diferencia: con if, la condición debe evaluarse como un valor booleano, pero aquí
puede ser de cualquier tipo. El tipo de coinen este ejemplo es .Coin enumeración
que definimos en la primera línea.

A continuación están los matchbrazos. Un brazo consta de dos partes: un patrón y


código. El primer brazo tiene un patrón que representa el valor Coin::Pennyy
luego el => operador que separa el patrón del código a ejecutar. En este caso, el
código es simplemente el valor.1 . Cada brazo se separa del siguiente con una
coma.
Rust
jueves, 22 de mayo de 2025 : Página 145 de 719

Cuando la matchexpresión se ejecuta, compara el valor resultante con el patrón


de cada brazo, en orden. Si un patrón coincide con el valor, se ejecuta el código
asociado a ese patrón. Si ese patrón no coincide con el valor, la ejecución
continúa con el siguiente brazo, como en una máquina clasificadora de monedas.
Podemos tener tantos brazos como necesitemos: en el Listado 6-3,
nuestromatch tiene cuatro brazos.

El código asociado con cada brazo es una expresión, y el valor resultante de la


expresión en el brazo coincidente es el valor que se devuelve para
la matchexpresión completa.

Normalmente no usamos llaves si el código del brazo coincidente es corto, como


en el Listado 6-3, donde cada brazo simplemente devuelve un valor. Si desea
ejecutar varias líneas de código en un brazo coincidente, debe usar llaves, y la
coma después del brazo es opcional. Por ejemplo, el siguiente código imprime
"¡Centavo de la suerte!" cada vez que se llama al método con un Coin::Penny,
pero aún devuelve el último valor del bloque 1:

fn value_in_cents(coin: Coin) -> u8 {


match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

Patrones que se vinculan a los valores

Otra característica útil de los brazos de coincidencia es que pueden enlazarse a


las partes de los valores que coinciden con el patrón. Así es como podemos
extraer valores de las variantes de enumeración.

Por ejemplo, modifiquemos una de nuestras variantes de enumeración para que


contenga datos. Entre 1999 y 2008, Estados Unidos acuñó monedas de 25
centavos con diseños diferentes para cada uno de los 50 estados en una cara.
Ninguna otra moneda tenía diseños de estados, por lo que solo las monedas de
25 centavos tienen este valor adicional. Podemos agregar esta
Rust
jueves, 22 de mayo de 2025 : Página 146 de 719

información enummodificando la Quartervariante para que incluya


un UsStatevalor almacenado, como hicimos en el Listado 6-4.

#[derive(Debug)] // so we can inspect the state in a minute


enum UsState {
Alabama,
Alaska,
// --snip--
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
Listado 6-4: Una Coinenumeración en la que la Quartervariante también contiene
un UsStatevalor

Imaginemos que un amigo intenta coleccionar las 50 monedas de veinticinco


centavos de cada estado. Mientras clasificamos el cambio por tipo de moneda,
también mencionaremos el nombre del estado asociado a cada moneda para que,
si nuestro amigo no la tiene, pueda añadirla a su colección.

En la expresión de coincidencia de este código, añadimos una variable


llamada stateal patrón que coincide con los valores de la variante Coin::Quarter.
Cuando a Coin::Quartercoincide, la statevariable se vincula al valor del estado de
ese trimestre. Luego, podemos usarla stateen el código para ese brazo, así:

fn value_in_cents(coin: Coin) -> u8 {


match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}

Si llamáramos
a value_in_cents(Coin::Quarter(UsState::Alaska)), coin sería Coin::Quarter(UsState
::Alaska). Al comparar ese valor con cada uno de los brazos coincidentes, ninguno
Rust
jueves, 22 de mayo de 2025 : Página 147 de 719

coincide hasta llegar a Coin::Quarter(state). En ese punto, la vinculación


para stateserá el valor UsState::Alaska. Podemos usar esa vinculación en
la println!expresión, obteniendo así el valor del estado interno de la Coinvariante
de enumeración para Quarter.

Coincidencia conOption<T>

En la sección anterior, queríamos obtener el Tvalor interno fuera del Some caso al
usar ` Option<T>`; también podemos manejar Option<T>el uso de ` match`,
como hicimos con la Coinenumeración. En lugar de comparar monedas,
compararemos las variantes de ` Option<T>`, pero el funcionamiento de
la matchexpresión sigue siendo el mismo.

Supongamos que queremos escribir una función que toma un


valor Option<i32>y, si contiene un valor, lo suma en 1. Si no contiene ningún
valor, la función debería devolverlo Noney no intentar realizar ninguna operación.

Esta función es muy fácil de escribir, gracias a match, y se verá como el Listado 6-
5.

fn plus_one(x: Option<i32>) -> Option<i32> {


match x {
None => None,
Some(i) => Some(i + 1),
}
}

let five = Some(5);


let six = plus_one(five);
let none = plus_one(None);
Listado 6-5: Una función que utiliza una matchexpresión en unOption<i32>

Examinemos la primera ejecución de plus_onecon más detalle. Al llamar


a plus_one(five), la variable xen el cuerpo de plus_onetendrá el valor Some(5).
Luego, comparamos esto con cada brazo coincidente:

None => None,

El Some(5)valor no coincide con el patrón None, por lo que continuamos con el


siguiente brazo:

Some(i) => Some(i + 1),


Rust
jueves, 22 de mayo de 2025 : Página 148 de 719

¿ Some(5)Coincide con Some(i)? ¡Sí! Tenemos la misma variante. Se i vincula al


valor contenido en Some, por lo que itoma el valor 5. Se ejecuta el código en el
brazo de coincidencia, por lo que sumamos 1 al valor de iy creamos un
nuevo Somevalor con nuestro total 6.

Consideremos ahora la segunda llamada de plus_oneen el Listado 6-5,


donde xes None. Introducimos matchy comparamos con el primer brazo:

None => None,

¡Coincide! No hay ningún valor que añadir, así que el programa se detiene y
devuelve el Nonevalor a la derecha de =>. Como el primer brazo coincidió, no se
comparan los demás.

La combinación matchde enumeraciones es útil en muchas situaciones. Verás


este patrón con frecuencia en el código de Rust: matchcontra una enumeración,
vincula una variable a los datos que contiene y luego ejecuta código basado en
ella. Al principio es un poco complicado, pero una vez que te acostumbras,
desearás tenerlo en todos los lenguajes. Es un clásico entre los usuarios.

Los partidos son exhaustivos

Hay otro aspecto que matchdebemos discutir: los patrones de los brazos deben
cubrir todas las posibilidades. Considere esta versión de nuestra plus_onefunción,
que tiene un error y no compila:

fn plus_one(x: Option<i32>) -> Option<i32> {


match x {
Some(i) => Some(i + 1),
}
}

No gestionamos el Nonecaso, así que este código provocará un error. Por suerte,
Rust sabe cómo detectarlo. Si intentamos compilarlo, obtendremos este error:

$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
Rust
jueves, 22 de mayo de 2025 : Página 149 de 719
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/
option.rs:571:1
|
571 | pub enum Option<T> {
| ^^^^^^^^^^^^^^^^^^
...
575 | None,
| ---- not covered
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with
a wildcard pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error

Rust sabe que no cubrimos todos los casos posibles, ¡e incluso sabe qué patrón
olvidamos! Las coincidencias en Rust son exhaustivas : debemos agotar hasta la
última posibilidad para que el código sea válido. Especialmente en el caso de
[ nombre del archivo Option<T>], cuando Rust nos impide olvidarnos de manejar
explícitamente el [nombre del archivo]. None caso explícitamente, nos protege de
asumir que tenemos un valor cuando podríamos tener un valor nulo, lo que
imposibilita el error millonario mencionado anteriormente.

Patrones de captura y el _marcador de posición

Usando enumeraciones, también podemos realizar acciones especiales para


algunos valores específicos, pero para todos los demás valores se realiza una
acción predeterminada. Imaginemos que implementamos un juego donde, si se
saca un 3 en una tirada de dados, el jugador no se mueve, sino que obtiene un
nuevo sombrero elegante. Si se saca un 7, el jugador pierde el sombrero
elegante. Para todos los demás valores, el jugador avanza esa cantidad de
espacios en el tablero. Aquí hay un ejemplo matchque implementa esa lógica, con
el resultado de la tirada de dados codificado en lugar de un valor aleatorio, y toda
la lógica restante representada por funciones sin cuerpos, ya que su
implementación real queda fuera del alcance de este ejemplo:
Rust
jueves, 22 de mayo de 2025 : Página 150 de 719
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 los dos primeros brazos, los patrones son los valores literales 3y 7. Para el
último brazo, que abarca todos los demás valores posibles, el patrón es la
variable que hemos llamado other. El código que se ejecuta para el otherbrazo
usa la variable pasándola a la move_playerfunción.

Este código se compila, aunque no hemos enumerado todos los valores posibles
que u8puede tener a, ya que el último patrón coincidirá con todos los valores no
especificados. Este patrón general cumple con el requisito de matchser
exhaustivo. Tenga en cuenta que debemos colocar el brazo general al final, ya
que los patrones se evalúan en orden. Si colocamos el brazo general antes, los
demás brazos nunca se ejecutarían, por lo que Rust nos avisará si añadimos
brazos después de un patrón general.

Rust también tiene un patrón que podemos usar cuando queremos un catch-all,
pero no queremos usar el valor que contiene: _es un patrón especial que coincide
con cualquier valor y no se vincula a él. Esto le indica a Rust que no usaremos el
valor, por lo que Rust no nos avisará sobre una variable no utilizada.

Cambiemos las reglas del juego: ahora, si sacas un número distinto a 3 o 7, debes
volver a tirar. Ya no necesitamos usar el valor general, así que podemos cambiar
nuestro código para usar _en lugar de la variable llamada other:

let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
Rust
jueves, 22 de mayo de 2025 : Página 151 de 719

Este ejemplo también cumple con el requisito de exhaustividad porque ignoramos


explícitamente todos los demás valores en el último brazo; no hemos olvidado
nada.

Finalmente, cambiaremos las reglas del juego una vez más para que no ocurra
nada más en tu turno si obtienes cualquier número que no sea un 3 o un 7.
Podemos expresarlo usando el valor de la unidad (el tipo de tupla vacía que
mencionamos en la sección "El tipo de tupla" ) como el código que va con
el _brazo:

let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}

Aquí, le decimos a Rust explícitamente que no vamos a utilizar ningún otro valor
que no coincida con un patrón en un brazo anterior y no queremos ejecutar
ningún código en este caso.

En el capítulo 19 abordaremos más sobre patrones y correspondencias . Por


ahora, nos centraremos en la if letsintaxis, que puede ser útil en situaciones
donde la matchexpresión es un poco extensa.

Flujo de control conciso con if letylet else

La if letsintaxis permite combinar ify letde una forma más sencilla para gestionar
valores que coinciden con un patrón e ignorar los demás. Considere el programa
del Listado 6-6 que coincide con un Option<u8>valor en la config_maxvariable,
pero solo desea ejecutar código si el valor es la Some variante.

let config_max = Some(3u8);


match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
Listado 6-6: A matchque solo le importa ejecutar código cuando el valor esSome
Rust
jueves, 22 de mayo de 2025 : Página 152 de 719

Si el valor es Some, lo imprimimos en la Somevariante vinculándolo a la


variable maxdel patrón. No queremos modificar el Nonevalor. Para satisfacer
la matchexpresión, debemos agregar _ => ()después de procesar solo una
variante, lo cual implica un código repetitivo muy molesto.

En lugar de eso, podríamos escribir esto de forma más breve usando if let. El
siguiente código se comporta igual que el matchdel Listado 6-6:

let config_max = Some(3u8);


if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}

La sintaxis if lettoma un patrón y una expresión separados por un signo igual.


Funciona de forma similar a un match, donde la expresión se asigna a matchy el
patrón es su primer brazo. En este caso, el patrón es Some(max)y maxse enlaza
al valor dentro de Some. Podemos usar entonces maxen el cuerpo del if letbloque
de la misma manera que maxen el matchbrazo correspondiente. El código del if
letbloque solo se ejecuta si el valor coincide con el patrón.

Usarlo if letimplica menos escritura, menos sangría y menos código repetitivo. Sin
embargo, se pierde la comprobación exhaustiva que matchimpone. Elegir
entre matchyif let depende de lo que esté haciendo en su situación particular y de
si ganar concisión es una compensación adecuada por perder la comprobación
exhaustiva.

En otras palabras, se puede pensar en él if letcomo un azúcar sintáctico para


unmatch que ejecuta código cuando el valor coincide con un patrón y luego
ignora todos los demás valores.

Podemos incluir un elsecon un if let. El bloque de código que acompaña a elsees


el mismo que el que correspondería al _caso en la matchexpresión equivalente
a if letand else. Recordemos la Coindefinición de enumeración del Listado 6-4,
donde la Quartervariante también contenía un UsStatevalor. Si quisiéramos
contar todas las monedas que no sean de veinticinco centavos y, al mismo
tiempo, anunciar el estado de las monedas de veinticinco centavos, podríamos
hacerlo con una match expresión como esta:

let mut count = 0;


match coin {
Coin::Quarter(state) => println!("State quarter from {state:?}!"),
Rust
jueves, 22 de mayo de 2025 : Página 153 de 719
_ => count += 1,
}

O podríamos usar una expresión if letand else, como esta:

let mut count = 0;


if let Coin::Quarter(state) = coin {
println!("State quarter from {state:?}!");
} else {
count += 1;
}

Mantenerse en el “camino feliz” conlet else

Un patrón común es realizar un cálculo cuando existe un valor y devolver un valor


predeterminado en caso contrario. Siguiendo con nuestro ejemplo de monedas
con UsStatevalor, si quisiéramos decir algo curioso según la antigüedad del
estado de la moneda, podríamos introducir un método UsStatepara comprobar la
antigüedad de un estado, como se muestra a continuación:

impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}

Luego podríamos usar if letpara hacer coincidir el tipo de moneda, introduciendo


una state variable dentro del cuerpo de la condición, como en el Listado 6-7.

Nombre de archivo: src/main.rs


fn describe_state_quarter(coin: Coin) -> Option<String> {
if let Coin::Quarter(state) = coin {
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
} else {
None
}
}
Rust
jueves, 22 de mayo de 2025 : Página 154 de 719
Listado 6-7: Uso

Esto cumple su función, pero ha desplazado el trabajo al cuerpo de la if


let declaración, y si el trabajo a realizar es más complejo, podría ser difícil seguir
exactamente cómo se relacionan las ramas de nivel superior. También podríamos
aprovechar que las expresiones producen un valor para generar el valor statede if
leto para regresar antes, como en el Listado 6-8. (¡Podrías hacer algo similar con
a match, por supuesto!)

Nombre de archivo: src/main.rs


fn describe_state_quarter(coin: Coin) -> Option<String> {
let state = if let Coin::Quarter(state) = coin {
state
} else {
return None;
};

if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
Listado 6-8: Uso if letpara producir un valor o retorno anticipado.

Sin embargo, es un poco molesto seguir esto a su manera. Una rama if letproduce
un valor y la otra retorna de la función por completo.

Para simplificar la expresión de este patrón común, Rust ha implementado letla


sintaxis - else. La sintaxis let- elsetoma un patrón a la izquierda y una expresión a
la derecha, muy similar a if let, pero no tiene una iframa, solo una elserama. Si el
patrón coincide, vinculará el valor del patrón en el ámbito externo. Si el
patrón no coincide, el programa fluirá hacia el elsebrazo, que debe retornar desde
la función.

En el Listado 6-9, puede ver cómo se ve el Listado 6-8 al usar let- elseen lugar
de if let. Observe que, de esta manera, se mantiene en la ruta correcta en el
cuerpo principal de la función, sin tener un flujo de control significativamente
diferente para las dos ramas, como if letocurría con .

Nombre de archivo: src/main.rs


fn describe_state_quarter(coin: Coin) -> Option<String> {
let Coin::Quarter(state) = coin else {
Rust
jueves, 22 de mayo de 2025 : Página 155 de 719
return None;
};

if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
Listado 6-9: Uso de let- elsepara aclarar el flujo a través de la función.

Si tienes una situación en la que tu programa tiene una lógica demasiado


detallada para expresarla usando un match, recuerda que if lety let elsetambién
están en tu caja de herramientas de Rust.

Resumen

Hemos explicado cómo usar enumeraciones para crear tipos personalizados que
pueden ser uno de un conjunto de valores enumerados. Hemos mostrado cómo
el Option<T> tipo de la biblioteca estándar ayuda a usar el sistema de tipos para
evitar errores. Cuando los valores de enumeración contienen datos, se puede
usar matcho if letpara extraer y usar esos valores, según el número de casos que
se necesiten gestionar.

Tus programas Rust ahora pueden expresar conceptos en tu dominio mediante


estructuras y enumeraciones. La creación de tipos personalizados para usar en tu
API garantiza la seguridad de tipos: el compilador se asegurará de que tus
funciones solo obtengan valores del tipo que cada función espera.

Para proporcionar a sus usuarios una API bien organizada que sea fácil de usar y
que solo exponga exactamente lo que necesitarán, pasemos ahora a los módulos
de Rust.

Gestión de proyectos en crecimiento con paquetes, cajas y módulos

A medida que escriba programas grandes, organizar su código será cada vez más
importante. Al agrupar las funciones relacionadas y separar el código con
características distintivas, tendrá claro dónde encontrar el código que implementa
una característica específica y dónde ir para modificar su funcionamiento.

Los programas que hemos escrito hasta ahora se encuentran en un módulo


dentro de un archivo. A medida que un proyecto crece, conviene organizar el
Rust
jueves, 22 de mayo de 2025 : Página 156 de 719

código dividiéndolo en varios módulos y, posteriormente, en varios archivos. Un


paquete puede contener varios contenedores binarios y, opcionalmente, un
contenedor de biblioteca. A medida que un paquete crece, se pueden extraer
partes en contenedores separados que se convierten en dependencias externas.
Este capítulo abarca todas estas técnicas. Para proyectos muy grandes que
comprenden un conjunto de paquetes interrelacionados que evolucionan juntos,
Cargo proporciona espacios de trabajo , que abordaremos en la sección "Espacios
de trabajo de Cargo" del Capítulo 14.

También hablaremos sobre la encapsulación de detalles de implementación, lo


que permite reutilizar el código a un nivel superior: una vez implementada una
operación, otro código puede llamar al tuyo a través de su interfaz pública sin
necesidad de conocer su funcionamiento. La forma en que escribes código define
qué partes son públicas para que las use otro código y qué partes son detalles de
implementación privados que te reserva el derecho de modificar. Esta es otra
forma de limitar la cantidad de detalles que debes recordar.

Un concepto relacionado es el ámbito: el contexto anidado en el que se escribe el


código tiene un conjunto de nombres definidos como "dentro del ámbito". Al leer,
escribir y compilar código, los programadores y compiladores necesitan saber si
un nombre en un punto específico se refiere a una variable, función, estructura,
enumeración, módulo, constante u otro elemento, y qué significa dicho elemento.
Se pueden crear ámbitos y cambiar los nombres que están dentro o fuera del
ámbito. No se pueden tener dos elementos con el mismo nombre en el mismo
ámbito; existen herramientas para resolver conflictos de nombres.

Rust cuenta con diversas funciones que permiten gestionar la organización del
código, incluyendo qué detalles se exponen, cuáles son privados y qué nombres
se incluyen en cada ámbito de los programas. Estas funciones, a veces
denominadas colectivamente como el sistema de módulos , incluyen:

 Paquetes: una función de Cargo que te permite construir, probar y


compartir cajas
 Cajas: Un árbol de módulos que produce una biblioteca o un ejecutable
 Módulos y uso: Le permiten controlar la organización, el alcance y la
privacidad de las rutas.
 Rutas: una forma de nombrar un elemento, como una estructura, una
función o un módulo.
Rust
jueves, 22 de mayo de 2025 : Página 157 de 719

En este capítulo, cubriremos todas estas funciones, analizaremos cómo


interactúan y explicaremos cómo usarlas para gestionar el alcance. Al finalizar,
comprenderás a fondo el sistema de módulos y podrás trabajar con alcances
como un experto.

Paquetes y cajas

Las primeras partes del sistema de módulos que cubriremos son los paquetes y
las cajas.

Un crate es la cantidad mínima de código que el compilador de Rust considera a


la vez. Incluso si se ejecuta rustcen lugar de cargopasar un solo archivo de código
fuente (como hicimos en la sección "Escritura y ejecución de un programa en
Rust" del Capítulo 1), el compilador considera ese archivo como un crate. Los
crates pueden contener módulos, y estos pueden estar definidos en otros
archivos que se compilan con el crate, como veremos en las siguientes secciones.

Un cajón puede presentarse en dos formas: cajón binario o cajón de


biblioteca. Los cajones binarios son programas que se pueden compilar en un
ejecutable que se puede ejecutar, como un programa de línea de comandos o un
servidor. Cada uno debe tener una función llamada mainque define qué sucede
cuando se ejecuta el ejecutable. Todos los cajones que hemos creado hasta ahora
han sido cajones binarios.

Las cajas de biblioteca no tienen una mainfunción ni se compilan en un


ejecutable. En cambio, definen funcionalidades que se comparten con múltiples
proyectos. Por ejemplo, la randcaja que usamos en el Capítulo 2 proporciona una
funcionalidad que genera números aleatorios. La mayoría de las veces, cuando los
rustáceos dicen "caja", se refieren a una caja de biblioteca, y usan "caja"
indistintamente con el concepto general de programación de "biblioteca".

La raíz del cajón es un archivo fuente desde el cual se inicia el compilador de Rust
y conforma el módulo raíz de su cajón (explicaremos los módulos en profundidad
en la sección “Definición de módulos para controlar el alcance y la privacidad” ).

Un paquete es un conjunto de una o más cajas que proporciona un conjunto de


funciones. Un paquete contiene un Cargo.toml. que describe cómo compilar
dichos crates. Cargo es, en realidad, un paquete que contiene el crate binario
para la herramienta de línea de comandos que has estado usando para compilar
Rust
jueves, 22 de mayo de 2025 : Página 158 de 719

tu código. El paquete Cargo también contiene un crate de biblioteca del que


depende el crate binario. Otros proyectos pueden depender del crate de
biblioteca Cargo para usar la misma lógica que la herramienta de línea de
comandos Cargo. Un paquete puede contener tantos crates binarios como se
desee, pero como máximo solo un crate de biblioteca. Un paquete debe contener
al menos un crate, ya sea de biblioteca o binario.

Veamos qué sucede al crear un paquete. Primero, introducimos el comando cargo


new my-project:

$ cargo new my-project


Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

Después de ejecutar cargo new my-project, usamos lspara ver qué crea Cargo. En
el directorio del proyecto, hay un archivo Cargo.toml que nos proporciona un
paquete. También hay un directorio src que contiene main.rs. Abra Cargo.toml en
su editor de texto y observe que no se menciona src/main.rs . Cargo sigue la
convención de que src/main.rs es la raíz de un cajón binario con el mismo nombre
que el paquete. Asimismo, Cargo sabe que si el directorio del paquete
contiene src/lib.rs , el paquete contiene un cajón de biblioteca con el mismo
nombre, y src/lib.rs es su raíz. Cargo pasa los archivos raíz de este cajón a
[nombre del paquete] para rustccompilar la biblioteca o el binario.

Aquí tenemos un paquete que solo contiene src/main.rs , lo que significa que solo
contiene un contenedor binario llamado my-project. Si un paquete
contiene src/main.rs y src/lib.rs , tiene dos contenedores: un contenedor binario y
una biblioteca, ambos con el mismo nombre que el paquete. Un paquete puede
tener varios contenedores binarios colocando los archivos en el directorio src/bin :
cada archivo será un contenedor binario independiente.

Definición de módulos para controlar el alcance y la privacidad

En esta sección, hablaremos sobre los módulos y otras partes del sistema de
módulos, como las rutas , que permiten nombrar elementos; la usepalabra clave
que incluye una ruta en el ámbito; y la pubpalabra clave que permite que los
Rust
jueves, 22 de mayo de 2025 : Página 159 de 719

elementos sean públicos. También analizaremos la aspalabra clave, los paquetes


externos y el operador glob.

Hoja de trucos de módulos

Antes de entrar en detalles sobre módulos y rutas, ofrecemos una breve


referencia sobre cómo funcionan los módulos, las rutas, la usepalabra clave y
la pubpalabra clave en el compilador, y cómo la mayoría de los desarrolladores
organizan su código. A lo largo de este capítulo, repasaremos ejemplos de cada
una de estas reglas, pero este es un buen punto de referencia para recordar cómo
funcionan los módulos.

 Comenzar desde la raíz del cajón : al compilar un cajón, el compilador


primero busca en el archivo raíz del cajón (normalmente src/lib.rs para un
cajón de biblioteca o src/main.rs para un cajón binario) el código para
compilar.
 Declaración de módulos : En el archivo raíz del crate, puedes declarar
nuevos módulos; por ejemplo, declaras un módulo "garden" con mod
garden;. El compilador buscará el código del módulo en estos lugares:
o En línea, dentro de llaves que reemplazan el punto y coma que
siguemod garden
o En el archivo src/garden.rs
o En el archivo src/garden/mod.rs
 Declaración de submódulos : Se pueden declarar submódulos en
cualquier archivo, excepto en la raíz del crate. Por ejemplo, se podría
declarar mod vegetables;en src/garden.rs . El compilador buscará el código
del submódulo en el directorio del módulo padre en estos lugares:
o En línea, directamente después de mod vegetables, dentro de llaves
en lugar del punto y coma
o En el archivo src/garden/vegetables.rs
o En el archivo src/garden/vegetables/mod.rs
 Rutas de acceso al código en módulos : Una vez que un módulo forma
parte de tu crate, puedes acceder a su código desde cualquier otro lugar del
mismo crate, siempre que las reglas de privacidad lo permitan, utilizando la
ruta al código. Por ejemplo, un Asparagustipo en el módulo "hortalizas" se
encontraría en crate::garden::vegetables::Asparagus.
 Privado vs. público : El código de un módulo es privado de sus módulos
principales por defecto. Para que un módulo sea público, declárelo con pub
Rust
jueves, 22 de mayo de 2025 : Página 160 de 719

mod en lugar de mod. Para que los elementos de un módulo público


también sean públicos, use pubantes de sus declaraciones.
 La usepalabra clave : Dentro de un ámbito, la usepalabra clave crea
accesos directos a elementos para reducir la repetición de rutas largas. En
cualquier ámbito que pueda hacer referencia
a crate::garden::vegetables::Asparagus, se puede crear un acceso directo
con , use crate::garden::vegetables::Asparagus;y a partir de entonces solo
es necesario escribir Asparaguspara usar ese tipo en el ámbito.

Aquí, creamos un contenedor binario llamado backyardque ilustra estas reglas. El


directorio del contenedor, también llamado backyard, contiene estos archivos y
directorios:

backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rs

En este caso, el archivo raíz del paquete es src/main.rs y contiene:

Nombre de archivo: src/main.rs


use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
}

La pub mod garden;línea le dice al compilador que incluya el código que


encuentre en src/garden.rs , que es:

Nombre del archivo: src/garden.rs


pub mod vegetables;

Esto pub mod vegetables;significa que el código


en src/garden/vegetables.rs también está incluido. Ese código es:
Rust
jueves, 22 de mayo de 2025 : Página 161 de 719
#[derive(Debug)]
pub struct Asparagus {}

¡Ahora entremos en detalles sobre estas reglas y demostrémoslas en acción!

Agrupación de código relacionado en módulos

Los módulos nos permiten organizar el código dentro de un contenedor 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 implementación interna que no están
disponibles para uso externo. Podemos hacer públicos los módulos y sus
elementos, lo que los expone para que código externo pueda usarlos y depender
de ellos.

Por ejemplo, escribamos una biblioteca que proporcione la funcionalidad de un


restaurante. Definiremos las firmas de las funciones, pero dejaremos sus cuerpos
vacíos para centrarnos en la organización del código en lugar de en la
implementación del restaurante.

En la industria restaurantera, algunas partes del restaurante se conocen


como sala y otras como cocina . La sala es donde se encuentran los clientes; esto
abarca donde los anfitriones los sientan, los camareros toman los pedidos y los
pagos, y los bármanes preparan las bebidas. La cocina es donde los chefs y
cocineros trabajan, los lavaplatos limpian y los gerentes realizan el trabajo
administrativo.

Para estructurar nuestra caja de esta manera, podemos organizar sus funciones
en módulos anidados. Crea una nueva biblioteca llamada restaurantmediante la
ejecución de cargo new restaurant --lib. Luego, introduce el código del Listado 7-1
en src/lib.rs para definir algunos módulos y firmas de funciones; este código es la
sección de front-of-house.

Nombre de archivo: src/lib.rs


mod front_of_house {
mod hosting {
fn add_to_waitlist() {}

fn seat_at_table() {}
}

mod serving {
Rust
jueves, 22 de mayo de 2025 : Página 162 de 719
fn take_order() {}

fn serve_order() {}

fn take_payment() {}
}
}
Listado 7-1: Un front_of_housemódulo que contiene otros módulos que a su vez
contienen funciones

Definimos un módulo con la modpalabra clave seguida de su nombre (en este


caso, front_of_house). El cuerpo del módulo se coloca entre llaves. Dentro de los
módulos, podemos colocar otros módulos, como en este caso con
modules hostingy serving. Los módulos también pueden contener definiciones de
otros elementos, como estructuras, enumeraciones, constantes, rasgos y, como
en el Listado 7-1, funciones.

Al usar módulos, podemos agrupar definiciones relacionadas y explicar por qué


están relacionadas. Los programadores que usan este código pueden navegar por
él según los grupos en lugar de tener que leer todas las definiciones, lo que
facilita encontrar las relevantes. Los programadores que añadan nuevas
funcionalidades a este código sabrán dónde colocarlo para mantener el programa
organizado.

Anteriormente, mencionamos que src/main.rs y src/lib.rs se denominan raíces de


crate. Su nombre se debe a que el contenido de cualquiera de estos dos archivos
forma un módulo cuyo nombre se encuentra crateen la raíz de la estructura de
módulos del crate, conocida como árbol de módulos .

El listado 7-2 muestra el árbol de módulos para la estructura del Listado 7-1.

crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
Listado 7-2: El árbol de módulos para el código del Listado 7-1
Rust
jueves, 22 de mayo de 2025 : Página 163 de 719

Este árbol muestra cómo algunos módulos se anidan dentro de otros; por
ejemplo, hostingse anida dentro de front_of_house. El árbol también muestra que
algunos módulos son hermanos , lo que significa que están definidos en el mismo
módulo; hostingy servingson hermanos definidos dentro de front_of_house. Si el
módulo A está contenido dentro del módulo B, decimos que el módulo A
es hijo del módulo B y que el módulo B es padre del módulo A. Observe que todo
el árbol de módulos tiene su raíz en el módulo implícito llamado crate.

El árbol de módulos podría recordarte al árbol de directorios del sistema de


archivos de tu ordenador; ¡es una comparación muy acertada! Al igual que los
directorios en un sistema de archivos, se usan módulos para organizar el código.
Y, al igual que los archivos en un directorio, necesitamos una forma de encontrar
nuestros módulos.

Rutas para hacer referencia a un elemento en el árbol de módulos

Para indicarle a Rust dónde encontrar un elemento en un árbol de módulos,


usamos una ruta de la misma forma que la usamos al navegar por un sistema de
archivos. Para llamar a una función, necesitamos conocer su ruta.

Un camino puede tomar dos formas:

 Una ruta absoluta es la ruta completa que comienza desde la raíz de un


cajón; para el código de un cajón externo, la ruta absoluta comienza con el
nombre del cajón y, para el código del cajón actual, comienza con el
literal crate.
 Una ruta relativa comienza desde el módulo actual y utiliza self, super, o un
identificador en el módulo actual.

Las rutas absolutas y relativas van seguidas de uno o más identificadores


separados por dos puntos ( ::).

Volviendo al Listado 7-1, supongamos que queremos llamar a


la add_to_waitlistfunción. Esto equivale a preguntar: ¿cuál es la ruta de
la add_to_waitlistfunción? El Listado 7-3 contiene el Listado 7-1 con algunos
módulos y funciones eliminados.

Mostraremos dos maneras de llamar a la add_to_waitlistfunción desde una nueva


función, eat_at_restaurantdefinida en la raíz del crate. Estas rutas son correctas,
Rust
jueves, 22 de mayo de 2025 : Página 164 de 719

pero existe otro problema que impedirá que este ejemplo se compile tal cual.
Explicaremos por qué en breve.

La eat_at_restaurantfunción forma parte de la API pública de nuestra biblioteca,


por lo que la marcamos con la pubpalabra clave. En la sección "Exponer rutas con
la pub palabra clave" , profundizaremos en ella pub.

Nombre de archivo: src/lib.rs

mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}

pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();

// Relative path
front_of_house::hosting::add_to_waitlist();
}
Listado 7-3: Llamada a la add_to_waitlistfunción utilizando rutas absolutas y
relativas

La primera vez que llamamos a la add_to_waitlistfunción en eat_at_restaurant,


usamos una ruta absoluta. La add_to_waitlistfunción está definida en el mismo
crate que eat_at_restaurant, lo que significa que podemos usar la cratepalabra
clave para iniciar una ruta absoluta. Luego, incluimos cada uno de los módulos
sucesivos hasta llegar a add_to_waitlist. Imagine un sistema de archivos con la
misma estructura: especificaríamos la
ruta /front_of_house/hosting/add_to_waitlistpara ejecutar
el add_to_waitlistprograma; usar el cratenombre para iniciar desde la raíz del
crate es como usar /para iniciar desde la raíz del sistema de archivos en su shell.

La segunda vez que llamamos add_to_waitlista eat_at_restaurant, usamos una


ruta relativa. La ruta comienza con front_of_house, el nombre del módulo definido
en el mismo nivel del árbol de módulos que eat_at_restaurant. En este caso, el
equivalente en el sistema de archivos sería usar la
ruta front_of_house/hosting/add_to_waitlist. Al comenzar con el nombre de un
módulo, la ruta es relativa.
Rust
jueves, 22 de mayo de 2025 : Página 165 de 719

Elegir si usar una ruta relativa o absoluta es una decisión que tomará según su
proyecto y depende de si es más probable que mueva el código de definición de
elemento por separado o junto con el código que usa el elemento. Por ejemplo, si
movemos el front_of_housemódulo y la eat_at_restaurantfunción a un módulo
llamado customer_experience, necesitaríamos actualizar la ruta absoluta
a add_to_waitlist, pero la ruta relativa seguiría siendo válida. Sin embargo, si
movemos la eat_at_restaurantfunción por separado a un módulo llamado dining,
la ruta absoluta a la add_to_waitlistllamada permanecería igual, pero la ruta
relativa tendría que actualizarse. En general, preferimos especificar rutas
absolutas porque es más probable que queramos mover las definiciones de
código y las llamadas de elemento de forma independiente.

Intentemos compilar el Listado 7-3 y descubramos por qué no compila todavía.


Los errores que encontramos se muestran en el Listado 7-4.

$ 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 {
| ^^^^^^^^^^^

error[E0603]: module `hosting` is private


--> src/lib.rs:12:21
|
12 | 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
|
Rust
jueves, 22 de mayo de 2025 : Página 166 de 719
2 | mod hosting {
| ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
Listado 7-4: Errores del compilador al crear el código del Listado 7-3

Los mensajes de error indican que el módulo hostinges privado. En otras palabras,
tenemos las rutas correctas para el hostingmódulo y la add_to_waitlist función,
pero Rust no nos permite usarlas porque no tiene acceso a las secciones privadas.
En Rust, todos los elementos (funciones, métodos, estructuras, enumeraciones,
módulos y constantes) son privados de los módulos principales por defecto. Para
que un elemento, como una función o una estructura, sea privado, se coloca en
un módulo.

Los elementos de un módulo principal no pueden usar los elementos privados de


los módulos secundarios, pero sí pueden usar los elementos de sus módulos
predecesores. Esto se debe a que los módulos secundarios encapsulan y ocultan
sus detalles de implementación, pero estos pueden ver el contexto en el que se
definen. Siguiendo con nuestra metáfora, imagine las reglas de privacidad como
la oficina administrativa de un restaurante: lo que ocurre allí es privado para los
clientes, pero los gerentes de oficina pueden ver y hacer todo lo que ocurre en el
restaurante que gestionan.

Rust optó por que el sistema de módulos funcione de esta manera, de modo que
ocultar los detalles de implementación internos sea la opción predeterminada. De
esta forma, sabes qué partes del código interno puedes modificar sin afectar el
código externo. Sin embargo, Rust te permite exponer partes internas del código
de los módulos secundarios a los módulos ancestros externos usando
la pubpalabra clave para hacer público un elemento.

Exponer rutas con la pubpalabra clave

Volvamos al error del Listado 7-4 que indicaba que el hostingmódulo es privado.
Queremos que la eat_at_restaurantfunción del módulo principal tenga acceso a
la add_to_waitlistfunción del módulo secundario, por lo que marcamos
el hostingmódulo con la pubpalabra clave, como se muestra en el Listado 7-5.

Nombre de archivo: src/lib.rs


Rust
jueves, 22 de mayo de 2025 : Página 167 de 719
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: Declaración del hostingmódulo para pubutilizarlo
desdeeat_at_restaurant

Desafortunadamente, el código del Listado 7-5 aún genera errores de


compilación, como se muestra en el Listado 7-6.

$ 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() {}
| ^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private


--> src/lib.rs:12:30
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
Rust
jueves, 22 de mayo de 2025 : Página 168 de 719
Listado 7-6: Errores del compilador al crear el código del Listado 7-5

¿Qué sucedió? Añadir la pubpalabra clave delante de `` mod hostinghace que el


módulo sea público. Con este cambio, si podemos acceder a `` front_of_house,
podemos acceder a ` hosting`. Sin embargo, el contenido de hosting`` sigue
siendo privado; hacer público el módulo no lo hace público. La pubpalabra clave
en un módulo solo permite que el código de sus módulos predecesores se refiera
a él, no que acceda a su código interno. Dado que los módulos son contenedores,
no hay mucho que podamos hacer simplemente haciéndolo público; necesitamos
ir más allá y optar por hacer públicos también uno o más elementos dentro del
módulo.

Los errores del Listado 7-6 indican que la add_to_waitlistfunción es privada. Las
reglas de privacidad se aplican tanto a estructuras, enumeraciones, funciones y
métodos como a módulos.

También hagamos add_to_waitlistpública la función agregando la pub palabra


clave antes de su definición, como en el Listado 7-7.

Nombre de archivo: src/lib.rs


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 pubpalabra clave a mod hostingy fn add_to_waitlistnos
permite llamar a la función desdeeat_at_restaurant

¡Ahora el código compilará! Para entender por qué añadir la pubpalabra clave nos
permite usar estas rutas eat_at_restaurantrespetando las reglas de privacidad,
veamos las rutas absolutas y relativas.

En la ruta absoluta, comenzamos con crate, la raíz del árbol de módulos de


nuestro crate. El front_of_housemódulo está definido en la raíz del crate. Si
Rust
jueves, 22 de mayo de 2025 : Página 169 de 719

bien front_of_houseno es público, ya que la eat_at_restaurantfunción está definida


en el mismo módulo que front_of_house(es
decir, eat_at_restaurant y front_of_houseson hermanos), podemos hacer
referencia a front_of_housedesde eat_at_restaurant. A continuación, está
el hostingmódulo marcado con pub. Podemos acceder al módulo padre
de hosting, por lo que podemos acceder a hosting. Finalmente,
la add_to_waitlistfunción está marcada con puby podemos acceder a su módulo
padre, por lo que esta llamada a la función funciona.

En la ruta relativa, la lógica es la misma que en la ruta absoluta, excepto en el


primer paso: en lugar de comenzar desde la raíz del contenedor, la ruta comienza
desde front_of_house. El front_of_housemódulo 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 eat_at_restaurantestá definido funciona. Luego,
como hostingy add_to_waitlistestán marcados con pub, el resto de la ruta
funciona, y esta llamada a la función es válida.

Si planea compartir su crate de biblioteca para que otros proyectos puedan usar
su código, su API pública es el contrato con los usuarios de su crate, que
determina cómo pueden interactuar con su código. Hay muchas consideraciones
sobre la gestión de cambios en su API pública para facilitar que los usuarios
dependan de su crate. Estas consideraciones quedan fuera del alcance de este
libro; si le interesa este tema, consulte las Directrices de la API de Rust .

Mejores prácticas para paquetes con un binario y una biblioteca

Mencionamos que un paquete puede contener tanto una raíz de cajón


binario src/main.rs como una raíz de cajón de biblioteca src/lib.rs , y ambos
cajones tendrán el nombre del paquete por defecto. Normalmente, los paquetes
con este patrón de contener tanto una biblioteca como un cajón binario tendrán el
código justo en el cajón binario para iniciar un ejecutable que invoque código
dentro del cajón de biblioteca. Esto permite que otros proyectos se beneficien de
la mayor parte de la funcionalidad del paquete, ya que el código del cajón de
biblioteca se puede compartir.

El árbol de módulos debe definirse en src/lib.rs . De esta forma, cualquier


elemento público puede usarse en el contenedor binario iniciando las rutas con el
nombre del paquete. El contenedor binario se convierte en usuario del contenedor
de biblioteca, al igual que un contenedor completamente externo lo haría: solo
Rust
jueves, 22 de mayo de 2025 : Página 170 de 719

puede usar la API pública. Esto te ayuda a diseñar una buena API; no solo eres el
autor, sino también el cliente.

En el Capítulo 12 , demostraremos esta práctica organizacional con un programa


de línea de comandos que contendrá tanto un cajón binario como un cajón de
biblioteca.

Iniciar rutas relativas consuper

Podemos construir rutas relativas que comiencen en el módulo padre, en lugar del
módulo actual o la raíz del crate, usando `` superal inicio de la ruta`. Esto es
como iniciar una ruta del sistema de archivos con la ..sintaxis. Usar `` supernos
permite referenciar un elemento que sabemos que está en el módulo padre, lo
que facilita la reorganización del árbol de módulos cuando el módulo está
estrechamente relacionado con el padre, pero este podría moverse a otra parte
del árbol de módulos en el futuro.

Considere el código del Listado 7-8 que modela la situación en la que un chef
prepara un pedido incorrecto y se lo entrega personalmente al cliente. La
función fix_incorrect_orderdefinida en el back_of_housemódulo llama a la
función deliver_orderdefinida en el módulo principal especificando la ruta
a deliver_order, comenzando por super.

Nombre de archivo: src/lib.rs


fn deliver_order() {}

mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}

fn cook_order() {}
}
Listado 7-8: Llamada a una función usando una ruta relativa que comienza
consuper

La fix_incorrect_orderfunción está en el back_of_housemódulo, así que podemos


usar superpara ir al módulo padre de back_of_house, que en este caso es crate, la
raíz. Desde allí, lo buscamos deliver_ordery lo encontramos. ¡Éxito! Creemos que
es probable que el back_of_housemódulo y la deliver_orderfunción mantengan la
misma relación y se muevan juntos si decidimos reorganizar el árbol de módulos
Rust
jueves, 22 de mayo de 2025 : Página 171 de 719

del contenedor. Por lo tanto, usamos superpara tener menos espacio para
actualizar el código en el futuro si este se mueve a otro módulo.

Hacer públicas las estructuras y enumeraciones

También podemos usar pubpara designar estructuras y enumeraciones como


públicas, pero existen algunos detalles adicionales sobre su uso pub. Si usamos
`` pub antes de la definición de una estructura``, la estructura se hace pública,
pero sus campos siguen siendo privados. Podemos hacer que cada campo sea
público o no, según el caso. En el Listado 7-9, definimos
una back_of_house::Breakfastestructura pública con un toastcampo público, pero
uno privado seasonal_fruit. Esto simula el caso de un restaurante donde el cliente
puede elegir el tipo de pan que viene con un plato, pero el chef decide qué fruta
acompaña el plato según la temporada y la disponibilidad. La fruta disponible
cambia rápidamente, por lo que los clientes no pueden elegir la fruta ni siquiera
ver qué fruta recibirán.

Nombre de archivo: src/lib.rs


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");
Rust
jueves, 22 de mayo de 2025 : Página 172 de 719
}
Listado 7-9: Una estructura con algunos campos públicos y algunos campos
privados

Dado que el toastcampo en la back_of_house::Breakfastestructura es


público, eat_at_restaurantpodemos escribir y leer en toastél usando la notación
de puntos. Tenga en cuenta que no podemos usar el seasonal_fruitcampo
en eat_at_restaurant, ya que seasonal_fruites privado. Pruebe a descomentar la
línea que modifica el seasonal_fruitvalor del campo para ver el error.

Además, tenga en cuenta que, dado que back_of_house::Breakfastla estructura


tiene un campo privado, debe proporcionar una función pública asociada que
construya una instancia de Breakfast(la hemos nombrado summeraquí).
Si Breakfastno tuviera dicha función, no podríamos crear una instancia
de Breakfastin eat_at_restaurantporque no podríamos establecer el valor
del seasonal_fruitcampo privado en eat_at_restaurant.

Por el contrario, si hacemos pública una enumeración, todas sus variantes


también lo son. Solo necesitamos el ``` pubantes de la enumpalabra clave``,
como se muestra en el Listado 7-10.

Nombre de archivo: src/lib.rs


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 una enumeración como pública hace que todas sus
variantes sean públicas

Como hicimos Appetizerpública la enumeración, podemos usar


las variantes Soupy en .Saladeat_at_restaurant

Las enumeraciones no son muy útiles a menos que sus variantes sean públicas;
sería molesto tener que anotar todas las variantes de enumeración con puben
todos los casos, por lo que el valor predeterminado para las variantes de
Rust
jueves, 22 de mayo de 2025 : Página 173 de 719

enumeración es que sean públicas. Las estructuras suelen ser útiles sin que sus
campos sean públicos, por lo que los campos de estructura siguen la regla
general de que todo es privado por defecto a menos que se anoten con pub.

Hay una situación más pubque no hemos abordado, y es la última característica


del sistema de módulos: la usepalabra clave. Primero la abordaremos usepor sí
sola y luego mostraremos cómo combinar puby use.

Incorporar rutas al alcance con la usepalabra clave

Escribir las rutas para llamar a las funciones puede resultar incómodo y repetitivo.
En el Listado 7-7, independientemente de si elegimos la ruta absoluta o relativa a
la add_to_waitlistfunción, cada vez que queríamos
llamarla add_to_waitlist teníamos que especificar
también front_of_house"y hosting". Afortunadamente, hay una forma de
simplificar este proceso: podemos crear un acceso directo a una ruta con
la use palabra clave una vez y luego usar el nombre más corto en el resto del
ámbito.

En el Listado 7-11, traemos el crate::front_of_house::hostingmódulo al alcance de


la eat_at_restaurantfunción, de modo que solo tenemos que
especificar hosting::add_to_waitlistque queramos llamar a
la add_to_waitlistfunción en eat_at_restaurant.

Nombre de archivo: 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();
}
Listado 7-11: Incorporación de un módulo al alcance conuse

Añadir useuna ruta a un ámbito es similar a crear un enlace simbólico en el


sistema de archivos. Al añadirla use crate::front_of_house::hostingen la raíz del
paquete, hostingahora es un nombre válido en ese ámbito, como si
Rust
jueves, 22 de mayo de 2025 : Página 174 de 719

el hosting módulo se hubiera definido en la raíz del paquete. Las rutas añadidas al
ámbito use también verifican la privacidad, como cualquier otra ruta.

Tenga en cuenta que usesolo crea el acceso directo para el ámbito específico en
el que useocurre. El Listado 7-12 mueve la eat_at_restaurantfunción a un nuevo
módulo secundario llamado customer, que entonces tiene un ámbito diferente al
de la use instrucción, por lo que el cuerpo de la función no se compilará.

Nombre de archivo: src/lib.rs

mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use crate::front_of_house::hosting;

mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
Listado 7-12: Una usedeclaración solo se aplica en el ámbito en el que se
encuentra

El error del compilador muestra que el acceso directo ya no se aplica dentro


del customermódulo:

$ 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;
|

warning: unused import: `crate::front_of_house::hosting`


--> src/lib.rs:7:5
|
Rust
jueves, 22 de mayo de 2025 : Página 175 de 719
7 | use crate::front_of_house::hosting;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default

For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning
emitted

Tenga en cuenta que también hay una advertencia que indica que useya no se
usa en su ámbito. Para solucionar este problema, mueva también el usedentro
del customermódulo o haga referencia al acceso directo en el módulo principal
con super::hostingdentro del customermódulo secundario.

useCreación de rutas idiomáticas

En el Listado 7-11, es posible que se haya preguntado por qué especificamos use
crate::front_of_house::hostingy luego
llamamos hosting::add_to_waitlista eat_at_restaurant, en lugar de especificar
la useruta hasta la add_to_waitlistfunción para lograr el mismo resultado, como en
el Listado 7-13.

Nombre de archivo: 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: Incorporar la add_to_waitlistfunción al alcance con use, lo cual no es
idiomático

Aunque tanto el Listado 7-11 como el Listado 7-13 realizan la misma tarea, el
Listado 7-11 es la forma idiomática de incluir una función en el ámbito con use.
Incluir el módulo padre de la función en el ámbito con useimplica especificar dicho
módulo al llamarla. Especificar el módulo padre al llamarla deja claro que la
función no está definida localmente, a la vez que minimiza la repetición de la ruta
completa. El código del Listado 7-13 no especifica dónde add_to_waitlistse define.
Rust
jueves, 22 de mayo de 2025 : Página 176 de 719

Por otro lado, al importar estructuras, enumeraciones y otros elementos con use,
es idiomático especificar la ruta completa. El Listado 7-14 muestra la forma
idiomática de incluir la HashMapestructura de la biblioteca estándar en el ámbito
de un contenedor binario.

Nombre de archivo: src/main.rs


use std::collections::HashMap;

fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
Listado 7-14: Traer HashMapal ámbito de una manera idiomática

No hay ninguna razón de peso detrás de este modismo: es simplemente la


convención que ha surgido y la gente se ha acostumbrado a leer y escribir código
Rust de esta manera.

La excepción a esta expresión es si traemos al ámbito dos elementos con el


mismo nombre mediante usesentencias, ya que Rust no lo permite. El Listado 7-
15 muestra cómo traer Resultal ámbito dos tipos con el mismo nombre, pero con
diferentes módulos principales, y cómo hacer referencia a ellos.

Nombre de archivo: src/lib.rs


use std::fmt;
use std::io;

fn function1() -> fmt::Result {


// --snip--
}

fn function2() -> io::Result<()> {


// --snip--
}
Listado 7-15: Para traer dos tipos con el mismo nombre al mismo ámbito es
necesario utilizar sus módulos principales.

Como puedes ver, usar los módulos principales distingue los dos Resulttipos. Si,
en cambio, escribiéramos use std::fmt::Resulty use std::io::Result, tendríamos
dos Resulttipos en el mismo ámbito, y Rust no sabría a cuál nos referíamos al
usar Result.

Proporcionar nuevos nombres con la aspalabra clave


Rust
jueves, 22 de mayo de 2025 : Página 177 de 719

Existe otra solución al problema de incluir dos tipos con el mismo nombre en el
mismo ámbito con use: después de la ruta, podemos especificar asun nuevo
nombre local, o alias , para el tipo. El Listado 7-16 muestra otra forma de escribir
el código del Listado 7-15, renombrando uno de los dos Resulttipos con as.

Nombre de archivo: src/lib.rs


use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {


// --snip--
}

fn function2() -> IoResult<()> {


// --snip--
}
Listado 7-16: Cambiar el nombre de un tipo cuando se incluye en el alcance con
la aspalabra clave

En la segunda usedeclaración, elegimos el nuevo nombre IoResultpara


el std::io::Resulttipo, que no entrará en conflicto con el
nombre Result"from std::fmt " que también hemos incluido en el alcance. Los
listados 7-15 y 7-16 se consideran idiomáticos, así que la decisión es suya.

Reexportación de nombres conpub use

Al incluir un nombre en el ámbito con la usepalabra clave, el nombre disponible


en el nuevo ámbito es privado. Para que el código que llama a nuestro código
haga referencia a ese nombre como si estuviera definido en su ámbito, podemos
combinar pub y use. Esta técnica se denomina reexportación porque, al incluir un
elemento en el ámbito, también lo ponemos a disposición de otros usuarios para
que lo incluyan en el suyo.

El listado 7-17 muestra el código del listado 7-11 con useel módulo raíz cambiado
a pub use.

Nombre de archivo: src/lib.rs


mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
Rust
jueves, 22 de mayo de 2025 : Página 178 de 719
pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Listado 7-17: Hacer que un nombre esté disponible para que cualquier código lo
use desde un nuevo ámbito conpub use

Antes de este cambio, el código externo debía llamar a la add_to_waitlist función


usando la ruta restaurant::front_of_house::hosting::add_to_waitlist(), lo que
también requería que el front_of_housemódulo se marcara como pub. Ahora que
se pub useha reexportado el hostingmódulo desde el módulo raíz, el código
externo puede usar la ruta restaurant::hosting::add_to_waitlist()en su lugar.

Reexportar es útil cuando la estructura interna de tu código difiere de cómo los


programadores que lo usan conciben el dominio. Por ejemplo, en esta metáfora
de un restaurante, quienes lo gestionan piensan en "servicio al cliente" y "servicio
técnico". Sin embargo, los clientes que visitan un restaurante probablemente no
piensen en las partes del restaurante en esos términos. Con [nombre del
restaurante] pub use, podemos escribir nuestro código con una estructura, pero
exponer una diferente. Esto facilita la organización de nuestra biblioteca tanto
para los programadores que trabajan en ella como para quienes la usan. Veremos
otro ejemplo de [nombre del restaurante] pub usey cómo afecta a la
documentación de tu caja en la sección "Exportar una API pública práctica con
[nombre del restaurante pub use]" del Capítulo 14.

Uso de paquetes externos

En el Capítulo 2, programamos un proyecto de juego de adivinanzas que utilizaba


un paquete externo llamado randpara obtener números aleatorios. Para
usarlo randen nuestro proyecto, añadimos esta línea a Cargo.toml :

Nombre del archivo: Cargo.toml


rand = "0.8.5"

Agregarlo randcomo dependencia en Cargo.toml le indica a Cargo que descargue


el randpaquete y cualquier dependencia de crates.io y los ponga randa
disposición de nuestro proyecto.

Luego, para incluir randlas definiciones en el alcance de nuestro paquete,


añadimos una uselínea que comenzaba con el nombre de la caja, randy
Rust
jueves, 22 de mayo de 2025 : Página 179 de 719

enumeramos los elementos que queríamos incluir. Recuerda que en la


sección "Generación de un número aleatorio" del capítulo 2, incluimos
el Rngatributo en el alcance y llamamos a la rand::thread_rngfunción:

use rand::Rng;

fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}

Los miembros de la comunidad Rust han puesto a disposición muchos paquetes


en crates.io , y extraer cualquiera de ellos para su paquete implica estos mismos
pasos: enumerarlos en el archivo Cargo.toml de su paquete y usarlos usepara
traer elementos de sus cajas al alcance.

Tenga en cuenta que la stdbiblioteca estándar también es una caja externa a


nuestro paquete. Dado que se incluye con Rust, no necesitamos
cambiar Cargo.toml a `include` std. Sin embargo, sí necesitamos referenciarla
con `with` usepara incorporar elementos desde allí al alcance de nuestro
paquete. Por ejemplo, con `with` HashMapusaríamos esta línea:

use std::collections::HashMap;

Esta es una ruta absoluta que comienza con std, el nombre del paquete de la
biblioteca estándar.

Uso de rutas anidadas para limpiar uselistas grandes

Si usamos varios elementos definidos en la misma caja o módulo, listar cada


elemento en su propia línea puede ocupar mucho espacio vertical en nuestros
archivos. Por ejemplo, estas dos usesentencias que teníamos en el juego de
adivinanzas del Listado 2-4 traen elementos stddel ámbito:

Nombre de archivo: src/main.rs


// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--

En su lugar, podemos usar rutas anidadas para incluir los mismos elementos en el
ámbito de aplicación en una sola línea. Para ello, especificamos la parte común de
Rust
jueves, 22 de mayo de 2025 : Página 180 de 719

la ruta, seguida de dos puntos y, a continuación, entre llaves, una lista de las
partes de las rutas que difieren, como se muestra en el Listado 7-18.

Nombre de archivo: src/main.rs


// --snip--
use std::{cmp::Ordering, io};
// --snip--
Listado 7-18: Especificación de una ruta anidada para traer múltiples elementos
con el mismo prefijo al alcance

En programas más grandes, traer muchos elementos al alcance desde la misma


caja o módulo usando rutas anidadas puede reducir usemucho la cantidad de
declaraciones separadas necesarias.

Podemos usar una ruta anidada en cualquier nivel de una ruta, lo cual resulta útil
al combinar dos usesentencias que comparten una subruta. Por ejemplo, el
Listado 7-19 muestra dos usesentencias: una que incluye std::iodentro del
alcance y otra que incluye std::io::Writedentro del alcance.

Nombre de archivo: src/lib.rs


use std::io;
use std::io::Write;
Listado 7-19: Dos usedeclaraciones donde una es una subruta de la otra

La parte común de estas dos rutas es std::io, y esa es la primera ruta completa.
Para fusionarlas en una sola useinstrucción, podemos usar selfen la ruta anidada,
como se muestra en el Listado 7-20.

Nombre de archivo: src/lib.rs


use std::io::{self, Write};
Listado 7-20: Combinación de las rutas del Listado 7-19 en una
sola usedeclaración

Esta línea trae std::ioy std::io::Writedentro del alcance.

El operador Glob

Si queremos traer todos los elementos públicos definidos en una ruta al alcance,
podemos especificar esa ruta seguida por el *operador glob:

use std::collections::*;
Rust
jueves, 22 de mayo de 2025 : Página 181 de 719

Esta usesentencia incorpora todos los elementos públicos definidos


en std::collectionsel ámbito actual. ¡Tenga cuidado al usar el operador glob! Glob
puede dificultar la identificación de los nombres dentro del ámbito y la ubicación
de un nombre en su programa.

El operador glob se utiliza a menudo durante las pruebas para traer todo lo que se
está probando al testsmódulo; hablaremos de eso en la sección “Cómo escribir
pruebas” en el Capítulo 11. El operador glob también se utiliza a veces como
parte del patrón de preludio: consulte la documentación de la biblioteca
estándar para obtener más información sobre ese patrón.

Separar módulos en archivos diferentes

Hasta ahora, todos los ejemplos de este capítulo definieron varios módulos en un
solo archivo. Cuando los módulos sean grandes, conviene trasladar sus
definiciones a un archivo aparte para facilitar la navegación por el código.

Por ejemplo, comencemos con el código del Listado 7-17, que tenía varios
módulos de restaurante. Extraeremos los módulos a archivos en lugar de tenerlos
todos definidos en el archivo raíz del crate. En este caso, el archivo raíz del crate
es src/lib.rs , pero este procedimiento también funciona con crates binarios cuyo
archivo raíz del crate es src/main.rs .

Primero, extraeremos el front_of_housemódulo a su propio archivo. Elimine el


código entre llaves del front_of_housemódulo, dejando solo la mod
front_of_house;declaración, para que src/lib.rs contenga el código que se muestra
en el Listado 7-21. Tenga en cuenta que esto no se compilará hasta que creemos
el archivo src/front_of_house.rs en el Listado 7-22.

Nombre de archivo: src/lib.rs

mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Listado 7-21: Declaración del front_of_housemódulo cuyo cuerpo estará
en src/front_of_house.rs
Rust
jueves, 22 de mayo de 2025 : Página 182 de 719

A continuación, coloque el código entre 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 encontró la declaración del módulo
en la raíz del paquete con el nombre front_of_house.

Nombre del archivo: src/front_of_house.rs


pub mod hosting {
pub fn add_to_waitlist() {}
}
Listado 7-22: Definiciones dentro del front_of_housemódulo
en src/front_of_house.rs

Tenga en cuenta que solo necesita cargar un archivo mediante


una moddeclaración una vez en el árbol de módulos. Una vez que el compilador
sabe que el archivo forma parte del proyecto (y dónde se encuentra el código en
el árbol de módulos gracias a la ubicación de la declaración mod ), los demás
archivos del proyecto deben hacer referencia al código del archivo cargado
mediante la ruta a su declaración, como se explica en la sección "Rutas para
hacer referencia a un elemento en el árbol de módulos" . En otras
palabras, nomod es una operación de "inclusión" que pueda haber visto en otros
lenguajes de programación.

A continuación, extraeremos el hostingmódulo a su propio archivo. El proceso es


ligeramente diferente, ya que hostinges un módulo hijo de front_of_house, no del
módulo raíz. Colocaremos el archivo hostingen un nuevo directorio que llevará el
nombre de sus antecesores en el árbol de módulos; en este
caso, src/front_of_house .

Para comenzar a movernos hosting, cambiamos src/front_of_house.rs para que


contenga solo la declaración del hostingmódulo:

Nombre del archivo: src/front_of_house.rs


pub mod hosting;

Luego creamos un directorio src/front_of_house y un archivo hosting.rs para


contener las definiciones realizadas en el hostingmódulo:

Nombre del archivo: src/front_of_house/hosting.rs


pub fn add_to_waitlist() {}
Rust
jueves, 22 de mayo de 2025 : Página 183 de 719

Si, en cambio, colocamos hosting.rs en el directorio src , el compilador esperaría


que el código de hosting.rs estuviera en un hostingmódulo declarado en la raíz del
paquete, y no como un front_of_housemódulo secundario. Las reglas del
compilador sobre qué archivos revisar para el código de cada módulo hacen que
los directorios y archivos coincidan más estrechamente con el árbol de módulos.

Rutas de archivo alternativas

Hasta ahora hemos cubierto las rutas de archivo más comunes que usa el
compilador de Rust, pero Rust también admite un estilo más antiguo de ruta de
archivo. Para un módulo llamado front_of_housedeclarado en la raíz del paquete,
el compilador buscará el código del módulo en:

 src/front_of_house.rs (lo que cubrimos)


 src/front_of_house/mod.rs (estilo antiguo, ruta aún compatible)

Para un módulo llamado hostingque es un submódulo de front_of_house, el


compilador buscará el código del módulo en:

 src/front_of_house/hosting.rs (lo que cubrimos)


 src/front_of_house/hosting/mod.rs (estilo antiguo, ruta aún compatible)

Si usas ambos estilos para el mismo módulo, recibirás un error de compilación.


Usar ambos estilos combinados para diferentes módulos del mismo proyecto está
permitido, pero podría resultar confuso para quienes navegan por el proyecto.

La principal desventaja del estilo que utiliza archivos llamados mod.rs es que su
proyecto puede terminar con muchos archivos llamados mod.rs , lo que puede
resultar confuso cuando los tiene abiertos en su editor al mismo tiempo.

Hemos movido el código de cada módulo a un archivo separado, y el árbol de


módulos permanece igual. Las llamadas a funciones eat_at_restaurantfuncionarán
sin modificaciones, aunque las definiciones se encuentren en archivos diferentes.
Esta técnica permite mover módulos a nuevos archivos a medida que aumentan
de tamaño.

Tenga en cuenta que la pub use crate::front_of_house::hostingdeclaración


en src/lib.rs tampoco ha cambiado ni useafecta a los archivos que se compilan
como parte del paquete. La modpalabra clave declara módulos, y Rust busca el
código que contiene en un archivo con el mismo nombre.
Rust
jueves, 22 de mayo de 2025 : Página 184 de 719
Resumen

Rust permite dividir un paquete en varias cajas y una caja en módulos para que
puedas hacer referencia a los elementos definidos en un módulo desde otro.
Puedes hacerlo especificando rutas absolutas o relativas. Estas rutas se pueden
incluir en el ámbito de aplicación mediante una usedeclaración, lo que permite
usar una ruta más corta para múltiples usos del elemento en ese ámbito. El
código del módulo es privado por defecto, pero puedes hacer públicas las
definiciones añadiendo la pubpalabra clave.

En el próximo capítulo, veremos algunas estructuras de datos de recopilación en


la biblioteca estándar que puedes usar en tu código perfectamente organizado.

Colecciones comunes

La biblioteca estándar de Rust incluye varias estructuras de datos muy útiles


llamadas colecciones . La mayoría de los demás tipos de datos representan un
valor específico, pero las colecciones pueden contener varios. A diferencia de los
tipos de matriz y tupla integrados, los datos a los que apuntan estas colecciones
se almacenan en el montón, lo que significa que no es necesario conocer la
cantidad de datos en tiempo de compilación y puede aumentar o disminuir a
medida que se ejecuta el programa. Cada tipo de colección tiene diferentes
capacidades y costos, y elegir la más adecuada para su situación actual es una
habilidad que desarrollará con el tiempo. En este capítulo, analizaremos tres
colecciones que se usan con frecuencia en los programas de Rust:

 Un vector le permite almacenar una cantidad variable de valores uno al lado


del otro.
 Una cadena es una colección de caracteres. Ya mencionamos el Stringtipo,
pero en este capítulo lo profundizaremos.
 Un mapa hash permite asociar un valor con una clave específica. Es una
implementación particular de una estructura de datos más general
llamada mapa .

Para obtener más información sobre los otros tipos de colecciones proporcionadas
por la biblioteca estándar, consulte la documentación .

Discutiremos cómo crear y actualizar vectores, cadenas y mapas hash, así como
también qué hace que cada uno sea especial.
Rust
jueves, 22 de mayo de 2025 : Página 185 de 719
Almacenamiento de listas de valores con vectores

El primer tipo de colección que analizaremos es Vec<T>, también conocido


como vector . Los vectores permiten almacenar más de un valor en una única
estructura de datos que los coloca uno junto al otro en la memoria. Los vectores
solo pueden almacenar valores del mismo tipo. Son útiles cuando se tiene una
lista de artículos, como las líneas de texto de un archivo o los precios de los
artículos en un carrito de compra.

Creando un nuevo vector

Para crear un nuevo vector vacío, llamamos a la Vec::newfunción, como se


muestra en el Listado 8-1.

let v: Vec<i32> = Vec::new();


Listado 8-1: Creación de un nuevo vector vacío para contener valores de tipoi32

Tenga en cuenta que añadimos una anotación de tipo. Dado que no insertamos
ningún valor en este vector, Rust desconoce qué tipo de elementos queremos
almacenar. Este es un punto importante. Los vectores se implementan mediante
genéricos; explicaremos cómo usarlos con sus propios tipos en el Capítulo 10. Por
ahora, tenga en cuenta que el Vec<T>tipo proporcionado por la biblioteca
estándar puede contener cualquier tipo. Al crear un vector para un tipo específico,
podemos especificarlo entre corchetes angulares. En el Listado 8-1, indicamos a
Rust que la Vec<T>instrucción in vcontendrá elementos de ese i32tipo.

Con mayor frecuencia, creará un vector Vec<T>con valores iniciales y Rust


inferirá el tipo de valor que desea almacenar, por lo que rara vez necesitará
realizar esta anotación de tipo. Rust proporciona convenientemente la vec!macro,
que creará un nuevo vector que contiene los valores que le asigne. El Listado 8-2
crea un nuevo vector Vec<i32>que contiene los valores 1, 2y 3. El tipo entero
se i32 debe a que es el tipo entero predeterminado, como se explicó en la
sección "Tipos de datos" del Capítulo 3.

let v = vec![1, 2, 3];


Listado 8-2: Creación de un nuevo vector que contiene valores

Debido a que hemos proporcionado i32valores iniciales, Rust puede inferir que el
tipo de v esVec<i32> , y la anotación de tipo no es necesaria. A continuación,
veremos cómo modificar un vector.
Rust
jueves, 22 de mayo de 2025 : Página 186 de 719
Actualizar un vector

Para crear un vector y luego agregarle elementos, podemos utilizar


el pushmétodo, como se muestra en el Listado 8-3.

let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);
Listado 8-3: Uso del pushmétodo para agregar valores a un vector

Al igual que con cualquier variable, si queremos poder cambiar su valor, debemos
hacerla mutable usando la mutpalabra clave, como se explica en el Capítulo 3.
Los números que colocamos dentro son todos de tipo i32, y Rust lo infiere de los
datos, por lo que no necesitamos laVec<i32> anotación.

Elementos de lectura de vectores

Hay dos formas de hacer referencia a un valor almacenado en un vector:


mediante indexación o utilizando elget método. En los siguientes ejemplos,
hemos anotado los tipos de valores que devuelven estas funciones para mayor
claridad.

El listado 8-4 muestra ambos métodos para acceder a un valor en un vector, con
la sintaxis de indexación y el getmétodo.

let v = vec![1, 2, 3, 4, 5];

let third: &i32 = &v[2];


println!("The third element is {third}");

let third: Option<&i32> = v.get(2);


match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
Listado 8-4: Uso de la sintaxis de indexación y uso del getmétodo para acceder a
un elemento en un vector

Tenga en cuenta algunos detalles. Usamos el valor del índice de 2para obtener el
tercer elemento, ya que los vectores se indexan por número, comenzando desde
cero. Usar &y [] nos da una referencia al elemento en el valor del índice. Al usar
Rust
jueves, 22 de mayo de 2025 : Página 187 de 719

el get método con el índice como argumento, obtenemos un Option<&T>que


podemos usar conmatch .

Rust ofrece estas dos maneras de referenciar un elemento para que puedas elegir
cómo se comporta el programa al intentar usar un valor de índice fuera del rango
de elementos existentes. A modo de ejemplo, veamos qué sucede cuando
tenemos un vector de cinco elementos e intentamos acceder a un elemento en el
índice 100 con cada técnica, como se muestra en el Listado 8-5.

let v = vec![1, 2, 3, 4, 5];

let does_not_exist = &v[100];


let does_not_exist = v.get(100);
Listado 8-5: Intento de acceder al elemento en el índice 100 en un vector que
contiene cinco elementos

Al ejecutar este código, el primer []método provocará un pánico en el programa


porque hace referencia a un elemento inexistente. Este método es ideal cuando
se desea que el programa se bloquee si se intenta acceder a un elemento más
allá del final del vector.

Cuando getse pasa al método un índice fuera del vector, este retorna Nonesin
entrar en pánico. Este método se usaría si, en circunstancias normales, se pudiera
acceder a un elemento fuera del rango del vector. El código tendría entonces
lógica para gestionar la presencia de Some(&element)o None, como se explicó en
el Capítulo 6. Por ejemplo, el índice podría provenir de una persona que introduce
un número. Si accidentalmente introduce un número demasiado grande y el
programa obtiene un Nonevalor, se podría indicar al usuario cuántos elementos
hay en el vector actual y darle otra oportunidad para introducir un valor válido.
Esto sería más intuitivo que bloquear el programa por un error tipográfico.

Cuando el programa tiene una referencia válida, el verificador de préstamos


aplica las reglas de propiedad y préstamo (descritas en el Capítulo 4) para
garantizar que esta referencia y cualquier otra referencia al contenido del vector
sigan siendo válidas. Recuerde la regla que establece que no se pueden tener
referencias mutables e inmutables en el mismo ámbito. Esta regla se aplica en el
Listado 8-6, donde mantenemos una referencia inmutable al primer elemento de
un vector e intentamos añadir un elemento al final. Este programa no funcionará
Rust
jueves, 22 de mayo de 2025 : Página 188 de 719

si también intentamos hacer referencia a ese elemento más adelante en la


función.

let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0];

v.push(6);

println!("The first element is: {first}");


Listado 8-6: Intentar agregar un elemento a un vector mientras se mantiene una
referencia a un elemento

Compilar este código generará este error:

$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
immutable
--> src/main.rs:6:5
|
4| let first = &v[0];
| - immutable borrow occurs here
5|
6| v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7|
8| println!("The first element is: {first}");
| ------- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error

El código del Listado 8-6 podría parecer funcional: ¿por qué una referencia al
primer elemento debería preocuparse por los cambios al final del vector? Este
error se debe al funcionamiento de los vectores: dado que los vectores colocan
los valores uno junto al otro en memoria, añadir un nuevo elemento al final del
vector podría requerir asignar nueva memoria y copiar los elementos antiguos al
nuevo espacio si no hay suficiente espacio para colocar todos los elementos uno
junto al otro donde se almacena actualmente el vector. En ese caso, la referencia
al primer elemento apuntaría a memoria desasignada. Las reglas de préstamo
evitan que los programas se encuentren en esa situación.
Rust
jueves, 22 de mayo de 2025 : Página 189 de 719

Nota: Para obtener más información sobre los detalles de implementación


del Vec<T>tipo, consulte “El Rustonomicon” .

Iterando sobre los valores de un vector

Para acceder a cada elemento de un vector, iteraremos por todos los elementos
en lugar de usar índices para acceder a cada uno. El listado 8-7 muestra cómo
usar un forbucle para obtener referencias inmutables a cada elemento de un
vector de i32valores e imprimirlas.

let v = vec![100, 32, 57];


for i in &v {
println!("{i}");
}
Listado 8-7: Impresión de cada elemento en un vector iterando sobre los
elementos usando un forbucle

También podemos iterar sobre las referencias mutables a cada elemento de un


vector mutable para realizar cambios en todos los elementos. El forbucle del
Listado 8-8 añadirá valores 50a cada elemento.

let mut v = vec![100, 32, 57];


for i in &mut v {
*i += 50;
}
Listado 8-8: Iteración sobre referencias mutables a elementos en un vector

Para cambiar el valor al que se refiere la referencia mutable, debemos usar


el *operador de desreferencia para obtener el valor iantes de poder usar
el += operador. Hablaremos más sobre el operador de desreferencia en la
sección "Seguimiento del puntero al valor con el operador de desreferencia" del
capítulo 15.

Iterar sobre un vector, ya sea de forma inmutable o mutable, es seguro gracias a


las reglas del verificador de préstamos. Si intentáramos insertar o eliminar
elementos en los for cuerpos de los bucles de los Listados 8-7 y 8-8, obtendríamos
un error de compilación similar al del código del Listado 8-6. La referencia al
vector que forcontiene el bucle impide la modificación simultánea de todo el
vector.

Uso de una enumeración para almacenar múltiples tipos


Rust
jueves, 22 de mayo de 2025 : Página 190 de 719

Los vectores solo pueden almacenar valores del mismo tipo. Esto puede ser un
inconveniente; sin duda, existen casos prácticos en los que es necesario
almacenar una lista de elementos de diferentes tipos. Afortunadamente, las
variantes de una enumeración se definen bajo el mismo tipo de enumeración, por
lo que cuando necesitamos que un tipo represente elementos de diferentes tipos,
podemos definir y usar una enumeración.

Por ejemplo, supongamos que queremos obtener valores de una fila en una hoja
de cálculo cuyas columnas contienen enteros, números de punto flotante y
cadenas. Podemos definir una enumeración cuyas variantes contendrán los
diferentes tipos de valor, y todas las variantes de la enumeración se considerarán
del mismo tipo: el de la enumeración. Después, podemos crear un vector que
contenga esa enumeración y, por lo tanto, contenga diferentes tipos. Esto se
demuestra en el Listado 8-9.

enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}

let row = vec![


SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
Listado 8-9: Definición de un enumpara almacenar valores de diferentes tipos en
un vector

Rust necesita saber qué tipos estarán en el vector en tiempo de compilación para
saber exactamente cuánta memoria en el montón se necesitará para almacenar
cada elemento. También debemos ser explícitos sobre los tipos permitidos en
este vector. Si Rust permitiera que un vector contuviera cualquier tipo, existiría la
posibilidad de que uno o más de estos tipos causaran errores en las operaciones
realizadas con los elementos del vector. Usar una enumeración más
una matchexpresión significa que Rust garantizará en tiempo de compilación que
se gestionen todos los casos posibles, como se explica en el Capítulo 6.

Si desconoce el conjunto completo de tipos que un programa obtendrá en tiempo


de ejecución para almacenar en un vector, la técnica de enumeración no
Rust
jueves, 22 de mayo de 2025 : Página 191 de 719

funcionará. En su lugar, puede usar un objeto de rasgo, que abordaremos en el


capítulo 18.

Ahora que hemos analizado algunas de las formas más comunes de usar
vectores, asegúrese de revisar la documentación de la API para conocer todos los
métodos útiles definidos en Vec<T>la biblioteca estándar. Por ejemplo, además
de push, un popmétodo elimina y devuelve el último elemento.

Al eliminar un vector se eliminan sus elementos

Al igual que cualquier otro struct, un vector se libera cuando queda fuera del
alcance, como se indica en el Listado 8-10.

{
let v = vec![1, 2, 3, 4];

// do stuff with v
} // <- v goes out of scope and is freed here
Listado 8-10: Mostrando dónde se colocan el vector y sus elementos

Al eliminar un vector, también se elimina todo su contenido, lo que significa que


se borrarán los enteros que contiene. El verificador de préstamos garantiza que
las referencias al contenido de un vector solo se utilicen mientras el vector sea
válido.

¡Pasemos al siguiente tipo de colección String:!

Almacenamiento de texto codificado en UTF-8 con cadenas

Hablamos de cadenas en el Capítulo 4, pero las analizaremos con más


profundidad ahora. Los nuevos usuarios de Rust suelen quedarse atascados en
cadenas por una combinación de tres razones: la propensión de Rust a exponer
posibles errores, que las cadenas sean una estructura de datos más compleja de
lo que muchos programadores creen, y UTF-8. Estos factores se combinan de una
manera que puede parecer difícil si vienes de otros lenguajes de programación.

Analizamos las cadenas en el contexto de las colecciones, ya que se implementan


como una colección de bytes, además de algunos métodos que proporcionan
funcionalidad útil cuando estos bytes se interpretan como texto. En esta sección,
abordaremos las operaciones que Stringrealiza cada tipo de colección, como la
creación, la actualización y la lectura. También analizaremos sus diferencias
Rust
jueves, 22 de mayo de 2025 : Página 192 de 719

con String respecto a otras colecciones, en particular, cómo la indexación en un


archivo Stringse complica debido a las diferencias entre la interpretación
de Stringlos datos por parte de las personas y las computadoras.

¿Qué es una cadena?

Primero definiremos el término cadena . Rust solo tiene un tipo de cadena en el


lenguaje principal: la porción de cadena, strque suele verse en su forma
prestada &str. En el capítulo 4, hablamos de las porciones de cadena , que son
referencias a datos de cadena codificados en UTF-8 almacenados en otro lugar.
Los literales de cadena, por ejemplo, se almacenan en el binario del programa y,
por lo tanto, son porciones de cadena.

El Stringtipo, proporcionado por la biblioteca estándar de Rust en lugar de estar


codificado en el lenguaje principal, es un tipo de cadena ampliable, mutable,
propio y codificado en UTF-8. Cuando los habitantes de Rust hablan de "cadenas"
en Rust, podrían referirse a los tipos Stringde fragmentos de cadena &str, no solo
a uno de ellos. Aunque esta sección trata principalmente sobre String, ambos
tipos se usan ampliamente en la biblioteca estándar de Rust, y tanto Stringlos
fragmentos de cadena como están codificados en UTF-8.

Creando una nueva cadena

Muchas de las mismas operaciones disponibles con también Vec<T>están


disponibles con porque se implementa como un contenedor alrededor de un
vector de bytes con garantías, restricciones y capacidades adicionales. Un
ejemplo de una función que funciona de la misma manera con y es la función para
crear una instancia, que se muestra en el Listado 8-
11.StringStringVec<T>Stringnew

let mut s = String::new();


Listado 8-11: Creación de un nuevo espacio vacíoString

Esta línea crea una nueva cadena vacía llamada s, en la que podemos cargar
datos. A menudo, tendremos datos iniciales con los que queremos iniciar la
cadena. Para ello, usamos el to_stringmétodo , disponible en cualquier tipo que
implemente el Displayatributo, como lo hacen los literales de cadena. El Listado 8-
12 muestra dos ejemplos.

let data = "initial contents";


Rust
jueves, 22 de mayo de 2025 : Página 193 de 719
let s = data.to_string();

// the method also works on a literal directly:


let s = "initial contents".to_string();
Listado 8-12: Uso del to_stringmétodo para crear Stringa partir de un literal de
cadena

Este código crea una cadena que contiene initial contents.

También podemos usar la función String::frompara crear un Stringa partir de un


literal de cadena. El código del Listado 8-13 es equivalente al del Listado 8-12,
que usa to_string.

let s = String::from("initial contents");


Listado 8-13: Uso de la String::fromfunción para crear Stringa partir de un literal
de cadena

Dado que las cadenas se usan para tantas cosas, podemos usar diversas API
genéricas para cadenas, lo que nos brinda una gran variedad de opciones.
Algunas pueden parecer redundantes, ¡pero todas tienen su utilidad! En este
caso, String::fromy to_stringhacen lo mismo, así que la elección es cuestión de
estilo y legibilidad.

Recuerde que las cadenas están codificadas en UTF-8, por lo que podemos incluir
en ellas cualquier dato codificado correctamente, como se muestra en el Listado
8-14.

let hello = String::from("‫;)"السالم عليكم‬


let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("‫;)"שלום‬
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
Listado 8-14: Almacenamiento de saludos en diferentes idiomas en cadenas

Todos estos son Stringvalores válidos.

Actualizar una cadena


Rust
jueves, 22 de mayo de 2025 : Página 194 de 719

Un objeto A Stringpuede aumentar de tamaño y su contenido puede cambiar, al


igual que el de un objeto A Vec<T>, si se le introducen más datos. Además, se
puede usar fácilmente el +operador o la format!macro para
concatenar Stringvalores.

Agregar a una cadena con push_strypush

Podemos hacer crecer a Stringusando el push_strmétodo para agregar una


porción de cadena, como se muestra en el Listado 8-15.

let mut s = String::from("foo");


s.push_str("bar");
Listado 8-15: Anexar una porción de cadena a un Stringusando el push_strmétodo

Después de estas dos líneas, scontendrá foobar. El push_strmétodo toma una


porción de cadena porque no queremos necesariamente tomar posesión del
parámetro. Por ejemplo, en el código del Listado 8-16, queremos poder
usar s2después de añadir su contenido a s1.

let mut s1 = String::from("foo");


let s2 = "bar";
s1.push_str(s2);
println!("s2 is {s2}");
Listado 8-16: Uso de una porción de cadena después de agregar su contenido a
unaString

Si el push_strmétodo tomara posesión de s2, no podríamos imprimir su valor en la


última línea. Sin embargo, este código funciona como esperábamos.

El pushmétodo toma un solo carácter como parámetro y lo agrega a String. El


Listado 8-17 agrega la letra l a a Stringusando el push método.

let mut s = String::from("lo");


s.push('l');
Listado 8-17: Agregar un carácter a un Stringvalor usandopush

Como resultado, scontendrá lol.

Concatenación con el +operador o la format!macro

A menudo, querrá combinar dos cadenas existentes. Una forma de hacerlo es


usar el +operador, como se muestra en el Listado 8-18.
Rust
jueves, 22 de mayo de 2025 : Página 195 de 719
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
Listado 8-18: Uso del +operador para combinar dos Stringvalores en un
nuevo Stringvalor

La cadena s3contendrá Hello, world!. El motivo s1deja de ser válido tras la


adición, y la razón por la que usamos una referencia a s2, se relaciona con la
firma del método que se llama al usar el +operador. El +operador usa
el addmétodo, cuya firma se ve así:

fn add(self, s: &str) -> String {

En la biblioteca estándar, verá addque se definen mediante genéricos y tipos


asociados. Aquí, hemos sustituido tipos concretos, que es lo que ocurre al llamar
a este método con Stringvalores. Analizaremos los genéricos en el capítulo 10.
Esta firma nos da las claves necesarias para comprender las partes complejas
del +operador.

Primero, s2tiene un &, lo que significa que estamos agregando una referencia de
la segunda cadena a la primera. Esto se debe al sparámetro de la add función:
solo podemos agregar a &stra a String; no podemos sumar dos String valores.
Pero espera: el tipo de &s2es &String, no &str, como se especifica en el segundo
parámetro de add. Entonces, ¿por qué se compila el Listado 8-18?

La razón por la que podemos usar [nombre del método] &s2en la llamada a
[nombre del método] addes que el compilador
puede convertir el &Stringargumento en un [ nombre &strdel método]. Al llamar
al add método, Rust utiliza una conversión de desreferencia , que en este caso se
convierte &s2en [nombre del método &s2[..]]. Analizaremos la conversión de
desreferencia con más detalle en el Capítulo 15. Dado que [nombre del
método] addno toma posesión del sparámetro, s2seguirá siendo
válido String después de esta operación.

En segundo lugar, podemos ver en la firma que addtoma posesión


de self porque selfno tiene un &. Esto significa que s1en el Listado 8-18 se moverá
a la addllamada y dejará de ser válido después de eso. Por lo tanto, aunque let s3
= s1 + &s2;parezca que copiará ambas cadenas y creará una nueva, esta
declaración en realidad toma posesión de s1, agregando una copia del contenido
des2 y luego devuelve la propiedad del resultado. En otras palabras, parece que
Rust
jueves, 22 de mayo de 2025 : Página 196 de 719

está haciendo muchas copias, pero no es así; la implementación es más eficiente


que copiar.

Si necesitamos concatenar varias cadenas, el comportamiento del +operador se


vuelve complicado:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;

En este punto, sserá tic-tac-toe. Con tantos caracteres +y " , es difícil ver qué
sucede. Para combinar cadenas de forma más compleja, podemos usar la format!
macro:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{s1}-{s2}-{s3}");

Este código también se establece sen tic-tac-toe. La format!macro funciona


como println!, pero en lugar de mostrar la salida en pantalla, devuelve
un Stringcon el contenido. La versión del código que usa format!es mucho más
fácil de leer, y el código generado por la format!macro usa referencias para que
esta llamada no tome posesión de ninguno de sus parámetros.

Indexación en cadenas

En muchos otros lenguajes de programación, acceder a caracteres individuales de


una cadena mediante referencias por índice es una operación válida y común. Sin
embargo, si intenta acceder a partes de una Stringsintaxis de indexación en Rust,
obtendrá un error. Considere el código inválido del Listado 8-19.

let s1 = String::from("hello");
let h = s1[0];
Listado 8-19: Intento de utilizar la sintaxis de indexación con una cadena

Este código dará como resultado el siguiente error:


Rust
jueves, 22 de mayo de 2025 : Página 197 de 719
$ 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 lo demuestran: las cadenas de Rust no admiten la indexación.


¿Pero por qué no? Para responder a esta pregunta, necesitamos analizar cómo
Rust almacena las cadenas en memoria.

Representación interna

A Stringes un contenedor sobre a Vec<u8>. Veamos algunas de nuestras cadenas


de ejemplo UTF-8 correctamente codificadas del Listado 8-14. Primero, esta:

let hello = String::from("Hola");

En este caso, lenserá 4, lo que significa el vector que almacena la


cadena "Hola" tiene una longitud de 4 bytes. Cada una de estas letras ocupa un
byte al codificarse en UTF-8. Sin embargo, la siguiente línea podría sorprenderle
(tenga en cuenta que esta cadena comienza con la letra cirílica mayúscula Ze , no
con el número 3):

let hello = String::from("Здравствуйте");

Si te preguntaran la longitud de la cadena, 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, ya que cada valor escalar Unicode de esa
cadena ocupa 2 bytes de almacenamiento. Por lo tanto, un índice en los bytes de
Rust
jueves, 22 de mayo de 2025 : Página 198 de 719

la cadena no siempre se correlacionará con un valor escalar Unicode válido. Para


demostrarlo, considera este código de Rust no válido:

let hello = "Здравствуйте";


let answer = &hello[0];

Ya sabes que answerno será Зla primera letra. Cuando se codifica en UTF-8, el
primer byte de Зes208 y el segundo es 151, por lo que parecería
que answerdebería ser 208, pero 208no es un carácter válido por sí mismo. 208Es
probable que el usuario no desee obtener la primera letra de esta cadena; sin
embargo, ese es el único dato que Rust tiene en el índice de byte 0. Los usuarios
generalmente no quieren que se devuelva el valor del byte, incluso si la cadena
contiene solo letras latinas: si &"hi"[0]fuera un código válido que devolviera el
valor del byte, devolvería 104, no h.

La respuesta, entonces, es que para evitar devolver un valor inesperado y causar


errores que podrían no descubrirse inmediatamente, Rust no compila este código
en absoluto y evita malentendidos al principio del proceso de desarrollo.

¡Bytes, valores escalares y clústeres de grafemas! ¡Dios mío!

Otro punto sobre UTF-8 es que en realidad hay tres formas relevantes de ver las
cadenas desde la perspectiva de Rust: como bytes, valores escalares y grupos de
grafemas (lo más cercano a lo que llamaríamos letras ).

Si observamos la palabra hindi “नमस्ते” escrita en escritura devanagari, se


almacena como un vector de u8valores que se ve así:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

Esos son 18 bytes, y así es como las computadoras almacenan estos datos. Si los
consideramos como valores escalares Unicode, que son el chartipo de Rust, esos
bytes se ven así:

['न', 'म', 'स', '्', 'त', 'े']

Aquí hay seis charvalores, pero el cuarto y el sexto no son letras: son diacríticos
que no tienen sentido por sí solos. Finalmente, si los consideramos como grupos
Rust
jueves, 22 de mayo de 2025 : Página 199 de 719

de grafemas, obtendríamos lo que una persona llamaría las cuatro letras que
componen la palabra hindi:

["न", "म", "स्", "ते"]

Rust proporciona diferentes formas de interpretar los datos de cadenas sin


procesar que almacenan las computadoras para que cada programa pueda elegir
la interpretación que necesita, sin importar en qué lenguaje humano estén los
datos.

Una última razón por la que Rust no nos permite indexar en unString para obtener
un carácter es que se espera que las operaciones de indexación siempre tomen
un tiempo constante (O(1)). Sin embargo, no es posible garantizar ese
rendimiento con a String, ya que Rust tendría que recorrer el contenido desde el
principio hasta el índice para determinar cuántos caracteres válidos hay.

Cortar cuerdas

Indexar una cadena suele ser una mala idea, ya que no está claro cuál debería ser
el tipo de retorno de la operación: un valor de byte, un carácter, un grupo de
grafemas o un fragmento de cadena. Por lo tanto, si realmente necesita usar
índices para crear fragmentos de cadena, Rust le pide que sea más específico.

En lugar de indexar []con un solo número, puedes utilizar []un rango para crear
una porción de cadena que contenga bytes específicos:

let hello = "Здравствуйте";

let s = &hello[0..4];

Aquí, shabrá un &strque contiene los primeros cuatro bytes de la cadena.


Anteriormente, mencionamos que cada uno de estos caracteres ocupaba dos
bytes, lo que significa que... s será Зд.

Si intentáramos cortar solo una parte de los bytes de un carácter con algo
como &hello[0..1], Rust entraría en pánico en el tiempo de ejecución de la misma
manera que si se accediera a un índice no válido en un vector:

$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/collections`
Rust
jueves, 22 de mayo de 2025 : Página 200 de 719
thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Debe tener cuidado al crear segmentos de cadenas con rangos, ya que hacerlo
puede provocar que su programa se bloquee.

Métodos para iterar sobre cadenas

La mejor manera de operar con fragmentos de cadenas es especificar


explícitamente si se desean caracteres o bytes. Para valores escalares Unicode
individuales, utilice el charsmétodo. Llamar charsa "Зд" separa y devuelve dos
valores de tipo char, y se puede iterar sobre el resultado para acceder a cada
elemento:

for c in "Зд".chars() {
println!("{c}");
}

Este código imprimirá lo siguiente:

З
д

Como alternativa, el bytesmétodo devuelve cada byte sin procesar, lo que podría
ser apropiado para su dominio:

for b in "Зд".bytes() {
println!("{b}");
}

Este código imprimirá los cuatro bytes que componen esta cadena:

208
151
208
180

Pero asegúrese de recordar que los valores escalares Unicode válidos pueden
estar compuestos de más de un byte.
Rust
jueves, 22 de mayo de 2025 : Página 201 de 719

Obtener grupos de grafemas a partir de cadenas, como en el caso del script


devanagari, es complejo, por lo que la biblioteca estándar no ofrece esta
funcionalidad. Si necesita esta funcionalidad, puede encontrar crates en crates.io.

Las cuerdas no son tan simples

En resumen, las cadenas son complejas. Los distintos lenguajes de programación


toman decisiones diferentes sobre cómo presentar esta complejidad al
programador. Rust ha optado por gestionarlas correctamente.String datos sea el
comportamiento predeterminado de todos sus programas, lo que implica que los
programadores deben prestar más atención al manejo de datos UTF-8 desde el
principio. Esta compensación expone una mayor complejidad de las cadenas que
la que se aprecia en otros lenguajes de programación, pero evita tener que
gestionar errores relacionados con caracteres no ASCII más adelante en el ciclo
de desarrollo.

La buena noticia es que la biblioteca estándar ofrece una gran cantidad de


funcionalidades basadas en los tipos Stringy &strpara ayudar a gestionar
correctamente estas situaciones complejas. Asegúrese de consultar la
documentación para encontrar métodos útiles, como containsbuscar en una
cadena yreplace sustituir partes de una cadena por otra.

Pasemos a algo un poco menos complejo: ¡mapas hash!

Almacenamiento de claves con valores asociados en mapas hash

La última de nuestras colecciones comunes es el mapa hash . El tipo HashMap<K,


V> almacena una asignación de claves de tipo Ka valores de tipo Vmediante
una función hash , que determina cómo se colocan estas claves y valores en
memoria. Muchos lenguajes de programación admiten este tipo de estructura de
datos, pero suelen usar un nombre diferente, como hash , mapa , objeto , tabla
hash , diccionario o matriz asociativa , por nombrar solo algunos.

Los mapas hash son útiles cuando se buscan datos no mediante un índice, como
ocurre con los vectores, sino mediante una clave de cualquier tipo. Por ejemplo,
en un partido, se podría registrar la puntuación de cada equipo en un mapa hash
donde cada clave es el nombre del equipo y los valores son la puntuación de cada
equipo. Dado el nombre de un equipo, se puede recuperar su puntuación.
Rust
jueves, 22 de mayo de 2025 : Página 202 de 719

En esta sección, repasaremos la API básica de los mapas hash, pero las funciones
definidas HashMap<K, V>por la biblioteca estándar ofrecen muchas más
ventajas. Como siempre, consulte la documentación de la biblioteca estándar
para obtener más información.

Creación de un nuevo mapa hash

Una forma de crear un mapa hash vacío es usar newy para agregar elementos
con insert. En el Listado 8-20, registramos las puntuaciones de dos equipos
llamados Azul y Amarillo . El equipo Azul empieza con 10 puntos y el equipo
Amarillo con 50.

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Listado 8-20: Creación de un nuevo mapa hash e inserción de algunas claves y
valores

Tenga en cuenta que primero debemos obtener usela HashMapsección de


colecciones de la biblioteca estándar. De nuestras tres colecciones comunes, esta
es la que menos se usa, por lo que no se incluye en las funciones que se incluyen
automáticamente en el preludio. Los mapas hash también tienen menos
compatibilidad con la biblioteca estándar; por ejemplo, no hay una macro
integrada para construirlos.

Al igual que los vectores, los mapas hash almacenan sus datos en el montículo.
Este HashMapcontiene claves de tipo Stringy valores de tipo i32. Al igual que los
vectores, los mapas hash son homogéneos: todas las claves y todos los valores
deben ser del mismo tipo.

Acceder a valores en un mapa hash

Podemos obtener un valor del mapa hash proporcionando su clave al get método,
como se muestra en el Listado 8-21.

use std::collections::HashMap;

let mut scores = HashMap::new();


Rust
jueves, 22 de mayo de 2025 : Página 203 de 719
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");


let score = scores.get(&team_name).copied().unwrap_or(0);
Listado 8-21: Acceso a la puntuación del equipo Azul almacenada en el mapa
hash

Aquí, scorese tendrá el valor asociado con el equipo Azul y el resultado será 10.
El getmétodo devuelve un Option<&V>; si no hay ningún valor para esa clave en
el mapa hash, getdevolverá None. Este programa gestiona
esto Optionllamando copieda ``` para obtener un`` Option<i32>en lugar de
un`` Option<&i32>y luego unwrap_orestableciéndolo scorea cero si scoresno hay
una entrada para la clave.

Podemos iterar sobre cada par clave-valor en un mapa hash de manera similar a
como lo hacemos con los vectores, usando un forbucle:

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

for (key, value) in &scores {


println!("{key}: {value}");
}

Este código imprimirá cada par en un orden arbitrario:

Yellow: 50
Blue: 10

Mapas hash y propiedad

Para los tipos que implementan el Copyrasgo, como i32, los valores se copian en
el mapa hash. Para los valores propios, como String, se moverán y el mapa hash
será el propietario de dichos valores, como se muestra en el Listado 8-22.

use std::collections::HashMap;

let field_name = String::from("Favorite color");


let field_value = String::from("Blue");
Rust
jueves, 22 de mayo de 2025 : Página 204 de 719

let mut map = HashMap::new();


map.insert(field_name, field_value);
// field_name and field_value are invalid at this point, try using them and
// see what compiler error you get!
Listado 8-22: Muestra que las claves y los valores son propiedad del mapa hash
una vez que se insertan

No podemos utilizar las variables field_namey field_valuedespués de que se hayan


movido al mapa hash con la llamada a insert.

Si insertamos referencias a valores en el mapa hash, estos no se moverán a él.


Los valores a los que apuntan las referencias deben ser válidos al menos mientras
el mapa hash sea válido. Analizaremos estos temas con más detalle en la
sección "Validación de referencias con tiempos de vida" del capítulo 10.

Actualización de un mapa hash

Aunque la cantidad de pares de clave y valor se puede aumentar, cada clave


única solo puede tener un valor asociado a la vez (pero no al revés: por ejemplo,
tanto el equipo Azul como el equipo Amarillo podrían tener el
valor 10 almacenado en el scoresmapa hash).

Al cambiar los datos de un mapa hash, debe decidir cómo gestionar el caso en
que una clave ya tenga un valor asignado. Puede reemplazar el valor anterior por
el nuevo, ignorándolo por completo. Puede conservar el valor anterior e ignorar el
nuevo, añadiendo este último solo si la clave aún no tiene un valor. O bien, puede
combinar el valor anterior con el nuevo. ¡Veamos cómo hacerlo!

Sobrescribir un valor

Si insertamos una clave y un valor en un mapa hash y luego insertamos esa


misma clave con un valor diferente, se reemplazará el valor asociado a esa clave.
Aunque el código del Listado 8-23 se invoca insertdos veces, el mapa hash solo
contendrá un par clave-valor, ya que insertamos el valor de la clave del equipo
Azul en ambas ocasiones.

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
Rust
jueves, 22 de mayo de 2025 : Página 205 de 719
scores.insert(String::from("Blue"), 25);

println!("{scores:?}");
Listado 8-23: Reemplazo de un valor almacenado con una clave particular

Este código imprimirá {"Blue": 25}. El valor original de 10se ha sobrescrito.

Agregar una clave y un valor solo si no hay una clave presente

Es común verificar si una clave particular ya existe en el mapa hash con un valor
y luego tomar las siguientes acciones: si la clave existe en el mapa hash, el valor
existente debe permanecer como está; si la clave no existe, insertarla y un valor
para ella.

Los mapas hash tienen una API especial para esto, llamada ``` entry, que toma la
clave que se desea verificar como parámetro. El valor de retorno del entrymétodo
es una enumeración llamada ``` Entryque representa un valor que podría existir o
no. Supongamos que queremos verificar si la clave del equipo amarillo tiene un
valor asociado. Si no lo tiene, insertamos el valor ``` 50, y lo mismo para el
equipo azul. Usando la entryAPI, el código se parece al Listado 8-24.

use std::collections::HashMap;

let mut scores = HashMap::new();


scores.insert(String::from("Blue"), 10);

scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);

println!("{scores:?}");
Listado 8-24: Uso del entrymétodo para insertar solo si la clave aún no tiene un
valor

El or_insertmétodo on Entryestá definido para devolver una referencia mutable al


valor de la Entryclave correspondiente si dicha clave existe. De no existir, inserta
el parámetro como el nuevo valor de dicha clave y devuelve una referencia
mutable al nuevo valor. Esta técnica es mucho más sencilla que escribir la lógica
nosotros mismos y, además, se integra mejor con el verificador de préstamos.

Al ejecutar el código del Listado 8-24, se imprimirá {"Yellow": 50, "Blue": 10}. La
primera llamada a entryinsertará la clave del equipo Amarillo con el valor, 50ya
Rust
jueves, 22 de mayo de 2025 : Página 206 de 719

que este no tiene un valor. La segunda llamada a entryno modificará el mapa


hash, ya que el equipo Azul ya tiene el valor 10.

Actualización de un valor basado en el valor anterior

Otro uso común de los mapas hash es consultar el valor de una clave y
actualizarlo según el valor anterior. Por ejemplo, el Listado 8-25 muestra código
que cuenta cuántas veces aparece cada palabra en un texto. Usamos un mapa
hash con las palabras como claves e incrementamos el valor para registrar
cuántas veces la hemos visto. Si es la primera vez que vemos una palabra,
primero insertamos el valor 0.

use std::collections::HashMap;

let text = "hello world wonderful world";

let mut map = HashMap::new();

for word in text.split_whitespace() {


let count = map.entry(word).or_insert(0);
*count += 1;
}

println!("{map:?}");
Listado 8-25: Conteo de ocurrencias de palabras usando un mapa hash que
almacena palabras y las cuenta

Este código imprimirá {"world": 2, "hello": 1, "wonderful": 1}. Es posible que vea
los mismos pares clave-valor impresos en un orden diferente: recuerde que en la
sección "Acceso a valores en un mapa hash" se explicó que la iteración sobre un
mapa hash se realiza en un orden arbitrario.

El split_whitespacemétodo devuelve un iterador sobre subsecciones, separadas


por espacios, del valor en text. El or_insertmétodo devuelve una referencia
mutable ( &mut V) al valor de la clave especificada. Aquí, almacenamos esa
referencia mutable en la countvariable, por lo que para asignarla a ese valor,
primero debemos desreferenciarla countmediante el asterisco ( *). La referencia
mutable queda fuera del ámbito al final del forbucle, por lo que todos estos
cambios son seguros y permitidos por las reglas de préstamo.

Funciones hash
Rust
jueves, 22 de mayo de 2025 : Página 207 de 719

De forma predeterminada, HashMaputiliza una función hash llamada SipHash que


puede proporcionar resistencia a ataques de denegación de servicio (DoS) que
involucran tablas hash 1 . Este no es el algoritmo hash más rápido disponible, pero
la compensación por una mejor seguridad que viene con la caída en el
rendimiento vale la pena. Si perfila su código y encuentra que la función hash
predeterminada es demasiado lenta para sus propósitos, puede cambiar a otra
función especificando un hasher diferente. Un hasher es un tipo que implementa
el BuildHashertrait. Hablaremos sobre traits y cómo implementarlos en el Capítulo
10 . No necesariamente tiene que implementar su propio hasher desde
cero; crates.io tiene bibliotecas compartidas por otros usuarios de Rust que
proporcionan hashers que implementan muchos algoritmos hash comunes.
1

https://en.wikipedia.org/wiki/SipHash

Resumen

Los vectores, cadenas y mapas hash proporcionan una gran cantidad de


funcionalidad necesaria en los programas para almacenar, acceder y modificar
datos. Aquí tienes algunos ejercicios que ya deberías poder resolver:

1. Dada una lista de números enteros, utilice un vector y devuelva la mediana


(cuando se ordena, el valor en la posición media) y la moda (el valor que
aparece con mayor frecuencia; un mapa hash será útil aquí) de la lista.
2. Convierte cadenas a pig latin. La primera consonante de cada palabra se
mueve al final y se añade ay , por lo que first se convierte en irst-fay . A las
palabras que empiezan por vocal se les añade hay al final ( apple se
convierte en apple-hay ). ¡Ten en cuenta los detalles sobre la codificación
UTF-8!
3. Usando un mapa hash y vectores, cree una interfaz de texto que permita al
usuario agregar nombres de empleados a un departamento de una
empresa; por ejemplo, "Agregar a Sally a Ingeniería" o "Agregar a Amir a
Ventas". A continuación, permita al usuario recuperar una lista de todas las
personas de un departamento o de todas las personas de la empresa por
departamento, ordenadas alfabéticamente.

La documentación de la API de la biblioteca estándar describe métodos que


tienen los vectores, cadenas y mapas hash que serán útiles para estos ejercicios.
Rust
jueves, 22 de mayo de 2025 : Página 208 de 719

Nos adentramos en programas más complejos en los que las operaciones pueden
fallar, así que es el momento perfecto para hablar sobre la gestión de errores. ¡Lo
haremos a continuación!

Manejo de errores

Los errores son parte de la vida en el software, por lo que Rust cuenta con
diversas funciones para gestionar situaciones en las que algo sale mal. En muchos
casos, Rust requiere que reconozcas la posibilidad de un error y tomes medidas
antes de que tu código se compile. Este requisito fortalece tu programa al
garantizar que detectes errores y los gestiones adecuadamente antes de
implementar tu código en producción.

Rust clasifica los errores en dos categorías


principales: recuperables e irrecuperables . En el caso de un error recuperable,
como un error de archivo no encontrado , lo más probable es que simplemente
queramos informar del problema al usuario y reintentar la operación. Los errores
irrecuperables siempre son síntomas de errores, como intentar acceder a una
ubicación más allá del final de una matriz, por lo que deseamos detener el
programa inmediatamente.

La mayoría de los lenguajes no distinguen entre estos dos tipos de errores y los
gestionan de la misma forma, mediante mecanismos como las excepciones. Rust
no tiene excepciones. En cambio, tiene el tipo Result<T, E>para errores
recuperables y la panic!macro que detiene la ejecución cuando el programa
encuentra un error irrecuperable. Este capítulo trata panic!primero la llamada y
luego la devolución Result<T, E>de valores. Además, exploraremos
consideraciones a la hora de decidir si intentar recuperarse de un error o detener
la ejecución.

Errores irrecuperables conpanic!

A veces ocurren cosas malas en tu código y no hay nada que puedas hacer al
respecto. En estos casos, Rust cuenta con la panic!macro. En la práctica, hay dos
maneras de provocar un pánico: realizando una acción que provoque un pánico
en nuestro código (como acceder a un array después del final) o llamando
explícitamente a la panic!macro. En ambos casos, provocamos un pánico en
nuestro programa. Por defecto, estos pánicos mostrarán un mensaje de error, se
desenrollarán, limpiarán la pila y saldrán. Mediante una variable de entorno,
Rust
jueves, 22 de mayo de 2025 : Página 209 de 719

también puedes hacer que Rust muestre la pila de llamadas cuando se produzca
un pánico para facilitar la localización de su origen.

Desenrollar la pila o abortar en respuesta a un pánico

Por defecto, cuando se produce un pánico, el programa inicia el desenrollado , lo


que significa que Rust recorre la pila y limpia los datos de cada función que
encuentra. Sin embargo, este proceso de desenrollado es bastante laborioso. Por
lo tanto, Rust permite abortar inmediatamente , lo que finaliza el programa sin
limpiar.

El sistema operativo deberá limpiar la memoria que el programa estaba usando.


Si en su proyecto necesita reducir al mínimo el binario resultante, puede cambiar
entre desenrollar y abortar en caso de pánico añadiendo panic =
'abort'las [profile]secciones correspondientes a su archivo Cargo.toml . Por
ejemplo, si desea abortar en caso de pánico en modo de liberación, añada lo
siguiente:

[profile.release]
panic = 'abort'

Intentemos llamar panic!a un programa simple:

Nombre de archivo: src/main.rs

fn main() {
panic!("crash and burn");
}

Cuando ejecutes el programa verás algo como esto:

$ 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!provoca el mensaje de error contenido en las dos últimas


líneas. La primera línea muestra nuestro mensaje de pánico y el lugar en nuestro
Rust
jueves, 22 de mayo de 2025 : Página 210 de 719

código fuente donde se produjo: 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 forma parte de nuestro código y, si accedemos a


ella, veremos la panic!llamada a la macro. En otros casos, la panic!llamada podría
estar en el código que nuestro código llama, y el nombre de archivo y el número
de línea que indica el mensaje de error corresponderán al código de otra persona
donde panic!se llama la macro, no a la línea de nuestro código que finalmente
provocó la panic!llamada.

Podemos usar el backtrace de las funciones panic!de donde proviene la llamada


para determinar la parte de nuestro código que causa el problema. Para entender
cómo usar el panic!backtrace, veamos otro ejemplo y veamos cómo se produce
cuando una panic!llamada proviene de una biblioteca debido a un error en
nuestro código, en lugar de que el código que llama a la macro directamente. El
Listado 9-1 contiene código que intenta acceder a un índice en un vector fuera del
rango de índices válidos.

Nombre de archivo: src/main.rs

fn main() {
let v = vec![1, 2, 3];

v[99];
}
Listado 9-1: Intentar acceder a un elemento más allá del final de un vector, lo que
provocará una llamada apanic!

Aquí, intentamos acceder al elemento número 100 de nuestro vector (que está en
el índice 99 porque la indexación empieza en cero), pero el vector solo tiene tres
elementos. En esta situación, Rust entrará en pánico. []Se supone que "using"
devuelve un elemento, pero si se pasa un índice no válido, no hay ningún
elemento que Rust pueda devolver correctamente.

En C, intentar leer más allá del final de una estructura de datos es un


comportamiento indefinido. Se podría obtener lo que se encuentre en la ubicación
de memoria correspondiente a ese elemento en la estructura de datos, incluso si
la memoria no pertenece a dicha estructura. Esto se denomina sobrelectura de
búfer y puede generar vulnerabilidades de seguridad si un atacante logra
Rust
jueves, 22 de mayo de 2025 : Página 211 de 719

manipular el índice para leer datos no permitidos, almacenados después de la


estructura de datos.

Para proteger tu programa de este tipo de vulnerabilidad, si intentas leer un


elemento en un índice inexistente, Rust detendrá la ejecución y se negará a
continuar. Probémoslo:

$ 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 99del vector en v.

La note:línea nos dice que podemos configurar la RUST_BACKTRACEvariable de


entorno para obtener un backtrace de exactamente qué sucedió para causar el
error. Un backtrace es una lista de todas las funciones que se han llamado para
llegar a este punto. Los backtrace en Rust funcionan como lo hacen en otros
lenguajes: la clave para leer el backtrace es comenzar desde el principio y leer
hasta que vea los archivos que escribió. Ese es el punto donde se originó el
problema. Las líneas por encima de ese punto son código que su código ha
llamado; las líneas por debajo son código que llamó a su código. Estas líneas de
antes y después pueden incluir código del núcleo de Rust, código de la biblioteca
estándar o crates que esté usando. Intentemos obtener un backtrace
configurando la RUST_BACKTRACEvariable de entorno en cualquier valor
excepto 0. El Listado 9-2 muestra una salida similar a la que verá.

$ RUST_BACKTRACE=1 cargo run


thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
at
/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/panicking.rs:
662:5
1: core::panicking::panic_fmt
at
/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/panicking.rs
:74:14
Rust
jueves, 22 de mayo de 2025 : Página 212 de 719
2: core::panicking::panic_bounds_check
at
/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/panicking.rs
:276:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at
/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/slice/
index.rs:302:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at
/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/slice/
index.rs:16:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at
/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/alloc/src/vec/
mod.rs:2920:9
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at
/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/ops/
function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose
backtrace.
Listado 9-2: El backtrace generado por una llamada se muestra cuando se
establece panic!la variable de entornoRUST_BACKTRACE

¡Eso es una gran cantidad de resultados! El resultado exacto que vea puede
variar según su sistema operativo y la versión de Rust. Para obtener backtraces
con esta información, debe habilitar los símbolos de depuración. Estos símbolos
están habilitados por defecto, independientemente de si se usa cargo
buildo cargo runno la --releasebandera, como en este caso.

En la salida del Listado 9-2, la línea 6 del backtrace apunta a la línea de nuestro
proyecto que causa el problema: la línea 4 de src/main.rs . Si no queremos que
nuestro programa entre en pánico, debemos comenzar nuestra investigación en
la ubicación indicada por la primera línea que menciona un archivo que
escribimos. En el Listado 9-1, donde escribimos código deliberadamente que
causaría el pánico, la forma de solucionarlo es no solicitar un elemento fuera del
rango de los índices del vector. Si su código entra en pánico en el futuro, deberá
determinar qué acción está tomando el código con qué valores para causar el
pánico y qué debería hacer el código en su lugar.
Rust
jueves, 22 de mayo de 2025 : Página 213 de 719

Volveremos a hablar sobre panic!cuándo usar y cuándo no usar panic!para


gestionar condiciones de error en la sección "Hacer panic!o no hacer panic!" más
adelante en este capítulo. A continuación, veremos cómo recuperarse de un error
usando Result.

Errores recuperables conResult

La mayoría de los errores no son lo suficientemente graves como para obligar a


detener el programa por completo. A veces, cuando una función falla, es por una
razón que se puede interpretar y a la que se puede responder fácilmente. Por
ejemplo, si intenta abrir un archivo y la operación falla porque el archivo no
existe, podría ser conveniente crearlo en lugar de finalizar el proceso.

Recuerde de “Manejo de posibles fallas con Result” en el Capítulo 2 que


la Resultenumeración se define con dos variantes, Oky Err, de la siguiente
manera:

enum Result<T, E> {


Ok(T),
Err(E),
}

Los parámetros de tipo genérico Ty Eson genéricos: analizaremos los genéricos


con más detalle en el capítulo 10. Lo que necesita saber ahora es que Trepresenta
el tipo del valor que se devolverá en caso de éxito dentro de la Ok variante
y Erepresenta el tipo de error que se devolverá en caso de fallo dentro de
la Errvariante. Dado que Resulttiene estos parámetros de tipo genérico, podemos
usar el Resulttipo y las funciones definidas en él en diversas situaciones donde el
valor de éxito y el valor de error que queremos devolver pueden ser diferentes.

Invoquemos una función que devuelve un Resultvalor, ya que podría fallar. En el


Listado 9-3, intentamos abrir un archivo.

Nombre de archivo: src/main.rs


use std::fs::File;

fn main() {
let greeting_file_result = File::open("hello.txt");
}
Listado 9-3: Apertura de un archivo
Rust
jueves, 22 de mayo de 2025 : Página 214 de 719

El tipo de retorno de File::openes a Result<T, E>. El parámetro genérico T se ha


completado mediante la implementación de File::opencon el tipo del valor de
éxito, std::fs::Fileque es un identificador de archivo. El tipo de Eutilizado en el
valor de error es std::io::Error. Este tipo de retorno significa que la llamada
a File::openpodría tener éxito y devolver un identificador de archivo que podemos
leer o escribir. La llamada a la función también podría fallar: por ejemplo, el
archivo podría no existir o no tener permiso para acceder a él.
La File::open función debe tener una forma de indicarnos si tuvo éxito o falló y, al
mismo tiempo, proporcionarnos el identificador de archivo o la información del
error. Esta información es exactamente lo que Resulttransmite la enumeración.

Si la operación File::openes correcta, el valor de la


variable greeting_file_resultserá una instancia de Okque contiene un identificador
de archivo. Si falla, el valor de greeting_file_resultserá una instancia de Errque
contiene más información sobre el tipo de error ocurrido.

Necesitamos agregar al código del Listado 9-3 las acciones necesarias según el
valor File::opendevuelto. El Listado 9-4 muestra una forma de manejar
esto Resultusando una herramienta básica: la matchexpresión que analizamos en
el Capítulo 6.

Nombre de archivo: src/main.rs


use std::fs::File;

fn main() {
let greeting_file_result = File::open("hello.txt");

let greeting_file = match greeting_file_result {


Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
Listado 9-4: Uso de una matchexpresión para manejar las Resultvariantes que
podrían devolverse

Tenga en cuenta que, al igual que la Optionenumeración, la Resultenumeración y


sus variantes se han incluido en el alcance mediante el preludio, por lo que no
necesitamos especificar Result:: antes las variantes Oky en los brazos.Errmatch

Cuando el resultado es Ok, este código devolverá el filevalor interno de


la Okvariante y luego asignaremos ese valor del identificador de archivo a la
Rust
jueves, 22 de mayo de 2025 : Página 215 de 719

variable greeting_file. Después dematch , podremos usar el identificador de


archivo para leer o escribir.

El otro brazo de [se necesita contexto para "controlar"] matchgestiona el caso en


el que obtenemos un Errvalor de [se necesita contexto para File::open". En este
ejemplo, hemos optado por llamar a la panic!macro]. Si no hay ningún archivo
llamado hello.txt en nuestro directorio actual y ejecutamos este código, veremos
el siguiente resultado de la panic!macro:

$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or
directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Como es habitual, este resultado nos indica exactamente qué salió mal.

Coincidencia de diferentes errores

El código del Listado 9-4 fallará panic!independientemente del motivo File::open.


Sin embargo, queremos tomar diferentes acciones según el motivo.
Si File::openfalla porque el archivo no existe, queremos crearlo y devolver el
identificador al nuevo archivo. Si File::openfalla por cualquier otro motivo (por
ejemplo, porque no teníamos permiso para abrir el archivo), queremos que el
código se ejecute panic!igual que en el Listado 9-4. Para ello, añadimos
una matchexpresión interna, como se muestra en el Listado 9-5.

Nombre de archivo: src/main.rs


use std::fs::File;
use std::io::ErrorKind;

fn main() {
let greeting_file_result = File::open("hello.txt");

let greeting_file = match greeting_file_result {


Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
Rust
jueves, 22 de mayo de 2025 : Página 216 de 719
other_error => {
panic!("Problem opening the file: {other_error:?}");
}
},
};
}
Listado 9-5: Manejo de diferentes tipos de errores de diferentes maneras

El tipo del valor que File::opendevuelve la Errvariante es io::Error, una estructura


proporcionada por la biblioteca estándar. Esta estructura tiene un
método kindque podemos llamar para obtener un io::ErrorKindvalor. La
enumeración io::ErrorKind, proporcionada por la biblioteca estándar, tiene
variantes que representan los diferentes tipos de errores que pueden resultar de
una io operación. La variante que queremos usar es ErrorKind::NotFound, que
indica que el archivo que intentamos abrir aún no existe. Por lo tanto, buscamos
en greeting_file_result, pero también tenemos una coincidencia interna
en error.kind().

La condición que queremos comprobar en la coincidencia interna es si el valor


devuelto por error.kind()es la NotFoundvariante de la ErrorKindenumeración. Si lo
es, intentamos crear el archivo con File::create. Sin embargo,
como File::create también podría fallar, necesitamos un segundo brazo en
la matchexpresión interna. Si no se puede crear el archivo, se imprime un
mensaje de error diferente. El segundo brazo de la expresión
externa matchpermanece igual, por lo que el programa entra en pánico ante
cualquier error, excepto el de archivo faltante.

Alternativas al uso matchconResult<T, E>

¡Eso es mucho match! La matchexpresión es muy útil, pero también bastante


primitiva. En el capítulo 13, aprenderá sobre los cierres, que se usan con muchos
de los métodos definidos en Result<T, E>. Estos métodos pueden ser más
concisos que usar matchal manejar Result<T, E>valores en su código.

Por ejemplo, aquí hay otra forma de escribir la misma lógica que se muestra en el
Listado 9-5, esta vez utilizando cierres y el unwrap_or_elsemétodo:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
Rust
jueves, 22 de mayo de 2025 : Página 217 de 719
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}

Aunque este código se comporta igual que el Listado 9-5, no


contiene matchexpresiones y es más claro. Vuelva a este ejemplo después de leer
el Capítulo 13 y busque el unwrap_or_elsemétodo en la documentación de la
biblioteca estándar. Muchos otros métodos de este tipo pueden
limpiar matchexpresiones anidadas enormes al gestionar errores.

Atajos para pánico por error: unwrapyexpect

Usar matchfunciona bastante bien, pero puede ser un poco verboso y no siempre
comunica bien la intención. El Result<T, E>tipo tiene muchos métodos auxiliares
definidos para realizar diversas tareas más específicas. El unwrapmétodo es un
método abreviado, implementado igual que la matchexpresión que escribimos en
el Listado 9-4. Si el Resultvalor es la Okvariante, unwrapdevolverá el valor dentro
de [nombre de la variable Ok]. Si [nombre Resultde la variable] es
la Errvariante, unwrapllamará a la panic!macro automáticamente. Aquí hay un
ejemplo unwrapen acción:

Nombre de archivo: src/main.rs


use std::fs::File;

fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}

Si ejecutamos este código sin un archivo hello.txt , veremos un mensaje de error


de la panic!llamada que unwraprealiza el método:

thread 'main' panicked at src/main.rs:4:49:


called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound,
message: "No such file or directory" }

De forma similar, el expectmétodo también nos permite elegir el panic!mensaje


de error. Usar expectinstead of unwrapy proporcionar mensajes de error
Rust
jueves, 22 de mayo de 2025 : Página 218 de 719

adecuados puede transmitir tu intención y facilitar la localización del origen del


pánico. La sintaxis de expectes la siguiente:

Nombre de archivo: 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");
}

Usamos ` :` expectde la misma manera que unwrap`:` para devolver el


identificador de archivo o llamar a la panic!macro. El mensaje de error usado por
` expecten su llamada a` panic! será el parámetro que pasamos a ` expect, en
lugar del panic!mensaje predeterminado que unwrapusa. Así es como se ve:

thread 'main' panicked at src/main.rs:5:10:


hello.txt should be included in this project: Os { code: 2, kind: NotFound,
message: "No such file or directory" }

En código de calidad de producción, la mayoría de los Rustaceans eligen expecten


lugar de unwrapy ofrecen más contexto sobre por qué se espera que la operación
siempre tenga éxito. De esta manera, si alguna vez se demuestra que sus
suposiciones son erróneas, tendrá más información para usar en la depuración.

Propagación de errores

Cuando la implementación de una función invoca algo que podría fallar, en lugar
de gestionar el error dentro de la propia función, se puede devolver el error al
código que la invoca para que este decida qué hacer. Esto se conoce
como propagación del error y otorga mayor control al código que la invoca, donde
podría haber más información o lógica que dicte cómo debe gestionarse el error
que la disponible en el contexto del código.

Por ejemplo, el Listado 9-6 muestra una función que lee un nombre de usuario de
un archivo. Si el archivo no existe o no se puede leer, esta función devolverá esos
errores al código que la llamó.

Nombre de archivo: src/main.rs


use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {


Rust
jueves, 22 de mayo de 2025 : Página 219 de 719
let username_file_result = File::open("hello.txt");

let mut username_file = match username_file_result {


Ok(file) => file,
Err(e) => return Err(e),
};

let mut username = String::new();

match username_file.read_to_string(&mut username) {


Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
Listado 9-6: Una función que devuelve errores al código de llamada
utilizandomatch

Esta función se puede escribir de forma mucho más breve, pero comenzaremos
por realizar gran parte de forma manual para explorar el manejo de errores; al
final, mostraremos la forma más breve. Veamos primero el tipo de retorno de la
función: Result<String, io::Error>. Esto significa que la función devuelve un valor
del tipo Result<T, E>, donde el parámetro genérico Tse ha rellenado con el tipo
concreto Stringy el tipo genérico Ese ha rellenado con el tipo concreto io::Error.

Si esta función se ejecuta correctamente sin problemas, el código que la llama


recibirá un Okvalor que contiene un String"—the username" que esta función leyó
del archivo. Si esta función encuentra algún problema, el código que la llama
recibirá un Errvalor que contiene una instancia de io::Error que contiene más
información sobre los problemas. Elegimos " io::Errorcomo tipo de retorno" de
esta función porque ese es el tipo del valor de error devuelto por las dos
operaciones que llamamos en el cuerpo de esta función que podrían fallar:
la File::openfunción y el read_to_stringmétodo.

El cuerpo de la función comienza con la llamada a la File::openfunción. Luego,


gestionamos el Resultvalor con un valor matchsimilar al matchdel Listado 9-4. Si
la File::openfunción tiene éxito, el identificador de archivo en la variable de
patrón file se convierte en el valor en la variable mutable username_filey la
función continúa. En este Errcaso, en lugar de llamar a panic!, usamos
la return palabra clave para salir anticipadamente de la función y pasar el valor
de error de File::open, ahora en la variable de patrón e, al código de llamada
como el valor de error de esta función.
Rust
jueves, 22 de mayo de 2025 : Página 220 de 719

Entonces, si tenemos un identificador de archivo en [nombre del


archivo] username_file, la función crea una nueva Stringvariable [nombre del
archivo] usernamey llama al read_to_stringmétodo en el identificador de archivo
[nombre del archivo] username_filepara leer el contenido del archivo en [nombre
del archivo] username. El read_to_stringmétodo también devuelve [nombre del
archivo] Result, ya que podría fallar, aunque File::openhaya tenido éxito. Por lo
tanto, necesitamos otro método matchpara gestionar esto Result:
si read_to_stringtiene éxito, nuestra función también lo ha hecho y devolvemos el
nombre de usuario del archivo que ahora está en username [nombre del archivo]
dentro de [nombre del archivo] Ok. Si read_to_stringfalla, devolvemos el valor de
error de la misma manera que lo devolvimos en [nombre del archivo] matchque
gestionaba el valor de retorno de [nombre del archivo File::open]. Sin embargo,
no es necesario especificar explícitamente [nombre del archivo] return, ya que
esta es la última expresión de la función.

El código que llama a este código se encargará de obtener un Okvalor que


contenga un nombre de usuario o un Errvalor que contenga un io::Error. El código
que llama decide qué hacer con esos valores. Si el código que llama obtiene
un Errvalor, podría invocar panic!y bloquear el programa, usar un nombre de
usuario predeterminado o buscar el nombre de usuario en un lugar distinto a un
archivo, por ejemplo. No tenemos suficiente información sobre lo que el código
que llama intenta hacer realmente, por lo que propagamos toda la información de
éxito o error hacia arriba para que la gestione adecuadamente.

Este patrón de propagación de errores es tan común en Rust que Rust


proporciona el operador de signo de interrogación ?para hacerlo más fácil.

Un atajo para propagar errores: el ?operador

El listado 9-7 muestra una implementación read_username_from_fileque tiene la


misma funcionalidad que en el listado 9-6, pero esta implementación utiliza
el ? operador.

Nombre de archivo: src/main.rs


use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {


let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Rust
jueves, 22 de mayo de 2025 : Página 221 de 719
Ok(username)
}
Listado 9-7: Una función que devuelve errores al código de llamada utilizando el ?
operador

El ` ?`` colocado después de un Resultvalor`` está definido para funcionar


prácticamente de la misma manera que las matchexpresiones que definimos para
manejar los Resultvalores en el Listado 9-6. Si el valor de ``` Resultes un`` Ok, el
valor dentro de `` Okse devolverá desde esta expresión y el programa continuará.
Si el valor es un ``` Err, Errse devolverá desde toda la función como si
hubiéramos usado la returnpalabra clave, por lo que el valor de error se propaga
al código de llamada.

Existe una diferencia entre la función de la matchexpresión del Listado 9-6 y la


del ?operador: los valores de error que se ?ejecutan con el operador pasan por
la fromfunción, definida en el Fromatributo de la biblioteca estándar, que se
utiliza para convertir valores de un tipo a otro. Cuando el ?operador llama a
la fromfunción, el tipo de error recibido se convierte al 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 posibles fallas, incluso si algunas partes
fallan por diversas razones.

Por ejemplo, podríamos modificar la read_username_from_filefunción del Listado


9-7 para que devuelva un tipo de error personalizado llamado OurErrorque
definamos. Si también definimos impl From<io::Error> for OurErrorque se
construya una instancia de OurErrora partir de un io::Error, entonces las ?
llamadas del operador en el cuerpo de read_username_from_filellamarán fromy
convertirán los tipos de error sin necesidad de agregar más código a la función.

En el contexto del Listado 9-7, la? final de la File::openllamada, devolverá el valor


dentro de an Oka la variable username_file. Si se produce un error, el ?operador
retornará anticipadamente de toda la función y asignará cualquier Errvalor al
código que la llama. Lo mismo ocurre con al ?final de la read_to_stringllamada.

El ?operador elimina gran parte del código repetitivo y simplifica la


implementación de esta función. Incluso podríamos acortar aún más este código
encadenando las llamadas a métodos inmediatamente después de ?, como se
muestra en el Listado 9-8.

Nombre de archivo: src/main.rs


Rust
jueves, 22 de mayo de 2025 : Página 222 de 719
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {


let mut username = String::new();

File::open("hello.txt")?.read_to_string(&mut username)?;

Ok(username)
}
Listado 9-8: Encadenamiento de llamadas a métodos después del ?operador

Hemos movido la creación del nuevo Stringin usernameal 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_stringdirectamente al resultado
de File::open("hello.txt")?. Seguimos teniendo un ?al final de
la read_to_string llamada y seguimos devolviendo un Okvalor que
contiene username cuando ambos File::openy read_to_stringtienen éxito en lugar
de devolver errores. La funcionalidad es la misma que en los Listados 9-6 y 9-7;
simplemente es una forma diferente y más ergonómica de escribirla.

El listado 9-9 muestra una forma de hacer esto aún más breve
utilizando fs::read_to_string.

Nombre de archivo: src/main.rs


use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {


fs::read_to_string("hello.txt")
}
Listado 9-9: Usar fs::read_to_stringen lugar de abrir y luego leer el archivo

Leer un archivo en una cadena es una operación bastante común, por lo que la
biblioteca estándar proporciona la fs::read_to_stringfunción conveniente que abre
el archivo, crea un nuevo String, lee el contenido del archivo, coloca el contenido
en eseString y lo devuelve. Por supuesto, usar fs::read_to_string no nos da la
oportunidad de explicar todo el manejo de errores, así que lo hicimos primero de
forma más extensa.

Donde El? se puede utilizar el operador


Rust
jueves, 22 de mayo de 2025 : Página 223 de 719

El ?operador solo se puede usar en funciones cuyo tipo de retorno sea compatible
con el valor ?en el que se usa. Esto se debe a que el ?operador está definido para
realizar un retorno anticipado de un valor fuera de la función, de la misma manera
que la matchexpresión que definimos en el Listado 9-6. En el Listado 9-6,
el matchusaba un Resultvalor, y el retorno anticipado arm devolvió
un Err(e) valor. El tipo de retorno de la función debe ser a Resultpara que sea
compatible con this return.

En el Listado 9-10, veamos el error que obtendremos si usamos el ?operador en


unamain función con un tipo de retorno que es incompatible con el tipo de valor
que usamos ?.

Nombre de archivo: src/main.rs

use std::fs::File;

fn main() {
let greeting_file = File::open("hello.txt")?;
}
Listado 9-10: Intentando usar el ?en elmain función que retorna ()no se compilará.

Este código abre un archivo, lo cual podría fallar. El ?operador sigue


el Result valor devuelto por File::open, pero esta mainfunción tiene el tipo de
retorno (), no Result. Al compilar este código, recibimos 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 `()`
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
Rust
jueves, 22 de mayo de 2025 : Página 224 de 719
4| let greeting_file = File::open("hello.txt")?;
5+ Ok(())
|

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous
error

Este error señala que solo se nos permite usar el ?operador en una función que
devuelve Result,Option u otro tipo que implemente FromResidual.

Para corregir el error, tiene dos opciones. Una es cambiar el tipo de retorno de su
función para que sea compatible con el valor ?en el que usa el operador, siempre
que no haya restricciones que lo impidan. La otra opción es usar una matcho una
de las siguientes:Result<T, E> métodos para manejar el Result<T, E> error de la
forma que sea adecuada.

El mensaje de error también menciona que ?se puede usar


con Option<T>valores. Al igual que con el uso de ?"on" Result, solo se puede
usar ?"on" Optionen una función que devuelve un " Option. El comportamiento
del ?operador cuando se llama en "an " Option<T>es similar al de "a" Result<T,
E>: si el valor es " None, Nonese devolverá "the" antes de tiempo desde la
función en ese punto. Si el valor es " Some, el valor dentro de "the" Somees el
valor resultante de la expresión y la función continúa. El Listado 9-11 muestra un
ejemplo de una función que encuentra el último carácter de la primera línea del
texto dado.

fn last_char_of_first_line(text: &str) -> Option<char> {


text.lines().next()?.chars().last()
}
Listado 9-11: Uso del ?operador en unOption<T> valor

Esta función retorna Option<char>porque es posible que exista un carácter, pero


también es posible que no. Este código toma el textargumento "segmento" de la
cadena y llama al linesmétodo correspondiente, que devuelve un iterador sobre
las líneas de la cadena. Dado que esta función desea examinar la primera línea,
llama nextal iterador para obtener su primer valor. Si textes una cadena vacía,
esta llamada a nextdevolverá None, en cuyo caso usamos ?para detener y
retornamos Nonedesde last_char_of_first_line. Si textno es una cadena
vacía, nextdevolverá un Somevalor que contiene un fragmento de la primera línea
de text.
Rust
jueves, 22 de mayo de 2025 : Página 225 de 719

Extrae ?la porción de cadena, y podemos llamar charsa esa porción de cadena
para obtener un iterador de sus caracteres. Nos interesa el último carácter de
esta primera línea, así que llamamos lasta para devolver el último elemento del
iterador. Esto se Optiondebe a que es posible que la primera línea sea la cadena
vacía; por ejemplo, `if` textcomienza con una línea en blanco pero tiene
caracteres en otras líneas, como en ` "\nhi". Sin embargo, si hay un último
carácter en la primera línea, se devolverá en la Somevariante. El ?operador en el
medio nos da una forma concisa de expresar esta lógica, permitiéndonos
implementar la función en una sola línea. Si no pudiéramos usar el ?operador en
` Option, tendríamos que implementar esta lógica usando más llamadas a
métodos o una matchexpresión.

Tenga en cuenta que puede usar el ?operador "on" Resulten una función que
devuelve "<sub>a</sub>" Result, así como el ?operador "on" Optionen una
función que devuelve "<sub>an </sub>" Option, pero no puede combinarlos. El ?
operador no convertirá automáticamente "a" Resulten "an" Optionni viceversa; en
esos casos, puede usar métodos como " okon" Resulto " ok_oron" Optionpara
realizar la conversión explícitamente.

Hasta ahora, todas las mainfunciones que hemos utilizado devuelven() .


Esta mainfunción es especial porque es el punto de entrada y el punto de salida
de un programa ejecutable, y existen restricciones sobre el tipo de retorno para
que el programa se comporte como se espera.

Afortunadamente, maintambié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
a main"be" Result<(), Box<dyn Error>>y hemos añadido un valor de
retorno.Ok(()) al final. Este código ahora se compilará.

Nombre de archivo: src/main.rs


use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {


let greeting_file = File::open("hello.txt")?;

Ok(())
}
Listado 9-12: Cambiar maina return Result<(), E>permite el uso del ?operador
onResult valores.
Rust
jueves, 22 de mayo de 2025 : Página 226 de 719

El Box<dyn Error>tipo es un objeto de rasgo , del cual hablaremos en


la sección "Uso de objetos de rasgo que admiten valores de diferentes
tipos"Box<dyn Error> del capítulo 18. Por ahora, se puede interpretar como
"cualquier tipo de error". Se permite usar " ?on" en un Result valor en
una mainfunción con el tipo error Box<dyn Error>porque permite Errdevolver
cualquier valor anticipadamente. Aunque el cuerpo de esta mainfunción solo
devolverá errores de tipo std::io::Error, al especificarBox<dyn Error> , esta firma
seguirá siendo correcta incluso si se añade código que devuelva otros errores al
cuerpo de main.

Cuando una mainfunción devuelve un Result<(), E>, el ejecutable saldrá con un


valor de 0si maindevuelve un valor Ok(()), y con un valor distinto de cero
si maindevuelve un Errvalor. Los ejecutables escritos en C devuelven enteros al
salir: los programas que salen correctamente devuelven el entero 0, y los
programas que fallan devuelven un entero distinto de 0. Rust también devuelve
enteros de los ejecutables para ser compatible con esta convención.

La mainfunción puede devolver cualquier tipo que


implemente el std::process::Terminationrasgo , que contiene una
función reportque devuelve un ExitCode. Consulte la documentación de la
biblioteca estándar para obtener más información sobre la implementación
del Terminationrasgo para sus propios tipos.

Ahora que hemos discutido los detalles de llamar panic!o devolver Result,
volvamos al tema de cómo decidir cuál es apropiado usar en qué casos.

Hacerlo panic!o no hacerlopanic!

¿Cómo se decide cuándo llamar panic!y cuándo devolver Result? Cuando el


código entra en pánico, no hay forma de recuperarse. Se puede
llamar panic! para cualquier situación de error, independientemente de si existe
una forma de recuperación, pero entonces se está tomando la decisión de que
una situación es irrecuperable en nombre del código que llama. Al elegir devolver
un Resultvalor, se le dan opciones al código que llama. El código que llama podría
optar por intentar recuperarse de una manera adecuada a su situación, o podría
decidir que un Err valor en este caso es irrecuperable, por lo que puede
llamar panic!y convertir el error recuperable en uno irrecuperable. Por lo tanto,
devolver Resultes una buena opción predeterminada al definir una función que
podría fallar.
Rust
jueves, 22 de mayo de 2025 : Página 227 de 719

En situaciones como ejemplos, código prototipo y pruebas, es más apropiado


escribir código que genere pánico en lugar de devolver un Result. Exploremos por
qué y luego analicemos situaciones en las que el compilador no puede determinar
que un fallo es imposible, pero usted, como humano, sí. El capítulo concluirá con
algunas pautas generales sobre cómo decidir si generar pánico en el código de la
biblioteca.

Ejemplos, código de prototipo y pruebas

Al escribir un ejemplo para ilustrar un concepto, incluir código robusto de gestión


de errores puede hacerlo menos claro. En los ejemplos, se entiende que una
llamada a un método como unwrapese podría generar pánico, lo que sirve como
un marcador de posición para la gestión de errores de la aplicación, que puede
variar según la actividad del resto del código.

De igual forma, los métodos unwrapy expectson muy útiles al crear prototipos,
antes de decidir cómo gestionar los errores. Dejan marcas claras en el código
para cuando esté listo para robustecer el programa.

Si una llamada a un método falla en una prueba, es deseable que toda la prueba
falle, incluso si ese método no es la funcionalidad bajo prueba. Porque panic!así
es como se marca una prueba como fallida, llamando unwrapoexpect es
exactamente lo que debería ocurrir.

Casos en los que tienes más información que el compilador

También sería apropiado llamar a unwrap`or` expectcuando se tiene otra lógica


que garantiza que `the` Resulttendrá un Okvalor, pero esta lógica no es algo que
el compilador entienda. Aún habrá un Resultvalor que deberá gestionar: cualquier
operación que esté llamando aún puede fallar en general, aunque sea
lógicamente imposible en su situación particular. Si puede asegurarse,
inspeccionando manualmente el código, de que nunca tendrá una Errvariante, es
perfectamente aceptable llamar a `` unwrap, e incluso mejor, documentar en el
texto la razón por la que cree que nunca tendrá una Errvariante expect. Aquí
tiene un ejemplo:

use std::net::IpAddr;

let home: IpAddr = "127.0.0.1"


Rust
jueves, 22 de mayo de 2025 : Página 228 de 719
.parse()
.expect("Hardcoded IP address should be valid");

Estamos creando una IpAddrinstancia analizando una cadena codificada. Vemos


que 127.0.0.1es una dirección IP válida, por lo que es aceptable
usarla expect aquí. Sin embargo, tener una cadena válida codificada no cambia el
tipo de retorno del parsemétodo: seguimos obteniendo un Resultvalor, y el
compilador nos obligará a manejar ``` Resultcomo si la Errvariante fuera una
posibilidad, ya que no es lo suficientemente inteligente como para ver que esta
cadena siempre es 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, existiera la posibilidad de fallo, definitivamente querríamos manejar
`` Result` de una manera más robusta. Mencionar la suposición de que esta
dirección IP está codificada nos incitará a cambiar expecta un código de gestión
de errores más eficaz si, en el futuro, necesitamos obtener la dirección IP de otra
fuente.

Pautas para el manejo de errores

Es recomendable que el código entre en pánico cuando exista la posibilidad de


que termine en un estado defectuoso. En este contexto, un estado defectuoso se
produce cuando se ha incumplido alguna suposición, garantía, contrato o
invariante, como cuando se pasan valores inválidos, contradictorios o faltantes al
código, además de uno o más de los siguientes:

 El mal estado es algo inesperado, a diferencia de algo que probablemente


sucederá ocasionalmente, como un usuario que ingresa datos en un
formato incorrecto.
 Después de este punto, su código debe confiar en no estar en este mal
estado, en lugar de verificar el problema en cada paso.
 No existe una buena manera de codificar esta información en los tipos que
utiliza. Analizaremos un ejemplo de lo que queremos decir en la
sección "Codificación de estados y comportamiento como tipos" del capítulo
18.

Si alguien llama a tu código y pasa valores sin sentido, lo mejor es devolver un


error, si es posible, para que el usuario de la biblioteca pueda decidir qué hacer
en ese caso. Sin embargo, en casos donde continuar podría ser inseguro o
perjudicial, la mejor opción podría ser llamar panic!y alertar a la persona que usa
Rust
jueves, 22 de mayo de 2025 : Página 229 de 719

tu biblioteca sobre el error en su código para que pueda corregirlo durante el


desarrollo. De igual forma,panic! suele ser apropiado si llamas a código externo
que escapa a tu control y este devuelve un estado no válido que no puedes
corregir.

Sin embargo, cuando se espera un fallo, es más apropiado devolver un Result que
realizar una panic!llamada. Por ejemplo, un analizador que recibe datos
incorrectos o una solicitud HTTP que devuelve un estado que indica que se ha
alcanzado un límite de velocidad. En estos casos, devolver un Resultindica que el
fallo es una posibilidad esperada y el código que realiza la llamada debe decidir
cómo gestionarlo.

Cuando tu código realiza una operación que podría poner en riesgo a un usuario si
se llama con valores no válidos, debería verificar primero la validez de los valores
y entrar en pánico si no lo son. Esto se debe principalmente a razones de
seguridad: intentar operar con datos no válidos puede exponer tu código a
vulnerabilidades. Esta es la razón principal por la que la biblioteca estándar
llamará panic!si intentas un acceso a memoria fuera de los límites: intentar
acceder a memoria que no pertenece a la estructura de datos actual es un
problema de seguridad común. Las funciones suelen tener contratos : su
comportamiento solo está garantizado si las entradas cumplen requisitos
específicos. Entrar en pánico cuando se viola el contrato tiene sentido porque una
violación del contrato siempre indica un error del lado de la llamada, y no es un
tipo de error que quieras que el código que llama tenga que gestionar
explícitamente. De hecho, no hay una forma razonable de que el código que llama
se recupere; los programadores que llaman deben corregir el código. Los
contratos de una función, especialmente cuando una violación causará un pánico,
deben explicarse en la documentación de la API de la función.

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
que realice 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 tienes un valor válido. Por ejemplo,
si tienes un tipo en lugar de un Option, tu programa espera tener something en
lugar de nothing . Tu código entonces no tiene que manejar dos casos para las
variantes Somey None: solo tendrá un caso para tener definitivamente un valor.
El código que intente pasar nothing a tu función ni siquiera se compilará, por lo
Rust
jueves, 22 de mayo de 2025 : Página 230 de 719

que tu función no tiene que comprobar ese caso en tiempo de ejecución. Otro
ejemplo es usar un tipo entero sin signo como u32, que garantiza que el
parámetro nunca sea negativo.

Creación de tipos personalizados para la validación

Llevemos la idea de usar el sistema de tipos de Rust para garantizar un valor


válido un paso más allá y veamos cómo crear un tipo personalizado para la
validación. Recordemos el juego de adivinanzas del Capítulo 2, en el que nuestro
código pedía 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 compararla
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. Sin embargo, sería una mejora
útil guiar al usuario hacia suposiciones válidas y tener un comportamiento
diferente cuando el usuario adivina un número fuera de rango que cuando
escribe, por ejemplo, letras en su lugar.

Una forma de hacer esto sería analizar la suposición como un i32en lugar de solo
un u32para permitir números potencialmente negativos y luego agregar una
verificación para que el número esté dentro del rango, de la siguiente manera:

Nombre de archivo: src/main.rs


loop {
// --snip--

let guess: i32 = match guess.trim().parse() {


Ok(num) => num,
Err(_) => continue,
};

if guess < 1 || guess > 100 {


println!("The secret number will be between 1 and 100.");
continue;
}

match guess.cmp(&secret_number) {
// --snip--
}

La ifexpresión comprueba si nuestro valor está fuera de rango, informa al usuario


del problema y llama continuepara iniciar la siguiente iteración del bucle y
Rust
jueves, 22 de mayo de 2025 : Página 231 de 719

solicitar otra aproximación. Después de la ifexpresión, podemos proceder con las


comparaciones entre guessy el número secreto sabiendo que guessestá entre 1 y
100.

Sin embargo, esta no es una solución ideal: si fuera absolutamente crítico que el
programa solo operara con valores entre 1 y 100, y tuviera muchas funciones con
este requisito, tener una verificación como esta en cada función sería tedioso (y
podría afectar el rendimiento).

En su lugar, podemos crear un nuevo tipo y colocar las validaciones en una


función para crear una instancia del tipo en lugar de repetir las validaciones en
todas partes. De esta forma, las funciones pueden usar el nuevo tipo en sus
firmas y usar con confianza los valores que reciben. El Listado 9-13 muestra una
forma de definir un Guesstipo que solo creará una instancia Guesssi la newfunción
recibe un valor entre 1 y 100.

pub struct Guess {


value: i32,
}

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 }
}

pub fn value(&self) -> i32 {


self.value
}
}
Listado 9-13: Un Guesstipo que solo continuará con valores entre 1 y 100

Primero, definimos una estructura llamada Guessque contiene un campo


llamado valueque contiene un i32. Aquí se almacenará el número.

Luego, implementamos una función asociada llamada " newon" Guessque crea
instancias de Guessvalores. La newfunción se define con un parámetro llamado
" valuede tipo" i32y devuelve un valor Guess. El código en el cuerpo de
la newfunción comprueba valueque esté entre 1 y 100. Si valueno supera la
Rust
jueves, 22 de mayo de 2025 : Página 232 de 719

prueba, realizamos una panic!llamada que alerta al programador que escribe el


código de llamada de que hay un error que debe corregir, ya que crear un
valor Guesscon un valor valuefuera de este rango violaría el contrato del
que Guess::newdepende. Las condiciones en las que Guess::newpodría generar
pánico se deben discutir en la documentación pública de la API; abordaremos las
convenciones de la documentación que indican la posibilidad de un valor panic!en
la documentación de la API que cree en el Capítulo 14. Si valuesupera la prueba,
creamos un nuevo valor Guesscon el valuecampo establecido en
el valueparámetro y devolvemos el valor Guess.

A continuación, implementamos un método llamado valueque toma prestado self,


no tiene otros parámetros y devuelve un i32. Este tipo de método a veces se
denomina getter porque su propósito es obtener datos de sus campos y
devolverlos. Este método público es necesario porque el valuecampo de
la Guess estructura es privado. Es importante que el valuecampo sea privado
para que el código que usa la Guessestructura no pueda
establecer valuedirectamente: el código externo al módulo debe usar
la Guess::newfunción para crear una instancia de Guess, lo que garantiza que a
no pueda Guesstener un a valueque no haya sido verificado por las condiciones
de la Guess::newfunción.

Una función que tiene un parámetro o devuelve solo números entre 1 y 100
podría entonces declarar en su firma que toma o devuelve a Guessen lugar de
an i32y no necesitaría hacer ninguna verificación adicional en su cuerpo.

Resumen

Las funciones de gestión de errores de Rust están diseñadas para ayudarte a


escribir código más robusto. La panic!macro indica que tu programa se encuentra
en un estado que no puede gestionar y te permite indicarle que se detenga en
lugar de intentar continuar con valores inválidos o incorrectos.
La Resultenumeración usa el sistema de tipos de Rust para indicar que las
operaciones podrían fallar de forma que tu código pueda recuperarse. Puedes
usarla Resultpara indicar al código que llama a tu código que también debe
gestionar posibles éxitos o fracasos. Usar " panic!y Result" en las situaciones
adecuadas hará que tu código sea más fiable ante problemas inevitables.
Rust
jueves, 22 de mayo de 2025 : Página 233 de 719

Ahora que ha visto formas útiles en que la biblioteca estándar usa genéricos con
las Optionenumeraciones y Result, hablaremos sobre cómo funcionan los
genéricos y cómo puede usarlos en su código.

Tipos genéricos, rasgos y tiempos de vida

Todo lenguaje de programación cuenta con herramientas para gestionar


eficazmente la duplicación de conceptos. En Rust, una de estas herramientas
son los genéricos : sustitutos abstractos de tipos concretos u otras propiedades.
Podemos expresar el comportamiento de los genéricos o su relación con otros
genéricos sin saber qué ocupará su lugar al compilar y ejecutar el código.

Las funciones pueden aceptar parámetros de un tipo genérico, en lugar de un tipo


concreto como i32o String, de la misma forma que aceptan parámetros con
valores desconocidos para ejecutar el mismo código con múltiples valores
concretos. De hecho, ya hemos usado genéricos en el Capítulo 6 con Option<T>,
en el Capítulo 8 con Vec<T>y HashMap<K, V>, y en el Capítulo 9 con Result<T,
E>. En este capítulo, explorarás cómo definir tus 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 crear una función genérica a
partir de dos funciones que difieren únicamente en el tipo de sus parámetros.
También explicaremos cómo usar tipos genéricos en las definiciones de
estructuras y enumeraciones.

Luego, aprenderá a usar rasgos para definir el comportamiento de forma


genérica. Puede combinar rasgos con tipos genéricos para restringir un tipo
genérico y que este acepte solo aquellos que tienen un comportamiento
específico, en lugar de cualquier tipo.

Finalmente, analizaremos los tiempos de vida : una variedad de genéricos que


proporcionan al compilador información sobre la relación entre las referencias.
Los tiempos de vida nos permiten proporcionar al compilador suficiente
información sobre los valores prestados para que pueda garantizar que las
referencias sean válidas en más situaciones de las que podrían sin nuestra ayuda.

Cómo eliminar la duplicación extrayendo una función


Rust
jueves, 22 de mayo de 2025 : Página 234 de 719

Los genéricos nos permiten reemplazar tipos específicos con un marcador que
representa varios tipos para eliminar la duplicación de código. Antes de
profundizar en la sintaxis de los genéricos, veamos cómo eliminar la duplicación
sin usar tipos genéricos, extrayendo una función que reemplaza valores
específicos con un marcador que representa varios valores. Después, aplicaremos
la misma técnica para extraer una función genérica. Al ver cómo reconocer código
duplicado que se puede extraer en una función, empezarás a reconocer código
duplicado que puede usar genéricos.

Comenzaremos con el programa corto del Listado 10-1 que encuentra el número
más grande en una lista.

Nombre de archivo: src/main.rs


fn main() {
let number_list = vec![34, 50, 25, 100, 65];

let mut largest = &number_list[0];

for number in &number_list {


if number > largest {
largest = number;
}
}

println!("The largest number is {largest}");


}
Listado 10-1: Encontrar el número más grande en una lista de números

Almacenamos una lista de enteros en la variable number_listy hacemos referencia


al primer número de la lista en una variable llamada largest. Luego, iteramos
todos los números de la lista y, si el número actual es mayor que el almacenado
en largest, reemplazamos la referencia en esa variable. Sin embargo, si el número
actual es menor o igual que el mayor número visto hasta el momento, la variable
no cambia y el código pasa al siguiente número de la lista. Tras considerar todos
los números de la lista, largestdebería hacer referencia al mayor, que en este
caso es 100.

Ahora debemos encontrar el número mayor en dos listas de números diferentes.


Para ello, podemos duplicar el código del Listado 10-1 y usar la misma lógica en
dos puntos diferentes del programa, como se muestra en el Listado 10-2.

Nombre de archivo: src/main.rs


Rust
jueves, 22 de mayo de 2025 : Página 235 de 719
fn main() {
let number_list = vec![34, 50, 25, 100, 65];

let mut largest = &number_list[0];

for number in &number_list {


if number > largest {
largest = number;
}
}

println!("The largest number is {largest}");

let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

let mut largest = &number_list[0];

for number in &number_list {


if number > largest {
largest = number;
}
}

println!("The largest number is {largest}");


}
Listado 10-2: Código para encontrar el número más grande en dos listas de
números

Aunque este código funciona, duplicarlo es tedioso y propenso a errores. Además,


debemos recordar actualizar el código en varias ubicaciones cuando queramos
modificarlo.

Para eliminar esta duplicación, crearemos una abstracción definiendo una función
que opera sobre cualquier lista de enteros pasada como parámetro. Esta solución
simplifica nuestro código y nos permite expresar el concepto de encontrar el
número mayor en una lista de forma abstracta.

En el Listado 10-3, extraemos el código que encuentra el número mayor en una


función llamada largest. Luego, llamamos a la función para encontrar el número
mayor en las dos listas del Listado 10-2. También podríamos usar la función en
cualquier otra lista de i32valores que tengamos en el futuro.

Nombre de archivo: src/main.rs


fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
Rust
jueves, 22 de mayo de 2025 : Página 236 de 719

for item in list {


if item > largest {
largest = item;
}
}

largest
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];

let result = largest(&number_list);


println!("The largest number is {result}");

let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

let result = largest(&number_list);


println!("The largest number is {result}");
}
Listado 10-3: Código abstracto para encontrar el número más grande en dos listas

La largestfunción tiene un parámetro llamado list, que representa cualquier


porción concreta dei32 valores que podríamos pasarle. Por lo tanto, al llamar a la
función, el código se ejecuta con los valores específicos que pasamos.

En resumen, estos son los pasos que seguimos para cambiar el código del Listado
10-2 al Listado 10-3:

1. Identificar código duplicado.


2. Extraiga el código duplicado en el cuerpo de la función y especifique las
entradas y los valores de retorno de ese código en la firma de la función.
3. Actualice las dos instancias de código duplicado para llamar a la función en
su lugar.

A continuación, seguiremos estos mismos pasos con genéricos para reducir la


duplicación de código. De la misma manera que el cuerpo de la función puede
operar con un valor abstracto listen lugar de valores específicos, los genéricos
permiten que el código opere con tipos abstractos.

Por ejemplo, supongamos que tenemos dos funciones: una que encuentra el
elemento más grande en un segmento de i32valores y otra que encuentra el
Rust
jueves, 22 de mayo de 2025 : Página 237 de 719

elemento más grande en un segmento de char valores. ¿Cómo eliminaríamos esa


duplicación? ¡Descubrámoslo!

Tipos de datos genéricos

Usamos genéricos para crear definiciones de elementos como firmas de funciones


o estructuras, que luego podemos usar con diversos tipos de datos concretos.
Primero, veamos cómo definir funciones, estructuras, enumeraciones y métodos
usando genéricos. Luego, analizaremos cómo afectan los genéricos al rendimiento
del código.

En definiciones de funciones

Al definir una función que usa genéricos, los colocamos en la firma de la función,
donde normalmente especificaríamos los tipos de datos de los parámetros y el
valor de retorno. Esto aumenta la flexibilidad del código y proporciona más
funcionalidad a quienes llaman a la función, a la vez que evita la duplicación de
código.

Continuando con nuestra largestfunción, el Listado 10-4 muestra dos funciones


que encuentran el valor máximo en una porción. Las combinaremos en una sola
función que usa genéricos.

Nombre de archivo: src/main.rs


fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];

for item in list {


if item > largest {
largest = item;
}
}

largest
}

fn largest_char(list: &[char]) -> &char {


let mut largest = &list[0];

for item in list {


if item > largest {
largest = item;
}
}
Rust
jueves, 22 de mayo de 2025 : Página 238 de 719

largest
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];

let result = largest_i32(&number_list);


println!("The largest number is {result}");

let char_list = vec!['y', 'm', 'a', 'q'];

let result = largest_char(&char_list);


println!("The largest char is {result}");
}
Listado 10-4: Dos funciones que difieren solo en sus nombres y en los tipos de sus
firmas

La largest_i32función que extrajimos en el Listado 10-3, que encuentra el valor


más grande i32de una porción, es la misma. Los cuerpos de
las largest_charfunciones chartienen el mismo código, así que eliminemos la
duplicación introduciendo un parámetro de tipo genérico en una sola función.

Para parametrizar los tipos en una función nueva, necesitamos nombrar el


parámetro de tipo, igual que para los parámetros de valor de una función. Se
puede usar cualquier identificador como nombre de parámetro de tipo. Sin
embargo, usaremos [nombre de parámetro de tipo] Tporque, por convención, los
nombres de los parámetros de tipo en Rust son cortos, a menudo de una sola
letra, y la convención de nombres de tipos de Rust es UpperCamelCase. La
abreviatura de [tipo ] Tes la opción predeterminada de la mayoría de los
programadores de Rust.

Cuando usamos un parámetro en el cuerpo de la función, debemos declarar su


nombre en la firma para que el compilador sepa su significado. De igual forma,
cuando usamos un nombre de parámetro de tipo en la firma de una función,
debemos declararlo antes de usarlo. Para definir la largestfunción genérica,
colocamos las declaraciones de nombre de tipo entre corchetes
angulares, <>entre el nombre de la función y la lista de parámetros, de la
siguiente manera:

fn largest<T>(list: &[T]) -> &T {


Rust
jueves, 22 de mayo de 2025 : Página 239 de 719

Esta definición se interpreta como: la función largestes genérica sobre un tipo


determinado T. Esta función tiene un parámetro llamado list, que corresponde a
una porción de valores del tipo T. La largestfunción devolverá una referencia a un
valor del mismo tipo T.

El listado 10-5 muestra la largestdefinición de la función combinada utilizando el


tipo de dato genérico en su firma. También muestra cómo podemos llamar a la
función con una porción de i32valores o charcon valores. Tenga en cuenta que
este código aún no se compila, pero lo solucionaremos más adelante en este
capítulo.

Nombre de archivo: src/main.rs

fn largest<T>(list: &[T]) -> &T {


let mut largest = &list[0];

for item in list {


if item > largest {
largest = item;
}
}

largest
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];

let result = largest(&number_list);


println!("The largest number is {result}");

let char_list = vec!['y', 'm', 'a', 'q'];

let result = largest(&char_list);


println!("The largest char is {result}");
}
Listado 10-5: La largestfunción que utiliza parámetros de tipo genérico; esto aún
no se compila

Si compilamos este código ahora mismo, obtendremos este error:

$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
Rust
jueves, 22 de mayo de 2025 : Página 240 de 719
--> src/main.rs:5:17
|
5| if item > largest {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

El texto de ayuda menciona std::cmp::PartialOrd, que es un rasgo , y hablaremos


de rasgos en la siguiente sección. Por ahora, tenga en cuenta que este error
indica que el cuerpo de largestno funcionará con todos los tipos posibles T . Dado
que queremos comparar valores de tipo Ten el cuerpo, solo podemos usar tipos
cuyos valores se puedan ordenar. Para facilitar las comparaciones, la biblioteca
estándar incluye el std::cmp::PartialOrdrasgo que se puede implementar en los
tipos (consulte el Apéndice C para obtener más información sobre este rasgo).
Siguiendo la sugerencia del texto de ayuda, restringimos los tipos válidos para Ta
solo aquellos que implementan PartialOrdy este ejemplo se compilará, ya que la
biblioteca estándar implementa PartialOrdtanto en i32como en char.

Definiciones de estructura

También podemos definir estructuras para usar un parámetro de tipo genérico en


uno o más campos mediante la <>sintaxis. El Listado 10-6 define
una Point<T>estructura para almacenar xy ycoordinar valores de cualquier tipo.

Nombre de archivo: 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 };
}
Listado 10-6: Una Point<T>estructura que contiene xvalores yde tipoT
Rust
jueves, 22 de mayo de 2025 : Página 241 de 719

La sintaxis para usar genéricos en las definiciones de estructuras es similar a la


de las definiciones de funciones. Primero, declaramos el nombre del parámetro de
tipo entre corchetes angulares justo después del nombre de la estructura. Luego,
usamos el tipo genérico en la definición de la estructura donde, de otro modo,
especificaríamos tipos de datos concretos.

Tenga en cuenta que, dado que solo usamos un tipo genérico para
definir Point<T>, esta definición indica que la Point<T>estructura es genérica
sobre algún tipo , y queT los campos xy yson del mismo tipo, independientemente
de su tipo. Si creamos una instancia de a con valores de diferentes tipos, como en
el Listado 10-7, nuestro código no compilará.Point<T>

Nombre de archivo: src/main.rs

struct Point<T> {
x: T,
y: T,
}

fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
Listado 10-7: Los campos xy ydeben ser del mismo tipo porque ambos tienen el
mismo tipo de datos genérico T.

En este ejemplo, al asignar el valor entero 5a x, le informamos al compilador que


el tipo genérico Tserá un entero para esta instancia de Point<T>. Luego, al
especificar 4.0for y, que hemos definido con el mismo tipo que x, obtendremos un
error de tipo no coincidente como este:

$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7| let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number

For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

Para definir una Pointestructura donde xy yson genéricos, pero podrían tener
tipos diferentes, podemos usar varios parámetros de tipo genérico. Por ejemplo,
Rust
jueves, 22 de mayo de 2025 : Página 242 de 719

en el Listado 10-8, cambiamos la definición de Pointpara que sea genérico sobre


los tipos T y Udonde xes de tipo Ty yes de tipo U.

Nombre de archivo: src/main.rs


struct Point<T, U> {
x: T,
y: U,
}

fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
Listado 10-8: Un Point<T, U>genérico sobre dos tipos de modo que xy ypueden
ser valores de diferentes tipos

¡Ahora se permiten todas las instancias de Point"mostradas"! Puedes usar tantos


parámetros de tipo genérico como quieras en una definición, pero usar
demasiados dificulta la lectura del código. Si necesitas muchos tipos genéricos en
tu código, podría indicar que necesita ser reestructurado en fragmentos más
pequeños.

En definiciones de enumeración

Al igual que hicimos con las estructuras, podemos definir enumeraciones para
almacenar tipos de datos genéricos en sus variantes. Analicemos nuevamente
la Option<T>enumeración que proporciona la biblioteca estándar, que usamos en
el capítulo 6:

enum Option<T> {
Some(T),
None,
}

Esta definición debería tener más sentido ahora. Como puede ver,
la Option<T>enumeración es genérica sobre el tipo Ty tiene dos variantes: Some,
que contiene un valor de tipo T, y una Nonevariante que no contiene ningún valor.
Al usar la Option<T>enumeración, podemos expresar el concepto abstracto de un
valor opcional y, al Option<T>ser genérica, podemos usar esta abstracción
independientemente del tipo del valor opcional.
Rust
jueves, 22 de mayo de 2025 : Página 243 de 719

Las enumeraciones también pueden usar múltiples tipos genéricos. La definición


de Result enumeración que usamos en el capítulo 9 es un ejemplo:

enum Result<T, E> {


Ok(T),
Err(E),
}

La Resultenumeración es genérica para dos tipos, Ty E, y tiene dos variantes: Ok,


que contiene un valor de tipo T, y Err, que contiene un valor de tipo E. Esta
definición facilita su uso Resulten cualquier operación que pueda tener éxito
(devolver un valor de algún tipo T) o fallar (devolver un error de algún tipo E). De
hecho, esto es lo que usamos para abrir un archivo en el Listado 9-3, donde Tse
rellenó con el tipo std::fs::Filecuando el archivo se abrió correctamente y Ese
rellenó con el tipo std::io::Errorcuando hubo problemas al abrirlo.

Cuando reconoce situaciones en su código con múltiples definiciones de


estructuras o enumeraciones que difieren solo en los tipos de valores que
contienen, puede evitar la duplicación utilizando tipos genéricos en su lugar.

En Definiciones de Métodos

Podemos implementar métodos en estructuras y enumeraciones (como hicimos


en el Capítulo 5) y también usar tipos genéricos en sus definiciones. El Listado 10-
9 muestra la Point<T> estructura definida en el Listado 10-6 con un método
llamado ximplementado en ella.

Nombre de archivo: 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 };

println!("p.x = {}", p.x());


Rust
jueves, 22 de mayo de 2025 : Página 244 de 719
}
Listado 10-9: Implementación de un método nombrado xen la Point<T>estructura
que devolverá una referencia al xcampo de tipoT

Aquí, hemos definido un método llamado xon Point<T>que devuelve una


referencia a los datos en el campo x.

Tenga en cuenta que debemos declarar Tjusto después de [ implnombre de la


estructura] para poder usar [ Tnombre de la estructura] para especificar que
estamos implementando métodos en el tipo [nombre de la estructura] Point<T>.
Al declarar Tcomo un tipo genérico después de [nombre de la estructura] impl,
Rust puede identificar que el tipo entre corchetes angulares Pointes un tipo
genérico en lugar de un tipo concreto. Podríamos haber elegido un nombre
diferente para este parámetro genérico que el declarado en la definición de la
estructura, pero usar el mismo nombre es convencional. Si escribe un método
dentro de [ implnombre de la estructura] que declara un tipo genérico, ese
método se definirá en cualquier instancia del tipo, independientemente del tipo
concreto que lo sustituya.

También podemos especificar restricciones sobre tipos genéricos al definir


métodos en el tipo. Por ejemplo, podríamos implementar métodos solo
en Point<f32>instancias, en lugar de en Point<T>instancias con cualquier tipo
genérico. En el Listado 10-10 usamos el tipo concreto f32, lo que significa que no
declaramos ningún tipo después de impl.

Nombre de archivo: 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 implbloque que solo se aplica a una estructura con un tipo
concreto particular para el parámetro de tipo genéricoT

Este código significa que el tipo Point<f32>tendrá


un distance_from_origin método; otras instancias de " Point<T>where T" que no
sean del tipo f32no tendrán este método definido. El método mide la distancia a
la que se encuentra nuestro punto respecto al punto en las coordenadas (0.0, 0.0)
y utiliza operaciones matemáticas disponibles solo para tipos de punto flotante.
Rust
jueves, 22 de mayo de 2025 : Página 245 de 719

Los parámetros de tipo genérico en una definición de estructura no siempre


coinciden con los que se usan en las firmas de método de esa misma estructura.
El Listado 10-11 utiliza los tipos genéricos ``` X1y `` Y1para la Pointestructura
y X2 Y2para la mixupfirma de método para que el ejemplo sea más claro. El
método crea una nueva Point instancia con el xvalor de ` self Point`` (de tipo X1)`
y el y valor de ``` pasado`` Point(de tipo Y2).

Nombre de archivo: src/main.rs


struct Point<X1, Y1> {
x: X1,
y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {


fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}

fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };

let p3 = p1.mixup(p2);

println!("p3.x = {}, p3.y = {}", p3.x, p3.y);


}
Listado 10-11: Un método que utiliza tipos genéricos diferentes de la definición de
su estructura

En [nombre del método] main, definimos una Pointfunción que tiene un


[ i32nombre xdel método 5] (con valor) y un [ f64nombre ydel método] (con
valor ) 10.4. La p2variable es una Pointestructura que tiene una porción de
cadena [nombre del método] x(con valor "Hello") y un [ charnombre del
método] y (con valor c). Al llamar mixupa [nombre del método p1] con el
argumento , p2obtenemos [nombre del método] p3, que tendrá un [ i32nombre
del método] xporque xvino de [nombre del método] p1. La p3variable tendrá un
[ charnombre del método] yporque yvino de [nombre del método] p2. La println!
macro imprimirá [nombre del método p3.x = 5, p3.y = c].
Rust
jueves, 22 de mayo de 2025 : Página 246 de 719

El propósito de este ejemplo es demostrar una situación en la que algunos


parámetros genéricos se declaran con implla definición del método y otros con
ella. En este caso, los parámetros genéricos X1y Y1se declaran
después implporque corresponden a la definición de la estructura. Los parámetros
genéricos X2 y Y2se declaran después fn mixupporque solo son relevantes para el
método.

Rendimiento del código utilizando genéricos

Quizás te preguntes si usar parámetros de tipo genérico tiene un costo de tiempo


de ejecución. La buena noticia es que usar tipos genéricos no hará que tu
programa se ejecute más lento que con tipos concretos.

Rust logra esto mediante la monomorfización del código mediante genéricos en


tiempo de compilación. La monomorfización es el proceso de convertir código
genérico en código específico rellenando los tipos concretos utilizados durante la
compilación. En este proceso, el compilador realiza los pasos inversos a los que
utilizamos para crear la función genérica del Listado 10-5: el compilador examina
todos los lugares donde se invoca código genérico y genera código para los tipos
concretos con los que se invoca.

Veamos cómo funciona esto utilizando la Option<T>enumeración genérica de la


biblioteca estándar:

let integer = Some(5);


let float = Some(5.0);

Cuando Rust compila este código, realiza una monomorfización. Durante este
proceso, el compilador lee los valores usados en Option<T> las instancias e
identifica dos tipos de Option<T>: uno es i32y el otro es f64. Por lo tanto,
expande la definición genérica de Option<T>en dos definiciones especializadas
en i32y f64, reemplazando así la definición genérica por las específicas.

La versión monomorfizada del código se parece a la siguiente (el compilador usa


nombres diferentes a los que usamos aquí para la ilustración):

Nombre de archivo: src/main.rs


enum Option_i32 {
Some(i32),
None,
}
Rust
jueves, 22 de mayo de 2025 : Página 247 de 719

enum Option_f64 {
Some(f64),
None,
}

fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}

El genérico Option<T>se reemplaza con las definiciones específicas creadas por


el compilador. Dado que Rust compila código genérico en código que especifica el
tipo en cada instancia, no incurrimos en costos de tiempo de ejecución por usar
genéricos. Al ejecutarse, el código funciona igual que si hubiéramos duplicado
cada definición manualmente. El proceso de monomorfización hace que los
genéricos de Rust sean extremadamente eficientes en tiempo de ejecución.

Rasgos: Definición del comportamiento compartido

Un rasgo define la funcionalidad que un tipo específico posee y puede compartir


con otros tipos. Podemos usar rasgos para definir un comportamiento compartido
de forma abstracta. Podemos usar límites de rasgos para especificar que un tipo
genérico puede ser cualquier tipo con un comportamiento determinado.

Nota: Los rasgos son similares a una característica a menudo


llamada interfaces en otros lenguajes, aunque con algunas diferencias.

Definiendo un rasgo

El comportamiento de un tipo consiste en los métodos que podemos invocar en


él. Diferentes tipos comparten el mismo comportamiento si podemos invocar los
mismos métodos en todos ellos. Las definiciones de rasgos son una forma de
agrupar las firmas de métodos para definir un conjunto de comportamientos
necesarios para lograr un propósito.

Por ejemplo, digamos que tenemos múltiples estructuras que contienen distintos
tipos y cantidades de texto: una NewsArticleestructura que contiene una noticia
archivada en una ubicación particular y una Tweetque puede tener, como
máximo, 280 caracteres junto con metadatos que indican si fue un nuevo tweet,
un retweet o una respuesta a otro tweet.
Rust
jueves, 22 de mayo de 2025 : Página 248 de 719

Queremos crear una biblioteca de agregadores de medios llamada


crate aggregatorque pueda mostrar resúmenes de datos que podrían estar
almacenados en una instancia NewsArticleo Tweet . Para ello, necesitamos un
resumen de cada tipo y lo solicitaremos llamando a un summarizemétodo en una
instancia. El listado 10-12 muestra la definición de un Summaryrasgo público que
expresa este comportamiento.

Nombre de archivo: src/lib.rs


pub trait Summary {
fn summarize(&self) -> String;
}
Listado 10-12: Un Summaryrasgo que consiste en el comportamiento
proporcionado por un summarizemétodo

Aquí, declaramos un rasgo usando la traitpalabra clave y luego su nombre,


que Summaryen este caso es . También declaramos el rasgo como pubpara que
las cajas que dependen de él también puedan usarlo, como veremos en algunos
ejemplos. Dentro de las llaves, declaramos las firmas de los métodos que
describen el comportamiento de los tipos que implementan este rasgo, que en
este caso es fn summarize(&self) -> String.

Después de la firma del método, en lugar de proporcionar una implementación


entre llaves, usamos un punto y coma. Cada tipo que implemente este atributo
debe proporcionar su propio comportamiento personalizado para el cuerpo del
método. El compilador garantizará que cualquier tipo que tenga
el Summaryatributo tenga el método summarize definido exactamente con esta
firma.

Un rasgo puede tener múltiples métodos en su cuerpo: las firmas de los métodos
se enumeran una por línea y cada línea termina en punto y coma.

Implementar un rasgo en un tipo

Ahora que hemos definido las firmas deseadas de los Summarymétodos del rasgo,
podemos implementarlo en los tipos de nuestro agregador de medios. El Listado
10-13 muestra una implementación del Summaryrasgo en
elNewsArticle estructura que utiliza el título, el autor y la ubicación para crear el
valor de retorno de summarize. Para la Tweetestructura,
definimos summarizecomo el nombre de usuario seguido del texto completo del
tuit, asumiendo que el contenido del tuit ya está limitado a 280 caracteres.
Rust
jueves, 22 de mayo de 2025 : Página 249 de 719
Nombre de archivo: src/lib.rs
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}

impl Summary for NewsArticle {


fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}

pub struct Tweet {


pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}

impl Summary for Tweet {


fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Listado 10-13: Implementación de laSummary rasgo en
los tipos NewsArticleyTweet

Implementar un rasgo en un tipo es similar a implementar métodos regulares. La


diferencia radica en que impl, después de `<string>`, se introduce el nombre del
rasgo que se desea implementar, se usa la forpalabra clave y se especifica el
nombre del tipo para el que se desea implementar. Dentro del implbloque, se
incluyen las firmas de método definidas por la definición del rasgo. En lugar de
añadir un punto y coma después de cada firma, se usan llaves y se completa el
cuerpo del método con el comportamiento específico que se desea que tengan los
métodos del rasgo para el tipo en cuestión.

Ahora que la biblioteca ha implementado el Summaryrasgo


en NewsArticley Tweet, los usuarios del crate pueden llamar a los métodos del
rasgo en instancias de NewsArticley Tweetde la misma forma que llamamos a los
métodos regulares. La única diferencia es que el usuario debe incluir el rasgo en
el ámbito, así como los tipos. Aquí hay un ejemplo de cómo un crate binario
podría usar nuestro aggregator crate de biblioteca:
Rust
jueves, 22 de mayo de 2025 : Página 250 de 719
use aggregator::{Summary, Tweet};

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,
};

println!("1 new tweet: {}", tweet.summarize());


}

Este código imprime 1 new tweet: horse_ebooks: of course, as you probably


already know, people.

Otras cajas que dependen de la aggregatorcaja también pueden incluir


el Summary rasgo en el alcance para implementarlo Summaryen sus propios
tipos. Una restricción a tener en cuenta es que solo podemos implementar un
rasgo en un tipo si el rasgo, el tipo o ambos son locales a nuestra caja. Por
ejemplo, podemos implementar rasgos de la biblioteca estándar Displayen un tipo
personalizado como Tweetparte de aggregatorla funcionalidad de nuestra caja, ya
que el tipo Tweetes local a nuestra aggregatorcaja. También podemos
implementar Summaryen Vec<T>nuestro aggregator caja, ya que el
rasgo Summaryes local a nuestra aggregator caja.

Pero no podemos implementar rasgos externos en tipos externos. Por ejemplo, no


podemos implementar el Displayrasgo Vec<T>dentro de nuestra aggregatorcaja
porque tanto Displayy Vec<T>como están definidos en la biblioteca estándar no
son locales a nuestra aggregatorcaja. Esta restricción forma parte de una
propiedad llamada coherencia , y más específicamente, regla "orphan" , llamada
así porque el tipo padre no está presente. Esta regla garantiza que el código de
otros usuarios no pueda romper el tuyo y viceversa. Sin esta regla, dos crates
podrían implementar el mismo rasgo para el mismo tipo, y Rust no sabría qué
implementación usar.

Implementaciones predeterminadas

A veces resulta útil tener un comportamiento predeterminado para algunos o


todos los métodos de un rasgo, en lugar de requerir implementaciones para todos
Rust
jueves, 22 de mayo de 2025 : Página 251 de 719

los métodos en cada tipo. De esta manera, al implementar el rasgo en un tipo


específico, podemos mantener o anular el comportamiento predeterminado de
cada método.

En el Listado 10-14, especificamos una cadena predeterminada para


el summarizemétodo del Summaryrasgo en lugar de solo definir la firma del
método, como hicimos en el Listado 10-12.

Nombre de archivo: src/lib.rs


pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
Listado 10-14: Definición de un Summaryrasgo con una implementación
predeterminada del summarizemétodo

Para utilizar una implementación predeterminada para resumir instancias


de NewsArticle, especificamos un implbloque vacío conimpl Summary for
NewsArticle {} .

Aunque ya no definimos el summarizemétodo NewsArticle directamente,


proporcionamos una implementación predeterminada y especificamos
que NewsArticleimplementa el Summaryatributo. Por lo tanto, aún podemos
llamar al summarizemétodo en una instancia de NewsArticle, como se muestra a
continuación:

let article = NewsArticle {


headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};

println!("New article available! {}", article.summarize());

Este código se imprimeNew article available! (Read more...).

La creación de una implementación predeterminada no requiere que cambiemos


nada en la implementación de SummaryonTweet en el Listado 10-13. Esto se
Rust
jueves, 22 de mayo de 2025 : Página 252 de 719

debe a que la sintaxis para sobrescribir una implementación predeterminada es la


misma que la sintaxis para implementar un método de rasgo sin implementación
predeterminada.

Las implementaciones predeterminadas pueden llamar a otros métodos del


mismo rasgo, incluso si estos no tienen una implementación predeterminada. De
esta manera, un rasgo puede proporcionar una gran cantidad de funcionalidades
útiles y solo requerir que los implementadores especifiquen una pequeña parte.
Por ejemplo, podríamos definir el Summaryrasgo con
un summarize_authormétodo cuya implementación sea obligatoria y luego definir
un summarizemétodo con una implementación predeterminada que lo
llame summarize_author:

pub trait Summary {


fn summarize_author(&self) -> String;

fn summarize(&self) -> String {


format!("(Read more from {}...)", self.summarize_author())
}
}

Para utilizar esta versión de Summary, solo necesitamos


definir summarize_author cuándo implementamos el rasgo en un tipo:

impl Summary for Tweet {


fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}

Tras definir summarize_author, podemos invocar summarizeinstancias de


la Tweetestructura, y la implementación predeterminada de summarizellamará a
la definición de summarize_authorque proporcionamos. Al haber
implementado summarize_author, el Summaryrasgo nos proporciona el
comportamiento del summarizemétodo sin necesidad de escribir más código. Así
es como se ve:

let tweet = Tweet {


username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
Rust
jueves, 22 de mayo de 2025 : Página 253 de 719
retweet: false,
};

println!("1 new tweet: {}", tweet.summarize());

Este código imprime 1 new tweet: (Read more from @horse_ebooks...).

Tenga en cuenta que no es posible llamar a la implementación predeterminada


desde una implementación anterior de ese mismo método.

Los rasgos como parámetros

Ahora que sabe cómo definir e implementar rasgos, podemos explorar cómo
usarlos para definir funciones que acepten diversos tipos. Usaremos
el Summaryrasgo que implementamos en los tipos NewsArticley Tweetdel Listado
10-13 para definir una notifyfunción que invoque el summarizemétodo en
su itemparámetro, que es de algún tipo que implementa el Summary rasgo. Para
ello, usamos la impl Traitsiguiente sintaxis:

pub fn notify(item: &impl Summary) {


println!("Breaking news! {}", item.summarize());
}

En lugar de un tipo concreto para el itemparámetro, especificamos la impl palabra


clave y el nombre del rasgo. Este parámetro acepta cualquier tipo que
implemente el rasgo especificado. En el cuerpo de notify, podemos llamar a
cualquier método item que provenga del Summaryrasgo, como summarize.
Podemos llamar notify y pasar cualquier instancia de NewsArticleo Tweet. El
código que llama a la función con cualquier otro tipo, como a Stringo an i32, no se
compilará porque esos tipos no implementan Summary.

Sintaxis ligada a rasgos

La impl Traitsintaxis funciona para casos sencillos, pero en realidad es azúcar


sintáctica para una forma más larga conocida como límite de rasgo ; se ve así:

pub fn notify<T: Summary>(item: &T) {


println!("Breaking news! {}", item.summarize());
}
Rust
jueves, 22 de mayo de 2025 : Página 254 de 719

Esta forma más larga es equivalente al ejemplo de la sección anterior, pero más
detallada. Colocamos los límites de los rasgos con la declaración del parámetro de
tipo genérico después de dos puntos y entre corchetes angulares.

La impl Traitsintaxis es práctica y permite un código más conciso en casos


sencillos, mientras que la sintaxis más completa, ligada a rasgos, puede expresar
mayor complejidad en otros casos. Por ejemplo, podemos tener dos parámetros
que implementen Summary. Hacerlo con la impl Traitsintaxis se ve así:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

Usar impl Traites apropiado si queremos que esta función


permita item1y item2tenga diferentes tipos (siempre que ambos
implementen Summary). Sin embargo, si queremos forzar que ambos parámetros
tengan el mismo tipo, debemos usar un atributo enlazado, como este:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

El tipo genérico Tespecificado como el tipo de los item1parámetros item2 y


restringe la función de modo que el tipo concreto del valor pasado como
argumento para item1y item2debe ser el mismo.

Especificación de múltiples límites de rasgos con la +sintaxis

También podemos especificar más de un atributo enlazado. Supongamos que


queremos notifyusar el formato de visualización, así como summarizeon item:
especificamos en la notify definición que itemse deben implementar
tanto Displayy Summary. Podemos hacerlo usando la +sintaxis:

pub fn notify(item: &(impl Summary + Display)) {

La +sintaxis también es válida con límites de rasgos en tipos genéricos:

pub fn notify<T: Summary + Display>(item: &T) {

Con los dos límites de características especificados, el cuerpo de notifypuede


llamar summarize y usar {}para formatear item.

Límites de rasgos más claros con wherecláusulas

Usar demasiados límites de rasgo tiene sus desventajas. Cada genérico tiene sus
propios límites de rasgo, por lo que las funciones con múltiples parámetros de
Rust
jueves, 22 de mayo de 2025 : Página 255 de 719

tipo genérico pueden contener mucha información de límites de rasgo entre el


nombre de la función y su lista de parámetros, lo que dificulta la lectura de la
firma de la función. Por esta razón, Rust tiene una sintaxis alternativa para
especificar los límites de rasgo dentro de una wherecláusula después de la firma
de la función. Así, en lugar de escribir esto:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

Podemos usar una wherecláusula como ésta:

fn some_function<T, U>(t: &T, u: &U) -> i32


where
T: Display + Clone,
U: Clone + Debug,
{

La firma de esta función es menos desordenada: el nombre de la función, la lista


de parámetros y el tipo de retorno están juntos, de manera similar a una función
sin muchos límites de características.

Devolución de tipos que implementan rasgos

También podemos usar la impl Traitsintaxis en la posición de retorno para


devolver un valor de algún tipo que implemente un rasgo, como se muestra aquí:

fn returns_summarizable() -> impl Summary {


Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}

Al usar impl Summary``para el tipo de retorno``, especificamos que


la returns_summarizablefunción devuelve un tipo que implementa
el Summary atributo sin especificar el tipo concreto. En este
caso, returns_summarizable devuelve `` Tweet, pero el código que llama a esta
función no necesita saberlo.
Rust
jueves, 22 de mayo de 2025 : Página 256 de 719

La capacidad de especificar un tipo de retorno únicamente por el atributo que


implementa es especialmente útil en el contexto de cierres e iteradores, que
abordamos en el Capítulo 13. Los cierres e iteradores crean tipos que solo el
compilador conoce o que requieren una especificación muy extensa. La impl
Traitsintaxis permite especificar concisamente que una función devuelve un tipo
que implementa el Iteratoratributo sin necesidad de escribir un tipo muy extenso.

Sin embargo, solo se puede usar impl Traitsi se devuelve un solo tipo. Por
ejemplo, este código que devuelve a NewsArticleo a Tweetcon el tipo de retorno
especificado como impl Summaryno funcionaría:

fn returns_summarizable(switch: bool) -> impl Summary {


if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
}

No se permite devolver ni a NewsArticleni a Tweetdebido a restricciones en


la impl Traitimplementación de la sintaxis en el compilador. Explicaremos cómo
escribir una función con este comportamiento en la sección "Uso de objetos de
rasgo que admiten valores de diferentes tipos" del capítulo 18.

Uso de límites de rasgos para implementar métodos condicionalmente


Rust
jueves, 22 de mayo de 2025 : Página 257 de 719

Al usar un atributo enlazado con un implbloque que utiliza parámetros de tipo


genérico, podemos implementar métodos condicionalmente para los tipos que
implementan los atributos especificados. Por ejemplo, el tipo Pair<T>del Listado
10-15 siempre implementa la newfunción para devolver una nueva instancia
de Pair<T>(recuerde, de la sección "Definición de métodos" del Capítulo 5,
que Self es un alias de tipo para el tipo del implbloque, que en este caso
es Pair<T>). Sin embargo, en el siguiente implbloque, Pair<T>solo implementa
el cmp_displaymétodo si su tipo interno Timplementa el PartialOrdatributo que
permite la comparación y el Displayatributo que permite la impresión.

Nombre de archivo: 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 }
}
}

impl<T: Display + PartialOrd> Pair<T> {


fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
Listado 10-15: Implementación condicional de métodos en un tipo genérico
dependiendo de los límites de los rasgos

También podemos implementar condicionalmente un rasgo para cualquier tipo


que implemente otro rasgo. Las implementaciones de un rasgo en cualquier tipo
que cumpla con los límites del rasgo se denominan implementaciones generales y
se utilizan ampliamente en la biblioteca estándar de Rust. Por ejemplo, la
biblioteca estándar implementa el ToStringrasgo en cualquier tipo que lo
implemente Display. El impl bloque en la biblioteca estándar se parece a este
código:
Rust
jueves, 22 de mayo de 2025 : Página 258 de 719
impl<T: Display> ToString for T {
// --snip--
}

Dado que la biblioteca estándar cuenta con esta implementación general,


podemos llamar al to_stringmétodo definido por el ToStringrasgo en cualquier tipo
que lo implemente Display. Por ejemplo, podemos convertir enteros en
sus Stringvalores correspondientes de la siguiente manera, ya que los enteros
implementan Display:

let s = 3.to_string();

Las implementaciones generales aparecen en la documentación de la


característica en la sección “Implementadores”.

Los rasgos y sus límites nos permiten escribir código que usa parámetros de tipo
genérico para reducir la duplicación, pero también para especificar al compilador
que queremos que el tipo genérico tenga un comportamiento específico. El
compilador puede entonces usar la información de los límites de rasgos para
verificar que todos los tipos concretos usados en nuestro código proporcionen el
comportamiento correcto. En lenguajes de tipado dinámico, obtendríamos un
error en tiempo de ejecución si invocáramos un método en un tipo que no lo
definiera. Pero Rust traslada estos errores al tiempo de compilación, por lo que
nos vemos obligados a corregir los problemas incluso antes de que nuestro código
pueda ejecutarse. Además, no tenemos que escribir código que verifique el
comportamiento en tiempo de ejecución, ya que ya lo hemos hecho en tiempo de
compilación. Esto mejora el rendimiento sin tener que renunciar a la flexibilidad
de los genéricos.

Validación de referencias con tiempos de vida

Los tiempos de vida son otro tipo de genérico que ya hemos estado usando. En
lugar de garantizar que un tipo tenga el comportamiento deseado, los tiempos de
vida garantizan que las referencias sean válidas mientras las necesitemos.

Un detalle que no abordamos en la sección "Referencias y Préstamos" del


Capítulo 4 es que cada referencia en Rust tiene un tiempo de vida (lifetime) , que
define el ámbito de validez de dicha referencia. Generalmente, los tiempos de
vida son implícitos e inferidos, al igual que los tipos. Solo debemos anotar tipos
cuando sea posible tener varios. De forma similar, debemos anotar los tiempos de
Rust
jueves, 22 de mayo de 2025 : Página 259 de 719

vida cuando los tiempos de vida de las referencias puedan estar relacionados de
diferentes maneras. Rust requiere que anotemos las relaciones utilizando
parámetros genéricos de tiempo de vida para garantizar que las referencias
utilizadas en tiempo de ejecución sean válidas.

Anotar tiempos de vida no es un concepto que la mayoría de los demás lenguajes


de programación tengan, por lo que esto le resultará desconocido. Aunque no
cubriremos los tiempos de vida en su totalidad en este capítulo, analizaremos
formas comunes en las que puede encontrar la sintaxis de tiempos de vida para
que se familiarice con el concepto.

Cómo evitar referencias colgantes con tiempos de vida

El objetivo principal de los tiempos de vida es evitar las referencias colgantes ,


que hacen que un programa haga referencia a datos distintos a los que debería.
Considere el programa del Listado 10-16, que tiene un ámbito externo y un
ámbito interno.

fn main() {
let r;

{
let x = 5;
r = &x;
}

println!("r: {r}");
}
Listado 10-16: Un intento de utilizar una referencia cuyo valor ha quedado fuera
del alcance

Nota: Los ejemplos de los Listados 10-16, 10-17 y 10-23 declaran variables sin
asignarles un valor inicial, por lo que el nombre de la variable existe en el ámbito
externo. A primera vista, esto podría parecer incompatible con el hecho de que
Rust no permite valores nulos. Sin embargo, si intentamos usar una variable
antes de asignarle un valor, obtendremos un error de compilación, lo que indica
que Rust no permite valores nulos.

El ámbito externo declara una variable llamada rsin valor inicial, y el ámbito
interno declara una variable llamada xcon el valor inicial de 5. Dentro del ámbito
Rust
jueves, 22 de mayo de 2025 : Página 260 de 719

interno, intentamos establecer el valor de rcomo referencia a x. Luego, el ámbito


interno finaliza e intentamos imprimir el valor en r. Este código no se compila
porque el valor al que rhace referencia ha salido del ámbito antes de que
intentáramos usarlo. Este es 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

El mensaje de error indica que la variable x"no tiene una vida útil suficiente". Esto
se debe a que xquedará fuera de alcance cuando el ámbito interno termine en la
línea 7. Sin embargo, rsigue siendo válida para el ámbito externo; dado que su
alcance es mayor, decimos que "tiene una vida útil mayor". Si Rust permitiera que
este código funcionara, restaría haciendo referencia a memoria desasignada
al xquedar fuera de alcance, y cualquier acción que intentáramos realizar rno
funcionaría correctamente. Entonces, ¿cómo determina Rust que este código no
es válido? Utiliza un comprobador de préstamos.

El verificador de préstamos

El compilador de Rust cuenta con un verificador de préstamos que compara los


ámbitos para determinar si todos los préstamos son válidos. El Listado 10-17
muestra el mismo código que el Listado 10-16, pero con anotaciones que indican
la duración de las variables.

fn main() {
let r; // ---------+-- 'a
// |
Rust
jueves, 22 de mayo de 2025 : Página 261 de 719
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
Listado 10-17: Anotaciones de las vidas de ry x, nombrados 'ay 'b,
respectivamente

Aquí, hemos anotado el tiempo de vida de ` rwith` 'ay el tiempo de vida


de x `with` 'b. Como puede ver, el 'bbloque interno es mucho más pequeño que el
externo 'a. En tiempo de compilación, Rust compara el tamaño de ambos tiempos
de vida y observa que `` rtiene un tiempo de vida de` 'apero que se refiere a
memoria con un tiempo de vida de `` 'b. El programa se rechaza porque ` 'b` es
más corto que` 'a: el objeto de la referencia no tiene una vida tan larga como la
referencia.

El listado 10-18 corrige el código para que no tenga una referencia colgante y se
compile sin errores.

fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {r}"); // | |
// --+ |
} // ----------+
Listado 10-18: Una referencia válida porque los datos tienen una vida útil más
larga que la referencia

Aquí, xtiene un tiempo de vida 'b, que en este caso es mayor que 'a. Esto significa
que rse puede hacer referencia x, ya que Rust sabe que la referencia en rsiempre
será válida mientras xsea válida.

Ahora que sabe qué son los tiempos de vida de las referencias y cómo Rust
analiza los tiempos de vida para garantizar que las referencias siempre sean
válidas, exploremos los tiempos de vida genéricos de los parámetros y los valores
de retorno en el contexto de las funciones.

Duración de la vida genérica en funciones


Rust
jueves, 22 de mayo de 2025 : Página 262 de 719

Escribiremos una función que devuelva el fragmento de cadena más largo de dos.
Esta función tomará dos fragmentos de cadena y devolverá uno solo. Tras
implementar la longestfunción, el código del Listado 10-19 debería mostrar
`<sub> The longest string is abcd.

Nombre de archivo: src/main.rs


fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

let result = longest(string1.as_str(), string2);


println!("The longest string is {result}");
}
Listado 10-19: Una mainfunción que llama a la longestfunción para encontrar la
porción más larga de dos cadenas

Tenga en cuenta que queremos que la función tome segmentos de cadena, que
son referencias, en lugar de cadenas, porque no queremos que la longestfunción
tome posesión de sus parámetros. Consulte la sección "Sectores de cadena como
parámetros" en el capítulo 4 para obtener más información sobre por qué los
parámetros que usamos en el listado 10-19 son los que necesitamos.

Si intentamos implementar la longestfunción como se muestra en el Listado 10-


20, no se compilará.

Nombre de archivo: src/main.rs

fn longest(x: &str, y: &str) -> &str {


if x.len() > y.len() {
x
} else {
y
}
}
Listado 10-20: Una implementación de la longestfunción que devuelve el
fragmento más largo de dos cadenas pero que aún no se compila

En cambio, obtenemos el siguiente error que habla de tiempos de vida:

$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
Rust
jueves, 22 de mayo de 2025 : Página 263 de 719
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature
does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

El texto de ayuda revela que el tipo de retorno necesita un parámetro genérico de


duración, ya que Rust no puede determinar si la referencia devuelta se refiere
a xo a y. En realidad, nosotros tampoco lo sabemos, ya que el ifbloque en el
cuerpo de esta función devuelve una referencia a xy el elsebloque devuelve una
referencia ay !

Al definir esta función, desconocemos los valores concretos que se le pasarán, por
lo que desconocemos si se ejecutará el ifcaso o el elsecaso. Tampoco conocemos
la duración de las referencias que se pasarán, por lo que no podemos examinar
los ámbitos como hicimos en los Listados 10-17 y 10-18 para determinar si la
referencia que devolvemos siempre será válida. El verificador de préstamos
tampoco puede determinar esto, ya que desconoce la relación entre la duración
de xy yla duración del valor de retorno. Para corregir este error, añadiremos
parámetros de duración genéricos que definen la relación entre las referencias
para que el verificador de préstamos pueda realizar su análisis.

Sintaxis de anotación de duración

Las anotaciones de duración no modifican la duración de las referencias. En


cambio, describen la relación entre las duraciones de múltiples referencias, sin
afectarlas. 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 duración especificando un parámetro de duración
genérico.

Las anotaciones de duración tienen una sintaxis un tanto inusual: los nombres de
los parámetros de duración deben comenzar con un apóstrofo ( ') y suelen ser
todos en minúsculas y muy cortos, como los tipos genéricos. La mayoría de las
Rust
jueves, 22 de mayo de 2025 : Página 264 de 719

personas usan el nombre 'apara la primera anotación de duración. Las


anotaciones de parámetros de duración se colocan después de &una referencia,
separando la anotación del tipo de la referencia con un espacio.

A continuación se muestran algunos ejemplos: una referencia a un i32sin un


parámetro de duración, una referencia a un i32que tiene un parámetro de
duración llamado 'ay una referencia mutable a un i32que también tiene el
parámetro de duración '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 duración por sí sola no tiene mucho significado, ya que su


propósito es indicar a Rust cómo se relacionan los parámetros de duración
genéricos de múltiples referencias. Examinemos cómo se relacionan las
anotaciones de duración en el contexto de la longestfunción.

Anotaciones de duración en las firmas de funciones

Para utilizar anotaciones de duración en las firmas de funciones, necesitamos


declarar los parámetros de duración genéricos dentro de corchetes angulares
entre el nombre de la función y la lista de parámetros, tal como lo hicimos con los
parámetros de tipo genérico .

Queremos que la firma exprese la siguiente restricción: la referencia devuelta


será válida mientras ambos parámetros sean válidos. Esta es la relación entre la
duración de los parámetros y el valor de retorno. Asignaremos un nombre a la
duración 'ay lo añadiremos a cada referencia, como se muestra en el Listado 10-
21.

Nombre de archivo: src/main.rs


fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Listado 10-21: La longestdefinición de función que especifica que todas las
referencias en la firma deben tener el mismo tiempo de vida'a
Rust
jueves, 22 de mayo de 2025 : Página 265 de 719

Este código debería compilarse y producir el resultado que queremos cuando lo


usamos con la mainfunción del Listado 10-19.

La firma de la función ahora indica a Rust que, durante un tiempo de vida 'a, la
función toma dos parámetros, ambos fragmentos de cadena con una vida útil al
menos igual a la de un tiempo de vida 'a. La firma de la función también indica a
Rust que el fragmento de cadena devuelto por la función tendrá una vida útil al
menos igual a la de un tiempo de vida 'a. En la práctica, esto significa que la vida
útil de la referencia devuelta por la longestfunción es igual al menor de los
tiempos de vida de los valores a los que hacen referencia los argumentos de la
función. Estas relaciones son las que queremos que Rust utilice al analizar este
código.

Recuerde que, al especificar los parámetros de duración en la firma de esta


función, no modificamos la duración de los valores pasados o devueltos. En
cambio, especificamos que el verificador de préstamos debe rechazar cualquier
valor que no cumpla con estas restricciones. Tenga en cuenta que
la longestfunción no necesita saber exactamente cuánto durará xy ycuánto
durará, solo que se puede sustituir algún ámbito 'aque cumpla con esta firma.

Al anotar los tiempos de vida de las funciones, estas se incluyen en la firma de la


función, no en su cuerpo. Estas anotaciones forman parte del contrato de la
función, al igual que los tipos en la firma. Que las firmas de las funciones incluyan
el contrato de vida simplifica el análisis del compilador de Rust. Si hay un
problema con la forma en que se anota o se llama una función, los errores del
compilador pueden identificar la parte de nuestro código y las restricciones con
mayor precisión. Si, por el contrario, el compilador de Rust hiciera más inferencias
sobre cómo pretendíamos que fueran las relaciones de los tiempos de vida, solo
podría identificar un uso de nuestro código muy alejado de la causa del problema.

Al pasar referencias concretas a longest, la duración concreta que se


sustituye 'aes la parte del alcance de xque se superpone con el alcance de y. En
otras palabras, la duración genérica 'aobtendrá la duración concreta igual al
menor de los valores de duración de xy y. Dado que hemos anotado la referencia
devuelta con el mismo parámetro de duración 'a, esta también será válida
durante el menor de los valores de duración de xy y.
Rust
jueves, 22 de mayo de 2025 : Página 266 de 719

Veamos cómo las anotaciones de duración restringen la longestfunción al pasar


referencias con diferentes duraciones concretas. El Listado 10-22 es un ejemplo
sencillo.

Nombre de archivo: 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: Uso de la longestfunción con referencias a Stringvalores que tienen
diferentes duraciones de vida concretas

En este ejemplo, string1es válido hasta el final del ámbito externo, string2 es
válido hasta el final del ámbito interno y resulthace referencia a algo válido hasta
el final del ámbito interno. Ejecute este código y verá que el verificador de
préstamos lo aprueba; compilará e imprimirá ` The longest string is long string is
long.`.

A continuación, probemos un ejemplo que muestra que el tiempo de vida de la


referencia en resultdebe ser el menor tiempo de vida de los dos argumentos.
Moveremos la declaración de la resultvariable fuera del ámbito interno, pero
dejaremos la asignación del valor a la resultvariable dentro del ámbito
con string2. Después, moveremos el println!que usa resultfuera del ámbito
interno, una vez que este haya finalizado. El código del Listado 10-23 no
compilará.

Nombre de archivo: 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: El intento de usar result"after" string2ha quedado fuera del alcance
Rust
jueves, 22 de mayo de 2025 : Página 267 de 719

Cuando intentamos compilar este código, obtenemos este error:

$ 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 indica que, para resultque la println!instrucción sea válida, string2debería


ser válida hasta el final del alcance externo. Rust lo sabe porque anotamos la
duración de los parámetros de la función y los valores de retorno con el mismo
parámetro de duración 'a.

Como humanos, podemos observar este código y ver que string1es más largo
que string2y, por lo tanto, resultcontendrá una referencia a string1. Dado
que string1aún no ha salido del ámbito, una referencia a string1seguirá siendo
válida para la println!instrucción. Sin embargo, el compilador no puede ver que la
referencia es válida en este caso. Hemos indicado a Rust que el tiempo de vida de
la referencia devuelta por la longestfunción es igual al menor de los tiempos de
vida de las referencias pasadas. Por lo tanto, el verificador de préstamos rechaza
el código del Listado 10-23 por la posibilidad de que contenga una referencia no
válida.

Intenta diseñar más experimentos que varíen los valores y la duración de las
referencias pasadas a la longestfunción y cómo se usa la referencia devuelta.
Formula hipótesis sobre si tus experimentos pasarán la verificación de préstamos
antes de compilar; luego, ¡verifica si aciertas!

Pensando en términos de vidas


Rust
jueves, 22 de mayo de 2025 : Página 268 de 719

La forma de especificar los parámetros de duración depende de la función. Por


ejemplo, si modificamos la implementación de la longestfunción para que siempre
devuelva el primer parámetro en lugar del fragmento de cadena más largo, no
sería necesario especificar la duración del yparámetro. El siguiente código se
compilará:

Nombre de archivo: src/main.rs


fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}

Hemos especificado un parámetro de duración 'apara el parámetro xy el tipo de


retorno, pero no para el parámetro y, porque la duración de yno tiene ninguna
relación con la duración de xo el valor de retorno.

Al devolver una referencia desde una función, el parámetro de duración del tipo
de retorno debe coincidir con el parámetro de duración de uno de los parámetros.
Si la referencia devuelta no se refiere a uno de los parámetros, debe referirse a
un valor creado dentro de esta función. Sin embargo, esto sería una referencia
pendiente, ya que el valor quedaría fuera del ámbito al final de la función.
Considere este intento de implementación de la longestfunción que no compila:

Nombre de archivo: src/main.rs

fn longest<'a>(x: &str, y: &str) -> &'a str {


let result = String::from("really long string");
result.as_str()
}

Aquí, aunque especificamos un parámetro de duración 'apara el tipo de retorno,


esta implementación no se compilará porque la duración del valor de retorno no
está relacionada con la duración de los parámetros. Este es el mensaje de error
que recibimos:

$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
Rust
jueves, 22 de mayo de 2025 : Página 269 de 719
| returns a value referencing data owned by the current function
| `result` is borrowed here

For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

El problema es que resultse sale del ámbito y se limpia al final de


la longestfunción. También intentamos devolver una referencia aresult la función.
No podemos especificar parámetros de duración que modifiquen la referencia
pendiente, y Rust no nos permite crearla. En este caso, la mejor solución sería
devolver un tipo de dato propio en lugar de una referencia, de modo que la
función que realiza la llamada se encargue de limpiar el valor.

En definitiva, la sintaxis de duración de vida consiste en conectar la duración de


varios parámetros y los valores de retorno de las funciones. Una vez conectados,
Rust dispone de suficiente información para permitir operaciones seguras en
memoria y rechazar operaciones que generen punteros colgantes o violen la
seguridad de la memoria.

Anotaciones de duración en definiciones de estructuras

Hasta ahora, todas las estructuras definidas contienen tipos propios. Podemos
definir estructuras para contener referencias, pero en ese caso necesitaríamos
añadir una anotación de duración a cada referencia en la definición de la
estructura. El listado 10-24 contiene una estructura llamada ImportantExcerptque
contiene una porción de cadena.

Nombre de archivo: 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().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
Listado 10-24: Una estructura que contiene una referencia, que requiere una
anotación de duración
Rust
jueves, 22 de mayo de 2025 : Página 270 de 719

Esta estructura tiene un único campo partque contiene una porción de cadena,
que es una referencia. Al igual que con los tipos de datos genéricos, declaramos
el nombre del parámetro de duración genérico entre corchetes angulares después
del nombre de la estructura para poder usarlo en el cuerpo de la definición de la
estructura. Esta anotación significa que una instancia de ImportantExcerptno
puede sobrevivir a la referencia que contiene en su...part campo.

La mainfunción crea una instancia de la ImportantExcerptestructura que contiene


una referencia a la primera oración de la Stringpropiedad de la variable novel. Los
datos en novelexisten antes de la ImportantExcerpt creación de la instancia.
Además, novelno se extingue hasta después de que se ImportantExcerptextinga,
por lo que la referencia en la ImportantExcerptinstancia es válida.

Elisión de por vida

Has aprendido que cada referencia tiene un tiempo de vida y que es necesario
especificar parámetros de tiempo de vida para funciones o estructuras que usan
referencias. Sin embargo, teníamos una función en el Listado 4-9, que se muestra
de nuevo en el Listado 10-25, que se compilaba sin anotaciones de tiempo de
vida.

Nombre de archivo: src/lib.rs


fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {


if item == b' ' {
return &s[0..i];
}
}

&s[..]
}
Listado 10-25: Una función que definimos en el Listado 4-9 que se compiló sin
anotaciones de duración, aunque el parámetro y el tipo de retorno son referencias

La razón por la que esta función se compila sin anotaciones de duración es


histórica: en versiones anteriores a la 1.0 de Rust, este código no se compilaba
porque cada referencia requería una duración explícita. En aquel entonces, la
firma de la función se habría escrito así:

fn first_word<'a>(s: &'a str) -> &'a str {


Rust
jueves, 22 de mayo de 2025 : Página 271 de 719

Tras escribir mucho código en Rust, el equipo descubrió que los programadores
introducían las mismas anotaciones de duración una y otra vez en situaciones
específicas. Estas situaciones eran predecibles y seguían ciertos patrones
deterministas. Los desarrolladores programaron estos patrones en el código del
compilador para que el verificador de préstamos pudiera inferir las duraciones en
estas situaciones y no necesitara anotaciones explícitas.

Este fragmento de la historia de Rust es relevante porque es posible que surjan


más patrones deterministas y se añadan al compilador. En el futuro, podrían
requerirse incluso menos anotaciones de duración.

Los patrones programados en el análisis de referencias de Rust se


denominan reglas de elisión de tiempos de vida . No son reglas que los
programadores deban seguir, sino un conjunto de casos particulares que el
compilador considerará, y si su código cumple con estos casos, no es necesario
escribir los tiempos de vida explícitamente.

Las reglas de elisión no proporcionan una inferencia completa. Si persiste la


ambigüedad sobre la duración de las referencias después de que Rust aplique las
reglas, el compilador no calculará la duración de las referencias restantes. En
lugar de calcularla, el compilador generará un error que se puede resolver
añadiendo las anotaciones de duración.

Los tiempos de vida de los parámetros de una función o método se


denominan tiempos de vida de entrada , y los tiempos de vida de los valores de
retorno se denominan tiempos de vida de salida .

El compilador utiliza tres reglas para determinar la duración de las referencias


cuando no hay anotaciones explícitas. La primera regla se aplica a la duración de
entrada, y la segunda y la tercera, a la de salida. Si el compilador llega al final de
las tres reglas y aún existen referencias cuya duración no puede determinar, se
detendrá con un error. Estas reglas se aplican tanto a fnlas definiciones como
a impllos bloques.

La primera regla es que el compilador asigna un parámetro de duración a cada


parámetro que sea una referencia. En otras palabras, una función con un
parámetro obtiene un parámetro de duración: fn foo<'a>(x: &'a i32); una función
con dos parámetros obtiene dos parámetros de duración separados: fn foo<'a,
'b>(x: &'a i32, y: &'b i32); y así sucesivamente.
Rust
jueves, 22 de mayo de 2025 : Página 272 de 719

La segunda regla es que, si hay exactamente un parámetro de duración de


entrada, esa duración se asigna a todos los parámetros de duración de salida: fn
foo<'a>(x: &'a i32) -> &'a i32.

La tercera regla es que, si hay varios parámetros de duración de entrada, pero


uno de ellos es &selfo, &mut selfdado que se trata de un método, la duración
de selfse asigna a todos los parámetros de duración de salida. Esta tercera regla
facilita la lectura y escritura de los métodos, ya que se requieren menos símbolos.

Imaginemos que somos el compilador. Aplicaremos estas reglas para determinar


la duración de las referencias en la firma de la first_wordfunción del Listado 10-
25. La firma comienza sin ninguna duración asociada a las referencias:

fn first_word(s: &str) -> &str {

Luego, el compilador aplica la primera regla, que especifica que cada parámetro
tiene su propio tiempo de vida. La llamaremos 'acomo siempre, por lo que ahora
la firma es esta:

fn first_word<'a>(s: &'a str) -> &str {

La segunda regla se aplica porque hay exactamente un tiempo de vida de


entrada. Esta regla especifica que el tiempo de vida de un parámetro de entrada
se asigna al tiempo de vida de salida, por lo que la firma ahora es la siguiente:

fn first_word<'a>(s: &'a str) -> &'a str {

Ahora todas las referencias en esta firma de función tienen tiempos de vida, y el
compilador puede continuar su análisis sin necesidad de que el programador
anote los tiempos de vida en esta firma de función.

Veamos otro ejemplo, esta vez utilizando la longestfunción que no tenía


parámetros de duración cuando comenzamos a trabajar con ella en el Listado 10-
20:

fn longest(x: &str, y: &str) -> &str {

Apliquemos la primera regla: cada parámetro tiene su propio tiempo de vida. Esta
vez, tenemos dos parámetros en lugar de uno, por lo que tenemos dos tiempos de
vida:
Rust
jueves, 22 de mayo de 2025 : Página 273 de 719
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

Se puede observar que la segunda regla no aplica porque hay más de un tiempo
de vida de entrada. La tercera regla tampoco aplica, porque longestes una
función en lugar de un método, por lo que ninguno de los parámetros es self. Tras
aplicar las tres reglas, aún no hemos determinado el tiempo de vida del tipo de
retorno. Por eso recibimos un error al intentar compilar el código del Listado 10-
20: el compilador aplicó las reglas de elisión de tiempo de vida, pero no pudo
determinar todos los tiempos de vida de las referencias en la firma.

Dado que la tercera regla realmente solo se aplica en las firmas de métodos,
analizaremos los tiempos de vida en ese contexto a continuación para ver por qué
la tercera regla significa que no tenemos que anotar tiempos de vida en las firmas
de métodos muy a menudo.

Anotaciones de duración en las definiciones de métodos

Al implementar métodos en una estructura con tiempos de vida, utilizamos la


misma sintaxis que para los parámetros de tipo genérico que se muestra en el
Listado 10-11. El lugar donde declaramos y utilizamos los parámetros de tiempo
de vida depende de si están relacionados con los campos de la estructura o con
los parámetros del método y sus valores de retorno.

Los nombres de duración de vida de los campos de estructura siempre deben


declararse después de la impl palabra clave y luego usarse después del nombre
de la estructura porque esos tiempos de vida son parte del tipo de la estructura.

En las firmas de métodos dentro del implbloque, las referencias pueden estar
vinculadas a la duración de las referencias en los campos de la estructura, o
pueden ser independientes. Además, las reglas de elisión de duración suelen
impedir que las anotaciones de duración sean necesarias en las firmas de
métodos. Veamos algunos ejemplos con la estructura
denominada ImportantExcerptque definimos en el Listado 10-24.

Primero usaremos un método llamado levelcuyo único parámetro es una


referencia a selfy cuyo valor de retorno es un i32, que no es una referencia a
nada:

impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
Rust
jueves, 22 de mayo de 2025 : Página 274 de 719
3
}
}

La declaración del parámetro de duración de vida imply su uso después del


nombre del tipo son obligatorios, pero no estamos obligados a anotar la duración
de vida de la referencia selfdebido a la primera regla de elisión.

A continuación se muestra un ejemplo en el que se aplica la regla de elisión de la


tercera vida:

impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}

Hay dos tiempos de vida de entrada, por lo que Rust aplica la primera regla de
elisión de tiempos de vida y proporciona ambos &selftiempos announcementde
vida. Como uno de los parámetros es &self, el tipo de retorno obtiene el tiempo
de vida de &self, y se han tenido en cuenta todos los tiempos de vida.

La vida estática

Un tiempo de vida especial que debemos analizar es 'static, que indica que la
referencia afectada puede existir durante toda la duración del programa. Todos
los literales de cadena tienen un 'statictiempo de vida, que podemos anotar de la
siguiente manera:

let s: &'static str = "I have a static lifetime.";

El texto de esta cadena se almacena directamente en el binario del programa,


que siempre está disponible. Por lo tanto, el tiempo de vida de todos los literales
de cadena es 'static.

Es posible que vea sugerencias en los mensajes de error para usar


el 'statictiempo de vida. Pero antes de especificarlo 'staticcomo tiempo de vida
para una referencia, considere si la referencia realmente dura todo el tiempo de
vida de su programa y si desea que así sea. Generalmente, un mensaje de error
que sugiere el 'statictiempo de vida se debe a que se intenta crear una referencia
pendiente o a una discrepancia entre los tiempos de vida disponibles. En tales
Rust
jueves, 22 de mayo de 2025 : Página 275 de 719

casos, la solución es corregir esos problemas, no especificar el 'statictiempo de


vida.

Parámetros de tipo genérico, límites de rasgos y tiempos de vida juntos

¡Veamos brevemente la sintaxis para especificar parámetros de tipo genérico,


límites de rasgos y duraciones, todo en una sola función!

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 longestfunción del Listado 10-21 que devuelve el segmento de cadena


más largo de dos. Ahora, incluye un parámetro adicional llamado " annde tipo
genérico" T, que puede ser rellenado por cualquier tipo que implemente
el Display atributo especificado en la wherecláusula. Este parámetro adicional se
imprimirá usando {}", por lo que Displayes necesario el enlace del atributo".
Dado que los tiempos de vida son un tipo de genérico, las declaraciones del
parámetro "de tiempo de vida" 'ay del parámetro de tipo genérico Tse incluyen 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 genéricos, los rasgos y los límites de rasgos, y los parámetros de duración de
vida genéricos, estás listo para escribir código sin repeticiones que funcione en
diversas situaciones. Los parámetros de tipo genéricos te permiten aplicar el
código a diferentes tipos. Los rasgos y los límites de rasgos garantizan que,
Rust
jueves, 22 de mayo de 2025 : Página 276 de 719

aunque los tipos sean genéricos, tengan el comportamiento que el código


necesita. Aprendiste a usar anotaciones de duración de vida para garantizar que
este código flexible no tenga referencias pendientes. Y todo este análisis se
realiza en tiempo de compilación, lo que no afecta el rendimiento en tiempo de
ejecución.

Aunque parezca increíble, hay mucho más que aprender sobre los temas que
tratamos en este capítulo: el capítulo 18 trata sobre los objetos de rasgos, que
son otra forma de usar rasgos. También existen escenarios más complejos que
involucran anotaciones de duración que solo necesitará en casos muy avanzados;
para ello, le recomendamos leer la Referencia de Rust . A continuación, aprenderá
a escribir pruebas en Rust para asegurarse de que su código funcione
correctamente.

Escritura de pruebas automatizadas

En su ensayo de 1972, "El humilde programador", Edsger W. Dijkstra afirmó: "Las


pruebas de programas pueden ser una forma muy eficaz de mostrar la presencia
de errores, pero son totalmente inadecuadas para demostrar su ausencia". ¡Eso
no significa que no debamos intentar realizar pruebas tanto como sea posible!

La corrección de nuestros programas se refiere al grado en que nuestro código


cumple con nuestras expectativas. Rust está diseñado con gran preocupación por
la corrección de los programas, pero esta es compleja y difícil de demostrar. El
sistema de tipos de Rust asume gran parte de esta responsabilidad, pero no
puede abarcarlo todo. Por ello, Rust permite escribir pruebas de software
automatizadas.

Supongamos que escribimos una función add_twoque 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. Al implementar y compilar esa función, Rust
realiza todas las comprobaciones de tipos y de préstamos que has aprendido
hasta ahora para garantizar que, por ejemplo, no estemos pasando un Stringvalor
o una referencia no válida a esta función. Sin embargo, Rust no puede comprobar
que esta función hará exactamente lo que pretendemos, que es devolver el
parámetro + 2 en lugar de, por ejemplo, el parámetro + 10 o el parámetro - 50.
Ahí es donde entran en juego las pruebas.
Rust
jueves, 22 de mayo de 2025 : Página 277 de 719

Podemos escribir pruebas que aseguren, por ejemplo, que al pasar 3a


la add_twofunción, el valor devuelto es 5. Podemos ejecutar estas pruebas
siempre que modifiquemos nuestro código para asegurarnos de que el
comportamiento correcto existente no haya cambiado.

Las pruebas son una habilidad compleja: aunque no podemos cubrir en un solo
capítulo todos los detalles sobre cómo escribir buenas pruebas, en este capítulo
analizaremos la mecánica de las herramientas de prueba de Rust. Hablaremos
sobre las anotaciones y macros disponibles al escribir las pruebas, el
comportamiento predeterminado y las opciones para ejecutarlas, y cómo
organizar las pruebas en pruebas unitarias y de integración.

Cómo escribir pruebas

Las pruebas son funciones de Rust que verifican que el código no relacionado con
las pruebas funcione correctamente. Los cuerpos de las funciones de prueba
suelen realizar estas tres acciones:

 Configure cualquier dato o estado necesario.


 Ejecute el código que desea probar.
 Afirma que los resultados son los que esperas.

Veamos las características que Rust proporciona específicamente para escribir


pruebas que realizan estas acciones, que incluyen el testatributo, algunas macros
y el should_panicatributo.

La anatomía de una función de prueba

En su forma más simple, una prueba en Rust es una función anotada con
el test atributo. Los atributos son metadatos sobre fragmentos de código de Rust;
un ejemplo es el deriveatributo que usamos con estructuras en el Capítulo 5. Para
convertir una función en una función de prueba, añada #[test]la línea antes de fn.
Al ejecutar las pruebas con el cargo testcomando, Rust crea un binario del
ejecutor de pruebas que ejecuta las funciones anotadas e informa si cada función
de prueba es correcta o incorrecta.

Cada vez que creamos un nuevo proyecto de biblioteca con Cargo, se genera
automáticamente un módulo de pruebas con una función de prueba. Este módulo
te proporciona una plantilla para escribir tus pruebas, así no tienes que consultar
Rust
jueves, 22 de mayo de 2025 : Página 278 de 719

la estructura y la sintaxis exactas cada vez que inicias un nuevo proyecto.


¡Puedes agregar tantas funciones y módulos de prueba como quieras!

Exploraremos algunos aspectos del funcionamiento de las pruebas


experimentando con la prueba de plantilla antes de probar el código. Después,
escribiremos pruebas reales que invoquen código que hemos escrito y confirmen
su correcto funcionamiento.

Creemos un nuevo proyecto de biblioteca llamado adderque sumará dos


números:

$ cargo new adder --lib


Created library `adder` project
$ cd adder

El contenido del archivo src/lib.rs en su adderbiblioteca debería verse como el


Listado 11-1.

Nombre de archivo: src/lib.rs


pub fn add(left: u64, right: u64) -> u64 {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Listado 11-1: El código generado automáticamente porcargo new

El archivo comienza con una addfunción de ejemplo, para que tengamos algo que
probar.

Por ahora, centrémonos únicamente en la it_worksfunción. Observe


la #[test] anotación: este atributo indica que se trata de una función de prueba,
por lo que el ejecutor de pruebas sabe que debe tratarla como tal. También
podríamos tener funciones que no sean de prueba en el testsmódulo para ayudar
Rust
jueves, 22 de mayo de 2025 : Página 279 de 719

a configurar escenarios comunes o realizar operaciones comunes, por lo que


siempre debemos indicar qué funciones son de prueba.

El cuerpo de la función de ejemplo usa la assert_eq!macro para afirmar


que result, que contiene el resultado de llamar addcon 2 y 2, es igual a 4. Esta
afirmación sirve como ejemplo del formato de una prueba típica. Ejecutémosla
para comprobar si la prueba es correcta.

El cargo testcomando ejecuta todas las pruebas en nuestro proyecto, como se


muestra 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 (file:///projects/adder/target/debug/deps/adder-
7acb243c25ffd9dc)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s
Listado 11-2: El resultado de ejecutar la prueba generada automáticamente

Cargo compiló y ejecutó la prueba. Vemos la línea running 1 test. La siguiente


línea muestra el nombre de la función de prueba generada,
llamada tests::it_works, y el resultado de ejecutarla es ok. El resumen general test
result: ok.indica que todas las pruebas se aprobaron, y la parte que lee 1 passed;
0 failedsuma el número de pruebas aprobadas o fallidas.

Es posible marcar una prueba como ignorada para que no se ejecute en una
instancia específica; lo explicaremos en la sección "Ignorar algunas pruebas a
menos que se solicite específicamente" más adelante en este capítulo. Como no
lo hemos hecho aquí, el resumen muestra 0 ignored.
Rust
jueves, 22 de mayo de 2025 : Página 280 de 719

La 0 measuredestadística se utiliza para pruebas de rendimiento. Actualmente,


las pruebas de rendimiento solo están disponibles en Rust nocturno. Consulta la
documentación sobre pruebas de rendimiento para obtener más información.

Podemos pasar un argumento al cargo testcomando para ejecutar solo las


pruebas cuyo nombre coincida con una cadena; esto se denomina filtrado y lo
explicaremos en la sección "Ejecución de un subconjunto de pruebas por
nombre" . En este caso, no hemos filtrado las pruebas en ejecución, por lo que al
final del resumen se muestra 0 filtered out.

La siguiente parte de la salida de prueba, a partir de [nombre del archivo], Doc-


tests addercorresponde a los resultados de las pruebas de documentación. Aún
no disponemos de pruebas de documentación, pero Rust puede compilar
cualquier ejemplo de código que aparezca en la documentación de nuestra API.
Esta función ayuda a mantener la documentación y el código sincronizados.
Explicaremos cómo escribir pruebas de documentación en la
sección "Comentarios de documentación como pruebas" del Capítulo 14. Por
ahora, ignoraremos la Doc-testssalida.

Empecemos a personalizar la prueba según nuestras necesidades. Primero,


cambie el nombre de lait_works función por uno diferente, como exploration, así:

Nombre de archivo: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {


left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

Luego, ejecútelo cargo testde nuevo. El resultado


ahora explorationmuestra it_works:
Rust
jueves, 22 de mayo de 2025 : Página 281 de 719
$ 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

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

Ahora añadiremos otra prueba, pero esta vez crearemos una prueba que falla. Las
pruebas fallan cuando algo en la función de prueba entra en pánico. Cada prueba
se ejecuta en un nuevo hilo, y cuando el hilo principal detecta que un hilo de
prueba ha finalizado, la prueba se marca como fallida. En el capítulo 9,
explicamos cómo la forma más sencilla de entrar en pánico es llamar a la panic!
macro. Introduzca la nueva prueba como una función llamada another, de modo
que su archivo src/lib.rs se vea como el Listado 11-3.

Nombre de archivo: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {


left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}

#[test]
fn another() {
Rust
jueves, 22 de mayo de 2025 : Página 282 de 719
panic!("Make this test fail");
}
}
Listado 11-3: Agregar una segunda prueba que fallará porque llamamos a
lapanic! macro

Ejecute las pruebas de nuevo con cargo test. El resultado debería ser similar al
del Listado 11-4, que muestra si la explorationprueba fue exitosa o anotherfallida.

$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok

failures:

---- tests::another stdout ----


thread 'tests::another' panicked at src/lib.rs:17:9:
Make this test fail
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;


finished in 0.00s

error: test failed, to rerun pass `--lib`


Listado 11-4: Resultados de la prueba cuando una prueba pasa y la otra falla

En lugar de ok, la línea test tests::anothermuestra FAILED. Aparecen dos nuevas


secciones entre los resultados individuales y el resumen: la primera muestra el
motivo detallado de cada fallo de prueba. En este caso, obtenemos los detalles
de anotherpor qué falló panicked at 'Make this test fail'en la línea 17 del
archivo src/lib.rs . La siguiente sección enumera solo los nombres de todas las
pruebas fallidas, lo cual es útil cuando hay muchas pruebas y muchos resultados
detallados de las mismas. Podemos usar el nombre de una prueba fallida para
ejecutarla solo y depurarla más fácilmente; hablaremos más sobre cómo ejecutar
pruebas en el... sección "Controlar la ejecución de las pruebas" .
Rust
jueves, 22 de mayo de 2025 : Página 283 de 719

La línea de resumen se muestra al final: en general, el resultado de nuestra


prueba esFAILED . Tuvimos una prueba aprobada y otra reprobada.

Ahora que has visto cómo se ven los resultados de las pruebas en diferentes
escenarios, veamos algunas macros distintas a las anteriores.panic! son útiles en
las pruebas.

Comprobación de resultados con elassert! macro

La assert!macro, proporcionada por la biblioteca estándar, es útil para garantizar


que alguna condición de una prueba se evalúe como true. Le asignamos a
la assert!macro un argumento que se evalúa como un booleano. Si el valor
es true, no ocurre nada y la prueba se supera. Si el valor es false, la assert!macro
invoca panic!para que la prueba falle. Usar la assert! macro nos ayuda a
comprobar que nuestro código funciona correctamente.

En el Capítulo 5, Listado 5-15, usamos una Rectangleestructura y


un can_hold método, que se repiten aquí en el Listado 11-5. Coloquemos este
código en el archivo src/lib.rs y luego escribamos algunas pruebas con la assert!
macro.

Nombre de archivo: 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
}
}
Listado 11-5: La Rectangleestructura y su can_holdmétodo del Capítulo 5

El can_holdmétodo devuelve un valor booleano, lo que lo convierte en un caso de


uso ideal para la assert!macro. En el Listado 11-6, escribimos una prueba que
ejercita el can_holdmétodo creando una Rectangleinstancia con un ancho de 8 y
una altura de 7, y afirmando que puede contener otra Rectangleinstancia con un
ancho de 5 y una altura de 1.

Nombre de archivo: src/lib.rs


Rust
jueves, 22 de mayo de 2025 : Página 284 de 719
#[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));
}
}
Listado 11-6: Una prueba can_holdque verifica si un rectángulo más grande puede
realmente contener un rectángulo más pequeño

Observe la use super::*;línea dentro del testsmódulo. El testsmódulo es un


módulo normal que sigue las reglas de visibilidad habituales que vimos en el
Capítulo 7, en la sección "Rutas para hacer referencia a un elemento en el árbol
de módulos" . Dado que el testsmódulo es interno, necesitamos incluir el código
bajo prueba en el módulo externo dentro del alcance del módulo interno. Aquí
usamos un glob, por lo que cualquier definición que hagamos en el módulo
externo estará disponible para este testsmódulo.

Hemos llamado a nuestra prueba larger_can_hold_smallery creado las


dos Rectangleinstancias necesarias. Luego, llamamos a la assert!macro y le
pasamos el resultado de llamar a larger.can_hold(&smaller). Se supone que esta
expresión devuelve true, por lo que nuestra prueba debería pasar. ¡Vamos a
averiguarlo!

$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 1 test
test tests::larger_can_hold_smaller ... ok
Rust
jueves, 22 de mayo de 2025 : Página 285 de 719
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

¡Sí que funciona! Añadamos otra prueba, esta vez afirmando que un rectángulo
más pequeño no puede contener un rectángulo más grande:

Nombre de archivo: src/lib.rs

#[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));
}
}

Dado que el resultado correcto de la can_holdfunción en este caso es false,


debemos negarlo antes de pasarlo a la assert!macro. Por lo tanto, nuestra prueba
será correcta si can_holddevuelve false:

$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Rust
jueves, 22 de mayo de 2025 : Página 286 de 719
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

¡Dos pruebas que pasan! Ahora veamos qué sucede con los resultados de
nuestras pruebas cuando introducimos un error en el código. Modificaremos la
implementación del can_hold método reemplazando el signo "mayor que" por uno
"menor que" al comparar los anchos:

// --snip--
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width < other.width && self.height > other.height
}
}

La ejecución de las pruebas ahora produce lo siguiente:

$ 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:

---- tests::larger_can_hold_smaller stdout ----


Rust
jueves, 22 de mayo de 2025 : Página 287 de 719
thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
tests::larger_can_hold_smaller

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;


finished in 0.00s

error: test failed, to rerun pass `--lib`

¡Nuestras pruebas detectaron el error! Como larger.widthes 8y smaller.widthes 5,


la comparación de los anchos en can_holdahora devuelve false: 8 no es menor
que 5.

Prueba de igualdad con las macros assert_eq!yassert_ne!

Una forma común de verificar la funcionalidad es comprobar la igualdad entre el


resultado del código bajo prueba y el valor esperado. Esto se puede hacer usando
la assert!macro y pasándole una expresión mediante el ==operador. Sin
embargo, esta prueba es tan común que la biblioteca estándar proporciona un par
de macros ( assert_eq!y assert_ne!) para realizarla de forma más conveniente.
Estas macros comparan dos argumentos de igualdad o desigualdad,
respectivamente. También imprimen los dos valores si la aserción falla, lo que
facilita ver por qué falló la prueba; por el contrario, la assert!macro solo indica
que obtuvo un falsevalor para la == expresión, sin imprimir los valores que
llevaron a lafalse .

En el Listado 11-7, escribimos una función llamada add_twoque suma 2a su


parámetro, luego probamos esta función usando la assert_eq!macro.

Nombre de archivo: src/lib.rs


pub fn add_two(a: usize) -> usize {
a+2
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_adds_two() {
Rust
jueves, 22 de mayo de 2025 : Página 288 de 719
let result = add_two(2);
assert_eq!(result, 4);
}
}
Listado 11-7: Prueba de la función add_twousando la assert_eq!macro

¡Vamos a comprobar que pasa!

$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

Creamos una variable llamada resultque contiene el resultado de llamar


a add_two(2). Luego, pasamos resulty 4como argumentos a assert_eq!. La línea
de salida de esta prueba es test tests::it_adds_two ... ok, y el ok texto indica que
la prueba fue exitosa.

Introduzcamos un error en nuestro código para ver cómo assert_eq!se ve cuando


falla. Modifiquemos la implementación de la add_twofunción para agregar 3:

pub fn add_two(a: usize) -> usize {


a+3
}

Ejecute las pruebas nuevamente:

$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Rust
jueves, 22 de mayo de 2025 : Página 289 de 719
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... FAILED

failures:

---- tests::it_adds_two stdout ----


thread 'tests::it_adds_two' panicked at src/lib.rs:12:9:
assertion `left == right` failed
left: 5
right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
tests::it_adds_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;


finished in 0.00s

error: test failed, to rerun pass `--lib`

¡Nuestra prueba detectó el error! La it_adds_twoprueba falló y el mensaje nos


indica assertion `left == right` failedcuáles son los valores leftde y right. Este
mensaje nos ayuda a iniciar la depuración: el leftargumento, donde obtuvimos el
resultado de llamar a add_two(2), era , 5pero el rightargumento era 4. Como
puede imaginar, esto sería especialmente útil cuando tenemos muchas pruebas
en curso.

Tenga en cuenta que en algunos lenguajes y entornos de prueba, los parámetros


de las funciones de aserción de igualdad se llaman ` expectedy` actual, y el orden
en que se especifican los argumentos es importante. Sin embargo, en Rust, se
llaman ` lefty` right, y el orden en que se especifican el valor esperado y el valor
que el código produce no importa. Podríamos escribir la aserción en esta prueba
como ` assert_eq!(4, result), lo que generaría el mismo mensaje de error que
muestra assertion failed: `(left == right)``.

La assert_ne!macro se ejecutará correctamente si los dos valores que le


asignamos no son iguales y fallará si lo son. Esta macro es especialmente útil
cuando no estamos seguros de cuál será un valor , pero sabemos cuál no debería
ser. ser. Por ejemplo, si probamos una función que garantiza que cambiará su
entrada de alguna manera, pero la forma en que cambia depende del día de la
Rust
jueves, 22 de mayo de 2025 : Página 290 de 719

semana en que ejecutamos las pruebas, lo mejor sería afirmar que la salida de la
función no es igual a la entrada.

Bajo la superficie, las macros assert_eq!y assert_ne!usan los operadores ==y !=,
respectivamente. Cuando las aserciones fallan, estas macros imprimen sus
argumentos usando formato de depuración, lo que significa que los valores que se
comparan deben implementar los rasgos PartialEqy Debug. Todos los tipos
primitivos y la mayoría de los tipos de la biblioteca estándar implementan estos
rasgos. Para las estructuras y enumeraciones que defina usted mismo, deberá
implementar PartialEqpara afirmar la igualdad de esos tipos. También deberá
implementar Debugpara imprimir los valores cuando la aserción falla. Debido a
que ambos rasgos son rasgos derivables, como se menciona en el Listado 5-12
del Capítulo 5, esto suele ser tan sencillo como agregar la #[derive(PartialEq,
Debug)]anotación a su definición de estructura o enumeración. Consulte el
Apéndice C, “Rasgos derivables”, para obtener más detalles sobre estos y otros
rasgos derivables.

Agregar mensajes de error personalizados

También puede agregar un mensaje personalizado que se imprima junto con el


mensaje de error como argumentos opcionales a las macros assert!, assert_eq!
y assert_ne!. Cualquier argumento especificado después de los argumentos
obligatorios se pasa a la format!macro (discutido en el Capítulo 8, en la
sección "Concatenación con el + operador o la format! macro" ), para que pueda
pasar una cadena de formato que contenga {}marcadores de posición y valores
para esos marcadores. Los mensajes personalizados son útiles para documentar
el significado de una aserción; cuando una prueba falla, tendrá una mejor idea del
problema con el código.

Por ejemplo, digamos que tenemos una función que saluda a las personas por su
nombre y queremos probar que el nombre que pasamos a la función aparece en
la salida:

Nombre de archivo: src/lib.rs

pub fn greeting(name: &str) -> String {


format!("Hello {name}!")
}

#[cfg(test)]
Rust
jueves, 22 de mayo de 2025 : Página 291 de 719
mod tests {
use super::*;

#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}

Los requisitos de este programa aún no se han acordado, y estamos bastante


seguros de que el Hellotexto al principio del saludo cambiará. Decidimos que no
queremos tener que actualizar la prueba cuando cambien los requisitos, así que,
en lugar de comprobar la igualdad exacta con el valor devuelto por
la greetingfunción, simplemente comprobaremos que la salida contiene el texto
del parámetro de entrada.

Ahora introduzcamos un error en este código cambiando greetinga


excluir namepara ver cómo se ve la falla de prueba predeterminada:

pub fn greeting(name: &str) -> String {


String::from("Hello!")
}

La ejecución de esta prueba produce el siguiente resultado:

$ 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:

---- tests::greeting_contains_name stdout ----


thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
assertion failed: result.contains("Carol")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
Rust
jueves, 22 de mayo de 2025 : Página 292 de 719
tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;


finished in 0.00s

error: test failed, to rerun pass `--lib`

Este resultado solo indica que la aserción falló y en qué línea se encuentra. Un
mensaje de error más útil imprimiría el valor de la greetingfunción. Añadamos un
mensaje de error personalizado compuesto por una cadena de formato con un
marcador de posición con el valor real obtenido de la greetingfunción:

#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{result}`"
);
}

Ahora, cuando ejecutemos la prueba, obtendremos un mensaje de error más


informativo:

$ 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:

---- tests::greeting_contains_name stdout ----


thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
Greeting did not contain name, value was `Hello!`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;


finished in 0.00s
Rust
jueves, 22 de mayo de 2025 : Página 293 de 719
error: test failed, to rerun pass `--lib`

Podemos ver el valor que realmente obtuvimos en la salida de la prueba, lo que


nos ayudaría a depurar lo que sucedió en lugar de lo que esperábamos que
sucediera.

Comprobación de pánicos conshould_panic

Además de verificar los valores de retorno, es importante comprobar que nuestro


código gestiona las condiciones de error según lo previsto. Por ejemplo, considere
el Guesstipo que creamos en el Capítulo 9, Listado 9-13. Otro código que
utiliza Guess depende de la garantía de que Guesslas instancias contendrán solo
valores entre 1 y 100. Podemos escribir una prueba que garantice que intentar
crear una Guessinstancia con un valor fuera de ese rango genere un pánico.

Para ello, añadimos el atributo should_panica nuestra función de prueba. La


prueba se ejecuta correctamente si el código dentro de la función entra en
pánico; falla si el código dentro de la función no entra en pánico.

El listado 11-8 muestra una prueba que verifica que las condiciones de
error Guess::new ocurren cuando esperamos que sucedan.

Nombre de archivo: src/lib.rs


pub struct Guess {
value: i32,
}

impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}

Guess { value }
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic]
fn greater_than_100() {
Rust
jueves, 22 de mayo de 2025 : Página 294 de 719
Guess::new(200);
}
}
Listado 11-8: Prueba de que una condición causará unapanic!

Colocamos el #[should_panic]atributo después del #[test]atributo y antes de la


función de prueba a la que se aplica. Veamos el resultado cuando se supera la
prueba:

$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/guessing_game-
57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

Doc-tests guessing_game

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

¡Se ve bien! Ahora, introduzcamos un error en nuestro código eliminando la


condición de que la newfunción entre en pánico 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 }
}
}

Cuando ejecutamos la prueba del Listado 11-8, fallará:


Rust
jueves, 22 de mayo de 2025 : Página 295 de 719
$ 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:

---- tests::greater_than_100 stdout ----


note: test did not panic as expected

failures:
tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;


finished in 0.00s

error: test failed, to rerun pass `--lib`

En este caso, no recibimos un mensaje muy útil, pero al revisar la función de


prueba, vemos que está anotada con #[should_panic]. El error que obtuvimos
significa que el código de la función de prueba no causó un pánico.

Las pruebas que usan should_panicpueden ser imprecisas.


Una should_panicprueba se aprobaría incluso si entra en pánico por una razón
distinta a la esperada. Para que should_paniclas pruebas sean más precisas,
podemos añadir un expectedparámetro opcional al should_panicatributo. El arnés
de pruebas se asegurará de que el mensaje de error contenga el texto
proporcionado. Por ejemplo, considere el código modificado para Guessel Listado
11-9, donde la newfunción entra en pánico con diferentes mensajes según si el
valor es demasiado pequeño o demasiado grande.

Nombre de archivo: src/lib.rs


// --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 {
Rust
jueves, 22 de mayo de 2025 : Página 296 de 719
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
}

Guess { value }
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
Listado 11-9: Prueba de un panic!con un mensaje de pánico que contiene una
subcadena especificada

Esta prueba se aprobará porque el valor que asignamos al


parámetro should_panicdel atributo expectedes una subcadena del
mensaje Guess::new con el que la función entra en pánico. Podríamos haber
especificado el mensaje de pánico completo esperado, que en este caso
sería Guess value must be less than or equal to 100, got 200. Lo que especifique
dependerá de la singularidad o dinamismo del mensaje de pánico y de la
precisión deseada para la prueba. En este caso, una subcadena del mensaje de
pánico es suficiente para garantizar que el código de la función de prueba ejecute
elelse if value > 100 caso.

Para ver qué sucede cuando falla una should_panicprueba con


un expectedmensaje, introduzcamos nuevamente un error en nuestro código
intercambiando los cuerpos de if value < 1los else if value > 100bloques y :

if value < 1 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
Rust
jueves, 22 de mayo de 2025 : Página 297 de 719
);
}

Esta vez, cuando ejecutemos la should_panicprueba, fallará:

$ 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:

---- tests::greater_than_100 stdout ----


thread 'tests::greater_than_100' panicked at src/lib.rs:12:13:
Guess value must be greater than or equal to 1, got 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: `"Guess value must be greater than or equal to 1, got 200."`,
expected substring: `"less than or equal to 100"`

failures:
tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;


finished in 0.00s

error: test failed, to rerun pass `--lib`

El mensaje de error indica que esta prueba sí generó un error como esperábamos,
pero no incluyó la cadena esperada less than or equal to 100. El mensaje de error
que recibimos en este caso fue: « Guess value must be greater than or equal to 1,
got 200.¡Ahora podemos empezar a averiguar dónde está el error!».

Uso Result<T, E>en pruebas

Hasta ahora, todas nuestras pruebas entran en pánico al fallar. También podemos
escribir pruebas que usen Result<T, E>!. Aquí está la prueba del Listado 11-1,
reescrita para usar Result<T, E>y devolver un Erren lugar de entrar en pánico:

#[test]
fn it_works() -> Result<(), String> {
Rust
jueves, 22 de mayo de 2025 : Página 298 de 719
let result = add(2, 2);

if result == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}

La it_worksfunción ahora tiene el Result<(), String>tipo de retorno. En el cuerpo


de la función, en lugar de llamar a la assert_eq!macro, devolvemos Ok(())cuando
la prueba se supera y Errcon un valor Stringdentro cuando falla.

Escribir pruebas de modo que devuelvan un Result<T, E>le permite utilizar el


operador de signo de interrogación en el cuerpo de las pruebas, lo que puede ser
una forma conveniente de escribir pruebas que deberían fallar si alguna
operación dentro de ellas devuelve una Errvariante.

No se puede usar la #[should_panic]anotación en pruebas que usan Result<T,


E>. Para afirmar que una operación devuelve una Errvariante, no use el operador
de signo de interrogación en el Result<T, E>valor. En su lugar, use assert!
(value.is_err()).

Ahora que conoce varias formas de escribir pruebas, veamos qué sucede cuando
ejecutamos nuestras pruebas y exploremos las diferentes opciones que podemos
usar con cargo test.

Controlar cómo se ejecutan las pruebas

Al igual que cargo runcompila el código y luego ejecuta el binario


resultante, cargo testcompila el código en modo de prueba y ejecuta el binario de
prueba resultante. El comportamiento predeterminado del binario generado cargo
testes ejecutar todas las pruebas en paralelo y capturar la salida generada
durante las ejecuciones de prueba, lo que evita que se muestre y facilita la
lectura de la salida relacionada con los resultados de la prueba. Sin embargo,
puede especificar opciones de línea de comandos para cambiar este
comportamiento predeterminado.

Algunas opciones de la línea de comandos se ejecutan en [nombre del


archivo] cargo testy otras en el binario de prueba resultante. Para separar estos
dos tipos de argumentos, se listan los argumentos que se ejecutan en [nombre
Rust
jueves, 22 de mayo de 2025 : Página 299 de 719

del archivo] cargo testseguidos del separador --y luego los que se ejecutan en el
binario de prueba. "Ejecutar" cargo test --helpmuestra las opciones que se
pueden usar con [nombre del archivo cargo test] y "ejecutar" cargo test -- --
helpmuestra las opciones que se pueden usar después del separador. Estas
opciones también se documentan en la sección "Pruebas" del libro de rustc .

Ejecución de pruebas en paralelo o consecutivamente

Al ejecutar varias pruebas, por defecto, se ejecutan en paralelo mediante


subprocesos, lo que significa que terminan más rápido y se obtiene
retroalimentación con mayor rapidez. Dado que las pruebas se ejecutan
simultáneamente, debe asegurarse de que no dependan entre sí ni de ningún
estado compartido, incluido un entorno compartido, como el directorio de trabajo
actual o las variables de entorno.

Por ejemplo, supongamos que cada prueba ejecuta código que crea un archivo en
el disco llamado test-output.txt y escribe datos en él. Cada prueba lee los datos
de ese archivo y confirma que contiene un valor específico, que varía en cada
prueba. Dado que las pruebas se ejecutan simultáneamente, una podría
sobrescribir el archivo entre la escritura y la lectura de otra. La segunda prueba
fallará, no porque el código sea incorrecto, sino porque las pruebas han
interferido entre sí al ejecutarse en paralelo. Una solución es asegurarse de que
cada prueba escriba en un archivo diferente; otra solución es ejecutar las pruebas
una a la vez.

Si no desea ejecutar las pruebas en paralelo o si desea un control más preciso


sobre el número de subprocesos utilizados, puede enviar el --test-
threadsindicador y el número de subprocesos que desea usar al binario de
prueba. Observe el siguiente ejemplo:

$ cargo test -- --test-threads=1

Establecemos el número de subprocesos de prueba en 1, lo que indica al


programa que no utilice paralelismo. Ejecutar las pruebas con un solo subproceso
tardará más que ejecutarlas en paralelo, pero las pruebas no interferirán entre sí
si comparten estado.

Mostrar la salida de la función


Rust
jueves, 22 de mayo de 2025 : Página 300 de 719

Por defecto, si una prueba es exitosa, la biblioteca de pruebas de Rust captura


todo lo que se imprime en la salida estándar. Por ejemplo, si ejecutamos println!
una prueba y esta es exitosa, no veremos la println!salida en la terminal; solo
veremos la línea que indica que la prueba fue exitosa. Si una prueba falla,
veremos lo que se imprimió en la salida estándar junto con el resto del mensaje
de error.

A modo de ejemplo, el Listado 11-10 tiene una función tonta que imprime el valor
de su parámetro y devuelve 10, así como una prueba que pasa y una prueba que
falla.

Nombre de archivo: src/lib.rs

fn prints_and_returns_10(a: i32) -> i32 {


println!("I got the value {a}");
10
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(value, 10);
}

#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(value, 5);
}
}
Listado 11-10: Pruebas para una función que llamaprintln!

Cuando ejecutamos estas pruebas con cargo test, veremos el siguiente resultado:

$ cargo test
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/silly_function-
160869f38cff9166)
Rust
jueves, 22 de mayo de 2025 : Página 301 de 719
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

failures:

---- tests::this_test_will_fail stdout ----


I got the value 8
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;


finished in 0.00s

error: test failed, to rerun pass `--lib`

Tenga en cuenta que en esta salida no aparece I got the value 4, que se imprime
al ejecutarse la prueba que supera el resultado. Esta salida ya se ha capturado. La
salida de la prueba fallida, I got the value 8, aparece en la sección de la salida del
resumen de la prueba, que también muestra la causa del fallo.

Si también queremos ver los valores impresos de las pruebas aprobadas,


podemos indicarle a Rust que también muestre el resultado de las pruebas
exitosas con --show-output:

$ cargo test -- --show-output

Cuando ejecutamos nuevamente las pruebas del Listado 11-10 con la --show-
outputbandera, vemos el siguiente resultado:

$ cargo test -- --show-output


Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/silly_function-
160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
Rust
jueves, 22 de mayo de 2025 : Página 302 de 719
test tests::this_test_will_pass ... ok

successes:

---- tests::this_test_will_pass stdout ----


I got the value 4

successes:
tests::this_test_will_pass

failures:

---- tests::this_test_will_fail stdout ----


I got the value 8
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;


finished in 0.00s

error: test failed, to rerun pass `--lib`

Ejecución de un subconjunto de pruebas por nombre

A veces, ejecutar un conjunto de pruebas completo puede llevar mucho tiempo. Si


trabajas con código en un área específica, quizás quieras ejecutar solo las
pruebas correspondientes. Puedes elegir qué pruebas ejecutar pasando cargo
testel nombre o los nombres de las pruebas que quieres ejecutar como
argumento.

Para demostrar cómo ejecutar un subconjunto de pruebas, primero crearemos


tres pruebas para nuestra add_twofunción, como se muestra en el Listado 11-11,
y elegiremos cuáles ejecutar.

Nombre de archivo: src/lib.rs


pub fn add_two(a: usize) -> usize {
a+2
Rust
jueves, 22 de mayo de 2025 : Página 303 de 719
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn add_two_and_two() {
let result = add_two(2);
assert_eq!(result, 4);
}

#[test]
fn add_three_and_two() {
let result = add_two(3);
assert_eq!(result, 5);
}

#[test]
fn one_hundred() {
let result = add_two(100);
assert_eq!(result, 102);
}
}
Listado 11-11: Tres pruebas con tres nombres diferentes

Si ejecutamos las pruebas sin pasar ningún argumento, como vimos


anteriormente, todas las pruebas 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

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

Doc-tests adder

running 0 tests
Rust
jueves, 22 de mayo de 2025 : Página 304 de 719
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

Ejecución de pruebas individuales

Podemos pasar el nombre de cualquier función de prueba para cargo testejecutar


solo esa prueba:

$ cargo test one_hundred


Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::one_hundred ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in
0.00s

Solo se ejecutó la prueba con el nombre one_hundred; las otras dos no lo


coincidieron. El resultado de la prueba nos indica que hubo más pruebas que no
se ejecutaron, mostrándose 2 filtered outal final.

No podemos especificar los nombres de varias pruebas de esta manera;


solo cargo testse usará el primer valor proporcionado. Sin embargo, existe una
manera de ejecutar varias pruebas.

Filtrado para ejecutar múltiples pruebas

Podemos especificar parte del nombre de una prueba, y se ejecutará cualquier


prueba cuyo nombre coincida con ese valor. Por ejemplo, como dos de nuestras
pruebas contienen addel nombre "<nombre de prueba< cargo test add/ ...

$ cargo test add


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 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
Rust
jueves, 22 de mayo de 2025 : Página 305 de 719
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in
0.00s

Este comando ejecutó todas las pruebas con addel nombre y filtró la prueba
denominada one_hundred. Tenga en cuenta también que el módulo en el que
aparece una prueba se convierte en parte del nombre de la prueba, por lo que
podemos ejecutar todas las pruebas de un módulo filtrando por su nombre.

Ignorar algunas pruebas a menos que se soliciten específicamente

A veces, la ejecución de algunas pruebas específicas puede requerir mucho


tiempo, por lo que conviene excluirlas durante la mayoría de las ejecuciones
de cargo test. En lugar de incluir como argumentos todas las pruebas que se
desean ejecutar, se pueden anotar las pruebas que requieren mucho tiempo
utilizando el ignoreatributo para excluirlas, como se muestra aquí:

Nombre de archivo: src/lib.rs

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}

#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
}

Después #[test], añadimos la #[ignore]línea a la prueba que queremos excluir.


Ahora, al ejecutar nuestras pruebas, it_worksse ejecuta, pero expensive_testno:

$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
Rust
jueves, 22 de mayo de 2025 : Página 306 de 719
running 2 tests
test tests::expensive_test ... ignored
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in
0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

La expensive_testfunción se muestra como ignored. Si queremos ejecutar solo las


pruebas ignoradas, podemos usar cargo test -- --ignored:

$ cargo test -- --ignored


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 expensive_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in
0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in
0.00s

Al controlar qué pruebas se ejecutan, puede asegurarse de que sus cargo


testresultados se devuelvan rápidamente. Cuando necesite comprobar los
resultados de las ignoredpruebas y tenga tiempo para esperarlos, puede
ejecutarlos cargo test -- --ignored. Si desea ejecutar todas las pruebas,
independientemente de si se ignoran o no, puede ejecutar cargo test -- --include-
ignored.

Organización de pruebas
Rust
jueves, 22 de mayo de 2025 : Página 307 de 719

Como se mencionó al principio del capítulo, las pruebas son una disciplina
compleja, y cada persona utiliza una terminología y organización distintas. La
comunidad de Rust considera las pruebas en dos categorías principales: pruebas
unitarias y pruebas de integración. Las pruebas unitarias son pequeñas y más
específicas, prueban un módulo de forma aislada a la vez y pueden probar
interfaces privadas. Las pruebas de integración son completamente externas a la
biblioteca y utilizan el código de la misma manera que cualquier otro código
externo, utilizando solo la interfaz pública y potencialmente ejercitando varios
módulos por prueba.

Escribir ambos tipos de pruebas es importante para garantizar que las piezas de
su biblioteca hagan lo que usted espera, por separado y en conjunto.

Pruebas unitarias

El propósito de las pruebas unitarias es probar cada unidad de código de forma


aislada del resto para identificar rápidamente dónde funciona correctamente y
dónde no. Las pruebas unitarias se colocarán en el directorio src de cada archivo
con el código que se está probando. La convención es crear un módulo con
nombre tests en cada archivo para contener las funciones de prueba y anotarlo
con cfg(test).

El módulo de pruebas y#[cfg(test)]

La #[cfg(test)]anotación en el testsmódulo indica a Rust que compile y ejecute el


código de prueba solo cuando se ejecuta [<nombre de la biblioteca>] cargo test,
no cuando se ejecuta [< cargo buildnombre de la biblioteca>]. Esto ahorra tiempo
de compilación cuando solo se desea compilar la biblioteca y espacio en el
artefacto compilado resultante, ya que las pruebas no se incluyen. Verá que,
como las pruebas de integración se almacenan en un directorio diferente, no
necesitan la #[cfg(test)]anotación. Sin embargo, como las pruebas unitarias se
almacenan en los mismos archivos que el código, usará [<nombre de la
biblioteca>] #[cfg(test)]para especificar que no se incluyan en el resultado
compilado.

Recordemos que cuando generamos el nuevo adderproyecto en la primera


sección de este capítulo, Cargo generó este código para nosotros:

Nombre de archivo: src/lib.rs


Rust
jueves, 22 de mayo de 2025 : Página 308 de 719
pub fn add(left: u64, right: u64) -> u64 {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

En el testsmódulo generado automáticamente, el atributo cfgrepresenta la


configuración e indica a Rust que el siguiente elemento solo debe incluirse si se
cumple una opción de configuración específica. En este caso, la opción de
configuración es test, proporcionada por Rust para compilar y ejecutar pruebas. Al
usar este cfgatributo, Cargo compila nuestro código de prueba solo si ejecutamos
las pruebas activamente con cargo test. Esto incluye cualquier función auxiliar
que pueda estar dentro de este módulo, además de las funciones anotadas
con #[test].

Prueba de funciones privadas

Existe un debate en la comunidad de pruebas sobre si las funciones privadas


deben probarse directamente, y otros lenguajes dificultan o imposibilitan su
prueba. Independientemente de la ideología de pruebas que adoptes, las reglas
de privacidad de Rust permiten probar funciones privadas. Considera el código
del Listado 11-12 con la función privada internal_adder.

Nombre de archivo: src/lib.rs


pub fn add_two(a: usize) -> usize {
internal_adder(a, 2)
}

fn internal_adder(left: usize, right: usize) -> usize {


left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
Rust
jueves, 22 de mayo de 2025 : Página 309 de 719
fn internal() {
let result = internal_adder(2, 2);
assert_eq!(result, 4);
}
}
Listado 11-12: Prueba de una función privada

Tenga en cuenta que la internal_adderfunción no está marcada como pub. Las


pruebas son simplemente código de Rust, y el testsmódulo es simplemente otro
módulo. Como se explicó en la sección "Rutas para hacer referencia a un
elemento en el árbol de módulos" , los elementos de los módulos secundarios
pueden usar los elementos de sus módulos predecesores. En esta prueba,
incorporamos todos los testselementos del módulo principal al ámbito con use
super::*, y luego la prueba puede llamar a internal_adder. Si no cree que las
funciones privadas deban probarse, no hay nada en Rust que le obligue a hacerlo.

Pruebas de integración

En Rust, las pruebas de integración son completamente externas a la biblioteca.


Usan la biblioteca como cualquier otro código, lo que significa que solo pueden
llamar a funciones que forman parte de la API pública de la biblioteca. Su
propósito es comprobar si varias partes de la biblioteca funcionan correctamente
juntas. Las unidades de código que funcionan correctamente por sí solas podrían
tener problemas al integrarse, por lo que la cobertura de las pruebas del código
integrado también es importante. Para crear pruebas de integración, primero se
necesita un directorio de pruebas .

El directorio de pruebas

Creamos un directorio de pruebas en el nivel superior de nuestro directorio de


proyecto, junto a src . Cargo sabe que debe buscar los archivos de pruebas de
integración en este directorio. Podemos crear tantos archivos de prueba como
queramos, y Cargo compilará cada uno como un crate individual.

Creemos una prueba de integración. Con el código del Listado 11-12 aún en el
archivo src/lib.rs , cree un directorio llamado tests y un nuevo archivo
llamado tests/integration_test.rs . La estructura de su directorio debería ser
similar a la siguiente:

adder
├── Cargo.lock
├── Cargo.toml
Rust
jueves, 22 de mayo de 2025 : Página 310 de 719
├── src
│ └── lib.rs
└── tests
└── integration_test.rs

Ingrese el código del Listado 11-13 en el archivo tests/integration_test.rs .

Nombre del archivo: tests/integration_test.rs


use adder::add_two;

#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
Listado 11-13: Una prueba de integración de una función en el addercajón

Cada archivo del directorio de pruebas es un contenedor independiente, por lo


que debemos incluir nuestra biblioteca en el alcance de cada contenedor de
pruebas. Por ello, lo añadimos use adder::add_two;al principio del código, algo que
no necesitábamos en las pruebas unitarias.

No necesitamos anotar ningún código en tests/integration_test.rs con #[cfg(test)].


Cargo trata el directorio de pruebas de forma específica y compila los archivos en
este directorio solo cuando ejecutamos cargo test. Ejecutar cargo testahora:

$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Rust
jueves, 22 de mayo de 2025 : Página 311 de 719

Las tres secciones de salida incluyen las pruebas unitarias, la prueba de


integración y las pruebas de documentación. Tenga en cuenta que si alguna
prueba de una sección falla, las siguientes no se ejecutarán. Por ejemplo, si una
prueba unitaria falla, no se generará ninguna salida para las pruebas de
integración y documentación, ya que estas solo se ejecutarán si todas las pruebas
unitarias son correctas.

La primera sección para las pruebas unitarias es la misma que hemos estado
viendo: una línea para cada prueba unitaria (una nombrada internalque agregamos
en el Listado 11-12) y luego una línea de resumen para las pruebas unitarias.

La sección de pruebas de integración comienza con la línea Running


tests/integration_test.rs. A continuación, hay una línea para cada función de prueba
de esa prueba de integración y una línea de resumen con los resultados de la
prueba de integración justo antes Doc-tests adderdel inicio de la sección.

Cada archivo de prueba de integración tiene su propia sección, por lo que si


agregamos más archivos en el directorio de pruebas , habrá más secciones de
prueba de integración.

Aún podemos ejecutar una función de prueba de integración específica


especificando su nombre como argumento de cargo test`. Para ejecutar todas las
pruebas de un archivo de prueba de integración específico, use el --testargumento
` cargo test seguido del nombre del archivo:`

$ cargo test --test integration_test


Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Este comando solo ejecuta las pruebas en el archivo tests/integration_test.rs .

Submódulos en pruebas de integración

A medida que agregue más pruebas de integración, podría ser útil crear más
archivos en el directorio "tests" para organizarlos mejor; por ejemplo, puede
agrupar las funciones de prueba según la funcionalidad que estén probando.
Rust
jueves, 22 de mayo de 2025 : Página 312 de 719

Como se mencionó anteriormente, cada archivo del directorio "tests " se compila
como su propio crate, lo cual resulta útil para crear ámbitos independientes que
imiten con mayor precisión el uso que los usuarios finales le darán. Sin embargo,
esto significa que los archivos del directorio "tests" no se comportan igual que los
archivos del directorio "src ", como se explicó en el capítulo 7 sobre cómo separar
el código en módulos y archivos.

El comportamiento diferente de los archivos del directorio de pruebas se aprecia


con mayor claridad cuando se utiliza un conjunto de funciones auxiliares en varios
archivos de prueba de integración e intenta seguir los pasos de la
sección "Separación de módulos en archivos diferentes" del Capítulo 7 para
extraerlos en un módulo común. Por ejemplo, si creamos tests/common.rs y
colocamos una función con nombre setupen él, podemos añadir el código setupque
queramos llamar desde varias funciones de prueba en varios archivos de prueba:

Nombre del archivo: tests/common.rs

pub fn setup() {
// setup code specific to your library's tests would go here
}

Cuando volvamos a ejecutar las pruebas, veremos una nueva sección en la salida
de la prueba para el archivo common.rs , aunque este archivo no contiene
ninguna función de prueba ni llamamos a la setupfunción desde ningún lugar:

$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

running 1 test
test it_adds_two ... ok
Rust
jueves, 22 de mayo de 2025 : Página 313 de 719
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Que commonapareciera en los resultados de la prueba con running 0 tests"displayed


for" no era lo que buscábamos. Simplemente queríamos compartir código con los
demás archivos de prueba de integración. Para evitar que commonapareciera en la
salida de la prueba, 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

Esta es la convención de nomenclatura anterior que Rust también entiende y que


mencionamos en la sección "Rutas de archivo alternativas" del Capítulo 7.
Nombrar el archivo de esta manera indica a Rust que no debe tratar
el commonmódulo como un archivo de prueba de integración. Al mover
el setupcódigo de la función a tests/common/mod.rs y eliminar el
archivo tests/common.rs , la sección en la salida de prueba ya no aparecerá. Los
archivos en subdirectorios del directorio tests no se compilan como crates
independientes ni tienen secciones en la salida de prueba.

Tras crear tests/common/mod.rs , podemos usarlo como módulo desde cualquiera


de los archivos de prueba de integración. A continuación, se muestra un ejemplo
de cómo llamar a la setup función desde la it_adds_twoprueba
en tests/integration_test.rs :

Nombre del archivo: tests/integration_test.rs

use adder::add_two;

mod common;

#[test]
Rust
jueves, 22 de mayo de 2025 : Página 314 de 719
fn it_adds_two() {
common::setup();

let result = add_two(2);


assert_eq!(result, 4);
}

Tenga en cuenta que la mod common;declaración es la misma que la del módulo


que mostramos en el Listado 7-21. Luego, en la función de prueba, podemos
llamar a la common::setup()función.

Pruebas de integración para cajas binarias

Si nuestro proyecto es un contenedor binario que solo contiene el


archivo src/main.rs y no el archivo src/lib.rs , no podemos crear pruebas de
integración en el directorio de pruebas ni incorporar las funciones definidas en el
archivo src/main.rs al ámbito de aplicación mediante una useinstrucción. Solo los
contenedores de biblioteca exponen funciones que otros contenedores pueden
usar; los contenedores binarios están diseñados para ejecutarse de forma
independiente.

Esta es una de las razones por las que los proyectos de Rust que proporcionan un
binario tienen un archivo src/main.rs sencillo que invoca la lógica contenida en el
archivo src/lib.rs . Con esta estructura, las pruebas de integración pueden probar
la biblioteca usepara que la funcionalidad importante esté disponible. Si la
funcionalidad importante funciona, la pequeña cantidad de código en el
archivo src/main.rs también funcionará, y no es necesario probarla.

Resumen
Las funciones de prueba de Rust permiten especificar cómo debe funcionar el
código para garantizar que siga funcionando según lo previsto, incluso al realizar
cambios. Las pruebas unitarias evalúan las diferentes partes de una biblioteca por
separado y pueden probar detalles de implementación privados. Las pruebas de
integración comprueban que varias partes de la biblioteca funcionen
correctamente juntas y utilizan la API pública de la biblioteca para probar el
código de la misma forma que lo haría el código externo. Si bien el sistema de
tipos y las reglas de propiedad de Rust ayudan a prevenir algunos tipos de
errores, las pruebas siguen siendo importantes para reducir los errores lógicos
relacionados con el comportamiento esperado del código.
Rust
jueves, 22 de mayo de 2025 : Página 315 de 719

¡Combinemos los conocimientos que aprendiste en este capítulo y en los capítulos


anteriores para trabajar en un proyecto!

Un proyecto de E/S: creación de un


programa de línea de comandos
Este capítulo es un resumen de las habilidades que has aprendido hasta ahora y
una exploración de algunas funciones adicionales de la biblioteca estándar.
Desarrollaremos una herramienta de línea de comandos que interactúa con
archivos y la entrada/salida de la línea de comandos para practicar algunos de los
conceptos de Rust que ya dominas.

La velocidad, seguridad, salida binaria única y compatibilidad multiplataforma de


Rust lo convierten en un lenguaje ideal para crear herramientas de línea de
comandos. Por lo tanto, para nuestro proyecto, crearemos nuestra propia versión
de la herramienta clásica de búsqueda de línea de comandos grep ( buscar
globalmente una expresión regular e imprimir ) . En el caso de uso más
simple, busca una cadena específica en un archivo específico. Para ello, toma
como argumentos una ruta de archivo y una cadena. Luego, lee el archivo,
encuentra líneas que contienen el argumento de cadena e imprime esas
líneas.grepgrep

A lo largo del proceso, mostraremos cómo hacer que nuestra herramienta de línea
de comandos utilice las funciones de terminal que muchas otras herramientas de
línea de comandos utilizan. Leeremos el valor de una variable de entorno para
que el usuario pueda configurar el comportamiento de nuestra herramienta.
También imprimiremos los mensajes de error en el flujo de la consola de error
estándar ( stderr) en lugar de la salida estándar ( stdout) para que, por ejemplo, el
usuario pueda redirigir la salida correcta a un archivo sin dejar de ver los
mensajes de error en pantalla.

Un miembro de la comunidad Rust, Andrew Gallant, ya ha creado una versión


completa y muy rápida de grep, llamada ripgrep. En comparación, nuestra versión
será bastante simple, pero este capítulo te brindará los conocimientos básicos
necesarios para comprender un proyecto real como ripgrep.

Nuestro grepproyecto combinará una serie de conceptos que has aprendido hasta
ahora:
Rust
jueves, 22 de mayo de 2025 : Página 316 de 719

 Organización del código ( Capítulo 7 )


 Uso de vectores y cadenas ( Capítulo 8 )
 Manejo de errores ( Capítulo 9 )
 Uso de rasgos y vidas útiles cuando sea apropiado ( Capítulo 10 )
 Pruebas de escritura ( Capítulo 11 )

También presentaremos brevemente cierres, iteradores y objetos de rasgos,


que los capítulos 13 y 18 cubrirán en detalle.

Aceptar argumentos de la línea de comandos


Creemos un nuevo proyecto con, como siempre, cargo new. Lo llamaremos
así minigreppara distinguirlo de la grepherramienta que quizás ya tengas instalada
en tu sistema.

$ cargo new minigrep


Created binary (application) `minigrep` project
$ cd minigrep

La primera tarea es hacer que minigrepacepte sus dos argumentos de línea de


comandos: la ruta del archivo y una cadena de búsqueda. 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 de búsqueda y una ruta de archivo, como se muestra a continuación:

$ cargo run -- searchstring example-filename.txt

Actualmente, el programa generado por cargo newno 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 apenas estás aprendiendo este concepto, implementemos esta
capacidad nosotros mismos.

Leyendo los valores del argumento

Para poder minigrepleer los valores de los argumentos de la línea de comandos


que le pasamos, necesitaremos la std::env::argsfunción proporcionada en la
biblioteca estándar de Rust. Esta función devuelve un iterador con los argumentos
de la línea de comandos pasados a minigrep. Abordaremos los iteradores a fondo
en el Capítulo 13. Por ahora, solo necesita conocer dos detalles sobre los
iteradores: producen una serie de valores y podemos llamar al collectmétodo de
Rust
jueves, 22 de mayo de 2025 : Página 317 de 719

un iterador para convertirlo en una colección, como un vector, que contiene todos
los elementos que produce.

El código del Listado 12-1 permite que su minigrepprograma lea cualquier


argumento de la línea de comandos que se le pase y luego recopile los valores en
un vector.

Nombre de archivo: src/main.rs


use std::env;

fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
Listado 12-1: Recopilación de los argumentos de la línea de comandos en un vector e impresión de los
mismos

Primero, incorporamos el std::envmódulo al ámbito de aplicación mediante


una usedeclaración para poder usar su argsfunción. Observe que
la std::env::argsfunción está anidada en dos niveles de módulos. Como se explicó
en el Capítulo 7 , cuando la función deseada está anidada en más de un módulo,
hemos optado por incorporar el módulo principal al ámbito de aplicación en lugar
de la función. De esta manera, podemos usar fácilmente otras funciones de
[nombre del módulo] std::env. Además, es menos ambiguo que agregar use
std::env::argsy luego llamar a la función con [nombre del módulo] args, ya
que argspodría confundirse fácilmente con una función definida en el módulo
actual.

La argsfunción y el Unicode no válido

Tenga en cuenta que std::env::argsse generará un pánico si algún argumento contiene Unicode no
válido. Si su programa necesita aceptar argumentos que contienen Unicode no válido,
utilice std::env::args_os``. Esta función devuelve un iterador que produce OsStringvalores en lugar
de String``. Hemos optado por usar std::env::args`` para simplificar, ya que OsStringlos valores
varían según la plataforma y son más complejos de manejar que Stringlos valores.

En la primera línea de main, llamamos a env::args, y usamos


inmediatamente collectpara convertir el iterador en un vector que contiene todos
los valores producidos por él. Podemos usar la collectfunción para crear varios
tipos de colecciones, por lo que anotamos explícitamente el tipo de argspara
especificar que queremos un vector de cadenas. Aunque rara vez es necesario
Rust
jueves, 22 de mayo de 2025 : Página 318 de 719

anotar tipos en Rust, collectes una función que sí se necesita anotar a menudo, ya
que Rust no puede inferir el tipo de colección que se desea.

Finalmente, imprimimos el vector usando la macro de depuración. Intentemos


ejecutar el código primero sin argumentos y luego con dos:

$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/minigrep`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
]
$ cargo run -- needle haystack
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
Running `target/debug/minigrep needle haystack`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
"needle",
"haystack",
]

Observe que el primer valor del vector es "target/debug/minigrep", que corresponde


al nombre de nuestro binario. Esto coincide con el comportamiento de la lista de
argumentos en C, permitiendo que los programas usen el nombre con el que
fueron invocados durante su ejecución. Suele ser conveniente tener acceso al
nombre del programa para imprimirlo en mensajes o cambiar su comportamiento
según el alias de línea de comandos utilizado para invocarlo. Sin embargo, para
este capítulo, lo ignoraremos y guardaremos solo los dos argumentos necesarios.

Guardar los valores de los argumentos en las variables

El programa puede acceder a los valores especificados como argumentos de la


línea de comandos. Ahora necesitamos guardar los valores de ambos argumentos
en variables para poder usarlos en el resto del programa. Esto se hace en el
Listado 12-2.

Nombre de archivo: src/main.rs


use std::env;

fn main() {
let args: Vec<String> = env::args().collect();

let query = &args[1];


let file_path = &args[2];
Rust
jueves, 22 de mayo de 2025 : Página 319 de 719

println!("Searching for {query}");


println!("In file {file_path}");
}
Listado 12-2: Creación de variables para contener el argumento de consulta y el argumento de ruta de
archivo

Como vimos al imprimir el vector, el nombre del programa ocupa el primer valor
del vector en [<sub>(<sub>) args[0]; por lo tanto, los argumentos comienzan en
el índice 1. El primer argumento minigreptoma la cadena que buscamos, por lo que
incluimos una referencia al primer argumento en la variable
[<sub>(<sub> query). El segundo argumento será la ruta del archivo, por lo que
incluimos una referencia al segundo argumento en la variable
[<sub>(<sub>) file_path.

Imprimimos temporalmente los valores de estas variables para comprobar que el


código funciona correctamente. Ejecutemos este programa de nuevo con los
argumentos test y sample.txt:

$ cargo run -- test sample.txt


Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt

¡Genial, el programa funciona! Los valores de los argumentos que necesitamos se


guardan en las variables correctas. Más adelante añadiremos gestión de errores
para solucionar posibles situaciones erróneas, como cuando el usuario no
proporciona argumentos; por ahora, ignoraremos esa situación y, en su lugar,
trabajaremos en la incorporación de funciones de lectura de archivos.

Leyendo un archivo
Ahora añadiremos la funcionalidad para leer el archivo especificado en
el file_path argumento. Primero, necesitamos un archivo de ejemplo para probarlo:
usaremos un archivo con poco texto en varias líneas y algunas palabras
repetidas. El Listado 12-3 contiene un poema de Emily Dickinson que funcionará
bien. Crea un archivo llamado poem.txt en la raíz de tu proyecto e introduce el
poema "¡No soy nadie! ¿Quién eres tú?".

Nombre del archivo: poema.txt


I'm nobody! Who are you?
Rust
jueves, 22 de mayo de 2025 : Página 320 de 719
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!


How public, like a frog
To tell your name the livelong day
To an admiring bog!
Listado 12-3: Un poema de Emily Dickinson constituye un buen caso de prueba.

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.

Nombre de archivo: src/main.rs


use std::env;
use std::fs;

fn main() {
// --snip--
println!("In file {file_path}");

let contents = fs::read_to_string(file_path)


.expect("Should have been able to read the file");

println!("With text:\n{contents}");
}
Listado 12-4: Lectura del contenido del archivo especificado por el segundo argumento

Primero traemos una parte relevante de la biblioteca estándar con


una use declaración: necesitamos std::fsmanejar archivos.

En main, la nueva declaración fs::read_to_stringtoma file_path, abre ese archivo y


devuelve un valor del tipo std::io::Result<String>que contiene el contenido del
archivo.

Después de eso, agregamos nuevamente una println!declaración temporal que


imprime el valor contentsdespués de leer el archivo, para que podamos verificar
que el programa está funcionando hasta ahora.

Ejecutemos este código con cualquier cadena como primer argumento de la línea
de comando (porque aún no hemos implementado la parte de búsqueda) y el
archivo poem.txt como segundo argumento:

$ cargo run -- the poem.txt


Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep the poem.txt`
Rust
jueves, 22 de mayo de 2025 : Página 321 de 719
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!


How public, like a frog
To tell your name the livelong day
To an admiring bog!

¡Genial! El código leyó e imprimió el contenido del archivo. Sin embargo, presenta
algunas fallas. Actualmente, la mainfunción tiene múltiples responsabilidades:
generalmente, las funciones son más claras y fáciles de mantener si cada una se
encarga de una sola idea. El otro problema es que no estamos gestionando los
errores tan bien como podríamos. El programa aún es pequeño, así que estas
fallas no representan un gran problema, pero a medida que crezca, será más
difícil corregirlas correctamente. Es recomendable comenzar a refactorizar desde
el principio del desarrollo, ya que es mucho más fácil refactorizar pequeñas
cantidades de código. Lo haremos a continuación.

Refactorización para mejorar la modularidad y el


manejo de errores
Para mejorar nuestro programa, corregiremos cuatro problemas relacionados con
su estructura y su gestión de posibles errores. En primer lugar,
nuestra main función ahora realiza dos tareas: analiza argumentos y lee archivos.
A medida que nuestro programa crece, mainaumentará el número de tareas
independientes que gestiona. A medida que una función adquiere más
responsabilidades, se vuelve más difícil razonar sobre ella, probarla y modificarla
sin romper alguna de sus partes. Es mejor separar las funciones para que cada
función sea responsable de una tarea.

Este problema también se relaciona con el segundo: aunque queryy file_path son
variables de configuración de nuestro programa, las variables como contentsse
utilizan para ejecutar la lógica del programa. Cuanto más largo sea main sea el
programa, más variables necesitaremos incluir en el alcance; cuantas más
variables tengamos en el alcance, más difícil será comprender el propósito de
Rust
jueves, 22 de mayo de 2025 : Página 322 de 719

cada una. Es mejor agrupar las variables de configuración en una sola estructura
para aclarar su propósito.

El tercer problema es que solíamos expectimprimir un mensaje de error cuando


fallaba la lectura del archivo, pero este simplemente se imprimía Should have been
able to read the file. La lectura de un archivo puede fallar de varias maneras: por
ejemplo, podría faltar el archivo o no tener permiso para abrirlo. Actualmente,
independientemente de la situación, imprimiríamos el mismo mensaje de error
para todo, lo que no proporcionaría ninguna información al usuario.

En cuarto lugar, usamos expectpara gestionar un error, y si el usuario ejecuta


nuestro programa sin especificar suficientes argumentos, recibirá un index out of
boundserror de Rust que no explica claramente el problema. Sería mejor que todo
el código de gestión de errores estuviera en un solo lugar para que los futuros
mantenedores tuvieran un único lugar donde consultar el código si fuera
necesario cambiar la lógica de gestión de errores. Tener todo el código de gestión
de errores en un solo lugar también garantizará que imprimamos mensajes
significativos para nuestros usuarios finales.

Abordemos estos cuatro problemas refactorizando nuestro proyecto.

Separación de preocupaciones para proyectos binarios

El problema organizativo de asignar la responsabilidad de múltiples tareas a


la mainfunción es común en muchos proyectos binarios. Por ello, la comunidad de
Rust ha desarrollado directrices para dividir las distintas tareas de un programa
binario cuando mainempieza a crecer. Este proceso consta de los siguientes
pasos:

 Divida su programa en un archivo main.rs y un archivo lib.rs y mueva la


lógica de su programa a lib.rs.
 Mientras la lógica de análisis de la línea de comandos sea pequeña, puede
permanecer en main.rs.
 Cuando la lógica de análisis de la línea de comandos comience a
complicarse, extráigala de main.rs y muévala a lib.rs.

Las responsabilidades que quedan en la mainfunción después de este proceso


deben limitarse a lo siguiente:
Rust
jueves, 22 de mayo de 2025 : Página 323 de 719

 Llamar a la lógica de análisis de la línea de comandos con los valores de los


argumentos
 Configurar cualquier otra configuración
 Llamar a una runfunción en lib.rs
 Manejo del error si runse devuelve un error

Este patrón se centra en la separación de tareas: main.rs gestiona la ejecución del


programa y lib.rs gestiona toda la lógica de la tarea en cuestión. Dado que no se
puede probar la mainfunción directamente, esta estructura permite probar toda la
lógica del programa moviéndola a las funciones de lib.rs. El código restante
en main.rs será lo suficientemente pequeño como para verificar su corrección
mediante su lectura. Reelaboremos nuestro programa siguiendo este proceso.

Extracción del analizador de argumentos

Extraeremos la funcionalidad para analizar argumentos en una función


que mainllamará para preparar el traslado de la lógica de análisis de la línea de
comandos a src/lib.rs . El Listado 12-5 muestra el nuevo inicio de mainque llama a
una nueva función parse_config, que definiremos en src/main.rs por ahora.

Nombre de archivo: src/main.rs


fn main() {
let args: Vec<String> = env::args().collect();

let (query, file_path) = parse_config(&args);

// --snip--
}

fn parse_config(args: &[String]) -> (&str, &str) {


let query = &args[1];
let file_path = &args[2];

(query, file_path)
}
Listado 12-5: Extracción de una parse_configfunción demain

Seguimos recopilando los argumentos de la línea de comandos en un vector, pero


en lugar de asignar el valor del argumento en el índice 1 a la variable queryy el
valor del argumento en el índice 2 a la variable file_pathdentro de la main función,
pasamos el vector completo a la parse_configfunción. La parse_configfunción
entonces contiene la lógica que determina qué argumento va en qué variable y
devuelve los valores a main. Seguimos creando las variables queryy en , pero ya no
Rust
jueves, 22 de mayo de 2025 : Página 324 de 719

tenemos la responsabilidad de determinar cómo se corresponden los argumentos


de la línea de comandos y las variables.file_pathmainmain

Esta modificación puede parecer excesiva para nuestro pequeño programa, pero
la estamos refactorizando poco a poco. Después de realizar este cambio, vuelva a
ejecutar el programa para verificar que el análisis de argumentos siga
funcionando. Es recomendable revisar el progreso con frecuencia para identificar
la causa de los problemas cuando ocurran.

Agrupación de valores de configuración

Podemos dar un pequeño paso parse_configmás para mejorar la función.


Actualmente, devolvemos una tupla, pero inmediatamente la descomponemos en
partes individuales. Esto indica que quizás aún no tenemos la abstracción
correcta.

Otro indicador que muestra que hay margen de mejora es la configparte


de parse_config, que implica que los dos valores que devolvemos están
relacionados y forman parte de un mismo valor de configuración. Actualmente, no
transmitimos este significado en la estructura de los datos, salvo agrupando los
dos valores en una tupla; en su lugar, los colocaremos en una estructura y
asignaremos a cada uno de sus campos un nombre significativo. De esta forma,
los futuros desarrolladores de este código comprenderán mejor cómo se
relacionan los diferentes valores y cuál es su propósito.

El listado 12-6 muestra las mejoras en la parse_configfunción.

Nombre de archivo: src/main.rs


fn main() {
let args: Vec<String> = env::args().collect();

let config = parse_config(&args);

println!("Searching for {}", config.query);


println!("In file {}", config.file_path);

let contents = fs::read_to_string(config.file_path)


.expect("Should have been able to read the file");

// --snip--
}

struct Config {
query: String,
file_path: String,
Rust
jueves, 22 de mayo de 2025 : Página 325 de 719
}

fn parse_config(args: &[String]) -> Config {


let query = args[1].clone();
let file_path = args[2].clone();

Config { query, file_path }


}
Listado 12-6: Refactorización parse_configpara devolver una instancia de una Configestructura

Hemos añadido una estructura llamada Configdefinida para contener los campos
llamados queryy file_path. La firma de parse_configahora indica que devuelve
un Configvalor. En el cuerpo de parse_config, donde antes devolvíamos fragmentos
de cadena que referenciaban Stringvalores en args, ahora la definimos Config para
que contenga Stringvalores propios. La argsvariable en maines la propietaria de los
valores de los argumentos y solo permite que la parse_configfunción los tome
prestados, lo que significa que violaríamos las reglas de préstamo de Rust
si Configintentáramos tomar posesión de los valores en . args .

Hay varias maneras de gestionar los Stringdatos; la más sencilla, aunque algo
ineficiente, es llamar al clonemétodo en los valores. Esto creará una copia
completa de los datos para la Configinstancia, lo que requiere más tiempo y
memoria que almacenar una referencia a los datos de la cadena. Sin embargo,
clonar los datos también simplifica mucho nuestro código, ya que no tenemos que
gestionar la duración de las referencias; en este caso, sacrificar algo de
rendimiento para ganar simplicidad es una buena compensación.

Las desventajas del usoclone

Muchos rustáceos tienden a evitar usar clonepara solucionar problemas de propiedad debido a su costo
en tiempo de ejecución. En el Capítulo 13 , aprenderás a usar métodos más eficientes en este tipo de
situaciones. Pero por ahora, puedes copiar algunas cadenas para seguir progresando, ya que solo las
harás una vez y la ruta de archivo y la cadena de consulta son muy pequeñas. Es mejor tener un
programa funcional que sea un poco ineficiente que intentar hiperoptimizar el código en la primera
pasada. A medida que adquieras más experiencia con Rust, será más fácil comenzar con la solución
más eficiente, pero por ahora, es perfectamente aceptable llamar a clone.

Lo hemos actualizado mainpara que coloque la instancia de Configdevuelto


por parse_configen una variable llamada config, y actualizamos el código que
anteriormente utilizaba las variables queryy separadas file_pathpara que ahora
utilice los campos en la Configestructura en su lugar.
Rust
jueves, 22 de mayo de 2025 : Página 326 de 719

Ahora nuestro código transmite con mayor claridad que " queryy" file_pathestán
relacionados y que su propósito es configurar el funcionamiento del programa.
Cualquier código que utilice estos valores sabe cómo encontrarlos en
la configinstancia, en los campos designados para su propósito.

Creando un constructor paraConfig

Hasta ahora, hemos extraído la lógica responsable de analizar los argumentos de


la línea de comandos mainy la hemos incluido en la parse_configfunción. Esto nos
ayudó a ver que los valores " queryy file_path" estaban relacionados, y que dicha
relación debería reflejarse en nuestro código. Luego, añadimos
una Configestructura para indicar el propósito relacionado de query"y file_path" y
para poder devolver los nombres de los valores como nombres de campo de la
estructura desde la parse_configfunción.

Ahora que el propósito de la parse_configfunción es crear una Config instancia,


podemos cambiar parse_configde una función simple a una función con
nombre newasociada a la Configestructura. Este cambio hará que el código sea
más idiomático. Podemos crear instancias de tipos en la biblioteca estándar,
como String, llamando a String::new. De igual forma, al cambiar parse_configa
una newfunción asociada a Config, podremos crear instancias de Configllamando
a Config::new. El Listado 12-7 muestra los cambios necesarios.

Nombre de archivo: src/main.rs


fn main() {
let args: Vec<String> = env::args().collect();

let config = Config::new(&args);

// --snip--
}

// --snip--

impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();

Config { query, file_path }


}
}
Listado 12-7: Transformando parse_configenConfig::new
Rust
jueves, 22 de mayo de 2025 : Página 327 de 719

Hemos actualizado mainla ubicación donde llamábamos parse_configa `to` en lugar


de `call` Config::new. Hemos cambiado el nombre de parse_config`to` newy lo
hemos movido dentro de un implbloque, lo que asocia la newfunción con Config`.
Intenta compilar este código de nuevo para asegurarte de que funciona.

Corrección del manejo de errores

Ahora trabajaremos en la corrección de la gestión de errores. Recuerde que


intentar acceder a los valores del argsvector en el índice 1 o 2 provocará un
pánico en el programa si el vector contiene menos de tres elementos. Intente
ejecutar el programa sin argumentos; se verá así:

$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Esta línea index out of bounds: the len is 1 but the index is 1 es un mensaje de error
dirigido a programadores. No ayudará a nuestros usuarios finales a entender qué
deben hacer. Vamos a solucionarlo ahora.

Mejorando el mensaje de error

En el Listado 12-8, agregamos una verificación en la newfunción que verificará


que la porción sea lo suficientemente larga antes de acceder al índice 1 y al
índice 2. Si la porción no es lo suficientemente larga, el programa entra en pánico
y muestra un mensaje de error mejor.

Nombre de archivo: src/main.rs


// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("not enough arguments");
}
// --snip--
Listado 12-8: Adición de una verificación para el número de argumentos

Este código es similar a la Guess::newfunción que escribimos en el Listado 9-13 ,


donde la llamamos panic!cuando el valueargumento estaba fuera del rango de
valores válidos. En lugar de verificar un rango de valores, verificamos que la
longitud de argssea al menos 3y que el resto de la función pueda operar bajo el
Rust
jueves, 22 de mayo de 2025 : Página 328 de 719

supuesto de que se cumple esta condición. Si argstiene menos de tres elementos,


esta condición será truey llamamos a la panic!macro para finalizar el programa
inmediatamente.

Con estas pocas líneas de código adicionales new, ejecutemos nuevamente el


programa sin ningún argumento 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

Esta salida es mejor: ahora tenemos un mensaje de error razonable. Sin embargo,
también tenemos información superflua que no queremos compartir con nuestros
usuarios. Quizás la técnica que usamos en el Listado 9-13 no sea la más
adecuada: una llamada a panic!es más apropiada para un problema de
programación que para uno de uso, como se explicó en el Capítulo 9. En su lugar,
usaremos la otra técnica que aprendiste en el Capítulo 9: devolver un Result que
indica éxito o error.

Devolver un Resulten lugar de llamarpanic!

En su lugar, podemos devolver un Resultvalor que contenga una Configinstancia en


el caso correcto y describa el problema en el caso de error. También
cambiaremos el nombre de la función de " newto" buildporque muchos
programadores esperan que newlas funciones nunca fallen. Cuando Config::buildse
comunica con "to main", podemos usar el Resulttipo para indicar que hubo un
problema. Luego, podemos cambiar mainpara convertir una Errvariante en un error
más práctico para nuestros usuarios sin el texto circundante "about" thread
'main'y RUST_BACKTRACEque causa una llamada a panic!".

El listado 12-9 muestra los cambios que debemos realizar en el valor de retorno
de la función que estamos llamando Config::buildy en el cuerpo de la función
necesario para devolver un Result. Tenga en cuenta que esto no se compilará
hasta que actualicemos maintambién, lo cual haremos en el siguiente listado.

Nombre de archivo: src/main.rs

impl Config {
Rust
jueves, 22 de mayo de 2025 : Página 329 de 719
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}

let query = args[1].clone();


let file_path = args[2].clone();

Ok(Config { query, file_path })


}
}
Listado 12-9: Devolución de un ResultdesdeConfig::build

Nuestra buildfunción devuelve una Resultinstancia Configen caso de éxito y una


cadena literal en caso de error. Nuestros valores de error siempre serán cadenas
literales con duración 'static.

Hemos realizado dos cambios en el cuerpo de la función: en lugar de


llamar panic! cuando el usuario no pasa suficientes argumentos, ahora
devolvemos un Errvalor y lo hemos encapsulado Configen un Ok. Estos cambios
hacen que la función se ajuste a su nueva firma de tipo.

Devolver un Errvalor Config::buildpermite que la mainfunción maneje el Resultvalor


devuelto por la buildfunción y salga del proceso de forma más limpia en caso de
error.

Errores de llamada Config::buildy manejo

Para gestionar el caso de error e imprimir un mensaje intuitivo, necesitamos


actualizar mainpara gestionar el Resultretorno de Config::build, como se muestra en
el Listado 12-10. También nos encargaremos de salir de la herramienta de línea
de comandos con un código de error distinto de cero panic!y, en su lugar, lo
implementaremos manualmente. Un estado de salida distinto de cero es una
convención para indicar al proceso que llamó a nuestro programa que este salió
con un estado de error.

Nombre de archivo: src/main.rs


use std::process;

fn main() {
let args: Vec<String> = env::args().collect();

let config = Config::build(&args).unwrap_or_else(|err| {


println!("Problem parsing arguments: {err}");
process::exit(1);
});
Rust
jueves, 22 de mayo de 2025 : Página 330 de 719

// --snip--
Listado 12-10: Salir con un código de error si Configfalla la construcción

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_elsenos permite definir algún panic!manejo personalizado, sin
errores. Si Resultes un Okvalor, el comportamiento de este método es similar
a unwrap: devuelve el valor interno que Okestá envolviendo. Sin embargo, si el
valor es un Errvalor, este método llama al código en el cierre , que es una función
anónima que definimos y pasamos como argumento a unwrap_or_else. Cubriremos
los cierres con más detalle en el Capítulo 13 . Por ahora, solo necesita saber
que unwrap_or_elsepasará el valor interno de Err, que en este caso es la cadena
estática "not enough arguments" que agregamos en el Listado 12-9, a nuestro cierre
en el argumento errque aparece entre las barras verticales. El código en el cierre
puede entonces usar el errvalor cuando se ejecuta.

Hemos añadido una nueva uselínea para importar processdesde la biblioteca


estándar al ámbito. El código del cierre que se ejecutará en caso de error consta
de solo dos líneas: imprimimos el errvalor y luego llamamos a process::exit.
La process::exitfunción detendrá el programa inmediatamente y devolverá el
número que se pasó como código de estado de salida. Esto es similar al panic!
manejo basado en que usamos en el Listado 12-8, pero ya no obtenemos toda la
salida adicional. 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

¡Genial! Esta salida es mucho más intuitiva para nuestros usuarios.

Extrayendo lógica demain

Ahora que hemos terminado de refactorizar el análisis de la configuración,


pasemos a la lógica del programa. Como se indicó en "Separación de
preocupaciones para proyectos binarios" , extraeremos una función
llamada runque contendrá toda la lógica actual de la mainfunción que no esté
relacionada con la configuración ni con la gestión de errores. Al terminar, mainserá
Rust
jueves, 22 de mayo de 2025 : Página 331 de 719

concisa y fácil de verificar mediante inspección, y podremos escribir pruebas para


el resto de la lógica.

El Listado 12-11 muestra la runfunción extraída. Por ahora, solo estamos


realizando una pequeña mejora incremental: la extracción de la función.
Seguimos definiéndola en src/main.rs .

Nombre de archivo: src/main.rs


fn main() {
// --snip--

println!("Searching for {}", config.query);


println!("In file {}", config.file_path);

run(config);
}

fn run(config: Config) {
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");

println!("With text:\n{contents}");
}

// --snip--
Listado 12-11: Extracción de una runfunción que contiene el resto de la lógica del programa

La runfunción ahora contiene toda la lógica restante de main, comenzando por leer
el archivo. La runfunción toma la Configinstancia como argumento.

Devolución de errores de la runfunción

Con la lógica restante del programa separada en la runfunción, podemos mejorar


la gestión de errores, como hicimos Config::builden el Listado 12-9. En lugar de
permitir que el programa entre en pánico al llamar a expect, la run función
devolverá un Result<T, E>cuando algo salga mal. Esto nos permitirá consolidar aún
más la lógica de gestión de errores en mainde una manera intuitiva. El Listado 12-
12 muestra los cambios que debemos realizar en la firma y el cuerpo de run.

Nombre de archivo: src/main.rs


use std::error::Error;

// --snip--

fn run(config: Config) -> Result<(), Box<dyn Error>> {


let contents = fs::read_to_string(config.file_path)?;
Rust
jueves, 22 de mayo de 2025 : Página 332 de 719
println!("With text:\n{contents}");

Ok(())
}
Listado 12-12: Cambiar la runfunción a devolverResult

Hemos realizado tres cambios significativos. Primero, cambiamos el tipo de


retorno de la runfunción a Result<(), Box<dyn Error>>. Anteriormente, esta función
devolvía el tipo de unidad, (), y lo mantenemos como el valor devuelto en
el Okcaso.

Para el tipo de error, usamos el objeto trait Box<dyn Error> (y lo


incorporamos std::error::Erroral ámbito mediante una usedeclaración al principio).
Abordaremos los objetos trait en el Capítulo 18. Por ahora, solo tenga en cuenta
que esto Box<dyn Error>significa que la función devolverá un tipo que implementa
el Errortrait, pero no es necesario especificar el tipo específico del valor de
retorno. Esto nos da flexibilidad para devolver valores de error que pueden ser de
diferentes tipos en distintos casos de error. La dynpalabra clave es la abreviatura
de dynamic .

En segundo lugar, hemos eliminado la llamada a expecten favor del ?operador,


como se explicó en el Capítulo 9. En lugar de, panic!en caso de error, ?devolverá el
valor de error de la función actual para que quien lo llama lo gestione.

En tercer lugar, la runfunción ahora devuelve un Okvalor en caso de éxito. Hemos


declarado el runtipo de éxito de la función como ()en la firma, lo que significa que
debemos incluir el valor del tipo de unidad en el Okvalor. Esta Ok(())sintaxis puede
parecer un poco extraña al principio, pero usarla ()así es la forma idiomática de
indicar que solo estamos llamando runa sus efectos secundarios; no devuelve el
valor necesario.

Cuando ejecute este código, se compilará pero mostrará una advertencia:

$ cargo run -- the poem.txt


Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
--> src/main.rs:19:5
|
19 | run(config);
| ^^^^^^^^^^^
|
= 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
Rust
jueves, 22 de mayo de 2025 : Página 333 de 719
|
19 | let _ = run(config);
| +++++++

warning: `minigrep` (bin "minigrep") generated 1 warning


Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!


How public, like a frog
To tell your name the livelong day
To an admiring bog!

Rust nos indica que nuestro código ignoró el Resultvalor, lo Resultcual podría
indicar un error. Sin embargo, no estamos comprobando si hubo un error, y el
compilador nos recuerda que probablemente pretendíamos tener código de
gestión de errores. Corrijamos ese problema ahora.

Manejo de errores devueltos rundesdemain

Comprobaremos si hay errores y los solucionaremos utilizando una técnica similar


a la que usamos Config::builden el Listado 12-10, pero con una ligera diferencia:

Nombre de archivo: src/main.rs

fn main() {
// --snip--

println!("Searching for {}", config.query);


println!("In file {}", config.file_path);

if let Err(e) = run(config) {


println!("Application error: {e}");
process::exit(1);
}
}

Usamos ` if leten lugar de` unwrap_or_elsepara comprobar si rundevuelve


un Errvalor y, process::exit(1)en caso afirmativo, para llamar. La runfunción no
devuelve el valor deseado unwrapde la misma forma que Config::builddevuelve
Rust
jueves, 22 de mayo de 2025 : Página 334 de 719

la Configinstancia. Dado que run`devuelve` ()en caso de éxito, solo nos importa
detectar un error, por lo que no necesitamos unwrap_or_elsedevolver el valor sin
encapsular, que solo sería `` ().

Los cuerpos de if letlas funciones y unwrap_or_elseson los mismos en ambos casos:


imprimimos el error y salimos.

Dividir el código en una biblioteca

¡ Nuestro minigrepproyecto pinta bien hasta ahora! Ahora dividiremos el


archivo src/main.rs y añadiremos código al archivo src/lib.rs . Así, podremos
probar el código y tener un archivo src/main.rs con menos responsabilidades.

Muevamos todo el código que no está en la mainfunción


de src/main.rs a src/lib.rs :

 La rundefinición de la función
 Las usedeclaraciones pertinentes
 La definición deConfig
 La Config::builddefinición de la función

El contenido de src/lib.rs debe tener las firmas que se muestran en el Listado 12-
13 (para mayor brevedad, hemos omitido los cuerpos de las funciones). Tenga en
cuenta que esto no se compilará hasta que modifiquemos src/main.rs en el
Listado 12-14.

Nombre de archivo: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {


pub query: String,
pub file_path: String,
}

impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
// --snip--
}
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {


// --snip--
Rust
jueves, 22 de mayo de 2025 : Página 335 de 719
}
Listado 12-13: Mover Configy runentrar en src/lib.rs

Hemos usado abundantemente la pubpalabra clave: on Config, en sus campos,


su buildmétodo y la runfunción. ¡Ahora tenemos una biblioteca con una API pública
que podemos probar!

Ahora necesitamos llevar el código que trasladamos a src/lib.rs al alcance del


paquete binario en src/main.rs , como se muestra en el Listado 12-14.

Nombre de archivo: src/main.rs


use std::env;
use std::process;

use minigrep::Config;

fn main() {
// --snip--
if let Err(e) = minigrep::run(config) {
// --snip--
}
}
Listado 12-14: Uso de la minigrepbiblioteca crate en src/main.rs

Añadimos una use minigrep::Configlínea para transferir el Configtipo del crate de la


biblioteca al ámbito del crate binario y prefijamos la runfunción con el nombre del
crate. Ahora, toda la funcionalidad debería estar conectada y funcionar
correctamente. Ejecuta el programa con cargo runy asegúrate de que todo
funciona correctamente.

¡Uf! Fue mucho trabajo, pero nos hemos preparado para el éxito en el futuro.
Ahora es mucho más fácil gestionar los errores y hemos hecho el código más
modular. De ahora en adelante, casi todo nuestro trabajo se realizará
en src/lib.rs .

Aprovechemos esta nueva modularidad haciendo algo que hubiera sido difícil con
el código antiguo pero que es fácil con el nuevo: ¡escribiremos algunas pruebas!

Desarrollo de la funcionalidad de la biblioteca


mediante desarrollo basado en pruebas
Ahora que hemos extraído la lógica a src/lib.rs y dejado la recopilación de
argumentos y la gestión de errores en src/main.rs , es mucho más fácil escribir
Rust
jueves, 22 de mayo de 2025 : Página 336 de 719

pruebas para la funcionalidad principal de nuestro código. Podemos llamar a


funciones directamente con varios argumentos y comprobar los valores de
retorno sin tener que llamar a nuestro binario desde la línea de comandos.

En esta sección, agregaremos la lógica de búsqueda al minigrepprograma


utilizando el proceso de desarrollo impulsado por pruebas (TDD) con los
siguientes pasos:

1. Escriba una prueba que falle y ejecútela para asegurarse de que falle por el
motivo esperado.
2. Escriba o modifique el código suficiente para que la nueva prueba pase.
3. Refactorice el código que acaba de agregar o cambiar y asegúrese de que
las pruebas sigan pasando.
4. ¡Repita desde el paso 1!

Aunque es solo una de las muchas maneras de escribir software, TDD puede
ayudar a impulsar el diseño de código. Escribir la prueba antes de escribir el
código que la aprueba ayuda a mantener una alta cobertura de pruebas durante
todo el proceso.

Probaremos la implementación de la funcionalidad que buscará la cadena de


consulta en el contenido del archivo y generará una lista de líneas que coincidan
con la consulta. Añadiremos esta funcionalidad en una función llamada search.

Cómo escribir un examen reprobado

Como ya no las necesitamos, eliminaremos las println!sentencias


de src/lib.rs y src/main.rs que usábamos para comprobar el comportamiento del
programa. Luego, en src/lib.rs , añadiremos un testsmódulo con una función de
prueba, como hicimos en el Capítulo 11. La función de prueba especifica el
comportamiento que queremos searchque tenga la función: tomará una consulta y
el texto que se buscará, y devolverá solo las líneas del texto que contengan la
consulta. El Listado 12-15 muestra esta prueba, que aún no se compila.

Nombre de archivo: src/lib.rs

#[cfg(test)]
mod tests {
use super::*;

#[test]
Rust
jueves, 22 de mayo de 2025 : Página 337 de 719
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";

assert_eq!(vec!["safe, fast, productive."], search(query, contents));


}
}
Listado 12-15: Creación de una prueba fallida para elsearch función que deseamos tener

Esta prueba busca la cadena "duct". El texto que buscamos consta de tres líneas,
de las cuales solo una contiene"duct" texto incompleto]. (Tenga en cuenta que la
barra invertida después de la comilla doble inicial indica a Rust que no debe
colocar un carácter de nueva línea al principio del contenido de esta cadena
literal). Afirmamos que el valor devuelto por la searchfunción contiene solo la línea
esperada.

Aún no podemos ejecutar esta prueba y ver si falla porque ni siquiera se compila:
¡la searchfunción aún no existe! De acuerdo con los principios de TDD,
agregaremos el código justo para que la prueba se compile y se ejecute
agregando una definición desearch función que siempre devuelve un vector vacío,
como se muestra en el Listado 12-16. Entonces, la prueba debería compilarse y
fallar porque un vector vacío no coincide con un vector que contenga la
línea."safe, fast, productive."

Nombre de archivo: src/lib.rs


pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
Listado 12-16: Definiendo lo suficiente desearch función para que nuestra prueba se compile

Tenga en cuenta que debemos definir un tiempo de vida explícito 'aen la firma
de searchy usar ese tiempo de vida con el contentsargumento y el valor de retorno.
Recuerde que en el Capítulo 10 los parámetros de tiempo de vida especifican qué
tiempo de vida del argumento está conectado con el tiempo de vida del valor de
retorno. En este caso, indicamos que el vector devuelto debe contener segmentos
de cadena que hagan referencia a segmentos del argumento. contents (en lugar
del argumento query).

En otras palabras, le indicamos a Rust que los datos devueltos por


la searchfunción se mantendrán mientras los datos pasados a la searchfunción en
Rust
jueves, 22 de mayo de 2025 : Página 338 de 719

el contentsargumento. ¡Esto es importante! Los datos referenciados por una


porción deben ser válidos para que la referencia sea válida; si el compilador
asume que estamos creando porciones de cadena queryen lugar de...contents , la
comprobación de seguridad será incorrecta.

Si olvidamos las anotaciones de duración e intentamos compilar esta función,


obtendremos este error:

$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:28:51
|
28 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not
say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
|
28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to 1 previous error

Rust no puede saber cuál de los dos argumentos necesitamos, así que debemos
decírselo explícitamente. Dado que contentses el argumento que contiene todo
nuestro texto y queremos devolver las partes de ese texto que coinciden,
sabemos...contents es" es el argumento que debe conectarse al valor de retorno
mediante la sintaxis de duración.

Otros lenguajes de programación no requieren que se conecten argumentos para


devolver valores en la firma, pero esta práctica se volverá más sencilla con el
tiempo. Puede comparar este ejemplo con los de "Validación de referencias con
tiempos de vida". del capítulo 10.

Ahora vamos a ejecutar la prueba:

$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.97s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... FAILED
Rust
jueves, 22 de mayo de 2025 : Página 339 de 719

failures:

---- tests::one_result stdout ----


thread 'tests::one_result' panicked at src/lib.rs:44:9:
assertion `left == right` failed
left: ["safe, fast, productive."]
right: []
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
tests::one_result

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Genial, la prueba falla, tal como esperábamos. ¡Hagamos que la pase!

Escribir código para pasar la prueba

Actualmente, nuestra prueba falla porque siempre devolvemos un vector vacío.


Para solucionarlo e implementarlo search, nuestro programa debe seguir estos
pasos:

1. Iterar a través de cada línea del contenido.


2. Compruebe si la línea contiene nuestra cadena de consulta.
3. Si es así, agréguelo a la lista de valores que estamos devolviendo.
4. Si no es así, no hagas nada.
5. Devuelve la lista de resultados que coinciden.

Trabajemos en cada paso, comenzando por iterar a través de las líneas.

Iterando a través de líneas con ellines método

Rust cuenta con un método útil para gestionar la iteración línea por línea de
cadenas, convenientemente llamado lines, que funciona como se muestra en el
Listado 12-17. Tenga en cuenta que esto aún no se compila.

Nombre de archivo: src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {


for line in contents.lines() {
// do something with line
}
Rust
jueves, 22 de mayo de 2025 : Página 340 de 719
}
Listado 12-17: Iterando a través de cada línea encontents

El linesmétodo devuelve un iterador. Hablaremos de iteradores en profundidad


en el Capítulo 13 , pero recuerda que viste esta forma de usar un iterador en el
Listado 3-5 , donde usamos un forbucle con un iterador para ejecutar código en
cada elemento de una colección.

Buscando cada línea para la consulta

A continuación, comprobaremos si la línea actual contiene nuestra cadena de


consulta. Afortunadamente, las cadenas tienen un método útil
llamado containsque realiza esta tarea automáticamente. Agregue una llamada
al containsmétodo en la searchfunción, como se muestra en el Listado 12-18. Tenga
en cuenta que esto aún no se compilará.

Nombre de archivo: src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {


for line in contents.lines() {
if line.contains(query) {
// do something with line
}
}
}
Listado 12-18: Agregar funcionalidad para ver si la línea contiene la cadena enquery

Actualmente, estamos desarrollando la funcionalidad. Para compilar el código,


necesitamos devolver un valor del cuerpo, como indicamos en la firma de la
función.

Almacenamiento de líneas coincidentes

Para completar esta función, necesitamos una forma de almacenar las líneas
coincidentes que queremos devolver. Para ello, podemos crear un vector mutable
antes del forbucle y llamar al pushmétodo para almacenar "a" lineen el vector.
Después del forbucle, devolvemos el vector, como se muestra en el Listado 12-19.

Nombre de archivo: src/lib.rs


pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();

for line in contents.lines() {


if line.contains(query) {
Rust
jueves, 22 de mayo de 2025 : Página 341 de 719
results.push(line);
}
}

results
}
Listado 12-19: Almacenar las líneas que coinciden para que podamos devolverlas

Ahora la searchfunción debería devolver solo las líneas que contienen query, y
nuestra prueba debería ser exitosa. Ejecutemos la prueba:

$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

¡Nuestra prueba pasó, así que sabemos que funciona!

En este punto, podríamos considerar oportunidades para refactorizar la


implementación de la función de búsqueda, manteniendo la aprobación de las
pruebas para mantener la misma funcionalidad. El código de la función de
búsqueda no es tan malo, pero no aprovecha algunas características útiles de los
iteradores. Retomaremos este ejemplo en el Capítulo 13 , donde exploraremos los
iteradores en detalle y veremos cómo mejorarlo.

Usando la searchFunción en la runFunción

Ahora que la searchfunción está funcionando y probada, necesitamos


llamarla search desde runella. Necesitamos pasar el config.queryvalor y
Rust
jueves, 22 de mayo de 2025 : Página 342 de 719

el contentsque runlee del archivo a la searchfunción. Luego, run imprimiremos cada


línea devuelta search:

Nombre de archivo: src/lib.rs

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {


let contents = fs::read_to_string(config.file_path)?;

for line in search(&config.query, &contents) {


println!("{line}");
}

Ok(())
}

Todavía usamos un forbucle para devolver cada línea searche imprimirla.

¡Ahora debería funcionar todo el programa! Probémoslo, primero con una palabra
que debería devolver exactamente una línea del poema de Emily Dickinson: rana .

$ cargo run -- frog poem.txt


Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog

¡Genial! Ahora probemos con una palabra que coincida con varias líneas,
como "body" :

$ cargo run -- body poem.txt


Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!

Y por último, asegurémonos de que no obtengamos ninguna línea cuando


busquemos una palabra que no está en ninguna parte del poema, como por
ejemplo monomorfización :

$ cargo run -- monomorphization poem.txt


Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization poem.txt`
Rust
jueves, 22 de mayo de 2025 : Página 343 de 719

¡Excelente! Creamos nuestra propia versión reducida de una herramienta clásica


y aprendimos mucho sobre cómo estructurar aplicaciones. También aprendimos
algo sobre entrada y salida de archivos, tiempos de vida, pruebas y análisis de
línea de comandos.

Para finalizar este proyecto, demostraremos brevemente cómo trabajar con


variables de entorno y cómo imprimir en el error estándar, ambos métodos útiles
cuando se escriben programas de línea de comandos.

Trabajar con variables de entorno


Mejoraremos minigrepañadiendo una función adicional: una opción para
búsquedas sin distinción entre mayúsculas y minúsculas, que el usuario puede
activar mediante una variable de entorno. Podríamos convertir esta función en
una opción de línea de comandos y exigir a los usuarios que la introduzcan cada
vez que deseen aplicarla, pero al convertirla en una variable de entorno,
permitimos a nuestros usuarios configurarla una sola vez y que todas sus
búsquedas no distingan entre mayúsculas y minúsculas en esa sesión de
terminal.

Cómo escribir una prueba fallida para una searchfunción que no


distingue entre mayúsculas y minúsculas

Primero, añadimos una nueva search_case_insensitivefunción que se llamará cuando


la variable de entorno tenga un valor. Seguiremos el proceso TDD, por lo que el
primer paso es, de nuevo, escribir una prueba fallida. Añadiremos una nueva
prueba para la nueva search_case_insensitivefunción y renombraremos la prueba
anterior de a one_resultpara case_sensitiveaclarar las diferencias entre ambas, como
se muestra en el Listado 12-20.

Nombre de archivo: src/lib.rs

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Rust
jueves, 22 de mayo de 2025 : Página 344 de 719
Pick three.
Duct tape.";

assert_eq!(vec!["safe, fast, productive."], search(query, contents));


}

#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Listado 12-20: Agregar una nueva prueba fallida para la función que no distingue entre mayúsculas y
minúsculas que estamos a punto de agregar

Tenga en cuenta que también hemos editado las pruebas anteriores contents.
Hemos añadido una nueva línea con el texto con D"Duct tape." mayúscula , que no
debería coincidir con la consulta al buscar con distinción entre mayúsculas y
minúsculas. Al modificar la prueba anterior de esta manera, nos aseguramos de
no interrumpir accidentalmente la función de búsqueda con distinción entre
mayúsculas y minúsculas que ya implementamos. Esta prueba debería ser
correcta ahora y debería seguir siendo correcta a medida que trabajamos en la
búsqueda sin distinción entre mayúsculas y minúsculas. "duct"

La nueva prueba para la búsqueda sin distinción entre mayúsculas y minúsculas


utiliza "rUsT"como consulta. En la search_case_insensitivefunción que vamos a
añadir, la consulta "rUsT" debe coincidir con la línea que
contiene "Rust:"una R mayúscula y coincidir con la línea "Trust me."aunque ambas
tengan un uso de mayúsculas y minúsculas diferente al de la consulta. Esta es la
prueba fallida y no se compilará porque aún no hemos definido
la search_case_insensitivefunción. Si lo desea, puede añadir una implementación
básica que siempre devuelva un vector vacío, similar a lo que hicimos para
la searchfunción del Listado 12-16 para ver si la prueba se compila y falla.

Implementando la search_case_insensitivefunción
Rust
jueves, 22 de mayo de 2025 : Página 345 de 719

La search_case_insensitivefunción, que se muestra en el Listado 12-21, será


prácticamente idéntica a la searchfunción anterior. La única diferencia es que
escribiremos en minúsculas "<sub>" queryy "<sub>" each</sub> linepara que,
independientemente de si los argumentos de entrada están en mayúsculas o
minúsculas, se expresen igual al comprobar si la línea contiene la consulta.

Nombre de archivo: 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();

for line in contents.lines() {


if line.to_lowercase().contains(&query) {
results.push(line);
}
}

results
}
Listado 12-21: Definición de la search_case_insensitivefunción para convertir en minúsculas la consulta y
la línea antes de compararlas

Primero, convertimos la querycadena a minúsculas y la almacenamos en una


nueva variable con el mismo nombre, eclipsando la original. to_lowercaseEs
necesario llamar a la consulta para que, independientemente de si la consulta del
usuario es "rust", "RUST", "Rust", o "rUsT", la tratemos como si lo fuera "rust"y no
distingamos entre mayúsculas y minúsculas. Aunque to_lowercasemanejará
Unicode básico, no será 100 % preciso. Si estuviéramos escribiendo una
aplicación real, querríamos profundizar un poco más en este aspecto, pero esta
sección trata sobre variables de entorno, no sobre Unicode, así que lo dejaremos
así.

Tenga en cuenta que queryahora es un Stringfragmento de cadena en lugar de


uno, ya que la llamada to_lowercasecrea nuevos datos en lugar de referenciar a los
existentes. "rUsT"Por ejemplo, supongamos que la consulta es : ese fragmento de
cadena no contiene un ``` minúscula`` upara tque lo usemos, por lo que debemos
asignar un nuevo ` `` que lo Stringcontenga "rust". Al pasar querycomo argumento
al containsmétodo, ahora debemos agregar un ```, ya que la firma `` containsestá
definida para aceptar un fragmento de cadena.
Rust
jueves, 22 de mayo de 2025 : Página 346 de 719

A continuación, añadimos una llamada a " to_lowercaseon each" linepara convertir


todos los caracteres en minúsculas. Ahora que hemos convertido " lineand query" a
minúsculas, encontraremos coincidencias independientemente de si la consulta
está en mayúsculas o minúsculas.

Veamos si esta implementación pasa las pruebas:

$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

¡Genial! Aprobaron. Ahora, llamemos a la nueva search_case_insensitivefunción


desde la runfunción. Primero, agregaremos una opción de configuración a
la Config estructura para alternar entre búsquedas con y sin distinción entre
mayúsculas y minúsculas. Agregar este campo provocará errores de compilación,
ya que aún no lo estamos inicializando en ninguna parte:

Nombre de archivo: src/lib.rs

pub struct Config {


pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
Rust
jueves, 22 de mayo de 2025 : Página 347 de 719

Agregamos el ignore_casecampo que contiene un valor booleano. A continuación,


necesitamos que la run función verifique el ignore_casevalor del campo y lo use
para decidir si se llama a la searchfunción o a la search_case_insensitive función,
como se muestra en el Listado 12-22. Esto aún no se compila.

Nombre de archivo: src/lib.rs

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {


let contents = fs::read_to_string(config.file_path)?;

let results = if config.ignore_case {


search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};

for line in results {


println!("{line}");
}

Ok(())
}
Listado 12-22: Llamar a cualquiera de los dos searcho search_case_insensitiveen función del valor
enconfig.ignore_case

Finalmente, necesitamos verificar la variable de entorno. Las funciones para


trabajar con variables de entorno se encuentran en el envmódulo de la biblioteca
estándar, por lo que lo incorporamos al alcance en la parte superior de src/lib.rs .
Luego, usaremos la varfunción del envmódulo para verificar si se ha definido algún
valor para la variable de entorno llamada IGNORE_CASE, como se muestra en el
Listado 12-23.

Nombre de archivo: 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");
}

let query = args[1].clone();


let file_path = args[2].clone();

let ignore_case = env::var("IGNORE_CASE").is_ok();

Ok(Config {
Rust
jueves, 22 de mayo de 2025 : Página 348 de 719
query,
file_path,
ignore_case,
})
}
}
Listado 12-23: Comprobación de cualquier valor en una variable de entorno denominadaIGNORE_CASE

Aquí, creamos una nueva variable, ignore_case. Para establecer su valor, llamamos
a la env::varfunción y le pasamos el nombre de la IGNORE_CASEvariable de entorno.
La env::varfunción devuelve una Result, que será la Okvariante correcta que
contiene el valor de la variable de entorno si esta tiene cualquier valor. Devolverá
la Errvariante si la variable de entorno no tiene valor.

Usamos el is_okmétodo en [nombre Resultdel programa] para comprobar si la


variable de entorno está configurada, lo que significa que el programa debe
realizar una búsqueda sin distinguir entre mayúsculas y minúsculas. Si
la IGNORE_CASEvariable de entorno no tiene ningún valor, [nombre del
programa is_ok] retornará falsey el programa realizará una búsqueda sin distinguir
entre mayúsculas y minúsculas. No nos importa el valor de la variable de entorno,
solo si está configurada o no, por lo que la comprobamos is_oken lugar de usar
[ nombre del programa unwrap], [nombre del programa] expecto cualquiera de los
otros métodos que hemos visto en [nombre del programa] Result.

Pasamos el valor de la ignore_casevariable a la Configinstancia para que


la runfunción pueda leer ese valor y decidir si llamar
a search_case_insensitiveo search, como implementamos en el Listado 12-22.

¡Intentémoslo! Primero, ejecutaremos nuestro programa sin la variable de entorno


definida y con la consulta to, que debería coincidir con cualquier línea que
contenga la palabra " to" en minúsculas:

$ cargo run -- to poem.txt


Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!

¡Parece que sigue funcionando! Ahora ejecutemos el programa con IGNORE_CASEel


valor establecido en , 1pero con la misma consulta :

$ IGNORE_CASE=1 cargo run -- to poem.txt


Rust
jueves, 22 de mayo de 2025 : Página 349 de 719

Si está utilizando PowerShell, deberá configurar la variable de entorno y ejecutar


el programa como comandos separados:

PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt

Esto persistirá IGNORE_CASEdurante el resto de la sesión de shell. Puede


desactivarse con el Remove-Itemcmdlet:

PS> Remove-Item Env:IGNORE_CASE

Deberíamos obtener líneas que contengan letras mayúsculas:

Are you nobody, too?


How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

Excelente, ¡también obtuvimos líneas que contienen "To" !


Nuestro minigrepprograma ahora puede realizar búsquedas sin distinción entre
mayúsculas y minúsculas, controladas por una variable de entorno. Ahora ya sabe
cómo administrar las opciones establecidas mediante argumentos de línea de
comandos o variables de entorno.

Algunos programas permiten argumentos y variables de entorno para la misma


configuración. En esos casos, los programas deciden que uno u otro tiene
prioridad. Para otro ejercicio, intente controlar la distinción entre mayúsculas y
minúsculas mediante un argumento de la línea de comandos o una variable de
entorno. Decida si el argumento de la línea de comandos o la variable de entorno
deben tener prioridad si el programa se ejecuta con uno configurado para
distinguir entre mayúsculas y minúsculas y el otro para ignorarlas.

El std::envmódulo contiene muchas más funciones útiles para trabajar con


variables de entorno: consulte su documentación para ver qué hay disponible.

Escribir mensajes de error en el error estándar


en lugar de en la salida estándar
Actualmente, estamos escribiendo toda nuestra salida en la terminal mediante
la println!macro. En la mayoría de las terminales, existen dos tipos de
salida: salida estándar ( stdout) para información general y error estándar ( stderr)
para mensajes de error. Esta distinción permite a los usuarios elegir entre dirigir
Rust
jueves, 22 de mayo de 2025 : Página 350 de 719

la salida correcta de un programa a un archivo y, al mismo tiempo, mostrar los


mensajes de error en pantalla.

La println!macro solo puede imprimir en la salida estándar, por lo que debemos


usar algo más para imprimir en el error estándar.

Cómo comprobar dónde se escriben los errores

Primero, observemos cómo minigrepse escribe actualmente en la salida estándar


el contenido impreso, incluyendo cualquier mensaje de error que queramos
escribir en la salida de error estándar. Para ello, redirigiremos el flujo de salida
estándar a un archivo, provocando intencionalmente un error. No redirigiremos el
flujo de salida de error estándar, por lo que cualquier contenido enviado a la
salida de error estándar seguirá mostrándose en pantalla.

Se espera que los programas de línea de comandos envíen mensajes de error al


flujo de error estándar, por lo que podemos seguir viéndolos en pantalla incluso si
redirigimos el flujo de salida estándar a un archivo. Nuestro programa no funciona
correctamente actualmente: ¡estamos a punto de ver que guarda la salida del
mensaje de error en un archivo!

Para demostrar este comportamiento, ejecutaremos el programa con >la ruta del
archivo output.txt al que queremos redirigir el flujo de salida estándar. No
pasaremos ningún argumento, lo que debería generar un error:

$ cargo run > output.txt

La >sintaxis indica al shell que escriba el contenido de la salida estándar


en output.txt en lugar de en la pantalla. No vimos el mensaje de error que
esperábamos impreso en la pantalla, así que eso significa que debió haber
terminado en el archivo. Esto es lo que contiene output.txt :

Problem parsing arguments: not enough arguments

Sí, nuestro mensaje de error se imprime en la salida estándar. Es mucho más útil
que este tipo de mensajes se impriman en la salida de error estándar para que
solo los datos de una ejecución exitosa se incluyan en el archivo. Lo
cambiaremos.

Errores de impresión según el error estándar


Rust
jueves, 22 de mayo de 2025 : Página 351 de 719

Usaremos el código del Listado 12-24 para cambiar la forma en que se imprimen
los mensajes de error. Debido a la refactorización realizada anteriormente en este
capítulo, todo el código que imprime los mensajes de error se encuentra en una
sola función, main. La biblioteca estándar proporciona la eprintln!macro que
imprime en el flujo de error estándar, así que cambiemos los dos lugares donde
llamábamos println!para imprimir errores eprintln! .

Nombre de archivo: src/main.rs


fn main() {
let args: Vec<String> = env::args().collect();

let config = Config::build(&args).unwrap_or_else(|err| {


eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});

if let Err(e) = minigrep::run(config) {


eprintln!("Application error: {e}");
process::exit(1);
}
}
Listado 12-24: Escritura de mensajes de error en el error estándar en lugar de la salida estándar
utilizandoeprintln!

Ahora ejecutemos nuevamente el programa de la misma manera, sin ningún


argumento y redirigiendo la salida estándar con >:

$ cargo run > output.txt


Problem parsing arguments: not enough arguments

Ahora vemos el error en pantalla y output.txt no contiene nada, que es el


comportamiento que esperamos de los programas de línea de comandos.

Ejecutemos nuevamente el programa con argumentos que no provoquen un error


pero que aún redirijan la salida estándar a un archivo, de la siguiente manera:

$ cargo run -- to poem.txt > output.txt

No veremos ninguna salida en la terminal y output.txt contendrá nuestros


resultados:

Nombre del archivo: output.txt

Are you nobody, too?


How dreary to be somebody!
Rust
jueves, 22 de mayo de 2025 : Página 352 de 719

Esto demuestra que ahora utilizamos la salida estándar para una salida exitosa y
el error estándar para una salida de error según corresponda.

Resumen
Este capítulo resumió algunos de los conceptos principales que has aprendido
hasta ahora y abordó cómo realizar operaciones comunes de E/S en Rust. Al usar
argumentos de línea de comandos, archivos, variables de entorno y la eprintln!
macro para imprimir errores, estás preparado para escribir aplicaciones de línea
de comandos. Combinando los conceptos de los capítulos anteriores, tu código
estará bien organizado, almacenará datos eficazmente en las estructuras de
datos adecuadas, gestionará errores correctamente y estará bien probado.

A continuación, exploraremos algunas características de Rust que fueron


influenciadas por los lenguajes funcionales: cierres e iteradores.

Características del lenguaje


funcional: iteradores y cierres
El diseño de Rust se ha inspirado en muchos lenguajes y técnicas existentes, y
una influencia significativa es la programación funcional . La programación
funcional suele incluir el uso de funciones como valores, pasándolas en
argumentos, devolviéndolas desde otras funciones, asignándolas a variables para
su posterior ejecución, etc.

En este capítulo no debatiremos la cuestión de qué es o no es la programación


funcional, sino que analizaremos algunas características de Rust que son
similares a las características de muchos lenguajes a las que a menudo se hace
referencia como funcionales.

Más específicamente, cubriremos:

 Cierres , una construcción similar a una función que puedes almacenar en


una variable
 Iteradores , una forma de procesar una serie de elementos
 Cómo utilizar cierres e iteradores para mejorar el proyecto de E/S en el
Capítulo 12
Rust
jueves, 22 de mayo de 2025 : Página 353 de 719

 El rendimiento de los cierres e iteradores (Alerta de spoiler: ¡son más


rápidos de lo que piensas!)

Ya hemos cubierto otras características de Rust, como la coincidencia de patrones


y las enumeraciones, que también se ven influenciadas por el estilo funcional.
Dado que dominar los cierres y los iteradores es fundamental para escribir código
Rust idiomático y rápido, dedicaremos este capítulo entero a ellos.

Cierres: Funciones anónimas que capturan su


entorno
Los cierres de Rust son funciones anónimas que se pueden guardar en una
variable o pasar como argumentos a otras funciones. Se puede crear el cierre en
un lugar y luego llamarlo en otro para evaluarlo en un contexto diferente. A
diferencia de las funciones, los cierres pueden capturar valores del ámbito en el
que se definen. Demostraremos cómo estas características de los cierres
permiten la reutilización de código y la personalización del comportamiento.

Capturando el entorno con cierres

Primero, examinaremos cómo podemos usar los cierres para capturar valores del
entorno en el que se definen para su uso posterior. Este es el escenario: De vez
en cuando, nuestra empresa de camisetas regala una camiseta exclusiva de
edición limitada a alguien de nuestra lista de correo como promoción. Los
usuarios de la lista de correo pueden agregar su color favorito a su perfil. Si la
persona elegida para recibir la camiseta gratis tiene su color favorito, recibe la
camiseta de ese color. Si no ha especificado un color favorito, recibe el color que
más tenga la empresa en ese momento.

Hay muchas maneras de implementar esto. En este ejemplo, usaremos una


enumeración llamada ShirtColorque contiene las variantes Redy Blue(limitando el
número de colores disponibles para simplificar). Representamos el inventario de
la empresa con una Inventoryestructura que tiene un campo llamado shirtsque
contiene un que Vec<ShirtColor>representa los colores de las camisas disponibles
actualmente. El método giveawaydefinido en Inventoryobtiene la preferencia
opcional de color de camisa del ganador de la camisa gratuita y devuelve el color
de camisa que recibirá. Esta configuración se muestra en el Listado 13-1:

Nombre de archivo: src/main.rs


Rust
jueves, 22 de mayo de 2025 : Página 354 de 719
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}

struct Inventory {
shirts: Vec<ShirtColor>,
}

impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}

fn most_stocked(&self) -> ShirtColor {


let mut num_red = 0;
let mut num_blue = 0;

for color in &self.shirts {


match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}

fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};

let user_pref1 = Some(ShirtColor::Red);


let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);

let user_pref2 = None;


let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
Listado 13-1: Situación de sorteo de camisetas de una empresa
Rust
jueves, 22 de mayo de 2025 : Página 355 de 719

El storeusuario definido maintiene dos camisetas azules y una roja por distribuir en
esta promoción de edición limitada. Llamamos al giveawaymétodo para un usuario
con preferencia por una camiseta roja y para un usuario sin preferencia.

Nuevamente, este código podría implementarse de muchas maneras, y aquí, para


centrarnos en los cierres, nos hemos ceñido a los conceptos ya aprendidos,
excepto por el cuerpo del giveawaymétodo que usa un cierre. En
el giveawaymétodo, obtenemos la preferencia del usuario como parámetro de
tipo Option<ShirtColor>e invocamos el unwrap_or_elsemétodo on user_preference.
El unwrap_or_elsemétodo on Option<T> está definido por la biblioteca estándar.
Toma un argumento: un cierre sin argumentos que devuelve un valor T (el mismo
tipo almacenado en la Somevariante de Option<T>, en este caso ShirtColor).
Si Option<T>es la Somevariante, unwrap_or_else devuelve el valor de dentro
de Some. Si Option<T>es la None variante, unwrap_or_elseinvoca el cierre y devuelve
el valor devuelto por este.

Especificamos la expresión de cierre || self.most_stocked()como argumento


de unwrap_or_else. Este cierre no acepta parámetros (si los tuviera, aparecerían
entre las dos barras verticales). El cuerpo del cierre llama a self.most_stocked().
Aquí definimos el cierre, y la implementación de unwrap_or_elselo evaluará
posteriormente si se necesita el resultado.

Al ejecutar este código se imprime:

$ 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

Un aspecto interesante es que hemos pasado un cierre que


invoca self.most_stocked()la Inventoryinstancia actual. La biblioteca estándar no
necesitaba saber nada sobre los tipos Inventoryo ShirtColorque definimos ni sobre la
lógica que queremos usar en este caso. El cierre captura una referencia inmutable
a la self Inventoryinstancia y la pasa al método con el código que
especificamos unwrap_or_else. Las funciones, en cambio, no pueden capturar su
entorno de esta manera.

Inferencia y anotación de tipos de cierre


Rust
jueves, 22 de mayo de 2025 : Página 356 de 719

Existen más diferencias entre funciones y cierres. Los cierres no suelen requerir la
anotación de los tipos de los parámetros ni del valor de retorno, como fnocurre
con las funciones. Las anotaciones de tipo son necesarias en las funciones porque
los tipos forman parte de una interfaz explícita expuesta a los usuarios. Definir
esta interfaz de forma rígida es importante para garantizar que todos estén de
acuerdo sobre los tipos de valores que usa y devuelve una función. Los cierres,
por otro lado, no se utilizan en una interfaz expuesta como esta: se almacenan en
variables y se utilizan sin nombrarlas ni exponerlas a los usuarios de nuestra
biblioteca.

Las clausuras suelen ser breves y relevantes solo en un contexto específico, no en


cualquier escenario arbitrario. En estos contextos limitados, el compilador puede
inferir los tipos de los parámetros y el tipo de retorno, de forma similar a como
infiere los tipos de la mayoría de las variables (existen casos excepcionales en los
que el compilador también necesita anotaciones de tipo de clausura).

Al igual que con las variables, podemos añadir anotaciones de tipo si queremos
ser más explícitos y claros, pero sin ser más detallados de lo estrictamente
necesario. La anotación de los tipos de un cierre se asemejaría a la definición que
se muestra en el Listado 13-2. En este ejemplo, definimos un cierre y lo
almacenamos en una variable, en lugar de definirlo donde lo pasamos como
argumento, como hicimos en el Listado 13-1.

Nombre de archivo: src/main.rs


let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
Listado 13-2: Agregar anotaciones de tipo opcionales de los tipos de parámetro y valor de retorno en el
cierre

Con las anotaciones de tipo añadidas, la sintaxis de los cierres se asemeja más a
la de las funciones. Aquí definimos una función que suma 1 a su parámetro y un
cierre con el mismo comportamiento, a modo de comparación. Hemos añadido
algunos espacios para alinear las partes relevantes. Esto ilustra cómo la sintaxis
de los cierres es similar a la de las funciones, salvo por el uso de barras verticales
y la cantidad de sintaxis opcional:

fn add_one_v1 (x: u32) -> u32 { x + 1 }


let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
Rust
jueves, 22 de mayo de 2025 : Página 357 de 719
let add_one_v4 = |x| x+1 ;

La primera línea muestra la definición de una función y la segunda, la definición


de un cierre completamente anotado. En la tercera línea, eliminamos las
anotaciones de tipo de la definición del cierre. En la cuarta línea, eliminamos los
corchetes, que son opcionales porque el cuerpo del cierre solo tiene una
expresión. Todas estas son definiciones válidas que producirán el mismo
comportamiento al ser invocadas. Las líneas ` add_one_v3and` add_one_v4requieren
que los cierres se evalúen para poder compilar, ya que los tipos se inferirán a
partir de su uso. Esto es similar a let v = Vec::new();necesitar anotaciones de tipo o
valores de algún tipo Vecpara que Rust pueda inferir el tipo.

Para las definiciones de cierre, el compilador inferirá un tipo concreto para cada
uno de sus parámetros y para su valor de retorno. Por ejemplo, el Listado 13-3
muestra la definición de un cierre corto que simplemente devuelve el valor que
recibe como parámetro. Este cierre no es muy útil, salvo para este ejemplo.
Tenga en cuenta que no hemos añadido ninguna anotación de tipo a la definición.
Al no haber anotaciones de tipo, podemos llamar al cierre con cualquier tipo,
como hicimos Stringla primera vez. Si intentamos llamar example_closurecon un
entero, obtendremos un error.

Nombre de archivo: src/main.rs

let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);
Listado 13-3: Intento de llamar a un cierre cuyos tipos se infieren con dos tipos diferentes.

El compilador nos da este error:

$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5| let n = example_closure(5);
| --------------- ^- help: try using a conversion method: `.to_string()`
| | |
| | expected `String`, found integer
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
Rust
jueves, 22 de mayo de 2025 : Página 358 de 719
--> src/main.rs:4:29
|
4| let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is
of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2| let example_closure = |x| x;
| ^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error

La primera vez que llamamos example_closurecon el Stringvalor, el compilador


infiere que el tipo de xy el tipo de retorno del cierre son String. Estos tipos se
bloquean en el cierre en example_closure, y obtenemos un error de tipo la próxima
vez que intentamos usar un tipo diferente con el mismo cierre.

Captura de referencias o transferencia de propiedad

Los cierres pueden capturar valores de su entorno de tres maneras, que se


corresponden directamente con las tres maneras en que una función puede tomar
un parámetro: tomando prestado de forma inmutable, tomando prestado de
forma mutable y tomando posesión. El cierre decidirá cuál usar en función de lo
que el cuerpo de la función haga con los valores capturados.

En el Listado 13-4, definimos un cierre que captura una referencia inmutable al


vector nombrado listporque solo necesita una referencia inmutable para imprimir
el valor:

Nombre de archivo: src/main.rs


fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");

let only_borrows = || println!("From closure: {list:?}");

println!("Before calling closure: {list:?}");


only_borrows();
println!("After calling closure: {list:?}");
}
Listado 13-4: Definición y llamada de un cierre que captura una referencia inmutable
Rust
jueves, 22 de mayo de 2025 : Página 359 de 719

Este ejemplo también ilustra que una variable puede vincularse a una definición
de cierre, y luego podemos llamar al cierre usando el nombre de la variable y
paréntesis como si el nombre de la variable fuera un nombre de función.

Dado que podemos tener múltiples referencias inmutables a listal mismo


tiempo, listsigue siendo accesible desde el código antes de la definición del cierre,
después de la definición del cierre pero antes de que se invoque, y después de
que se invoque. Este código se compila, se ejecuta e imprime:

$ cargo run
Locking 1 package to latest compatible version
Adding closure-example v0.1.0 (/Users/chris/dev/rust-lang/book/tmp/listings/ch13-
functional-features/listing-13-04)
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]

A continuación, en el Listado 13-5, modificamos el cuerpo del cierre para que


añada un elemento al listvector. El cierre ahora captura una referencia mutable:

Nombre de archivo: src/main.rs


fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");

let mut borrows_mutably = || list.push(7);

borrows_mutably();
println!("After calling closure: {list:?}");
}
Listado 13-5: Definición y llamada de un cierre que captura una referencia mutable

Este código se compila, se ejecuta y se imprime:

$ cargo run
Locking 1 package to latest compatible version
Adding closure-example v0.1.0 (/Users/chris/dev/rust-lang/book/tmp/listings/ch13-
functional-features/listing-13-05)
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]
Rust
jueves, 22 de mayo de 2025 : Página 360 de 719

Tenga en cuenta que ya no hay un println!entre la definición y la llamada


del borrows_mutablycierre: cuando borrows_mutablyse define, captura una referencia
mutable a list. No volvemos a usar el cierre después de llamarlo, por lo que el
préstamo mutable finaliza. Entre la definición del cierre y la llamada al cierre, no
se permite un préstamo inmutable a print porque no se permiten otros préstamos
cuando hay un préstamo mutable. Pruebe a añadir un ` println! there` para ver el
mensaje de error que aparece.

Si desea forzar el cierre a tomar posesión de los valores que utiliza en el entorno
aunque el cuerpo del cierre no necesite estrictamente posesión, puede utilizar
la movepalabra clave antes de la lista de parámetros.

Esta técnica es principalmente útil al pasar un cierre a un nuevo hilo para


transferir los datos de forma que sean propiedad del nuevo hilo. Analizaremos los
hilos y por qué conviene usarlos en detalle en el Capítulo 16, cuando hablemos de
concurrencia. Por ahora, exploraremos brevemente cómo generar un nuevo hilo
mediante un cierre que requiere la movepalabra clave. 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.

Nombre de archivo: src/main.rs


use std::thread;

fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");

thread::spawn(move || println!("From thread: {list:?}"))


.join()
.unwrap();
}
Listado 13-6: Uso movepara forzar el cierre para que el hilo tome posesión delist

Creamos un nuevo hilo y le asignamos un cierre para ejecutarlo como argumento.


El cuerpo del cierre imprime la lista. En el Listado 13-4, el cierre solo se
capturó listusando una referencia inmutable porque esa es la menor cantidad de
acceso a listnecesaria para imprimirla. En este ejemplo, aunque el cuerpo del
cierre solo necesita una referencia inmutable, debemos especificar que listse debe
mover al cierre colocando la movepalabra clave al principio de la definición del
cierre. El nuevo hilo podría terminar antes que el resto del hilo principal, o el hilo
principal podría terminar primero. Si el hilo principal mantuvo la propiedad
de listpero terminó antes que el nuevo hilo y eliminó list, la referencia inmutable
Rust
jueves, 22 de mayo de 2025 : Página 361 de 719

en el hilo no sería válida. Por lo tanto, el compilador requiere que listse mueva al
cierre dado al nuevo hilo para que la referencia sea válida. ¡Intenta eliminar
la movepalabra clave o usar list en el hilo principal después de definir el cierre para
ver qué errores de compilación obtienes!

Sacar los valores capturados de los cierres y los Fnrasgos

Una vez que un cierre ha capturado una referencia o la propiedad de un valor del
entorno donde se define (lo que afecta a lo que, si acaso, se mueve dentro del
cierre), el código del cuerpo del cierre define qué sucede con las referencias o
valores cuando el cierre se evalúa posteriormente (lo que afecta a lo que, si
acaso, se mueve fuera del cierre). Un cuerpo de cierre puede realizar cualquiera
de las siguientes acciones: mover un valor capturado fuera del cierre, mutar el
valor capturado, no mover ni mutar el valor, o no capturar nada del entorno desde
el principio.

La forma en que un cierre captura y gestiona los valores del entorno afecta los
rasgos que implementa, y los rasgos permiten que las funciones y estructuras
especifiquen qué tipos de cierres pueden usar. Los cierres implementarán
automáticamente uno, dos o los tres rasgos Fn, de forma aditiva, según cómo el
cuerpo del cierre gestione los valores:

1. FnOnceSe aplica a los cierres que se pueden llamar una vez. Todos los
cierres implementan al menos esta característica, ya que todos se pueden
llamar. Un cierre que extrae valores capturados de su cuerpo solo
implementará FnOnceesta Fncaracterística, ya que solo se puede llamar una
vez.
2. FnMutSe aplica a cierres que no extraen los valores capturados de su
cuerpo, pero que podrían mutarlos. Estos cierres pueden llamarse más de
una vez.
3. FnSe aplica a los cierres que no extraen los valores capturados de su cuerpo
ni los mutan, así como a los que no capturan nada de su entorno. Estos
cierres pueden llamarse más de una vez sin mutar su entorno, lo cual es
importante en casos como llamar a un cierre varias veces simultáneamente.

Veamos la definición del unwrap_or_elsemétodo Option<T>que utilizamos en el


Listado 13-1:

impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
Rust
jueves, 22 de mayo de 2025 : Página 362 de 719
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}

Recuerde que Tes el tipo genérico que representa el tipo del valor en
la Somevariante de un Option. Ese tipo Ttambién es el tipo de retorno de
la unwrap_or_elsefunción: código que invoca unwrap_or_elseun Option<String> , por
ejemplo, obtendrá un String.

A continuación, observe que la unwrap_or_elsefunción tiene el parámetro de tipo


genérico adicional F. El Ftipo es el tipo del parámetro denominadof , que es el
cierre que proporcionamos al llamar a unwrap_or_else.

El límite de rasgo especificado en el tipo genérico Fes FnOnce() -> T, lo que


significa que Fdebe poder llamarse una vez, no tomar argumentos y devolver
un T. El uso FnOncedel límite de rasgo expresa la restricción de
que unwrap_or_elsesolo se llamará fcomo máximo una vez. En el cuerpo
de unwrap_or_else, podemos ver que si Optiones Some, fno se llamará.
Si Optiones None, fse llamará una vez. Dado que todos los cierres
implementan FnOnce,unwrap_or_else acepta los tres tipos de cierres y es lo más
flexible posible.

Nota: Si lo que queremos hacer no requiere capturar un valor del entorno, podemos usar el nombre de
una función en lugar de un cierre. Por ejemplo, podríamos llamar unwrap_or_else(Vec::new)a
un Option<Vec<T>>valor para obtener un nuevo vector vacío si el valor es None. El compilador
implementa automáticamente el atributo Fnaplicable a la definición de una función.

Ahora veamos el método de la biblioteca estándar sort_by_keydefinido en las


porciones para ver en qué se diferencia de
" unwrap_or_elseuses" sort_by_keyen FnMutlugar de "uses FnOnce" para el atributo
enlazado. La clausura recibe un argumento en forma de referencia al elemento
actual en la porción considerada y devuelve un valor de tipo Kordenable. Esta
función es útil cuando se desea ordenar una porción por un atributo específico de
cada elemento. En el Listado 13-7, tenemos una lista de Rectangleinstancias y
usamos "uses" sort_by_key para ordenarlas por su widthatributo, de menor a
mayor:
Rust
jueves, 22 de mayo de 2025 : Página 363 de 719
Nombre de archivo: src/main.rs
#[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:#?}");
}
Listado 13-7: Uso sort_by_keypara ordenar rectángulos por ancho

Este código imprime:

$ 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 sort_by_keypor la que se define un FnMutcierre es que lo invoca varias


veces: una por cada elemento de la porción. El cierre |r| r.widthno captura, muta ni
extrae nada de su entorno, por lo que cumple con los requisitos de los atributos.

En cambio, el Listado 13-8 muestra un ejemplo de un cierre que implementa solo


el FnOnceatributo, ya que extrae un valor del entorno. El compilador no nos
permite usar este cierre con sort_by_key:

Nombre de archivo: src/main.rs


Rust
jueves, 22 de mayo de 2025 : Página 364 de 719

#[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 },
];

let mut sort_operations = vec![];


let value = String::from("closure called");

list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
Listado 13-8: Intento de utilizar un FnOncecierre consort_by_key

Esta es una forma artificial y enrevesada (que no funciona) de intentar contar el


número de veces sort_by_keyque se llama al cierre al ordenar list. Este código
intenta realizar este conteo insertando value—a Stringdesde el entorno del cierre—
en el sort_operationsvector. El cierre captura value y luego lo saca valuedel cierre
transfiriendo la propiedad de valueal sort_operationsvector. Este cierre se puede
llamar una vez; intentar llamarlo una segunda vez no funcionaría porque valueya
no estaría en el entorno para ser insertado sort_operationsde nuevo. Por lo tanto,
este cierre solo implementa FnOnce. Al intentar compilar este código, obtenemos
este error: valueno se puede sacar del cierre porque este debe implementar FnMut:

$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ move occurs because `value` has type `String`, which does not
implement the `Copy` trait
|
Rust
jueves, 22 de mayo de 2025 : Página 365 de 719
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++

For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error

El error apunta a la línea del cuerpo del cierre que se desplaza valuefuera del
entorno. Para solucionarlo, debemos modificar el cuerpo del cierre para que no
mueva valores fuera del entorno. Para contar el número de veces que se llama al
cierre, mantener un contador en el entorno e incrementar su valor en el cuerpo
del cierre es una forma más sencilla de calcularlo. El cierre del Listado 13-9
funciona sort_by_keyporque solo captura una referencia mutable
al num_sort_operationscontador y, por lo tanto, puede llamarse más de una vez:

Nombre de archivo: src/main.rs


#[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 },
];

let mut num_sort_operations = 0;


list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!("{list:#?}, sorted in {num_sort_operations} operations");
}
Listado 13-9: Se permite el uso de un FnMutcierre consort_by_key

Los Fnrasgos son importantes al definir o usar funciones o tipos que utilizan
cierres. En la siguiente sección, analizaremos los iteradores. Muchos métodos de
iterador aceptan argumentos de cierre, así que tenga en cuenta estos detalles al
continuar.

Procesamiento de una serie de elementos con


iteradores
Rust
jueves, 22 de mayo de 2025 : Página 366 de 719

El patrón iterador permite ejecutar una tarea en una secuencia de elementos, uno
por uno. Un iterador se encarga de la lógica de iterar sobre cada elemento y
determinar cuándo finaliza la secuencia. Al usar iteradores, no es necesario
reimplementar esa lógica.

En Rust, los iteradores son perezosos , lo que significa que no tienen efecto hasta
que se invocan métodos que consumen el iterador para agotarlo. Por ejemplo, el
código del Listado 13-10 crea un iterador sobre los elementos del
vector v1invocando el itermétodo definido en Vec<T>. Este código por sí solo no
tiene ninguna utilidad.

Nombre de archivo: src/main.rs


let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();


Listado 13-10: Creación de un iterador

El iterador se almacena en la v1_itervariable. Una vez creado un iterador, podemos


usarlo de diversas maneras. En el Listado 3-5 del Capítulo 3, iteramos sobre un
array mediante un forbucle para ejecutar código en cada uno de sus elementos.
En esencia, esto creaba y luego consumía implícitamente un iterador, pero hasta
ahora no hemos explicado su funcionamiento exacto.

En el ejemplo del Listado 13-11, separamos la creación del iterador de su uso en


el forbucle. Cuando forse llama al bucle utilizando el iterador en v1_iter , cada
elemento del iterador se utiliza en una iteración del bucle, que imprime cada
valor.

Nombre de archivo: src/main.rs


let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

for val in v1_iter {


println!("Got: {val}");
}
Listado 13-11: Uso de un iterador en un forbucle

En los lenguajes que no tienen iteradores proporcionados por sus bibliotecas


estándar, probablemente escribiría esta misma funcionalidad iniciando 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 alcance el
número total de elementos en el vector.
Rust
jueves, 22 de mayo de 2025 : Página 367 de 719

Los iteradores gestionan toda esa lógica automáticamente, reduciendo el código


repetitivo que podrías cometer errores. Los iteradores te brindan mayor
flexibilidad para usar la misma lógica con diferentes tipos de secuencias, no solo
con estructuras de datos indexables, como vectores. Analicemos cómo lo hacen
los iteradores.

El Iteratorrasgo y el nextmétodo

Todos los iteradores implementan un atributo llamado atributo Iterator, definido en


la biblioteca estándar. La definición del atributo es similar a la siguiente:

pub trait Iterator {


type Item;

fn next(&mut self) -> Option<Self::Item>;

// methods with default implementations elided


}

Observe que esta definición utiliza una sintaxis nueva: type Itemy Self::Item, que
definen un tipo asociado con este atributo. Hablaremos sobre los tipos asociados
en profundidad en el Capítulo 20. Por ahora, solo necesita saber que este código
indica que la implementación del Iteratoratributo requiere que también defina
un Itemtipo, y este Itemtipo se utiliza en el tipo de retorno del next método. En
otras palabras,Item tipo será el tipo devuelto por el iterador.

El Iteratorrasgo solo requiere que los implementadores definan un método:


el nextmétodo, que devuelve un elemento del iterador a la vez envuelto en Somey,
cuando termina la iteración, devuelveNone .

Podemos llamar al nextmétodo en iteradores directamente; el Listado 13-12


demuestra qué valores se devuelven a partir de llamadas repetidas al nextiterador
creado a partir del vector.

Nombre de archivo: src/lib.rs


#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];

let mut v1_iter = v1.iter();

assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
Rust
jueves, 22 de mayo de 2025 : Página 368 de 719
}
Listado 13-12: Llamada al nextmétodo en un iterador

Tenga en cuenta que necesitábamos hacerlo v1_itermutable: al llamar


al nextmétodo en un iterador, se cambia el estado interno que este utiliza para
registrar su posición en la secuencia. En otras palabras, este código consume el
iterador. Cada llamada consume nextun elemento del iterador. No necesitábamos
hacerlo v1_itermutable al usar un forbucle, ya que este v1_iterlo convertía en
mutable en segundo plano.

Tenga en cuenta también que los valores que obtenemos de las llamadas
a nextson referencias inmutables a los valores del vector. El itermétodo genera un
iterador sobre referencias inmutables. Si queremos crear un iterador que tome
propiedad de v1valores propios y los devuelva, podemos llamar a into_iteren lugar
de a iter. De forma similar, si queremos iterar sobre referencias mutables,
podemos llamar a iter_muten lugar de a.iter .

Métodos que consumen el iterador

El Iteratorrasgo tiene varios métodos diferentes con implementaciones


predeterminadas proporcionadas por la biblioteca estándar; puede obtener más
información sobre estos métodos consultando la documentación de la API de la
biblioteca estándar del Iterator rasgo. Algunos de estos métodos nextlo llaman en
su definición, por lo que es necesario implementarlo nextal implementar el
rasgo. Iterator rasgo.

Los métodos que invocan nextse denominan adaptadores consumidores , ya que


al llamarlos se consume el iterador. Un ejemplo es el summétodo, que toma
posesión del iterador e itera a través de los elementos llamando repetidamente
a next, consumiendo así el iterador. A medida que itera, suma cada elemento a un
total acumulado y devuelve el total al finalizar la iteración. El Listado 13-13
incluye una prueba que ilustra el uso del summétodo:

Nombre de archivo: src/lib.rs


#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

let total: i32 = v1_iter.sum();

assert_eq!(total, 6);
Rust
jueves, 22 de mayo de 2025 : Página 369 de 719
}
Listado 13-13: Llamada al summétodo para obtener el total de todos los elementos en el iterador

No se nos permite usarlo v1_iterdespués de la llamada a sumporque sumtoma


propiedad del iterador en el que lo llamamos.

Métodos que producen otros iteradores

Los adaptadores de iterador son métodos definidos en elIterator rasgo que no


consumen el iterador. En su lugar, generan iteradores diferentes modificando
algún aspecto del iterador original.

El listado 13-14 muestra un ejemplo de la llamada al método adaptador de


iteradores map, que requiere un cierre para cada elemento a medida que se itera.
El mapmétodo devuelve un nuevo iterador que genera los elementos modificados.
El cierre crea un nuevo iterador en el que cada elemento del vector se incrementa
en 1:

Nombre de archivo: src/main.rs

let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1);
Listado 13-14: Llamada al adaptador de iterador mappara crear un nuevo iterador

Sin embargo, este código produce una advertencia:

$ 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);
| +++++++

warning: `iterators` (bin "iterators") generated 1 warning


Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
Rust
jueves, 22 de mayo de 2025 : Página 370 de 719

El código del Listado 13-14 no realiza ninguna acción; el cierre especificado nunca
se llama. La advertencia nos recuerda por qué: los adaptadores de iteradores son
perezosos y necesitamos consumir el iterador aquí.

Para corregir esta advertencia y consumir el iterador, usaremos el collectmétodo


que usamos en el Capítulo 12 conenv::args el Listado 12-1. Este método consume
el iterador y recopila los valores resultantes en un tipo de datos de colección.

En el Listado 13-15, recopilamos los resultados de iterar sobre el iterador devuelto


por la llamada a mapen un vector. Este vector contendrá cada elemento del vector
original, incrementado en 1.

Nombre de archivo: src/main.rs


let v1: Vec<i32> = vec![1, 2, 3];

let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);


Listado 13-15: Llamar al mapmétodo para crear un nuevo iterador y luego llamar al collectmétodo para
consumir el nuevo iterador y crear un vector

Dado que mapse utiliza un cierre, podemos especificar cualquier operación que
queramos realizar en cada elemento. Este es un excelente ejemplo de cómo los
cierres permiten personalizar el comportamiento mientras se reutiliza el
comportamiento de iteración que Iteratorproporciona el atributo.

Puedes encadenar múltiples llamadas a adaptadores de iteradores para realizar


acciones complejas de forma legible. Sin embargo, como todos los iteradores son
perezosos, debes llamar a uno de los métodos de adaptador que los consumen
para obtener resultados de las llamadas a los adaptadores de iteradores.

Uso de cierres que capturan su entorno

Muchos adaptadores de iteradores toman cierres como argumentos y,


comúnmente, los cierres que especificaremos como argumentos para los
adaptadores de iteradores serán cierres que capturan su entorno.

En este ejemplo, usaremos el filtermétodo que toma un cierre. El cierre obtiene un


elemento del iterador y devuelve un bool. Si el cierre devuelve true, el valor se
incluirá en la iteración producida por filter. Si el cierre devuelve false, el valor no se
incluirá.
Rust
jueves, 22 de mayo de 2025 : Página 371 de 719

En el Listado 13-16, usamos filterun cierre que captura la shoe_size variable de su


entorno para iterar sobre una colección de Shoeinstancias de estructura.
Devolverá solo zapatos de la talla especificada.

Nombre de archivo: src/lib.rs


#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {


shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

#[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"),
},
];

let in_my_size = shoes_in_size(shoes, 10);

assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}
Rust
jueves, 22 de mayo de 2025 : Página 372 de 719
Listado 13-16: Uso del filtermétodo con un cierre que capturashoe_size

La shoes_in_sizefunción toma como parámetros un vector de zapatos y una talla.


Devuelve un vector que contiene únicamente zapatos de la talla especificada.

En el cuerpo de shoes_in_size, llamamos into_iterpara crear un iterador que toma


posesión del vector. Luego, llamamos filterpara adaptar ese iterador a un nuevo
iterador que solo contenga elementos para los que la clausura devuelva true.

El cierre captura el shoe_sizeparámetro del entorno y compara su valor con la talla


de cada zapato, conservando solo los zapatos de la talla especificada. Finalmente,
la llamada collectrecopila los valores devueltos por el iterador adaptado en un
vector que devuelve la función.

La prueba muestra que cuando llamamos shoes_in_size, solo obtenemos zapatos


que tienen el mismo tamaño que el valor que especificamos.

Mejorando nuestro proyecto de E/S


Con este nuevo conocimiento sobre iteradores, podemos mejorar el proyecto de
E/S del Capítulo 12, usándolos para que las partes del código sean más claras y
concisas. Veamos cómo los iteradores pueden mejorar nuestra implementación
de la Config::buildfunción y la searchfunción misma.

Eliminar un cloneuso de un iterador

En el Listado 12-6, añadimos código que tomaba una porción de Stringvalores y


creaba una instancia de la Configestructura indexándola en la porción y clonando
los valores, lo que permitía que la Configestructura los poseyera. En el Listado 13-
17, reproducimos la implementación de la Config::buildfunción tal como estaba en
el Listado 12-23:

Nombre de archivo: src/lib.rs


impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}

let query = args[1].clone();


let file_path = args[2].clone();

let ignore_case = env::var("IGNORE_CASE").is_ok();


Rust
jueves, 22 de mayo de 2025 : Página 373 de 719

Ok(Config {
query,
file_path,
ignore_case,
})
}
}
Listado 13-17: Reproducción de la Config::buildfunción del Listado 12-23

En aquel momento, dijimos que no nos preocupáramos por las clonellamadas


ineficientes, porque las eliminaríamos en el futuro. ¡Pues ese momento ha
llegado!

Lo necesitábamos cloneaquí porque tenemos una porción con Stringelementos en


el parámetro args, pero la buildfunción no posee args. Para devolver la propiedad
de una Configinstancia, tuvimos que clonar los valores de los campos query y
de para que la instancia pudiera poseer sus valores.file_pathConfigConfig

Con nuestros nuevos conocimientos sobre iteradores, podemos modificar


la buildfunción para que tome posesión de un iterador como argumento, en lugar
de tomar prestada una porción. Utilizaremos la funcionalidad del iterador en lugar
del código que verifica la longitud de la porción e indexa en ubicaciones
específicas. Esto aclarará la Config::buildfunción, ya que el iterador accederá a los
valores.

Una vez que Config::buildtomamos posesión del iterador y dejamos de usar


operaciones de indexación que toman prestado, podemos mover los Stringvalores
del iterador a Configen lugar de llamar cloney hacer una nueva asignación.

Usando el iterador devuelto directamente

Abra el archivo src/main.rs de su proyecto de E/S , que debería verse así:

Nombre de archivo: src/main.rs

fn main() {
let args: Vec<String> = env::args().collect();

let config = Config::build(&args).unwrap_or_else(|err| {


eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});

// --snip--
Rust
jueves, 22 de mayo de 2025 : Página 374 de 719
}

Primero, cambiaremos el inicio de la mainfunción del Listado 12-24 al código del


Listado 13-18, que esta vez usa un iterador. Esto no se compilará hasta
que Config::buildtambién lo actualicemos.

Nombre de archivo: src/main.rs

fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});

// --snip--
}
Listado 13-18: Pasando el valor de retorno de env::argsaConfig::build

¡La env::argsfunción devuelve un iterador! En lugar de recopilar los valores del


iterador en un vector y luego pasarle una porción Config::build, ahora pasamos
directamente la propiedad del iterador env::argsdevuelto Config::build.

A continuación, necesitamos actualizar la definición de . En el


archivo src/lib.rsConfig::build de tu proyecto de E/S , cambiemos la firma de para
que se parezca al Listado 13-19. Esto sigue sin compilar porque necesitamos
actualizar el cuerpo de la función.Config::build

Nombre de archivo: src/lib.rs

impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
Listado 13-19: Actualización de la firma de Config::buildpara esperar un iterador

La documentación de la biblioteca estándar para la env::argsfunción muestra que


el tipo del iterador que devuelve es std::env::Args, y ese tipo implementa
el Iteratorrasgo y devuelve Stringvalores.

Hemos actualizado la firma de la Config::buildfunción para que el


parámetro argstenga un tipo genérico con los límites del rasgo impl Iterator<Item =
String> en lugar de &[String]. Este uso de la impl Traitsintaxis que explicamos en la
Rust
jueves, 22 de mayo de 2025 : Página 375 de 719

sección "Rasgos como parámetros" del capítulo 10 significa que argspuede ser
cualquier tipo que implemente el Iteratorrasgo y devuelvaString elementos.

Dado que tomamos propiedad argsy mutaremos argsiterando sobre él, podemos
agregar la mutpalabra clave en la especificación del argsparámetro para hacerlo
mutable.

Uso de Iteratormétodos de rasgos en lugar de indexación

A continuación, corregiremos el cuerpo de Config::build. Como argsimplementa


el Iteratorrasgo, sabemos que podemos llamar al nextmétodo en él. El Listado 13-
20 actualiza el código del Listado 12-23 para usar el nextmétodo:

Nombre de archivo: src/lib.rs


impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();

let query = match args.next() {


Some(arg) => arg,
None => return Err("Didn't get a query string"),
};

let file_path = match args.next() {


Some(arg) => arg,
None => return Err("Didn't get a file path"),
};

let ignore_case = env::var("IGNORE_CASE").is_ok();

Ok(Config {
query,
file_path,
ignore_case,
})
}
}
Listado 13-20: Cambiar el cuerpo de Config::buildpara usar métodos iteradores

Recuerda que el primer valor en el valor de retorno de env::argses el nombre del


programa. Queremos ignorarlo y pasar al siguiente valor, así que primero
llamamos a nexty no hacemos nada con el valor de retorno. Segundo,
llamamos nextpara obtener el valor que queremos poner en el querycampo
de Config. Si nextdevuelve a Some, usamos a matchpara extraer el valor. Si
Rust
jueves, 22 de mayo de 2025 : Página 376 de 719

devuelve None, significa que no se proporcionaron suficientes argumentos y


regresamos antes con un Errvalor. Hacemos lo mismo con el file_pathvalor.

Cómo hacer el código más claro con adaptadores de iteradores

También podemos aprovechar los iteradores en la searchfunción en nuestro


proyecto de E/S, que se reproduce aquí en el Listado 13-21 como estaba en el
Listado 12-19:

Nombre de archivo: src/lib.rs


pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();

for line in contents.lines() {


if line.contains(query) {
results.push(line);
}
}

results
}
Listado 13-21: La implementación de la searchfunción del Listado 12-19

Podemos escribir este código de forma más concisa utilizando métodos de


adaptador de iterador. Esto también nos permite evitar un resultsvector
intermedio mutable. El estilo de programación funcional prefiere minimizar la
cantidad de estados mutables para que el código sea más claro. Eliminar el
estado mutable podría permitir una futura mejora para que las búsquedas se
realicen en paralelo, ya que no tendríamos que gestionar el acceso concurrente
al resultsvector. El listado 13-22 muestra este cambio:

Nombre de archivo: src/lib.rs


pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
Listado 13-22: Uso de métodos de adaptador de iterador en la implementación de la searchfunción

Recuerde que el propósito de la searchfunción es devolver todas las líneas


que contentscontienen el query. Similar al filterejemplo del Listado 13-16, este
código usa el filteradaptador para conservar solo las líneas
que line.contains(query)devuelven " truefor". Luego, recopilamos las líneas
Rust
jueves, 22 de mayo de 2025 : Página 377 de 719

coincidentes en otro vector con collect. ¡Mucho más simple! Puede hacer el mismo
cambio para usar métodos iterativos en la search_case_insensitivefunción.

Elegir entre bucles o iteradores

La siguiente pregunta lógica es qué estilo debería elegir en su código y por qué: la
implementación original del Listado 13-21 o la versión que usa iteradores del
Listado 13-22. La mayoría de los programadores de Rust prefieren usar el estilo
iterador. Al principio, es un poco más difícil de entender, pero una vez que se
familiariza con los distintos adaptadores de iteradores y su función, los iteradores
se vuelven más fáciles de entender. En lugar de manipular los distintos
fragmentos del bucle y crear nuevos vectores, el código se centra en el objetivo
general del bucle. Esto abstrae parte del código común, lo que facilita la
comprensión de los conceptos exclusivos de este código, como la condición de
filtrado que debe pasar cada elemento del iterador.

Pero ¿son ambas implementaciones realmente equivalentes? La suposición


intuitiva podría ser que el bucle de nivel más bajo será más rápido. Hablemos del
rendimiento.

Comparación del rendimiento: bucles e


iteradores
Para determinar si utilizar bucles o iteradores, necesita saber qué implementación
es más rápida: la versión de la searchfunción con un forbucle explícito o la versión
con iteradores.

Realizamos una prueba comparativa cargando todo el contenido de Las aventuras


de Sherlock Holmes de Sir Arthur Conan Doyle en un archivo Stringy buscando la
palabra "the" en el contenido. Estos son los resultados de la prueba comparativa
en la versión searchcon el forbucle y la versión con iteradores:

test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)


test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)

Las dos implementaciones tienen un rendimiento similar. No explicaremos el


código de referencia aquí, ya que el objetivo no es demostrar que las dos
versiones sean equivalentes, sino obtener una idea general de cómo se comparan
en rendimiento.
Rust
jueves, 22 de mayo de 2025 : Página 378 de 719

Para una evaluación comparativa más completa, deberías comprobar el uso de


textos de distintos tamaños como el [texto faltante] contents, diferentes palabras y
palabras de diferentes longitudes como el [ querytexto faltante], y todo tipo de
variaciones. La cuestión es la siguiente: los iteradores, aunque son una
abstracción de alto nivel, se compilan a aproximadamente el mismo código que si
hubieras escrito el código de nivel inferior tú mismo. Los iteradores son una de las
abstracciones de coste cero de Rust , lo que significa que su uso no supone
ninguna sobrecarga adicional en tiempo de ejecución. Esto es análogo a cómo
Bjarne Stroustrup, el diseñador e implementador original de C++, define la
sobrecarga cero en "Fundamentos de C++" (2012):

En general, las implementaciones de C++ siguen el principio de cero gastos


generales: lo que no se usa, no se paga. Y además: lo que se usa, no se puede
programar mejor a mano.

Como otro ejemplo, el siguiente código proviene de un decodificador de audio. El


algoritmo de decodificación utiliza la operación matemática de predicción lineal
para estimar valores futuros basándose en una función lineal de las muestras
anteriores. Este código utiliza una cadena de iteradores para realizar cálculos
matemáticos con tres variables dentro del alcance: una bufferporción de datos, un
array de 12 coefficientsy una cantidad para desplazar los datos en qlp_shift. Hemos
declarado las variables en este ejemplo, pero no les hemos asignado ningún
valor; aunque este código no tiene mucho significado fuera de su contexto, sigue
siendo un ejemplo conciso y práctico de cómo Rust traduce ideas de alto nivel a
código de bajo nivel.

let buffer: &mut [i32];


let coefficients: [i64; 12];
let qlp_shift: i16;

for i in 12..buffer.len() {
let prediction = coefficients.iter()
.zip(&buffer[i - 12..i])
.map(|(&c, &s)| c * s as i64)
.sum::<i64>() >> qlp_shift;
let delta = buffer[i];
buffer[i] = prediction as i32 + delta;
}

Para calcular el valor de prediction, este código itera sobre cada uno de los 12
valores de coefficientsy utiliza el zipmétodo para emparejar los valores de los
coeficientes con los 12 valores anteriores de buffer. Luego, para cada par,
Rust
jueves, 22 de mayo de 2025 : Página 379 de 719

multiplicamos los valores, sumamos todos los resultados y desplazamos los bits
de la suma qlp_shifta la derecha.

Los cálculos en aplicaciones como decodificadores de audio suelen priorizar el


rendimiento. Aquí, creamos un iterador, usamos dos adaptadores y luego
consumimos el valor. ¿A qué código ensamblador se compilaría este código de
Rust? Bueno, al momento de escribir esto, se compila al mismo ensamblado que
escribirías a mano. No hay ningún bucle correspondiente a la iteración sobre los
valores en coefficients: Rust sabe que hay 12 iteraciones, por lo que "desenrolla" el
bucle. El desenrollado es una optimización que elimina la sobrecarga del código
que controla el bucle y, en su lugar, genera código repetitivo para cada iteración
del bucle.

Todos los coeficientes se almacenan en registros, lo que significa que acceder a


los valores es muy rápido. No hay comprobaciones de límites en el acceso al array
en tiempo de ejecución. Todas estas optimizaciones que Rust puede aplicar hacen
que el código resultante sea extremadamente eficiente. Ahora que sabes esto,
¡puedes usar iteradores y cierres sin problemas! Hacen que el código parezca de
alto nivel, pero no suponen una penalización en el rendimiento en tiempo de
ejecución.

Resumen
Los cierres e iteradores son características de Rust inspiradas en las ideas de los
lenguajes de programación funcional. Contribuyen a la capacidad de Rust para
expresar con claridad ideas de alto nivel con un rendimiento de bajo nivel. Las
implementaciones de cierres e iteradores no afectan el rendimiento en tiempo de
ejecución. Esto forma parte del objetivo de Rust de proporcionar abstracciones de
coste cero.

Ahora que hemos mejorado la expresividad de nuestro proyecto de E/S, veamos


algunas características más que cargonos ayudarán a compartir el proyecto con el
mundo.

Más sobre Cargo y Crates.io


Hasta ahora, solo hemos usado las funciones más básicas de Cargo para compilar,
ejecutar y probar nuestro código, pero puede hacer mucho más. En este capítulo,
Rust
jueves, 22 de mayo de 2025 : Página 380 de 719

analizaremos algunas de sus funciones más avanzadas para mostrarte cómo


hacer lo siguiente:

 Personaliza tu compilación a través de perfiles de lanzamiento


 Publicar bibliotecas en crates.io
 Organice grandes proyectos con espacios de trabajo
 Instalar binarios desde crates.io
 Ampliar la carga mediante comandos personalizados

Cargo puede hacer incluso más que la funcionalidad que cubrimos en este
capítulo, por lo que para obtener una explicación completa de todas sus
características, consulte su documentación .

Personalización de compilaciones con perfiles de


lanzamiento
En Rust, los perfiles de lanzamiento son perfiles predefinidos y personalizables
con diferentes configuraciones que permiten al programador tener mayor control
sobre diversas opciones de compilación de código. Cada perfil se configura
independientemente de los demás.

Cargo tiene dos perfiles principales: el devque usa al ejecutar cargo buildy
el releaseque usa al ejecutar cargo build --release. El devperfil se define con valores
predeterminados adecuados para el desarrollo y el releaseperfil también para las
versiones de lanzamiento.

Es posible que estos nombres de perfil le resulten familiares a partir del resultado
de sus compilaciones:

$ cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build --release
Finished `release` profile [optimized] target(s) in 0.32s

Los devy releaseson estos diferentes perfiles utilizados por el compilador.

Cargo tiene configuraciones predeterminadas para cada perfil, que se aplican si


no se han agregado [profile.*]secciones explícitamente en
el archivo Cargo.toml[profile.*] del proyecto. Al agregar secciones para cualquier
perfil que desee personalizar, se anula cualquier subconjunto de las
Rust
jueves, 22 de mayo de 2025 : Página 381 de 719

configuraciones predeterminadas. Por ejemplo, estos son los valores


predeterminados para opt-levellos perfiles devy release:

Nombre del archivo: Cargo.toml

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

Esta opt-levelconfiguración controla la cantidad de optimizaciones que Rust


aplicará a tu código, con un rango de 0 a 3. Aplicar más optimizaciones prolonga
el tiempo de compilación, así que si estás en desarrollo y compilas tu código con
frecuencia, querrás menos optimizaciones para compilar más rápido, incluso si el
código resultante se ejecuta más lento. El valor predeterminado opt-
levelpara deves, por lo tanto 0, . Cuando estés listo para publicar tu código, es
mejor dedicar más tiempo a compilar. Solo compilarás en modo de lanzamiento
una vez, pero ejecutarás el programa compilado muchas veces, por lo que el
modo de lanzamiento sacrifica un mayor tiempo de compilación por un código
que se ejecuta más rápido. Por eso, el valor predeterminado opt-levelpara
el releaseperfil es 3.

Puedes anular una configuración predeterminada añadiéndole un valor diferente


en Cargo.toml . Por ejemplo, si queremos usar el nivel de optimización 1 en el
perfil de desarrollo, podemos añadir estas dos líneas al archivo Cargo.toml de
nuestro proyecto :

Nombre del archivo: Cargo.toml

[profile.dev]
opt-level = 1

Este código anula la configuración predeterminada de 0. Ahora, al ejecutar cargo


build, Cargo usará los valores predeterminados del devperfil, además de nuestra
personalización a opt-level. Al configurarlo opt-levelen 1, Cargo aplicará más
optimizaciones que la predeterminada, pero no tantas como en una versión de
lanzamiento.

Para obtener la lista completa de opciones de configuración y valores


predeterminados para cada perfil, consulte la documentación de Cargo .
Rust
jueves, 22 de mayo de 2025 : Página 382 de 719

Publicar una caja en Crates.io


Hemos usado paquetes de crates.io como dependencias de nuestro proyecto,
pero también puedes compartir tu código con otros publicando tus propios
paquetes. El registro de crates en crates.io. distribuye el código fuente de tus
paquetes, por lo que aloja principalmente código abierto.

Rust y Cargo cuentan con características que facilitan la búsqueda y el uso de tu


paquete publicado. A continuación, hablaremos de algunas de estas
características y luego explicaremos cómo publicar un paquete.

Cómo hacer comentarios útiles sobre la documentación

Documentar tus paquetes con precisión ayudará a otros usuarios a saber cómo y
cuándo usarlos, por lo que vale la pena dedicar tiempo a escribir la
documentación. En el Capítulo 3, explicamos cómo comentar el código de Rust
usando dos barras diagonales. //Rust también cuenta con un tipo particular de
comentario para la documentación, conocido como comentario de
documentación , que genera documentación HTML. El HTML muestra el contenido
de los comentarios de documentación para elementos de la API pública, dirigidos
a programadores interesados en saber cómo usar tu crate, en lugar de cómo
se implementa .

Los comentarios de documentación usan tres barras diagonales, ///en lugar de


dos, y admiten la notación Markdown para formatear el texto. Coloque los
comentarios de documentación justo antes del elemento que documentan. El
Listado 14-1 muestra los comentarios de documentación de una add_onefunción
en un crate llamado my_crate.

Nombre de archivo: src/lib.rs


/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x+1
}
Rust
jueves, 22 de mayo de 2025 : Página 383 de 719
Listado 14-1: Un comentario de documentación para una función

Aquí, describimos la add_onefunción, iniciamos una sección con el


encabezado Examplesy proporcionamos código que muestra cómo usarla add_one.
Podemos generar la documentación HTML a partir de este comentario ejecutando
` cargo doc. Este comando ejecuta la rustdocherramienta distribuida con Rust y
guarda la documentación HTML generada en el directorio `target/doc` .

Para mayor comodidad, ejecutar cargo doc --opengenerará el HTML de la


documentación de su crate actual (así como la documentación de todas sus
dependencias) y abrirá el resultado en un navegador web. Navegue hasta
la add_onefunción y verá cómo se procesa el texto en los comentarios de la
documentación, como se muestra en la Figura 14-1.

Figura 14-1: Documentación HTML de la add_one función

Secciones de uso común

Usamos el # Examplesencabezado Markdown del Listado 14-1 para crear una


sección en el HTML titulada "Ejemplos". Aquí hay otras secciones que los autores
de cajas suelen usar en su documentación:
Rust
jueves, 22 de mayo de 2025 : Página 384 de 719

 Pánicos : Escenarios en los que la función documentada podría entrar en


pánico. Quienes llaman a la función y no desean que sus programas entren
en pánico deben asegurarse de no llamarla 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 causar que se devuelvan esos
errores puede ser útil para quienes llaman para que puedan escribir código
para manejar los diferentes tipos de errores de diferentes maneras.
 Seguridad : si la función se unsafedebe llamar (analizamos la inseguridad
en el Capítulo 20), debe haber una sección que explique por qué la función
no es segura y que cubra las invariantes que la función espera que quienes
la llaman respeten.

La mayoría de los comentarios de la documentación no necesitan todas estas


secciones, pero esta es una buena lista de verificación para recordarle los
aspectos de su código que los usuarios estarán interesados en conocer.

Comentarios de documentación como pruebas

Añadir bloques de código de ejemplo en los comentarios de la documentación


puede ayudar a demostrar cómo usar la biblioteca, y además, tiene una ventaja:
¡al ejecutar, cargo testse ejecutarán los ejemplos de código de la documentación
como pruebas! Nada es mejor que la documentación con ejemplos. Pero nada es
peor que los ejemplos que no funcionan porque el código ha cambiado desde que
se escribió la documentación. Si ejecutamos cargo testcon la documentación de
la add_one función del Listado 14-1, veremos una sección en los resultados de la
prueba como esta:

Doc-tests my_crate

running 1 test
test src/lib.rs - add_one (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s

Ahora, si cambiamos la función o el ejemplo para que el assert_eq!ejemplo entre


en pánico y se ejecute cargo testnuevamente, veremos que las pruebas de
documentación detectan que el ejemplo y el código no están sincronizados entre
sí.

Cómo comentar elementos contenidos


Rust
jueves, 22 de mayo de 2025 : Página 385 de 719

El estilo de comentario de documentación //!añade documentación al elemento


que contiene los comentarios, en lugar de a los elementos posteriores.
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 describa el propósito de


la my_crate caja que contiene la add_onefunción, agregamos comentarios de
documentación que comienzan con //!al comienzo del archivo src/lib.rs , como se
muestra en el Listado 14-2:

Nombre de archivo: src/lib.rs


//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.

/// Adds one to the number given.


// --snip--
Listado 14-2: Documentación de la my_cratecaja en su conjunto

Observe que no hay código después de la última línea que empieza por //!. Como
empezamos los comentarios con //!en lugar de ///, documentamos el elemento que
contiene este comentario, no el elemento que lo sigue. En este caso, ese
elemento es el archivo src/lib.rs , que es la raíz del crate. Estos comentarios
describen el crate completo.

Cuando ejecutamos cargo doc --open, estos comentarios se mostrarán en la página


principal de la documentación my_crateencima de la lista de elementos públicos en
el paquete, como se muestra en la Figura 14-2:
Rust
jueves, 22 de mayo de 2025 : Página 386 de 719

Figura 14-2: Documentación renderizada para my_crate, incluido el comentario que describe
la caja en su totalidad

Los comentarios de documentación dentro de los elementos son útiles para


describir, especialmente, cajas y módulos. Úsalos para explicar el propósito
general del contenedor y ayudar a los usuarios a comprender la organización de
la caja.

Exportar una API pública conveniente conpub use

La estructura de tu API pública es un factor clave al publicar un crate. Quienes lo


usan están menos familiarizados con la estructura que tú y podrían tener
dificultades para encontrar las piezas que desean usar si tu crate tiene una
jerarquía de módulos extensa.

En el Capítulo 7, explicamos cómo hacer públicos los elementos mediante


la pubpalabra clave y cómo incluirlos en un ámbito con la usepalabra clave. Sin
embargo, la estructura que te resulta lógica al desarrollar un crate podría no ser
muy conveniente para tus usuarios. Quizás quieras organizar tus estructuras en
una jerarquía de varios niveles, pero quienes quieran usar un tipo definido en un
nivel profundo de la jerarquía podrían tener dificultades para descubrir su
existencia. También podrían sentirse molestos por tener que usar
`enter` use my_crate::some_module::another_module::UsefulType;en lugar
de use my_crate::UsefulType;`.

La buena noticia es que si la estructura no es conveniente para que otros la usen


desde otra biblioteca, no es necesario reorganizar la organización interna: en su
Rust
jueves, 22 de mayo de 2025 : Página 387 de 719

lugar, se pueden reexportar elementos para crear una estructura pública


diferente de la privada usando pub use. Al reexportar, un elemento público de una
ubicación se convierte en público en otra, como si estuviera definido en la otra.

Por ejemplo, supongamos que creamos una biblioteca llamada artpara modelar
conceptos artísticos. Esta biblioteca contiene dos módulos: un kindsmódulo con
dos enumeraciones llamadas PrimaryColory SecondaryColory un utilsmódulo con una
función llamada mix, como se muestra en el Listado 14-3:

Nombre de archivo: src/lib.rs


//! # Art
//!
//! A library for modeling artistic concepts.

pub mod kinds {


/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}

/// The secondary colors according to the RYB color model.


pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}

pub mod utils {


use crate::kinds::*;

/// Combines two primary colors in equal amounts to create


/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
}
}
Listado 14-3: Una artbiblioteca con elementos organizados kindsen utilsmódulos

cargo docLa figura 14-3 muestra cómo se vería la página principal de la


documentación para esta caja generada por :
Rust
jueves, 22 de mayo de 2025 : Página 388 de 719

Figura 14-3: Página principal de la documentación art que enumera los módulos kindsyutils

Tenga en cuenta que los tipos PrimaryColory SecondaryColorno aparecen en la


página principal, ni tampoco la mixfunción. Debemos hacer clic en kindsy utilspara
verlos.

Otra caja que depende de esta biblioteca necesitaría useinstrucciones que traigan
los elementos artal ámbito, especificando la estructura del módulo definida
actualmente. El listado 14-4 muestra un ejemplo de una caja que utiliza
los elementos ` PrimaryColory` de la caja:mixart

Nombre de archivo: src/main.rs


use art::kinds::PrimaryColor;
use art::utils::mix;

fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
Listado 14-4: Una caja que utiliza artlos elementos de la caja con su estructura interna exportada

El autor del código del Listado 14-4, que usa el artcrate, tuvo que averiguar
qué PrimaryColorestá en el kindsmódulo y mixqué está en el utilsmódulo. La
estructura del módulo del artcrate es más relevante para los desarrolladores que
trabajan en artél que para quienes lo usan. La estructura interna no contiene
información útil para quienes intentan entender cómo usar el artcrate, sino que
Rust
jueves, 22 de mayo de 2025 : Página 389 de 719

genera confusión, ya que los desarrolladores que lo usan deben averiguar dónde
buscar y especificar los nombres de los módulos en el... use instrucciones.

Para eliminar la organización interna de la API pública, podemos modificar


el artcódigo de la caja en el Listado 14-3 para agregar pub usedeclaraciones para
volver a exportar los elementos en el nivel superior, como se muestra en el
Listado 14-5:

Nombre de archivo: src/lib.rs


//! # Art
//!
//! A library for modeling artistic concepts.

pub use self::kinds::PrimaryColor;


pub use self::kinds::SecondaryColor;
pub use self::utils::mix;

pub mod kinds {


// --snip--
}

pub mod utils {


// --snip--
}
Listado 14-5: Cómo agregar pub usedeclaraciones para reexportar artículos

La documentación de API que cargo docse genera para esta caja ahora enumerará
y vinculará las reexportaciones en la página principal, como se muestra en la
Figura 14-4, lo que hará que los PrimaryColortipos SecondaryColory la mixfunción
sean más fáciles de encontrar.
Rust
jueves, 22 de mayo de 2025 : Página 390 de 719

Figura 14-4: La página principal de la documentación art que enumera las reexportaciones

Los artusuarios del cajón aún pueden ver y usar la estructura interna del Listado
14-3 como se muestra en el Listado 14-4, o pueden usar la estructura más
conveniente del Listado 14-5, como se muestra en el Listado 14-6:

Nombre de archivo: src/main.rs


use art::mix;
use art::PrimaryColor;

fn main() {
// --snip--
}
Listado 14-6: Un programa que utiliza los elementos reexportados de la artcaja

En casos con muchos módulos anidados, reexportar los tipos en el nivel


superior pub usepuede marcar una diferencia significativa en la experiencia de los
usuarios del crate. Otro uso común pub usees reexportar las definiciones de una
dependencia en el crate actual para que las definiciones de ese crate formen
parte de la API pública del crate.
Rust
jueves, 22 de mayo de 2025 : Página 391 de 719

Crear una estructura de API pública útil es más un arte que una ciencia, y puedes
iterar para encontrar la API que mejor se adapte a tus usuarios. Elegir una pub
usete da flexibilidad para estructurar tu crate internamente y desvincula esa
estructura interna de lo que presentas a tus usuarios. Revisa el código de los
crates que has instalado para ver si su estructura interna difiere de su API pública.

Configuración de una cuenta en Crates.io

Antes de publicar cualquier crate, necesitas crear una cuenta en crates.io y


obtener un token de API. Para ello, visita la página principal de crates.io e inicia
sesión con una cuenta de GitHub. (Actualmente, se requiere una cuenta de
GitHub, pero el sitio podría ofrecer otras opciones para crear una cuenta en el
futuro). Una vez iniciada la sesión, visita la configuración de tu cuenta
en https://crates.io/me/ y recupera tu clave de API. Luego, ejecuta el cargo
logincomando y pégala cuando se te solicite, como se muestra a continuación:

$ cargo login
abcdefghijklmnopqrstuvwxyz012345

Este comando informará a Cargo de su token de API y lo almacenará localmente


en ~/.cargo/credentials . Tenga en cuenta que este token es secreto : no lo
comparta con nadie más. Si lo comparte con alguien por cualquier motivo, debe
revocarlo y generar un nuevo token en crates.io. .

Agregar metadatos a una nueva caja

Supongamos que tienes una caja que quieres publicar. Antes de publicarla,
deberás agregar metadatos en la [package]sección del archivo Cargo.toml de la
caja .

Tu caja necesitará un nombre único. Mientras trabajas en una caja localmente,


puedes nombrarla como quieras. Sin embargo, los nombres de las cajas
en crates.io se asignan por orden de llegada. Una vez que se usa un nombre de
caja, nadie más puede publicar una caja con ese nombre. Antes de intentar
publicar una caja, busca el nombre que quieres usar. Si ya se ha usado, tendrás
que buscar otro nombre y editar el namecampo en el archivo Cargo.toml , en
la [package]sección correspondiente, para usar el nuevo nombre para la
publicación, como se muestra a continuación:

Nombre del archivo: Cargo.toml


Rust
jueves, 22 de mayo de 2025 : Página 392 de 719
[package]
name = "guessing_game"

Incluso si has elegido un nombre único, cuando intentes cargo publishpublicar el


paquete en este punto, recibirá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 (status 400 Bad Request): missing or empty
metadata fields: description, license. Please see
https://doc.rust-lang.org/cargo/reference/manifest.html for more information on configuring
these field

Este error se debe a que falta información crucial: se requieren una descripción y
una licencia para que los usuarios sepan qué hace tu crate y bajo qué condiciones
pueden usarlo. En Cargo.toml , añade una descripción de una o dos frases, ya que
aparecerá con tu crate en los resultados de búsqueda. Para este licensecampo,
debes proporcionar un valor de identificador de licencia . El Intercambio de Datos
de Paquetes de Software (SPDX) de la Fundación Linux enumera los
identificadores que puedes usar para este valor. Por ejemplo, para especificar que
has licenciado tu crate con la Licencia MIT, añade el MITidentificador:

Nombre del archivo: Cargo.toml

[package]
name = "guessing_game"
license = "MIT"

Si desea utilizar una licencia que no aparece en el SPDX, debe colocar el texto de
esa licencia en un archivo, incluir el archivo en su proyecto y luego usar license-
filepara especificar el nombre de ese archivo en lugar de usar la licenseclave.

Este libro no aborda la orientación sobre qué licencia es la adecuada para su


proyecto. Muchos miembros de la comunidad de Rust licencian sus proyectos de
la misma manera que Rust, utilizando una licencia dual de MIT OR Apache-2.0. Esta
práctica demuestra que también puede especificar varios identificadores de
licencia separados por ORpara tener varias licencias para su proyecto.
Rust
jueves, 22 de mayo de 2025 : Página 393 de 719

Con un nombre único, la versión, su descripción y una licencia agregada, el


archivo Cargo.toml de un proyecto que está listo para publicar podría verse así:

Nombre del archivo: Cargo.toml

[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]

La documentación de Cargo describe otros metadatos que puede especificar para


garantizar que otros puedan descubrir y usar su caja más fácilmente.

Publicación en Crates.io

Ahora que has creado una cuenta, guardado tu token de API, elegido un nombre
para tu crate y especificado los metadatos necesarios, ¡estás listo para publicar!
Al publicar un crate, se carga una versión específica en crates.io. para que otros
la usen.

Tenga cuidado, ya que 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 servir 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 imposibilitaría
cumplir este objetivo. Sin embargo, no hay límite en la cantidad de versiones de
crates que se pueden publicar.

Ejecute el cargo publishcomando de nuevo. Debería funcionar correctamente:

$ cargo publish
Updating crates.io index
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)

¡Felicitaciones! Ya compartiste tu código con la comunidad de Rust y cualquiera


puede agregar fácilmente tu crate como dependencia de su proyecto.
Rust
jueves, 22 de mayo de 2025 : Página 394 de 719

Publicar una nueva versión de un crate existente

Cuando hayas realizado cambios en tu crate y estés listo para publicar una nueva
versión, modifica el versionvalor especificado en tu archivo Cargo.toml y vuelve a
publicar. Usa las reglas de Versiones Semánticas para determinar el número de
versión apropiado según los cambios realizados. Luego, ejecuta el comando cargo
publishpara cargar la nueva versión.

Descontinuación de versiones de Crates.io concargo yank

Aunque no se pueden eliminar versiones anteriores de un crate, se puede evitar


que proyectos futuros las agreguen como una nueva dependencia. Esto es útil
cuando una versión del crate falla por algún motivo. En tales situaciones, Cargo
permite retirar una versión del crate.

Eliminar una versión impide que nuevos proyectos dependan de ella, a la vez que
permite que todos los proyectos existentes que dependen de ella continúen. En
esencia, eliminar una versión significa que todos los proyectos con
un Cargo.lock no se romperán, y los archivos Cargo.lock generados en el futuro no
usarán la versión eliminada.

Para extraer una versión de un crate, en el directorio del crate previamente


publicado, ejecute cargo yanky especifique la versión que desea extraer. Por
ejemplo, si publicamos un crate llamado guessing_gameversión 1.0.1 y queremos
extraerlo, en el directorio del proyecto guessing_gameejecutaríamos:

$ cargo yank --vers 1.0.1


Updating crates.io index
Yank [email protected]

Al agregar --undoal comando, también puede deshacer un yank y permitir que los
proyectos comiencen a depender de una versión nuevamente:

$ cargo yank --vers 1.0.1 --undo


Updating crates.io index
Unyank [email protected]

Un "yank" no elimina ningún código. Por ejemplo, no puede eliminar secretos


cargados accidentalmente. Si esto ocurre, debes restablecerlos inmediatamente.

Espacios de trabajo de carga


Rust
jueves, 22 de mayo de 2025 : Página 395 de 719

En el Capítulo 12, creamos un paquete que incluía un contenedor binario y un


contenedor de biblioteca. A medida que tu proyecto se desarrolla, podrías
observar que el contenedor de biblioteca crece cada vez más y que quieras dividir
tu paquete en varios contenedores de biblioteca. Cargo ofrece una función
llamada " espacios de trabajo" que permite gestionar varios paquetes
relacionados que se desarrollan simultáneamente.

Creación de un espacio de trabajo

Un espacio de trabajo es un conjunto de paquetes que comparten el


mismo Cargo.lock y el directorio de salida. Creemos un proyecto con un espacio
de trabajo; usaremos código simple para centrarnos en su estructura. Hay varias
maneras de estructurar un espacio de trabajo, así que solo mostraremos una
común. Tendremos un espacio de trabajo que contiene un binario y dos
bibliotecas. El binario, que proporcionará la funcionalidad principal, dependerá de
las dos bibliotecas. Una biblioteca proporcionará una add_onefunción y la otra
biblioteca una add_twofunción. Estos tres paquetes formarán parte del mismo
espacio de trabajo. Comenzaremos creando un nuevo directorio para el espacio
de trabajo:

$ mkdir add
$ cd add

A continuación, en el directorio "add" , creamos el archivo Cargo.toml que


configurará todo el espacio de trabajo. Este archivo no tendrá
ninguna [package]sección. En su lugar, comenzará con una [workspace]sección que
nos permitirá añadir miembros al espacio de trabajo. También nos aseguramos de
usar la versión más reciente y eficaz del algoritmo de resolución de Cargo en
nuestro espacio de trabajo, estableciendo el resolvervalor en "2".

Nombre del archivo: Cargo.toml

[workspace]
resolver = "2"

A continuación, crearemos el adderpaquete binario ejecutándolo cargo newdentro


del directorio de adición :

$ cargo new adder


Creating binary (application) `adder` package
Adding `adder` as member of workspace at `file:///projects/add`
Rust
jueves, 22 de mayo de 2025 : Página 396 de 719

Ejecutarse cargo newdentro de un espacio de trabajo también agrega


automáticamente el paquete recién creado a la membersclave en
la [workspace]definición en el espacio de trabajo Cargo.toml, de esta manera:

[workspace]
resolver = "2"
members = ["adder"]

En este punto, podemos crear el espacio de trabajo ejecutando cargo build. Los
archivos en el directorio de adición deberían verse así:

├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target

El espacio de trabajo tiene un directorio de destino en el nivel superior donde se


colocarán los artefactos compilados; el adderpaquete no tiene su propio
directorio de destino . Incluso si se ejecutara cargo builddesde el directorio adder ,
los artefactos compilados terminarían en add/target en lugar
de add/adder/target . Cargo estructura el directorio de destino en un espacio de
trabajo de esta manera porque los crates de un espacio de trabajo dependen
entre sí. Si cada crate tuviera su propio directorio de destino , cada crate tendría
que recompilar cada uno de los demás crates del espacio de trabajo para colocar
los artefactos en su propio directorio de destino . Al compartir un directorio de
destino , los crates pueden evitar recompilaciones innecesarias.

Creación del segundo paquete en el espacio de trabajo

A continuación, crearemos otro paquete miembro en el espacio de trabajo y lo


llamaremos . Modifiquemos el archivo Cargo.tomladd_one de nivel superior para
especificar la ruta add_one en la lista:members

Nombre del archivo: Cargo.toml

[workspace]
resolver = "2"
members = ["adder", "add_one"]

Luego genere una nueva biblioteca llamada add_one:


Rust
jueves, 22 de mayo de 2025 : Página 397 de 719
$ cargo new add_one --lib
Creating library `add_one` package
Adding `add_one` as member of workspace at `file:///projects/add`

Su directorio agregado ahora debería tener estos directorios y archivos:

├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target

En el archivo add_one/src/lib.rs , agreguemos una add_onefunción:

Nombre de archivo: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {


x+1
}

Ahora podemos tener el adderpaquete con nuestra dependencia binaria en


el add_one paquete que contiene nuestra biblioteca. Primero, debemos agregar
una dependencia de ruta add_onea adder/Cargo.toml .

Nombre del archivo: adder/Cargo.toml

[dependencies]
add_one = { path = "../add_one" }

Cargo no asume que las cajas en un espacio de trabajo dependerán unas de


otras, por lo que debemos ser explícitos sobre las relaciones de dependencia.

A continuación, usemos la add_onefunción (del add_onecrate) en el addercrate. Abra


el archivo adder/src/main.rs y modifique la main función para que llame a
la add_onefunción, como se muestra en el Listado 14-7.

Nombre de archivo: adder/src/main.rs


fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
Rust
jueves, 22 de mayo de 2025 : Página 398 de 719
Listado 14-7: Uso de la add_onecaja de la biblioteca en la addercaja

¡Construyamos el espacio de trabajo ejecutándolo en el directorio de adicióncargo


build de nivel superior !

$ cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s

Para ejecutar el paquete binario desde el directorio de adición , podemos


especificar qué paquete en el espacio de trabajo queremos ejecutar usando el -
pargumento y el nombre del paquete con cargo run:

$ cargo run -p adder


Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!

Esto ejecuta el código en adder/src/main.rs , que depende de la add_onecaja.

Depender de un paquete externo en un espacio de trabajo

Tenga en cuenta que el espacio de trabajo solo tiene un archivo Cargo.lock en el


nivel superior, en lugar de tener un Cargo.lock en el directorio de cada cajón. Esto
garantiza que todos los cajones usen la misma versión de todas las dependencias.
Si añadimos el rand paquete a los
archivos adder/Cargo.toml y add_one/Cargo.toml , Cargo resolverá ambos a una
sola versión randy la registrará en un único archivo Cargo.lock . Al hacer que todos
los cajones del espacio de trabajo usen las mismas dependencias, siempre serán
compatibles entre sí. Añadamos el randcajón a la [dependencies]sección del
archivo add_one/Cargo.toml para poder usarlo randdentro del add_onecajón:

Nombre del archivo: add_one/Cargo.toml

[dependencies]
rand = "0.8.5"

Ahora podemos agregar use rand;al archivo add_one/src/lib.rs , y al crear todo el


espacio de trabajo desde cargo buildel directorio add , se incorporará y compilará
el randpaquete. Recibiremos una advertencia porque no nos referimos a lo
que randincluimos en el ámbito:

$ cargo build
Rust
jueves, 22 de mayo de 2025 : Página 399 de 719
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
--> add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default

warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1
suggestion)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s

El archivo Cargo.lock de nivel superior ahora contiene información sobre la


dependencia de add_oneon rand. Sin embargo, aunque randse usa en algún lugar
del espacio de trabajo, no podemos usarlo en otros crates del espacio de trabajo a
menos que también lo agreguemos randa sus archivos Cargo.toml . Por ejemplo, si
lo agregamos use rand; al archivo adder/src/main.rs del adderpaquete,
obtendremos un error:

$ 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, edite el archivo Cargo.toml del adderpaquete e indique


que randtambién es una dependencia. Al compilar el adderpaquete, se
añadirá randa la lista de dependencias adderde Cargo.lock , pero no se
descargarán copias adicionales de [nombre del paquete rand]. Cargo garantizará
que todos los crates de cada paquete del espacio de trabajo que randlo utilicen
usen la misma versión, siempre que se especifiquen versiones compatibles de
[nombre del paquete] rand. Esto nos ahorra espacio y garantiza la compatibilidad
entre los crates del espacio de trabajo.

Si las cajas en el espacio de trabajo especifican versiones incompatibles de la


misma dependencia, Cargo resolverá cada una de ellas, pero intentará resolver la
menor cantidad de versiones posible.
Rust
jueves, 22 de mayo de 2025 : Página 400 de 719
Agregar una prueba a un espacio de trabajo

Para otra mejora, agreguemos una prueba de la add_one::add_onefunción dentro


del add_onepaquete:

Nombre de archivo: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {


x+1
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
assert_eq!(3, add_one(2));
}
}

Ahora, ejecútelo en el directorio de adicióncargo test de nivel superior . Al


ejecutarlo en un espacio de trabajo con esta estructura, se ejecutarán las pruebas
para todos los contenedores del espacio de trabajo: cargo test

$ cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
Running unittests src/lib.rs (target/debug/deps/add_one-f0253159197f7841)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running unittests src/main.rs (target/debug/deps/adder-49979ff40686fa8e)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

La primera sección del resultado muestra que la it_worksprueba en la add_one caja


se aprobó. La siguiente sección muestra que no se encontraron pruebas en
Rust
jueves, 22 de mayo de 2025 : Página 401 de 719

la adder caja, y la última sección muestra que no se encontraron pruebas de


documentación en la add_onecaja.

También podemos ejecutar pruebas para una caja en particular en un espacio de


trabajo desde el directorio de nivel superior usando la -pbandera y especificando
el nombre de la caja que queremos probar:

$ cargo test -p add_one


Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/add_one-b3235fea9a156f74)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Esta salida muestra cargo testque solo se ejecutaron las pruebas para
la add_onecaja y no se ejecutaron las adderpruebas de la caja.

Si publicas las cajas en el espacio de trabajo en crates.io , cada caja deberá


publicarse por separado. Por ejemplo cargo test, podemos publicar una caja
específica en nuestro espacio de trabajo usando la -p bandera y especificando el
nombre de la caja que queremos publicar.

¡Para practicar más, agrega una add_twocaja a este espacio de trabajo de manera
similar a la add_onecaja!

A medida que tu proyecto crezca, considera usar un espacio de trabajo: es más


fácil comprender componentes individuales más pequeños que un gran bloque de
código. Además, mantener los contenedores en un espacio de trabajo facilita la
coordinación entre ellos si se modifican con frecuencia al mismo tiempo.

Instalación de binarios concargo install


El cargo installcomando permite instalar y usar crates binarios localmente. Esto no
pretende reemplazar los paquetes del sistema; es una forma conveniente para
que los desarrolladores de Rust instalen herramientas que otros han compartido
Rust
jueves, 22 de mayo de 2025 : Página 402 de 719

en crates.io . Tenga en cuenta que solo puede instalar paquetes con destinos
binarios. Un destino binario es el programa ejecutable que se crea si el crate tiene
un archivo src/main.rs u otro archivo especificado como binario, a diferencia de
un destino de biblioteca que no es ejecutable por sí solo, pero es adecuado para
incluirlo en otros programas. Normalmente, los crates tienen información en el
archivo README. de los crates indica si son bibliotecas, tienen un destino binario
o ambos.

Todos los binarios instalados con se almacenan en la carpeta bincargo install de la


raíz de instalación . Si instalaste Rust con rustup.rs y no tienes configuraciones
personalizadas, este directorio será $HOME/.cargo/bin . Asegúrate de que este
directorio esté en tu directorio para poder ejecutar los programas que instalaste
con .$PATHcargo install

Por ejemplo, en el Capítulo 12 mencionamos que existe una implementación en


Rust de la grepherramienta llamada ripgreppara buscar archivos. Para
instalarla ripgrep, podemos ejecutar lo siguiente:

$ cargo install ripgrep


Updating crates.io index
Downloaded ripgrep v13.0.0
Downloaded 1 crate (243.3 KB) in 0.88s
Installing ripgrep v13.0.0
--snip--
Compiling ripgrep v13.0.0
Finished `release` profile [optimized + debuginfo] target(s) in 10.64s
Installing ~/.cargo/bin/rg
Installed package `ripgrep v13.0.0` (executable `rg`)

La penúltima línea de la salida muestra la ubicación y el nombre del binario


instalado, que en el caso de ripgrepes rg. Si el directorio de instalación está en
tu $PATH, como se mencionó anteriormente, puedes ejecutar rg --helpy empezar a
usar una herramienta más rápida y robusta para buscar archivos.

Ampliación de carga con comandos


personalizados
Cargo está diseñado para que puedas ampliarlo con nuevos subcomandos sin
tener que modificarlo. Si un binario en tu $PATHse llama cargo-something, puedes
ejecutarlo como si fuera un subcomando de Cargo ejecutando cargo something.
Comandos personalizados como este también se listan al ejecutar cargo --list.
Poder usar cargo installpara instalar extensiones y luego ejecutarlas como las
Rust
jueves, 22 de mayo de 2025 : Página 403 de 719

herramientas integradas de Cargo es una ventaja muy práctica del diseño de


Cargo.

Resumen
Compartir código con Cargo y crates.io es parte de lo que hace que el ecosistema
de Rust sea útil para diversas tareas. La biblioteca estándar de Rust es pequeña y
estable, pero los crates son fáciles de compartir, usar y mejorar en un plazo
diferente al del lenguaje. No dudes en compartir código que te sea útil
en crates.io ; ¡es probable que también le sea útil a alguien más!

Punteros inteligentes
Un puntero es un concepto general para una variable que contiene una dirección
en memoria. Esta dirección se refiere, o "apunta", a otros datos. El tipo de
puntero más común en Rust es una referencia, que aprendiste en el Capítulo 4.
Las referencias se indican con el &símbolo y toman prestado el valor al que
apuntan. No tienen ninguna función especial aparte de referirse a datos y no
tienen sobrecarga.

Los punteros inteligentes , por otro lado, son estructuras de datos que actúan
como un puntero, pero que también cuentan con metadatos y capacidades
adicionales. El concepto de punteros inteligentes no es exclusivo de Rust: se
originaron en C++ y también existen en otros lenguajes. Rust cuenta con diversos
punteros inteligentes definidos en la biblioteca estándar que ofrecen
funcionalidades adicionales a las de las referencias. Para explorar el concepto
general, analizaremos un par de ejemplos de punteros inteligentes, incluyendo un
tipo de puntero inteligente con conteo de referencias . Este puntero permite que
los datos tengan múltiples propietarios, controlando su número y, cuando no
quedan propietarios, limpiando los datos.

Rust, con su concepto de propiedad y préstamo, tiene una diferencia adicional


entre referencias y punteros inteligentes: mientras que las referencias solo toman
prestados datos, en muchos casos, los punteros inteligentes poseen los datos a
los que apuntan.

Aunque no los llamamos así en su momento, ya hemos encontrado algunos


punteros inteligentes en este libro, incluyendo Stringy Vec<T>en el capítulo 8.
Rust
jueves, 22 de mayo de 2025 : Página 404 de 719

Ambos tipos se consideran punteros inteligentes porque poseen memoria y


permiten manipularla. También tienen metadatos y capacidades o garantías
adicionales. String, por ejemplo, almacena su capacidad como metadatos y tiene
la capacidad adicional de garantizar que sus datos siempre sean UTF-8 válidos.

Los punteros inteligentes suelen implementarse mediante estructuras. A


diferencia de una estructura común, los punteros inteligentes implementan
los rasgos Derefy . El rasgo permite que una instancia de la estructura del puntero
inteligente se comporte como una referencia, lo que permite escribir código para
que funcione con referencias o punteros inteligentes. El rasgo permite
personalizar el código que se ejecuta cuando una instancia del puntero inteligente
queda fuera del ámbito. En este capítulo, analizaremos ambos rasgos y
demostraremos su importancia para los punteros inteligentes. DropDerefDrop

Dado que el patrón de puntero inteligente es un patrón de diseño general que se


usa con frecuencia en Rust, este capítulo no cubrirá todos los punteros
inteligentes existentes. Muchas bibliotecas tienen sus propios punteros
inteligentes, e incluso puedes crear los tuyos propios. Abordaremos los punteros
inteligentes más comunes en la biblioteca estándar:

 Box<T>para asignar valores en el montón


 Rc<T>, un tipo de conteo de referencias que permite la propiedad múltiple
 Ref<T>y RefMut<T>, al que se accede a través de RefCell<T>, un tipo que
aplica las reglas de préstamo en tiempo de ejecución en lugar de en tiempo
de compilación

Además, abordaremos el patrón de mutabilidad interna , donde un tipo inmutable


expone una API para mutar un valor interno. También analizaremos los ciclos de
referencia. : cómo pueden causar fugas de memoria y cómo prevenirlas.

¡Vamos a sumergirnos!

Uso Box<T> para señalar datos en el montón


El puntero inteligente más sencillo es una caja , cuyo tipo se escribe Box<T>. Las
cajas permiten almacenar datos en el montón en lugar de en la pila. Lo que
permanece en la pila es el puntero a los datos del montón. Consulte el Capítulo 4
para revisar la diferencia entre la pila y el montón.
Rust
jueves, 22 de mayo de 2025 : Página 405 de 719

Las cajas no tienen sobrecarga de rendimiento, salvo que almacenan sus datos en
el montón en lugar de en la pila. Sin embargo, tampoco ofrecen muchas funciones
adicionales. Se utilizan con mayor frecuencia en estas situaciones:

 Cuando tienes un tipo cuyo tamaño no se puede conocer en tiempo de


compilación y quieres usar un valor de ese tipo en un contexto que requiere
un tamaño exacto
 Cuando tiene una gran cantidad de datos y desea transferir la propiedad,
pero se asegura de que los datos no se copien al hacerlo
 Cuando quieres poseer un valor y solo te importa que sea un tipo que
implementa un rasgo particular en lugar de ser de un tipo específico

Demostraremos la primera situación en la sección "Habilitación de tipos


recursivos con cajas" . En el segundo caso, transferir la propiedad de una gran
cantidad de datos puede tardar mucho tiempo, ya que estos se copian en la pila.
Para mejorar el rendimiento en esta situación, podemos almacenar la gran
cantidad de datos del montón en una caja. De esta manera, solo se copia en la
pila una pequeña cantidad de datos del puntero, mientras que los datos a los que
hace referencia permanecen en un solo lugar en el montón. El tercer caso se
conoce como objeto de rasgo , y el capítulo 18 dedica una sección completa, "Uso
de objetos de rasgo que admiten valores de diferentes tipos", a este tema. ¡Lo
que aprenda aquí lo aplicará de nuevo en el capítulo 18!

Uso de a Box<T>para almacenar datos en el montón

Antes de analizar el caso de uso de almacenamiento en montón para Box<T>,


cubriremos la sintaxis y cómo interactuar con los valores almacenados dentro de
un Box<T>.

El listado 15-1 muestra cómo usar un cuadro para almacenar un i32valor en el


montón:

Nombre de archivo: src/main.rs


fn main() {
let b = Box::new(5);
println!("b = {b}");
}
Listado 15-1: Almacenamiento de un i32valor en el montón mediante una caja

Definimos la variable bcon el valor de a Boxque apunta al valor 5, que está


asignado en el montón. Este programa imprimirá b = 5; en este caso, podemos
Rust
jueves, 22 de mayo de 2025 : Página 406 de 719

acceder a los datos de la caja de forma similar a como lo haríamos si estuvieran


en la pila. Al igual que cualquier valor propio, cuando una caja sale del ámbito,
como bocurre al final de main, se desasignará. La desasignación se produce tanto
para la caja (almacenada en la pila) como para los datos a los que apunta
(almacenados en el montón).

Colocar un solo valor en el montón no es muy útil, por lo que no se usarán las
cajas por sí solas de esta manera muy a menudo. Tener valores como uno
solo i32en la pila, donde se almacenan por defecto, es más apropiado en la
mayoría de las situaciones. Veamos un caso donde las cajas nos permiten definir
tipos que no podríamos definir sin ellas.

Habilitación de tipos recursivos con cuadros

Un valor de tipo recursivo puede tener otro valor del mismo tipo como parte de sí
mismo. Los tipos recursivos plantean un problema porque, en tiempo de
compilación, Rust necesita saber cuánto espacio ocupa un tipo. Sin embargo, la
anidación de valores de tipos recursivos podría, en teoría, continuar infinitamente,
por lo que Rust no puede saber cuánto espacio necesita el valor. Dado que las
cajas tienen un tamaño conocido, podemos habilitar los tipos recursivos
insertando una caja en la definición del tipo recursivo.

Como ejemplo de un tipo recursivo, exploremos la lista de cons . Este tipo de dato
se encuentra comúnmente en lenguajes de programación funcional. El tipo de
lista de cons que definiremos es simple, salvo por la recursión; por lo tanto, los
conceptos del ejemplo que utilizaremos serán útiles en situaciones más complejas
que involucren tipos recursivos.

Más información sobre la lista de desventajas

Una lista enlazada (cons) es una estructura de datos del lenguaje de


programación Lisp y sus dialectos, compuesta por pares anidados. Es la versión
Lisp de una lista enlazada. Su nombre proviene de la consfunción (abreviatura de
"construct function") en Lisp que construye un nuevo par a partir de sus dos
argumentos. Al invocar consun par compuesto por un valor y otro par, podemos
construir listas enlazadas compuestas por pares recursivos.

Por ejemplo, aquí hay una representación en pseudocódigo de una lista de


contras que contiene la lista 1, 2, 3 con cada par entre paréntesis:
Rust
jueves, 22 de mayo de 2025 : Página 407 de 719
(1, (2, (3, Nil)))

Cada elemento de una lista de cons contiene dos elementos: el valor del
elemento actual y el siguiente. El último elemento de la lista contiene solo un
valor llamado, Nil sin un elemento siguiente. Una lista de cons se genera mediante
una llamada recursiva a la cons función. El nombre canónico para el caso base de
la recursión es Nil. Tenga en cuenta que esto no es lo mismo que el concepto
"null" o "nil" del Capítulo 6, que representa un valor inválido o ausente.

La lista de contras no es una estructura de datos común en Rust. Generalmente,


al tener una lista de elementos en Rust, Vec<T>es una mejor opción. Otros tipos
de datos recursivos más complejos son útiles en diversas situaciones, pero al
comenzar con la lista de contras en este capítulo, podemos explorar cómo las
cajas nos permiten definir un tipo de dato recursivo sin mayor distracción.

El listado 15-2 contiene la definición de una enumeración para una lista de cons.
Tenga en cuenta que este código aún no se compila porque el Listtipo no tiene un
tamaño conocido, lo cual demostraremos.

Nombre de archivo: src/main.rs

enum List {
Cons(i32, List),
Nil,
}
Listado 15-2: El primer intento de definir una enumeración para representar una estructura de datos de
lista de i32valores de cons

i32Nota: Para este ejemplo, estamos implementando una lista de cons que solo contiene valores.
Podríamos haberla implementado usando genéricos, como se explicó en el capítulo 10, para definir un
tipo de lista de cons que pudiera almacenar valores de cualquier tipo.

El uso del Listtipo para almacenar la lista 1, 2, 3se vería como el código del Listado
15-3:

Nombre de archivo: src/main.rs

use crate::List::{Cons, Nil};

fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
Listado 15-3: Uso de la Listenumeración para almacenar la lista1, 2, 3
Rust
jueves, 22 de mayo de 2025 : Página 408 de 719

El primer Consvalor se cumple 1y otro Listvalor. Este Listvalor es otro Consvalor que
se cumple 2y otro Listvalor. Este Listvalor es otro Consvalor que se cumple 3y
un Listvalor, que es finalmente Nil, la variante no recursiva que indica el final de la
lista.

Si intentamos compilar el código del Listado 15-3, obtenemos el error que se


muestra en el Listado 15-4:

Nombre del archivo: output.txt


$ 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>),
| ++++ +

error[E0391]: cycle detected when computing when `List` needs drop


--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
|
= note: ...which immediately requires computing when `List` needs drop again
= note: cycle used when computing whether `List` needs drop
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-
dev-guide.rust-lang.org/query.html for more information

Some errors have detailed explanations: E0072, E0391.


For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
Listado 15-4: El error que obtenemos al intentar definir una enumeración recursiva

El error indica que este tipo tiene un tamaño infinito. Esto se debe a que lo hemos
definido Listcon una variante recursiva: contiene directamente otro valor de sí
mismo. Como resultado, Rust no puede determinar cuánto espacio necesita para
almacenar un Listvalor. Analicemos por qué aparece este error. Primero, veremos
cómo Rust determina cuánto espacio necesita para almacenar un valor de un tipo
no recursivo.

Cálculo del tamaño de un tipo no recursivo


Rust
jueves, 22 de mayo de 2025 : Página 409 de 719

Recuerde la Messageenumeración que definimos en el Listado 6-2 cuando


analizamos las definiciones de enumeración en el Capítulo 6:

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

Para determinar cuánto espacio asignar a un Messagevalor, Rust analiza cada


variante para ver cuál necesita más espacio. Rust detecta que Message::Quitno
necesita espacio, que Message::Movenecesita suficiente espacio para almacenar
dos i32valores, y así sucesivamente. Dado que solo se usará una variante, cuanto
más espacio...Message valor necesitará es el que ocuparía para almacenar la
mayor de sus variantes.

Compare esto con lo que ocurre cuando Rust intenta determinar cuánto
espacio Listnecesita un tipo recursivo como la enumeración del Listado 15-2. El
compilador comienza examinando la Consvariante, que contiene un valor de
tipo i32y un valor de tipo List. Por lo tanto, Consnecesita una cantidad de espacio
igual al tamaño de an i32más el tamaño de a List. Para determinar cuánta
memoria List necesita el tipo, el compilador examina las variantes, comenzando
por la Cons variante. La Consvariante contiene un valor de tipo i32y un valor de
tipo List, y este proceso continúa infinitamente, como se muestra en la Figura 15-
1.

Figura 15-1: Un infinito Listque consta de infinitas Consvariantes

UsandoBox<T> para obtener un tipo recursivo con un tamaño conocido

Como Rust no puede determinar cuánto espacio asignar para los tipos definidos
de forma recursiva, el compilador arroja un error con esta útil sugerencia:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2| Cons(i32, Box<List>),
| ++++ +
Rust
jueves, 22 de mayo de 2025 : Página 410 de 719

En esta sugerencia, “indirección” significa que en lugar de almacenar un valor


directamente, deberíamos cambiar la estructura de datos para almacenar el valor
indirectamente almacenando en su lugar un puntero al valor.

Como a Box<T>es un puntero, Rust siempre sabe cuánto espacio Box<T> necesita:
el tamaño de un puntero no cambia según la cantidad de datos a los que apunta.
Esto significa que podemos colocar a Box<T>dentro de la Consvariante en lugar de
otro Listvalor directamente. Box<T>Apuntará al siguiente List valor que estará en
el montón, en lugar de dentro de la Consvariante. Conceptualmente, seguimos
teniendo una lista, creada con listas que contienen otras listas, pero esta
implementación ahora se asemeja más a colocar los elementos uno al lado del
otro en lugar de uno dentro del otro.

Podemos cambiar la definición de la Listenumeración en el Listado 15-2 y el uso


del mismo Listen el Listado 15-3 al código en el Listado 15-5, que compilará:

Nombre de archivo: src/main.rs


enum List {
Cons(i32, Box<List>),
Nil,
}

use crate::List::{Cons, Nil};

fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Listado 15-5: Definición de Listque utiliza Box<T>para tener un tamaño conocido

La Consvariante necesita el tamaño de un objeto i32más el espacio para


almacenar los datos del puntero de la caja. La Nilvariante no almacena valores,
por lo que necesita menos espacio que la Consvariante. Ahora sabemos que
cualquier Listvalor ocupará el tamaño de un objeto i32más el tamaño de los datos
del puntero de una caja. Al usar una caja, hemos roto la cadena recursiva infinita,
de modo que el compilador puede determinar el tamaño necesario para
almacenar un Listvalor. La Figura 15-2 muestra el Consaspecto actual de la
variante.

Figura 15-2: Un Listque no tiene un tamaño infinito porque Conscontiene unBox


Rust
jueves, 22 de mayo de 2025 : Página 411 de 719

Las cajas solo proporcionan indirección y asignación de montón; no tienen otras


capacidades especiales, como las que veremos con los otros tipos de punteros
inteligentes. Tampoco tienen la sobrecarga de rendimiento que estas capacidades
especiales implican, por lo que pueden ser útiles en casos como la lista de
contras, donde la indirección es la única característica que necesitamos. Veremos
más casos de uso para las cajas en el Capítulo 18.

El Box<T>tipo es un puntero inteligente porque implementa la Derefcaracterística,


que permite Box<T>que los valores se traten como referencias. Cuando
un Box<T> valor queda fuera del ámbito, los datos del montón a los que apunta la
caja también se limpian gracias a la Dropimplementación de la característica.
Estas dos características serán aún más importantes para la funcionalidad de los
demás tipos de puntero inteligente que analizaremos en el resto de este capítulo.
Analicemos estas dos características con más detalle.

Tratar los punteros inteligentes como


referencias regulares con el Derefrasgo
Implementar el Derefrasgo permite personalizar el comportamiento del operador
de desreferencia * (no confundir con el operador de multiplicación o glob). Al
implementarDeref de forma que un puntero inteligente se considere una
referencia normal, se puede escribir código que opere con referencias y usarlo
también con punteros inteligentes.

Primero, veamos cómo funciona el operador de desreferencia con referencias


regulares. Después, intentaremos definir un tipo personalizado que se comporte
como Box<T>y veremos por qué el operador de desreferencia no funciona como
una referencia en nuestro tipo recién definido. Exploraremos cómo la
implementación de este Derefatributo permite que los punteros inteligentes
funcionen de forma similar a las referencias. Después, analizaremos la función de
conversión de desreferencia de Rust y cómo nos permite trabajar con referencias
o punteros inteligentes.

Nota: Hay una gran diferencia entre el MyBox<T>tipo que vamos a construir y el real Box<T>:
nuestra versión no almacenará sus datos en el montón. En este ejemplo, nos centramos en Deref, por lo
que el lugar donde se almacenan los datos es menos importante que el comportamiento similar al de un
puntero.

Siguiendo el puntero hacia el valor


Rust
jueves, 22 de mayo de 2025 : Página 412 de 719

Una referencia regular es un tipo de puntero, y una forma de pensar en un


puntero es como una flecha que apunta a un valor almacenado en otro lugar. En
el Listado 15-6, creamos una referencia a un i32valor y luego usamos el operador
de desreferencia para seguir la referencia al valor:

Nombre de archivo: src/main.rs


fn main() {
let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);
}
Listado 15-6: Uso del operador de desreferencia para seguir una referencia a un i32valor

La variable xcontiene un i32valor 5. Establecemos que ysea igual a una referencia


a x. Podemos afirmar que xes igual a 5. Sin embargo, si queremos hacer una
afirmación sobre el valor en y, debemos usar *ypara seguir la referencia al valor al
que apunta (de ahí desreferenciar ) para que el compilador pueda comparar el
valor real. Una vez desreferenciada y, tenemos acceso al valor entero yal que
apunta, con el que podemos comparar 5.

Si intentáramos escribir assert_eq!(5, y);en su lugar, obtendríamos este error de


compilación:

$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6| assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-
backtrace for more info)
help: consider dereferencing here
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/macros/
mod.rs:46:35
|
46| if !(*left_val == **right_val) {
| +

For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 413 de 719

No se permite comparar un número con una referencia a un número porque son


de tipos diferentes. Debemos usar el operador de desreferencia para seguir la
referencia hasta el valor al que apunta.

Usando Box<T>como referencia

Podemos reescribir el código del Listado 15-6 para utilizar a Box<T>en lugar de
una referencia; el operador de desreferencia utilizado en el Box<T>Listado 15-7
funciona de la misma manera que el operador de desreferencia utilizado en la
referencia en el Listado 15-6:

Nombre de archivo: src/main.rs


fn main() {
let x = 5;
let y = Box::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}
Listado 15-7: Uso del operador de desreferencia en unBox<i32>

La principal diferencia entre el Listado 15-7 y el Listado 15-6 es que aquí


configuramos ycomo una instancia de a que Box<T>apunta a un valor copiado
de xen lugar de una referencia que apunta al valor de x. En la última aserción,
podemos usar el operador de desreferencia para seguir el puntero de
the Box<T>de la misma manera que lo hicimos cuando ywas era una referencia. A
continuación, exploraremos las características especiales de Box<T> que nos
permite usar el operador de desreferencia definiendo nuestro propio tipo.

Definiendo nuestro propio puntero inteligente

Construyamos un puntero inteligente similar al Box<T>tipo que proporciona la


biblioteca estándar para experimentar cómo se comportan de forma diferente a
las referencias predeterminadas. Después, veremos cómo añadir la capacidad de
usar el operador de desreferencia.

El Box<T>tipo se define básicamente como una estructura de tupla con un


elemento, por lo que el Listado 15-8 define un MyBox<T>tipo de la misma manera.
También definiremos una newfunción que coincida con la newfunción definida
en Box<T>.

Nombre de archivo: src/main.rs


struct MyBox<T>(T);
Rust
jueves, 22 de mayo de 2025 : Página 414 de 719

impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
Listado 15-8: Definición de un MyBox<T>tipo

Definimos una estructura llamada MyBoxy declaramos un parámetro genérico T,


ya que queremos que nuestro tipo contenga valores de cualquier tipo.
El MyBoxtipo es una estructura de tupla con un elemento de tipo T.
La MyBox::newfunción toma un parámetro de tipo Ty devuelve una MyBoxinstancia
que contiene el valor pasado.

Intentemos añadir la mainfunción del Listado 15-7 al Listado 15-8 y modificarla


para que use el MyBox<T>tipo que definimos en lugar de Box<T>. El código del
Listado 15-9 no compila porque Rust no sabe cómo desreferenciar MyBox.

Nombre de archivo: src/main.rs

fn main() {
let x = 5;
let y = MyBox::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}
Listado 15-9: Intentando utilizar MyBox<T>de la misma manera que usamos referencias yBox<T>

Aquí está el error de compilación resultante:

$ 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 MyBox<T>tipo no se puede desreferenciar porque no hemos


implementado esa capacidad. Para habilitar la desreferenciación con el *operador,
implementamos el Derefatributo.
Rust
jueves, 22 de mayo de 2025 : Página 415 de 719

Tratar un tipo como una referencia mediante la implementación


del Derefrasgo

Como se explicó en la sección "Implementación de un rasgo en un tipo" del


Capítulo 10, para implementar un rasgo, necesitamos implementar los métodos
requeridos. El Derefrasgo, proporcionado por la biblioteca estándar, requiere que
implementemos un método llamado derefque toma prestados selflos datos
internos y los devuelve como referencia. El Listado 15-10 contiene una
implementación de Derefpara complementar la definición de MyBox:

Nombre de archivo: src/main.rs


use std::ops::Deref;

impl<T> Deref for MyBox<T> {


type Target = T;

fn deref(&self) -> &Self::Target {


&self.0
}
}
Listado 15-10: Implementación DerefenMyBox<T>

La type Target = T;sintaxis define un tipo asociado para el Deref atributo. Los tipos
asociados son una forma ligeramente distinta de declarar un parámetro genérico,
pero no es necesario preocuparse por ellos por ahora; los abordaremos con más
detalle en el Capítulo 20.

Completamos el cuerpo del derefmétodo con [nombre del método], por &self.0lo
que derefdevuelve una referencia al valor al que queremos acceder con
el *operador; recuerde la sección "Uso de estructuras de tupla sin campos con
nombre para crear diferentes tipos" del Capítulo 5, que .0accede al primer valor
de una estructura de tupla. La mainfunción del Listado 15-9 que llama *a [nombre
del método]MyBox<T> valor ahora compila y las aserciones pasan.

Sin el Derefrasgo, el compilador solo puede desreferenciar &referencias.


El derefmétodo permite al compilador tomar un valor de cualquier tipo que
implemente Derefy llamar al derefmétodo para obtener una &referencia que sepa
cómo desreferenciar.

Cuando ingresamos *yal Listado 15-9, detrás de escena Rust ejecutó este código:

*(y.deref())
Rust
jueves, 22 de mayo de 2025 : Página 416 de 719

Rust sustituye el *operador con una llamada al derefmétodo y luego una simple
desreferencia, por lo que no tenemos que pensar si necesitamos llamar
al derefmétodo. Esta característica de Rust nos permite escribir código que
funciona de forma idéntica, independientemente de si tenemos una referencia
regular o un tipo que implementa Deref.

La razón por la que el derefmétodo devuelve una referencia a un valor, y por la


que la desreferencia simple fuera de los paréntesis *(y.deref())sigue siendo
necesaria, se relaciona con el sistema de propiedad. Si el derefmétodo devolviera
el valor directamente en lugar de una referencia a él, este se movería fuera
de self. No queremos tomar propiedad del valor interno MyBox<T>en este caso ni
en la mayoría de los casos donde usamos el operador de desreferencia.

Tenga en cuenta que el *operador se reemplaza con una llamada al derefmétodo y


luego con una llamada al *operador solo una vez, cada vez que usamos "a" *en
nuestro código. Dado que la sustitución del *operador no tiene recursividad
infinita, obtenemos datos de tipo i32, que coinciden con " 5in" assert_eq!del Listado
15-9.

Coerciones de desreferencia implícitas con funciones y métodos

La coerción de desreferencia convierte una referencia a un tipo que implementa


la Deref característica en una referencia a otro tipo. Por ejemplo, la coerción de
desreferencia puede convertir &Stringa &strporque Stringimplementa
la Derefcaracterística de forma que devuelve &str. La coerción de desreferencia es
una función que Rust aplica a los argumentos de funciones y métodos, y solo
funciona con tipos que implementan la Deref característica. Se produce
automáticamente cuando pasamos una referencia al valor de un tipo específico
como argumento a una función o método que no coincide con el tipo del
parámetro en la definición de la función o método. Una secuencia de llamadas
al deref método convierte el tipo proporcionado al tipo que necesita el parámetro.

Se agregó la coerción de desreferencias a Rust para que los programadores que


escriben llamadas a funciones y métodos no necesiten agregar tantas referencias
explícitas y desreferencias con &y* . Esta función también nos permite escribir
más código compatible con referencias o punteros inteligentes.

Para ver la coerción de deref en acción, usemos el MyBox<T>tipo definido en el


Listado 15-8, así como su implementación Derefen el Listado 15-10. El Listado 15-
Rust
jueves, 22 de mayo de 2025 : Página 417 de 719

11 muestra la definición de una función con un parámetro de segmento de


cadena:

Nombre de archivo: src/main.rs


fn hello(name: &str) {
println!("Hello, {name}!");
}
Listado 15-11: Una hellofunción que tiene el parámetro namede tipo&str

Podemos llamar a la hellofunción con una cadena como argumento,


por hello("Rust");ejemplo. La coerción de deref permite llamar hello con una
referencia a un valor de tipo MyBox<String>, como se muestra en el Listado 15-12:

Nombre de archivo: src/main.rs


fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
Listado 15-12: Llamada hellocon una referencia a un MyBox<String>valor, que funciona debido a la
coerción de deref

Aquí llamamos a la hellofunción con el argumento &m, que es una referencia a


un MyBox<String>valor. Dado que implementamos el Derefatributo
"on" MyBox<T>en el Listado 15-10, Rust puede
convertir &MyBox<String>"in" &String llamando a deref. La biblioteca estándar
proporciona una implementación de " Deref on" Stringque devuelve un fragmento
de cadena, y esto se encuentra en la documentación de la API de Deref.
Rust derefvuelve a llamar para convertir " &Stringin" &str, que coincide con
la hellodefinición de la función.

Si Rust no implementara la coerción de deref, tendríamos que escribir el código


del Listado 15-13 en lugar del código del Listado 15-12 para llamar hellocon un
valor de tipo &MyBox<String>.

Nombre de archivo: src/main.rs


fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
Listado 15-13: El código que tendríamos que escribir si Rust no tuviera coerción de deref

Desreferencia (*m)el [the] MyBox<String>en un [a] String. Luego, el [ &and] [..]toma


una porción de la cadena [the] Stringque es igual a la cadena completa para que
coincida con la firma de [the hello]. Este código sin desreferencias es más difícil de
Rust
jueves, 22 de mayo de 2025 : Página 418 de 719

leer, escribir y comprender con todos estos símbolos involucrados. La


desreferencia permite que Rust gestione estas conversiones automáticamente.

Cuando Derefse define el rasgo para los tipos involucrados, Rust analizará los tipos
y los usará Deref::dereftantas veces como sea necesario para obtener una
referencia que coincida con el tipo del parámetro. El número de veces
que Deref::derefse debe insertar se determina en tiempo de compilación, por lo
que no hay penalización en tiempo de ejecución por aprovechar la coerción de
deref.

Cómo interactúa la coerción de Deref con la mutabilidad

De manera similar a cómo se usa el Derefrasgo para anular el *operador en


referencias inmutables, puede usar el DerefMutrasgo para anular el * operador en
referencias mutables.

Rust realiza una coerción de desreferenciación cuando encuentra tipos e


implementaciones de características en tres casos:

 Desde &Thasta&UcuandoT: Deref<Target=U>


 Desde &mut Thasta &mut UcuandoT: DerefMut<Target=U>
 Desde &mut Thasta &UcuandoT: Deref<Target=U>

Los dos primeros casos son iguales, excepto que el segundo implementa la
mutabilidad. El primero establece que si se tiene una variable &Ty T se
implementa Derefen algún tipo U, se puede obtener una variable &Ude forma
transparente. El segundo establece que la misma coerción de desreferencias
ocurre para las referencias mutables.

El tercer caso es más complejo: Rust también convierte una referencia mutable
en una inmutable. Pero lo contrario no es posible: las referencias inmutables
nunca convierten referencias mutables. Debido a las reglas de préstamo, si se
tiene una referencia mutable, esta debe ser la única referencia a esos datos (de lo
contrario, el programa no compilaría). Convertir una referencia mutable en una
inmutable nunca infringirá las reglas de préstamo. Convertir una referencia
inmutable en una mutable requeriría que la referencia inmutable inicial fuera la
única referencia inmutable a esos datos, pero las reglas de préstamo no lo
garantizan. Por lo tanto, Rust no puede asumir que sea posible convertir una
referencia inmutable en una mutable.
Rust
jueves, 22 de mayo de 2025 : Página 419 de 719

Ejecución de código en la limpieza con


el Droprasgo
El segundo rasgo importante para el patrón de puntero inteligente es Drop, que
permite personalizar qué sucede cuando un valor está a punto de salir del ámbito.
Se puede implementar el Droprasgo en cualquier tipo, y ese código puede usarse
para liberar recursos como archivos o conexiones de red.

Lo presentamos Dropen el contexto de los punteros inteligentes porque la


funcionalidad del Droprasgo casi siempre se usa al implementar un puntero
inteligente. Por ejemplo, al Box<T>eliminar a, se libera el espacio en el montón al
que apunta la caja.

En algunos lenguajes, para ciertos tipos, el programador debe invocar código para
liberar memoria o recursos cada vez que termina de usar una instancia de esos
tipos. Algunos ejemplos son los manejadores de archivos, los sockets o los
bloqueos. Si se olvidan, el sistema podría sobrecargarse y bloquearse. En Rust, se
puede especificar que se ejecute un fragmento de código específico cuando un
valor se sale del ámbito, y el compilador lo insertará automáticamente. Por lo
tanto, no es necesario tener cuidado al colocar código de limpieza en todos los
lugares del programa donde se termina de usar una instancia de un tipo
específico; ¡aún así, no se perderán recursos!

Se especifica el código que se ejecuta cuando un valor queda fuera del ámbito
implementando el Droprasgo. El Droprasgo requiere que se implemente un método
llamado dropque tome una referencia mutable a self. Para ver cuándo Rust llama
a drop, implementemos las declaraciones dropcon println!por ahora.

El listado 15-14 muestra una CustomSmartPointerestructura cuya única


funcionalidad personalizada es que se imprimirá Dropping CustomSmartPointer!
cuando la instancia salga del alcance, para mostrar cuándo Rust ejecuta
la dropfunción.

Nombre de archivo: src/main.rs


struct CustomSmartPointer {
data: String,
}

impl Drop for CustomSmartPointer {


fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
Rust
jueves, 22 de mayo de 2025 : Página 420 de 719
}
}

fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}
Listado 15-14: Una CustomSmartPointerestructura que implementa el Droprasgo donde colocaríamos
nuestro código de limpieza

El Droprasgo está incluido en el preludio, por lo que no es necesario incluirlo en el


ámbito de aplicación. Implementamos el Droprasgo en CustomSmartPointery
proporcionamos una implementación para el dropmétodo que llama a println!. El
cuerpo de la dropfunción es donde se colocaría la lógica que se desea ejecutar
cuando una instancia de su tipo queda fuera del ámbito de aplicación.
Imprimimos texto aquí para mostrar visualmente cuándo Rust llamará a drop.

En main, creamos dos instancias de CustomSmartPointery luego


imprimimos CustomSmartPointers created. Al final de main, nuestras instancias
de CustomSmartPointerquedarán fuera del ámbito de aplicación y Rust llamará al
código que introdujimos en el dropmétodo, imprimiendo nuestro mensaje final.
Tenga en cuenta que no fue necesario llamar al dropmétodo explícitamente.

Cuando ejecutamos este programa, veremos el siguiente resultado:

$ 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`!

dropRust nos llamó automáticamente cuando nuestras instancias quedaron fuera


de alcance, ejecutando el código que especificamos. Las variables se eliminan en
orden inverso a su creación, por lo que dse eliminó antes que c[nombre del
método]. El propósito de este ejemplo es ofrecer una guía visual
del dropfuncionamiento del método; normalmente, se especificaría el código de
limpieza que el tipo necesita ejecutar, en lugar de un mensaje de impresión.
Rust
jueves, 22 de mayo de 2025 : Página 421 de 719

Dejar caer un valor anticipadamente constd::mem::drop

Lamentablemente, no es sencillo desactivar la drop función


automática. dropNormalmente no es necesario; la función del Droptrait se gestiona
automáticamente. Sin embargo, en ocasiones, podría ser conveniente eliminar un
valor antes de tiempo. Un ejemplo es al usar punteros inteligentes que gestionan
bloqueos: podría ser útil forzar el dropmétodo que libera el bloqueo para que otro
código en el mismo ámbito pueda adquirirlo. Rust no permite llamar
manualmente al método Dropdel trait drop; en su lugar, debe llamar a
la std::mem::dropfunción proporcionada por la biblioteca estándar si desea forzar la
eliminación de un valor antes del final de su ámbito.

Si intentamos llamar al método Dropdel rasgo dropmanualmente modificando


la mainfunción del Listado 15-14, como se muestra en el Listado 15-15,
obtendremos un error del compilador:

Nombre de archivo: 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.");
}
Listado 15-15: Intentar llamar al dropmétodo desde el Droprasgo manualmente para limpiarlo de manera
temprana

Cuando intentemos compilar este código, obtendremos este error:

$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
--> src/main.rs:16:7
|
16 | c.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
16 | drop(c);
| +++++ ~

For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 422 de 719

Este mensaje de error indica que no se permite llamar explícitamente a drop. El


mensaje de error utiliza 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 dropfunción en Rust es un
destructor específico.

Rust no nos permite llamar dropexplícitamente porque seguiría llamando


automáticamente dropal valor al final de main. Esto causaría una doble
liberación. ya que Rust intentaría limpiar el mismo valor dos veces.

No podemos desactivar la inserción automática dropcuando un valor queda fuera


del ámbito, ni podemos llamar al dropmétodo explícitamente. Por lo tanto, si
necesitamos forzar la limpieza anticipada de un valor, usamos
la std::mem::dropfunción.

La std::mem::dropfunción es diferente del dropmétodo en el Drop rasgo. La


llamamos pasando como argumento el valor que queremos forzar a eliminar. La
función está en el preludio, por lo que podemos modificar mainel Listado 15-15
para llamarla drop, como se muestra en el Listado 15-16:

Nombre de archivo: 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.");
}
Listado 15-16: Llamada std::mem::droppara eliminar explícitamente un valor antes de que quede fuera
del alcance

Al ejecutar este código se imprimirá lo siguiente:

$ 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`!se imprime entre


el texto CustomSmartPointer created.y , lo que muestra que se llama al código del
Rust
jueves, 22 de mayo de 2025 : Página 423 de 719

método para eliminarlo en ese punto.CustomSmartPointer dropped before the end of


main.dropc

Puedes usar el código especificado en Dropla implementación de un rasgo de


muchas maneras para que la limpieza sea cómoda y segura: por ejemplo,
¡podrías usarlo para crear tu propio asignador de memoria! Con el Droprasgo y el
sistema de propiedad de Rust, no tienes que acordarte de limpiar, ya que Rust lo
hace automáticamente.

Tampoco tiene que preocuparse por problemas que resulten de la limpieza


accidental de valores que aún están en uso: el sistema de propiedad que
garantiza que las referencias siempre sean válidas también garantiza que dropse
lo llame solo una vez cuando el valor ya no se usa.

Ahora que hemos examinado Box<T>algunas de las características de los


punteros inteligentes, veamos algunos otros punteros inteligentes definidos en la
biblioteca estándar.

, el puntero inteligente de referencia


Rc<T>

contado
En la mayoría de los casos, la propiedad es clara: se sabe exactamente qué
variable posee un valor dado. Sin embargo, hay casos en los que un mismo valor
puede tener múltiples propietarios. Por ejemplo, en las estructuras de datos de
grafos, varias aristas pueden apuntar al mismo nodo, y ese nodo es
conceptualmente propiedad de todas las aristas que apuntan a él. Un nodo no
debe limpiarse a menos que no tenga aristas que lo apunten y, por lo tanto, no
tenga propietarios.

Debe habilitar la propiedad múltiple explícitamente mediante el tipo de


Rust Rc<T>, abreviatura de " reference counting" (conteo de referencias) .
Este Rc<T>tipo registra el número de referencias a un valor para determinar si
este sigue en uso. Si no hay ninguna referencia a un valor, este se puede eliminar
sin que ninguna se invalide.

ImaginarRc<T> un televisor en una sala familiar. Cuando una persona entra a ver
la televisión, la enciende. Los demás pueden entrar a la habitación y verla.
Cuando la última persona sale, apaga el televisor porque ya no se usa. Si alguien
Rust
jueves, 22 de mayo de 2025 : Página 424 de 719

apaga el televisor mientras otros lo siguen viendo, ¡se armaría un alboroto entre
los demás!

Usamos este Rc<T>tipo cuando queremos asignar datos en el montón para que
varias partes de nuestro programa los lean y no podemos determinar en tiempo
de compilación qué parte terminará de usar los datos en último lugar. Si
supiéramos qué parte terminará en último lugar, podríamos simplemente
convertirla en la propietaria de los datos, y las reglas de propiedad habituales
aplicadas en tiempo de compilación se aplicarían.

Tenga en cuenta que esto Rc<T>solo se aplica a escenarios de un solo


subproceso. Cuando tratemos la concurrencia en el Capítulo 16, explicaremos
cómo realizar el conteo de referencias en programas multiproceso.

Uso Rc<T>para compartir datos

Volvamos a nuestro ejemplo de lista de contras del Listado 15-5. Recordemos que
la definimos usando Box<T>. Esta vez, crearemos dos listas que comparten la
propiedad de una tercera lista. Conceptualmente, esto se parece a la Figura 15-3:

Figura 15-3: Dos listas, by c, compartiendo la propiedad de una tercera lista,a

Crearemos una lista aque contenga 5 y luego 10. Luego haremos dos listas
más: buna que comience con 3 y cotra que comience con 4.
Ambas blistas c continuarán hasta la primera alista que contiene 5 y 10. En otras
palabras, ambas listas compartirán la primera lista que contiene 5 y 10.

Intentar implementar este escenario utilizando nuestra definición


de Listwith Box<T> no funcionará, como se muestra en el Listado 15-17:

Nombre de archivo: src/main.rs

enum List {
Cons(i32, Box<List>),
Nil,
}

use crate::List::{Cons, Nil};

fn main() {
Rust
jueves, 22 de mayo de 2025 : Página 425 de 719
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
Listado 15-17: Demostración de que no se nos permite tener dos listas Box<T>que intenten compartir la
propiedad de una tercera lista

Cuando compilamos este código, obtenemos este error:

$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy`
trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error

Las Consvariantes poseen los datos que contienen, por lo que, al crear
la blista, a se mueve a by bposee a. Luego, al intentar usarla ade nuevo al crear c,
no se nos permite hacerlo porque ase ha movido.

Podríamos cambiar la definición de Conspara que contenga referencias, pero


entonces tendríamos que especificar parámetros de duración. Al especificar estos
parámetros, estaríamos especificando que cada elemento de la lista tendrá una
vida útil al menos igual a la de la lista completa. Esto aplica a los elementos y
listas de los Listados 15-17, pero no en todos los casos.

En su lugar, cambiaremos nuestra definición de Listpara usarla Rc<T>en lugar


de Box<T>, como se muestra en el Listado 15-18. Cada Consvariante contendrá
ahora un valor y un Rc<T>que apunta a un List. Al crear b, en lugar de tomar
propiedad de a, clonaremos el Rc<List>que acontiene, aumentando así el número
de referencias de una a dos y acompartiendo b la propiedad de los datos en
ese Rc<List>. También clonaremos aal crear c, aumentando el número de
referencias de dos a tres. Cada vez que llamemos a Rc::clone, el número de
referencias a los datos dentro de Rc<List>aumentará, y los datos no se limpiarán a
menos que no haya ninguna referencia a ellos.
Rust
jueves, 22 de mayo de 2025 : Página 426 de 719
Nombre de archivo: src/main.rs
enum List {
Cons(i32, Rc<List>),
Nil,
}

use crate::List::{Cons, Nil};


use std::rc::Rc;

fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
Listado 15-18: Una definición de Listque utilizaRc<T>

Necesitamos agregar una usedeclaración para incluirla Rc<T>en el ámbito de


aplicación, ya que no está en el preludio. En [insertar contexto main], creamos la
lista que contiene 5 y 10 y la almacenamos en un nuevo
[ insertar Rc<List>contexto] a. Luego, al crear [insertar contexto b] y c[insertar
contexto], llamamos a la Rc::clonefunción y pasamos una referencia al
[ Rc<List>insertar contexto] acomo argumento.

Podríamos haber llamado a.clone()en lugar de Rc::clone(&a), pero la convención de


Rust es usar Rc::cloneen este caso. La implementación de Rc::cloneno realiza una
copia profunda de todos los datos como clonelo hacen las implementaciones de la
mayoría de los tipos. La llamada a Rc::clonesolo incrementa el recuento de
referencias, lo cual no toma mucho tiempo. Las copias profundas de datos pueden
tomar mucho tiempo. Al usar Rc::clonepara el recuento de referencias, podemos
distinguir visualmente entre los tipos de clones de copia profunda y los tipos de
clones que aumentan el recuento de referencias. Al buscar problemas de
rendimiento en el código, solo debemos considerar los clones de copia profunda y
podemos ignorar las llamadas a Rc::clone.

La clonación Rc<T>aumenta el recuento de referencias

Cambiemos nuestro ejemplo de trabajo en el Listado 15-18 para que podamos ver
cómo cambian los recuentos de referencias a medida que creamos y eliminamos
referencias al Rc<List>in a.

En el Listado 15-19, lo cambiaremos mainpara que tenga un alcance interno


alrededor de la lista c; luego podremos ver cómo cambia el recuento de
referencias cuando csale del alcance.
Rust
jueves, 22 de mayo de 2025 : Página 427 de 719
Nombre de archivo: src/main.rs
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));
}
Listado 15-19: Impresión del recuento de referencias

En cada punto del programa donde cambia el recuento de referencias, lo


imprimimos, el cual obtenemos al llamar a la Rc::strong_countfunción. Esta función
se llama strong_counten lugar de countporque el Rc<T>tipo también tiene
un weak_count; veremos weak_countpara qué se usa en la sección "Prevención de
ciclos de referencia: Convertir un Rc<T>en un Weak<T>" .

Este código imprime lo siguiente:

$ 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 Rc<List>in atiene un recuento de referencia inicial de 1; luego,


cada vez que llamamos a clone, el recuento aumenta en 1. Cuando csale del
alcance, el recuento disminuye en 1. No tenemos que llamar a una función para
disminuir el recuento de referencia como tenemos que llamar Rc::clonepara
aumentar el recuento de referencia: la implementación del Droprasgo disminuye
el recuento de referencia automáticamente cuando un Rc<T> valor sale del
alcance.

Lo que no podemos ver en este ejemplo es que cuando by asalen del ámbito al
final de main, el recuento es 0 y Rc<List>se limpia por completo.
Usar Rc<T>permite que un mismo valor tenga varios propietarios, y el recuento
garantiza que el valor siga siendo válido mientras exista alguno de los
propietarios.
Rust
jueves, 22 de mayo de 2025 : Página 428 de 719

Mediante referencias inmutables, Rc<T>se pueden compartir datos entre varias


partes del programa, solo para lectura. Si Rc<T>se permiten múltiples referencias
mutables, se podría infringir una de las reglas de préstamo explicadas en el
Capítulo 4: múltiples préstamos mutables en el mismo lugar pueden causar
carreras de datos e inconsistencias. Sin embargo, poder mutar datos es muy útil.
En la siguiente sección, analizaremos el patrón de mutabilidad interna y
el RefCell<T> tipo que se puede usar junto con un Rc<T>para trabajar con esta
restricción de inmutabilidad.

RefCell<T> y el patrón de mutabilidad interior


La mutabilidad interior es un patrón de diseño en Rust que permite mutar datos
incluso con referencias inmutables a ellos; normalmente, esta acción no está
permitida por las reglas de préstamo. Para mutar datos, el patrón
utiliza unsafecódigo dentro de una estructura de datos para modificar las reglas
habituales de Rust que rigen la mutación y el préstamo. El código inseguro indica
al compilador que estamos comprobando las reglas manualmente en lugar de
confiar en que el compilador las compruebe por nosotros; analizaremos el código
inseguro con más detalle en el Capítulo 20.

Podemos usar tipos que emplean el patrón de mutabilidad interna solo cuando
podemos garantizar que las reglas de préstamo se seguirán en tiempo de
ejecución, aunque el compilador no pueda garantizarlo. El unsafecódigo
involucrado se encapsula entonces en una API segura, y el tipo externo sigue
siendo inmutable.

Exploremos este concepto observando el RefCell<T>tipo que sigue el patrón de


mutabilidad interior.

Aplicación de reglas de préstamo en tiempo de ejecución


conRefCell<T>

A diferencia de Rc<T>, el RefCell<T>tipo representa la propiedad única de los


datos que contiene. Entonces, ¿qué lo RefCell<T>diferencia de un tipo
como Box<T>? Recuerda las reglas de préstamo que aprendiste en el Capítulo 4:

 En cualquier momento, puedes tener una referencia mutable (pero no


ambas) o cualquier cantidad de referencias inmutables.
 Las referencias deben ser siempre válidas.
Rust
jueves, 22 de mayo de 2025 : Página 429 de 719

Con referencias y Box<T>, las invariantes de las reglas de préstamo se aplican en


tiempo de compilación. Con RefCell<T>, estas invariantes se aplican en tiempo de
ejecución . Con referencias, si se incumplen estas reglas, se obtendrá un error de
compilación. Con RefCell<T> , si se incumplen estas reglas, el programa entrará en
pánico y se cerrará.

Las ventajas de verificar las reglas de préstamo en tiempo de compilación son


que los errores se detectan antes en el proceso de desarrollo y no se ve afectado
el rendimiento en tiempo de ejecución, ya que todo el análisis se realiza de
antemano. Por estas razones, verificar las reglas de préstamo en tiempo de
compilación es la mejor opción en la mayoría de los casos, razón por la cual es la
opción predeterminada de Rust.

La ventaja de verificar las reglas de préstamo en tiempo de ejecución es que se


permiten ciertos escenarios seguros para la memoria, que las verificaciones en
tiempo de compilación habrían prohibido. El análisis estático, al igual que el
compilador de Rust, es inherentemente conservador. Algunas propiedades del
código son imposibles de detectar mediante su análisis: el ejemplo más conocido
es el problema de la parada, que queda fuera del alcance de este libro, pero es un
tema interesante para investigar.

Dado que algunos análisis son imposibles, si el compilador de Rust no puede


garantizar que el código cumpla con las reglas de propiedad, podría rechazar un
programa correcto; de esta forma, es conservador. Si Rust aceptara un programa
incorrecto, los usuarios no podrían confiar en las garantías que ofrece. Sin
embargo, si Rust rechaza un programa correcto, el programador sufrirá
inconvenientes, pero no puede ocurrir nada catastrófico. El RefCell<T>tipo es útil
cuando se está seguro de que el código cumple con las reglas de préstamo, pero
el compilador no puede comprenderlo ni garantizarlo.

Similar a Rc<T>, RefCell<T>solo se usa en escenarios de un solo subproceso y


generará un error de compilación si intenta usarlo en un contexto multiproceso.
En el capítulo 16, explicaremos cómo obtener la funcionalidad de RefCell<T>en un
programa multiproceso.

A continuación se presenta un resumen de las razones para elegir Box<T>, Rc<T>,


o RefCell<T>:
Rust
jueves, 22 de mayo de 2025 : Página 430 de 719

 Rc<T>permite que varios propietarios de los mismos


datos Box<T>y RefCell<T> tengan propietarios únicos.
 Box<T>permite préstamos inmutables o mutables controlados en tiempo de
compilación; Rc<T> permite sólo préstamos inmutables controlados en
tiempo de compilación; RefCell<T>permite préstamos inmutables o mutables
controlados en tiempo de ejecución.
 Debido a que RefCell<T>permite préstamos mutables comprobados en
tiempo de ejecución, puede mutar el valor dentro de RefCell<T>incluso
cuando RefCell<T>es inmutable.

La mutación del valor dentro de un valor inmutable se conoce como patrón de


mutabilidad interna . Analicemos una situación en la que la mutabilidad interna
resulta útil y examinemos cómo es posible.

Mutabilidad interior: un préstamo mutable a un valor inmutable

Una consecuencia de las reglas de préstamo es que, cuando se tiene un valor


inmutable, no se puede tomar prestado de forma mutable. Por ejemplo, este
código no se compilará:

fn main() {
let x = 5;
let y = &mut x;
}

Si intentara compilar este código, obtendría el siguiente error:

$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3| let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2| let mut x = 5;
| +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 431 de 719

Sin embargo, existen situaciones en las que sería útil que un valor se mutara a sí
mismo en sus métodos, pero que pareciera inmutable para otro código. El código
externo a los métodos del valor no podría mutarlo. Usar RefCell<T>es una forma
de obtener la capacidad de mutabilidad interna, pero RefCell<T> no evita
completamente las reglas de préstamo: el verificador de préstamos del
compilador permite esta mutabilidad interna, y las reglas de préstamo se
verifican en tiempo de ejecución. Si se infringen las reglas, se obtendrá un
error panic!en lugar de un error del compilador.

Trabajemos con un ejemplo práctico en el que podemos RefCell<T>mutar un valor


inmutable y veamos por qué es útil.

Un caso de uso para la mutabilidad interior: objetos simulados

A veces, durante las pruebas, un programador usa un tipo en lugar de otro para
observar un comportamiento específico y asegurar su correcta implementación.
Este tipo de marcador se denomina " doble de prueba" . Piénselo como un "doble
de acción" en el cine, donde una persona sustituye a un actor en una escena
complicada. Los dobles de prueba sustituyen a otros tipos al ejecutar
pruebas. Los objetos simulados son tipos específicos de dobles de prueba que
registran lo que sucede durante una prueba para asegurar que se realizaron las
acciones correctas.

Rust no tiene objetos en el mismo sentido que otros lenguajes, y no tiene la


funcionalidad de objetos simulados integrada en la biblioteca estándar como otros
lenguajes. Sin embargo, es posible crear una estructura que cumpla las mismas
funciones que un objeto simulado.

Este es el escenario que probaremos: crearemos una biblioteca que monitoriza un


valor con respecto a un valor máximo y envía mensajes según la proximidad del
valor actual al máximo. Esta biblioteca podría usarse para controlar la cuota de un
usuario en cuanto al número de llamadas a la API que puede realizar, por ejemplo.

Nuestra biblioteca solo proporcionará la funcionalidad de rastrear la proximidad


de un valor al máximo y qué mensajes deberían enviarse en cada momento. Se
espera que las aplicaciones que usan nuestra biblioteca proporcionen el
mecanismo para enviar los mensajes: la aplicación podría insertar un mensaje en
la aplicación, enviar un correo electrónico, enviar un mensaje de texto, etc. La
biblioteca no necesita conocer ese detalle. Solo necesita algo que implemente un
Rust
jueves, 22 de mayo de 2025 : Página 432 de 719

atributo que proporcionaremos llamado Messenger. El listado 15-20 muestra el


código de la biblioteca:

Nombre de archivo: src/lib.rs


pub trait Messenger {
fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {


messenger: &'a T,
value: usize,
max: usize,
}

impl<'a, T> LimitTracker<'a, T>


where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}

pub fn set_value(&mut self, value: usize) {


self.value = value;

let percentage_of_max = self.value as f64 / self.max as f64;

if percentage_of_max >= 1.0 {


self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
Listado 15-20: Una biblioteca para realizar un seguimiento de qué tan cerca está un valor de un valor
máximo y advertir cuando el valor está en ciertos niveles

Una parte importante de este código es que el Messengerrasgo tiene un método


llamado sendque toma una referencia inmutable a selfy el texto del mensaje. Este
rasgo es la interfaz que nuestro objeto simulado debe implementar para que
pueda usarse de la misma manera que un objeto real. Otra parte importante es
que queremos probar el comportamiento del set_valuemétodo en el [nombre del
Rust
jueves, 22 de mayo de 2025 : Página 433 de 719

objeto LimitTracker]. Podemos cambiar lo que pasamos como valueparámetro,


pero set_valueno devuelve nada sobre lo que podamos hacer afirmaciones.
Queremos poder decir que si creamos un [ nombre del objeto] LimitTrackercon algo
que implementa el Messengerrasgo y un valor particular para max[nombre del
objeto], al pasar diferentes números para [nombre del objeto] value, se le indica al
mensajero que envíe los mensajes apropiados.

Necesitamos un objeto simulado que, en lugar de enviar un correo electrónico o


un mensaje de texto al llamar a send, solo registre los mensajes que se le
indiquen. Podemos crear una nueva instancia del objeto simulado, crear
un LimitTrackerque lo use, llamar al set_valuemétodo en LimitTrackery luego
comprobar que el objeto simulado contiene los mensajes esperados. El listado 15-
21 muestra un intento de implementar un objeto simulado para hacer
precisamente eso, pero el verificador de préstamos no lo permite:

Nombre de archivo: src/lib.rs

#[cfg(test)]
mod tests {
use super::*;

struct MockMessenger {
sent_messages: Vec<String>,
}

impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}

impl Messenger for MockMessenger {


fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}

#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

limit_tracker.set_value(80);

assert_eq!(mock_messenger.sent_messages.len(), 1);
Rust
jueves, 22 de mayo de 2025 : Página 434 de 719
}
}
Listado 15-21: Un intento de implementar un MockMessengerque no está permitido por el verificador de
préstamos

Este código de prueba define una MockMessengerestructura que tiene


un sent_messages campo con Vecvalores ``` Stringpara registrar los mensajes que
se le indica que envíe. También definimos una función asociada newpara facilitar
la creación de nuevos MockMessengervalores que comiencen con una lista vacía de
mensajes. Luego, implementamos el Messengeratributo ```
para MockMessengerpoder asignar ``` MockMessengera ``` LimitTracker. En la
definición del sendmétodo, tomamos el mensaje pasado como parámetro y lo
almacenamos en la MockMessenger lista ``` sent_messages.

En la prueba, analizamos qué sucede cuando LimitTrackerse le indica a `the` que


se establezca valueen un valor superior al 75 % del maxvalor. Primero, creamos un
`new` MockMessenger, que comenzará con una lista vacía de mensajes. Luego,
creamos un `new` LimitTrackery le asignamos una referencia al
`new` MockMessengery un maxvalor de 100. Llamamos al set_valuemétodo
`the` LimitTrackercon un valor de 80, que es superior al 75 % de 100. Después,
confirmamos que la lista de mensajes que `the` MockMessengerregistra ahora debe
contener un solo mensaje.

Sin embargo, hay un problema con esta prueba, como se muestra aquí:

$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to
cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait`
definition
|
2 ~ fn send(&mut self, msg: &str);
3 |}
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 435 de 719

No podemos modificar MockMessengerpara realizar un seguimiento de los


mensajes, ya que el sendmétodo toma una referencia inmutable a self. Tampoco
podemos usar la sugerencia del texto de error &mut selftanto en el implmétodo
como en la traitdefinición. No queremos cambiar el Messengeratributo solo para
realizar pruebas. En cambio, necesitamos encontrar la manera de que nuestro
código de prueba funcione correctamente con nuestro diseño actual.

En esta situación, la mutabilidad interna puede ser útil. Almacenaremos


el sent_messagesdentro de un [nombre de objeto] RefCell<T>y, a continuación,
el sendmétodo podrá modificarse sent_messagespara almacenar los mensajes que
hemos visto. El listado 15-22 muestra cómo se ve esto:

Nombre de archivo: src/lib.rs


#[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![]),
}
}
}

impl Messenger for MockMessenger {


fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}

#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--

assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
Listado 15-22: Uso RefCell<T>para mutar un valor interno mientras el valor externo se considera
inmutable
Rust
jueves, 22 de mayo de 2025 : Página 436 de 719

El sent_messagescampo ahora es de tipo RefCell<Vec<String>>en lugar


de Vec<String>. En la newfunción, creamos una
nueva RefCell<Vec<String>> instancia alrededor del vector vacío.

Para la implementación del sendmétodo, el primer parámetro sigue siendo un


préstamo inmutable de self, que coincide con la definición del rasgo.
Llamamos borrow_muta RefCell<Vec<String>>in self.sent_messagespara obtener una
referencia mutable al valor dentro de RefCell<Vec<String>>, que es el vector.
Luego, podemos llamar pusha la referencia mutable al vector para realizar un
seguimiento de los mensajes enviados durante la prueba.

El último cambio que tenemos que hacer es en la afirmación: para ver cuántos
elementos hay en el vector interno, llamamos borrowa RefCell<Vec<String>>para
obtener una referencia inmutable al vector.

¡Ahora que has visto cómo usar RefCell<T>, profundicemos en cómo funciona!

Seguimiento de préstamos en tiempo de ejecución conRefCell<T>

Al crear referencias inmutables y mutables, usamos la sintaxis ` &`` y ` &mut `,


respectivamente. Con ` RefCell<T>``, usamos los métodos ``` borrowy
`` borrow_mut , que forman parte de la API segura de ``` RefCell<T>.
El borrowmétodo devuelve el tipo de puntero inteligente ` Ref<T>`` y borrow_mut ``
devuelve el tipo de puntero inteligente `` RefMut<T>`. Ambos tipos implementan
``` Deref, por lo que podemos tratarlos como referencias normales.

El RefCell<T>rastrea la cantidad de Ref<T>punteros RefMut<T>inteligentes activos.


Cada vez que llamamos a borrow, RefCell<T> aumenta el número de préstamos
inmutables activos. Cuando un Ref<T> valor queda fuera del alcance, el número
de préstamos inmutables disminuye en uno. Al igual que las reglas de préstamos
en tiempo de compilación, RefCell<T>permite tener varios préstamos inmutables o
uno mutable en cualquier momento.

Si intentamos infringir estas reglas, en lugar de obtener un error de compilación


como ocurriría con las referencias, la implementación de RefCell<T>entrará en
pánico en tiempo de ejecución. El Listado 15-23 muestra una modificación de la
implementación de senddel Listado 15-22. Intentamos crear deliberadamente dos
préstamos mutables activos para el mismo ámbito para ilustrar que
esto RefCell<T>nos impide hacerlo en tiempo de ejecución.
Rust
jueves, 22 de mayo de 2025 : Página 437 de 719
Nombre de archivo: src/lib.rs

impl Messenger for MockMessenger {


fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();

one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
Listado 15-23: Creación de dos referencias mutables en el mismo ámbito para ver
que RefCell<T>entrará en pánico

Creamos una variable one_borrowpara el RefMut<T>puntero inteligente devuelto


desde borrow_mut. Luego, creamos otro préstamo mutable de la misma manera en
la variable two_borrow. Esto crea dos referencias mutables en el mismo ámbito, lo
cual no está permitido. Al ejecutar las pruebas de nuestra biblioteca, el código del
Listado 15-23 se compilará sin errores, pero la prueba fallará:

$ 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:

---- tests::it_sends_an_over_75_percent_warning_message stdout ----


thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
tests::it_sends_an_over_75_percent_warning_message

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Observe que el código generó un mensaje de pánico already borrowed:


BorrowMutError. Así RefCell<T>se gestionan las infracciones de las reglas de
préstamo en tiempo de ejecución.
Rust
jueves, 22 de mayo de 2025 : Página 438 de 719

Elegir detectar errores de préstamos en tiempo de ejecución en lugar de en


tiempo de compilación, como hicimos aquí, significa que podrías encontrar errores
en tu código más adelante en el proceso de desarrollo: posiblemente no hasta
que tu código se implemente en producción. Además, tu código sufriría una
pequeña penalización de rendimiento en tiempo de ejecución al registrar los
préstamos en tiempo de ejecución en lugar de en tiempo de compilación. Sin
embargo, el uso RefCell<T>permite escribir un objeto simulado que puede
modificarse a sí mismo para registrar los mensajes que ha visto mientras lo usas
en un contexto donde solo se permiten valores inmutables. Puedes
usar, RefCell<T> a pesar de sus desventajas, para obtener más funcionalidad que
las referencias regulares.

Tener múltiples propietarios de datos mutables mediante la


combinación Rc<T>yRefCell<T>

Una forma común de usarlo RefCell<T>es en combinación con Rc<T>. Recuerda


que Rc<T>te permite tener varios propietarios de algunos datos, pero solo da
acceso inmutable a ellos. Si tienes un Rc<T>que contiene un RefCell<T>, puedes
obtener un valor que puede tener varios propietarios y que puedes mutar.

Por ejemplo, recuerde el ejemplo de la lista cons del Listado 15-18, donde
permitíamos que Rc<T>varias listas compartieran la propiedad de otra. Dado
que Rc<T>solo contiene valores inmutables, no podemos cambiar ninguno de los
valores de la lista una vez creada. Añadamos [in] RefCell<T>para poder cambiar
los valores de las listas. El Listado 15-24 muestra que, al usar [a] RefCell<T>en
la Consdefinición, podemos modificar el valor almacenado en todas las listas:

Nombre de archivo: src/main.rs


#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}

use crate::List::{Cons, Nil};


use std::cell::RefCell;
use std::rc::Rc;

fn main() {
let value = Rc::new(RefCell::new(5));

let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));


Rust
jueves, 22 de mayo de 2025 : Página 439 de 719
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

*value.borrow_mut() += 10;

println!("a after = {a:?}");


println!("b after = {b:?}");
println!("c after = {c:?}");
}
Listado 15-24: Usando Rc<RefCell<i32>>para crear un Listque podamos mutar

Creamos un valor que es una instancia de Rc<RefCell<i32>>y lo almacenamos en


una variable llamada valuepara poder acceder a él directamente más tarde.
Luego, creamos un Listin acon una Consvariante que contiene value. Necesitamos
clonar valuepara que ambos ay valuetengan la propiedad del 5valor interno en
lugar de transferir la propiedad de valuea ao pedir aprestado de value.

Envolvemos la lista aen un Rc<T>de modo que cuando creamos listas by c, ambas
puedan hacer referencia a a, que es lo que hicimos en el Listado 15-18.

Tras crear las listas en a, by c, queremos sumar 10 al valor en value. Para ello,
llamamos borrow_muta on value, que utiliza la función de desreferenciación
automática que explicamos en el capítulo 5 (véase la sección "¿Dónde está el -
>operador?" ) para desreferenciar al valor Rc<T>interno RefCell<T>.
El borrow_mut método devuelve un RefMut<T>puntero inteligente, sobre el cual
utilizamos el operador de desreferenciación para modificar el valor interno.

Cuando imprimimos a, b, y c, podemos ver que todos tienen el valor modificado


de 15 en lugar de 5:

$ 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 ingeniosa! Al usar RefCell<T>, obtenemos un Listvalor


aparentemente inmutable. Sin embargo, podemos usar los métodos
en RefCell<T>que proporcionan acceso a su mutabilidad interna para modificar
nuestros datos cuando sea necesario. Las comprobaciones en tiempo de
ejecución de las reglas de préstamo nos protegen de las carreras de datos, y a
veces vale la pena sacrificar velocidad a cambio de esta flexibilidad en nuestras
estructuras de datos. Tenga en cuenta que RefCell<T>no funciona con código
Rust
jueves, 22 de mayo de 2025 : Página 440 de 719

multihilo. Mutex<T>Es la versión segura para subprocesos de RefCell<T>, que


analizaremos Mutex<T>en el capítulo 16.

Los ciclos de referencia pueden provocar


pérdidas de memoria
Las garantías de seguridad de memoria de Rust dificultan, pero no imposibilitan,
la creación accidental de memoria que nunca se limpia (lo que se conoce
como fuga de memoria ). Prevenir las fugas de memoria por completo no es una
de las garantías de Rust, lo que significa que las fugas de memoria son seguras
en Rust. Podemos ver que Rust permite fugas de memoria al
usar Rc<T>y RefCell<T>: es posible crear referencias donde los elementos se
refieren entre sí en un ciclo. Esto crea fugas de memoria porque el recuento de
referencias de cada elemento en el ciclo nunca llegará a 0 y los valores nunca se
eliminarán.

Creación de un ciclo de referencia

Veamos cómo podría ocurrir un ciclo de referencia y cómo evitarlo, comenzando


con la definición de la Listenumeración y un tailmétodo en el Listado 15-25:

Nombre de archivo: src/main.rs


use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}

impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}

fn main() {}
Listado 15-25: Una definición de lista de contras que contiene una RefCell<T>para que podamos
modificar a qué Consse refiere una variante
Rust
jueves, 22 de mayo de 2025 : Página 441 de 719

Usamos otra variación de la Listdefinición del Listado 15-5. El segundo elemento


de la Consvariante ahora es RefCell<Rc<List>>, lo que significa que, en lugar de
poder modificar el i32valor como en el Listado 15-24, queremos modificar
el Listvalor Consal que apunta una variante. También añadimos un tailmétodo para
facilitar el acceso al segundo elemento si tenemos una Consvariante.

En el Listado 15-26, agregamos una mainfunción que usa las definiciones del
Listado 15-25. Este código crea una lista en ay una lista en bque apunta a la lista
en a. Luego, modifica la lista en apara que apunte a b, creando un ciclo de
referencia. Hay println!instrucciones a lo largo del proceso para mostrar los
recuentos de referencia en varios puntos de este proceso.

Nombre de archivo: src/main.rs


fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

println!("a initial rc count = {}", Rc::strong_count(&a));


println!("a next item = {:?}", a.tail());

let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

println!("a rc count after b creation = {}", Rc::strong_count(&a));


println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());

if let Some(link) = a.tail() {


*link.borrow_mut() = Rc::clone(&b);
}

println!("b rc count after changing a = {}", Rc::strong_count(&b));


println!("a rc count after changing a = {}", Rc::strong_count(&a));

// Uncomment the next line to see that we have a cycle;


// it will overflow the stack
// println!("a next item = {:?}", a.tail());
}
Listado 15-26: Creación de un ciclo de referencia de dos Listvalores que apuntan entre sí

Creamos una Rc<List>instancia que contiene un Listvalor en la variable a con una


lista inicial de 5, Nil. Luego, creamos una Rc<List>instancia que contiene
otro Listvalor en la variable bque contiene el valor 10 y apunta a la lista en a.

Lo modificamos apara que apunte a ben lugar de a Nil, creando un ciclo. Para ello,
usamos el tailmétodo para obtener una referencia a RefCell<Rc<List>> in a, que
colocamos en la variable link. Luego, usamos el borrow_mut método
Rust
jueves, 22 de mayo de 2025 : Página 442 de 719

en RefCell<Rc<List>>para cambiar el valor dentro de un valor Rc<List> que


contiene un Nilvalor a Rc<List>in b.

Cuando ejecutamos este código, dejando el último println!comentario por el


momento, obtendremos este resultado:

$ 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

El recuento de referencias de las Rc<List>instancias en ambos ason b2 después de


cambiar la lista en apara que apunte a b. Al final de main, Rust elimina la
variable b, lo que reduce el recuento de referencias de la b Rc<List>instancia de 2
a 1. La memoria que Rc<List>tiene en el montón no se eliminará en este punto,
porque su recuento de referencias es 1, no 0. Entonces, Rust elimina a, lo
que a Rc<List>también reduce el recuento de referencias de la instancia de 2 a 1.
La memoria de esta instancia tampoco se puede eliminar, porque la
otra Rc<List>instancia aún la referencia. La memoria asignada a la lista
permanecerá sin recopilarse indefinidamente. Para visualizar este ciclo de
referencia, hemos creado un diagrama en la Figura 15-4.

Figura 15-4: Un ciclo de referencia de listas ay b apuntamientos entre sí

Si descomentas el último println!y ejecutas el programa, Rust intentará imprimir


este ciclo aapuntando a bapuntando a ay así sucesivamente hasta que desborde
la pila.

En comparación con un programa real, las consecuencias de crear un ciclo de


referencia en este ejemplo no son tan graves: justo después de crearlo, el
programa finaliza. Sin embargo, si un programa más complejo asignara mucha
memoria en un ciclo y la mantuviera durante mucho tiempo, usaría más memoria
Rust
jueves, 22 de mayo de 2025 : Página 443 de 719

de la necesaria y podría saturar el sistema, provocando que se agotara la


memoria disponible.

Crear ciclos de referencia no es fácil, pero tampoco es imposible. Si


tienesRefCell<T> valores que contienen Rc<T>valores o combinaciones anidadas
similares de tipos con mutabilidad interna y conteo de referencias, debe
asegurarse de no crear ciclos; no puede confiar en que Rust los detecte. Crear un
ciclo de referencia sería un error lógico en su programa que debería minimizar
mediante pruebas automatizadas, revisiones de código y otras prácticas de
desarrollo de software.

Otra solución para evitar ciclos de referencia es reorganizar las estructuras de


datos de modo que algunas referencias expresen propiedad y otras no. Como
resultado, se pueden tener ciclos compuestos por relaciones de propiedad y
relaciones de no propiedad, y solo las relaciones de propiedad influyen en la
posibilidad de descartar un valor. En el Listado 15-25, siempre queremos
que Cons las variantes posean su lista, por lo que no es posible reorganizar la
estructura de datos. Veamos un ejemplo con grafos compuestos por nodos padre
e hijo para ver cuándo las relaciones de no propiedad son una forma adecuada de
evitar ciclos de referencia.

Prevención de ciclos de referencia: Activación de unRc<T> en


unWeak<T>

Hasta ahora, hemos demostrado que llamar Rc::cloneaumenta el


valor strong_countde una Rc<T>instancia, y una Rc<T>instancia solo se limpia
si strong_countes 0. También puede crear una referencia débil al valor dentro de
una Rc<T>instancia llamando Rc::downgradey pasando una referencia a Rc<T>. Las
referencias fuertes permiten compartir la propiedad de una
instancia.Rc<T> instancia. Las referencias débiles no expresan una relación de
propiedad, y su número no afecta el momento en que Rc<T>se limpia una
instancia. No causarán un ciclo de referencias, ya que cualquier ciclo que incluya
referencias débiles se interrumpirá una vez que el número de valores
involucrados en la referencia fuerte sea 0.

Al llamar a Rc::downgrade, se obtiene un puntero inteligente de tipo Weak<T>. En


lugar de aumentar el valor strong_countde la Rc<T>instancia en 1, al llamar
a , Rc::downgradese aumenta el valor weak_counten 1. El Rc<T>tipo se
usa weak_countpara registrar cuántas Weak<T>referencias existen, de forma
Rust
jueves, 22 de mayo de 2025 : Página 444 de 719

similar a strong_count. La diferencia radica en queweak_count no es necesario que


sea 0 para Rc<T>que se limpie la instancia.

Dado que el valor al que Weak<T>hace referencia podría haberse descartado, para
realizar cualquier acción con el valor al que Weak<T>apunta a, debe asegurarse
de que el valor aún exista. Para ello, llame al upgrademétodo en
una Weak<T> instancia, que devolverá un Option<Rc<T>>. Obtendrá un resultado
de Some si el Rc<T>valor aún no se ha descartado y un resultado de Nonesi
el Rc<T>valor se ha descartado. Dado que upgradedevuelve un Option<Rc<T>>,
Rust se asegurará de queSome mayúsculas y Noneminúsculas, y no habrá un
puntero inválido.

A modo de ejemplo, en lugar de utilizar una lista cuyos elementos solo conocen el
siguiente elemento, crearemos un árbol cuyos elementos conocen sus elementos
secundarios y sus elementos primarios.

Creación de una estructura de datos de árbol: aNode con nodos


secundarios

Para empezar, construiremos un árbol con nodos que conocen a sus nodos
secundarios. Crearemos una estructura llamada Nodeque contenga su
propio i32valor, así como referencias a sus nodos secundarios. Node los valores de
sus nodos secundarios:

Nombre de archivo: src/main.rs

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}

Queremos que a Nodesea propietario de sus hijos y compartir esa propiedad con
las variables para poder acceder Nodedirectamente a cada una en el árbol. Para
ello, definimos los Vec<T>elementos como valores de tipo Rc<Node>. También
queremos modificar qué nodos son hijos de otro nodo, por lo que tenemos
un RefCell<T>in childrenalrededor deVec<Rc<Node>> .
Rust
jueves, 22 de mayo de 2025 : Página 445 de 719

A continuación, utilizaremos nuestra definición de estructura y crearemos


una Nodeinstancia llamada leafcon el valor 3 y sin hijos, y otra instancia
llamada branch con el valor 5 y leafcomo uno de sus hijos, como se muestra en el
Listado 15-27:

Nombre de archivo: src/main.rs


fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});

let branch = Rc::new(Node {


value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}
Listado 15-27: Creación de un leafnodo sin hijos y un branchnodo con leafuno de sus hijos

Clonamos el Rc<Node>in leafy lo almacenamos en branch, lo que significa


que Nodeahora leaftiene dos propietarios: leafy branch. Podemos acceder
de brancha leafa través de branch.children, pero no hay forma de acceder
de leafa branch. Esto se debe a que leafno tiene referencia a branchy desconoce su
relación. Queremos leafsaber que branches su padre. Lo haremos a continuación.

Agregar una referencia de un elemento secundario a su elemento


primario

Para que el nodo hijo reconozca a su padre, necesitamos añadir un parentcampo a


nuestra Nodedefinición de estructura. El problema radica en decidir
cuál parentdebe ser el tipo de . Sabemos que no puede contener un Rc<T>, ya que
eso crearía un ciclo de referencia con leaf.parentapuntando
a branchy branch.childrenapuntando a leaf, lo que haría que sus strong_count valores
nunca fueran 0.

Considerando las relaciones desde otra perspectiva, un nodo padre debería ser
propietario de sus hijos: si se elimina un nodo padre, sus hijos también deberían
eliminarse. Sin embargo, un hijo no debería ser propietario de su padre: si
eliminamos un nodo hijo, el padre debería seguir existiendo. ¡Este es un caso de
referencias débiles!

En lugar de Rc<T>, usaremos el tipo de parentuso Weak<T>, específicamente


a RefCell<Weak<Node>>. Nuestra Nodedefinición de estructura se ve así:
Rust
jueves, 22 de mayo de 2025 : Página 446 de 719

Nombre de archivo: 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á hacer referencia a su nodo padre, pero no será propietario de este.
En el Listado 15-28, actualizamos mainesta nueva definición para que el leaf nodo
pueda hacer referencia a su padre branch:

Nombre de archivo: src/main.rs


fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});

println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

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!("leaf parent = {:?}", leaf.parent.borrow().upgrade());


}
Listado 15-28: Un leafnodo con una referencia débil a su nodo padrebranch

La creación del leafnodo es similar al Listado 15-27 con la excepción


del parentcampo: leafcomienza sin un padre, por lo que creamos una
nueva Weak<Node>instancia de referencia vacía.

En este punto, al intentar obtener una referencia al padre leafmediante


el upgrademétodo, obtenemos un Nonevalor. Esto se observa en la salida de la
primera println!instrucción:

leaf parent = None


Rust
jueves, 22 de mayo de 2025 : Página 447 de 719

Al crear el branchnodo, este también tendrá una nueva Weak<Node> referencia en


el parentcampo, ya que branchno tiene un nodo padre. Aún lo tenemos leafcomo
uno de los hijos de [nombre del nodo] branch. Una vez que tenemos
la Nodeinstancia en [nombre del nodo] branch, podemos modificarla leafpara darle
una Weak<Node> referencia a su padre. Usamos el borrow_mutmétodo
en RefCell<Weak<Node>>el parentcampo de [ nombre del nodo leaf] y luego usamos
la Rc::downgradefunción para crear una Weak<Node>referencia a branchdesde
[nombre del Rc<Node>nodo] branch.

Al imprimir leafde nuevo el padre de, esta vez obtendremos una Somevariante que
contiene branch: ¡ahora leafpuede acceder a su padre! Al imprimir leaf, también
evitamos el ciclo que finalmente terminó en un desbordamiento de pila como el
del Listado 15-26; las Weak<Node>referencias se imprimen como (Weak):

leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },


children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

La falta de salida infinita indica que este código no creó un ciclo de referencia.
También podemos comprobarlo observando los valores obtenidos al llamar a
` Rc::strong_county` Rc::weak_count.

Visualizar cambios en strong_countyweak_count

Veamos cómo cambian los valores strong_county de las instancias al crear un


nuevo ámbito interno y mover la creación de a dicho ámbito. De esta manera,
podemos ver qué sucede cuando se crea y se elimina al salir del ámbito. Las
modificaciones se muestran en el Listado 15-29: weak_countRc<Node>branchbranch

Nombre de archivo: src/main.rs


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 {
Rust
jueves, 22 de mayo de 2025 : Página 448 de 719
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});

*leaf.parent.borrow_mut() = Rc::downgrade(&branch);

println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);

println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}

println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());


println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
Listado 15-29: Creación branchen un ámbito interno y examen de recuentos de referencias fuertes y
débiles

Después leafde crearlo, Rc<Node>tiene un conteo fuerte de 1 y un conteo débil de


0. En el ámbito interno, lo creamos branchy lo asociamos con leaf. Al imprimir los
conteos, el Rc<Node>in branch tendrá un conteo fuerte de 1 y un conteo débil de 1
(para leaf.parentapuntar a branchcon un Weak<Node>). Al imprimir los conteos
en leaf, veremos que tendrá un conteo fuerte de 2, porque branchahora tiene un
clon del Rc<Node>of leafalmacenado enbranch.children , pero su valor es 0.

Cuando el ámbito interno finaliza, branchse sale del ámbito y el conteo fuerte de
[número] Rc<Node>disminuye a 0, por lo que Nodese descarta. El conteo débil de
[número] de [número] leaf.parentno influye en Nodesu eliminación, por lo que no se
producen fugas de memoria.

Si intentamos acceder al padre de leafuna vez finalizado el ámbito, obtendremos


" Nonede nuevo". Al final del programa, " Rc<Node>in" leaftiene un conteo fuerte
de 1 y un conteo débil de 0, porque la variable leafes ahora la única referencia a
"de Rc<Node>nuevo".
Rust
jueves, 22 de mayo de 2025 : Página 449 de 719

Toda la lógica que gestiona los conteos y la eliminación de valores está integrada
en Rc<T>y Weak<T>y en sus implementaciones del Droprasgo. Al especificar que la
relación entre un elemento secundario y su elemento primario debe ser
una Weak<T>referencia en la definición de Node, se puede hacer que los nodos
primarios apunten a los secundarios y viceversa sin crear un ciclo de referencia ni
fugas de memoria.

Resumen
Este capítulo abordó cómo usar punteros inteligentes para generar garantías y
compensaciones diferentes a las que Rust ofrece por defecto con referencias
regulares. El Box<T>tipo tiene un tamaño conocido y apunta a datos asignados en
el montón. El Rc<T>tipo registra el número de referencias a los datos en el
montón, de modo que estos puedan tener múltiples propietarios. RefCell<T>Su
mutabilidad interna nos proporciona un tipo que podemos usar cuando
necesitamos un tipo inmutable, pero necesitamos cambiar un valor interno de ese
tipo; además, aplica las reglas de préstamo en tiempo de ejecución en lugar de
en tiempo de compilación.

También se analizaron los rasgos Derefy Drop, que habilitan gran parte de la
funcionalidad de los punteros inteligentes. Exploramos los ciclos de referencia que
pueden causar fugas de memoria y cómo prevenirlas mediante Weak<T>.

Si este capítulo ha despertado tu interés y quieres implementar tus propios


punteros inteligentes, consulta “El Rustonomicon” para obtener más información
útil.

A continuación, hablaremos sobre la concurrencia en Rust. Incluso aprenderás


sobre algunos nuevos indicadores inteligentes.

Concurrencia sin miedo


Gestionar la programación concurrente de forma segura y eficiente es otro de los
principales objetivos de Rust. La programación concurrente , donde las diferentes
partes de un programa se ejecutan de forma independiente, y la programación
paralela... , donde las distintas partes de un programa se ejecutan
simultáneamente, cobran cada vez mayor importancia a medida que más
computadoras aprovechan sus múltiples procesadores. Históricamente,
Rust
jueves, 22 de mayo de 2025 : Página 450 de 719

programar en estos contextos ha sido difícil y propenso a errores: Rust espera


cambiar esto.

Inicialmente, el equipo de Rust pensó que garantizar la seguridad de la memoria y


prevenir los problemas de concurrencia eran dos desafíos separados que debían
resolverse con métodos diferentes. Con el tiempo, el equipo descubrió que los
sistemas de propiedad y tipo son un poderoso conjunto de herramientas para
ayudar a administrar la seguridad de la memoria y los problemas de concurrencia.
Al aprovechar la propiedad y la verificación de tipos, muchos errores de
concurrencia son errores de tiempo de compilación en Rust en lugar de errores de
tiempo de ejecución. Por lo tanto, en lugar de hacerte perder mucho tiempo
tratando de reproducir las circunstancias exactas bajo las cuales ocurre un error
de concurrencia en tiempo de ejecución, el código incorrecto se negará a compilar
y presentará un error que explica el problema. Como resultado, puedes corregir
tu código mientras trabajas en él en lugar de potencialmente después de que se
haya enviado a producción. Hemos apodado este aspecto de
Rust fearless concurrency . Fearless concurrency te permite escribir código libre
de errores sutiles y es fácil de refactorizar sin introducir nuevos errores.

Nota: Para simplificar, nos referiremos a muchos de los problemas como concurrentes en lugar de, para
mayor precisión, decir concurrentes o paralelos . Si este libro tratara sobre concurrencia o paralelismo,
seríamos más específicos. En este capítulo, por favor, sustituya mentalmente concurrentes o
paralelos siempre que usemos concurrentes .

Muchos lenguajes son dogmáticos en cuanto a las soluciones que ofrecen para
gestionar problemas concurrentes. Por ejemplo, Erlang ofrece una funcionalidad
elegante para la concurrencia en el paso de mensajes, pero solo ofrece métodos
poco claros para compartir el estado entre hilos. Admitir solo un subconjunto de
posibles soluciones es una estrategia razonable para lenguajes de alto nivel, ya
que estos prometen beneficios al ceder parte del control para obtener
abstracciones. Sin embargo, se espera que los lenguajes de bajo nivel
proporcionen la solución con el mejor rendimiento en cualquier situación dada y
tengan menos abstracciones sobre el hardware. Por lo tanto, Rust ofrece diversas
herramientas para modelar problemas de la forma más adecuada para su
situación y requisitos.

Estos son los temas que cubriremos en este capítulo:


Rust
jueves, 22 de mayo de 2025 : Página 451 de 719

 Cómo crear subprocesos para ejecutar múltiples fragmentos de código al


mismo tiempo
 Concurrencia en el paso de mensajes , donde los canales envían mensajes
entre subprocesos
 Estado compartido , donde varios subprocesos tienen acceso a algún dato
 Los rasgos Syncy 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.

Uso de subprocesos para ejecutar código


simultáneamente
En la mayoría de los sistemas operativos actuales, el código de un programa se
ejecuta en un proceso , y el sistema operativo gestiona varios procesos
simultáneamente. Dentro de un programa, también puede haber partes
independientes que se ejecutan simultáneamente. Las funciones que ejecutan
estas partes independientes se denominan subprocesos . Por ejemplo, un servidor
web podría tener varios subprocesos para poder responder a más de una solicitud
simultáneamente.

Dividir el cálculo de tu programa en varios subprocesos para ejecutar varias


tareas simultáneamente puede mejorar el rendimiento, pero también añade
complejidad. Dado que los subprocesos pueden ejecutarse simultáneamente, no
existe una garantía inherente sobre el orden en que se ejecutarán las partes de tu
código en los diferentes subprocesos. Esto puede generar problemas como:

 Condiciones de carrera, donde los subprocesos acceden a datos o recursos


en un orden inconsistente
 Interbloqueos, donde dos subprocesos se esperan uno al otro, lo que impide
que ambos subprocesos continúen
 Errores que ocurren solo en ciertas situaciones y son difíciles de reproducir
y solucionar de manera confiable.

Rust intenta mitigar los efectos negativos del uso de subprocesos, pero
programar en un contexto multiproceso aún requiere una reflexión cuidadosa y
una estructura de código diferente a la de los programas que se ejecutan en un
solo subproceso.
Rust
jueves, 22 de mayo de 2025 : Página 452 de 719

Los lenguajes de programación implementan hilos de diferentes maneras, y


muchos sistemas operativos proporcionan una API que el lenguaje puede llamar
para crear nuevos hilos. La biblioteca estándar de Rust utiliza un modelo 1:1 de
implementación de hilos, donde un programa utiliza un hilo del sistema operativo
por cada hilo del lenguaje. Existen paquetes que implementan otros modelos de
subprocesos que ofrecen diferentes ventajas respecto al modelo 1:1. (El sistema
asíncrono de Rust, que veremos en el siguiente capítulo, también ofrece otro
enfoque para la concurrencia).

Creando un nuevo hilo conspawn

Para crear un nuevo hilo, llamamos a la thread::spawnfunción y le pasamos un


cierre (hablamos de cierres en el capítulo 13) que contiene el código que
queremos ejecutar en el nuevo hilo. El ejemplo del Listado 16-1 imprime texto de
un hilo principal y texto de un nuevo hilo:

Nombre de archivo: src/main.rs


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));
}
}
Listado 16-1: Creación de un nuevo hilo para imprimir una cosa mientras el hilo principal imprime otra
cosa

Tenga en cuenta que cuando se completa el hilo principal de un programa Rust,


todos los hilos generados se cierran, independientemente de si han terminado de
ejecutarse. El resultado de este programa puede variar ligeramente cada vez,
pero se verá similar al siguiente:

hi number 1 from the main thread!


hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
Rust
jueves, 22 de mayo de 2025 : Página 453 de 719
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

Las llamadas para thread::sleepforzar la detención de la ejecución de un hilo


durante un breve periodo, permitiendo la ejecución de otro. Los hilos
probablemente se turnarán, pero esto no está garantizado: depende de cómo los
programe el sistema operativo. En esta ejecución, el hilo principal imprimió
primero, aunque la sentencia de impresión del hilo generado aparece primero en
el código. Y aunque le indicamos al hilo generado que imprimiera hasta... i 9, solo
llegó a 5 antes de que el hilo principal se cerrara.

Si ejecuta este código y solo ve el resultado del hilo principal, o no ve ninguna


superposición, intente aumentar los números en los rangos para crear más
oportunidades para que el sistema operativo cambie entre los hilos.

Esperando a que todos los hilos terminen


usando joincontroladores

El código en el Listado 16-1 no solo detiene el hilo generado de manera


prematura la mayor parte del tiempo debido a la finalización del hilo principal,
sino que debido a que no hay garantía del orden en el que se ejecutan los hilos,
tampoco podemos garantizar que el hilo generado se ejecute en absoluto.

Podemos solucionar el problema de que el hilo generado no se ejecute o finalice


prematuramente guardando el valor de retorno de thread::spawnen una variable. El
tipo de retorno de thread::spawnes JoinHandle. A JoinHandlees un valor propio que, al
llamar al joinmétodo sobre él, esperará a que finalice su hilo. El Listado 16-2
muestra cómo usar el JoinHandledel hilo creado en el Listado 16-1 y llamar joinpara
asegurar que el hilo generado finalice antes de mainsalir:

Nombre de archivo: 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));
}
});
Rust
jueves, 22 de mayo de 2025 : Página 454 de 719
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}

handle.join().unwrap();
}
Listado 16-2: Guardar un JoinHandleformulario thread::spawnpara garantizar que el hilo se ejecute hasta
su finalización

Al llamar joinal manejador, se bloquea el hilo en ejecución hasta que el hilo


representado por el manejador finaliza. Bloquear un hilo significa que no puede
realizar ningún trabajo ni salir. Dado que la llamada se realizó joindespués del
bucle del hilo principal for, ejecutar el Listado 16-2 debería generar un resultado
similar a este:

hi number 1 from the main thread!


hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

Los dos hilos continúan alternándose, pero el hilo principal espera debido a la
llamada a handle.join()y no finaliza hasta que finaliza el hilo generado.

Pero veamos qué sucede cuando, en cambio, nos movemos handle.join()antes


del forbucle en main, de esta manera:

Nombre de archivo: 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();
Rust
jueves, 22 de mayo de 2025 : Página 455 de 719

for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}

El hilo principal esperará a que el hilo generado termine y luego ejecutará


su forbucle, por lo que la salida ya no se intercalará, como se muestra aquí:

hi number 1 from the spawned thread!


hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!

Pequeños detalles, como dónde joinse llama, pueden afectar si los subprocesos se
ejecutan al mismo tiempo o no.

Uso de movecierres con subprocesos

A menudo usaremos la movepalabra clave con los cierres pasados thread::spawn


porque el cierre tomará la propiedad de los valores que utiliza del entorno,
transfiriendo así la propiedad de esos valores de un hilo a otro. En la
sección "Capturar referencias o transferir la propiedad" del capítulo 13, lo
analizamos moveen el contexto de los cierres. Ahora, nos centraremos más en la
interacción entre moveythread::spawn .

Observe en el Listado 16-1 que la clausura a la que pasamos thread::spawnno


acepta argumentos: no usamos datos del hilo principal en el código del hilo
generado. Para usar datos del hilo principal en el hilo generado, la clausura del
hilo generado debe capturar los valores que necesita. El Listado 16-3 muestra un
intento de crear un vector en el hilo principal y usarlo en el hilo generado. Sin
embargo, esto aún no funciona, como verá en breve.

Nombre de archivo: src/main.rs


Rust
jueves, 22 de mayo de 2025 : Página 456 de 719
use std::thread;

fn main() {
let v = vec![1, 2, 3];

let handle = thread::spawn(|| {


println!("Here's a vector: {v:?}");
});

handle.join().unwrap();
}
Listado 16-3: Intento de utilizar un vector creado por el hilo principal en otro hilo

El cierre usa v, por lo que lo capturará vy lo integrará en su entorno. Dado


que thread::spawnse ejecuta en un nuevo hilo, deberíamos poder acceder va él. Sin
embargo, al compilar 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 [<sub>y</sub> v] y, como println!solo necesita una


referencia a [<sub>,</sub>] v, el cierre intenta tomar prestado
[<sub> v.</sub>]. Sin embargo, existe un problema: Rust no puede determinar
cuánto tiempo se ejecutará el hilo generado, por lo que no sabe si la referencia a
[<sub> vsiempre</sub> será válida.
Rust
jueves, 22 de mayo de 2025 : Página 457 de 719

El listado 16-4 proporciona un escenario en el que es más probable que haya una
referencia v que no sea válida:

Nombre de archivo: src/main.rs

use std::thread;

fn main() {
let v = vec![1, 2, 3];

let handle = thread::spawn(|| {


println!("Here's a vector: {v:?}");
});

drop(v); // oh no!

handle.join().unwrap();
}
Listado 16-4: Un hilo con un cierre que intenta capturar una referencia vdesde un hilo principal que
abandonav

Si Rust nos permitiera ejecutar este código, es posible que el hilo generado se
ponga inmediatamente en segundo plano sin ejecutarse. El hilo generado tiene
una referencia a vdentro, pero el hilo principal cierra inmediatamente v, usando
la dropfunción que explicamos en el Capítulo 15. Entonces, cuando el hilo
generado empieza a ejecutarse, vdeja de ser válido, por lo que una referencia a él
también es inválida. ¡Oh, no!

Para corregir el error del compilador en el Listado 16-3, podemos usar los
consejos del mensaje de error:

help: to force the closure to take ownership of `v` (and any other referenced variables), use
the `move` keyword
|
6| let handle = thread::spawn(move || {
| ++++

Al añadir la movepalabra clave antes del cierre, lo obligamos a tomar posesión de


los valores que utiliza, en lugar de permitir que Rust infiera que debe tomarlos
prestados. La modificación del Listado 16-3, que se muestra en el Listado 16-5, se
compilará y ejecutará según lo previsto:

Nombre de archivo: src/main.rs


use std::thread;

fn main() {
Rust
jueves, 22 de mayo de 2025 : Página 458 de 719
let v = vec![1, 2, 3];

let handle = thread::spawn(move || {


println!("Here's a vector: {v:?}");
});

handle.join().unwrap();
}
Listado 16-5: Uso de la movepalabra clave para forzar que un cierre tome posesión de los valores que
utiliza

Podríamos intentar lo mismo para corregir el código del Listado 16-4, donde el
hilo principal realizaba una llamada dropmediante un movecierre. Sin embargo,
esta solución no funcionará porque lo que el Listado 16-4 intenta hacer no está
permitido por otra razón. Si agregáramos algo moveal cierre, nos moveríamos val
entorno del cierre y ya no podríamos llamarlo dropen el hilo principal. En su lugar,
obtendríamos este error del compilador:

$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the
`Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {v:?}");
| - variable moved due to use in closure
...
10 | drop(v); // oh no!
| ^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error

¡Las reglas de propiedad de Rust nos han salvado de nuevo! Recibimos un error
en el código del Listado 16-3 porque Rust estaba siendo conservador y solo
tomaba prestado vpara el hilo, lo que significaba que el hilo principal, en teoría,
podría invalidar la referencia del hilo generado. Al indicarle a Rust que transfiera
la propiedad val hilo generado, le garantizamos que el hilo principal vya no la
usará. Si modificamos el Listado 16-4 de la misma manera, violamos las reglas de
propiedad al intentar usar ven el hilo principal. Esta movepalabra clave anula el
método conservador predeterminado de Rust de tomar prestado; no nos permite
violar las reglas de propiedad.
Rust
jueves, 22 de mayo de 2025 : Página 459 de 719

Con un conocimiento básico de los subprocesos y la API de subprocesos, veamos


qué podemos hacer con los subprocesos.

Uso del paso de mensajes para transferir datos


entre subprocesos
Un enfoque cada vez más popular para garantizar la concurrencia segura es el
paso de mensajes , donde los hilos o actores se comunican enviándose mensajes
con datos. Esta idea se encuentra en un eslogan de la documentación del
lenguaje Go : «No te comuniques compartiendo memoria; en su lugar, comparte
memoria comunicándote».

Para lograr la concurrencia en el envío de mensajes, la biblioteca estándar de


Rust proporciona una implementación de canales . Un canal es un concepto
general de programación mediante el cual se envían datos de un hilo a otro.

En programación, un canal se puede imaginar como un canal de agua direccional,


como un arroyo o un río. Si se introduce algo como un patito de goma en un río,
este se desplazará río abajo hasta el final del cauce.

Un canal tiene dos mitades: un transmisor y un receptor. La mitad del transmisor


es el punto río arriba, donde se introducen los patitos de goma, y la mitad del
receptor es donde el patito termina río abajo. Una parte del código invoca
métodos en el transmisor con los datos que se desean enviar, y otra parte verifica
la recepción de mensajes. Se dice que un canal está cerrado si se desconecta la
mitad del transmisor o del receptor.

Aquí, desarrollaremos un programa con un hilo para generar valores y enviarlos


por un canal, y otro hilo para recibirlos y imprimirlos. Enviaremos valores simples
entre hilos mediante un canal para ilustrar esta función. Una vez familiarizado con
la técnica, podrá usar canales para cualquier hilo que necesite comunicarse entre
sí, como un sistema de chat o un sistema donde varios hilos realizan partes de un
cálculo y las envían 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 no se compilará todavía porque Rust no puede
determinar qué tipo de valores queremos enviar a través del canal.

Nombre de archivo: src/main.rs


Rust
jueves, 22 de mayo de 2025 : Página 460 de 719

use std::sync::mpsc;

fn main() {
let (tx, rx) = mpsc::channel();
}

Listado 16-6: Creación de un canal y asignación de las dos mitades a txyrx

Creamos un nuevo canal usando la mpsc::channelfunción; mpsc( múltiples


productores, un solo consumidor) . En resumen, la forma en que la biblioteca
estándar de Rust implementa los canales significa que un canal puede tener
múltiples extremos emisores que producen valores, pero solo un
extremo receptor que los consume. Imaginemos varios flujos que fluyen juntos en
un gran río: todo lo que se envíe por cualquiera de los flujos terminará en un río al
final. Comenzaremos con un solo productor por ahora, pero agregaremos varios
productores cuando este ejemplo funcione.

La mpsc::channelfunción devuelve una tupla, cuyo primer elemento es el emisor (el


transmisor) y el segundo es el receptor (el receptor). Las abreviaturas txy rxse
utilizan tradicionalmente en muchos campos para el transmisor y el receptor ,
respectivamente, por lo que nombramos nuestras variables como tales para
indicar cada extremo. Usamos una letsentencia con un patrón que desestructura
las tuplas; analizaremos el uso de patrones en letsentencias y la
desestructuración en el capítulo 19. Por ahora, sepa que usar una letsentencia de
esta manera es un método conveniente para extraer las partes de la tupla
devuelta por mpsc::channel.

Muevamos el extremo transmisor a un hilo generado y hagamos que envíe una


cadena 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 río
arriba o enviar un mensaje de chat de un hilo a otro.

Nombre de archivo: 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();
Rust
jueves, 22 de mayo de 2025 : Página 461 de 719
});
}
Listado 16-7: Pasar txa un hilo generado y enviar un "hola"

Nuevamente, usamos thread::spawnpara crear un nuevo hilo y luego


usamos move para movernos txal cierre, de modo que el hilo generado posea
[nombre del hilo] tx. El hilo generado necesita poseer el transmisor para poder
enviar mensajes a través del canal. El transmisor tiene un sendmétodo que toma
el valor que queremos enviar. El sendmétodo devuelve un Result<T, E>tipo, por lo
que si el receptor ya se ha descartado y no hay dónde enviar un valor, la
operación de envío devolverá un error. En este ejemplo, llamamos unwrapa pánico
en caso de error. Sin embargo, en una aplicación real, lo gestionaríamos
correctamente: vuelva al Capítulo 9 para revisar las estrategias para el manejo
adecuado de errores.

En el Listado 16-8, obtendremos el valor del receptor en el hilo principal. Esto es


como recuperar el patito de goma del agua al final del río o recibir un mensaje de
chat.

Nombre de archivo: 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();
});

let received = rx.recv().unwrap();


println!("Got: {received}");
}
Listado 16-8: Recibir el valor “hi” en el hilo principal e imprimirlo

El receptor tiene dos métodos útiles: recvy try_recv. Usamos recv, abreviatura
de receive , que bloqueará la ejecución del hilo principal y esperará hasta que se
envíe un valor por el canal. Una vez enviado un valor, recvlo devolverá en
un Result<T, E>. Cuando el transmisor se cierra, recvdevolverá un error para
indicar que no se recibirán más valores.

El try_recvmétodo no se bloquea, sino que devuelve Result<T, E> inmediatamente


un Okvalor que contiene un mensaje si hay uno disponible y un Err valor si no hay
Rust
jueves, 22 de mayo de 2025 : Página 462 de 719

mensajes en este momento. Usarlo try_recves útil si este hilo tiene otras tareas
que realizar mientras espera mensajes: podríamos escribir un bucle que
llame try_recvcon frecuencia, gestione un mensaje si hay uno disponible y, de lo
contrario, realice otras tareas durante un tiempo hasta que se verifique de nuevo.

Lo hemos utilizado recven este ejemplo para simplificar; no tenemos ningún otro
trabajo que hacer para el hilo principal excepto esperar mensajes, por lo que
bloquear el hilo principal es apropiado.

Cuando ejecutamos el código del Listado 16-8, veremos el valor impreso desde el
hilo principal:

Got: hi

¡Perfecto!

Canales y transferencia de propiedad

Las reglas de propiedad son vitales en el envío de mensajes, ya que ayudan a


escribir código seguro y concurrente. La ventaja de considerar la propiedad en
todos los programas de Rust es evitar errores en la programación concurrente.
Hagamos un experimento para mostrar cómo los canales y la propiedad se
combinan para prevenir problemas: intentaremos usar un valvalor en el hilo
generado después de enviarlo por el canal. Pruebe a compilar el código del
Listado 16-9 para ver por qué no se permite:

Nombre de archivo: 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}");
});

let received = rx.recv().unwrap();


println!("Got: {received}");
}
Listado 16-9: Intentando usarlo valdespués de haberlo enviado por el canal
Rust
jueves, 22 de mayo de 2025 : Página 463 de 719

Aquí, intentamos imprimir valdespués de enviarlo por el canal mediante [nombre


del hilo] tx.send. Permitir esto sería una mala idea: una vez enviado el valor a otro
hilo, este podría modificarlo o descartarlo antes de que intentemos usarlo de
nuevo. Potencialmente, las modificaciones del 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 del Listado 16-9:

$ cargo run
Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:10:26
|
8 | let val = String::from("hi");
| --- move occurs because `val` has type `String`, which does not implement the
`Copy` trait
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!("val is {val}");
| ^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the
expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.
error: could not compile `message-passing` (bin "message-passing") due to 1 previous error

Nuestro error de concurrencia ha provocado un error de compilación.


La sendfunción toma posesión de su parámetro y, al mover el valor, el receptor
también lo toma. Esto evita que usemos el valor accidentalmente después de
enviarlo; el sistema de propiedad verifica que todo esté correcto.

Enviar múltiples valores y ver al receptor esperando

El código del Listado 16-8 se compiló y ejecutó, pero no mostró claramente que
dos hilos separados se comunicaban entre sí a través del canal. En el Listado 16-
10, hemos realizado algunas modificaciones que comprobarán que el código del
Listado 16-8 se ejecuta simultáneamente: el hilo generado ahora enviará varios
mensajes y se detendrá un segundo entre cada uno.

Nombre de archivo: src/main.rs


use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
let (tx, rx) = mpsc::channel();
Rust
jueves, 22 de mayo de 2025 : Página 464 de 719
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];

for val in vals {


tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});

for received in rx {
println!("Got: {received}");
}
}
Listado 16-10: Envío de múltiples mensajes y pausa entre cada uno

Esta vez, el hilo generado tiene un vector de cadenas que queremos enviar al hilo
principal. Las iteramos, enviándolas individualmente, y hacemos una pausa entre
cada una llamando a la thread::sleepfunción con un Durationvalor de 1 segundo.

En el hilo principal, ya no llamamos a la recvfunción explícitamente; en su lugar, la


tratamos rxcomo un iterador. Imprimimos cada valor recibido. Al cerrar el canal, la
iteración finaliza.

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

Como no tenemos ningún código que pause o demore el forbucle en el hilo


principal, podemos saber que el hilo principal está esperando recibir valores del
hilo generado.

Creación de múltiples productores mediante la clonación del


transmisor

Anteriormente mencionamos que mpscera un acrónimo de "múltiple productor,


único consumidor" . Utilicemos mpscy ampliemos el código del Listado 16-10 para
Rust
jueves, 22 de mayo de 2025 : Página 465 de 719

crear múltiples hilos que envíen valores al mismo receptor. Podemos hacerlo
clonando el transmisor, como se muestra en el Listado 16-11:

Nombre de archivo: src/main.rs


// --snip--

let (tx, rx) = mpsc::channel();

let tx1 = tx.clone();


thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];

for val in vals {


tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});

thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];

for val in vals {


tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});

for received in rx {
println!("Got: {received}");
}

// --snip--
Listado 16-11: Envío de múltiples mensajes de múltiples productores

Esta vez, antes de crear el primer hilo generado, llamamos cloneal transmisor.
Esto nos dará un nuevo transmisor que podemos pasar al primer hilo generado.
Pasamos el transmisor original a un segundo hilo generado. Esto nos da dos hilos,
cada uno enviando mensajes diferentes al mismo receptor.

Cuando ejecute el código, su salida debería verse así:


Rust
jueves, 22 de mayo de 2025 : Página 466 de 719
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, dependiendo de tu sistema. Esto es
lo que hace que la concurrencia sea interesante y a la vez difícil. Si experimentas
con [nombre del método] thread::sleep, asignándole diferentes valores en los
distintos subprocesos, cada ejecución será menos determinista y generará una
salida distinta cada vez.

Ahora que hemos visto cómo funcionan los canales, veamos un método diferente
de concurrencia.

Concurrencia de estado compartido


El paso de mensajes es una buena manera de gestionar la concurrencia, pero no
es la única. Otro método sería que varios subprocesos accedieran a los mismos
datos compartidos. Consideremos de nuevo esta parte del eslogan de la
documentación del lenguaje Go: «No se comuniquen compartiendo memoria».

¿Cómo sería la comunicación compartiendo memoria? Además, ¿por qué los


entusiastas de la transmisión de mensajes advierten que no se debe compartir la
memoria?

En cierto modo, los canales en cualquier lenguaje de programación son similares


a la propiedad única, ya que una vez que se transfiere un valor a través de un
canal, ya no se debe usar. La concurrencia en memoria compartida es similar a la
propiedad múltiple: varios hilos pueden acceder a la misma ubicación de memoria
simultáneamente. Como se vio en el Capítulo 15, donde los punteros inteligentes
posibilitaron la propiedad múltiple, esta puede añadir complejidad, ya que estos
diferentes propietarios necesitan gestión. El sistema de tipos y las reglas de
propiedad de Rust facilitan enormemente esta gestión. Por ejemplo, veamos los
mutex, una de las primitivas de concurrencia más comunes para la memoria
compartida.

Uso de mutex para permitir el acceso a los datos desde un hilo a


la vez
Rust
jueves, 22 de mayo de 2025 : Página 467 de 719

Mutex es la abreviatura de exclusión mutua . Un mutex permite que solo un hilo


acceda a ciertos datos en un momento dado. Para acceder a los datos de un
mutex, un hilo debe indicar primero que desea acceder solicitando la adquisición
del bloqueo del mutex . El bloqueo es una estructura de datos que forma parte
del mutex y que registra quién tiene acceso exclusivo a los datos. Por lo tanto, el
mutex protege los datos que contiene mediante el sistema de bloqueo.

Los mutex tienen fama de ser difíciles de usar porque hay que recordar dos
reglas:

 Debe intentar adquirir el bloqueo antes de utilizar los datos.


 Cuando haya terminado con los datos que protege el mutex, deberá
desbloquear los datos para que otros subprocesos puedan adquirir el
bloqueo.

Para una metáfora práctica de un mutex, imaginemos una mesa redonda en una
conferencia con un solo micrófono. Antes de que un panelista pueda hablar, debe
pedir o indicar que quiere usar el micrófono. Una vez que lo recibe, puede hablar
todo el tiempo que desee y luego cederlo al siguiente panelista que lo solicite. Si
un panelista olvida entregar el micrófono al terminar de usarlo, nadie más puede
hablar. Si la gestión del micrófono compartido falla, el panel no funcionará según
lo previsto.

La gestión correcta de mutex puede ser increíblemente complicada, razón por la


cual muchos se entusiasman con los canales. Sin embargo, gracias al sistema de
tipos y las reglas de propiedad de Rust, es imposible equivocarse al bloquear y
desbloquear.

La API deMutex<T>

Como ejemplo de cómo usar un mutex, comencemos usando un mutex en un


contexto de un solo subproceso, como se muestra en el Listado 16-12:

Nombre de archivo: src/main.rs


use std::sync::Mutex;

fn main() {
let m = Mutex::new(5);

{
let mut num = m.lock().unwrap();
*num = 6;
Rust
jueves, 22 de mayo de 2025 : Página 468 de 719
}

println!("m = {m:?}");
}
Listado 16-12: Exploración de la API Mutex<T>en un contexto de un solo subproceso para simplificar

Como con muchos tipos, creamos un [nombre de tipo] Mutex<T>usando la función


asociada new. Para acceder a los datos dentro del mutex, usamos el lockmétodo
para adquirir el bloqueo. Esta llamada bloqueará el hilo actual, impidiéndole
realizar ninguna tarea hasta que sea nuestro turno para obtener el bloqueo.

La llamada a lockfallaría si otro hilo con el bloqueo entrara en pánico. En ese caso,
nadie podría obtener el bloqueo, así que hemos optado por que unwrapeste hilo
entre en pánico si nos encontramos en esa situación.

Tras adquirir el bloqueo, podemos tratar el valor de retorno, numen este caso
llamado, como una referencia mutable a los datos internos. El sistema de tipos
garantiza que adquiramos un bloqueo antes de usar el valor en [nombre del
objeto] m. El tipo de [nombre del objeto] mes [nombre del objeto] Mutex<i32>y no
[nombre del objeto] i32, por lo que debemos llamar lockpara poder usar
el i32 valor. No podemos olvidarlo; de lo contrario, el sistema de tipos no nos
permitirá acceder al interior i32 .

Como podría sospechar, Mutex<T>es un puntero inteligente. Más precisamente, la


llamada a lock devuelve un puntero inteligente llamado MutexGuard, envuelto en
un LockResultque manejamos con la llamada a unwrap. El MutexGuardpuntero
inteligente implementa Derefto que apunta a nuestros datos internos; el puntero
inteligente también tiene una Dropimplementación que libera el bloqueo
automáticamente cuando a MutexGuardsale del ámbito, lo que ocurre al final del
ámbito interno. Como resultado, no corremos el riesgo de olvidar liberar el
bloqueo y evitar que otros subprocesos usen el mutex, ya que la liberación del
bloqueo se realiza automáticamente.

Después de eliminar el bloqueo, podemos imprimir el valor del mutex y ver que
pudimos cambiar el interno i32a 6.

Compartir Mutex<T>entre varios subprocesos

Ahora, intentemos compartir un valor entre varios hilos usando Mutex<T>.


Activaremos 10 hilos y haremos que cada uno incremente el valor de un contador
en 1, de modo que el contador pase de 0 a 10. El siguiente ejemplo del Listado
Rust
jueves, 22 de mayo de 2025 : Página 469 de 719

16-13 tendrá un error de compilación, y lo usaremos para aprender más sobre su


uso Mutex<T>y cómo Rust nos ayuda a usarlo correctamente.

Nombre de archivo: 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);
}

for handle in handles {


handle.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());


}
Listado 16-13: Diez subprocesos incrementan cada uno un contador protegido por unMutex<T>

Creamos una countervariable para almacenar un `an` i32dentro de `a` Mutex<T>,


como hicimos en el Listado 16-12. A continuación, creamos 10 hilos iterando
sobre un rango de números. Usamos thread::spawny asignamos a todos los hilos el
mismo cierre: uno que mueve el contador al hilo, adquiere un bloqueo en
` Mutex<T>al llamar al lockmétodo` y luego suma 1 al valor del mutex. Cuando un
hilo termina de ejecutar su cierre,num saldrá del ámbito` y liberará el bloqueo
para que otro hilo pueda adquirirlo.

En el hilo principal, recopilamos todos los controladores de unión. Luego, como


hicimos en el Listado 16-2, llamamos joina cada controlador para asegurarnos de
que todos los hilos finalicen. En ese momento, el hilo principal adquirirá el
bloqueo e imprimirá el resultado de este programa.

Insinuamos que este ejemplo no compilaría. ¡Ahora veamos por qué!

$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
Rust
jueves, 22 de mayo de 2025 : Página 470 de 719
error[E0382]: borrow of moved value: `counter`
--> src/main.rs:21:29
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex<i32>`, which does not
implement the `Copy` trait
...
8 | for _ in 0..10 {
| -------------- inside of this loop
9 | let handle = thread::spawn(move || {
| ------- value moved into closure here, in previous iteration of loop
...
21 | println!("Result: {}", *counter.lock().unwrap());
| ^^^^^^^ value borrowed here after move
|
help: consider moving the expression out of the loop so it is only moved once
|
8 ~ let mut value = counter.lock();
9 ~ for _ in 0..10 {
10 | let handle = thread::spawn(move || {
11 ~ let mut num = value.unwrap();
|

For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error

El mensaje de error indica que el countervalor se movió en la iteración anterior del


bucle. Rust nos indica que no podemos mover la propiedad de countera varios
subprocesos. Corrijamos el error del compilador con un método de propiedad
múltiple que explicamos en el capítulo 15.

Propiedad múltiple con múltiples subprocesos

En el Capítulo 15, asignamos a un valor varios propietarios usando el puntero


inteligente Rc<T>para crear un valor de referencia contado. Hagamos lo mismo
aquí y veamos qué sucede. Encapsularemos el Mutex<T>in Rc<T>en el Listado 16-
14 y clonaremos el Rc<T>antes de transferir la propiedad al hilo.

Nombre de archivo: src/main.rs

use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
Rust
jueves, 22 de mayo de 2025 : Página 471 de 719
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();

*num += 1;
});
handles.push(handle);
}

for handle in handles {


handle.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());


}
Listado 16-14: Intento de usar Rc<T>para permitir que varios subprocesos posean elMutex<T>

Una vez más, compilamos y obtenemos… ¡diferentes errores! El compilador nos


está enseñando mucho.

$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
|| |
|| required by a bound introduced by this call
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented
for `Rc<Mutex<i32>>`, which is required by `{closure@src/main.rs:11:36: 11:43}: Send`
note: required because it's used within this closure
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/std/src/thread/
mod.rs:675:8
|
672 | pub fn spawn<F, T>(f: F) -> JoinHandle<T>
| ----- required by a bound in this function
...
675 | F: Send + 'static,
| ^^^^ required by this bound in `spawn`
Rust
jueves, 22 de mayo de 2025 : Página 472 de 719

For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error

¡Vaya, ese mensaje de error es muy extenso! Aquí está la parte importante en la
que debemos centrarnos: `Rc<Mutex<i32>>` cannot be sent between threads safely . El
compilador también nos indica el motivo: the trait `Send` is not implemented for
`Rc<Mutex<i32>>`. Hablaremos de ello Senden la siguiente sección: es una de las
características que garantiza que los tipos que usamos con los subprocesos estén
diseñados para usarse en situaciones concurrentes.

Lamentablemente, Rc<T>no es seguro compartirlo entre subprocesos.


Al Rc<T> gestionar el recuento de referencias, lo suma con cada llamada cloney lo
resta al descartar cada clon. Sin embargo, no utiliza primitivas de concurrencia
para garantizar que los cambios en el recuento no puedan ser interrumpidos por
otro subproceso. Esto podría generar recuentos erróneos, errores sutiles que, a su
vez, podrían provocar fugas de memoria o la eliminación de un valor antes de
terminar. Necesitamos un tipo exactamente igual, Rc<T>pero que realice cambios
en el recuento de referencias de forma segura para subprocesos.

Conteo de referencia atómica conArc<T>

Afortunadamente, Arc<T> un tipo como Rc<T>ese es seguro de usar en


situaciones concurrentes. La a significa atómico , lo que significa que es un
tipo contado por referencia atómica . Los atómicos son un tipo adicional de
primitivo de concurrencia que no abordaremos en detalle aquí: consulte la
documentación de la biblioteca estándar para std::sync::atomicobtener más
detalles. En este punto, solo necesita saber que los atómicos funcionan como
tipos primitivos, pero se pueden compartir de forma segura entre subprocesos.

Quizás te preguntes por qué no todos los tipos primitivos son atómicos y por qué
los tipos de la biblioteca estándar no están implementados para usarse Arc<T>por
defecto. La razón es que la seguridad de subprocesos conlleva una penalización
de rendimiento que solo conviene pagar cuando realmente es necesario. Si solo
realizas operaciones con valores dentro de un solo subproceso, tu código puede
ejecutarse más rápido si no tiene que aplicar las garantías que ofrecen los tipos
atómicos.
Rust
jueves, 22 de mayo de 2025 : Página 473 de 719

Volvamos a nuestro ejemplo: Arc<T>y Rc<T>tenemos la misma API, así que


corregimos nuestro programa cambiando la uselínea, la llamada a newy la llamada
a clone. El código del Listado 16-15 finalmente se compilará y ejecutará:

Nombre de archivo: src/main.rs


use std::sync::{Arc, Mutex};
use std::thread;

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);
}

for handle in handles {


handle.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());


}
Listado 16-15: Uso de un Arc<T>para encapsular Mutex<T>y poder compartir la propiedad entre varios
subprocesos

Este código imprimirá lo siguiente:

Result: 10

¡Lo logramos! Contamos del 0 al 10, lo cual puede no parecer muy impresionante,
pero nos enseñó mucho sobre Mutex<T>seguridad de subprocesos. También
puedes usar la estructura de este programa para realizar operaciones más
complejas que simplemente incrementar un contador. Con esta estrategia,
puedes dividir un cálculo en partes independientes, dividirlas entre subprocesos y
luego usar un Mutex<T>para que cada subproceso actualice el resultado final con
su parte.

Tenga en cuenta que, si realiza operaciones numéricas sencillas, existen tipos


más simples que Mutex<T>los proporcionados por el std::sync::atomicmódulo de la
biblioteca estándar . Estos tipos proporcionan acceso seguro, concurrente y
Rust
jueves, 22 de mayo de 2025 : Página 474 de 719

atómico a tipos primitivos. En este ejemplo, optamos por usar Mutex<T>un tipo
primitivo para centrarnos en su Mutex<T>funcionamiento.

Similitudes entre RefCell<T>/ Rc<T>y Mutex<T>/Arc<T>

Quizás hayas notado que counteres inmutable, pero podríamos obtener una
referencia mutable a su valor; esto significa que Mutex<T>proporciona mutabilidad
interna, como la Cellfamilia. De la misma manera que usamos RefCell<T>en el
Capítulo 15 para permitirnos mutar el contenido dentro de un Rc<T>,
usamos Mutex<T> para mutar el contenido dentro de un Arc<T>.

Otro detalle a tener en cuenta es que Rust no puede protegerte de todo tipo de
errores lógicos cuando usas Mutex<T>. Recuerda en el Capítulo 15 que
usar Rc<T>conllevaba el riesgo de crear ciclos de referencia, donde
dos Rc<T>valores se refieren entre sí, causando fugas de memoria. De igual
forma, Mutex<T>conlleva el riesgo de crear interbloqueos . Estos ocurren cuando
una operación necesita bloquear dos recursos y dos subprocesos han adquirido
cada uno uno de los bloqueos, haciendo que esperen el uno al otro eternamente.
Si te interesan los interbloqueos, intenta crear un programa en Rust que tenga un
interbloqueo; luego, investiga estrategias de mitigación de interbloqueos para
mutex en cualquier lenguaje e intenta implementarlas en Rust. La documentación
de la API de la biblioteca estándar para Mutex<T>y MutexGuardofrece información
útil.

Completaremos este capítulo hablando sobre los Sendrasgos Syncy y cómo


podemos usarlos con tipos personalizados.

Concurrencia extensible con los rasgos Sync ySend


Curiosamente, el lenguaje Rust cuenta con muy pocas funciones de concurrencia.
Casi todas las funciones de concurrencia que hemos abordado en este capítulo
forman parte de la biblioteca estándar, no del lenguaje. Las opciones para
gestionar la concurrencia no se limitan al lenguaje ni a la biblioteca estándar;
puedes crear tus propias funciones de concurrencia o usar las desarrolladas por
otros.

Sin embargo, dos conceptos de concurrencia están incorporados en el lenguaje:


los std::markerrasgos Syncy Send.
Rust
jueves, 22 de mayo de 2025 : Página 475 de 719

Permitir la transferencia de propiedad entre subprocesos


conSend

El Sendatributo marcador indica que la propiedad de los valores del tipo que
implementa Sendpuede transferirse entre subprocesos. Casi todos los tipos de
Rust son Send, pero hay algunas excepciones, como Rc<T>: esto no puede ser
así, Sendya que si se clonó un Rc<T>valor e intentó transferir la propiedad del clon
a otro subproceso, ambos subprocesos podrían actualizar el recuento de
referencias simultáneamente. Por esta razón, Rc<T>se implementa para su uso en
situaciones de un solo subproceso donde no se desea pagar la penalización de
rendimiento de la seguridad de subprocesos.

Por lo tanto, el sistema de tipos de Rust y los límites de rasgos garantizan que
nunca se pueda enviar accidentalmente un Rc<T>valor entre subprocesos de
forma insegura. Al intentar hacer esto en el Listado 16-14, obtuvimos el error the
trait Send is not implemented for Rc<Mutex<i32>> . Al cambiar a Arc<T>, que es Send,
el código se compiló.

Cualquier tipo compuesto enteramente de Sendtipos también se marca


automáticamente como Send. Casi todos los tipos primitivos son Send, salvo los
punteros sin formato, que analizaremos en el capítulo 20.

Permitir el acceso desde múltiples subprocesos conSync

El Syncrasgo marcador indica que es seguro que el tipo que implementa Syncsea
referenciado desde múltiples subprocesos. En otras palabras, cualquier
tipo Tes Syncsi &T(una referencia inmutable a T) es Send, lo que significa que la
referencia puede enviarse de forma segura a otro subproceso. Similar a Send, los
tipos primitivos son Sync, y los tipos compuestos completamente por tipos que
son Synctambién son Sync.

El puntero inteligente Rc<T>tampoco es Syncpor las mismas razones por las que
no es Send. El RefCell<T>tipo (del que hablamos en el Capítulo 15) y la familia
de Cell<T>tipos relacionados no son Sync. La implementación de la comprobación
de préstamos RefCell<T>en tiempo de ejecución no es segura para subprocesos. El
puntero inteligente Mutex<T>sí lo es Syncy puede usarse para compartir el acceso
con varios subprocesos, como se vio en la sección "Compartir un Mutex<T>entre
varios subprocesos" .

Implementarlo Sendmanualmente Syncno es seguro


Rust
jueves, 22 de mayo de 2025 : Página 476 de 719

Dado que los tipos compuestos por rasgos Sendy Synctambién son
automáticamente Sendy Sync, no es necesario implementarlos manualmente.
Como rasgos marcadores, ni siquiera requieren métodos de implementación.
Simplemente son útiles para aplicar invariantes relacionadas con la concurrencia.

Implementar manualmente estos rasgos implica implementar código inseguro de


Rust. Hablaremos sobre el uso de código inseguro de Rust en el Capítulo 20; por
ahora, lo importante es que crear nuevos tipos concurrentes que no estén
compuestos Sendpor Syncpartes requiere una cuidadosa reflexión para mantener
las garantías de seguridad. "El Rustonomicon" ofrece más información sobre estas
garantías y cómo mantenerlas.

Resumen
Esto no es lo último que verá sobre concurrencia en este libro: todo el siguiente
capítulo se centra en la programación asincrónica, y el proyecto del Capítulo 21
utilizará los conceptos de este capítulo en una situación más realista que los
ejemplos más pequeños analizados aquí.

Como se mencionó anteriormente, dado que Rust gestiona muy poco la


concurrencia, muchas soluciones de concurrencia se implementan como crates.
Estos evolucionan más rápido que la biblioteca estándar, así que asegúrese de
buscar en línea los crates más modernos para usar en entornos multihilo.

La biblioteca estándar de Rust proporciona canales para el paso de mensajes y


tipos de punteros inteligentes, como Mutex<T>y Arc<T>, que son seguros de usar
en contextos concurrentes. El sistema de tipos y el verificador de préstamos
garantizan que el código que utiliza estas soluciones no genere carreras de datos
ni referencias inválidas. Una vez compilado el código, puede estar seguro de que
se ejecutará correctamente en múltiples hilos sin los errores difíciles de localizar
comunes en otros lenguajes. La programación concurrente ya no es un concepto
al que temer: ¡anímese y haga que sus programas sean concurrentes, sin miedo!

Fundamentos de la programación
asincrónica: Async, Await, Futures y
Streams
Rust
jueves, 22 de mayo de 2025 : Página 477 de 719

Muchas operaciones que le pedimos a la computadora que haga pueden tardar un


tiempo en finalizar. Sería bueno si pudiéramos hacer algo más mientras
esperamos que se completen esos procesos de larga ejecución. Las computadoras
modernas ofrecen dos técnicas para trabajar en más de una operación a la vez:
paralelismo y concurrencia. Sin embargo, una vez que comenzamos a escribir
programas que involucran operaciones paralelas o concurrentes, rápidamente nos
encontramos con nuevos desafíos inherentes a la programación asincrónica ,
donde las operaciones pueden no finalizar secuencialmente en el orden en que se
iniciaron. Este capítulo se basa en el uso de hilos del Capítulo 16 para paralelismo
y concurrencia al presentar un enfoque alternativo a la programación asincrónica:
Futures de Rust, Streams, la asyncy la awaitsintaxis que los respalda, y las
herramientas para administrar y coordinar entre operaciones asincrónicas.

Consideremos un ejemplo. Supongamos que exportas un video de una


celebración familiar, una operación que podría tardar desde minutos hasta horas.
La exportación del video consumirá la mayor cantidad de CPU y GPU posible. Si
solo tuvieras un núcleo de CPU y tu sistema operativo no pausara la exportación
hasta que se completara (es decir, si la ejecutara sincrónicamente ), no podrías
hacer nada más en tu computadora mientras se ejecutaba la tarea. Sería una
experiencia bastante frustrante. Afortunadamente, el sistema operativo de tu
computadora puede, y de hecho lo hace, interrumpir la exportación de forma
invisible con la suficiente frecuencia como para permitirte realizar otras tareas
simultáneamente.

Ahora, supongamos que estás descargando un video compartido por otra


persona, lo cual también puede tardar un poco, pero no consume tanto tiempo de
CPU. En este caso, la CPU tiene que esperar a que lleguen los datos de la red. Si
bien puedes empezar a leer los datos una vez que llegan, podría tardar un tiempo
en visualizarse por completo. Incluso una vez que todos los datos estén
presentes, si el video es bastante grande, podría tardar al menos uno o dos
segundos en cargarse. Puede que no parezca mucho, pero es mucho tiempo para
un procesador moderno, que puede realizar miles de millones de operaciones por
segundo. De nuevo, el sistema operativo interrumpirá el programa de forma
invisible para permitir que la CPU realice otras tareas mientras espera a que
finalice la llamada de red.

La exportación de video es un ejemplo de una operación dependiente de la


CPU o del cómputo . Está limitada por la velocidad potencial de procesamiento de
datos de la computadora dentro de la CPU o GPU, y la proporción de esa
Rust
jueves, 22 de mayo de 2025 : Página 478 de 719

velocidad que puede dedicar a la operación. La descarga de video es un ejemplo


de una operación dependiente de E/S , ya que está limitada por la velocidad
de entrada y salida de la computadora ; su velocidad solo puede ser la que
permita enviar los datos a través de la red.

En ambos ejemplos, las interrupciones invisibles del sistema operativo


proporcionan una forma de concurrencia. Sin embargo, dicha concurrencia solo
ocurre a nivel de todo el programa: el sistema operativo interrumpe un programa
para que otros puedan realizar su trabajo. En muchos casos, dado que
comprendemos nuestros programas a un nivel mucho más granular que el
sistema operativo, podemos detectar oportunidades de concurrencia que este no
puede detectar.

Por ejemplo, si desarrollamos una herramienta para gestionar la descarga de


archivos, deberíamos poder escribir nuestro programa de forma que al iniciar una
descarga no se bloquee la interfaz de usuario, y los usuarios puedan iniciar varias
descargas simultáneamente. Sin embargo, muchas API de sistemas operativos
para interactuar con la red bloquean el progreso del programa hasta que los
datos que procesa estén completamente listos.

Nota: Si lo piensa bien, así es como funcionan la mayoría de las llamadas a funciones. Sin embargo, el
término "bloqueo" suele reservarse para llamadas a funciones que interactúan con archivos, la red u
otros recursos del ordenador, ya que en esos casos un programa individual se beneficiaría de una
operación no bloqueante.

Podríamos evitar bloquear nuestro hilo principal creando un hilo dedicado a


descargar cada archivo. Sin embargo, la sobrecarga de esos hilos eventualmente
se convertiría en un problema. Sería preferible que la llamada no se bloqueara
desde el principio. También sería mejor si pudiéramos escribir con el mismo estilo
directo que usamos en el código de bloqueo, similar a esto:

let data = fetch_data_from(url).await;


println!("{data}");

Eso es exactamente lo que nos ofrece la abstracción async (abreviatura


de asynchronous ) de Rust . En este capítulo, aprenderás todo sobre async
mientras abordamos los siguientes temas:
Rust
jueves, 22 de mayo de 2025 : Página 479 de 719

 Cómo usar la sintaxis asyncde Rustawait


 Cómo utilizar el modelo asíncrono para resolver algunos de los mismos
desafíos que vimos en el Capítulo 16
 Cómo el multithreading y el async proporcionan soluciones
complementarias, que puedes combinar en muchos casos

Sin embargo, antes de ver cómo funciona async en la práctica, debemos hacer un
pequeño desvío para analizar las diferencias entre paralelismo y concurrencia.

Paralelismo y concurrencia

Hasta ahora, hemos tratado el paralelismo y la concurrencia como prácticamente


intercambiables. Ahora necesitamos distinguirlos con mayor precisión, ya que las
diferencias se harán evidentes a medida que empecemos a trabajar.

Considere las diferentes maneras en que un equipo podría dividir el trabajo en un


proyecto de software. Podría asignar varias tareas a un solo miembro, asignar una
tarea a cada miembro o usar una combinación de ambos enfoques.

Cuando una persona trabaja en varias tareas antes de completar ninguna, se


habla de concurrencia . Quizás tengas dos proyectos diferentes en tu ordenador,
y cuando te aburras o te quedes atascado en uno, cambies al otro. Eres una sola
persona, así que no puedes avanzar en ambas tareas a la vez, pero sí puedes
realizar varias tareas a la vez, avanzando en una a la vez alternando entre ellas
(ver Figura 17-1).

Figura 17-1: Un flujo de trabajo simultáneo, alternando entre la tarea A y la tarea B

Cuando el equipo divide un grupo de tareas, cada miembro se encarga de una y


trabaja en ella individualmente, se habla de paralelismo . Cada persona del
equipo puede avanzar al mismo tiempo (véase la Figura 17-2).

Figura 17-2: Un flujo de trabajo paralelo, donde el trabajo se realiza en la Tarea A y la Tarea B de
forma independiente

En ambos flujos de trabajo, es posible que deba coordinar diferentes tareas.


Quizás pensaba que la tarea asignada a una persona era totalmente
independiente del trabajo de los demás, pero en realidad requiere que otra
persona del equipo la complete primero. Parte del trabajo podría realizarse en
Rust
jueves, 22 de mayo de 2025 : Página 480 de 719

paralelo, pero parte sería serial : solo podría realizarse en serie, una tarea tras
otra, como se muestra en la Figura 17-3.

Figura 17-3: Un flujo de trabajo parcialmente paralelo, donde el trabajo se realiza en la Tarea A y
la Tarea B de forma independiente hasta que la Tarea A3 se bloquea en los resultados de la Tarea B3.

De igual manera, podrías darte cuenta de que una de tus tareas depende de otra.
Ahora tu trabajo simultáneo también se ha vuelto serial.

El paralelismo y la concurrencia también pueden intersecarse. Si descubres que


un compañero está bloqueado hasta que termines una de tus tareas,
probablemente concentrarás todos tus esfuerzos en esa tarea para
"desbloquearlo". Tú y tu compañero ya no podrán trabajar en paralelo, ni tampoco
podrán trabajar simultáneamente en sus propias tareas.

La misma dinámica básica se aplica al software y al hardware. En una máquina


con un solo núcleo de CPU, esta solo puede realizar una operación a la vez, pero
puede trabajar simultáneamente. Mediante herramientas como subprocesos,
procesos y asíncronos, la computadora puede pausar una actividad y cambiar a
otras antes de volver a la primera. En una máquina con varios núcleos de CPU,
también puede trabajar en paralelo. Un núcleo puede realizar una tarea mientras
otro realiza una completamente independiente, y ambas operaciones ocurren
simultáneamente.

Al trabajar con async en Rust, siempre trabajamos con concurrencia.


Dependiendo del hardware, el sistema operativo y el entorno de ejecución
asíncrono que usemos (más información sobre entornos de ejecución asíncronos
en breve), dicha concurrencia también puede usar paralelismo.

Ahora, profundicemos en cómo funciona realmente la programación asincrónica


en Rust.

Futuros y la sintaxis asíncrona


Los elementos clave de la programación asincrónica en Rust son los
futurosasync y las palabras clave de Rust await.

Un futuro es un valor que puede no estar listo ahora, pero que lo estará en algún
momento. (Este mismo concepto aparece en muchos lenguajes, a veces con otros
Rust
jueves, 22 de mayo de 2025 : Página 481 de 719

nombres como tarea o promesa ). Rust proporciona un Futuretrait como


componente básico para que se puedan implementar diferentes operaciones
asíncronas con diferentes estructuras de datos, pero con una interfaz común. En
Rust, los futuros son tipos que implementan el Futuretrait. Cada futuro contiene su
propia información sobre el progreso realizado y el significado de "listo".

Puedes aplicar la asyncpalabra clave a bloques y funciones para especificar que se


pueden interrumpir y reanudar. Dentro de un bloque o función asíncrona, puedes
usar la awaitpalabra clave para esperar un futuro (es decir, esperar a que esté
listo). Cualquier punto en el que esperes un futuro dentro de un bloque o función
asíncrona es un punto potencial para que ese bloque o función asíncrona se
pause y se reanude. El proceso de comprobar con un futuro si su valor ya está
disponible se denomina sondeo .

Otros lenguajes, como C# y JavaScript, también usan las palabras clave


" asyncy await " para la programación asíncrona. Si estás familiarizado con estos
lenguajes, notarás diferencias significativas en el funcionamiento de Rust,
incluyendo su manejo de la sintaxis. Y con razón, como veremos.

Al escribir en Rust asíncrono, usamos las asyncpalabras awaitclave ```` casi


siempre. Rust las compila en código equivalente usando el Futuretrait, de forma
similar a como compila forbucles en código equivalente usando el Iteratortrait. Sin
embargo, dado que Rust proporciona el Futuretrait, también puedes
implementarlo para tus propios tipos de datos cuando lo necesites. Muchas de las
funciones que veremos a lo largo de este capítulo devuelven tipos con sus propias
implementaciones de ``` Future. Volveremos a la definición del trait al final del
capítulo y profundizaremos en su funcionamiento, pero estos detalles son
suficientes para continuar.

Todo esto puede parecer un poco abstracto, así que escribamos nuestro primer
programa asíncrono: un pequeño web scraper. Pasaremos dos URL desde la línea
de comandos, las recuperaremos simultáneamente y devolveremos el resultado
de la que termine primero. Este ejemplo tendrá bastante sintaxis nueva, pero no
se preocupen: les explicaremos todo lo necesario sobre la marcha.

Nuestro primer programa asincrónico


Para centrar este capítulo en el aprendizaje de código asíncrono en lugar de en la
integración de diferentes partes del ecosistema, hemos creado el trplcrate
Rust
jueves, 22 de mayo de 2025 : Página 482 de 719

( trplabreviatura de "El lenguaje de programación Rust"). Este crate reexporta


todos los tipos, rasgos y funciones que necesitarás, principalmente de
los crates futuresy . El crate es el espacio oficial para la experimentación de código
asíncrono en Rust, y de hecho es donde se diseñó originalmente el rasgo. Tokio es
el entorno de ejecución asíncrono más utilizado en Rust actualmente,
especialmente para aplicaciones web. Existen otros entornos de ejecución
excelentes que podrían ser más adecuados para tus propósitos. Usamos el crate
en segundo plano porque está bien probado y es ampliamente
utilizado.tokiofuturesFuturetokiotrpl

En algunos casos, trpltambién renombra o encapsula las API originales para que te
centres en los detalles relevantes de este capítulo. Si quieres entender qué hace
el crate, te recomendamos consultar su código fuente . Podrás ver de qué crate
proviene cada reexportación, y hemos incluido comentarios detallados que
explican su función.

Cree un nuevo proyecto binario llamado hello-asyncy agregue el trplpaquete como


dependencia:

$ cargo new hello-async


$ cd hello-async
$ cargo add trpl

Ahora podemos usar las distintas piezas proporcionadas por trplpara escribir
nuestro primer programa asíncrono. Construiremos una pequeña herramienta de
línea de comandos que recupera dos páginas web, extrae el <title>elemento de
cada una e imprime el título de la página que complete primero el proceso.

Definición de la función page_title

Comencemos escribiendo una función que tome la URL de una página como
parámetro, le haga una solicitud y devuelva el texto del elemento de título (ver
Listado 17-1).

Nombre de archivo: src/main.rs


use trpl::Html;

async fn page_title(url: &str) -> Option<String> {


let response = trpl::get(url).await;
let response_text = response.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title_element| title_element.inner_html())
Rust
jueves, 22 de mayo de 2025 : Página 483 de 719
}
Listado 17-1: Definición de una función asíncrona para obtener el elemento de título de una página
HTML

Primero, definimos una función llamada page_titley la marcamos con


la async palabra clave. Luego, usamos la trpl::getfunción para obtener la URL que
se nos pase y añadimos la awaitpalabra clave para esperar la respuesta. Para
obtener el texto de la respuesta, llamamos a su textmétodo y, una vez más, la
esperamos con la await palabra clave. Ambos pasos son asíncronos. Para
la getfunción, debemos esperar a que el servidor envíe la primera parte de su
respuesta, que incluirá encabezados HTTP, cookies, etc., y puede entregarse por
separado del cuerpo de la respuesta. Especialmente si el cuerpo es muy extenso,
puede tardar un tiempo en llegar. Dado que debemos esperar a que llegue la
respuesta completatext , el método también es asíncrono.

Debemos esperar explícitamente ambos futuros, ya que los futuros en Rust


son perezosos : no hacen nada hasta que se les pida con la awaitpalabra clave.
(De hecho, Rust mostrará una advertencia del compilador si no se usa un futuro).
Esto podría recordarle la discusión sobre iteradores del Capítulo 13 en la
sección Procesamiento de una serie de elementos con iteradores . Los iteradores
no hacen nada a menos que llame a su nextmétodo, ya sea directamente o
mediante forbucles o métodos similares a maplos que se usan nextinternamente.
Del mismo modo, los futuros no hacen nada a menos que se les pida
explícitamente. Esta pereza permite a Rust evitar ejecutar código asíncrono hasta
que realmente se necesita.

Nota: Esto difiere del comportamiento que vimos en el capítulo anterior al usar thread::spawn" Crear
un nuevo hilo" con "spawn" , donde el cierre que pasamos a otro hilo empezó a ejecutarse
inmediatamente. También difiere de cómo muchos otros lenguajes abordan el método "async". Sin
embargo, es importante para Rust, y veremos por qué más adelante.

Una vez que tenemos response_text, podemos analizarlo en una instancia


del Html tipo usando Html::parse. En lugar de una cadena sin formato, ahora
tenemos un tipo de dato que podemos usar para trabajar con el HTML como una
estructura de datos más rica. En particular, podemos usar el select_firstmétodo
para encontrar la primera instancia de un selector CSS dado. Al pasar la
cadena "title", obtendremos el primer <title>elemento en el documento, si lo hay.
Dado que puede que no haya ningún elemento coincidente, select_firstdevuelve
un Option<ElementRef>. Finalmente, usamos el Option::mapmétodo , que nos
Rust
jueves, 22 de mayo de 2025 : Página 484 de 719

permite trabajar con el elemento en el Optionsi está presente y no hacer nada si


no lo está. (También podríamos usar una matchexpresión aquí, pero mapes más
idiomático). En el cuerpo de la función que proporcionamos a map,
llamamos inner_htmla title_elementpara obtener su contenido, que es un String. En
definitiva, tenemos un Option<String>.

Ten en cuenta que la palabra clave de Rust awaitva después de la expresión que
esperas, no antes. Es decir, es una palabra clave postfija . Esto puede ser
diferente a lo que estás acostumbrado si lo has usado asyncen otros lenguajes,
pero en Rust facilita mucho el trabajo con cadenas de métodos. Como resultado,
podemos cambiar el cuerpo de page_url_forpara encadenar las
llamadas trpl::geta textfunciones y entre awaitellas, como se muestra en el Listado
17-2.

Nombre de archivo: src/main.rs


let response_text = trpl::get(url).await.text().await;
Listado 17-2: Encadenamiento con la awaitpalabra clave

Con esto, ¡hemos escrito con éxito nuestra primera función asíncrona! Antes de
añadir el código mainpara llamarla, hablemos un poco más sobre lo que hemos
escrito y su significado.

Cuando Rust detecta un bloque marcado con la asyncpalabra clave, lo compila en


un tipo de dato único y anónimo que implementa la Futurecaracterística. Cuando
Rust detecta una función marcada con async, la compila en una función no
asíncrona cuyo cuerpo es un bloque asíncrono. El tipo de retorno de una función
asíncrona es el tipo de dato anónimo que el compilador crea para ese bloque
asíncrono.

Por lo tanto, escribir async fnequivale a escribir una función que devuelve
un futuro del tipo de retorno. Para el compilador, una definición de función como
la async fn page_titledel Listado 17-1 equivale a una función no asíncrona definida
así:

use std::future::Future;
use trpl::Html;

fn page_title(url: &str) -> impl Future<Output = Option<String>> + '_ {


async move {
let text = trpl::get(url).await.text().await;
Html::parse(&text)
.select_first("title")
Rust
jueves, 22 de mayo de 2025 : Página 485 de 719
.map(|title| title.inner_html())
}
}

Repasemos cada parte de la versión transformada:

 Utiliza la impl Traitsintaxis que discutimos en el Capítulo 10 en la


sección “Rasgos como parámetros” .
 El atributo devuelto es un Futurecon un tipo asociado de Output. Observe que
el Outputtipo es Option<String>, que es el mismo que el tipo de retorno
original de la async fnversión de page_title.
 Todo el código llamado en el cuerpo de la función original está encapsulado
en un async movebloque. Recuerda que los bloques son expresiones. Este
bloque completo es la expresión devuelta por la función.
 Este bloque asíncrono genera un valor de tipo Option<String>, como se
acaba de describir. Ese valor coincide con el Outputtipo del tipo de retorno.
Es similar a otros bloques que has visto.
 El nuevo cuerpo de la función es un async movebloque debido a cómo usa
el urlparámetro. (Hablaremos mucho más sobre " asyncversus" async
movemás adelante en este capítulo).
 La nueva versión de la función tiene un tipo de duración que no habíamos
visto antes en el tipo de salida: '_. Dado que la función devuelve un futuro
que hace referencia a una referencia (en este caso, la referencia
del urlparámetro), debemos indicar a Rust que queremos que se incluya
dicha referencia. No es necesario especificar la duración aquí, ya que Rust
sabe que solo puede haber una referencia involucrada, pero sí debemos
especificar que el futuro resultante está limitado por esa duración.

Ahora podemos llamar page_titleen main.

Cómo determinar el título de una sola página


Para empezar, obtendremos el título de una sola página. En el Listado 17-3,
seguimos el mismo patrón que usamos en el Capítulo 12 para obtener
argumentos de la línea de comandos en la sección "Aceptar argumentos de la
línea de comandos" . Luego, pasamos la primera URL page_titley esperamos el
resultado. Dado que el valor generado por el futuro es un Option<String>, usamos
una matchexpresión para imprimir diferentes mensajes que indiquen si la página
tenía un <title>.
Rust
jueves, 22 de mayo de 2025 : Página 486 de 719
Nombre de archivo: src/main.rs

async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
}
Listado 17-3: Llamada a la page_titlefunción desde maincon un argumento proporcionado por el usuario

Lamentablemente, este código no compila. Solo podemos usar la await palabra


clave en funciones o bloques asíncronos, y Rust no nos permite marcar
la mainfunción especial como async.

error[E0752]: `main` function is not allowed to be `async`


--> src/main.rs:6:1
|
6 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`

La razón por la mainque no se puede marcar asynces que el código asíncrono


necesita un entorno de ejecución : una crate de Rust que gestiona los detalles de
la ejecución de código asíncrono. La mainfunción de un programa
puede inicializar un entorno de ejecución, pero no es un entorno de ejecución en
sí mismo . (Veremos más sobre esto más adelante). Todo programa de Rust que
ejecuta código asíncrono tiene al menos un lugar donde configura un entorno de
ejecución y ejecuta los futuros.

La mayoría de los lenguajes que admiten async incluyen un entorno de ejecución,


pero Rust no. En su lugar, existen numerosos entornos de ejecución asíncronos,
cada uno con diferentes ventajas y desventajas, adecuados para el caso de uso al
que se dirige. Por ejemplo, un servidor web de alto rendimiento con muchos
núcleos de CPU y una gran cantidad de RAM tiene necesidades muy diferentes a
las de un microcontrolador con un solo núcleo, poca RAM y sin capacidad de
asignación de memoria dinámica. Los paquetes que proporcionan estos entornos
de ejecución también suelen ofrecer versiones asíncronas de funciones comunes,
como la E/S de archivos o de red.

Aquí, y a lo largo del resto de este capítulo, usaremos la runfunción del trplcrate,
que toma un futuro como argumento y lo ejecuta hasta su finalización. Tras
bambalinas, la llamada runconfigura un entorno de ejecución que se utiliza para
Rust
jueves, 22 de mayo de 2025 : Página 487 de 719

ejecutar el futuro introducido. Una vez completado el futuro, rundevuelve el valor


que generó.

Podríamos pasar el futuro devuelto por page_titledirectamente a run, y una vez


completado, podríamos hacer coincidir con el Option<String>, como intentamos
hacer en el Listado 17-3. Sin embargo, en la mayoría de los ejemplos del capítulo
(y en la mayor parte del código asíncrono en el mundo real), realizaremos más de
una llamada a una función asíncrona, por lo que, en su lugar, pasaremos
un asyncbloque y esperaremos explícitamente el resultado de la page_titlellamada,
como en el Listado 17-4.

Nombre de archivo: src/main.rs


fn main() {
let args: Vec<String> = std::env::args().collect();

trpl::run(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
})
}
Listado 17-4: Esperando un bloque asíncrono contrpl::run

Cuando ejecutamos este código, obtenemos el comportamiento que esperábamos


inicialmente:

$ cargo run -- https://www.rust-lang.org


Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
Rust Programming Language

¡Uf! ¡Por fin tenemos código asíncrono funcional! Pero antes de añadir el código
para que los dos sitios compitan entre sí, volvamos a analizar brevemente cómo
funcionan los futuros.

Cada punto de espera (es decir, cada lugar donde el código usa la await palabra
clave) representa un punto donde se devuelve el control al entorno de ejecución.
Para que esto funcione, Rust necesita registrar el estado del bloque asíncrono
para que el entorno de ejecución pueda iniciar otro trabajo y regresar cuando esté
listo para intentar avanzar el primero. Esta es una máquina de estados invisible,
Rust
jueves, 22 de mayo de 2025 : Página 488 de 719

como si se hubiera escrito una enumeración como esta para guardar el estado
actual en cada punto de espera:

enum PageTitleFuture<'a> {
Initial { url: &'a str },
GetAwaitPoint { url: &'a str },
TextAwaitPoint { response: trpl::Response },
}

Sin embargo, escribir el código para la transición entre cada estado manualmente
sería tedioso y propenso a errores, especialmente cuando se necesita añadir más
funcionalidad y estados al código posteriormente. Afortunadamente, el
compilador de Rust crea y gestiona automáticamente las estructuras de datos de
la máquina de estados para código asíncrono. Las reglas habituales de préstamo
y propiedad de las estructuras de datos siguen vigentes y, afortunadamente, el
compilador también se encarga de comprobarlas y proporciona mensajes de error
útiles. Analizaremos algunas de ellas más adelante en este capítulo.

En última instancia, algo tiene que ejecutar esta máquina de estados, y ese algo
es un entorno de ejecución. (Por eso, al consultar entornos de ejecución, puede
encontrar referencias a ejecutores : un ejecutor es la parte del entorno de
ejecución responsable de ejecutar el código asíncrono).

Ahora puedes ver por qué el compilador nos impidió convertirnos en mainuna
función asíncrona en el Listado 17-3. Si mainfuera una función asíncrona, otra
función tendría que gestionar la máquina de estados para cualquier
futuro maindevuelto, pero maines el punto de partida del programa. En su lugar,
llamamos a la trpl::run función mainpara configurar un entorno de ejecución y
ejecutar el futuro devuelto por el asyncbloque hasta que devuelva Ready.

Nota: Algunos entornos de ejecución proporcionan macros para escribir una función asíncrona main .
Estas macros se reescriben async fn main() { ... }como una función normal fn main, lo que hace lo
mismo que hicimos manualmente en el Listado 17-5: llamar a una función que ejecuta un futuro hasta
su finalización trpl::run.

Ahora juntemos estas piezas y veamos cómo podemos escribir código


concurrente.

Compitiendo nuestras dos URL entre sí


Rust
jueves, 22 de mayo de 2025 : Página 489 de 719

En el Listado 17-5, llamamos page_titlecon dos URL diferentes pasadas desde la


línea de comando y las ejecutamos.

Nombre de archivo: src/main.rs


use trpl::{Either, Html};

fn main() {
let args: Vec<String> = std::env::args().collect();

trpl::run(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);

let (url, maybe_title) =


match trpl::race(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};

println!("{url} returned first");


match maybe_title {
Some(title) => println!("Its page title is: '{title}'"),
None => println!("Its title could not be parsed."),
}
})
}

async fn page_title(url: &str) -> (&str, Option<String>) {


let text = trpl::get(url).await.text().await;
let title = Html::parse(&text)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
Listado 17-5:

Comenzamos llamando page_titlea cada una de las URL proporcionadas por el


usuario. Guardamos los futuros resultantes como title_fut_1y title_fut_2. Recuerde
que estos no hacen nada todavía, ya que los futuros son perezosos y aún no los
hemos esperado. Luego, pasamos los futuros a trpl::race, que devuelve un valor
que indica cuál de los futuros que se le pasan finaliza primero.

Nota: En esencia, racese basa en una función más general, selectque encontrarás con más frecuencia
en el código de Rust. Una select función puede hacer muchas cosas que la trpl::racefunción no puede,
pero también presenta cierta complejidad adicional que podemos obviar por ahora.

Cualquiera de los futuros puede "ganar" legítimamente, por lo que no tiene


sentido devolver a Result. En su lugar, racedevuelve un tipo que no hemos visto
Rust
jueves, 22 de mayo de 2025 : Página 490 de 719

antes, trpl::Either. El Eithertipo es similar a a Resulten que tiene dos casos. ResultSin
embargo, a diferencia de , no hay una noción de éxito o fracaso integrada
en Either. En su lugar, usa Lefty Rightpara indicar "uno u otro":

enum Either<A, B> {


Left(A),
Right(B),
}

La racefunción retorna Leftcon la salida de ese futuro si el primer argumento es


válido, y Rightcon la salida del segundo argumento si este último es válido. Esto
coincide con el orden en que aparecen los argumentos al llamar a la función: el
primer argumento está a la izquierda del segundo.

También actualizamos page_titlepara devolver la misma URL introducida. De esta


forma, si la página que regresa primero no tiene una URL que <title>podamos
resolver, podemos imprimir un mensaje significativo. Con esta información
disponible, finalizamos actualizando nuestra println!salida para indicar qué URL
terminó primero y cuál es la URL, si la hay, <title>para la página web en esa URL.

¡Ya has creado un pequeño raspador web funcional! Selecciona un par de URL y
ejecuta la herramienta de línea de comandos. Descubrirás que algunos sitios son
consistentemente más rápidos que otros, mientras que en otros casos la
velocidad varía según la ejecución. Y lo que es más importante, has aprendido los
fundamentos del trabajo con futuros, así que ahora podemos profundizar en lo
que podemos hacer con async.

Aplicación de concurrencia con Async


En esta sección, aplicaremos async a algunos de los mismos desafíos de
concurrencia que abordamos con los subprocesos en el capítulo 16. Debido a que
ya hablamos sobre muchas de las ideas clave allí, en esta sección nos
centraremos en las diferencias entre los subprocesos y los futuros.

En muchos casos, las API para trabajar con concurrencia mediante async son muy
similares a las de los subprocesos. En otros casos, resultan ser bastante
diferentes. Incluso cuando las API parecen similares entre subprocesos y async,
suelen tener un comportamiento distinto y, casi siempre, características de
rendimiento distintas.

Creando una nueva tarea conspawn_task


Rust
jueves, 22 de mayo de 2025 : Página 491 de 719

La primera operación que abordamos en "Crear un nuevo hilo con Spawn" fue el
conteo progresivo en dos hilos separados. Hagamos lo mismo usando async.
El trplcrate proporciona una spawn_taskfunción muy similar a la thread::spawnAPI y
una sleepfunción que es una versión asíncrona de la thread::sleepAPI. Podemos
usarlas juntas para implementar el ejemplo de conteo, como se muestra en el
Listado 17-6.

Nombre de archivo: src/main.rs


use std::time::Duration;

fn main() {
trpl::run(async {
trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});

for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
}
Listado 17-6: Creación de una nueva tarea para imprimir una cosa mientras la tarea principal imprime
otra cosa

Como punto de partida, configuramos nuestra mainfunción trpl::runpara que


nuestra función de nivel superior pueda ser asincrónica.

Nota: A partir de este punto del capítulo, todos los ejemplos incluirán este mismo código de ajuste
con trpl::runin main, por lo que a menudo lo omitiremos, al igual que con main. ¡No olvides incluirlo
en tu código!

Luego, escribimos dos bucles dentro de ese bloque, cada uno con
una trpl::sleepllamada que espera medio segundo (500 milisegundos) antes de
enviar el siguiente mensaje. Colocamos un bucle en el cuerpo de
"a" trpl::spawn_tasky el otro en un bucle de nivel superior for. También añadimos
"an" awaitdespués de las sleepllamadas.

Este código se comporta de manera similar a la implementación basada en


subprocesos, incluido el hecho de que es posible que veas los mensajes aparecer
en un orden diferente en tu propia terminal cuando lo ejecutas:
Rust
jueves, 22 de mayo de 2025 : Página 492 de 719
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!

Esta versión se detiene en cuanto forfinaliza el bucle en el cuerpo del bloque


asíncrono principal, ya que la tarea generada por spawn_taskse cierra
al main finalizar la función. Si desea que se ejecute completamente hasta
completar la tarea, deberá usar un identificador de unión para esperar a que se
complete la primera tarea. Con los hilos, usamos el joinmétodo para "bloquear"
hasta que el hilo terminara de ejecutarse. En el Listado 17-7, podemos
usar awaitpara hacer lo mismo, ya que el identificador de la tarea es un futuro.
Su Outputtipo es a Result, por lo que también lo desenvolvemos después de
esperarlo.

Nombre de archivo: src/main.rs


let handle = trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});

for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}

handle.await.unwrap();
Listado 17-7: Uso awaitde un controlador de unión para ejecutar una tarea hasta su finalización

Esta versión actualizada se ejecuta hasta que finalicen ambos bucles.

hi number 1 from the second task!


hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
Rust
jueves, 22 de mayo de 2025 : Página 493 de 719
hi number 8 from the first task!
hi number 9 from the first task!

Hasta ahora, parece que async y los subprocesos nos dan los mismos resultados
básicos, solo que con una sintaxis diferente: usar awaiten lugar de llamar joinal
controlador de unión y esperar las sleepllamadas.

La mayor diferencia radica en que no necesitamos generar otro hilo del sistema
operativo para esto. De hecho, ni siquiera necesitamos generar una tarea. Dado
que los bloques asíncronos se compilan en futuros anónimos, podemos colocar
cada bucle en un bloque asíncrono y hacer que el entorno de ejecución los
ejecute completamente usando la trpl::join función.

En la sección "Esperando a que todos los hilos finalicen


usando join manejadores" , mostramos cómo usar el joinmétodo en
el JoinHandletipo devuelto al llamar a std::thread::spawn. La trpl::joinfunción es
similar, pero para futuros. Al asignarle dos futuros, produce un único futuro nuevo
cuya salida es una tupla que contiene la salida de cada futuro pasado una vez
que ambos se completan. Por lo tanto, en el Listado 17-8, usamos trpl::joinesperar
a que ambos fut1y fut2finalicen. No usamos await fut1y, fut2sino el nuevo futuro
producido por trpl::join. Ignoramos la salida, ya que es solo una tupla que contiene
dos valores unitarios.

Nombre de archivo: src/main.rs


let fut1 = async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};

let fut2 = async {


for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};

trpl::join(fut1, fut2).await;
Listado 17-8: Uso trpl::joinpara esperar dos futuros anónimos

Cuando ejecutamos esto, vemos que ambos futuros se ejecutan hasta su


finalización:

hi number 1 from the first task!


Rust
jueves, 22 de mayo de 2025 : Página 494 de 719
hi number 1 from the second task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!

Ahora, verás exactamente el mismo orden cada vez, lo cual es muy diferente de
lo que vimos con los hilos. Esto se debe a que la trpl::joinfunción es justa , lo que
significa que comprueba cada futuro con la misma frecuencia, alternando entre
ellos, y nunca deja que uno avance si el otro está listo. Con los hilos, el sistema
operativo decide qué hilo comprobar y durante cuánto tiempo dejarlo correr. Con
Rust asíncrono, el entorno de ejecución decide qué tarea comprobar. (En la
práctica, los detalles se complican porque un entorno de ejecución asíncrono
puede usar hilos del sistema operativo bajo el capó como parte de cómo gestiona
la concurrencia, por lo que garantizar la imparcialidad puede ser más trabajo para
un entorno de ejecución, ¡pero sigue siendo posible!). Los entornos de ejecución
no tienen que garantizar la imparcialidad para ninguna operación dada, y a
menudo ofrecen diferentes API para permitirte elegir si quieres o no
imparcialidad.

Pruebe algunas de estas variaciones de esperar el futuro y vea qué hacen:

 Elimina el bloque asíncrono de uno o ambos bucles.


 Espere cada bloque asíncrono inmediatamente después de definirlo.
 Envuelva solo el primer bucle en un bloque asíncrono y espere el futuro
resultante después del cuerpo del segundo bucle.

¡Para un desafío adicional, intenta averiguar cuál será el resultado en cada


caso antes de ejecutar el código!

Conteo progresivo de dos tareas mediante el paso de mensajes

Compartir datos entre futuros también resultará familiar: volveremos a usar el


paso de mensajes, pero esta vez con versiones asíncronas de los tipos y
funciones. Tomaremos un camino ligeramente diferente al que tomamos en Uso
del paso de mensajes para transferir datos entre subprocesos" para ilustrar
Rust
jueves, 22 de mayo de 2025 : Página 495 de 719

algunas de las diferencias clave entre la concurrencia basada en subprocesos y la


basada en futuros. En el Listado 17-9, comenzaremos con un solo bloque
asíncrono, sin generar una tarea independiente, como sí lo hicimos con un
subproceso.

Nombre de archivo: src/main.rs


let (tx, mut rx) = trpl::channel();

let val = String::from("hi");


tx.send(val).unwrap();

let received = rx.recv().await.unwrap();


println!("Got: {received}");
Listado 17-9: Creación de un canal asincrónico y asignación de las dos mitades a txyrx

Aquí usamostrpl::channel , una versión asíncrona de la API de canal de múltiples


productores y un solo consumidor que usamos con hilos en el Capítulo 16. La
versión asíncrona de la API es solo ligeramente diferente de la versión basada en
hilos: usa un receptor mutable en lugar de uno inmutable rx, y su recvmétodo
produce un futuro que debemos esperar en lugar de producir el valor
directamente. Ahora podemos enviar mensajes del emisor al receptor. Observe
que no es necesario generar un hilo independiente ni una tarea; simplemente
necesitamos esperar la rx.recv llamada.

El Receiver::recvmétodo sincrónico enstd::mpsc::channel se bloquea hasta recibir un


mensaje. Este trpl::Receiver::recvmétodo no lo hace porque es asíncrono. En lugar
de bloquearse, devuelve el control al entorno de ejecución hasta que se recibe un
mensaje o se cierra el canal de envío. En cambio, no esperamos la sendllamada,
ya que no se bloquea. No es necesario, ya que el canal al que lo enviamos no
tiene límites.

Nota: Dado que todo este código asincrónico se ejecuta en un bloque asincrónico durante
una trpl::run llamada, todo lo que se encuentra dentro de él puede evitar el bloqueo. Sin embargo, el
código externo se bloqueará al runretornar la función. Ese es el propósito de la trpl::runfunción:
permite elegir. dónde bloquear un conjunto de código asincrónico y, por lo tanto, dónde realizar la
transición entre código sincrónico y asincrónico. En la mayoría de los entornos de ejecución
asincrónicos, runse llama así block_onprecisamente por esta razón.

Observe dos aspectos de este ejemplo. Primero, el mensaje llegará de inmediato.


Segundo, aunque aquí usamos un futuro, todavía no hay concurrencia. Todo en la
lista sucede en secuencia, tal como ocurriría si no hubiera futuros involucrados.
Rust
jueves, 22 de mayo de 2025 : Página 496 de 719

Abordemos la primera parte enviando una serie de mensajes y durmiendo entre


ellos, como se muestra en el Listado 17-10.

Nombre de archivo: src/main.rs


let (tx, mut rx) = trpl::channel();

let vals = vec![


String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];

for val in vals {


tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}

while let Some(value) = rx.recv().await {


println!("received '{value}'");
}
Listado 17-10: Envío y recepción de múltiples mensajes a través del canal asíncrono y suspensión con
unawait intervalo entre cada mensaje

Además de enviar los mensajes, necesitamos recibirlos. En este caso, como


sabemos cuántos mensajes llegan, podríamos hacerlo manualmente
llamando rx.recv().awaitcuatro veces. Sin embargo, en la vida real, generalmente
esperaremos a que lleguen algunos. número indeterminado de mensajes, por lo
que debemos seguir esperando hasta determinar que no hay más.

En el Listado 16-10, usamos un forbucle para procesar todos los elementos


recibidos de un canal síncrono. Sin embargo, Rust aún no permite escribir
un forbucle sobre una serie de elementos asíncronos , por lo que necesitamos
usar un bucle desconocido: el while letbucle condicional. Esta es la versión en
bucle de la if letconstrucción que vimos en la sección " Flujo de control conciso
con if lety".let else . El bucle continuará ejecutándose mientras el patrón que
especifica siga coincidiendo con el valor.

La rx.recvllamada produce un futuro, que esperamos. El tiempo de ejecución


pausará el futuro hasta que esté listo. Una vez que llega un mensaje, el futuro se
resolverá Some(message)tantas veces como llegue un mensaje. Cuando el canal se
cierra, independientemente de si alguno ha llegadoNone mensaje, el futuro se
resolverá a para indicar que no hay más valores y, por lo tanto, debemos detener
el sondeo; es decir, dejar de esperar.
Rust
jueves, 22 de mayo de 2025 : Página 497 de 719

El while letbucle reúne todo esto. Si el resultado de la


llamada rx.recv().awaites Some(message), obtenemos acceso al mensaje y podemos
usarlo en el cuerpo del bucle, tal como lo haríamos con if let . Si el resultado
es None, el bucle finaliza. Cada vez que el bucle se completa, vuelve a alcanzar el
punto de espera, por lo que el motor de ejecución lo pausa hasta que llega otro
mensaje.

El código ahora envía y recibe correctamente todos los mensajes.


Lamentablemente, aún existen un par de problemas. Por un lado, los mensajes no
llegan a intervalos de medio segundo, sino todos a la vez, 2 (2000 milisegundos)
después de iniciar el programa. Por otro lado, ¡el programa nunca termina! En
cambio, espera eternamente nuevos mensajes. Deberás cerrarlo usando Ctrl+C .

Comencemos examinando por qué los mensajes llegan todos a la vez después del
retraso completo, en lugar de llegar con retrasos entre cada uno. Dentro de un
bloque asíncrono dado, el orden en que... await aparecen las palabras clave en el
código también coincide con el orden en que se ejecutan al ejecutarse el
programa.

Solo hay un bloque asíncrono en el Listado 17-10, por lo que todo se ejecuta
linealmente. Sigue sin haber concurrencia. Todas las tx.sendllamadas se realizan,
intercaladas con todas las trpl::sleepllamadas y sus puntos de espera asociados.
Solo entonces el while letbucle puede pasar por alguno de los await puntos
delrecv llamadas.

Para obtener el comportamiento deseado, donde el retraso de suspensión ocurre


entre cada mensaje, necesitamos colocar las operaciones txy rxen sus propios
bloques asíncronos, como se muestra en el Listado 17-11. Entonces, el entorno de
ejecución puede ejecutar cada una de ellas por separado usando trpl::join, como
en el ejemplo de conteo. Nuevamente, esperamos el resultado de llamar
a trpl::join, no los futuros individuales. Si esperáramos los futuros individuales en
secuencia, simplemente terminaríamos de nuevo en un flujo secuencial, justo lo
que intentamos evitar .

Nombre de archivo: src/main.rs


let tx_fut = async {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
Rust
jueves, 22 de mayo de 2025 : Página 498 de 719
];

for val in vals {


tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};

let rx_fut = async {


while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};

trpl::join(tx_fut, rx_fut).await;
Listado 17-11: Separarse senden recvsus propios asyncbloques y esperar el futuro de esos bloques

Con el código actualizado en el Listado 17-11, los mensajes se imprimen a


intervalos de 500 milisegundos, en lugar de todos rápidamente después de 2
segundos.

Sin embargo, el programa nunca sale, debido a la forma en que while letel bucle
interactúa con trpl::join:

 El futuro retornado trpl::joinse completa solo una vez que ambos futuros que
se le pasaron se han completado.
 El txfuturo se completa una vez que termina de dormir después de enviar el
último mensaje en vals.
 El rxfuturo no estará completo hasta que el while letbucle termine.
 El while letbucle no finalizará hasta que awaiting rx.recvproduzca None.
 La espera rx.recvregresará Nonesólo una vez que el otro extremo del canal
esté cerrado.
 El canal se cerrará sólo si llamamos rx.closeo cuando el lado del
remitente, tx, se cae.
 No llamamos rx.closea ninguna parte y txno se descartará hasta
que trpl::runfinalice el bloque asíncrono más externo al que se pasa.
 El bloqueo no puede finalizar porque está bloqueado al trpl::joincompletarse,
lo que nos lleva de nuevo al principio de esta lista.

Podríamos cerrarlo manualmente rxllamando rx.closea algún lugar, pero eso no


tiene mucho sentido. Detenerse después de procesar una cantidad arbitraria de
mensajes haría que el programa se cerrara, pero podríamos perder mensajes.
Rust
jueves, 22 de mayo de 2025 : Página 499 de 719

Necesitamos otra forma de asegurarnos de que txse elimine antes. del final de la
función.

Actualmente, el bloque asíncrono donde enviamos los mensajes solo toma


prestados datos, txya que enviar un mensaje no requiere propiedad. Sin embargo,
si pudiéramos movernos txa ese bloque asíncrono, se descartaría al finalizar. En la
sección " Capturar referencias o mover la propiedad" del capítulo 13 , aprendiste
a usar la palabra clave con cierres y, como se explicó en la sección " Usar cierres
con subprocesos"move del capítulo 16 , a menudo necesitamos mover datos a
cierres al trabajar con subprocesos. La misma dinámica básica se aplica a los
bloques asíncronos, por lo que la palabra clave funciona con ellos igual que con
los cierres.movemove

En el Listado 17-12, cambiamos el bloque usado para enviar mensajes


desde asynca async move. Al ejecutar esta versión del código, se cierra
correctamente después de enviar y recibir el último mensaje.

Nombre de archivo: src/main.rs


let (tx, mut rx) = trpl::channel();

let tx_fut = async move {


let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];

for val in vals {


tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};

let rx_fut = async {


while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};

trpl::join(tx_fut, rx_fut).await;
Listado 17-12: Una revisión del código del Listado 17-11 que se apaga correctamente cuando se
completa
Rust
jueves, 22 de mayo de 2025 : Página 500 de 719

Este canal asincrónico también es un canal de múltiples productores, por lo que


podemos llamarlo clone si txqueremos enviar mensajes desde múltiples futuros,
como se muestra en el Listado 17-13.

Nombre de archivo: src/main.rs


let (tx, mut rx) = trpl::channel();

let tx1 = tx.clone();


let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];

for val in vals {


tx1.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};

let rx_fut = async {


while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};

let tx_fut = async move {


let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];

for val in vals {


tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(1500)).await;
}
};

trpl::join3(tx1_fut, tx_fut, rx_fut).await;


Listado 17-13: Uso de múltiples productores con bloques asíncronos

Primero, clonamos tx, creando tx1fuera del primer bloque asíncrono. Nos
movemos tx1a ese bloque como hicimos antes con tx. Después, movemos el
original txa un nuevo bloque asíncrono, donde enviamos más mensajes con un
retraso ligeramente menor. Este nuevo bloque asíncrono se coloca después del
Rust
jueves, 22 de mayo de 2025 : Página 501 de 719

bloque asíncrono para recibir mensajes, pero también podría ir antes. La clave
está en el orden en que se esperan los futuros, no en el que se crean.

Ambos bloques asíncronos para enviar mensajes deben ser async movebloques
para que tanto " txy " como tx1"se eliminen" al finalizar. De lo contrario,
volveremos al bucle infinito inicial. Finalmente, cambiamos de
" trpl::joina" trpl::join3para gestionar el futuro adicional.

Ahora vemos todos los mensajes de ambos futuros de envío y, debido a que los
futuros de envío utilizan retrasos ligeramente diferentes después del envío, los
mensajes también se reciben en esos intervalos diferentes.

received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'

Este es un buen comienzo, pero nos limita a unos pocos futuros: dos con joino tres
con join3. Veamos cómo podríamos trabajar con más futuros.

Trabajar con cualquier número de futuros


Cuando cambiamos de usar dos futuros a tres en la sección anterior, también
tuvimos que cambiar de "using" joina "using" join3. Sería molesto tener que llamar
a una función diferente cada vez que cambiáramos el número de futuros que
queríamos unir. Afortunadamente, tenemos una macro de joinla que podemos
pasar cualquier número de argumentos. Esta también gestiona la espera de los
futuros. Por lo tanto, podríamos reescribir el código del Listado 17-13 para usar
"use" join!en lugar de join3"using", como en el Listado 17-14.

Nombre de archivo: src/main.rs


trpl::join!(tx1_fut, tx_fut, rx_fut);
Listado 17-14: Uso join!para esperar futuros múltiples

Esto es definitivamente una mejora respecto a intercambiar


entre joiny join3y join4y, etc. Sin embargo, incluso esta forma de macro solo
funciona cuando conocemos el número de futuros de antemano. En Rust, sin
Rust
jueves, 22 de mayo de 2025 : Página 502 de 719

embargo, insertar futuros en una colección y luego esperar a que algunos o todos
se completen es un patrón común.

Para comprobar todos los futuros de una colección, necesitaremos iterarlos y


unirlos . La trpl::join_allfunción acepta cualquier tipo que implemente
el Iteratoratributo, que aprendiste en el capítulo 13 de "El atributo Iterador y
el nextmétodo" , así que parece ideal. Intentemos introducir nuestros futuros en
un vector y reemplazarlos join!con, join_allcomo se muestra en el listado 17-15.

let futures = vec![tx1_fut, rx_fut, tx_fut];

trpl::join_all(futures).await;
Listado 17-15: Almacenamiento de futuros anónimos en un vector y llamadajoin_all

Lamentablemente, este código no compila. En su lugar, aparece este error:

error[E0308]: mismatched types


--> src/main.rs:45:37
|
10 | let tx1_fut = async move {
| ---------- the expected `async` block
...
24 | let rx_fut = async {
| ----- the found `async` block
...
45 | let futures = vec![tx1_fut, rx_fut, tx_fut];
| ^^^^^^ expected `async` block, found a
different `async` block
|
= note: expected `async` block `{async block@src/main.rs:10:23: 10:33}`
found `async` block `{async block@src/main.rs:24:22: 24:27}`
= note: no two async blocks, even if identical, have the same type
= help: consider pinning your async block and casting it to a trait object

Esto podría resultar sorprendente. Al fin y al cabo, ninguno de los bloques


asíncronos devuelve nada, por lo que cada uno produce un Future<Output =
()>. FutureSin embargo, recuerda que se trata de una característica, y que el
compilador crea una enumeración única para cada bloque asíncrono. No se
pueden incluir dos estructuras diferentes escritas a mano en un Vec, y la misma
regla se aplica a las diferentes enumeraciones generadas por el compilador.

Para que esto funcione, necesitamos usar objetos de rasgo , tal como lo hicimos
en “Devolver errores de la función de ejecución” en el Capítulo 12. (Cubriremos
los objetos de rasgo en detalle en el Capítulo 18). El uso de objetos de rasgo nos
Rust
jueves, 22 de mayo de 2025 : Página 503 de 719

permite tratar cada uno de los futuros anónimos producidos por estos tipos como
el mismo tipo, porque todos implementan el Futurerasgo.

Nota: En la sección del Capítulo 8, " Uso de una enumeración para almacenar múltiples valores" ,
analizamos otra forma de incluir múltiples tipos en un vector Vec: usar una enumeración para
representar cada tipo que puede aparecer en el vector. Sin embargo, no podemos hacerlo aquí. Por un
lado, no tenemos forma de nombrar los diferentes tipos, ya que son anónimos. Por otro lado, la razón
por la que usamos un vector, join_allen primer lugar, fue para poder trabajar con una colección
dinámica de futuros donde solo nos importa que tengan el mismo tipo de salida.

Comenzamos envolviendo cada futuro en vec!un Box::new, como se muestra en el


Listado 17-16.

Nombre de archivo: src/main.rs

let futures =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];

trpl::join_all(futures).await;
Listado 17-16: Uso Box::newpara alinear los tipos de futuros en unVec

Lamentablemente, este código sigue sin compilar. De hecho, obtenemos el mismo


error básico que obtuvimos antes, tanto en la segunda como en la
tercera Box::newllamada, además de nuevos errores relacionados con
el Unpinatributo. Volveremos a los Unpinerrores en breve. Primero, corrijamos los
errores de tipo en las Box::newllamadas anotando explícitamente el tipo de
la futuresvariable (véase el Listado 17-17).

Nombre de archivo: src/main.rs

let futures: Vec<Box<dyn Future<Output = ()>>> =


vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
Listado 17-17: Corrección del resto de errores de desajuste de tipos mediante una declaración de tipo
explícita

Esta declaración de tipo es un poco compleja, así que veámosla:

1. El tipo más interno es el propio futuro. Indicamos explícitamente que la


salida del futuro es el tipo unidad ()al escribir Future<Output = ()>.
2. Luego anotamos el rasgo con dynpara marcarlo como dinámico.
3. La referencia completa del rasgo está envuelta en un Box.
Rust
jueves, 22 de mayo de 2025 : Página 504 de 719

4. Por último, declaramos explícitamente que futureses un Veccontiene estos


elementos.

Eso ya marcó una gran diferencia. Ahora, al ejecutar el compilador, solo


obtenemos los errores que mencionan Unpin. Aunque son tres, su contenido es
muy similar.

error[E0308]: mismatched types


--> src/main.rs:46:46
|
10 | let tx1_fut = async move {
| ---------- the expected `async` block
...
24 | let rx_fut = async {
| ----- the found `async` block
...
46 | vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
| -------- ^^^^^^ expected `async` block, found a different `async`
block
| |
| arguments to this function are incorrect
|
= note: expected `async` block `{async block@src/main.rs:10:23: 10:33}`
found `async` block `{async block@src/main.rs:24:22: 24:27}`
= note: no two async blocks, even if identical, have the same type
= help: consider pinning your async block and casting it to a trait object
note: associated function defined here
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/alloc/src/boxed.rs:255:12
|
255 | pub fn new(x: T) -> Self {
| ^^^

error[E0308]: mismatched types


--> src/main.rs:46:64
|
10 | let tx1_fut = async move {
| ---------- the expected `async` block
...
30 | let tx_fut = async move {
| ---------- the found `async` block
...
46 | vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
| -------- ^^^^^^ expected `async` block, found a
different `async` block
| |
| arguments to this function are incorrect
|
= note: expected `async` block `{async block@src/main.rs:10:23: 10:33}`
found `async` block `{async block@src/main.rs:30:22: 30:32}`
= note: no two async blocks, even if identical, have the same type
= help: consider pinning your async block and casting it to a trait object
note: associated function defined here
Rust
jueves, 22 de mayo de 2025 : Página 505 de 719
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/alloc/src/boxed.rs:255:12
|
255 | pub fn new(x: T) -> Self {
| ^^^

error[E0277]: `{async block@src/main.rs:10:23: 10:33}` cannot be unpinned


--> src/main.rs:48:24
|
48 | trpl::join_all(futures).await;
| -------------- ^^^^^^^ the trait `Unpin` is not implemented for `{async
block@src/main.rs:10:23: 10:33}`, which is required by `Box<{async
block@src/main.rs:10:23: 10:33}>: Future`
| |
| required by a bound introduced by this call
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current
scope
= note: required for `Box<{async block@src/main.rs:10:23: 10:33}>` to implement
`Future`
note: required by a bound in `join_all`
--> file:///home/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-util-0.3.30/
src/future/join_all.rs:105:14
|
102 | pub fn join_all<I>(iter: I) -> JoinAll<I::Item>
| -------- required by a bound in this function
...
105 | I::Item: Future,
| ^^^^^^ required by this bound in `join_all`

error[E0277]: `{async block@src/main.rs:10:23: 10:33}` cannot be unpinned


--> src/main.rs:48:9
|
48 | trpl::join_all(futures).await;
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Unpin` is not implemented for
`{async block@src/main.rs:10:23: 10:33}`, which is required by `Box<{async
block@src/main.rs:10:23: 10:33}>: Future`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current
scope
= note: required for `Box<{async block@src/main.rs:10:23: 10:33}>` to implement
`Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-util-0.3.30/src/
future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`

error[E0277]: `{async block@src/main.rs:10:23: 10:33}` cannot be unpinned


Rust
jueves, 22 de mayo de 2025 : Página 506 de 719
--> src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `{async
block@src/main.rs:10:23: 10:33}`, which is required by `Box<{async
block@src/main.rs:10:23: 10:33}>: Future`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current
scope
= note: required for `Box<{async block@src/main.rs:10:23: 10:33}>` to implement
`Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-util-0.3.30/src/
future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`

Hay mucho que digerir, así que analicémoslo. La primera parte del mensaje indica
que el primer bloque async ( src/main.rs:8:23: 20:10) no implementa el Unpinatributo
y sugiere usar pin!o Box::pinpara resolverlo. Más adelante en el capítulo,
profundizaremos en algunos detalles sobre Piny Unpin. Por el momento, podemos
seguir las recomendaciones del compilador para solucionar el problema. En los
Listados 17-18, comenzamos actualizando la anotación de tipo para futures, con
un Pinencapsulado para cada Box. En segundo lugar, usamos Box::pinpara fijar los
futuros.

Nombre de archivo: src/main.rs


let futures: Vec<Pin<Box<dyn Future<Output = ()>>>> =
vec![Box::pin(tx1_fut), Box::pin(rx_fut), Box::pin(tx_fut)];
Listado 17-18: Uso de Piny Box::pinpara realizar la Vecverificación de tipo

Si compilamos y ejecutamos esto, finalmente obtenemos el resultado que


esperábamos:

received 'hi'
received 'more'
received 'from'
received 'messages'
received 'the'
received 'for'
received 'future'
received 'you'
Rust
jueves, 22 de mayo de 2025 : Página 507 de 719

¡Uf!

Hay algo más que explorar aquí. Por un lado, usar Pin<Box<T>>añade una
pequeña sobrecarga al colocar estos futuros en el montón con Box—y solo lo
hacemos para que los tipos coincidan. Después de todo, no necesitamos la
asignación del montón: estos futuros son locales para esta función en particular.
Como se mencionó anteriormente, Pines en sí mismo un tipo contenedor, por lo
que podemos obtener la ventaja de tener un solo tipo en Vec—la razón original
por la que lo buscamos— sin tener que Boxrealizar una asignación del montón.
Podemos usar Pindirectamente con cada futuro mediante la std::pin::pinmacro.

Sin embargo, debemos ser explícitos sobre el tipo de referencia fija; de lo


contrario, Rust no sabrá interpretarlos como objetos de rasgo dinámico, que es lo
que necesitamos que sean en el Vec. Por lo tanto, pin!cada futuro al definirlo, y lo
definimos futurescomo un Vecque contiene referencias mutables fijadas al tipo de
futuro dinámico, como en el Listado 17-19.

Nombre de archivo: src/main.rs


let tx1_fut = pin!(async move {
// --snip--
});

let rx_fut = pin!(async {


// --snip--
});

let tx_fut = pin!(async move {


// --snip--
});

let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =


vec![tx1_fut, rx_fut, tx_fut];
Listado 17-19: Uso Pindirecto con la pin!macro para evitar asignaciones de montón innecesarias

Hemos llegado hasta aquí ignorando que podríamos tener diferentes Output tipos.
Por ejemplo, en el Listado 17-20, el futuro anónimo de aimplements Future<Output
= u32>, el futuro anónimo de bimplements Future<Output = &str>y el futuro
anónimo de cimplements Future<Output = bool>.

Nombre de archivo: src/main.rs


let a = async { 1u32 };
let b = async { "Hello!" };
let c = async { true };

let (a_result, b_result, c_result) = trpl::join!(a, b, c);


Rust
jueves, 22 de mayo de 2025 : Página 508 de 719
println!("{a_result}, {b_result}, {c_result}");
Listado 17-20: Tres futuros con tipos distintos

Podemos usar trpl::join!para esperarlos, ya que nos permite pasar múltiples tipos
de futuros y produce una tupla de esos tipos. No podemos usar trpl::join_all, ya que
requiere que todos los futuros pasados tengan el mismo tipo. Recuerda, ese error
fue lo que nos impulsó a empezar esta aventura con Pin!

Este es un equilibrio fundamental: podemos gestionar un número dinámico de


futuros con join_all, siempre que todos sean del mismo tipo, o podemos gestionar
un número fijo de futuros con las joinfunciones o la join!macro, incluso si son de
tipos diferentes. Este es el mismo escenario que enfrentaríamos al trabajar con
cualquier otro tipo en Rust. Los futuros no son especiales, aunque disponemos de
una sintaxis sencilla para trabajar con ellos, lo cual es positivo.

Futuros de las carreras

Al "unir" futuros con la joinfamilia de funciones y macros, necesitamos


que todos finalicen antes de continuar. Sin embargo, a veces solo necesitamos
que finalice un futuro de un conjunto antes de continuar, algo similar a competir
entre futuros.

En el Listado 17-21, una vez más usamos trpl::racepara ejecutar dos


futuros, slowy fast, uno contra el otro.

Nombre de archivo: src/main.rs


let slow = async {
println!("'slow' started.");
trpl::sleep(Duration::from_millis(100)).await;
println!("'slow' finished.");
};

let fast = async {


println!("'fast' started.");
trpl::sleep(Duration::from_millis(50)).await;
println!("'fast' finished.");
};

trpl::race(slow, fast).await;
Listado 17-21: Uso racepara obtener el resultado de cualquier futuro que termine primero

Cada futuro imprime un mensaje al iniciarse, se detiene durante un tiempo


llamando a `y awaiting` sleep, y luego imprime otro mensaje al finalizar. Luego,
pasamos `y` slowa` fasty trpl::raceesperamos a que uno de ellos finalice. (El
Rust
jueves, 22 de mayo de 2025 : Página 509 de 719

resultado no es sorprendente: fastgana). A diferencia de cuando


usamos race`en "Nuestro primer programa asíncrono" , simplemente ignoramos
la Eitherinstancia que devuelve, ya que todo el comportamiento interesante ocurre
en el cuerpo de los bloques asíncronos.

Tenga en cuenta que si invierte el orden de los argumentos a race, el orden de los
mensajes "iniciados" cambia, aunque el fastfuturo siempre se complete primero.
Esto se debe a que la implementación de esta racefunción en particular no es
justa. Siempre ejecuta los futuros pasados como argumentos en el orden en que
se pasan. Otras implementaciones son justas y elegirán aleatoriamente qué
futuro sondear primero. Sin embargo, independientemente de si la
implementación de "race" que usemos es justa, uno de los futuros se ejecutará
hasta el primero awaiten su cuerpo antes de que pueda comenzar otra tarea.

Recordemos que, en "Nuestro Primer Programa Asíncrono" , en cada punto de


espera, Rust permite al tiempo de ejecución pausar la tarea y cambiar a otra si el
futuro que se espera no está listo. Lo inverso también es cierto: Rust solo pausa
los bloques asíncronos y devuelve el control al tiempo de ejecución en un punto
de espera. Todo entre los puntos de espera es síncrono.

Esto significa que si realizas mucho trabajo en un bloque asíncrono sin un punto
de espera, ese futuro impedirá que otros futuros avancen. A veces se dice que un
futuro está dejando sin recursos a otros futuros. En algunos casos, esto puede no
ser un gran problema. Sin embargo, si estás realizando una configuración costosa
o un trabajo de larga duración, o si tienes un futuro que realizará una tarea
específica indefinidamente, tendrás que pensar cuándo y dónde devolver el
control al entorno de ejecución.

Del mismo modo, si tiene operaciones de bloqueo de larga duración, async puede
ser una herramienta útil para proporcionar formas para que las diferentes partes
del programa se relacionen entre sí.

Pero ¿ cómo devolverías el control al tiempo de ejecución en esos casos?

Ceder el control al tiempo de ejecución

Simulemos una operación de larga duración. El Listado 17-22 presenta


una slow función.

Nombre de archivo: src/main.rs


Rust
jueves, 22 de mayo de 2025 : Página 510 de 719
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
Listado 17-22: Uso thread::sleeppara simular operaciones lentas

Este código usa ` std::thread::sleepen lugar de` trpl::sleeppara que la


llamada slowbloquee el hilo actual durante ciertos milisegundos. Podemos usar
` slowpara` representar operaciones reales de larga duración y bloqueantes.

En el Listado 17-23, usamos slowpara emular la realización de este tipo de trabajo


limitado por la CPU en un par de futuros.

Nombre de archivo: src/main.rs


let a = async {
println!("'a' started.");
slow("a", 30);
slow("a", 10);
slow("a", 20);
trpl::sleep(Duration::from_millis(50)).await;
println!("'a' finished.");
};

let b = async {
println!("'b' started.");
slow("b", 75);
slow("b", 10);
slow("b", 15);
slow("b", 350);
trpl::sleep(Duration::from_millis(50)).await;
println!("'b' finished.");
};

trpl::race(a, b).await;
Listado 17-23: Uso thread::sleeppara simular operaciones lentas

Para empezar, cada futuro solo devuelve el control al entorno de


ejecución tras realizar varias operaciones lentas. Si ejecuta este código, verá este
resultado:

'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.
Rust
jueves, 22 de mayo de 2025 : Página 511 de 719

Al igual que en nuestro ejemplo anterior, racetodavía termina en cuanto ase


termina. Sin embargo, no hay intercalación entre los dos futuros. El afuturo realiza
todo su trabajo hasta que trpl::sleepse espera la llamada, entonces...b futuro
realiza todo su trabajo hasta que trpl::sleepse espera su propia llamada, y
finalmente ela se completa. Para que ambos futuros puedan avanzar entre sus
tareas lentas, necesitamos puntos de espera para poder devolver el control al
entorno de ejecución. ¡Eso significa que necesitamos algo que podamos esperar!

Ya podemos ver este tipo de transferencia en el Listado 17-23: si elimináramos


el trpl::sleepal final del afuturo, se completaría sin que el bfuturo se ejecutara en
absoluto . Intentemos usar elsleep el Listado 17-24.

Nombre de archivo: src/main.rs


let one_ms = Duration::from_millis(1);

let a = async {
println!("'a' started.");
slow("a", 30);
trpl::sleep(one_ms).await;
slow("a", 10);
trpl::sleep(one_ms).await;
slow("a", 20);
trpl::sleep(one_ms).await;
println!("'a' finished.");
};

let b = async {
println!("'b' started.");
slow("b", 75);
trpl::sleep(one_ms).await;
slow("b", 10);
trpl::sleep(one_ms).await;
slow("b", 15);
trpl::sleep(one_ms).await;
slow("b", 35);
trpl::sleep(one_ms).await;
println!("'b' finished.");
};
Listado 17-24: Usosleep para permitir que las operaciones se apaguen y se avance

En el Listado 17-24, añadimos trpl::sleepllamadas con puntos de espera entre cada


llamada a slow. Ahora, el trabajo de los dos futuros está intercalado:

'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
Rust
jueves, 22 de mayo de 2025 : Página 512 de 719
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.

El afuturo aún corre un poco antes de entregar el control a b, porque


llama slowantes de llamar.trpl::sleep , pero después los futuros se intercambian
cada vez que uno de ellos alcanza un punto de espera. En este caso, lo hemos
hecho después de cada llamada a slow, pero podríamos dividir el trabajo de la
forma que nos resulte más conveniente.

Sin embargo, no queremos quedarnos dormidos aquí: queremos avanzar lo más


rápido posible. Solo necesitamos devolver el control al entorno de ejecución.
Podemos hacerlo directamente usando la yield_nowfunción. En el Listado 17-25,
reemplazamos todas esas sleepllamadas conyield_now .

Nombre de archivo: src/main.rs


let a = async {
println!("'a' started.");
slow("a", 30);
trpl::yield_now().await;
slow("a", 10);
trpl::yield_now().await;
slow("a", 20);
trpl::yield_now().await;
println!("'a' finished.");
};

let b = async {
println!("'b' started.");
slow("b", 75);
trpl::yield_now().await;
slow("b", 10);
trpl::yield_now().await;
slow("b", 15);
trpl::yield_now().await;
slow("b", 35);
trpl::yield_now().await;
println!("'b' finished.");
};
Listado 17-25: Usoyield_now para permitir que las operaciones se apaguen y se avance

Este código es más claro sobre la intención real y puede ser significativamente
más rápido que usar sleep, ya que temporizadores como el usado por sleepsuelen
tener límites en su granularidad. La versión de que sleepestamos usando, por
ejemplo, siempre estará en reposo durante al menos un milisegundo, incluso si le
Rust
jueves, 22 de mayo de 2025 : Página 513 de 719

pasamos Durationun nanosegundo. De nuevo, las computadoras modernas


son rápidas : ¡pueden hacer mucho en un milisegundo!

Puedes comprobarlo tú mismo configurando una pequeña prueba de rendimiento,


como la del Listado 17-26. (Esta no es una forma especialmente rigurosa de
realizar pruebas de rendimiento, pero basta para mostrar la diferencia).

Nombre de archivo: src/main.rs


let one_ns = Duration::from_nanos(1);
let start = Instant::now();
async {
for _ in 1..1000 {
trpl::sleep(one_ns).await;
}
}
.await;
let time = Instant::now() - start;
println!(
"'sleep' version finished after {} seconds.",
time.as_secs_f32()
);

let start = Instant::now();


async {
for _ in 1..1000 {
trpl::yield_now().await;
}
}
.await;
let time = Instant::now() - start;
println!(
"'yield' version finished after {} seconds.",
time.as_secs_f32()
);
Listado 17-26: Comparación del desempeño desleep yyield_now

Aquí, omitimos la impresión de estado, pasamos un


nanosegundo Durationa trpl::sleepy dejamos que cada futuro se ejecute por sí solo,
sin alternar entre ellos. Luego, ejecutamos 1000 iteraciones y vemos cuánto
tiempo tarda el futuro usando trpl::sleep en comparación con el que
usa trpl::yield_now.

La versión con yield_nowes mucho más rápida!

Esto significa que async puede ser útil incluso para tareas de computación,
dependiendo de qué otras tareas esté realizando el programa, ya que proporciona
una herramienta útil para estructurar las relaciones entre las diferentes partes del
Rust
jueves, 22 de mayo de 2025 : Página 514 de 719

programa. Esta es una forma de multitarea cooperativa , donde cada futuro tiene
la capacidad de determinar cuándo cede el control mediante puntos de espera.
Por lo tanto, cada futuro también tiene la responsabilidad de evitar bloqueos
prolongados. En algunos sistemas operativos embebidos basados en Rust, esta es
la única tipo de multitarea.

En código real, no se suelen alternar llamadas a funciones con puntos de espera


en cada línea. Si bien ceder el control de esta manera es relativamente
económico, no es gratuito. En muchos casos, intentar dividir una tarea limitada
por cómputo puede ralentizarla considerablemente, por lo que a veces es mejor
para el rendimiento general permitir que una operación se bloquee brevemente.
Mida siempre para ver cuáles son los cuellos de botella de rendimiento reales de
su código. Sin embargo, es importante tener en cuenta la dinámica subyacente si
observa que se realiza mucho trabajo en serie que esperaba que se realizara
simultáneamente.

Construyendo nuestras propias abstracciones asíncronas

También podemos componer futuros para crear nuevos patrones. Por ejemplo,
podemos crear una timeoutfunción con bloques de construcción asíncronos que ya
tenemos. Al terminar, el resultado será otro bloque de construcción que podremos
usar para crear aún más abstracciones asíncronas.

El listado 17-27 muestra cómo esperaríamos que esto timeoutfuncione con un


futuro lento.

Nombre de archivo: src/main.rs


let slow = async {
trpl::sleep(Duration::from_millis(100)).await;
"I finished!"
};

match timeout(slow, Duration::from_millis(10)).await {


Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
Listado 17-27: Utilizando nuestra imaginación timeoutpara ejecutar una operación lenta con un límite
de tiempo

¡Implementémoslo! Para empezar, pensemos en la API para timeout:


Rust
jueves, 22 de mayo de 2025 : Página 515 de 719

 Debe ser una función asíncrona para que podamos esperarla.


 Su primer parámetro debe ser un futuro a ejecutar. Podemos hacerlo
genérico para que funcione con cualquier futuro.
 Su segundo parámetro será el tiempo máximo de espera. Si usamos
un Duration, será más fácil pasarlo a trpl::sleep.
 Debería devolver un Result. Si el futuro se completa correctamente,
el . Resulttendrá Okel valor generado por el futuro. Si el tiempo de espera se
agota primero, el . Resulttendrá Errla duración esperada del tiempo de
espera.

El listado 17-28 muestra esta declaración.

Nombre de archivo: src/main.rs


async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
// Here is where our implementation will go!
}
Listado 17-28: Definición de la firma detimeout

Esto satisface nuestros objetivos para los tipos. Ahora, pensemos en


el comportamiento que necesitamos: queremos comparar el futuro introducido
con la duración. Podemos usar trpl::sleeppara crear un temporizador futuro a partir
de la duración y usar trpl::racepara ejecutarlo con el futuro introducido por el
usuario.

También sabemos que raceno es justo sondear los argumentos en el orden en que
se pasan. Por lo tanto, pasamos future_to_trya raceprimero para que tenga la
oportunidad de completarse, incluso si max_timela duración es muy corta.
Si future_to_trytermina primero, raceretornará Leftcon la salida de future_to_try.
Si timertermina primero, raceretornará Rightcon la salida del temporizador de() .

En el Listado 17-29, coincidimos en el resultado de esperar trpl::race.

Nombre de archivo: src/main.rs


use trpl::Either;

// --snip--

fn main() {
trpl::run(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
Rust
jueves, 22 de mayo de 2025 : Página 516 de 719
"Finally finished"
};

match timeout(slow, Duration::from_secs(2)).await {


Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}

async fn timeout<F: Future>(


future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
match trpl::race(future_to_try, trpl::sleep(max_time)).await {
Either::Left(output) => Ok(output),
Either::Right(_) => Err(max_time),
}
Listado 17-29: Definición timeoutcon raceysleep

Si la future_to_tryoperación tiene éxito y obtenemos un Left(output),


devolvemos Ok(output). Si, en cambio, transcurre el tiempo de suspensión y
obtenemos un Right(()), ignoramos el ()con _y devolvemos Err(max_time).

Con esto, tenemos un timeoutproyecto funcional con otros dos ayudantes


asíncronos. Si ejecutamos nuestro código, mostrará el modo de fallo después del
tiempo de espera:

Failed after 2 seconds

Dado que los futuros se integran con otros futuros, se pueden crear herramientas
muy potentes utilizando bloques de construcción asíncronos más pequeños. Por
ejemplo, se puede usar este mismo enfoque para combinar tiempos de espera
con reintentos y, a su vez, usarlos con operaciones como llamadas de red (uno de
los ejemplos del principio del capítulo).

En la práctica, normalmente trabajarás directamente con asyncy await, y


secundariamente con funciones y macros como join, join_all, race, etc. Solo
necesitarás usar for pinocasionalmente para usar futuros con esas API.

Hemos visto varias maneras de trabajar con múltiples futuros simultáneamente. A


continuación, veremos cómo podemos trabajar con múltiples futuros en una
secuencia a lo largo del tiempo con streams . Sin embargo, aquí hay un par de
cosas más que quizás quieras considerar primero:
Rust
jueves, 22 de mayo de 2025 : Página 517 de 719

 Usamos a Veccon join_allpara esperar a que terminaran todos los futuros de


un grupo. ¿Cómo se podría usar a Vecpara procesar un grupo de futuros en
secuencia? ¿Cuáles son las desventajas de hacerlo?
 Observa el futures::stream::FuturesUnorderedtipo de la futures caja. ¿En qué se
diferenciaría usarlo de usar un Vec? (No te preocupes por que provenga de
la streamparte de la caja; funciona perfectamente con cualquier conjunto de
futuros).

Flujos: Futuros en secuencia


Hasta ahora en este capítulo, nos hemos centrado principalmente en futuros
individuales. La única gran excepción fue el canal asíncrono que usamos.
Recuerda cómo usamos el receptor para nuestro canal asíncrono anteriormente
en este capítulo, en la sección "Transmisión de mensajes" . El método
asíncrono recvproduce una secuencia de elementos a lo largo del tiempo. Este es
un ejemplo de un patrón mucho más general conocido como flujo .

Vimos una secuencia de elementos en el Capítulo 13, cuando analizamos


el Iterator rasgo en la sección "El rasgo del iterador y el nextmétodo" , pero existen
dos diferencias entre los iteradores y el receptor de canal asíncrono. La primera
diferencia es el tiempo: los iteradores son síncronos, mientras que el receptor de
canal es asíncrono. La segunda es la API. Al trabajar directamente con Iterator,
llamamos a su nextmétodo síncrono. Con el trpl::Receiverflujo en particular,
llamamos a un recvmétodo asíncrono. Por lo demás, estas API parecen muy
similares, y esa similitud no es casualidad. Un flujo es como una forma asíncrona
de iteración. Mientras que el trpl::Receiverespera específicamente para recibir
mensajes, la API de flujo de propósito general es mucho más amplia: proporciona
el siguiente elemento de la misma manera.Iterator , pero de forma asíncrona.

La similitud entre iteradores y flujos en Rust significa que podemos crear un flujo
a partir de cualquier iterador. Al igual que con un iterador, podemos trabajar con
un flujo llamando a su nextmétodo y esperando la salida, como en el Listado 17-
30.

Nombre de archivo: src/main.rs

let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];


let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
Rust
jueves, 22 de mayo de 2025 : Página 518 de 719
while let Some(value) = stream.next().await {
println!("The value was: {value}");
}
Listado 17-30: Creación de un flujo a partir de un iterador e impresión de sus valores

Comenzamos con un array de números, que convertimos en un iterador y luego


invocamos mappara duplicar todos los valores. Después, convertimos el iterador
en un flujo mediante la trpl::stream_from_iterfunción. A continuación, recorremos los
elementos del flujo a medida que llegan con el while letbucle.

Desafortunadamente, cuando intentamos ejecutar el código, no se compila, sino


que informa que no hay ningún nextmétodo disponible:

error[E0599]: no method named `next` found for struct `Iter` in the current scope
--> src/main.rs:10:40
|
10 | while let Some(value) = stream.next().await {
| ^^^^
|
= note: the full type name has been written to
'file:///projects/async_await/target/debug/deps/async_await-9de943556a6001b8.long-type-
1281356139287206597.txt'
= note: consider using `--verbose` to print the full type name to the console
= help: items from traits can only be used if the trait is in scope
help: the following traits which provide `next` are implemented but not in scope; perhaps you
want to import one of them
|
1 + use crate::trpl::StreamExt;
|
1 + use futures_util::stream::stream::StreamExt;
|
1 + use std::iter::Iterator;
|
1 + use std::str::pattern::Searcher;
|
help: there is a method `try_next` with a similar name
|
10 | while let Some(value) = stream.try_next().await {
| ~~~~~~~~

Como se explica en esta salida, el error del compilador se debe a que


necesitamos el atributo correcto en el ámbito para poder usar el nextmétodo.
Dado lo que hemos discutido hasta ahora, cabría esperar razonablemente que ese
atributo fuera Stream, pero en realidad es . La StreamExtabreviatura
de extensiónExt es un patrón común en la comunidad de Rust para extender un
atributo con otro.
Rust
jueves, 22 de mayo de 2025 : Página 519 de 719

Explicaremos los rasgos Streamy StreamExtcon un poco más de detalle al final del
capítulo, pero por ahora todo lo que necesita saber es que el Streamrasgo define
una interfaz de bajo nivel que combina efectivamente los
rasgos Iteratory Future. StreamExtproporciona un conjunto de API de nivel superior
además de Stream, incluido el nextmétodo , así como otros métodos de utilidad
similares a los proporcionados por el Iteratorrasgo. Streamy StreamExtaún no son
parte de la biblioteca estándar de Rust, pero la mayoría de las cajas de
ecosistemas usan la misma definición.

La solución al error del compilador es agregar una usedeclaración


para trpl::StreamExt, como en el Listado 17-31.

Nombre de archivo: src/main.rs


use trpl::StreamExt;

fn main() {
trpl::run(async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);

while let Some(value) = stream.next().await {


println!("The value was: {value}");
}
});
}
Listado 17-31: Cómo usar con éxito un iterador como base para una secuencia

Con todas esas piezas juntas, ¡este código funciona como queremos! Además,
ahora que está StreamExtdentro del alcance, podemos usar todos sus métodos de
utilidad, al igual que con los iteradores. Por ejemplo, en el Listado 17-32, usamos
el filtermétodo para filtrar todo excepto los múltiplos de tres y cinco.

Nombre de archivo: src/main.rs


use trpl::StreamExt;

fn main() {
trpl::run(async {
let values = 1..101;
let iter = values.map(|n| n * 2);
let stream = trpl::stream_from_iter(iter);

let mut filtered =


stream.filter(|value| value % 3 == 0 || value % 5 == 0);

while let Some(value) = filtered.next().await {


println!("The value was: {value}");
Rust
jueves, 22 de mayo de 2025 : Página 520 de 719
}
});
}
Listado 17-32: Filtrado de un flujo con el StreamExt::filtermétodo

Claro que esto no es muy interesante, ya que podríamos hacer lo mismo con
iteradores normales y sin ningún tipo de async. Veamos qué podemos hacer
que sea exclusivo de los streams.

Composición de secuencias

Muchos conceptos se representan naturalmente como flujos: elementos que se


vuelven disponibles en una cola, fragmentos de datos que se extraen
incrementalmente del sistema de archivos cuando el conjunto de datos completo
es demasiado grande para la computadora, o datos que llegan a través de la red
a lo largo del tiempo. Dado que los flujos son futuros, podemos usarlos con
cualquier otro tipo de futuro y combinarlos de maneras interesantes. Por ejemplo,
podemos agrupar eventos para evitar activar demasiadas llamadas de red,
establecer tiempos de espera en secuencias de operaciones de larga duración o
limitar los eventos de la interfaz de usuario para evitar trabajo innecesario.

Comencemos construyendo un pequeño flujo de mensajes que sirva como


sustituto de un flujo de datos que podríamos ver desde un WebSocket u otro
protocolo de comunicación en tiempo real, como se muestra en el Listado 17-33.

Nombre de archivo: src/main.rs


use trpl::{ReceiverStream, Stream, StreamExt};

fn main() {
trpl::run(async {
let mut messages = get_messages();

while let Some(message) = messages.next().await {


println!("{message}");
}
});
}

fn get_messages() -> impl Stream<Item = String> {


let (tx, rx) = trpl::channel();

let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
for message in messages {
tx.send(format!("Message: '{message}'")).unwrap();
}

ReceiverStream::new(rx)
Rust
jueves, 22 de mayo de 2025 : Página 521 de 719
}
Listado 17-33: Utilizando el rxreceptor comoReceiverStream

Primero, creamos una función llamada get_messagesque devuelve impl Stream<Item


= String>. Para su implementación, creamos un canal asíncrono, recorremos las
primeras 10 letras del alfabeto inglés y las enviamos a través del canal.

También usamos un nuevo tipo: ReceiverStream, que convierte el rxreceptor


de trpl::channela Streammediante un nextmétodo. En main, usamos un while letbucle
para imprimir todos los mensajes del flujo.

Cuando ejecutamos este código, obtenemos exactamente los resultados que


esperábamos:

Message: 'a'
Message: 'b'
Message: 'c'
Message: 'd'
Message: 'e'
Message: 'f'
Message: 'g'
Message: 'h'
Message: 'i'
Message: 'j'

Nuevamente, podríamos hacer esto con la ReceiverAPI regular o incluso


la IteratorAPI regular, así que agreguemos una característica que requiere
transmisiones: agregar un tiempo de espera que se aplique a cada elemento en la
transmisión y un retraso en los elementos que emitimos, como se muestra en el
Listado 17-34.

Nombre de archivo: src/main.rs


use std::{pin::pin, time::Duration};
use trpl::{ReceiverStream, Stream, StreamExt};

fn main() {
trpl::run(async {
let mut messages =
pin!(get_messages().timeout(Duration::from_millis(200)));

while let Some(result) = messages.next().await {


match result {
Ok(message) => println!("{message}"),
Err(reason) => eprintln!("Problem: {reason:?}"),
}
}
})
Rust
jueves, 22 de mayo de 2025 : Página 522 de 719
}
Listado 17-34: Uso del StreamExt::timeoutmétodo para establecer un límite de tiempo para los elementos
de una secuencia

Comenzamos añadiendo un tiempo de espera al flujo con el timeoutmétodo, que


proviene del StreamExtatributo. Luego, actualizamos el cuerpo del while let bucle,
ya que el flujo ahora devuelve un Result. La Okvariable indica que un mensaje
llegó a tiempo; la Errvariable indica que el tiempo de espera transcurrió antes de
que llegara cualquier mensaje. matchEn ese resultado, imprimimos el mensaje al
recibirlo correctamente o un aviso sobre el tiempo de espera. Finalmente, observe
que fijamos los mensajes después de aplicarles el tiempo de espera, ya que el
ayudante de tiempo de espera genera un flujo que debe fijarse para ser
sondeado.

Sin embargo, como no hay retrasos entre mensajes, este tiempo de espera no
altera el comportamiento del programa. Añadamos una variable de retraso a los
mensajes que enviamos, como se muestra en el Listado 17-35.

Nombre de archivo: src/main.rs


fn get_messages() -> impl Stream<Item = String> {
let (tx, rx) = trpl::channel();

trpl::spawn_task(async move {
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
for (index, message) in messages.into_iter().enumerate() {
let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
trpl::sleep(Duration::from_millis(time_to_sleep)).await;

tx.send(format!("Message: '{message}'")).unwrap();
}
});

ReceiverStream::new(rx)
}
Listado 17-35: Envío de mensajes txcon un retraso asíncrono sin crear get_messagesuna función
asíncrona

En get_messages, usamos el enumeratemétodo iterador con el messages array para


obtener el índice de cada elemento que enviamos, junto con el elemento en sí.
Luego, aplicamos un retraso de 100 milisegundos a los elementos de índice par y
de 300 milisegundos a los de índice impar para simular los diferentes retrasos que
podríamos observar en un flujo de mensajes en el mundo real. Dado que nuestro
tiempo de espera es de 200 milisegundos, esto debería afectar a la mitad de los
mensajes.
Rust
jueves, 22 de mayo de 2025 : Página 523 de 719

Para dormir entre mensajes en la get_messagesfunción sin bloquearse,


necesitamos usar async. Sin embargo, no podemos convertirla get_messagesen
una función asíncrona, ya que devolveríamos a Future<Output = Stream<Item =
String>> en lugar de a Stream<Item = String>>. El invocador tendría que
esperar get_messagesa sí mismo para acceder al flujo. Pero recuerda: todo en un
futuro dado ocurre linealmente; la concurrencia ocurre entre futuros.
Esperar get_messagesrequeriría enviar todos los mensajes, incluyendo el retardo
de sueño entre cada mensaje, antes de devolver el flujo del receptor. Como
resultado, el tiempo de espera sería inútil. No habría retrasos en el flujo en sí;
todos ocurrirían incluso antes de que el flujo estuviera disponible.

En lugar de ello, lo dejamos get_messagescomo una función regular que devuelve


una transmisión y generamos una tarea para manejar las sleepllamadas
asincrónicas.

Nota: Llamar spawn_taskde esta manera funciona porque ya configuramos nuestro entorno de
ejecución; de lo contrario, se generaría un pánico. Otras implementaciones optan por diferentes
soluciones: pueden generar un nuevo entorno de ejecución y evitar el pánico, pero terminan con una
sobrecarga adicional, o simplemente no ofrecen una forma independiente de generar tareas sin
referencia a un entorno de ejecución. ¡Asegúrese de conocer la solución elegida por su entorno de
ejecución y escriba su código en consecuencia!

Ahora nuestro código tiene un resultado mucho más interesante. Entre cada par
de mensajes, un Problem: Elapsed(())error.

Message: 'a'
Problem: Elapsed(())
Message: 'b'
Message: 'c'
Problem: Elapsed(())
Message: 'd'
Message: 'e'
Problem: Elapsed(())
Message: 'f'
Message: 'g'
Problem: Elapsed(())
Message: 'h'
Message: 'i'
Problem: Elapsed(())
Message: 'j'

El tiempo de espera no impide que los mensajes lleguen finalmente. Seguimos


recibiendo todos los mensajes originales, ya que nuestro canal es ilimitado :
Rust
jueves, 22 de mayo de 2025 : Página 524 de 719

puede almacenar tantos mensajes como quepan en la memoria. Si el mensaje no


llega antes del tiempo de espera, nuestro controlador de flujo lo tendrá en cuenta,
pero al sondear el flujo de nuevo, es posible que el mensaje ya haya llegado.

Si es necesario, se puede obtener un comportamiento diferente utilizando otros


tipos de canales o flujos de forma más general. Veamos uno de ellos en la
práctica: combine un flujo de intervalos de tiempo con este flujo de mensajes.

Fusión de secuencias

Primero, creemos otro flujo, que emitirá un elemento cada milisegundo si lo


ejecutamos directamente. Para simplificar, podemos usar la sleepfunción para
enviar un mensaje con un retraso y combinarlo con el mismo enfoque que usamos
para get_messagescrear un flujo desde un canal. La diferencia es que, esta vez,
devolveremos el recuento de intervalos transcurridos, por lo que el tipo de
retorno será [número impl Stream<Item = u32>] y podremos llamar a la
función get_intervals(ver Listado 17-36).

Nombre de archivo: src/main.rs


fn get_intervals() -> impl Stream<Item = u32> {
let (tx, rx) = trpl::channel();

trpl::spawn_task(async move {
let mut count = 0;
loop {
trpl::sleep(Duration::from_millis(1)).await;
count += 1;
tx.send(count).unwrap();
}
});

ReceiverStream::new(rx)
}
Listado 17-36: Creación de una secuencia con un contador que se emitirá una vez cada milisegundo

Comenzamos definiendo a counten la tarea. (También podríamos definirlo fuera de


la tarea, pero es más claro limitar el alcance de cualquier variable dada). Luego,
creamos un bucle infinito. Cada iteración del bucle se suspende asincrónicamente
durante un milisegundo, incrementa el conteo y luego lo envía por el canal. Dado
que todo esto está encapsulado en la tarea creada por spawn_task, todo, incluido el
bucle infinito, se limpiará junto con el tiempo de ejecución.

Este tipo de bucle infinito, que termina solo cuando se desconecta todo el entorno
de ejecución, es bastante común en Rust asíncrono: muchos programas necesitan
Rust
jueves, 22 de mayo de 2025 : Página 525 de 719

ejecutarse indefinidamente. Con el modo asíncrono, esto no bloquea nada más,


siempre que haya al menos un punto de espera en cada iteración del bucle.

Ahora, de vuelta en el bloque async de nuestra función principal, podemos


intentar fusionar los flujos messagesy intervals, como se muestra en el Listado 17-
37.

Nombre de archivo: src/main.rs

let messages = get_messages().timeout(Duration::from_millis(200));


let intervals = get_intervals();
let merged = messages.merge(intervals);
Listado 17-37: Intento de fusionar los flujos messagesyintervals

Comenzamos llamando a get_intervals. Luego, fusionamos los


flujos messagesy intervalscon el mergemétodo , que combina varios flujos en uno
solo que produce elementos de cualquiera de los flujos de origen tan pronto como
estén disponibles, sin imponer ningún orden específico. Finalmente, iteramos
sobre ese flujo combinado en lugar de sobre messages.

En este punto, ni messages`nor` intervalsnecesita estar fijado ni ser mutable, ya


que ambos se combinarán en un único mergedflujo. Sin embargo, esta llamada
a mergeno compila. (Tampoco la nextllamada en el while letbucle, pero volveremos
a ello). Esto se debe a que los dos flujos tienen tipos diferentes. El messagesflujo
tiene el tipo ``` Timeout<impl Stream<Item = String>>, donde `` Timeoutes el tipo
que implementa `` Streampara una timeout llamada`. El intervalsflujo tiene el tipo
`` impl Stream<Item = u32>`. Para fusionar estos dos flujos, necesitamos
transformar uno para que coincida con el otro. Reescribiremos el flujo
``intervals`, ya que ``messages` ya tiene el formato básico deseado y debe
gestionar errores de tiempo de espera (véase el Listado 17-38).

Nombre de archivo: src/main.rs


let messages = get_messages().timeout(Duration::from_millis(200));
let intervals = get_intervals()
.map(|count| format!("Interval: {count}"))
.timeout(Duration::from_secs(10));
let merged = messages.merge(intervals);
let mut stream = pin!(merged);
Listado 17-38: Alineación del tipo de flujo intervalscon el tipo de messagesflujo

Primero, podemos usar el mapmétodo auxiliar para transformar ``` intervalsen una
cadena. Segundo, necesitamos que coincida con ` Timeout`` messages. Sin
Rust
jueves, 22 de mayo de 2025 : Página 526 de 719

embargo, como no queremos un tiempo de espera para ``` intervals, podemos


simplemente crear un tiempo de espera mayor que las otras duraciones que
estamos usando. Aquí, creamos un tiempo de espera de 10 segundos con
``` Duration::from_secs(10). Finalmente, necesitamos hacer que streamsea mutable,
para que las llamadas while letdel bucle nextpuedan iterar a través del flujo, y
fijarlo para que sea seguro hacerlo. Esto nos lleva casi a donde necesitamos estar.
Todo verifica el tipo. Sin embargo, si ejecutas esto, habrá dos problemas. Primero,
¡nunca se detendrá! Deberás detenerlo con Ctrl+C . Segundo, los mensajes del
alfabeto inglés quedarán enterrados entre todos los mensajes del contador de
intervalos:

--snip--
Interval: 38
Interval: 39
Interval: 40
Message: 'a'
Interval: 41
Interval: 42
Interval: 43
--snip--

El listado 17-39 muestra una forma de resolver estos dos últimos problemas.

Nombre de archivo: src/main.rs


let messages = get_messages().timeout(Duration::from_millis(200));
let intervals = get_intervals()
.map(|count| format!("Interval: {count}"))
.throttle(Duration::from_millis(100))
.timeout(Duration::from_secs(10));
let merged = messages.merge(intervals).take(20);
let mut stream = pin!(merged);
Listado 17-39: Uso throttley takegestión de los flujos fusionados

Primero, usamos el throttlemétodo en el intervalsflujo para evitar su


sobrecarga messages. La limitación es una forma de limitar la velocidad con la que
se llamará a una función o, en este caso, la frecuencia con la que se sondeará el
flujo. Una vez cada 100 milisegundos debería ser suficiente, ya que esa es
aproximadamente la frecuencia con la que llegan nuestros mensajes.

Para limitar la cantidad de elementos que aceptaremos de un flujo, aplicamos


el take método al mergedflujo, porque queremos limitar la salida final, no solo un
flujo u otro.
Rust
jueves, 22 de mayo de 2025 : Página 527 de 719

Ahora, al ejecutar el programa, se detiene tras extraer 20 elementos del flujo, y


los intervalos no sobrecargan los mensajes. Tampoco obtenemos ```` Interval:
100ni Interval: 200``, sino ``` Interval: 1, Interval: 2```, y así sucesivamente, a pesar
de tener un flujo de origen que puede producir un evento cada milisegundo. Esto
se debe a que la throttlellamada produce un nuevo flujo que encapsula el flujo
original, de modo que este se sondea solo a la velocidad de aceleración, no a su
propia velocidad "nativa". No tenemos un montón de mensajes de intervalo sin
gestionar que decidamos ignorar. En cambio, ¡nunca los producimos! Esta es la
inherente "pereza" de los futuros de Rust en funcionamiento, lo que nos permite
elegir nuestras características de rendimiento.

Interval: 1
Message: 'a'
Interval: 2
Interval: 3
Problem: Elapsed(())
Interval: 4
Message: 'b'
Interval: 5
Message: 'c'
Interval: 6
Interval: 7
Problem: Elapsed(())
Interval: 8
Message: 'd'
Interval: 9
Message: 'e'
Interval: 10
Interval: 11
Problem: Elapsed(())
Interval: 12

Hay un último aspecto que debemos gestionar: ¡los errores! Con ambos flujos
basados en canales, las sendllamadas podrían fallar al cerrarse el otro lado del
canal; esto depende de cómo el entorno de ejecución ejecute los futuros que
conforman el flujo. Hasta ahora, hemos ignorado esta posibilidad llamando
a unwrap, pero en una aplicación con buen funcionamiento, deberíamos gestionar
el error explícitamente, como mínimo finalizando el bucle para no intentar enviar
más mensajes. El listado 17-40 muestra una estrategia de error sencilla: imprimir
el problema y luego breakdesde los bucles.

fn get_messages() -> impl Stream<Item = String> {


let (tx, rx) = trpl::channel();

trpl::spawn_task(async move {
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
Rust
jueves, 22 de mayo de 2025 : Página 528 de 719

for (index, message) in messages.into_iter().enumerate() {


let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
trpl::sleep(Duration::from_millis(time_to_sleep)).await;

if let Err(send_error) = tx.send(format!("Message: '{message}'")) {


eprintln!("Cannot send message '{message}': {send_error}");
break;
}
}
});

ReceiverStream::new(rx)
}

fn get_intervals() -> impl Stream<Item = u32> {


let (tx, rx) = trpl::channel();

trpl::spawn_task(async move {
let mut count = 0;
loop {
trpl::sleep(Duration::from_millis(1)).await;
count += 1;

if let Err(send_error) = tx.send(count) {


eprintln!("Could not send interval {count}: {send_error}");
break;
};
}
});

ReceiverStream::new(rx)
}
Listado 17-40: Manejo de errores y cierre de bucles

Como es habitual, la forma correcta de manejar un error de envío de mensaje


variará; solo asegúrese de tener una estrategia.

Ahora que hemos visto un montón de async en la práctica, demos un paso atrás y
profundicemos en algunos de los detalles de cómo Future, Stream, y otras
características clave que Rust usa para hacer que async funcione.

Una mirada más de cerca a los rasgos de Async


A lo largo del capítulo, hemos usado los
rasgos Future, Pin, Unpin, Streamy StreamExtde diversas maneras. Sin embargo,
hasta ahora hemos evitado profundizar demasiado en su funcionamiento o cómo
se integran, lo cual es adecuado en la mayoría de los casos para el trabajo diario
Rust
jueves, 22 de mayo de 2025 : Página 529 de 719

con Rust. Sin embargo, a veces te encontrarás con situaciones en las que
necesitarás comprender algunos detalles adicionales. En esta sección,
profundizaremos lo suficiente para ayudarte en esos casos, dejando el
análisis más profundo para otra documentación.

El Futurerasgo

Comencemos analizando más detenidamente cómo Futurefunciona este rasgo. Así


lo define Rust:

use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {


type Output;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;


}

Esa definición de rasgo incluye un montón de nuevos tipos y también algo de


sintaxis que no hemos visto antes, así que repasemos la definición pieza por
pieza.

En primer lugar, Futureel tipo asociado Outputde indica en qué se resuelve el


futuro. Esto es análogo al Itemtipo asociado para el Iteratorrasgo. En segundo
lugar, Futuretambién tiene el pollmétodo, que toma una Pin referencia especial
para su selfparámetro y una referencia mutable a un Contexttipo, y devuelve
un Poll<Self::Output>. Hablaremos más sobre Piny Contexten breve. Por ahora,
centrémonos en lo que devuelve el método: el Polltipo:

enum Poll<T> {
Ready(T),
Pending,
}

Este Polltipo es similar a un Option. Tiene una variante con valor, Ready(T), y otra
sin valor, Pending. Sin embargo, Pollsignifica algo muy diferente de Option.
La Pendingvariante indica que el futuro aún tiene trabajo por hacer, por lo que el
invocador deberá volver a comprobarlo más tarde. La Ready variante indica que el
futuro ha finalizado su trabajo y el Tvalor está disponible.

Nota: En la mayoría de los futuros, el emisor no debe pollvolver a llamar después de que el futuro haya
regresado Ready. Muchos futuros entrarán en pánico si se les vuelve a sondear después de estar listos.
Rust
jueves, 22 de mayo de 2025 : Página 530 de 719

Los futuros que se pueden sondear de nuevo de forma segura lo indicarán explícitamente en su
documentación. Esto es similar a cómo Iterator::nextse comporta.

Cuando ves código que usa await, Rust lo compila internamente en código que
llama a poll. Si revisas el Listado 17-4, donde imprimimos el título de la página
para una URL una vez resuelta, Rust lo compila en algo similar (aunque no
exactamente) a esto:

match page_title(url).poll() {
Ready(page_title) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// But what goes here?
}
}

¿Qué debemos hacer cuando el futuro está en calma Pending? Necesitamos una
forma de intentarlo una y otra vez, hasta que el futuro esté finalmente listo. En
otras palabras, necesitamos un bucle:

let mut page_title_fut = page_title(url);


loop {
match page_title_fut.poll() {
Ready(value) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// continue
}
}
}

Sin embargo, si Rust compilara exactamente ese código, todo awaitestaría


bloqueado, ¡justo lo contrario de lo que buscábamos! En cambio, Rust se asegura
de que el bucle pueda transferir el control a algo que pueda pausar el trabajo en
este futuro para trabajar en otros futuros y luego volver a verificar este. Como
hemos visto, ese algo es un entorno de ejecución asíncrono, y esta programación
y coordinación es una de sus principales funciones.

Anteriormente en este capítulo, describimos la espera de rx.recv. La recvllamada


devuelve un futuro, y la espera del futuro lo sondea. Observamos que un entorno
de ejecución pausará el futuro hasta que esté listo
Rust
jueves, 22 de mayo de 2025 : Página 531 de 719

con Some(message)o Nonecuando el canal se cierra. Con una comprensión más


profunda del Futurerasgo, y específicamente de Future::poll, podemos ver cómo
funciona. El entorno de ejecución sabe que el futuro no está listo cuando
devuelve Poll::Pending. Por el contrario, sabe que el futuro está listo y lo adelanta
cuando polldevuelve Poll::Ready(Some(message))o Poll::Ready(None).

Los detalles exactos de cómo un tiempo de ejecución hace eso están más allá del
alcance de este libro, pero la clave es ver la mecánica básica de los futuros: un
tiempo de ejecución sondea cada futuro del que es responsable, volviendo a
poner el futuro a dormir cuando aún no está listo.

Los rasgos PinyUnpin

Cuando introdujimos la idea de la fijación en el Listado 17-16, nos encontramos


con un mensaje de error muy molesto. Aquí está la parte relevante:

error[E0277]: `{async block@src/main.rs:10:23: 10:33}` cannot be unpinned


--> src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `{async
block@src/main.rs:10:23: 10:33}`, which is required by `Box<{async
block@src/main.rs:10:23: 10:33}>: Future`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current
scope
= note: required for `Box<{async block@src/main.rs:10:23: 10:33}>` to implement
`Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-util-0.3.30/src/
future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`

Este mensaje de error nos indica no solo que necesitamos fijar los valores, sino
también por qué es necesario. La trpl::join_allfunción devuelve una estructura
llamada JoinAll. Esta estructura es genérica sobre un tipo F, que está restringido a
implementar el Futureatributo. Esperar directamente un futuro con , awaitfija el
futuro implícitamente. Por eso no necesitamos usarla pin!siempre que queramos
esperar futuros.
Rust
jueves, 22 de mayo de 2025 : Página 532 de 719

Sin embargo, no estamos esperando directamente un futuro. En su lugar,


construimos un nuevo futuro, JoinAllpasando una colección de futuros a
la join_all función. La firma `for` join_allrequiere que todos los tipos de elementos
de la colección implementen el Futurerasgo, y Box<T>se implementa Futuresolo si
el Tobjeto que envuelve es un futuro que implementa el Unpinrasgo.

¡Hay mucho que asimilar! Para comprenderlo bien, profundicemos un poco más
en cómo Futurefunciona realmente el rasgo, en particular en lo que respecta a la
fijación .

Mire nuevamente la definición del Futurerasgo:

use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {


type Output;

// Required method
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

El cxparámetro y su Contexttipo son clave para que un entorno de ejecución sepa


cuándo verificar un futuro dado sin perder la pereza. Nuevamente, los detalles de
su funcionamiento quedan fuera del alcance de este capítulo, y generalmente
solo es necesario considerarlos al escribir una Futureimplementación
personalizada. Nos centraremos en el tipo "for" self, ya que es la primera vez que
vemos un método "where" selfcon una anotación de tipo. Una anotación de tipo
" selfis" funciona como las anotaciones de tipo para otros parámetros de función,
pero con dos diferencias clave:

 Le dice a Rust qué tipo selfdebe ser el método para que se llame.
 No puede ser cualquier tipo. Se limita al tipo en el que se implementa el
método, una referencia o un puntero inteligente a ese tipo, o
una Pinreferencia envolvente a ese tipo.

Veremos más sobre esta sintaxis en el Capítulo 18. Por ahora, basta con saber
que, si queremos sondear un futuro para comprobar si es Pendingo Ready(Output),
necesitamos una Pinreferencia mutable con encapsulado de tipo.

Pines un contenedor para tipos similares a punteros como &, &mut, Boxy Rc.
(Técnicamente, Pinfunciona con tipos que implementan los rasgos Derefo , pero
Rust
jueves, 22 de mayo de 2025 : Página 533 de 719

esto es efectivamente equivalente a trabajar solo con punteros). no es un puntero


en sí mismo y no tiene ningún comportamiento propio como el conteo de
referencias; es puramente una herramienta que el compilador puede usar para
imponer restricciones en el uso de punteros. DerefMutPinRcArc

Recordar que awaitse implementa en términos de llamadas a pollempieza a


explicar el mensaje de error que vimos antes, pero eso era en términos de Unpin,
no de Pin. Entonces, ¿cómo se relaciona exactamente Pincon Unpin, y por
qué Futurenecesita selfestar en un Pintipo para llamar a poll?

Recuerda que, como se mencionó anteriormente en este capítulo, una serie de


puntos de espera en un futuro se compilan en una máquina de estados. El
compilador se asegura de que dicha máquina siga todas las reglas habituales de
seguridad de Rust, incluyendo el préstamo y la propiedad. Para ello, Rust analiza
los datos necesarios entre un punto de espera y el siguiente, o bien, el final del
bloque asíncrono. A continuación, crea una variante correspondiente en la
máquina de estados compilada. Cada variante obtiene el acceso necesario a los
datos que se utilizarán en esa sección del código fuente, ya sea apropiándose de
ellos o obteniendo una referencia mutable o inmutable a ellos.

Hasta ahora, todo bien: si nos equivocamos en la propiedad o las referencias de


un bloque asíncrono, el verificador de préstamos nos lo indicará. Cuando
queremos movernos por el futuro correspondiente a ese bloque (por ejemplo,
moverlo a un objeto Vecpara pasarlo), join_allla cosa se complica.

Cuando movemos un futuro, ya sea insertándolo en una estructura de datos para


usarlo como iterador join_allo devolviéndolo de una función, en realidad significa
mover la máquina de estados que Rust crea. Y a diferencia de la mayoría de los
demás tipos en Rust, los futuros que Rust crea para bloques asíncronos pueden
tener referencias a sí mismos en los campos de cualquier variante, como se
muestra en la ilustración simplificada de la Figura 17-4.

Figura 17-4: Un tipo de datos autorreferencial.

Sin embargo, por defecto, no es seguro mover ningún objeto que tenga una
referencia a sí mismo, ya que las referencias siempre apuntan a la dirección de
memoria real de aquello a lo que hacen referencia (véase la Figura 17-5). Si se
mueve la estructura de datos, esas referencias internas seguirán apuntando a la
ubicación anterior. Sin embargo, esa ubicación de memoria ya no es válida. Por
Rust
jueves, 22 de mayo de 2025 : Página 534 de 719

un lado, su valor no se actualizará al modificar la estructura de datos. Y, por otro


lado, aún más importante, ¡el ordenador ahora puede reutilizar esa memoria para
otros fines! Podría terminar leyendo datos completamente ajenos posteriormente.

Figura 17-5: El resultado inseguro de mover un tipo de datos autorreferencial

En teoría, el compilador de Rust podría intentar actualizar cada referencia a un


objeto cada vez que se mueve, pero esto podría suponer una sobrecarga de
rendimiento considerable, especialmente si se necesita actualizar toda una red de
referencias. Si, en cambio, pudiéramos asegurarnos de que la estructura de datos
en cuestión no se mueva en memoria , no tendríamos que actualizar ninguna
referencia. Esto es precisamente lo que requiere el verificador de préstamos de
Rust: en código seguro, impide mover cualquier elemento con una referencia
activa.

PinSe basa en esto para brindarnos la garantía exacta que necesitamos. Al fijar un
valor envolviendo un puntero a ese valor en [nombre del objeto] Pin, este ya no
puede moverse. Por lo tanto, si tiene [nombre del objeto] Pin<Box<SomeType>>, en
realidad fija el SomeTypevalor, no el Boxpuntero. La Figura 17-6 ilustra este
proceso.

Figura 17-6: Fijación de un `Box` que apunta a un tipo futuro autorreferencial.

De hecho, el Boxpuntero aún puede moverse libremente. Recuerde: nos importa


asegurarnos de que los datos referenciados permanezcan en su lugar. Si un
puntero se mueve, pero los datos a los que apunta permanecen en el mismo
lugar , como en la Figura 17-7, no hay ningún problema potencial. Como ejercicio
independiente, consulte la documentación de los tipos, así como
del std::pinmódulo, e intente averiguar cómo se haría esto con
un Pinencapsulado Box. La clave es que el tipo autorreferencial no se puede
mover, porque sigue anclado.

Figura 17-7: Mover un `Box` que apunta a un tipo futuro autorreferencial.

Sin embargo, la mayoría de los tipos se pueden mover con total seguridad,
incluso si se encuentran detrás de un Pinpuntero. Solo debemos considerar la
fijación cuando los elementos tienen referencias internas. Los valores primitivos,
como números y booleanos, son seguros, ya que, obviamente, no tienen
Rust
jueves, 22 de mayo de 2025 : Página 535 de 719

referencias internas, por lo que obviamente son seguros. Tampoco la mayoría de


los tipos con los que se trabaja normalmente en Rust. VecPor ejemplo, se puede
mover un objeto a sin preocupaciones. Dado lo que hemos visto hasta ahora, si se
tiene un objeto a Pin<Vec<String>>, se tendría que hacer todo mediante las API
seguras pero restrictivas que proporciona Pin, aunque Vec<String>siempre es
seguro mover un objeto a si no hay otras referencias a él. Necesitamos una forma
de indicarle al compilador que no hay problema en mover elementos en casos
como este, y ahí es donde Unpinentra en juego.

UnpinEs un rasgo marcador, similar a los rasgos Sendy Syncque vimos en el


Capítulo 16, y por lo tanto no tiene funcionalidad propia. Los rasgos marcadores
existen únicamente para indicar al compilador que es seguro usar el tipo que
implementa un rasgo dado en un contexto particular. UnpinInforma al compilador
que un tipo dado no necesita garantizar que el valor en cuestión se pueda mover
de forma segura.

Al igual que con Send`and` Sync, el compilador implementa Unpinautomáticamente


para todos los tipos donde pueda demostrar su seguridad. Un caso especial,
similar a ` Sendand` Sync, es `where` noUnpin se implementa para un tipo. La
notación para esto es ` `, donde` es el nombre de un tipo que sí necesita
mantener dichas garantías para ser seguro siempre que se use un puntero a ese
tipo en `` .impl !Unpin for SomeTypeSomeTypePin

En otras palabras, hay dos aspectos a tener en cuenta sobre la relación


entre Piny Unpin. Primero, Unpines el caso "normal" y !Unpines el caso especial.
Segundo, si un tipo implementa Unpino !Unpin solo importa cuando se usa un
puntero anclado a ese tipo, como .Pin<&mut SomeType>

Para concretarlo, piense en un String: tiene una longitud y los caracteres Unicode
que lo componen. Podemos encapsular un String, Pincomo se muestra en la Figura
17-8. Sin embargo, Stringimplementa automáticamente Unpin, como la mayoría de
los demás tipos en Rust.

Figura 17-8: Fijación de una `Cadena`; la línea punteada indica que la `Cadena` implementa el
rasgo `Desanclar` y, por lo tanto, no está fijada.

Como resultado, podemos hacer cosas que serían ilegales si Stringse


implementaran !Unpin, como reemplazar una cadena por otra en la misma
ubicación de memoria que en la Figura 17-9. Esto no viola el Pincontrato, ya
Rust
jueves, 22 de mayo de 2025 : Página 536 de 719

que Stringno tiene referencias internas que hagan que sea inseguro moverse.
Precisamente por eso implementa Unpinen lugar de !Unpin.

Figura 17-9: Reemplazo de la `String` con una `String` completamente diferente en la memoria.

Ahora sabemos lo suficiente para comprender los errores reportados para


esa join_allllamada en el Listado 17-17. Originalmente, intentamos mover los
futuros generados por bloques asíncronos a un Vec<Box<dyn Future<Output =
()>>>, pero como hemos visto, esos futuros pueden tener referencias internas,
por lo que no implementan Unpin. Deben estar fijados, y luego podemos pasar
el Pintipo a Vec, con la seguridad de que los datos subyacentes en los
futuros no se moverán.

Piny Unpinson importantes principalmente para crear bibliotecas de bajo nivel o al


crear un entorno de ejecución, más que para el código diario de Rust. Sin
embargo, cuando veas estas características en los mensajes de error, ¡tendrás
una mejor idea de cómo corregir tu código!

Nota: Esta combinación de Piny Unpinpermite implementar de forma segura toda una clase de tipos
complejos en Rust, que de otro modo resultarían difíciles por ser autorreferenciales. Los tipos que
requieren Pinson más comunes en Rust asíncrono actualmente, pero ocasionalmente también pueden
verse en otros contextos.

Los detalles de cómo Pinfuncionan Unpiny las reglas que deben respetar se cubren extensamente en la
documentación de la API de std::pin, por lo que, si está interesado en obtener más información, este es
un excelente lugar para comenzar.

Si quieres entender con más detalle cómo funcionan las cosas bajo el capó, consulta los
Capítulos 2 y 4 de Programación asincrónica en Rust .

ElStreamrasgo

Ahora que comprende mejor los rasgos Future, Piny Unpin, podemos centrarnos en
el Streamrasgo. Como aprendió anteriormente en este capítulo, los flujos son
similares a los iteradores asíncronos. A diferencia de Iteratory Future, sin
embargo, Streamno tiene una definición en la biblioteca estándar al momento de
escribir este artículo, pero existe una definición muy común en la biblioteca
estándar.futures contenedor utilizado en todo el ecosistema.
Rust
jueves, 22 de mayo de 2025 : Página 537 de 719

Repasemos las definiciones de los rasgos Iteratory Futureantes de ver cómo


un Streamrasgo podría combinarlos. De Iterator, obtenemos la idea de una
secuencia: su nextmétodo proporciona un Option<Self::Item>. De Future, obtenemos
la idea de la disponibilidad a lo largo del tiempo: su pollmétodo proporciona
un Poll<Self::Output>. Para representar una secuencia de elementos que se vuelven
disponibles con el tiempo, definimos un Streamrasgo que reúne esas
características:

use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
type Item;

fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>
) -> Poll<Option<Self::Item>>;
}

El Streamrasgo define un tipo asociado llamado Itempara el tipo de los elementos


producidos por el flujo. Esto es similar a Iterator, donde puede haber de cero a
muchos elementos, y a diferencia de Future, donde siempre hay un solo Output,
incluso si es del tipo unidad.() .

StreamTambién define un método para obtener esos elementos. Lo


llamamos poll_next, para aclarar que sondea de la misma manera Future::polly
produce una secuencia de elementos de la misma manera Iterator::next. Su tipo de
retorno se combina Pollcon Option. El tipo externo es Poll, porque debe
comprobarse su disponibilidad, al igual que un futuro. El tipo interno es Option ,
porque necesita indicar si hay más mensajes, al igual que un iterador.

Es probable que algo muy similar a esta definición se incorpore a la biblioteca


estándar de Rust. Mientras tanto, forma parte del conjunto de herramientas de la
mayoría de los entornos de ejecución, así que puedes confiar en ella, y todo lo
que veremos a continuación debería ser aplicable en general.

Sin embargo, en el ejemplo que vimos en la sección sobre streaming, no usamos


" poll_next or" Stream , sino " nextand StreamExt". Podríamos trabajar directamente
con la poll_nextAPI escribiendo manualmente nuestras propias Stream máquinas de
estados, por supuesto, al igual que podríamos trabajar con futuros directamente
Rust
jueves, 22 de mayo de 2025 : Página 538 de 719

mediante su pollmétodo. Usar awaites mucho más práctico, y el StreamExt rasgo


proporciona el nextmétodo para que podamos hacer precisamente eso:

trait StreamExt: Stream {


async fn next(&mut self) -> Option<Self::Item>
where
Self: Unpin;

// other methods...
}

Nota: La definición que usamos anteriormente en este capítulo es ligeramente diferente, ya que admite
versiones de Rust que aún no admitían el uso de funciones asíncronas en rasgos. Por lo tanto, se ve así:

fn next(&mut self) -> Next<'_, Self> where Self: Unpin;

Ese Nexttipo es un structque implementa Futurey nos permite nombrar el tiempo de vida de la
referencia a selfcon Next<'_, Self>, para que await pueda trabajar con este método.

El StreamExtrasgo también es el hogar de todos los métodos interesantes


disponibles para usar con streams. StreamExtse implementa automáticamente
para cada tipo que implementa Stream, pero estos rasgos se definen por separado
para permitir que la comunidad itere en API de conveniencia sin afectar el rasgo
fundamental.

En la versión de StreamExtusada en el trplcrate, el rasgo no solo define


el nextmétodo, sino que también proporciona una implementación
predeterminada de next que gestiona correctamente los detalles de la
llamada Stream::poll_next. Esto significa que, incluso cuando necesite escribir su
propio tipo de datos de streaming, solo tiene que implementar Stream, y cualquier
persona que use su tipo de datos podrá usar StreamExty sus métodos
automáticamente.

Eso es todo lo que cubriremos en cuanto a los detalles básicos de estos rasgos.
Para concluir, veamos cómo se integran los futuros (incluidos los flujos), las tareas
y los hilos.

Juntándolo todo: futuros, tareas e hilos


Como vimos en el Capítulo 16 , los hilos ofrecen un enfoque para la concurrencia.
En este capítulo, hemos visto otro enfoque: usar async con futuros y flujos. Si te
Rust
jueves, 22 de mayo de 2025 : Página 539 de 719

preguntas cuándo elegir un método u otro, la respuesta es: ¡depende! Y, en


muchos casos, la elección no es entre hilos o async, sino entre hilos y async.

Muchos sistemas operativos han proporcionado modelos de concurrencia basados


en subprocesos durante décadas, y como resultado, muchos lenguajes de
programación los admiten. Sin embargo, estos modelos tienen sus
inconvenientes. En muchos sistemas operativos, utilizan bastante memoria para
cada subproceso y conllevan cierta sobrecarga al iniciarse y apagarse. Además,
los subprocesos solo son una opción cuando el sistema operativo y el hardware
los admiten. A diferencia de los ordenadores de escritorio y móviles
convencionales, algunos sistemas integrados no tienen sistema operativo, por lo
que tampoco tienen subprocesos.

El modelo asíncrono ofrece un conjunto de ventajas y desventajas diferente, y en


última instancia complementaria. En él, las operaciones concurrentes no
requieren sus propios subprocesos. En cambio, pueden ejecutarse en tareas,
como cuando solíamos trpl::spawn_taskiniciar el trabajo desde una función síncrona
en la sección de flujos. Una tarea es similar a un subproceso, pero en lugar de ser
administrada por el sistema operativo, es administrada por código a nivel de
biblioteca: el entorno de ejecución.

En la sección anterior, vimos que podíamos crear un flujo usando un canal


asíncrono y generando una tarea asíncrona que podíamos llamar desde código
síncrono. Podemos hacer exactamente lo mismo con un hilo. En el Listado 17-40,
usamos trpl::spawn_tasky trpl::sleep. En el Listado 17-41, los reemplazamos con las
API thread::spawny thread::sleepde la biblioteca estándar en la get_intervalsfunción.

Nombre de archivo: src/main.rs


fn get_intervals() -> impl Stream<Item = u32> {
let (tx, rx) = trpl::channel();

// This is *not* `trpl::spawn` but `std::thread::spawn`!


thread::spawn(move || {
let mut count = 0;
loop {
// Likewise, this is *not* `trpl::sleep` but `std::thread::sleep`!
thread::sleep(Duration::from_millis(1));
count += 1;

if let Err(send_error) = tx.send(count) {


eprintln!("Could not send interval {count}: {send_error}");
break;
};
}
Rust
jueves, 22 de mayo de 2025 : Página 540 de 719
});

ReceiverStream::new(rx)
}
Listado 17-41: Uso de las std::threadAPI en lugar de las API asíncronas trplpara la get_intervalsfunción

Si ejecuta este código, la salida es idéntica a la del Listado 17-40. Observe los
pocos cambios desde la perspectiva del código de llamada. Es más, aunque una
de nuestras funciones generó una tarea asíncrona en el entorno de ejecución y la
otra generó un subproceso del sistema operativo, los flujos resultantes no se
vieron afectados por las diferencias.

A pesar de sus similitudes, estos dos enfoques se comportan de forma muy


diferente, aunque podría resultar difícil medirlo en este ejemplo tan sencillo.
Podríamos generar millones de tareas asíncronas en cualquier ordenador personal
moderno. Si intentáramos hacerlo con hilos, ¡literalmente nos quedaríamos sin
memoria!

Sin embargo, existe una razón por la que estas API son tan similares. Los hilos
actúan como límite para conjuntos de operaciones síncronas; la concurrencia es
posible entre hilos. Las tareas actúan como límite para conjuntos
de operaciones asíncronas ; la concurrencia es posible tanto entre tareas
como dentro de ellas, ya que una tarea puede alternar entre futuros en su cuerpo.
Finalmente, los futuros son la unidad de concurrencia más granular de Rust, y
cada futuro puede representar un árbol de otros futuros. El entorno de ejecución
(específicamente, su ejecutor) gestiona las tareas, y las tareas gestionan los
futuros. En ese sentido, las tareas son similares a hilos ligeros gestionados por el
entorno de ejecución, con capacidades adicionales gracias a que son gestionadas
por un entorno de ejecución en lugar del sistema operativo.

Esto no significa que las tareas asíncronas sean siempre mejores que los hilos (o
viceversa). La concurrencia con hilos es, en cierto modo, un modelo de
programación más sencillo que la concurrencia con async. Esto puede ser una
fortaleza o una debilidad. Los hilos son, en cierto modo, un proceso de "disparar y
olvidar"; no tienen un equivalente nativo a un futuro, por lo que simplemente se
ejecutan hasta su finalización sin ser interrumpidos, excepto por el propio sistema
operativo. Es decir, no tienen soporte integrado para la concurrencia
intratarea como sí lo tienen los futuros. Los hilos en Rust tampoco tienen
mecanismos de cancelación, un tema que no hemos abordado explícitamente en
Rust
jueves, 22 de mayo de 2025 : Página 541 de 719

este capítulo, pero que se insinuó por el hecho de que, al finalizar un futuro, su
estado se limpiaba correctamente.

Estas limitaciones también dificultan la composición de hilos que de futuros. Por


ejemplo, es mucho más difícil usar hilos para crear ayudantes como los
métodos timeouty throttleque desarrollamos anteriormente en este capítulo. El
hecho de que los futuros sean estructuras de datos más ricas significa que
pueden componerse juntos de forma más natural, como hemos visto.

Las tareas, por lo tanto, nos brindan mayor control sobre los futuros,
permitiéndonos elegir dónde y cómo agruparlos. Resulta que los hilos y las tareas
suelen funcionar muy bien juntos, ya que las tareas pueden (al menos en algunos
entornos de ejecución) moverse entre hilos. De hecho, el entorno de ejecución
que hemos estado usando, incluidas las funciones spawn_blockingy spawn_task, es
multihilo por defecto. Muchos entornos de ejecución utilizan un enfoque
llamado robo de trabajo para mover tareas de forma transparente entre hilos,
según su uso actual, y así mejorar el rendimiento general del sistema. Este
enfoque requiere hilos y tareas, y por lo tanto, futuros.

Al pensar qué método utilizar en cada caso, tenga en cuenta estas reglas
generales:

 Si el trabajo es muy paralelizable , como procesar un montón de datos


donde cada parte se puede procesar por separado, los subprocesos son una
mejor opción.
 Si el trabajo es muy concurrente , como manejar mensajes de varias
fuentes diferentes que pueden llegar en distintos intervalos o a distintas
velocidades, async es una mejor opción.

Y si necesitas paralelismo y concurrencia, no tienes que elegir entre subprocesos


y asíncrono. Puedes usarlos juntos libremente, permitiendo que cada uno
desempeñe su función principal. Por ejemplo, el Listado 17-42 muestra un
ejemplo bastante común de esta combinación en código Rust real.

Nombre de archivo: src/main.rs


use std::{thread, time::Duration};

fn main() {
let (tx, mut rx) = trpl::channel();

thread::spawn(move || {
Rust
jueves, 22 de mayo de 2025 : Página 542 de 719
for i in 1..11 {
tx.send(i).unwrap();
thread::sleep(Duration::from_secs(1));
}
});

trpl::run(async {
while let Some(message) = rx.recv().await {
println!("{message}");
}
});
}
Listado 17-42: Envío de mensajes con código de bloqueo en un hilo y espera de los mensajes en un
bloque asíncrono

Comenzamos creando un canal asíncrono y luego generamos un hilo que asume


la propiedad del lado emisor del canal. Dentro del hilo, enviamos los números del
1 al 10, con un intervalo de espera de un segundo entre cada uno. Finalmente,
ejecutamos un futuro creado con un bloque asíncrono, trpl::runcomo se ha hecho a
lo largo del capítulo. En ese futuro, esperamos esos mensajes, como en los otros
ejemplos de paso de mensajes que hemos visto.

Para retomar el escenario inicial del capítulo, imaginemos que ejecutamos un


conjunto de tareas de codificación de vídeo mediante un hilo dedicado (ya que la
codificación de vídeo está limitada por el cálculo), pero notificamos a la interfaz
de usuario que dichas operaciones se realizan con un canal asíncrono. Existen
innumerables ejemplos de este tipo de combinaciones en casos prácticos reales.

Resumen
Esta no es la última vez que verá concurrencia en este libro. El proyecto
del Capítulo 21 aplicará estos conceptos en una situación más realista que los
ejemplos más sencillos aquí presentados y comparará la resolución de problemas
con subprocesos frente a las tareas de forma más directa.

Independientemente de cuál de estos enfoques elija, Rust le brinda las


herramientas que necesita para escribir código seguro, rápido y simultáneo, ya
sea para un servidor web de alto rendimiento o para un sistema operativo
integrado.

A continuación, hablaremos sobre las formas idiomáticas de modelar problemas y


estructurar soluciones a medida que tus programas en Rust crecen. Además,
Rust
jueves, 22 de mayo de 2025 : Página 543 de 719

analizaremos cómo se relacionan los modismos de Rust con aquellos que quizás
conozcas de la programación orientada a objetos.

Características de programación
orientada a objetos de Rust
La programación orientada a objetos (POO) es una forma de modelar programas.
Los objetos, como concepto programático, se introdujeron en el lenguaje de
programación Simula en la década de 1960. Estos objetos influyeron en la
arquitectura de programación de Alan Kay, en la que los objetos se intercambian
mensajes. Para describir esta arquitectura, acuñó el término " programación
orientada a objetos" en 1967. Existen muchas definiciones que compiten entre sí
para definir qué es la POO, y según algunas de ellas, Rust se considera orientado
a objetos, pero según otras no. En este capítulo, exploraremos ciertas
características que se consideran comúnmente orientadas a objetos y cómo se
traducen en el lenguaje común de Rust. A continuación, le mostraremos cómo
implementar un patrón de diseño orientado a objetos en Rust y analizaremos las
ventajas y desventajas de hacerlo frente a implementar una solución que
aproveche algunas de las ventajas de Rust.

Características de los lenguajes orientados a


objetos
No existe consenso en la comunidad de programación sobre las características
que debe tener un lenguaje para ser considerado orientado a objetos. Rust se ve
influenciado por muchos paradigmas de programación, incluyendo la
programación orientada a objetos; por ejemplo, exploramos las características
derivadas de la programación funcional en el capítulo 13. Se podría decir que los
lenguajes orientados a objetos comparten ciertas características comunes, como
los objetos, la encapsulación y la herencia. Analicemos el significado de cada una
de estas características y si Rust las admite.

Los objetos contienen datos y comportamiento

El libro "Patrones de Diseño: Elementos de Software Orientado a Objetos


Reutilizable" de Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides
(Addison-Wesley Professional, 1994), conocido popularmente como " El libro de la
Rust
jueves, 22 de mayo de 2025 : Página 544 de 719

Banda de los Cuatro ", es un catálogo de patrones de diseño orientados a objetos.


Define la POO de esta manera:

Los programas orientados a objetos se componen de objetos. Un objeto contiene


tanto datos como los procedimientos que operan sobre ellos. Los procedimientos
suelen denominarse métodos u operaciones .

Según esta definición, Rust está orientado a objetos: las estructuras y las
enumeraciones contienen datos, y impllos bloques proporcionan métodos en ellas.
Aunque las estructuras y las enumeraciones con métodos no
se denominan objetos, ofrecen la misma funcionalidad, según la definición de
objetos de la Banda de los Cuatro.

Encapsulación que oculta detalles de implementación

Otro aspecto comúnmente asociado con la programación orientada a objetos


(POO) es la idea de encapsulación , lo que significa que los detalles de
implementación de un objeto no son accesibles para el código que lo utiliza. Por lo
tanto, la única forma de interactuar con un objeto es a través de su API pública; el
código que lo utiliza no debería poder acceder a sus componentes internos y
modificar directamente los datos o el comportamiento. Esto permite al
programador modificar y refactorizar los componentes internos de un objeto sin
necesidad de modificar el código que lo utiliza.

En el Capítulo 7, explicamos cómo controlar la encapsulación: podemos usar


la pub palabra clave para decidir qué módulos, tipos, funciones y métodos de
nuestro código deben ser públicos, y por defecto, todo lo demás es privado. Por
ejemplo, podemos definir una estructura AveragedCollectionque tenga un campo
que contenga un vector de i32valores. La estructura también puede tener un
campo que contenga el promedio de los valores del vector, lo que significa que no
es necesario calcular el promedio cuando se necesita. En otras
palabras, AveragedCollectionalmacenará en caché el promedio calculado. El Listado
18-1 contiene la definición de la AveragedCollectionestructura:

Nombre de archivo: src/lib.rs


pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
Listado 18-1: Una AveragedCollectionestructura que mantiene una lista de números enteros y el promedio
de los elementos de la colección
Rust
jueves, 22 de mayo de 2025 : Página 545 de 719

La estructura está marcada pubpara que otro código pueda usarla, pero sus
campos permanecen privados. Esto es importante en este caso, ya que queremos
asegurar que, al añadir o eliminar un valor de la lista, también se actualice el
promedio. Para ello, implementamos los métodos add, remove, y averageen la
estructura, como se muestra en el Listado 18-2:

Nombre de archivo: src/lib.rs


impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}

pub fn remove(&mut self) -> Option<i32> {


let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}

pub fn average(&self) -> f64 {


self.average
}

fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
Listado 18-2: Implementaciones de los métodos públicos add, remove, y averageenAveragedCollection

Los métodos públicos add, removey averageson las únicas formas de acceder o
modificar datos en una instancia de AveragedCollection. Cuando se añade o elimina
un elemento mediante listel método, las implementaciones de cada
uno también invocan el método privado que gestiona la actualización del
campo.addremoveupdate_averageaverage

Mantenemos los campos listy averageprivados para que el código externo no


pueda añadir ni eliminar elementos listdirectamente; de lo contrario,
el averagecampo podría desincronizarse con los list cambios. El averagemétodo
devuelve el valor del averagecampo, lo que permite que el código externo lo
lea, averagepero no lo modifique.
Rust
jueves, 22 de mayo de 2025 : Página 546 de 719

Dado que hemos encapsulado los detalles de implementación de


struct AveragedCollection, podemos modificar fácilmente aspectos como la
estructura de datos en el futuro. Por ejemplo, podríamos usar `a` HashSet<i32>en
lugar de `a` Vec<i32>para el listcampo. Mientras las firmas de los
métodos add`, remove` y average`public` permanezcan iguales, el código que
usa AveragedCollectionno necesitaría cambiar para compilar. Si, listen cambio,
hiciéramos `public`, esto no sería necesariamente
así: HashSet<i32>y Vec<i32>tendríamos diferentes métodos para añadir y eliminar
elementos, por lo que el código externo probablemente tendría que cambiar si se
modificara listdirectamente.

Si la encapsulación es un aspecto necesario para que un lenguaje se considere


orientado a objetos, Rust cumple con ese requisito. La opción de usarla pubo no
para diferentes partes del código permite la encapsulación de los detalles de
implementación.

La herencia como sistema de tipos y como código compartido

La herencia es un mecanismo mediante el cual un objeto puede heredar


elementos de la definición de otro objeto, obteniendo así los datos y el
comportamiento del objeto padre sin tener que definirlos nuevamente.

Si un lenguaje debe tener herencia para ser orientado a objetos, Rust no lo es. No
es posible definir una estructura que herede los campos y las implementaciones
de métodos de la estructura principal sin usar una macro.

Sin embargo, si estás acostumbrado a tener herencia en tu caja de herramientas


de programación, puedes usar otras soluciones en Rust, dependiendo del motivo
por el cual recurres a la herencia en primer lugar.

Se puede elegir la herencia por dos razones principales. Una es la reutilización del
código: se puede implementar un comportamiento específico para un tipo, y la
herencia permite reutilizar esa implementación para un tipo diferente. Esto se
puede hacer de forma limitada en el código de Rust mediante implementaciones
predeterminadas de métodos de traits, como se vio en el Listado 10-14 cuando
añadimos una implementación predeterminada del summarizemétodo en
el Summarytrait. Cualquier tipo que implemente el Summarytrait tendrá
el summarizemétodo disponible sin necesidad de código adicional. Esto es similar a
que una clase padre tenga la implementación de un método y una clase hija
Rust
jueves, 22 de mayo de 2025 : Página 547 de 719

heredera también tenga la implementación del método. También se puede anular


la implementación predeterminada del summarizemétodo al implementar
el Summarytrait, lo cual es similar a que una clase hija anule la implementación de
un método heredado de una clase padre.

La otra razón para usar la herencia se relaciona con el sistema de tipos: permitir
que un tipo hijo se use en los mismos lugares que el tipo padre. Esto también se
denomina polimorfismo , lo que significa que se pueden sustituir varios objetos
entre sí en tiempo de ejecución si comparten ciertas características.

Polimorfismo

Para muchos, el polimorfismo es sinónimo de herencia. Sin embargo, en realidad es un concepto más
general que se refiere al código que puede trabajar con datos de múltiples tipos. En el caso de la
herencia, estos tipos suelen ser subclases.

En cambio, Rust utiliza genéricos para abstraer diferentes tipos posibles y límites de características, e
imponer restricciones sobre lo que esos tipos deben proporcionar. Esto a veces se
denomina polimorfismo paramétrico acotado .

La herencia ha perdido popularidad recientemente como solución de diseño de


programación en muchos lenguajes, ya que a menudo corre el riesgo de
compartir más código del necesario. Las subclases no siempre deberían compartir
todas las características de su clase padre, pero sí lo harán con la herencia. Esto
puede reducir la flexibilidad del diseño de un programa. También introduce la
posibilidad de invocar métodos en subclases que no tienen sentido o que causan
errores porque los métodos no se aplican a la subclase. Además, algunos
lenguajes solo permiten la herencia simple (es decir, una subclase solo puede
heredar de una clase), lo que restringe aún más la flexibilidad del diseño de un
programa.

Por estas razones, Rust adopta un enfoque diferente: usar objetos de rasgo en
lugar de herencia. Veamos cómo los objetos de rasgo facilitan el polimorfismo en
Rust.

Uso de objetos de rasgos que permiten valores


de diferentes tipos
Rust
jueves, 22 de mayo de 2025 : Página 548 de 719

En el Capítulo 8, mencionamos que una limitación de los vectores es que solo


pueden almacenar elementos de un tipo. Creamos una solución alternativa en el
Listado 8-9, donde definimos una SpreadsheetCellenumeración con variantes para
almacenar enteros, números de punto flotante y texto. Esto significaba que
podíamos almacenar diferentes tipos de datos en cada celda y aun así tener un
vector que representara una fila de celdas. Esta es una solución ideal cuando
nuestros elementos intercambiables son un conjunto fijo de tipos que conocemos
al compilar el código.

Sin embargo, a veces queremos que el usuario de nuestra biblioteca pueda


ampliar el conjunto de tipos válidos en una situación particular. Para mostrar
cómo podemos lograrlo, crearemos un ejemplo de herramienta de interfaz gráfica
de usuario (GUI) que itera sobre una lista de elementos, invocando
un drawmétodo en cada uno para mostrarlo en pantalla (una técnica común en
herramientas GUI). Crearemos un contenedor de biblioteca llamado guique
contiene la estructura de una biblioteca GUI. Este contenedor podría incluir
algunos tipos, como Buttono TextField. Además, guilos usuarios querrán crear sus
propios tipos que se puedan mostrar: por ejemplo, un programador podría añadir
un Imagey otro un SelectBox.

No implementaremos una biblioteca GUI completa para este ejemplo, pero


mostraremos cómo encajarían las piezas. Al momento de escribir la biblioteca, no
podemos conocer ni definir todos los tipos que otros programadores podrían
querer crear. Pero sí sabemos que guinecesita registrar muchos valores de
diferentes tipos y que necesita llamar a un drawmétodo para cada uno de estos
valores con tipos diferentes. No necesita saber exactamente qué sucederá al
llamar al drawmétodo, solo que el valor tendrá ese método disponible para que lo
llamemos.

Para lograr esto en un lenguaje con herencia, podríamos definir una clase
llamada Componentque contenga un método llamado draw. Las demás clases,
como Button, Imagey SelectBox, heredarían de Componenty, por lo tanto, heredarían
el drawmétodo. Cada una podría sobrescribir el drawmétodo para definir su
comportamiento personalizado, pero el framework podría tratar todos los tipos
como si fueran Componentinstancias e invocarlos draw. Sin embargo, como Rust no
tiene herencia, necesitamos otra forma de estructurar la guibiblioteca para que los
usuarios puedan extenderla con nuevos tipos.

Definición de un rasgo de comportamiento común


Rust
jueves, 22 de mayo de 2025 : Página 549 de 719

Para implementar el comportamiento que queremos guitener, definiremos un trait


llamado Drawque tendrá un método llamado draw. Luego podemos definir un
vector que toma un objeto trait . Un objeto 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
trait especificando algún tipo de puntero, como una &referencia o
un Box<T>puntero inteligente, luego la dyn palabra clave y finalmente
especificando el trait relevante. (Hablaremos sobre la razón por la que los objetos
trait deben usar un puntero en el Capítulo 20 en la sección “Tipos de tamaño
dinámico y el SizedTrait” ). Podemos usar objetos trait en lugar de un tipo genérico
o concreto. Dondequiera que usemos un objeto trait, el sistema de tipos de Rust
garantizará en tiempo de compilación que cualquier valor usado en ese contexto
implementará el trait del objeto trait. En consecuencia, no necesitamos conocer
todos los tipos posibles en tiempo de compilación.

Hemos mencionado que, en Rust, evitamos llamar "objetos" a las estructuras y


enumeraciones para distinguirlas de los objetos de otros lenguajes. En una
estructura o enumeración, los datos de sus campos y el comportamiento
de impllos bloques están separados, mientras que en otros lenguajes, la
combinación de datos y comportamiento en un solo concepto suele denominarse
objeto. Sin embargo, los objetos de rasgo se parecen más a los objetos de otros
lenguajes en el sentido de que combinan datos y comportamiento. Sin embargo,
los objetos de rasgo se diferencian de los objetos tradicionales en que no se
pueden añadir datos a un objeto de rasgo. Los objetos de rasgo no son tan útiles
como los de otros lenguajes: su propósito específico es permitir la abstracción del
comportamiento común.

El listado 18-3 muestra cómo definir un rasgo nombrado Drawcon un método


llamado draw:

Nombre de archivo: src/lib.rs


pub trait Draw {
fn draw(&self);
}
Listado 18-3: Definición del Drawrasgo

Esta sintaxis debería resultar familiar tras nuestra explicación sobre cómo definir
rasgos en el Capítulo 10. A continuación, se presenta una sintaxis nueva: el
Listado 18-4 define una estructura llamada Screenque contiene un vector
llamado components. Este vector es de tipo Box<dyn Draw>, que es un objeto de
Rust
jueves, 22 de mayo de 2025 : Página 550 de 719

rasgo; es un sustituto de cualquier tipo dentro de a Boxque implemente


el Drawrasgo.

Nombre de archivo: src/lib.rs


pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
Listado 18-4: Definición de la Screenestructura con un componentscampo que contiene un vector de
objetos de rasgo que implementan el Drawrasgo

En la Screenestructura, definiremos un método llamado runque llamará


al drawmétodo en cada uno de sus components, como se muestra en el Listado 18-
5:

Nombre de archivo: src/lib.rs


impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
Listado 18-5: Un runmétodo Screenque llama al drawmétodo en cada componente

Esto funciona de forma diferente a definir una estructura que utiliza un parámetro
de tipo genérico con límites de rasgo. Un parámetro de tipo genérico solo puede
sustituirse por un tipo concreto a la vez, mientras que los objetos de rasgo
permiten que varios tipos concretos lo reemplacen en tiempo de ejecución. Por
ejemplo, podríamos haber definido la Screenestructura utilizando un tipo genérico
y un límite de rasgo, como en el Listado 18-6:

Nombre de archivo: src/lib.rs


pub struct Screen<T: Draw> {
pub components: Vec<T>,
}

impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
Listado 18-6: Una implementación alternativa de la Screenestructura y su runmétodo utilizando
genéricos y límites de rasgos
Rust
jueves, 22 de mayo de 2025 : Página 551 de 719

Esto nos limita a una Screeninstancia con una lista de componentes, todos de
tipo Buttono todos de tipo TextField. Si solo se tienen colecciones homogéneas, es
preferible usar genéricos y límites de rasgos, ya que las definiciones se
monomorfizarán en tiempo de compilación para usar los tipos concretos.

Por otro lado, con el método que utiliza objetos de rasgo, una Screeninstancia
puede contener un objeto Vec<T>que contiene un objeto Box<Button>y un
objeto Box<TextField>. Veamos cómo funciona esto y luego hablaremos de las
implicaciones para el rendimiento en tiempo de ejecución.

Implementando el rasgo

Ahora agregaremos algunos tipos que implementan el Drawatributo.


Proporcionaremos el Buttontipo. Nuevamente, la implementación de una biblioteca
GUI queda fuera del alcance de este libro, por lo que el drawmétodo no tendrá
ninguna implementación útil en su cuerpo. Para imaginar cómo sería la
implementación, una Buttonestructura podría tener campos para width, height,
y label, como se muestra en el Listado 18-7:

Nombre de archivo: src/lib.rs


pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}

impl Draw for Button {


fn draw(&self) {
// code to actually draw a button
}
}
Listado 18-7: Una Buttonestructura que implementa el Drawrasgo

Los campos width, height, y de un componente serán diferentes a los de otros


componentes; por ejemplo, un tipo podría tener esos mismos campos más
un campo. Cada tipo que queramos dibujar en la pantalla implementará
el atributo, pero usará código diferente en el método para definir cómo dibujar
ese tipo en particular, como se muestra aquí (sin el código de la interfaz gráfica
de usuario, como se mencionó). El tipo, por ejemplo, podría tener un bloque
adicional con métodos relacionados con lo que sucede cuando un usuario hace
clic en el botón. Este tipo de métodos no se aplican a tipos
como .labelButtonTextFieldplaceholderDrawdrawButtonButtonimplTextField
Rust
jueves, 22 de mayo de 2025 : Página 552 de 719

Si alguien que usa nuestra biblioteca decide implementar una SelectBoxestructura


que tiene los campos width, height, y , también optionsimplementa el Drawrasgo en
el tipo, como se muestra en el Listado 18-8:SelectBox

Nombre de archivo: src/main.rs


use gui::Draw;

struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}

impl Draw for SelectBox {


fn draw(&self) {
// code to actually draw a select box
}
}
Listado 18-8: Otra caja que usa guie implementa el Drawrasgo en una SelectBoxestructura

El usuario de nuestra biblioteca ahora puede escribir su mainfunción para crear


una Screen instancia. A la Screeninstancia, puede agregar "a" SelectBoxy
"a" Button colocando cada uno en "a" Box<T>para convertirlo en un objeto de
rasgo. Luego, puede llamar al runmétodo en la Screeninstancia, que llamará drawa
cada uno de los componentes. El listado 18-9 muestra esta implementación:

Nombre de archivo: src/main.rs


use gui::{Button, Screen};

fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};

screen.run();
Rust
jueves, 22 de mayo de 2025 : Página 553 de 719
}
Listado 18-9: Uso de objetos de rasgo para almacenar valores de diferentes tipos que implementan el
mismo rasgo

Cuando escribimos la biblioteca, no sabíamos que alguien podría agregar


el SelectBoxtipo, pero nuestra Screenimplementación pudo operar en el nuevo tipo
y dibujarlo porque SelectBoximplementa el Drawrasgo, lo que significa que
implementa el drawmétodo.

Este concepto —preocuparse únicamente por los mensajes a los que responde un
valor, en lugar de por su tipo concreto— es similar al concepto de tipado de
pato en lenguajes de tipado dinámico: si camina como un pato y grazna como un
pato, ¡debe ser un pato! En la implementación de runon Screen del Listado 18-
5, runno necesita saber cuál es el tipo concreto de cada componente. No
comprueba si un componente es una instancia de a Button o a SelectBox,
simplemente invoca el drawmétodo en el componente. Al especificar Box<dyn
Draw>el tipo de los valores en el components vector, hemos definido Screenque
necesitamos valores sobre los que podemos invocar el draw método.

La ventaja de usar objetos de rasgo y el sistema de tipos de Rust para escribir


código similar al que usa tipado de pato es que nunca tenemos que comprobar si
un valor implementa un método específico en tiempo de ejecución ni
preocuparnos por 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 rasgos que necesitan los objetos de rasgo.

Por ejemplo, el Listado 18-10 muestra lo que sucede si intentamos crear


un Screen con a Stringcomo componente:

Nombre de archivo: src/main.rs

use gui::Screen;

fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};

screen.run();
}
Listado 18-10: Intento de usar un tipo que no implementa el rasgo del objeto de rasgo
Rust
jueves, 22 de mayo de 2025 : Página 554 de 719

Obtendremos este error porque Stringno se implementa el Drawrasgo:

$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5| components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not
implemented for `String`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast from `Box<String>` to `Box<dyn Draw>`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error

Este error nos permite saber que estamos pasando algo que Screenno queríamos
pasar y por lo tanto deberíamos pasar un tipo diferente o deberíamos
implementarlo Drawpara Stringpoder Screenllamarlo draw.

Los objetos de rasgo realizan un envío dinámico

Recuerde en la sección “Rendimiento del código usando genéricos” en el Capítulo


10 nuestra discusión sobre el proceso de monomorfización realizado en genéricos
por el compilador: 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á
haciendo un despacho estático , que es cuando el compilador sabe qué método
está llamando en tiempo de compilación. Esto es opuesto al despacho dinámico ,
que es cuando el compilador no puede decir en tiempo de compilación qué
método está llamando. En los casos de despacho dinámico, el compilador emite
código que en tiempo de ejecución averiguará qué método llamar.

Al usar objetos de rasgo, Rust debe usar el envío dinámico. El compilador


desconoce todos los tipos que podrían usarse con el código que usa objetos de
rasgo, 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 objeto de rasgo
para saber qué método llamar. Esta búsqueda implica un costo de tiempo de
ejecución que no ocurre con el envío estático. El envío dinámico también impide
que el compilador elija insertar el código de un método en línea, lo que a su vez
impide algunas optimizaciones. Rust tiene algunas reglas sobre dónde se puede y
no se puede usar el envío dinámico, llamadas compatibilidad dyn . Sin embargo,
Rust
jueves, 22 de mayo de 2025 : Página 555 de 719

obtuvimos flexibilidad adicional en el código que escribimos en el Listado 18-5 y


pudimos admitir en el Listado 18-9, por lo que es una compensación a considerar.

Implementación de un patrón de diseño


orientado a objetos
El patrón de estado es un patrón de diseño orientado a objetos. Su esencia reside
en definir un conjunto de estados que un valor puede tener internamente. Los
estados se representan mediante un conjunto de objetos de estado , y el
comportamiento del valor cambia en función de su estado. Vamos a trabajar con
un ejemplo de una estructura de entrada de blog que tiene un campo para
almacenar su estado, que será un objeto de estado del conjunto "borrador",
"revisión" o "publicado".

Los objetos de estado comparten funcionalidad: en Rust, por supuesto, usamos


estructuras y rasgos en lugar de objetos y herencia. Cada objeto de estado es
responsable de su propio comportamiento y de determinar cuándo debe cambiar
a otro estado. El valor que contiene un objeto de estado no conoce el
comportamiento de los estados ni cuándo realizar la transición entre ellos.

La ventaja de usar el patrón de estado es que, cuando cambian los requisitos de


negocio del programa, no es necesario modificar el código del valor que contiene
el estado ni el código que lo utiliza. Solo es necesario actualizar el código dentro
de uno de los objetos de estado para cambiar sus reglas o quizás añadir más
objetos de estado.

Primero, implementaremos el patrón de estado de una forma más tradicional,


orientada a objetos, y luego usaremos un enfoque más natural en Rust.
Profundicemos en la implementación incremental de un flujo de trabajo de
entradas de blog utilizando el patrón de estado.

La funcionalidad final se verá así:

1. Una publicación de blog comienza como un borrador vacío.


2. Una vez finalizado el borrador se solicita la revisión de la publicación.
3. Cuando se aprueba la publicación, se publica.
4. Sólo las publicaciones de blog publicadas devuelven el contenido para
imprimir, por lo que las publicaciones no aprobadas no pueden publicarse
accidentalmente.
Rust
jueves, 22 de mayo de 2025 : Página 556 de 719

Cualquier otro cambio que se intente en una publicación no debería tener efecto.
Por ejemplo, si intentamos aprobar un borrador de una publicación del blog antes
de solicitar una revisión, la publicación debería permanecer como borrador no
publicado.

El Listado 18-11 muestra este flujo de trabajo en código: este es un ejemplo de


uso de la API que implementaremos en un cajón de biblioteca llamado blog. Aún
no se compilará porque no hemos implementado el blogcajón.

Nombre de archivo: src/main.rs

use blog::Post;

fn main() {
let mut post = Post::new();

post.add_text("I ate a salad for lunch today");


assert_eq!("", post.content());

post.request_review();
assert_eq!("", post.content());

post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
Listado 18-11: Código que demuestra el comportamiento deseado que queremos blogque tenga nuestra
caja

Queremos permitir que el usuario cree un borrador de entrada de blog con


[nombre del método] Post::new. Queremos permitir que se añada texto a la
entrada. Si intentamos obtener el contenido de la entrada inmediatamente, antes
de su aprobación, no deberíamos obtener ningún texto, ya que la entrada sigue
siendo un borrador. Hemos añadido assert_eq!el código para fines de
demostración. Una excelente prueba unitaria sería comprobar que un borrador de
entrada de blog devuelve una cadena vacía del contentmétodo, pero no vamos a
escribir pruebas para este ejemplo.

A continuación, queremos habilitar una solicitud de revisión de la publicación


y contentdevolver una cadena vacía mientras esperamos la revisión. Cuando la
publicación reciba la aprobación, debería publicarse, lo que significa que se
devolverá el texto de la publicación al contentllamarla.
Rust
jueves, 22 de mayo de 2025 : Página 557 de 719

Tenga en cuenta que el único tipo con el que interactuamos desde la caja es
el Post tipo. Este tipo usará el patrón de estado y contendrá un valor que será uno
de los tres objetos de estado que representan los diferentes estados en los que
puede encontrarse una publicación: borrador, en espera de revisión o publicada.
El cambio de un estado a otro se gestionará internamente dentro del Posttipo. Los
estados cambian en respuesta a los métodos llamados por los usuarios de nuestra
biblioteca en la Postinstancia, pero no tienen que gestionar los cambios de estado
directamente. Además, los usuarios no pueden cometer errores con los estados,
como publicar una publicación antes de que se revise.

Definición Posty creación de una nueva instancia en el estado de


borrador

¡Comencemos con la implementación de la biblioteca! Sabemos que necesitamos


una Postestructura pública que contenga contenido, así que comenzaremos con la
definición de la estructura y una newfunción pública asociada para crear una
instancia de Post, como se muestra en el Listado 18-12. También crearemos
un Stateatributo privado que definirá el comportamiento que Post deben tener
todos los objetos de estado para a .

Luego, Postse guardará un objeto de rasgo Box<dyn State>dentro Option<T> de un


campo privado llamado statepara guardar el objeto de estado. Verás por
qué Option<T>es necesario más adelante.

Nombre de archivo: src/lib.rs


pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}

impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}

trait State {}

struct Draft {}

impl State for Draft {}


Rust
jueves, 22 de mayo de 2025 : Página 558 de 719
Listado 18-12: Definición de una Postestructura y una newfunción que crea una nueva Postinstancia,
un Staterasgo y una Draftestructura

El Staterasgo define el comportamiento compartido por los diferentes estados de


las publicaciones. Los objetos de estado son Draft, PendingReviewy Published, y todos
implementan el Staterasgo. Por ahora, el rasgo no tiene métodos, y
comenzaremos definiendo solo el Draftestado, ya que es el estado en el que
queremos que comience una publicación.

Al crear un nuevo objeto Post, asignamos statea su campo un Somevalor que


contiene un objeto Box. Esto Boxapunta a una nueva instancia de
la Draftestructura. Esto garantiza que, al crear una nueva instancia de Post,
comience como borrador. Dado que el statecampo de Postes privado, no es posible
crear un objeto Posten ningún otro estado. En la Post::newfunción, asignamos
al contentcampo un objeto . vacío String.

Almacenamiento del texto del contenido de la publicación

Vimos en el Listado 18-11 que queremos poder llamar a un método


llamado add_texty pasarle un `a`, &strque luego se añade como contenido de texto
de la entrada del blog. Lo implementamos como un método, en lugar de exponer
el content campo como `` pub, para que posteriormente podamos implementar un
método que controle cómo contentse leen los datos del campo. El add_textmétodo
es bastante sencillo, así que agreguemos la implementación del Listado 18-13
al impl Postbloque:

Nombre de archivo: src/lib.rs


impl Post {
// --snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Listado 18-13: Implementación del add_textmétodo para agregar texto a una publicacióncontent

El add_textmétodo toma una referencia mutable a self, ya que cambiamos


la Postinstancia a la que llamamos add_text. Luego,
llamamos push_stra Stringin contenty pasamos el textargumento para añadir a
saved content. Este comportamiento no depende del estado de la publicación, por
lo que no forma parte del patrón de estado. El add_textmétodo no interactúa con
el statecampo en absoluto, pero forma parte del comportamiento que queremos
admitir.
Rust
jueves, 22 de mayo de 2025 : Página 559 de 719

Cómo asegurarse de que el contenido de un borrador de


publicación esté vacío

Incluso después de llamar add_texty añadir contenido a nuestra publicación,


queremos que el contentmétodo devuelva un fragmento de cadena vacío, ya que
la publicación sigue en borrador, como se muestra en la línea 7 del Listado 18-11.
Por ahora, implementemos el contentmétodo con la opción más sencilla para
cumplir este requisito: devolver siempre un fragmento de cadena vacío.
Cambiaremos esto más adelante, una vez que implementemos la posibilidad de
cambiar el estado de una publicación para que pueda publicarse. Hasta ahora, las
publicaciones solo pueden estar en borrador, por lo que el contenido de la
publicación siempre debe estar vacío. El Listado 18-14 muestra esta
implementación de marcador de posición:

Nombre de archivo: src/lib.rs


impl Post {
// --snip--
pub fn content(&self) -> &str {
""
}
}
Listado 18-14: Agregar una implementación de marcador de posición para el contentmétodo Postque
siempre devuelve una porción de cadena vacía

Con este contentmétodo agregado, todo en el Listado 18-11 hasta la línea 7


funciona según lo previsto.

Solicitar una revisión del puesto cambia su estado

A continuación, necesitamos añadir la función para solicitar la revisión de una


publicación, lo que debería cambiar su estado de Drafta PendingReview. El listado
18-15 muestra este código:

Nombre de archivo: 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>;
}
Rust
jueves, 22 de mayo de 2025 : Página 560 de 719

struct Draft {}

impl State for Draft {


fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
}

struct PendingReview {}

impl State for PendingReview {


fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
}
Listado 18-15: Implementación de request_reviewmétodos en Posty el Staterasgo

Damos Postun método público llamado request_reviewque tomará una referencia


mutable a self. Luego, llamamos a un request_reviewmétodo interno en el estado
actual de Post, y este segundo request_reviewmétodo consume el estado actual y
devuelve un nuevo estado.

Añadimos el request_reviewmétodo al Stateatributo; todos los tipos que


implementan el atributo ahora deberán implementar el request_reviewmétodo.
Tenga en cuenta que, en lugar de tener self, &selfo &mut selfcomo primer
parámetro del método, tenemos self: Box<Self>. Esta sintaxis significa que el
método solo es válido cuando se invoca en un Boxque contiene el tipo. Esta
sintaxis toma posesión de Box<Self>, invalidando el estado anterior para que el
valor de estado de Postpueda transformarse en un nuevo estado.

Para consumir el estado anterior, el request_reviewmétodo debe tomar posesión del


valor del estado. Aquí es donde entra en juego el `` Optionen el statecampo
de` Post : llamamos al takemétodo para extraer el Somevalor del state campo y
dejar `` Noneen su lugar`, ya que Rust no permite tener campos vacíos en las
estructuras. Esto nos permite extraer el statevalor de ` Post`en lugar de tomarlo
prestado`. Luego, estableceremos el statevalor de la publicación en el resultado
de esta operación.

Necesitamos configurarlo statetemporalmente Noneen lugar de hacerlo


directamente con código, como self.state = self.state.request_review();para obtener la
propiedad del statevalor. Esto garantiza Postque no se pueda usar el statevalor
anterior después de transformarlo a un nuevo estado.
Rust
jueves, 22 de mayo de 2025 : Página 561 de 719

El request_reviewmétodo "on" Draftdevuelve una nueva instancia encapsulada de


una nueva PendingReviewestructura, que representa el estado en el que una
publicación espera una revisión. La PendingReviewestructura también implementa
el request_reviewmétodo, pero no realiza ninguna transformación. En su lugar, se
devuelve a sí misma, ya que cuando solicitamos una revisión de una publicación
que ya está en ese PendingReviewestado, debe permanecer en PendingReviewél.

Ahora podemos empezar a ver las ventajas del patrón de estado:


el request_reviewmétodo Postes el mismo independientemente de su statevalor.
Cada estado es responsable de sus propias reglas.

Dejaremos el contentmétodo Postcomo está, devolviendo una porción de cadena


vacía. Ahora podemos tener a Posten el PendingReviewestado, así como en
el Draftestado, pero queremos el mismo comportamiento en
el PendingReviewestado. ¡El listado 18-11 ahora funciona hasta la línea 10!

Añadiendo approvepara cambiar el comportamiento decontent

El approvemétodo será similar al request_reviewmétodo : se establecerá stateen el


valor que el estado actual dice que debería tener cuando se apruebe ese estado,
como se muestra en el Listado 18-16:

Nombre de archivo: src/lib.rs


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 {}

impl State for Draft {


// --snip--
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
Rust
jueves, 22 de mayo de 2025 : Página 562 de 719
struct PendingReview {}

impl State for PendingReview {


// --snip--
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}

struct Published {}

impl State for Published {


fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}

fn approve(self: Box<Self>) -> Box<dyn State> {


self
}
}
Listado 18-16: Implementación del approvemétodo en Posty el Staterasgo

Agregamos el approvemétodo al Staterasgo y agregamos una nueva estructura que


implementa Stateel Publishedestado.

Similar al funcionamiento request_reviewde on PendingReview, si llamamos


al approvemétodo en a Draft, no tendrá ningún efecto porque approvedevolverá self.
Cuando llamamos approvea on PendingReview, devuelve una nueva instancia
encapsulada de la Publishedestructura. La Publishedestructura implementa
el Staterasgo y, tanto para el request_reviewmétodo como para el approve método,
se devuelve a sí misma, ya que la publicación debe permanecer en
el Published estado en esos casos.

Ahora necesitamos actualizar el contentmétodo en Post. Queremos que el valor


devuelto de contentdependa del estado actual de Post, por lo que tendremos
el Postdelegado a un contentmétodo definido en su state, como se muestra en el
Listado 18-17:

Nombre de archivo: src/lib.rs

impl Post {
// --snip--
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
// --snip--
}
Rust
jueves, 22 de mayo de 2025 : Página 563 de 719
Listado 18-17: Actualización del contentmétodo en Postpara delegar a un contentmétodo enState

Dado que el objetivo es mantener todas estas reglas dentro de las estructuras
que implementan State, llamamos a un contentmétodo en el valor statey pasamos
la instancia de la publicación (es decir, self) como argumento. Luego, devolvemos
el valor obtenido al usar el contentmétodo en el statevalor.

Llamamos al as_refmétodo en [nombre del método] Optionporque queremos una


referencia al valor dentro de [nombre del método] Optionen lugar de la propiedad
del valor. Dado que [nombre del método state ] es un [ Option<Box<dyn
State>>nombre del método], al llamar a [nombre del
método] as_ref, Option<&Box<dyn State>>se devuelve un [nombre del método]. Si
no llamamos a [nombre del método] as_ref, obtendríamos un error porque no
podemos salir statedel [nombre del método] tomado &selfdel parámetro de la
función.

Luego, llamamos al unwrapmétodo, que sabemos que nunca entrará en pánico,


porque sabemos que los métodos de " Postasegure" statesiempre contendrán
un Some valor al finalizar. Este es uno de los casos que abordamos en la
sección "Casos en los que se tiene más información que el compilador" del
capítulo 9, cuando sabemos que un Nonevalor nunca es posible, aunque el
compilador no pueda comprenderlo.

En este punto, al llamar contenta &Box<dyn State>, la coerción de deref se aplicará


a , por &lo Boxque el contentmétodo se llamará en el tipo que implementa
el Staterasgo. Esto significa que debemos agregar información contenta
la Statedefinición del rasgo, y ahí es donde colocaremos la lógica para determinar
el contenido que se devolverá según el estado, como se muestra en el Listado 18-
18:

Nombre de archivo: src/lib.rs


trait State {
// --snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}

// --snip--
struct Published {}

impl State for Published {


// --snip--
Rust
jueves, 22 de mayo de 2025 : Página 564 de 719
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
Listado 18-18: Adición del contentmétodo al Staterasgo

Agregamos una implementación predeterminada para el contentmétodo que


devuelve una porción de cadena vacía. Esto significa que no necesitamos
implementarla contenten las estructuras ` Draft `` y ` PendingReview`.
La Publishedestructura sobrescribirá el content método y devolverá el valor en
` post.content``.

Tenga en cuenta que necesitamos anotaciones de duración de vida en este


método, como analizamos en el Capítulo 10. Estamos tomando una referencia a
a postcomo argumento y devolviendo una referencia a parte de eso post, por lo
que la duración de vida de la referencia devuelta está relacionada con la duración
de vida del postargumento.

¡Listo! ¡Todo el Listado 18-11 ya funciona! Implementamos el patrón de estado


con las reglas del flujo de trabajo de entradas de blog. La lógica relacionada con
las reglas reside en los objetos de estado, en lugar de estar dispersa por todo el
archivo Post.

¿Por qué no una enumeración?

Quizás te hayas preguntado por qué no usamos una enumcon los diferentes estados de publicación
posibles como variantes. Sin duda, es una posible solución; pruébala y compara los resultados para ver
cuál prefieres. Una desventaja de usar una enumeración es que cada lugar que verifique el valor de la
enumeración necesitará una matchexpresión o algo similar para gestionar todas las variantes posibles.
Esto podría resultar más repetitivo que esta solución con el objeto de rasgo.

Compensaciones del patrón estatal

Hemos demostrado que Rust es capaz de implementar el patrón de estado


orientado a objetos para encapsular los diferentes comportamientos que una
publicación debe tener en cada estado. Los métodos Postno conocen estos
comportamientos. Según la forma en que organizamos el código, solo tenemos
que buscar en un punto para conocer los diferentes comportamientos de una
publicación: la implementación del State rasgo en la Publishedestructura.
Rust
jueves, 22 de mayo de 2025 : Página 565 de 719

Si creáramos una implementación alternativa que no usara el patrón de estado,


podríamos usar matchexpresiones en los métodos Posto incluso en el maincódigo
que verifica el estado de la publicación y cambia el comportamiento en esos
puntos. Esto significaría que tendríamos que analizar varias opciones para
comprender todas las implicaciones de que una publicación esté en el estado
publicado. Esto solo aumentaría cuantos más estados agregáramos: cada una de
esas matchexpresiones necesitaría un brazo adicional.

Con el patrón de estado, los Postmétodos y los lugares que usamos Postno
necesitan matchexpresiones, y para agregar un nuevo estado, solo necesitaríamos
agregar una nueva estructura e implementar los métodos de rasgos en esa
estructura.

La implementación que utiliza el patrón de estado es fácil de ampliar para añadir


más funcionalidad. Para comprobar lo sencillo que resulta mantener el código que
utiliza el patrón de estado, pruebe algunas de estas sugerencias:

 Agregue un rejectmétodo que cambie el estado de la publicación


de PendingReviewatrás a Draft.
 Se requieren dos llamadas approveantes de que el estado pueda cambiarse
a Published.
 Permite que los usuarios agreguen texto solo cuando una publicación esté
en Draftestado. Consejo: el objeto de estado se encarga de los cambios que
puedan ocurrir en el contenido, pero no de modificar el archivo Post.

Una desventaja del patrón de estado es que, dado que los estados implementan
las transiciones entre estados, algunos están acoplados entre sí. Si añadimos otro
estado entre PendingReviewy Published, como Scheduled, tendríamos que cambiar el
código en PendingReview[transición a] Scheduleden su lugar. Sería menos trabajo
si PendingReviewno fuera necesario cambiar con la adición de un nuevo estado,
pero eso implicaría cambiar a otro patrón de diseño.

Otra desventaja es que hemos duplicado parte de la lógica. Para eliminar la


duplicación, podríamos intentar implementar por defecto
los métodos request_reviewy en el rasgo que devuelve ; sin embargo, esto no sería
compatible con dyn, ya que el rasgo desconoce cuál será exactamente el
concreto. Queremos poder usarlo como un objeto de rasgo, por lo que
necesitamos que sus métodos sean compatibles con dyn. approveStateselfselfState
Rust
jueves, 22 de mayo de 2025 : Página 566 de 719

Otra duplicación incluye las implementaciones similares de los


métodos request_review y approveen Post. Ambos métodos delegan en la
implementación del mismo método en el valor del statecampo de Optiony asignan
el nuevo valor del statecampo al resultado. Si tuviéramos muchos métodos
en Post que siguieran este patrón, podríamos considerar definir una macro para
eliminar la repetición (véase la sección "Macros"). en el Capítulo 20).

Al implementar el patrón de estado exactamente como está definido para


lenguajes orientados a objetos, no aprovechamos al máximo las ventajas de Rust.
Veamos algunos cambios que podemos realizar en el blogcrate y que pueden
provocar errores de compilación en estados y transiciones no válidos.

Codificación de estados y comportamientos como tipos

Te mostraremos cómo replantear el patrón de estados para obtener un conjunto


diferente de compensaciones. En lugar de encapsular completamente los estados
y las transiciones para que el código externo no los conozca, codificaremos los
estados en diferentes tipos. En consecuencia, el sistema de verificación de tipos
de Rust evitará los intentos de usar borradores de publicaciones donde solo se
permiten publicaciones publicadas, generando un error de compilación.

Consideremos la primera parte del mainListado 18-11:

Nombre de archivo: src/main.rs


fn main() {
let mut post = Post::new();

post.add_text("I ate a salad for lunch today");


assert_eq!("", post.content());
}

Seguimos permitiendo la creación de nuevas publicaciones en estado de


borrador Post::new y la posibilidad de añadir texto a su contenido. Pero en lugar de
tener un contentmétodo en una publicación de borrador que devuelva una cadena
vacía, haremos que las publicaciones de borrador no tengan dicho contentmétodo.
De esta forma, si intentamos obtener el contenido de una publicación de
borrador, recibiremos un error del compilador que indica que el método no existe.
Como resultado, será imposible mostrar accidentalmente el contenido de una
publicación de borrador en producción, ya que ese código ni siquiera compilará.
Los listados 18-19 muestran la definición de una Postestructura y
una DraftPostestructura, así como los métodos de cada una:
Rust
jueves, 22 de mayo de 2025 : Página 567 de 719
Nombre de archivo: src/lib.rs
pub struct Post {
content: String,
}

pub struct DraftPost {


content: String,
}

impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}

pub fn content(&self) -> &str {


&self.content
}
}

impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Listado 18-19: A Postcon contentmétodo y DraftPostsin contentmétodo

Tanto las estructuras Postcomo DraftPosttienen un campo privado contentque


almacena el texto de la entrada del blog. Estas estructuras ya no tienen
este statecampo porque estamos trasladando la codificación del estado a los tipos
de las estructuras. La Post estructura representará una entrada publicada y tiene
un contentmétodo que devuelve el content.

Aún tenemos una Post::newfunción, pero en lugar de devolver una instancia


de Post, devuelve una instancia de DraftPost. Como contentes privada y no hay
funciones que devuelvan Post, no es posible crear una instancia de Postahora
mismo.

La DraftPostestructura tiene un add_textmétodo, por lo que podemos agregar


texto contentcomo antes, pero tenga en cuenta que DraftPostno tiene un
método.content método definido. Por lo tanto, ahora el programa garantiza que
todas las publicaciones comiencen como borradores, y que su contenido no esté
disponible para su visualización. Cualquier intento de eludir estas restricciones
generará un error de compilación.

Implementando transiciones como transformaciones en diferentes tipos


Rust
jueves, 22 de mayo de 2025 : Página 568 de 719

¿Cómo publicamos una entrada? Queremos aplicar la regla de que un borrador


debe ser revisado y aprobado antes de publicarse. Una entrada en estado
pendiente de revisión no debería mostrar ningún contenido. Implementemos
estas restricciones añadiendo otra estructura, PendingReviewPost, definiendo
el request_reviewmétodo on DraftPostpara que devuelva a PendingReviewPost, y
definiendo un approvemétodo on PendingReviewPostpara que devuelva a Post, como
se muestra en el Listado 18-20:

Nombre de archivo: src/lib.rs


impl DraftPost {
// --snip--
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
content: self.content,
}
}
}

pub struct PendingReviewPost {


content: String,
}

impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
Listado 18-20: Un PendingReviewPostque se crea al llamar request_reviewa DraftPosty un approvemétodo
que convierte a PendingReviewPosten un publicadoPost

Los métodos request_review`and` toman posesión de `, consumiendo así las


instancias `and` y transformándolas en `a` y `a` publicada` , respectivamente.
De esta forma, no tendremos instancias persistentes después de llamarlas , y así
sucesivamente. La estructura no tiene un método definido, por lo que intentar
leer su contenido genera un error de compilación, como con ` . Dado que la única
manera de obtener una instancia publicada con un método definido es llamarlo en
`a` , y la única manera de obtener `a` es llamarlo en `a` , hemos codificado el
flujo de trabajo de la entrada del blog en el sistema de
tipos.approveselfDraftPostPendingReviewPostPendingReviewPostPostDraftPostrequest_revie
wPendingReviewPostcontentDraftPostPostcontentapprovePendingReviewPostPendingReviewP
ostrequest_reviewDraftPost
Rust
jueves, 22 de mayo de 2025 : Página 569 de 719

Pero también debemos realizar pequeños cambios en main. Los


métodos request_reviewy approvedevuelven nuevas instancias en lugar de modificar
la estructura en la que se invocan, por lo que necesitamos agregar más let post
=asignaciones de shadowing para guardar las instancias devueltas. Tampoco
podemos permitir que las aserciones sobre el contenido de las publicaciones en
borrador y pendientes de revisión sean cadenas vacías, ni las necesitamos: ya no
podemos compilar código que intente usar el contenido de las publicaciones en
esos estados. El código actualizado mainse muestra en el Listado 18-21:

Nombre de archivo: src/main.rs


use blog::Post;

fn main() {
let mut post = Post::new();

post.add_text("I ate a salad for lunch today");

let post = post.request_review();

let post = post.approve();

assert_eq!("I ate a salad for lunch today", post.content());


}
Listado 18-21: Modificaciones para mainutilizar la nueva implementación del flujo de trabajo de
publicaciones del blog

Los cambios que tuvimos que hacer para mainreasignar postsignifican que esta
implementación ya no sigue exactamente el patrón de estado orientado a
objetos: las transformaciones entre los estados ya no están encapsuladas
completamente dentro delPost implementación. Sin embargo, nuestra ventaja es
que los estados inválidos ahora son imposibles gracias al sistema de tipos y a la
comprobación de tipos que se realiza en tiempo de compilación. Esto garantiza
que ciertos errores, como la visualización del contenido de una entrada no
publicada, se detecten antes de que lleguen a producción.

Pruebe las tareas sugeridas al principio de esta sección en el blogcrate tal como
está después del Listado 18-21 para ver qué opina sobre el diseño de esta versión
del código. Tenga en cuenta que algunas tareas podrían ya estar completadas en
este diseño.

Hemos visto que, si bien Rust es capaz de implementar patrones de diseño


orientados a objetos, otros patrones, como la codificación del estado en el
sistema de tipos, también están disponibles en Rust. Estos patrones tienen
Rust
jueves, 22 de mayo de 2025 : Página 570 de 719

diferentes ventajas y desventajas. Aunque quizás estés familiarizado con los


patrones orientados a objetos, replantear el problema para aprovechar las
características de Rust puede ofrecer beneficios, como la prevención de 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 la
propiedad, que los lenguajes orientados a objetos no tienen.

Resumen
Independientemente de si consideras o no que Rust es un lenguaje orientado a
objetos después de leer este capítulo, ahora sabes que puedes usar objetos trait
para obtener algunas características orientadas a objetos en Rust. El envío
dinámico puede dar a tu código cierta flexibilidad a cambio de un poco de
rendimiento en tiempo de ejecución. Puedes usar esta flexibilidad para
implementar patrones orientados a objetos que mejoren la mantenibilidad de tu
código. Rust también tiene otras características, como la propiedad, que los
lenguajes orientados a objetos no tienen. Un patrón orientado a objetos no
siempre será la mejor manera de aprovechar las fortalezas de Rust, pero es una
opción disponible.

A continuación, analizaremos los patrones, otra característica de Rust que ofrece


gran flexibilidad. Los hemos visto brevemente a lo largo del libro, pero aún no
hemos visto todo su potencial. ¡Comencemos!

Patrones y combinaciones
Los patrones son una sintaxis especial en Rust que permite la comparación con la
estructura de tipos, tanto complejos como simples. Usar patrones junto
con match expresiones y otras construcciones proporciona mayor control sobre el
flujo de control de un programa. Un patrón consiste en una combinación de los
siguientes elementos:

 Literales
 Matrices, enumeraciones, estructuras o tuplas desestructuradas
 Variables
 Comodines
 Marcadores de posición
Rust
jueves, 22 de mayo de 2025 : Página 571 de 719

Algunos ejemplos de patrones incluyen x, (a, 3)y Some(Color::Red). En los contextos


en los que los patrones son válidos, estos componentes describen la forma de los
datos. Nuestro programa compara los valores con los patrones para determinar si
la forma de los datos es la correcta para continuar ejecutando un fragmento de
código específico.

Para usar un patrón, lo comparamos con un valor. Si el patrón coincide con el


valor, usamos las partes del valor en nuestro código.
Recordemos...match expresiones del 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 partes con nombre. De lo contrario, el código
asociado con el patrón no se ejecutará.

Este capítulo es una referencia sobre todo lo relacionado con los patrones.
Abordaremos los lugares válidos para su uso, la diferencia entre patrones
refutables e irrefutables, y los diferentes tipos de sintaxis de patrones que podría
encontrar. Al final del capítulo, sabrá cómo usar patrones para expresar diversos
conceptos con claridad.

Todos los lugares donde se pueden utilizar


patrones
Los patrones aparecen en varias partes de Rust, ¡y los has estado usando mucho
sin darte cuenta! Esta sección analiza todos los casos en los que los patrones son
válidos.

matchBrazos

Como se explicó en el Capítulo 6, usamos patrones en los brazos de matchlas


expresiones. Formalmente, matchlas expresiones se definen como la palabra
clave match, un valor con el que se busca coincidencia y uno o más brazos de
coincidencia que consisten en un patrón y una expresión que se ejecuta si el valor
coincide con el patrón de ese brazo, como se muestra a continuación:

match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
Rust
jueves, 22 de mayo de 2025 : Página 572 de 719

Por ejemplo, aquí está la matchexpresión del Listado 6-5 que coincide con
un Option<i32>valor en la variable x:

match x {
None => None,
Some(i) => Some(i + 1),
}

Los patrones en esta matchexpresión son Noney Some(i)a la izquierda de cada


flecha.

Un requisito para matchlas expresiones es que deben ser exhaustivas , es


decir, matchdeben considerarse todas las posibilidades para el valor de la
expresión. Una forma de garantizar que se cubran todas las posibilidades es tener
un patrón general 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 en particular _coincide con cualquier valor, pero nunca se vincula a una
variable, por lo que suele usarse en el último brazo de coincidencia. El _patrón
puede ser útil, por ejemplo, cuando se desea ignorar cualquier valor no
especificado. Abordaremos el _patrón con más detalle en la sección "Ignorar
valores en un patrón" más adelante en este capítulo.

if letExpresiones condicionales

En el capítulo 6, analizamos cómo usar if letexpresiones, principalmente como una


forma abreviada de escribir el equivalente de "a" matchque solo coincide con un
caso. Opcionalmente, if letpuede tener un elsecódigo contenedor correspondiente
para ejecutarse si el patrón en "the" if letno coincide.

El Listado 19-1 muestra que también es posible combinar expresiones if let, else if,
y else if let. Esto nos brinda mayor flexibilidad que una matchexpresión en la que
solo podemos expresar un valor para comparar con los patrones. Además, Rust no
requiere que las condiciones en una serie de brazos if let, else if, else if letestén
relacionadas entre sí.

El código del Listado 19-1 determina el color del fondo según una serie de
comprobaciones de varias condiciones. Para este ejemplo, hemos creado
variables con valores predefinidos que un programa real podría recibir de la
entrada del usuario.
Rust
jueves, 22 de mayo de 2025 : Página 573 de 719
Nombre de archivo: src/main.rs
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();

if let Some(color) = favorite_color {


println!("Using your favorite color, {color}, as the background");
} else if is_tuesday {
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
}
Listado 19-1: Mezcla de if let, else if, else if let, yelse

Si el usuario especifica un color favorito, este se usa como fondo. Si no se


especifica ningún 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
correctamente como un número, el color es morado o naranja, según el valor del
número. Si no se cumple ninguna de estas condiciones, el color de fondo es azul.

Esta estructura condicional nos permite cumplir con requisitos complejos. Con los
valores codificados, este ejemplo imprimirá Using purple as the background color.

Como puede ver, if lettambién se pueden introducir nuevas variables que eclipsan
las existentes, de la misma forma que matcharms: la línea if let Ok(age) =
age introduce una nueva agevariable que contiene el valor dentro de la Okvariante,
eclipsando la agevariable existente. Esto significa que debemos colocar la if age >
30condición dentro de ese bloque: no podemos combinar estas dos condiciones
en [ <sub> if let Ok(age) = age && age > 30...age

La desventaja de usar if letexpresiones es que el compilador no verifica la


exhaustividad, mientras que con matchlas expresiones sí lo hace. Si omitiéramos
el último elsebloque y, por lo tanto, no gestionáramos algunos casos, el
compilador no nos alertaría del posible error lógico.

while letBucles condicionales


Rust
jueves, 22 de mayo de 2025 : Página 574 de 719

Similar en construcción a if let, el while letbucle condicional permite que


un whilebucle se ejecute mientras un patrón siga coincidiendo. Vimos un while
letbucle por primera vez en el Capítulo 17, donde lo usamos para continuar el
bucle mientras un flujo genera nuevos valores. De igual forma, en el Listado 19-2
mostramos un while let bucle que espera los mensajes enviados entre
subprocesos, pero en este caso verificando un Resulten lugar de un Option.

let (tx, rx) = std::sync::mpsc::channel();


std::thread::spawn(move || {
for val in [1, 2, 3] {
tx.send(val).unwrap();
}
});

while let Ok(value) = rx.recv() {


println!("{value}");
}
Listado 19-2: Uso de un while letbucle para imprimir valores mientras rx.recv()retornaOk

Este ejemplo imprime 1, 2 y 3. Como vimos recven el Capítulo 16,


desempaquetamos el error directamente o interactuamos con él como un iterador
mediante un for bucle. Sin embargo, como muestra el Listado 19-2, también
podemos usar while let, ya que el recvmétodo retorna Okmientras el emisor esté
produciendo mensajes y luego produce un Errcuando el emisor se desconecta.

forBucles

En un forbucle, el valor que sigue directamente a la palabra clave fores un patrón.


Por ejemplo, en for x in yel xes el patrón. El Listado 19-3 muestra cómo usar un
patrón en un forbucle para desestructurar, o dividir, una tupla como parte
del forbucle.

let v = vec!['a', 'b', 'c'];

for (index, value) in v.iter().enumerate() {


println!("{value} is at index {index}");
}
Listado 19-3: Uso de un patrón en un forbucle para desestructurar una tupla

El código del Listado 19-3 imprimirá lo siguiente:

$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
Running `target/debug/patterns`
a is at index 0
Rust
jueves, 22 de mayo de 2025 : Página 575 de 719
b is at index 1
c is at index 2

Adaptamos un iterador usando el enumeratemétodo para que produzca un valor y


su índice, ubicado en una tupla. El primer valor producido es la tupla (0, 'a').
Cuando este valor coincide con el patrón (index, value), indexserá 0y valueserá 'a',
imprimiendo la primera línea de la salida.

letDeclaraciones

Antes de este capítulo, solo habíamos hablado explícitamente del uso de patrones
con matchy if let, pero, de hecho, también los hemos usado en otros contextos,
incluso en letsentencias. Por ejemplo, considere esta sencilla asignación de
variable con let:

let x = 5;

Cada vez que has usado una letdeclaración como esta, has estado usando
patrones, ¡aunque quizá no te hayas dado cuenta! De forma más formal,
una letdeclaración se ve así:

let PATTERN = EXPRESSION;

En declaraciones como let x = 5;las que tienen un nombre de variable en


la PATTERNranura, este nombre es simplemente una forma particularmente simple
de un patrón. Rust compara la expresión con el patrón y asigna los nombres que
encuentra. En el let x = 5;ejemplo,x es un patrón que significa "vincular lo que
coincida aquí a la variable x". Dado que el nombre xes el patrón completo, este
patrón significa efectivamente "vincular todo a la variable x, sea cual sea su
valor".

Para ver el aspecto de coincidencia de patrones letcon mayor claridad, considere


el Listado 19-4, que utiliza un patrón con letpara desestructurar una tupla.

let (x, y, z) = (1, 2, 3);


Listado 19-4: Uso de un patrón para desestructurar una tupla y crear tres variables a la vez

Aquí, comparamos una tupla con un patrón. Rust compara el valor (1, 2, 3) con el
patrón (x, y, z)y ve que coincide con él, por lo que Rust se
vincula 1a x, 2paray y 3a z. Puedes imaginar este patrón de tupla como si anidara
tres patrones de variables individuales dentro de él.
Rust
jueves, 22 de mayo de 2025 : Página 576 de 719

Si el número de elementos del patrón no coincide con el de la tupla, el tipo


general no coincidirá y se generará un error de compilación. Por ejemplo, el
Listado 19-5 muestra un intento de desestructurar una tupla con tres elementos
en dos variables, lo cual no funcionará.

let (x, y) = (1, 2, 3);


Listado 19-5: Construcción incorrecta de un patrón cuyas variables no coinciden con el número de
elementos de la tupla

Intentar compilar este código da como resultado este tipo de error:

$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
--> src/main.rs:2:9
|
2| let (x, y) = (1, 2, 3);
| ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
| |
| expected a tuple with 3 elements, found one with 2 elements
|
= note: expected tuple `({integer}, {integer}, {integer})`
found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error

Para corregir el error, podríamos ignorar uno o más de los valores en la tupla
usando _o .., como verá en la sección “Ignorar valores en un patrón”. . Si el
problema radica en que tenemos demasiadas variables en el patrón, la solución
es hacer que los tipos coincidan eliminando variables para que el número de
variables sea igual al número de elementos de la tupla.

Parámetros de función

Los parámetros de función también pueden ser patrones. El código del Listado 19-
6, que declara una función llamada fooque toma un parámetro llamado xde
tipo i32, debería resultar familiar.

fn foo(x: i32) {
// code goes here
}
Listado 19-6: Una firma de función utiliza patrones en los parámetros
Rust
jueves, 22 de mayo de 2025 : Página 577 de 719

¡La xparte es un patrón! Como hicimos con let, podríamos hacer coincidir una
tupla en los argumentos de una función con el patrón. El listado 19-7 divide los
valores de una tupla al pasarla a una función.

Nombre de archivo: src/main.rs


fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({x}, {y})");
}

fn main() {
let point = (3, 5);
print_coordinates(&point);
}
Listado 19-7: Una función con parámetros que desestructura una tupla

Este código imprime Current location: (3, 5). Los valores &(3, 5)coinciden con el
patrón &(x, y), por lo que xes el valor 3y yes el valor 5.

También podemos usar patrones en listas de parámetros de cierre de la misma


manera que en listas de parámetros de función, porque los cierres son similares a
las funciones, como se analiza en el Capítulo 13.

Hasta ahora, has visto varias maneras de usar patrones, pero no funcionan igual
en todos los casos. En algunos casos, los patrones deben ser irrefutables; en otras
circunstancias, pueden serlo. A continuación, analizaremos estos dos conceptos.

Refutabilidad: si un patrón podría no coincidir


Los patrones se presentan en dos formas: refutables e irrefutables. Los patrones
que coinciden con cualquier valor posible pasado son irrefutables . Un ejemplo
sería xla declaración let x = 5;porque xcoincide con cualquier valor y, por lo tanto,
no puede fallar. Los patrones que pueden fallar con algún valor posible
son refutables . Un ejemplo sería Some(x)la expresión if let Some(x) = a_valueporque
si el valor de la a_valuevariable es Noneen lugar de Some, el Some(x)patrón no
coincidirá.

Los parámetros, letsentencias y forbucles de función solo aceptan patrones


irrefutables, ya que el programa no puede hacer nada significativo cuando los
valores no coinciden. Las expresiones " if lety while let" y " let-" .else " aceptan
patrones refutables e irrefutables, pero el compilador advierte sobre los patrones
irrefutables porque, por definición, están diseñados para gestionar posibles fallos:
Rust
jueves, 22 de mayo de 2025 : Página 578 de 719

la funcionalidad de un condicional reside en su capacidad de funcionar de forma


diferente según el éxito o el fracaso.

En general, no deberías preocuparte por la distinción entre patrones refutables e


irrefutables; sin embargo, es necesario familiarizarse con el concepto de
refutabilidad para poder responder cuando lo veas en un mensaje de error. En
esos casos, tendrás que cambiar el patrón o la construcción con la que lo uses,
según el comportamiento previsto del código.

Veamos un ejemplo de lo que sucede al intentar usar un patrón refutable donde


Rust requiere un patrón irrefutable y viceversa. El Listado 19-8 muestra
una letsentencia, pero para el patrón que hemos especificado Some(x), un patrón
refutable. Como era de esperar, este código no compilará.

let Some(x) = some_option_value;


Listado 19-8: Intentar utilizar un patrón refutable conlet

Si some_option_valuefuera un Nonevalor, no coincidiría con el patrón Some(x), lo que


significa que el patrón es refutable. Sin embargo, la letsentencia solo puede
aceptar un patrón irrefutable porque el código no puede hacer nada válido con
un Nonevalor. En tiempo de compilación, Rust se quejará de que hemos intentado
usar un patrón refutable cuando se requiere uno irrefutable:

$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
--> src/main.rs:3:9
|
3| let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only
one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
|
3| let Some(x) = some_option_value else { todo!() };
| ++++++++++++++++

For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Rust
jueves, 22 de mayo de 2025 : Página 579 de 719

Como no cubrimos (¡y no pudimos cubrir!) todos los valores válidos con el
patrón Some(x), Rust produce legítimamente un error de compilación.

Si tenemos un patrón refutable que requiere un patrón irrefutable, podemos


corregirlo modificando el código que lo usa: en lugar de usar let, podemos usar if
let. Si el patrón no coincide, el código simplemente omitirá el código entre llaves,
lo que le permitirá continuar de forma válida. El Listado 19-9 muestra cómo
corregir el código del Listado 19-8.

if let Some(x) = some_option_value {


println!("{x}");
}
Listado 19-9: Uso if letde un bloque con patrones refutables en lugar delet

¡Le hemos dado una salida al código! Este código ahora es perfectamente válido.
Sin embargo, si damos if letun patrón irrefutable (un patrón que siempre
coincidirá), como xse muestra en el Listado 19-10, el compilador emitirá una
advertencia.

if let x = 5 {
println!("{x}");
};
Listado 19-10: Intentar utilizar un patrón irrefutable conif let

Rust se queja de que no tiene sentido utilizar if let con un patrón irrefutable:

$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `if let` pattern
--> src/main.rs:2:8
|
2| if let x = 5 {
| ^^^^^^^^^
|
= note: this pattern will always match, so the `if let` is useless
= help: consider replacing the `if let` with a `let`
= note: `#[warn(irrefutable_let_patterns)]` on by default

warning: `patterns` (bin "patterns") generated 1 warning


Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
Running `target/debug/patterns`
5

Por esta razón, los brazos de coincidencia deben usar patrones refutables,
excepto el último brazo, que debe coincidir con los valores restantes con un
patrón irrefutable. Rust permite usar un patrón irrefutable en una
Rust
jueves, 22 de mayo de 2025 : Página 580 de 719

instrucción matchcon un solo brazo, pero esta sintaxis no es particularmente útil y


podría reemplazarse con una letsentencia más simple.

Ahora que sabes dónde usar patrones y la diferencia entre patrones refutables e
irrefutables, cubramos toda la sintaxis que podemos usar para crear patrones.

Sintaxis de patrones
En esta sección, reunimos toda la sintaxis válida en patrones y analizamos por
qué y cuándo es posible que desees utilizar cada uno.

Literales coincidentes

Como se vio en el Capítulo 6, se pueden comparar patrones con literales


directamente. El siguiente código muestra algunos ejemplos:

let x = 1;

match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}

Este código se imprime oneporque el valor enx es 1. Esta sintaxis es útil cuando
desea que su código realice una acción si obtiene un valor concreto particular.

Coincidencia de variables con nombre

Las variables con nombre son patrones irrefutables que coinciden con cualquier
valor, y las hemos usado muchas veces en este libro. Sin embargo, existe una
complicación al usar variables con nombre en expresiones match, if leto while let.
Dado que cada uno de estos tipos de expresión inicia un nuevo ámbito, las
variables declaradas como parte de un patrón dentro de la expresión sombrearán
a las que tengan el mismo nombre en el exterior, como ocurre con todas las
variables. En el Listado 19-11, declaramos una variable con nombre xy Some(5)una
variable ycon el valor 10. Luego, creamos una matchexpresión con el valor x.
Observe los patrones en los brazos de coincidencia y println!al final, e intente
averiguar qué imprimirá el código antes de ejecutarlo o seguir leyendo.

Nombre de archivo: src/main.rs


let x = Some(5);
Rust
jueves, 22 de mayo de 2025 : Página 581 de 719
let y = 10;

match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {x:?}"),
}

println!("at the end: x = {x:?}, y = {y}");


Listado 19-11: Una matchexpresión con un brazo que introduce una nueva variable que sombrea una
variable existentey

Analicemos lo que sucede cuando matchse ejecuta la expresión. El patrón del


primer brazo coincidente no coincide con el valor definido de x , por lo que el
código continúa.

El patrón en el segundo brazo de coincidencia introduce una nueva variable


llamada yque coincidirá con cualquier valor dentro de un Somevalor. Dado que
estamos en un nuevo ámbito dentro de la matchexpresión, esta es una
nueva yvariable, no la que ydeclaramos inicialmente con el valor 10. Esta
nueva yvinculación coincidirá con cualquier valor dentro de un Some, que es lo
que tenemos en x. Por lo tanto, este nuevo yse vincula al valor interno
de Somein x. Ese valor es 5, por lo que la expresión para ese brazo se ejecuta e
imprime Matched, y = 5.

Si xhubiera sido un Nonevalor en lugar de Some(5), los patrones de los dos


primeros brazos no habrían coincidido, por lo que el valor habría coincidido con el
guion bajo. No introdujimos la xvariable en el patrón del brazo del guion bajo, por
lo que xen la expresión sigue siendo el exterior xsin sombrear. En este caso
hipotético, matchse imprimiría Default case, x = None.

Cuando la matchexpresión termina, su alcance finaliza, al igual que el del


interno y. El último println!produce at the end: x = Some(5), y = 10.

Para crear una matchexpresión que compare los valores de los valores
externos xy y, en lugar de introducir una nueva variable que reemplace la
existente y , necesitaríamos usar una condición de protección de coincidencia.
Hablaremos de las protecciones de coincidencia más adelante, en la
sección "Condicionales adicionales con protecciones de coincidencia" .

Patrones múltiples
Rust
jueves, 22 de mayo de 2025 : Página 582 de 719

Se pueden hacer coincidir varios patrones mediante la |sintaxis del operador


patrón o . Por ejemplo, en el siguiente código, comparamos el valor de xcon el
valor de brazos coincidentes, el primero de los cuales tiene una opción `` ` . Esto
significa que si el valor de `` xcoincide con alguno de los valores de ese brazo, se
ejecutará el código de ese brazo:

let x = 1;

match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}

Este código imprime one or two.

Coincidencia de rangos de valores con..=

La ..=sintaxis nos permite hacer coincidir un rango inclusivo de valores. En el


siguiente código, cuando un patrón coincide con cualquiera de los valores dentro
del rango dado, ese brazo se ejecutará:

let x = 5;

match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}

Si xes 1, 2, 3, 4 o 5, el primer brazo coincidirá. Esta sintaxis es más conveniente


para múltiples valores de coincidencia que usar el |operador para expresar la
misma idea; si lo usáramos, |tendríamos que especificar 1 | 2 | 3 | 4 | 5. Especificar
un rango es mucho más corto, especialmente si queremos coincidir, por ejemplo,
con cualquier número entre 1 y 1000.

El compilador verifica que el rango no esté vacío en el momento de la compilación


y, debido a que los únicos tipos para los cuales Rust puede determinar si un rango
está vacío o no son charlos valores numéricos, solo se permiten rangos
con charvalores numéricos o .

A continuación se muestra un ejemplo que utiliza rangos de charvalores:

let x = 'c';

match x {
Rust
jueves, 22 de mayo de 2025 : Página 583 de 719
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}

El óxido puede decir que 'c'está dentro del rango del primer patrón y se
imprime early ASCII letter.

Desestructurar para romper valores

También podemos usar patrones para desestructurar estructuras, enumeraciones


y tuplas y usar diferentes partes de estos valores. Analicemos cada valor.

Desestructuración de estructuras

El listado 19-12 muestra una Pointestructura con dos campos, xy y, que podemos
dividir usando un patrón con una letdeclaración.

Nombre de archivo: 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);
}
Listado 19-12: Desestructuración de los campos de una estructura en variables separadas

Este código crea las variables ay bque coinciden con los valores de los campos x y
de la estructura. Este ejemplo muestra que los nombres de las variables en el
patrón no tienen que coincidir con los nombres de los campos de la estructura.
Sin embargo, es común hacer coincidir los nombres de las variables con los
nombres de los campos para facilitar la memorización de las variables. Debido a
este uso común y a que la escritura conlleva mucha duplicación, Rust tiene una
abreviatura para los patrones que coinciden con los campos de la estructura: solo
se necesita indicar el nombre del campo de la estructura, y las variables creadas
a partir del patrón tendrán los mismos nombres. El Listado 19-13 se comporta de
la misma manera que el código del Listado 19-12, pero las variables creadas en
el patrón son y en lugar de y .yplet Point { x: x, y: y } = p;letxyab
Rust
jueves, 22 de mayo de 2025 : Página 584 de 719
Nombre de archivo: src/main.rs
struct Point {
x: i32,
y: i32,
}

fn main() {
let p = Point { x: 0, y: 7 };

let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
Listado 19-13: Desestructuración de campos de estructura utilizando la abreviatura de campo de
estructura

Este código crea las variables xy yque coinciden con los campos xy de la variable.
El resultado es que las variables y contienen los valores de la estructura.ypxyp

También podemos desestructurar con valores literales como parte del patrón de
estructura, en lugar de crear variables para todos los campos. Esto nos permite
probar algunos campos para valores específicos mientras creamos variables para
desestructurar los demás.

En el Listado 19-14, tenemos una matchexpresión que separa Pointlos valores en


tres casos: puntos que se encuentran directamente sobre el xeje (lo cual es
verdadero cuando y = 0), sobre el yeje ( x = 0) o ninguno.

Nombre de archivo: 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})");
}
}
}
Listado 19-14: Desestructuración y coincidencia de valores literales en un patrón

El primer brazo coincidirá con cualquier punto que se encuentre en el xeje,


especificando que el ycampo coincide si su valor coincide con el literal 0. El patrón
crea una xvariable que podemos usar en el código para este brazo.
Rust
jueves, 22 de mayo de 2025 : Página 585 de 719

De forma similar, el segundo brazo coincide con cualquier punto del yeje
especificando que el xcampo coincide si su valor es [ ] 0y crea una variable ypara
dicho valor y. El tercer brazo no especifica ningún literal, por lo que coincide con
cualquier otro Pointy crea variables para los campos [ ] xy [ y].

En este ejemplo, el valor pcoincide con el segundo brazo en virtud de x contener


un 0, por lo que este código imprimirá On the y axis at 7.

Recuerde que una matchexpresión deja de verificar los brazos una vez que ha
encontrado el primer patrón coincidente, por lo que, aunque Point { x: 0, y: 0}esté
en el xeje y el yeje, este código solo se imprimiráOn the x axis at 0 .

Desestructuración de enumeraciones

Hemos desestructurado enumeraciones en este libro (por ejemplo, en el Listado


6-5 del Capítulo 6), pero aún no hemos explicado explícitamente que el patrón
para desestructurar una enumeración se corresponde con la forma en que se
definen los datos almacenados en ella. Por ejemplo, en el Listado 19-15 usamos
la Messageenumeración del Listado 6-2 y escribimos una matchcon patrones que
desestructurarán cada valor interno.

Nombre de archivo: 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}");
}
}
}
Rust
jueves, 22 de mayo de 2025 : Página 586 de 719
Listado 19-15: Desestructuración de variantes de enumeración que contienen diferentes tipos de valores

Este código se imprimirá Change the color to red 0, green 160, and blue 255 . Intenta
cambiar el valor de msgpara ver el código de la ejecución de otros brazos.

Para variantes de enumeración sin datos, como [nombre de la


variable] Message::Quit, no podemos desestructurar más el valor. Solo podemos
coincidir con el Message::Quitvalor literal, y ninguna variable sigue ese patrón.

Para variantes de enumeración similares a estructuras, como Message::Move,


podemos usar un patrón similar al que especificamos para encontrar estructuras.
Después del nombre de la variante, colocamos llaves y enumeramos los campos
con variables para descomponer las partes que se usarán en el código de este
brazo. Aquí usamos la forma abreviada, como en el Listado 19-13.

Para variantes de enumeración similares a tuplas, como Message::Writela que


contiene una tupla con un elemento y Message::ChangeColorla que contiene una
tupla con tres elementos, el patrón es similar al que especificamos para la
coincidencia de tuplas. El número de variables en el patrón debe coincidir con el
número de elementos en la variante que buscamos.

Desestructuración de estructuras anidadas y enumeraciones

Hasta ahora, todos nuestros ejemplos han sido coincidencias de estructuras o


enumeraciones de un nivel de profundidad, pero la coincidencia también puede
funcionar con elementos anidados. Por ejemplo, podemos refactorizar el código
del Listado 19-15 para que admita colores RGB y HSV en el ChangeColor mensaje,
como se muestra en el Listado 19-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));
Rust
jueves, 22 de mayo de 2025 : Página 587 de 719
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}");
}
_ => (),
}
}
Listado 19-16: Coincidencia en enumeraciones anidadas

El patrón del primer brazo de la matchexpresión coincide con


una Message::ChangeColorvariante de enumeración que contiene
una Color::Rgbvariante; luego, el patrón se vincula a los tres i32valores internos. El
patrón del segundo brazo también coincide con una Message::ChangeColorvariante
de enumeración, pero coincide con la enumeración interna Color::Hsv. Podemos
especificar estas condiciones complejas en una matchexpresión, aunque se
utilicen dos enumeraciones.

Desestructuración de estructuras y tuplas

Podemos combinar, combinar y anidar patrones de desestructuración de maneras


aún más complejas. El siguiente ejemplo muestra una desestructuración compleja
donde anidamos estructuras y tuplas dentro de una tupla y desestructuramos
todos los valores primitivos:

let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });

Este código nos permite dividir tipos complejos en sus partes componentes para
que podamos usar los valores que nos interesan por separado.

La desestructuración con patrones es una forma conveniente de utilizar


fragmentos de valores, como el valor de cada campo en una estructura, por
separado unos de otros.

Ignorar valores en un patrón

Has visto que a veces es útil ignorar valores en un patrón, como en el último
brazo de un match, para obtener un catchall que no hace nada, pero que sí tiene
en cuenta todos los valores posibles restantes. Hay varias maneras de ignorar
valores completos o partes de valores en un patrón: usando el _ patrón (como ya
has visto), usando el _patrón dentro de otro patrón, usando un nombre que
Rust
jueves, 22 de mayo de 2025 : Página 588 de 719

empiece por un guion bajo o usando.. para ignorar las partes restantes de un
valor. Exploremos cómo y por qué usar cada uno de estos patrones.

Ignorar un valor entero con_

Hemos usado el guión bajo como patrón comodín que coincide con cualquier
valor, pero no se vincula a él. Esto es especialmente útil como último brazo de
una match expresión, pero también podemos usarlo en cualquier patrón,
incluyendo parámetros de función, como se muestra en el Listado 19-17.

Nombre de archivo: src/main.rs


fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}

fn main() {
foo(3, 4);
}
Listado 19-17: Uso _en una firma de función

Este código ignorará por completo el valor 3pasado como primer argumento e
imprimirá This code only uses the y parameter: 4.

En la mayoría de los casos, cuando ya no se necesita un parámetro de función en


particular, se cambia la firma para que no incluya el parámetro no utilizado.
Ignorar un parámetro de función puede ser especialmente útil, por ejemplo, al
implementar un rasgo que requiere una firma de tipo específica, pero el cuerpo
de la función en la implementación no necesita uno de los parámetros. De esta
forma, se evita recibir una advertencia del compilador sobre parámetros de
función no utilizados, como ocurriría si se usara un nombre en su lugar.

Ignorar partes de un valor con una función anidada_

También podemos usar _dentro de otro patrón para ignorar solo una parte de un
valor, por ejemplo, cuando queremos probar solo una parte de un valor, pero no
necesitamos las demás partes en el código correspondiente que queremos
ejecutar. El Listado 19-18 muestra el código responsable de gestionar el valor de
una configuración. Los requisitos de negocio establecen que el usuario no debe
poder sobrescribir una personalización existente de una configuración, pero
puede deshabilitarla y asignarle un valor si no está configurada.

let mut setting_value = Some(5);


let new_setting_value = Some(10);
Rust
jueves, 22 de mayo de 2025 : Página 589 de 719

match (setting_value, new_setting_value) {


(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}

println!("setting is {setting_value:?}");
Listado 19-18: Uso de un guión bajo dentro de patrones que coinciden Somecon variantes cuando no
necesitamos usar el valor dentro delSome

Este código imprimirá Can't overwrite an existing customized valuey luego setting is
Some(5). En el primer brazo de coincidencia, no necesitamos hacer coincidir ni
usar los valores dentro de ninguna de Somelas variantes, pero sí debemos
comprobar si setting_valuey new_setting_valueson la Somevariante. En ese caso,
imprimimos el motivo por el cual no se cambia setting_value, y no se modifica.

En todos los demás casos (si setting_valueo new_setting_valueson None) expresados


por el _patrón en el segundo brazo, queremos permitir new_setting_valueque se
conviertan en setting_value.

También podemos usar guiones bajos en varios lugares dentro de un patrón para
ignorar valores específicos. El Listado 19-19 muestra un ejemplo de cómo ignorar
el segundo y el cuarto valor en una tupla de cinco elementos.

let numbers = (2, 4, 8, 16, 32);

match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}")
}
}
Listado 19-19: Ignorar varias partes de una tupla

Este código imprimirá Some numbers: 2, 8, 32y los valores 4 y 16 serán ignorados.

Ignorar una variable no utilizada al comenzar su nombre con _

Si creas una variable pero no la usas, Rust suele emitir una advertencia, ya que
una variable sin usar podría ser un error. Sin embargo, a veces es útil crear una
variable que aún no usarás, como al crear prototipos o al iniciar un proyecto. En
este caso, puedes indicar a Rust que no te advierta sobre la variable sin usar
Rust
jueves, 22 de mayo de 2025 : Página 590 de 719

comenzando su nombre con un guion bajo. En el Listado 19-20, creamos dos


variables sin usar, pero al compilar este código, solo deberíamos recibir una
advertencia sobre una de ellas.

Nombre de archivo: src/main.rs


fn main() {
let _x = 5;
let y = 10;
}
Listado 19-20: Cómo comenzar el nombre de una variable con un guión bajo para evitar recibir
advertencias de variables no utilizadas

Aquí recibimos una advertencia sobre no utilizar la variable y, pero no recibimos


una advertencia sobre no utilizar _x.

Tenga en cuenta que existe una sutil diferencia entre usar solo _y usar un nombre
que empieza con un guion bajo. La sintaxis _xsigue vinculando el valor a la
variable, mientras que _no lo hace en absoluto. Para ilustrar un caso donde esta
distinción es importante, el Listado 19-21 nos mostrará un error.

let s = Some(String::from("Hello!"));

if let Some(_s) = s {
println!("found a string");
}

println!("{s:?}");
Listado 19-21: Una variable no utilizada que comienza con un guión bajo aún vincula el valor, que
podría tomar posesión del valor.

Recibiremos un error porque el svalor se moverá a _s, lo que nos impide


usarlo sde nuevo. Sin embargo, usar el guion bajo por sí solo nunca se vincula al
valor. El listado 19-22 se compilará sin errores porque sno se mueve a _.

let s = Some(String::from("Hello!"));

if let Some(_) = s {
println!("found a string");
}

println!("{s:?}");
Listado 19-22: El uso de un guión bajo no vincula el valor

Este código funciona bien porque nunca nos vinculamos sa nada; no se mueve.
Rust
jueves, 22 de mayo de 2025 : Página 591 de 719
Ignorar las partes restantes de un valor con..

Con valores con muchas partes, podemos usar la ..sintaxis para usar partes
específicas e ignorar el resto, evitando así la necesidad de incluir guiones bajos
para cada valor ignorado. El ..patrón ignora cualquier parte de un valor que no
hayamos encontrado explícitamente en el resto del patrón. En el Listado 19-23,
tenemos una Pointestructura que contiene una coordenada en un espacio
tridimensional. En la matchexpresión, queremos operar solo sobre la xcoordenada
e ignorar los valores de los campos ` y`y`` z.

struct Point {
x: i32,
y: i32,
z: i32,
}

let origin = Point { x: 0, y: 0, z: 0 };

match origin {
Point { x, .. } => println!("x is {x}"),
}
Listado 19-23: Ignorar todos los campos de a Pointexcepto xmediante el uso..

Enumeramos el xvalor y luego incluimos el ..patrón. Esto es más rápido que tener
que enumerar y: _y z: _, sobre todo cuando trabajamos con estructuras con
muchos campos en situaciones donde solo uno o dos son relevantes.

La sintaxis ..se expandirá a tantos valores como sea necesario. El listado 19-24
muestra cómo usarla ..con una tupla.

Nombre de archivo: src/main.rs


fn main() {
let numbers = (2, 4, 8, 16, 32);

match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
Listado 19-24: Coincidencia solo del primer y último valor de una tupla e ignoración de todos los
demás valores

En este código, el primer y el último valor se comparan con first"y" last. ..Coincidirá
e ignorará todo lo intermedio.
Rust
jueves, 22 de mayo de 2025 : Página 592 de 719

Sin embargo, el uso ..debe ser inequívoco. Si no está claro qué valores deben
coincidir y cuáles deben ignorarse, Rust generará un error. El Listado 19-25
muestra un ejemplo de uso ..ambiguo, por lo que no compilará.

Nombre de archivo: src/main.rs

fn main() {
let numbers = (2, 4, 8, 16, 32);

match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
Listado 19-25: Un intento de uso ..de forma ambigua

Cuando compilamos este ejemplo, obtenemos este error:

$ 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 de la tupla ignorar antes de


hacer coincidir un valor con secondy cuántos valores adicionales ignorar
posteriormente. Este código podría indicar que queremos ignorar `` 2,
enlazar seconda 4`` y luego ignorar 8``, 16`` y ` 32`;`, o que queremos ignorar
`` 2y `` 4, enlazar seconda 8`` y luego ignorar 16`` y 32``;`, y así sucesivamente.
El nombre de la variable secondno tiene ningún significado especial para Rust, por
lo que obtenemos un error de compilación porque usar `` ..en dos lugares como
este es ambiguo.

Condicionales adicionales con guardias de partido

Una protección de coincidencia es una condición adicional if, especificada después


del patrón en un matchbrazo, que también debe coincidir para que dicho brazo sea
seleccionado. Las protecciones de coincidencia son útiles para expresar ideas más
Rust
jueves, 22 de mayo de 2025 : Página 593 de 719

complejas que las que permite un patrón por sí solo. Solo están disponibles
en matchexpresiones, no en expresiones if leto .while let

La condición puede usar variables creadas en el patrón. El Listado 19-26 muestra


un ejemplo matchdonde el primer brazo tiene el patrón Some(x)y también una
protección de coincidencia if x % 2 == 0(lo cual será verdadero si el número es
par).

let num = Some(4);

match num {
Some(x) if x % 2 == 0 => println!("The number {x} is even"),
Some(x) => println!("The number {x} is odd"),
None => (),
}
Listado 19-26: Cómo añadir un protector de fósforo a un patrón

Este ejemplo imprimirá The number 4 is even. Al numcompararlo con el patrón del
primer brazo, coincide, ya que Some(4)coincide con Some(x). Luego, el protector de
coincidencias comprueba si el resto de la división xentre 2 es igual a 0 y, como lo
es, se selecciona el primer brazo.

Si numhubiera sido Some(5)así, la protección del partido en el primer brazo habría


sido falsa porque el resto de 5 dividido por 2 es 1, que no es igual a 0. Rust luego
iría al segundo brazo, que coincidiría porque el segundo brazo no tiene protección
de partido y, por lo tanto, coincide con cualquier Somevariante.

No es posible expresar la if x % 2 == 0condición dentro de un patrón, por lo que la


protección de coincidencia nos permite expresar esta lógica. La desventaja de
esta expresividad adicional es que el compilador no intenta verificar la
exhaustividad cuando se utilizan expresiones de protección de coincidencia.

En el Listado 19-11, mencionamos que podíamos usar protecciones de


coincidencia para resolver nuestro problema de sombreado de patrones.
Recordemos que creamos una nueva variable dentro del patrón en
la matchexpresión, en lugar de usar la variable fuera de [nombre del
patrón] match. Esta nueva variable impedía realizar pruebas con el valor de la
variable externa. El Listado 19-27 muestra cómo podemos usar una protección de
coincidencia para solucionar este problema.

Nombre de archivo: src/main.rs


fn main() {
Rust
jueves, 22 de mayo de 2025 : Página 594 de 719
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:?}"),
}

println!("at the end: x = {x:?}, y = {y}");


}
Listado 19-27: Uso de un protector de coincidencia para probar la igualdad con una variable externa

Este código ahora imprimirá Default case, x = Some(5). El patrón en el segundo


brazo de coincidencia no introduce una nueva variable yque sombree la externa y,
lo que significa que podemos usar la externa yen la protección de coincidencia. En
lugar de especificar el patrón como Some(y), que habría sombreado la externa y,
especificamos Some(n). Esto crea una nueva variable nque no sombrea nada
porque no hay ninguna nvariable fuera de match.

La protección de coincidencia if n == yno es un patrón y, por lo tanto, no introduce


nuevas variables. Esta y es la variable externa y, no una nueva yvariable que la
sombrea, y podemos buscar un valor que coincida con la variable
externa ycomparándola ncon y.

También puede usar el operador `or`| en una protección de coincidencia para


especificar varios patrones; la condición de protección de coincidencia se aplicará
a todos los patrones. El Listado 19-28 muestra la precedencia al combinar un
patrón que usa |con una protección de coincidencia. Lo importante de este
ejemplo es que la if yprotección de coincidencia se aplica a 4`, 5` y 6 `, aunque
parezca que if ysolo se aplica a 6`.`.

let x = 4;
let y = false;

match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
Listado 19-28: Combinación de múltiples patrones con un protector de fósforo

La condición de coincidencia establece que el brazo solo coincide si el valor


de xes igual a 4, 5o 6 y si yes true. Al ejecutar este código, el patrón del primer
brazo coincide porque xes 4, pero la protección de coincidencia if y es falsa, por lo
que no se elige el primer brazo. El código pasa al segundo brazo, que sí coincide,
Rust
jueves, 22 de mayo de 2025 : Página 595 de 719

y este programa imprime no. Esto se debe a que la if condición se aplica a todo el
patrón 4 | 5 | 6, no solo al último valor 6. En otras palabras, la precedencia de una
protección de coincidencia con respecto a un patrón se comporta así:

(4 | 5 | 6) if y => ...

En lugar de esto:

4 | 5 | (6 if y) => ...

Después de ejecutar el código, el comportamiento de precedencia es evidente: si


la protección de coincidencia se aplicara solo al valor final en la lista de valores
especificados usando el |operador, el brazo habría coincidido y el programa habría
impreso yes.

@Encuadernaciones

El operador `at`@ nos permite crear una variable que contiene un valor al mismo
tiempo que probamos dicho valor para una coincidencia de patrón. En el Listado
19-29, queremos comprobar que un Message::Hello idcampo esté dentro del
rango 3..=7. También queremos vincular el valor a la variable id_variablepara poder
usarlo en el código asociado con el brazo. Podríamos nombrar esta variable idigual
que el campo, pero para este ejemplo usaremos un nombre diferente.

enum Message {
Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
Message::Hello {
id: id_variable @ 3..=7,
} => println!("Found an id in range: {id_variable}"),
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
Listado 19-29: Uso @para enlazar a un valor en un patrón mientras también se prueba

Este ejemplo imprimirá Found an id in range: 5. Al especificar id_variable @antes del


rango 3..=7, capturamos el valor que coincidió con el rango y, al mismo tiempo,
comprobamos que coincidió con el patrón del rango.
Rust
jueves, 22 de mayo de 2025 : Página 596 de 719

En el segundo brazo, donde solo tenemos un rango especificado en el patrón, el


código asociado al brazo no tiene una variable que contenga el valor real
del idcampo. El idvalor del campo podría haber sido 10, 11 o 12, pero el código
que acompaña a ese patrón no sabe cuál es. El código del patrón no puede usar el
valor del idcampo porque no lo hemos guardado iden una variable.

En el último brazo, donde especificamos una variable sin rango, el valor está
disponible para su uso en el código del brazo en una variable llamada id. Esto se
debe a que usamos la sintaxis abreviada de campo de estructura. Sin embargo,
no aplicamos ninguna prueba al valor del idcampo en este brazo, como hicimos
con los dos primeros: cualquier valor coincidiría con este patrón.

El uso @nos permite probar un valor y guardarlo en una variable dentro de un


patrón.

Resumen
Los patrones de Rust son muy útiles para distinguir entre diferentes tipos de
datos. Al usarlos en matchexpresiones, Rust garantiza que los patrones cubran
todos los valores posibles; de lo contrario, el programa no compilará. Los patrones
en letsentencias y parámetros de funciones hacen que estas construcciones sean
más útiles, permitiendo la desestructuración de valores en partes más pequeñas
al mismo tiempo que se asignan a variables. Podemos crear patrones simples o
complejos según nuestras necesidades.

A continuación, en el penúltimo capítulo del libro, veremos algunos aspectos


avanzados de una variedad de características de Rust.

Funciones avanzadas
Ya has aprendido las partes más comunes del lenguaje de programación Rust.
Antes de continuar con otro proyecto en el Capítulo 21, analizaremos algunos
aspectos del lenguaje que podrías encontrarte de vez en cuando, pero que quizás
no uses a diario. Puedes usar este capítulo como referencia cuando te encuentres
con alguna incógnita. Las funciones que se tratan aquí son útiles en situaciones
muy específicas. Aunque quizás no las uses a menudo, queremos asegurarnos de
que domines todas las funciones que Rust ofrece.

En este capítulo cubriremos:


Rust
jueves, 22 de mayo de 2025 : Página 597 de 719

 Rust inseguro: cómo renunciar a algunas de las garantías de Rust y asumir


la responsabilidad de cumplirlas manualmente
 Rasgos avanzados: tipos asociados, parámetros de tipo predeterminados,
sintaxis totalmente calificada, superrasgos y el patrón newtype en relación
con los rasgos
 Tipos avanzados: más información sobre el patrón newtype, los alias de
tipos, el tipo never y los tipos de tamaño dinámico
 Funciones y cierres avanzados: punteros de función y cierres de retorno
 Macros: formas de definir código que define más código en tiempo de
compilación

¡Es una gran variedad de funciones de Rust para todos los gustos! ¡Vamos a
profundizar!

Óxido peligroso
Todo el código que hemos analizado hasta ahora tenía las garantías de seguridad
de memoria de Rust implementadas en tiempo de compilación. Sin embargo, Rust
tiene un segundo lenguaje oculto que no las implementa: se llama Rust no
seguro y funciona igual que el Rust normal, pero con superpoderes adicionales.

Rust inseguro existe porque, por naturaleza, el análisis estático es conservador.


Cuando el compilador intenta determinar si el código cumple con las garantías, es
mejor rechazar algunos programas válidos que aceptar algunos inválidos. Aunque
el código sea correcto, si el compilador de Rust no tiene suficiente información
para confiar, lo rechazará. En estos casos, puedes usar código inseguro para
decirle al compilador: "Confía en mí, sé lo que hago". Sin embargo, ten en cuenta
que usar Rust inseguro es bajo tu propio riesgo: si lo usas incorrectamente,
pueden surgir problemas debido a la inseguridad de la memoria, como la
desreferenciación de punteros nulos.

Otra razón por la que Rust tiene un alter ego inseguro es que el hardware
subyacente es inherentemente inseguro. Si Rust no permitiera realizar
operaciones inseguras, no se podrían realizar ciertas tareas. Rust necesita
permitir la programación de sistemas de bajo nivel, como interactuar
directamente con el sistema operativo o incluso escribir el propio. Trabajar con
programación de sistemas de bajo nivel es uno de los objetivos del lenguaje.
Exploremos qué podemos hacer con Rust inseguro y cómo hacerlo.
Rust
jueves, 22 de mayo de 2025 : Página 598 de 719

Superpotencias inseguras

Para cambiar a Rust inseguro, usa la unsafepalabra clave y luego crea un nuevo
bloque que contenga el código inseguro. Puedes realizar cinco acciones en Rust
inseguro que no puedes realizar en Rust seguro, llamadas superpoderes
inseguros . Estos superpoderes incluyen la capacidad de:

 Desreferenciar un puntero sin formato


 Llamar a una función o método inseguro
 Acceder o modificar una variable estática mutable
 Implementar un rasgo inseguro
 Campos de acceso de ununion

Es importante entender que esto unsafeno desactiva el verificador de préstamos ni


deshabilita ninguna otra comprobación de seguridad de Rust: si usas una
referencia en código inseguro, esta se comprobará igualmente. La unsafepalabra
clave solo te da acceso a estas cinco funciones, que el compilador no comprueba
para la seguridad de la memoria. Aun así, obtendrás cierto grado de seguridad
dentro de un bloque inseguro.

Además, unsafeno significa que el código dentro del bloque sea necesariamente
peligroso o que definitivamente tendrá problemas de seguridad de memoria: la
intención es que, como programador, te asegures de que el código dentro de un
bloque sea seguro.unsafe bloque accederá a la memoria de manera válida.

Las personas son falibles y se cometen errores, pero al exigir que estas cinco
operaciones inseguras se realicen dentro de bloques anotados, unsafesabrá que
cualquier error relacionado con la seguridad de la memoria debe estar dentro de
un unsafebloque. Mantenga unsafebloques pequeños; lo agradecerá más adelante
cuando investigue errores de memoria.

Para aislar el código inseguro al máximo, es recomendable encerrarlo en una


abstracción segura y proporcionar una API segura, lo cual abordaremos más
adelante en este capítulo, al examinar las funciones y métodos inseguros. Partes
de la biblioteca estándar se implementan como abstracciones seguras sobre
código inseguro auditado. Encapsular el código inseguro en una abstracción
segura evita que los usos unsafe se filtren a todos los lugares donde usted o sus
usuarios podrían querer usar la funcionalidad implementada con unsafecódigo, ya
que usar una abstracción segura es seguro.
Rust
jueves, 22 de mayo de 2025 : Página 599 de 719

Analicemos cada uno de los cinco superpoderes inseguros. También veremos


algunas abstracciones que proporcionan una interfaz segura para el código
inseguro.

Desreferenciar un puntero sin formato

En el Capítulo 4, en la sección "Referencias Colgantes" , mencionamos que el


compilador garantiza que las referencias sean siempre válidas. Rust no seguro
cuenta con dos nuevos tipos llamados punteros sin formato , similares a las
referencias. Al igual que con las referencias, los punteros sin formato pueden ser
inmutables o mutables y se escriben como *const Ty *mut T, respectivamente. El
asterisco no es el operador de desreferenciación; forma parte del nombre del tipo.
En el contexto de los punteros sin formato, inmutable significa que no se puede
asignar directamente al puntero después de desreferenciarlo.

A diferencia de las referencias y los punteros inteligentes, los punteros sin


procesar:

 Se permite ignorar las reglas de préstamo al tener punteros inmutables y


mutables o múltiples punteros mutables a la misma ubicación
 No se garantiza que apunten a una memoria válida
 Se permite que sean nulos
 No implemente ninguna limpieza automática

Al optar por no permitir que Rust aplique estas garantías, puede renunciar a la
seguridad garantizada a cambio de un mayor rendimiento o la capacidad de
interactuar con otro lenguaje o hardware donde las garantías de Rust no se
aplican.

El listado 20-1 muestra cómo crear un puntero sin formato inmutable y uno
mutable.

let mut num = 5;

let r1 = &raw const num;


let r2 = &raw mut num;
Listado 20-1: Creación de punteros sin procesar con los operadores de préstamo sin procesar

Tenga en cuenta que no incluimos la unsafepalabra clave en este código. Podemos


crear punteros sin formato en código seguro; simplemente no podemos
desreferenciarlos fuera de un bloque inseguro, como verá más adelante.
Rust
jueves, 22 de mayo de 2025 : Página 600 de 719

Hemos creado punteros sin procesar mediante los operadores de préstamo sin
procesar: &raw const num crea un *const i32puntero sin procesar inmutable y &raw
mut numcrea un *mut i32puntero sin procesar mutable. Dado que los creamos
directamente desde una variable local, sabemos que estos punteros sin procesar
son válidos, pero no podemos asumirlo con cualquier puntero sin procesar.

Para demostrar esto, crearemos un puntero sin formato cuya validez no es tan
segura, utilizando asla conversión de un valor en lugar de los operadores de
referencia sin formato. El Listado 20-2 muestra cómo crear un puntero sin formato
a una ubicación arbitraria en memoria. Intentar usar memoria arbitraria no está
definido: podría haber datos en esa dirección o no, el compilador podría optimizar
el código para que no haya acceso a memoria, o el programa podría generar un
error de segmentación. Normalmente, no hay una buena razón para escribir
código como este, especialmente cuando se puede usar un operador de préstamo
sin formato, pero es posible.

let address = 0x012345usize;


let r = address as *const i32;
Listado 20-2: Creación de un puntero sin formato a una dirección de memoria arbitraria

Recuerde que podemos crear punteros sin procesar en código seguro, pero no
podemos desreferenciarlos ni leer los datos a los que apuntan. En el Listado 20-3,
usamos el operador de desreferencia *en un puntero sin procesar que requiere
un unsafebloque.

let mut num = 5;

let r1 = &raw const num;


let r2 = &raw mut num;

unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
Listado 20-3: Desreferenciación de punteros sin procesar dentro de un unsafebloque

Crear un puntero no hace daño; solo cuando intentamos acceder al valor al que
apunta es cuando podemos terminar tratando con un valor no válido.

Tenga en cuenta también que en los Listados 20-1 y 20-3, creamos


punteros *const i32sin *mut i32 procesar que apuntaban a la misma ubicación de
memoria, donde numse almacena. Si, en cambio, hubiéramos intentado crear una
referencia inmutable y una mutable a num, el código no se habría compilado, ya
Rust
jueves, 22 de mayo de 2025 : Página 601 de 719

que las reglas de propiedad de Rust no permiten una referencia mutable


simultáneamente con ninguna referencia inmutable. Con punteros sin procesar,
podemos crear un puntero mutable y un puntero inmutable a la misma ubicación
y modificar los datos a través del puntero mutable, lo que podría generar una
carrera de datos. ¡Tenga cuidado!

Con todos estos peligros, ¿por qué usar punteros sin formato? Un caso de uso
importante es la interacción con código C, como verá en la siguiente
sección, "Llamar a una función o método inseguro". Otro caso es la creación de
abstracciones seguras que el verificador de préstamos no comprende.
Presentaremos las funciones inseguras y luego veremos un ejemplo de una
abstracción segura que utiliza código inseguro.

Llamar a una función o método inseguro

El segundo tipo de operación que se puede realizar en un bloque inseguro es


llamar a funciones inseguras. Las funciones y métodos inseguros son idénticos a
las funciones y métodos normales, pero tienen una instrucción
adicional unsafeantes del resto de la definición.unsafe palabra clave en este
contexto indica que la función tiene requisitos que debemos cumplir al llamarla,
ya que Rust no puede garantizar que se cumplan. Al llamar a una función
insegura dentro de un unsafebloque, indicamos que hemos leído la documentación
de la función y asumimos la responsabilidad de cumplir sus contratos.

Aquí hay una función insegura llamada dangerousque no hace nada en su cuerpo:

unsafe fn dangerous() {}

unsafe {
dangerous();
}

Debemos llamar a la dangerousfunción dentro de un unsafebloque aparte. Si


intentamos llamar dangeroussin el unsafebloque, obtendremos un error:

$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe function or
block
--> src/main.rs:4:5
|
4| dangerous();
| ^^^^^^^^^^^ call to unsafe function
|
Rust
jueves, 22 de mayo de 2025 : Página 602 de 719
= note: consult the function's documentation for information on how to avoid undefined
behavior

For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

Con elunsafe bloque, le afirmamos a Rust que hemos leído la documentación de la


función, que entendemos cómo usarla correctamente y que hemos verificado que
estamos cumpliendo el contrato de la función.

Para realizar operaciones inseguras en el cuerpo de una función insegura, es


necesario usar un unsafebloque, igual que en una función normal, y el compilador
le avisará si lo olvida. Esto ayuda a mantener unsafelos bloques lo más pequeños
posible, ya que las operaciones inseguras podrían no ser necesarias en todo el
cuerpo de la función.

Creando una abstracción segura sobre código inseguro

El hecho de que una función contenga código inseguro no significa que debamos
marcarla como tal. De hecho, encapsular código inseguro en una función segura
es una abstracción común. A modo de ejemplo, estudiemos la split_at_mutfunción
de la biblioteca estándar, que requiere código inseguro. Exploraremos cómo
implementarla. Este método seguro se define en segmentos mutables: toma un
segmento y lo convierte en dos dividiéndolo en el índice dado como argumento. El
listado 20-4 muestra cómo usar split_at_mut.

let mut v = vec![1, 2, 3, 4, 5, 6];

let r = &mut v[..];

let (a, b) = r.split_at_mut(3);

assert_eq!(a, &mut [1, 2, 3]);


assert_eq!(b, &mut [4, 5, 6]);
Listado 20-4: Uso de la split_at_mutfunción segura

No podemos implementar esta función usando únicamente Rust seguro. Un


intento podría ser similar al Listado 20-5, que no compilará. Para simplificar, la
implementaremos split_at_mutcomo una función en lugar de un método y solo para
fragmentos de i32valores, no para un tipo genérico T.

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
Rust
jueves, 22 de mayo de 2025 : Página 603 de 719

assert!(mid <= len);

(&mut values[..mid], &mut values[mid..])


}
Listado 20-5: Un intento de implementación del split_at_mutuso exclusivo de Rust seguro

Esta función primero obtiene la longitud total del segmento. Luego, verifica si el
índice dado como parámetro está dentro del segmento, verificando si es menor o
igual a la longitud. Esto significa que si pasamos un índice mayor que la longitud
para dividir el segmento, la función entrará en pánico antes de intentar usarlo.

Luego devolvemos dos porciones mutables en una tupla: una desde el comienzo
de la porción original hasta el midíndice y otra desde midel final de la porción.

Cuando intentamos compilar el código del Listado 20-5, obtendremos un error.

$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:6:31
|
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6| (&mut values[..mid], &mut values[mid..])
| --------------------------^^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*values` is borrowed for `'1`
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices

For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

El verificador de préstamos de Rust no puede entender que estamos tomando


prestadas diferentes partes de la porción; solo sabe que estamos tomando
prestadas de la misma porción dos veces. Tomar prestadas diferentes partes de
una porción es básicamente correcto porque ambas no se superponen, pero Rust
no es lo suficientemente inteligente como para detectarlo. Cuando sabemos que
el código es correcto, pero Rust no, es hora de buscar código inseguro.
Rust
jueves, 22 de mayo de 2025 : Página 604 de 719

El listado 20-6 muestra cómo utilizar un unsafebloque, un puntero sin formato y


algunas llamadas a funciones inseguras para realizar la implementación
del split_at_muttrabajo.

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();

assert!(mid <= len);

unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
Listado 20-6: Uso de código inseguro en la implementación de la split_at_mutfunción

Recuerde de la sección "El tipo de rebanada" del capítulo 4 que las rebanadas son
un puntero a datos y su longitud. Usamos el lenmétodo para obtener la longitud
de una rebanada y el as_mut_ptr método para acceder a su puntero sin formato. En
este caso, dado que tenemos una rebanada mutable
a i32valores, as_mut_ptrdevuelve un puntero sin formato con el tipo *mut i32, que
hemos almacenado en la variable ptr.

Mantenemos la afirmación de que el midíndice está dentro de la porción. Luego,


llegamos al código inseguro: la slice::from_raw_parts_mutfunción toma un puntero
sin formato y una longitud, y crea una porción. Usamos esta función para crear
una porción que comienza en ptry tiene miduna longitud de elementos. Luego,
llamamos al add método "on" ptrcon midcomo argumento para obtener un puntero
sin formato que comienza en midy creamos una porción usando ese puntero y el
número de elementos restantes midcomo longitud.

La función slice::from_raw_parts_mutno es segura porque toma un puntero sin


formato y debe confiar en su validez. El addmétodo con punteros sin formato
tampoco es seguro, ya que debe confiar en que la ubicación de desplazamiento
también sea un puntero válido. Por lo tanto, tuvimos que bloquear unsafenuestras
llamadas a [nombre slice::from_raw_parts_mutdel addmétodo] para poder llamarlas.
Al observar el código y agregar la afirmación de que middebe ser menor o igual
que [ nombre del método len], podemos determinar que todos los punteros sin
Rust
jueves, 22 de mayo de 2025 : Página 605 de 719

formato utilizados dentro del unsafebloque serán punteros válidos a los datos
dentro de la porción. Este es un uso aceptable y apropiado de [nombre del
método].unsafe .

Tenga en cuenta que no es necesario marcar la split_at_mutfunción resultante


como unsafe, y podemos llamarla desde Rust de forma segura. Hemos creado una
abstracción segura para el código inseguro con una implementación de la función
que usa unsafeel código de forma segura, ya que solo crea punteros válidos a
partir de los datos a los que tiene acceso.

Por el contrario, el uso de slice::from_raw_parts_muten el Listado 20-7


probablemente fallaría al usar la porción. Este código toma una ubicación de
memoria arbitraria y crea una porción de 10 000 elementos.

use std::slice;

let address = 0x01234usize;


let r = address as *mut i32;

let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };


Listado 20-7: Creación de una porción desde una ubicación de memoria arbitraria

No poseemos la memoria en esta ubicación arbitraria, y no hay garantía de que la


porción que crea este código contenga i32valores válidos. Intentar
usarla valuescomo si fuera una porción válida genera un comportamiento
indefinido.

Uso de externfunciones para llamar a código externo

En ocasiones, tu código Rust podría necesitar interactuar con código escrito en


otro lenguaje. Para ello, Rust cuenta con la palabra clave externque facilita la
creación y el uso de una Interfaz de Función Externa (FFI) . Una FFI permite a un
lenguaje de programación definir funciones y habilitar un lenguaje de
programación diferente (externo) para llamarlas.

El Listado 20-8 muestra cómo configurar una integración con la absfunción de la


biblioteca estándar de C. Las funciones declaradas dentro de externbloques no
suelen ser seguras para llamar desde el código de Rust, por lo que también deben
marcarse como unsafe. Esto se debe a que otros lenguajes no aplican las reglas y
garantías de Rust, y Rust no puede verificarlas, por lo que la responsabilidad de
garantizar la seguridad recae en el programador.
Rust
jueves, 22 de mayo de 2025 : Página 606 de 719
Nombre de archivo: src/main.rs
unsafe extern "C" {
fn abs(input: i32) -> i32;
}

fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
Listado 20-8: Declaración y llamada de una externfunción definida en otro lenguaje

Dentro del unsafe extern "C"bloque, listamos los nombres y las firmas de las
funciones externas de otro lenguaje que queremos llamar. Esta "C"parte define
la interfaz binaria de aplicación (ABI) que utiliza la función externa: la ABI define
cómo llamar a la función a nivel de ensamblador. La "C"ABI es la más común y
sigue la ABI del lenguaje de programación C.

Sin embargo, esta función en particular no tiene ninguna consideración de


seguridad de memoria. De hecho, sabemos que cualquier llamada a abssiempre
será segura para cualquier ``` i32, por lo que podemos usar la safepalabra clave
para indicar que esta función específica es segura de llamar incluso si está en
un unsafe externbloque. Una vez realizado este cambio, llamarla ya no requiere
un unsafebloque, como se muestra en el Listado 20-9.

Nombre de archivo: src/main.rs


unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}

fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
Listado 20-9: Marcar explícitamente una función como safedentro de un unsafe externbloque y llamarla
de forma segura

Marcar una función como safesegura no la hace intrínsecamente segura. Es como


prometerle a Rust que es segura. ¡Aún es tu responsabilidad asegurarte de que
esa promesa se cumpla!

Llamar a funciones de Rust desde otros lenguajes

También podemos usar esta externfunción para crear una interfaz que permita a otros lenguajes llamar
a funciones de Rust. En lugar de crear un bloque completo ,extern añadimos la externpalabra clave y
especificamos la ABI que se usará justo antes de la fnpalabra clave para la función correspondiente.
Rust
jueves, 22 de mayo de 2025 : Página 607 de 719

También necesitamos añadir una #[unsafe(no_mangle)]anotación para indicar al compilador de Rust


que no altere el nombre de esta función. es cuando un compilador cambia el nombre que le hemos dado
a una función por uno diferente que contiene más información para que otras partes del proceso de
compilación la consuman, pero es menos legible para humanos. Cada compilador de lenguaje de
programación altera los nombres de forma ligeramente diferente, por lo que para que una función de
Rust sea nombrable por otros lenguajes, debemos deshabilitar la alteración de nombres del compilador
de Rust. Esto es inseguro porque podría haber colisiones de nombres entre bibliotecas sin la alteración
incorporada, por lo que es nuestra responsabilidad asegurarnos de que el nombre que hemos exportado
sea seguro para exportar sin alterar.

En el siguiente ejemplo, hacemos que la call_from_cfunción sea accesible desde el código C, después
de compilarla en una biblioteca compartida y vincularla desde C:

#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}

Este uso de externno requiere unsafe.

Cómo acceder o modificar una variable estática mutable

En este libro, aún no hemos hablado de las variables globales , que Rust admite,
pero que pueden ser problemáticas con sus reglas de propiedad. Si dos hilos
acceden a la misma variable global mutable, puede causar una carrera de datos.

En Rust, las variables globales se denominan variables estáticas . El Listado 20-10


muestra un ejemplo de declaración y uso de una variable estática con un
segmento de cadena como valor.

Nombre de archivo: src/main.rs


static HELLO_WORLD: &str = "Hello, world!";

fn main() {
println!("name is: {HELLO_WORLD}");
}
Listado 20-10: Definición y uso de una variable estática inmutable

Las variables estáticas son similares a las constantes, que analizamos en la


sección "Constantes" del Capítulo 3. Los nombres de las variables estáticas se
usan SCREAMING_SNAKE_CASEpor convención. Las variables estáticas solo pueden
almacenar referencias con su 'static tiempo de vida, lo que significa que el
Rust
jueves, 22 de mayo de 2025 : Página 608 de 719

compilador de Rust puede determinarlo y no es necesario anotarlo


explícitamente. Acceder a una variable estática inmutable es seguro.

Una sutil diferencia entre las constantes y las variables estáticas inmutables es
que los valores de una variable estática tienen una dirección fija en memoria.
Usar el valor siempre accederá a los mismos datos. Las constantes, en cambio,
pueden duplicar sus datos al usarse. Otra diferencia es que las variables estáticas
pueden ser mutables. Acceder y modificar variables estáticas mutables no
es seguro . El listado 20-11 muestra cómo declarar, acceder y modificar una
variable estática mutable llamada COUNTER.

Nombre de archivo: src/main.rs


static mut COUNTER: u32 = 0;

/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}

fn main() {
unsafe {
// SAFETY: This is only called from a single thread in `main`.
add_to_count(3);
println!("COUNTER: {}", *(&raw const COUNTER));
}
}
Listado 20-11: Leer o escribir en una variable estática mutable no es seguro

Al igual que con las variables regulares, especificamos la mutabilidad mediante


la mutpalabra clave. Cualquier código que lea o escriba COUNTERdebe estar dentro
de un unsafebloque. El código del Listado 20-11 se compila e imprime COUNTER:
3como cabría esperar, ya que es de un solo subproceso. El acceso de varios
subprocesos COUNTERprobablemente provocaría carreras de datos, por lo que se
trata de un comportamiento indefinido. Por lo tanto, debemos marcar toda la
función comounsafe y documentar la limitación de seguridad, para que cualquier
persona que la llame sepa qué puede hacer de forma segura y qué no.

Al escribir una función insegura, es idiomático escribir un comentario que


comience con [nombre del SAFETYusuario] y explique qué debe hacer el invocador
para llamar a la función de forma segura. Asimismo, al realizar una operación
Rust
jueves, 22 de mayo de 2025 : Página 609 de 719

insegura, es idiomático escribir un comentario que comience con [nombre del


usuario] SAFETYpara explicar cómo se cumplen las reglas de seguridad.

Además, el compilador no permite crear referencias a una variable estática


mutable. Solo se puede acceder a ella mediante un puntero sin formato, creado
con uno de los operadores de préstamo sin formato. Esto incluye los casos en que
la referencia se crea de forma invisible, como cuando se utiliza en println!este
listado de código. El requisito de que las referencias a variables estáticas
mutables solo se puedan crear mediante punteros sin formato ayuda a que los
requisitos de seguridad para su uso sean más evidentes.

Con datos mutables accesibles globalmente, es difícil garantizar que no haya


carreras de datos, por lo que Rust considera que las variables estáticas mutables
son inseguras. Siempre que sea posible, es preferible usar las técnicas de
concurrencia y los punteros inteligentes seguros para subprocesos que
analizamos en el Capítulo 16 para que el compilador verifique que el acceso a los
datos desde diferentes subprocesos se realice de forma segura.

Implementar un rasgo inseguro

Podemos usar unsafepara implementar un rasgo inseguro. Un rasgo es inseguro


cuando al menos uno de sus métodos tiene una invariante que el compilador no
puede verificar. Declaramos que un rasgo es unsafeinseguro añadiendo
la unsafepalabra clave antes trait y marcando su implementación
como unsafetambién, como se muestra en el Listado 20-12.

unsafe trait Foo {


// methods go here
}

unsafe impl Foo for i32 {


// method implementations go here
}

fn main() {}
Listado 20-12: Definición e implementación de un rasgo inseguro

Mediante el usounsafe impl , prometemos que mantendremos las invariantes que


el compilador no puede verificar.

Como ejemplo, recuerde los rasgos de marcador Syncy Sendque analizamos en la


sección "Concurrencia extensible con los rasgos Syncy Send " del capítulo 16: el
Rust
jueves, 22 de mayo de 2025 : Página 610 de 719

compilador implementa estos rasgos automáticamente si nuestros tipos están


compuestos completamente por tipos Sendy Sync. Si implementamos un tipo que
contiene un tipo que no es Sendo Sync, como punteros sin formato, y queremos
marcar ese tipo como Sendo Sync, debemos usar unsafe. Rust no puede verificar
que nuestro tipo cumpla con las garantías de que se pueda enviar de forma
segura entre subprocesos o acceder desde múltiples subprocesos; por lo tanto,
debemos realizar estas comprobaciones manualmente e indicarlo con unsafe .

Acceso a los campos de una unión

La última acción que solo funciona con unsafees acceder a los campos de
una unión . A uniones similar a a struct, pero solo se usa un campo declarado en
cada instancia a la vez. Las uniones se usan principalmente para interactuar con
uniones en código C. Acceder a los campos de unión no es seguro porque Rust no
puede garantizar el tipo de los datos almacenados en la instancia de unión.
Puedes obtener más información sobre las uniones en la Referencia de Rust .

Usar Miri para comprobar código inseguro

Al escribir código inseguro, conviene comprobar que lo escrito sea seguro y


correcto. Una de las mejores maneras de hacerlo es usar Miri , una herramienta
oficial de Rust para detectar comportamientos indefinidos. Mientras que el
verificador de préstamos es una herramienta estática que funciona en tiempo de
compilación, Miri es una herramienta dinámica que funciona en tiempo de
ejecución. Comprueba el código ejecutando el programa o su conjunto de
pruebas, y detectando cuándo se infringen las reglas que entiende sobre el
funcionamiento de Rust.

Usar Miri requiere una compilación nocturna de Rust (de la que hablaremos más
en el Apéndice G: Cómo se crea Rust y "Nightly Rust" ). Puedes instalar tanto la
versión nocturna de Rust como la herramienta Miri escribiendo rustup +nightly
component add miri. Esto no cambia la versión de Rust que usa tu proyecto; solo
añade la herramienta a tu sistema para que puedas usarla cuando quieras.
Puedes ejecutar Miri en un proyecto escribiendo cargo +nightly miri runo cargo
+nightly miri test.

Para ver un ejemplo de lo útil que puede ser esto, considere lo que sucede cuando
lo ejecutamos contra el Listado 20-11:

$ cargo +nightly miri run


Rust
jueves, 22 de mayo de 2025 : Página 611 de 719
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `/Users/chris/.rustup/toolchains/nightly-aarch64-apple-darwin/bin/cargo-miri
runner target/miri/aarch64-apple-darwin/debug/unsafe-example`
warning: creating a shared reference to mutable static is discouraged
--> src/main.rs:14:33
|
14 | println!("COUNTER: {}", COUNTER);
| ^^^^^^^ shared reference to mutable static
|
= note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-
2024/static-mut-references.html>
= note: shared references to mutable statics are dangerous; it's undefined behavior if the
static is mutated or if a mutable reference is created for it while the shared reference lives
= note: `#[warn(static_mut_refs)]` on by default

COUNTER: 3

Detecta de forma útil y correcta que hemos compartido referencias a datos


mutables y nos advierte al respecto. En este caso, no nos indica cómo solucionar
el problema, pero significa que sabemos que existe un posible problema y
podemos pensar en cómo garantizar su seguridad. En otros casos, puede incluso
indicarnos que algún código seguramente es incorrecto y ofrecer
recomendaciones para solucionarlo.

Miri no detecta todos los errores que podrías cometer al escribir código inseguro.
Por un lado, al ser una comprobación dinámica, solo detecta problemas en el
código que realmente se ejecuta. Esto significa que deberás usarla junto con
buenas técnicas de prueba para aumentar tu confianza en el código inseguro que
has escrito. Por otro lado, no cubre todas las posibles maneras en que tu código
puede ser incorrecto. Si Miri detecta un problema, sabes que hay un error, pero
que Miri no detecte un error no significa que no haya un problema. Sin embargo,
Miri puede detectar muchos. ¡Intenta ejecutarla en los otros ejemplos de código
inseguro de este capítulo y comprueba lo que dice!

Cuándo utilizar código inseguro

Usar unsafepara realizar una de las cinco acciones (superpoderes) que acabamos
de mencionar no es incorrecto ni está mal visto. Sin embargo, es más complicado
que unsafeel código sea correcto porque el compilador no puede ayudar a
mantener la seguridad de la memoria. Cuando se tiene una razón para
usar unsafecódigo, se puede hacer, y tener la unsafe anotación explícita facilita la
localización del origen de los problemas cuando ocurren. Siempre que se escribe
Rust
jueves, 22 de mayo de 2025 : Página 612 de 719

código inseguro, se puede usar Miri para tener mayor confianza en que el código
escrito cumple con las reglas de Rust.

Para una exploración mucho más profunda de cómo trabajar eficazmente con
Rust inseguro, lea la guía oficial de Rust sobre el tema, el Rustonomicon .

Rasgos avanzados
Primero abordamos los rasgos en la sección "Rasgos: Definición del
comportamiento compartido" del Capítulo 10, pero no abordamos los detalles más
avanzados. Ahora que conoces más sobre Rust, podemos profundizar en los
detalles.

Especificación de tipos de marcadores de posición en


definiciones de rasgos con tipos asociados

Tipos asociados conectan un marcador de posición de tipo con un rasgo, de modo


que las definiciones de métodos del rasgo puedan usar estos tipos de marcador
de posición en sus firmas. El implementador de un rasgo especificará el tipo
concreto que se usará en lugar del tipo de marcador de posición para la
implementación específica. De esta forma, podemos definir un rasgo que use
ciertos tipos sin necesidad de saber exactamente cuáles son hasta su
implementación.

En este capítulo, hemos descrito la mayoría de las funciones avanzadas como


poco necesarias. Los tipos asociados se encuentran en un punto intermedio: se
usan con menos frecuencia que las funciones explicadas en el resto del libro, pero
con mayor frecuencia que muchas de las demás funciones que se describen en
este capítulo.

Un ejemplo de un rasgo con un tipo asociado es el Iteratorrasgo proporcionado por


la biblioteca estándar. El tipo asociado se nombra Itemy representa el tipo de los
valores sobre los que Iteratoritera el tipo que implementa el rasgo. La definición
del Iteratorrasgo se muestra en el Listado 20-13.

pub trait Iterator {


type Item;

fn next(&mut self) -> Option<Self::Item>;


}
Listado 20-13: La definición del Iteratorrasgo que tiene un tipo asociadoItem
Rust
jueves, 22 de mayo de 2025 : Página 613 de 719

El tipo Itemes un marcador de posición, y la nextdefinición del método indica que


devolverá valores de tipo Option<Self::Item>. Los implementadores del Iteratorrasgo
especificarán el tipo concreto para Item, y el next método devolverá un Optionque
contiene un valor de ese tipo concreto.

Los tipos asociados pueden parecer un concepto similar al de los genéricos, ya


que estos últimos permiten definir una función sin especificar qué tipos puede
manejar. Para examinar la diferencia entre ambos conceptos, analizaremos una
implementación del Iteratorrasgo en un tipo llamado Counterque especifica que
el Itemtipo es u32:

Nombre de archivo: src/lib.rs


impl Iterator for Counter {
type Item = u32;

fn next(&mut self) -> Option<Self::Item> {


// --snip--

Esta sintaxis parece comparable a la de los genéricos. Entonces, ¿por qué no


definir el Iteratorrasgo con genéricos, como se muestra en el Listado 20-14?

pub trait Iterator<T> {


fn next(&mut self) -> Option<T>;
}
Listado 20-14: Una definición hipotética del Iteratorrasgo utilizando genéricos

La diferencia radica en que al usar genéricos, como en el Listado 20-14, debemos


anotar los tipos en cada implementación; dado que también podemos
implementar Iterator<String> for Countero cualquier otro tipo, podríamos tener
múltiples implementaciones de Iteratorfor Counter. En otras palabras, cuando un
rasgo tiene un parámetro genérico, puede implementarse para un tipo varias
veces, cambiando los tipos concretos de los parámetros de tipo genérico cada
vez. Al usar el nextmétodo on Counter, tendríamos que proporcionar anotaciones
de tipo para indicar qué implementación de Iteratorqueremos usar.

Con tipos asociados, no necesitamos anotarlos, ya que no podemos implementar


un atributo en un tipo varias veces. En el Listado 20-13, con la definición que usa
tipos asociados, solo podemos elegir el tipo de Itemuna vez, ya que solo puede
haber un tipo impl Iterator for Counter. No tenemos que especificar que queremos un
iterador de u32valores en cada llamada nexta Counter.
Rust
jueves, 22 de mayo de 2025 : Página 614 de 719

Los tipos asociados también forman parte del contrato del rasgo: los
implementadores del rasgo deben proporcionar un tipo que sustituya al marcador
de tipo asociado. Los tipos asociados suelen tener un nombre que describe cómo
se usarán, y documentarlos en la documentación de la API es una buena práctica.

Parámetros de tipo genérico predeterminados y sobrecarga de


operadores

Cuando usamos parámetros de tipo genérico, podemos especificar un tipo


concreto predeterminado para el tipo genérico. Esto elimina la necesidad de que
los implementadores del rasgo especifiquen un tipo concreto si el tipo
predeterminado funciona. Se especifica un tipo predeterminado al declarar un
tipo genérico con el<PlaceholderType=ConcreteType> sintaxis.

Un gran ejemplo de una situación en la que esta técnica es útil es la sobrecarga


de operadores , en la que se personaliza el comportamiento de un operador
(como +) en situaciones particulares.

Rust no permite crear operadores propios ni sobrecargar operadores arbitrarios.


Sin embargo, sí es posible sobrecargar las operaciones y los atributos
correspondientes enumerados std::opsimplementando los atributos asociados al
operador. Por ejemplo, en el Listado 20-15 sobrecargamos el +operador para
sumar dos Point instancias. Para ello, implementamos el Addatributo en
una Point estructura:

Nombre de archivo: src/main.rs


use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]


struct Point {
x: i32,
y: i32,
}

impl Add for Point {


type Output = Point;

fn add(self, other: Point) -> Point {


Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
Rust
jueves, 22 de mayo de 2025 : Página 615 de 719
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
Listado 20-15: Implementación del Addrasgo para sobrecargar el +operador para Pointinstancias

El addmétodo suma los xvalores de dos instancias Pointpara crear un nuevo


atributo . El atributo tiene un tipo asociado llamado `` que determina el tipo que
devuelve el método.yPointPointAddOutputadd

El tipo genérico predeterminado en este código se encuentra dentro del Addrasgo.


Aquí está su definición:

trait Add<Rhs=Self> {
type Output;

fn add(self, rhs: Rhs) -> Self::Output;


}

Este código debería resultar familiar: un trait con un método y un tipo asociado.
La novedad es que Rhs=Selfesta sintaxis se denomina "parámetros de tipo
predeterminados ". El Rhsparámetro de tipo genérico (abreviatura de "lado
derecho") define el tipo del rhsparámetro en el addmétodo. Si no especificamos un
tipo concreto Rhsal implementar el Addtrait, el tipo Rhspredeterminado será " Self,
que será el tipo en el que lo implementamos" Add.

Al implementar " Addfor" Point, usamos el valor predeterminado Rhsporque


queríamos agregar dos Pointinstancias. Veamos un ejemplo de implementación
del Addatributo donde queremos personalizar el Rhstipo en lugar de usar el valor
predeterminado.

Tenemos dos estructuras, Millimetersy Meters, que contienen valores en diferentes


unidades. Esta envoltura fina de un tipo existente en otra estructura se conoce
como el patrón newtype , que describimos con más detalle en la sección "Uso del
patrón newtype para implementar atributos externos en tipos externos" .
Queremos sumar valores en milímetros a valores en metros y que la
implementación de Addrealice la conversión correctamente. Podemos
implementar Add for Millimeterscon Meterscomo Rhs, como se muestra en el Listado
20-16.

Nombre de archivo: src/lib.rs


Rust
jueves, 22 de mayo de 2025 : Página 616 de 719
use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {


type Output = Millimeters;

fn add(self, other: Meters) -> Millimeters {


Millimeters(self.0 + (other.0 * 1000))
}
}
Listado 20-16: Implementando el Addrasgo Millimeterspara agregar MillimetersaMeters

Para agregar Millimetersy Meters, especificamos impl Add<Meters>que se establezca


el valor del Rhsparámetro de tipo en lugar de usar el valor predeterminado de Self .

Utilizarás parámetros de tipo predeterminados de dos maneras principales:

 Para ampliar un tipo sin romper el código existente


 Para permitir la personalización en casos específicos, la mayoría de los
usuarios no necesitarán

El rasgo de la biblioteca estándar Addes un ejemplo del segundo propósito:


normalmente, se agregan dos tipos similares, pero el Addrasgo ofrece la
posibilidad de personalizarlo más allá de eso. Usar un parámetro de tipo
predeterminado en la Adddefinición del rasgo significa que, en la mayoría de los
casos, no es necesario especificar el parámetro adicional. En otras palabras, no se
requiere un código repetitivo de implementación, lo que facilita el uso del rasgo.

El primer propósito es similar al segundo, pero a la inversa: si desea agregar un


parámetro de tipo a un rasgo existente, puede darle un valor predeterminado
para permitir la extensión de la funcionalidad del rasgo sin romper el código de
implementación existente.

Sintaxis totalmente calificada para desambiguación: llamar a


métodos con el mismo nombre

En Rust, nada impide que un rasgo tenga un método con el mismo nombre que el
de otro rasgo, ni impide implementar ambos rasgos en un mismo tipo. También es
posible implementar un método directamente en el tipo con el mismo nombre que
los métodos de los rasgos.
Rust
jueves, 22 de mayo de 2025 : Página 617 de 719

Al llamar a métodos con el mismo nombre, deberá indicarle a Rust cuál desea
usar. Considere el código del Listado 20-17, donde definimos dos
rasgos, Piloty Wizard, y ambos tienen un método llamado fly. Luego,
implementamos ambos rasgos en un tipo Humanque ya tiene implementado un
método llamado fly. Cada flymétodo realiza una función diferente.

Nombre de archivo: src/main.rs


trait Pilot {
fn fly(&self);
}

trait Wizard {
fn fly(&self);
}

struct Human;

impl Pilot for Human {


fn fly(&self) {
println!("This is your captain speaking.");
}
}

impl Wizard for Human {


fn fly(&self) {
println!("Up!");
}
}

impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
Listado 20-17: Se definen dos rasgos que hacen que un method and are implemented on thehumano type,
and avuele method is implemented ondirectamente.

Cuando llamamos flya una instancia de Human, el compilador por defecto llama al
método que está implementado directamente en el tipo, como se muestra en el
Listado 20-18.

Nombre de archivo: src/main.rs


fn main() {
let person = Human;
person.fly();
}
Listado 20-18: Llamada flya una instancia deHuman
Rust
jueves, 22 de mayo de 2025 : Página 618 de 719

Al ejecutar este código se imprimirá *waving arms furiously*, mostrando que Rust
llamó al flymétodo implementado Humandirectamente.

Para llamar a los flymétodos desde el Pilotrasgo o desde el Wizardrasgo,


necesitamos usar una sintaxis más explícita para especificar a qué flymétodo nos
referimos. El Listado 20-19 muestra esta sintaxis.

Nombre de archivo: src/main.rs


fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
Listado 20-19: Especificación del método de qué rasgo flyqueremos llamar

Especificar el nombre del rasgo antes del nombre del método le indica a Rust qué
implementación de flyqueremos llamar. También podríamos
escribir Human::fly(&person)```, equivalente al person.fly()que usamos en el Listado
20-19, pero es un poco más largo si no necesitamos desambiguar``.

Al ejecutar este código se imprime lo siguiente:

$ 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 flymétodo toma un selfparámetro, si tuviéramos dos tipos que


implementan una característica , Rust podría determinar qué implementación de
una característica usar en función del tipo de self.

Sin embargo, las funciones asociadas que no son métodos no


tienen self parámetros. Cuando existen varios tipos o atributos que definen
funciones no metodológicas con el mismo nombre, Rust no siempre sabe a qué
tipo se refiere, a menos que se utilice la sintaxis completa . Por ejemplo, en el
Listado 20-20, creamos un atributo para un refugio de animales que desea
nombrar a todos los cachorros de perro como Spot . Creamos un Animalatributo
con una función no metodológica asociada baby_name. El Animalatributo se
implementa para la estructura Dog, en la que también proporcionamos
directamente una función no metodológica asociada baby_name.
Rust
jueves, 22 de mayo de 2025 : Página 619 de 719
Nombre de archivo: src/main.rs
trait Animal {
fn baby_name() -> String;
}

struct Dog;

impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}

impl Animal for Dog {


fn baby_name() -> String {
String::from("puppy")
}
}

fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
Listado 20-20: Un rasgo con una función asociada y un tipo con una función asociada del mismo
nombre que también implementa el rasgo

Implementamos el código para nombrar a todos los cachorros con Spot en


la baby_namefunción asociada definida en Dog. El Dogtipo también implementa el
rasgo Animal, que describe las características que poseen todos los animales. Las
crías de perro se llaman cachorros, y esto se expresa en la implementación
del Animal rasgo on Dogen elbaby_name función asociada a dicho Animalrasgo.

En main, llamamos a la Dog::baby_namefunción que llama Dogdirectamente a la


función asociada definida en . Este código imprime lo siguiente:

$ 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

Esta salida no es la que buscábamos. Queremos llamar a la baby_namefunción que


forma parte del Animalrasgo que implementamos Dogpara que el código imprima A
baby dog is called a puppy. La técnica de especificar el nombre del rasgo que
usamos en el Listado 20-19 no es útil en este caso; si cambiamos main al código
del Listado 20-21, obtendremos un error de compilación.

Nombre de archivo: src/main.rs


Rust
jueves, 22 de mayo de 2025 : Página 620 de 719

fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
Listado 20-21: Intentando llamar a la baby_namefunción desde elAnimal rasgo, pero Rust no sabe qué
implementación usar

Como Animal::baby_nameno tiene un selfparámetro y podría haber otros tipos que


implementen el Animalatributo, Rust no puede determinar qué
implementación Animal::baby_namenecesitamos. Obtendremos este error del
compilador:

$ 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 Animalfor Dogen lugar de la implementación deAnimal for" de otro tipo,
necesitamos usar la sintaxis completa. El Listado 20-22 muestra cómo usar la
sintaxis completa.

Nombre de archivo: src/main.rs


fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
Listado 20-22: Uso de sintaxis completamente calificada para especificar que queremos llamar a
la baby_namefunción desde elAnimal rasgo tal como se implementó enDog

Proporcionamos a Rust una anotación de tipo entre corchetes angulares, que


indica que queremos llamar al baby_namemétodo desde el Animalrasgo tal como
Rust
jueves, 22 de mayo de 2025 : Página 621 de 719

está implementado, Dogindicando que queremos tratar el Dogtipo como


un Animalpara esta llamada de función. Este código ahora mostrará lo que
queremos:

$ 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

En general, la sintaxis completamente calificada se define de la siguiente manera:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

Para las funciones asociadas que no sean métodos, no habría una receiver :`, solo
la lista de otros argumentos. Se podría usar la sintaxis completa siempre que se
llamen funciones o métodos. Sin embargo, se permite omitir cualquier parte de
esta sintaxis que Rust pueda deducir de otra información del programa. Solo se
necesita usar esta sintaxis más detallada cuando haya varias implementaciones
con el mismo nombre y Rust necesite ayuda para identificar a qué
implementación se desea llamar.

Uso de superrasgos para exigir la funcionalidad de un rasgo


dentro de otro rasgo

A veces, se puede escribir una definición de rasgo que depende de otro rasgo:
para que un tipo implemente el primer rasgo, se requiere que ese tipo también
implemente el segundo. Esto se hace para que la definición de rasgo pueda usar
los elementos asociados del segundo rasgo. El rasgo en el que se basa la
definición de rasgo se denomina superrasgo .

Por ejemplo, supongamos que queremos crear un OutlinePrintatributo con


un outline_printmétodo que imprima un valor dado, formateado entre asteriscos. Es
decir, dada una Pointestructura que implementa el atributo de la biblioteca
estándar Displaypara que resulte en (x, y), al llamar outline_printa una Pointinstancia
que tenga 1for xy 3for y, debería imprimir lo siguiente:

**********
* *
* (1, 3) *
* *
**********
Rust
jueves, 22 de mayo de 2025 : Página 622 de 719

En la implementación del outline_printmétodo, queremos usar


la Displayfuncionalidad del rasgo. Por lo tanto, debemos especificar que
el OutlinePrintrasgo solo funcionará con tipos que también implementen Displayy
proporcionen la funcionalidad OutlinePrintnecesaria. Podemos hacerlo en la
definición del rasgo especificando `<string>` OutlinePrint: Display. Esta técnica es
similar a agregar un rasgo enlazado al rasgo. El Listado 20-23 muestra una
implementación del OutlinePrintrasgo.

Nombre de archivo: src/main.rs


use std::fmt;

trait OutlinePrint: fmt::Display {


fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
Listado 20-23: Implementación del OutlinePrintrasgo que requiere la funcionalidad deDisplay

Como especificamos que OutlinePrintse requiere el Displayatributo, podemos usar


la to_stringfunción que se implementa automáticamente para cualquier tipo que
implemente Display. Si intentáramos usarla to_stringsin añadir dos puntos y
especificando el Displayatributo después del nombre del atributo, obtendríamos un
error indicando que no to_stringse encontró ningún método nombrado para el
tipo &Selfen el ámbito actual.

Veamos qué sucede cuando intentamos implementar OutlinePrinten un tipo que no


implementa Display, como la Pointestructura:

Nombre de archivo: src/main.rs

struct Point {
x: i32,
y: i32,
}

impl OutlinePrint for Point {}

Recibimos un error que dice que Displayes obligatorio pero no está implementado:
Rust
jueves, 22 de mayo de 2025 : Página 623 de 719
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`

error[E0277]: `Point` doesn't implement `std::fmt::Display`


--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ `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::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function

For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors

Para solucionar esto, implementamos Displayon Pointy satisfacemos la restricción


que OutlinePrintrequiere, de la siguiente manera:

Nombre de archivo: src/main.rs


use std::fmt;

impl fmt::Display for Point {


fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}

Luego, al implementar el OutlinePrintrasgo Point, se compilará exitosamente y


podremos llamar outline_printa una Pointinstancia para mostrarla dentro de un
contorno de asteriscos.
Rust
jueves, 22 de mayo de 2025 : Página 624 de 719

Uso del patrón Newtype para implementar rasgos externos en


tipos externos

En el Capítulo 10, en la sección "Implementación de un rasgo en un tipo" ,


mencionamos la regla huérfana que establece que solo se permite implementar
un rasgo en un tipo si el rasgo o el tipo son locales a nuestra caja. Es posible
sortear esta restricción utilizando el patrón newtype , que implica crear un nuevo
tipo en una estructura de tupla. (Abordamos las estructuras de tupla en la
sección "Uso de estructuras de tupla sin campos con nombre para crear
diferentes tipos" del Capítulo 5). La estructura de tupla tendrá un campo y será
una envoltura ligera alrededor del tipo para el que queremos implementar un
rasgo. Entonces, el tipo de envoltura es local a nuestra caja, y podemos
implementar el rasgo en la envoltura. Newtype es un término que se origina en el
lenguaje de programación Haskell. No hay penalización de rendimiento en tiempo
de ejecución por usar este patrón, y el tipo de envoltura se omite en tiempo de
compilación.

Por ejemplo, supongamos que queremos implementar Displayon Vec<T>, algo que
la regla de objetos huérfanos nos impide hacer directamente porque
el Displayrasgo y el Vec<T>tipo están definidos fuera de nuestro crate. Podemos
crear una Wrapperestructura que contenga una instancia de Vec<T>; luego,
podemos implementar Displayon Wrappery usar el Vec<T>valor, como se muestra
en el Listado 20-24.

Nombre de archivo: src/main.rs


use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {


fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}

fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {w}");
}
Listado 20-24: Creación de un Wrappertipo Vec<String>para implementarDisplay
Rust
jueves, 22 de mayo de 2025 : Página 625 de 719

La implementación de Displayuses self.0para acceder al interno Vec<T>, ya


que Wrapperes una estructura de tupla y Vec<T>es el elemento en el índice 0 de la
tupla. Entonces, podemos usar la funcionalidad del Displayrasgo en Wrapper.

La desventaja de usar esta técnica es que, Wrapperal ser un tipo nuevo, no tiene
los métodos del valor que contiene. Tendríamos que implementar todos los
métodos de Vec<T>directamente en [ Wrappernombre del tipo] para que deleguen
a [nombre del tipo] self.0, lo que nos permitiría tratarlo Wrapperexactamente como
un [nombre del tipo Vec<T>]. Si quisiéramos que el nuevo tipo tuviera todos los
métodos del tipo interno, implementar el Derefatributo [nombre del tipo]
(discutido en el Capítulo 15, en la sección "Tratar punteros inteligentes como
referencias regulares con el Deref atributo" ) en [ nombre del tipo Wrapper] para
devolver el tipo interno sería una solución. Si no queremos que el Wrappertipo
tenga todos los métodos del tipo interno (por ejemplo, para restringir
su Wrappercomportamiento), tendríamos que implementar manualmente solo los
métodos que necesitamos.

Este nuevo patrón de tipo también es útil incluso cuando no se utilizan rasgos.
Cambiemos de enfoque y veamos algunas formas avanzadas de interactuar con el
sistema de tipos de Rust.

Tipos avanzados
El sistema de tipos de Rust tiene algunas características que ya hemos
mencionado, pero que aún no hemos analizado. Comenzaremos analizando los
nuevos tipos en general, y analizaremos su utilidad como tipos. Después,
abordaremos los alias de tipo, una característica similar a la de los nuevos tipos,
pero con una semántica ligeramente diferente. También analizaremos el !tipo y
los tipos de tamaño dinámico.

Uso del patrón Newtype para la seguridad y abstracción de tipos

Nota: Esta sección asume que ha leído la sección anterior “Uso del patrón Newtype para implementar
rasgos externos en tipos externos”.

El patrón newtype también es útil para tareas distintas a las que hemos descrito
hasta ahora, como asegurar estáticamente que los valores nunca se confundan e
indicar las unidades de un valor. Vimos un ejemplo del uso de newtypes para
indicar unidades en el Listado 20-16: recordemos que las
Rust
jueves, 22 de mayo de 2025 : Página 626 de 719

estructuras Millimetersy Meters encapsulaban u32los valores en un newtype. Si


escribiéramos una función con un parámetro de tipo Millimeters, no podríamos
compilar un programa que intentara llamar accidentalmente a esa función con un
valor de tipo Meterso un valor simple u32.

También podemos utilizar el patrón newtype para abstraer algunos detalles de


implementación de un tipo: el nuevo tipo puede exponer una API pública que es
diferente de la API del tipo interno privado.

Los nuevos tipos también pueden ocultar la implementación interna. Por ejemplo,
podríamos proporcionar un Peopletipo para encapsular un HashMap<i32, String>que
almacene el ID de una persona asociado a su nombre. Código que usa People solo
interactuaría con la API pública que proporcionamos, como un método para
agregar una cadena de nombre a la People colección; ese código no necesitaría
saber que asignamos un i32ID a los nombres internamente. El patrón newtype es
una forma ligera de lograr la encapsulación para ocultar los detalles de
implementación, lo cual explicamos en la sección "Encapsulación que oculta los
detalles de implementación" del Capítulo 18.

Creación de sinónimos de tipo con alias de tipo

Rust permite declarar un alias de tipo para darle otro nombre a un tipo existente.
Para ello, usamos la typepalabra clave. Por ejemplo, podemos crear el
alias Kilometersde i32la siguiente manera:

type Kilometers = i32;

Ahora, el alias Kilometerses sinónimo de i32; a diferencia de los


tipos Millimeters y Metersque creamos en el Listado 20-16, Kilometersno es un tipo
nuevo e independiente. Los valores que tienen el tipo Kilometersse tratarán igual
que los valores del tipo i32:

type Kilometers = i32;

let x: i32 = 5;
let y: Kilometers = 5;

println!("x + y = {}", x + y);

Porque Kilometersyi32 son del mismo tipo, podemos sumar valores de ambos tipos
y pasar Kilometersvalores a funciones que aceptan i32 parámetros. Sin embargo,
con este método, no obtenemos las ventajas de la verificación de tipos que ofrece
Rust
jueves, 22 de mayo de 2025 : Página 627 de 719

el patrón newtype mencionado anteriormente. En otras palabras, si confundimos


los valores Kilometersde y i32en algún punto, el compilador no generará un error.

El principal uso de los sinónimos de tipo es reducir la repetición. Por ejemplo,


podríamos tener un tipo extenso como este:

Box<dyn Fn() + Send + 'static>

Escribir este tipo extenso en las firmas de funciones y como anotaciones de tipo
en todo el código puede ser tedioso y propenso a errores. Imagine tener un
proyecto lleno de código como el del Listado 20-25.

let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {


// --snip--
}

fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {


// --snip--
}
Listado 20-25: Uso de un tipo largo en muchos lugares

Un alias de tipo facilita la gestión de este código al reducir la repetición. En el


Listado 20-26, introdujimos un alias con Thunkel nombre del tipo verboso y
podemos reemplazar todos los usos del tipo con el alias más corto Thunk.

type Thunk = Box<dyn Fn() + Send + 'static>;

let f: Thunk = Box::new(|| println!("hi"));

fn takes_long_type(f: Thunk) {
// --snip--
}

fn returns_long_type() -> Thunk {


// --snip--
}
Listado 20-26: Introducción de un alias de tipoThunk para reducir la repetición

¡Este código es mucho más fácil de leer y escribir! Elegir un nombre significativo
para un alias de tipo también puede ayudar a comunicar tu intención. thunk se
refiere al código que se evaluará posteriormente, por lo que es un nombre
apropiado para un cierre que se almacena).
Rust
jueves, 22 de mayo de 2025 : Página 628 de 719

Los alias de tipo también se usan comúnmente con el Result<T, E>tipo para
reducir la repetición. Considere el std::iomódulo de la biblioteca estándar. Las
operaciones de E/S suelen devolver un Result<T, E>para gestionar situaciones en
las que las operaciones fallan. Esta biblioteca tiene una std::io::Errorestructura que
representa todos los posibles errores de E/S. Muchas de las funciones
de std::iodevolverán Result<T, E>donde el tipo Ees std::io::Error, como estas
funciones en el Writerasgo:

use std::fmt;
use std::io::Error;

pub trait Write {


fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
fn flush(&mut self) -> Result<(), Error>;

fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;


fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}

Se Result<..., Error>repite mucho. Por lo tanto, std::iotiene esta declaración de alias


de tipo:

type Result<T> = std::result::Result<T, std::io::Error>;

Dado que esta declaración está en el std::iomódulo, podemos usar el alias


completo std::io::Result<T>; es decir, a Result<T, E>con el E como std::io::Error.
Las Writefirmas de las funciones de rasgo se ven así:

pub trait Write {


fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;

fn write_all(&mut self, buf: &[u8]) -> Result<()>;


fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}

El alias de tipo ayuda de dos maneras: facilita la escritura del código y nos
proporciona una interfaz consistente en todo el código std::io. Al ser un alias, es
simplemente otro.Result<T, E> , lo que significa que podemos usar cualquier
método compatible Result<T, E>con él, así como sintaxis especial como el ?
operador.

El tipo que nunca regresa


Rust
jueves, 22 de mayo de 2025 : Página 629 de 719

Rust tiene un tipo especial llamado tipo vacío! en la jerga de la teoría de tipos, ya
que no tiene valores. Preferimos llamarlo tipo "nunca" porque reemplaza al tipo
de retorno cuando una función nunca retorna. Aquí hay un ejemplo:

fn bar() -> ! {
// --snip--
}

Este código se lee como "la función barnunca retorna". Las funciones que nunca
retornarán se denominan funciones divergentes . No podemos crear valores de
este tipo, ! por lo que barnunca podrían retornarse.

Pero ¿de qué sirve un tipo para el que nunca se pueden crear valores? Recuerda
el código del Listado 2-5, parte del juego de adivinanzas; hemos reproducido un
fragmento aquí en el Listado 20-27.

let guess: u32 = match guess.trim().parse() {


Ok(num) => num,
Err(_) => continue,
};
Listado 20-27: A matchcon un brazo que termina encontinue

En aquel momento, omitimos algunos detalles de este código. En el capítulo 6, en


la sección "El matchoperador de flujo de control" , explicamos que matchtodos los
brazos deben devolver el mismo tipo. Por ejemplo, el siguiente código no
funciona:

let guess = match guess.trim().parse() {


Ok(_) => 5,
Err(_) => "hello",
};

El tipo de guessen este código tendría que ser un entero y una cadena, y Rust
requiere que guesssolo tengan un tipo. Entonces, ¿qué continue devuelve? ¿Cómo
se nos permitió devolver "a" u32de un brazo y tener otro brazo que termina en
"" continueen el Listado 20-27?

Como habrás adivinado, continuetiene un !valor. Es decir, cuando Rust calcula el


tipo de guess, examina ambos brazos coincidentes: el primero con un valor
de u32y el segundo con un !valor. Como !nunca puede tener un valor, Rust decide
que el tipo de guesses u32.
Rust
jueves, 22 de mayo de 2025 : Página 630 de 719

La forma formal de describir este comportamiento es que las expresiones de tipo !


pueden ser forzadas a cualquier otro tipo. Podemos terminar este matchbrazo con
`` continueporque`` continueno devuelve un valor; en su lugar, devuelve el control
al principio del bucle, por lo que, en este Errcaso, nunca asignamos un valor a
` guess`.

panic!El tipo "never" también es útil con la macro. Recuerde la unwrap función que
llamamos en Option<T>valores para producir un valor o pánico con esta definición:

impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}

En este código, ocurre lo mismo que en el matchListado 20-27: Rust detecta


que valtiene el tipo Ty panic!tiene el tipo !, por lo que el resultado de
la matchexpresión general es T. Este código funciona porque panic! no produce un
valor; finaliza el programa. En elNone caso, no devolveremos un valor de unwrap,
por lo que este código es válido.

Una expresión final que tiene el tipo !es a loop:

print!("forever ");

loop {
print!("and ever ");
}

Aquí, el bucle nunca termina, al igual !que el valor de la expresión. Sin embargo,
esto no sería cierto si incluyéramos unbreak , ya que el bucle terminaría al llegar
al break.

Tipos de tamaño dinámico y elSized rasgo

Rust necesita conocer ciertos detalles sobre sus tipos, como cuánto espacio
asignar a un valor de un tipo en particular. Esto deja un aspecto de su sistema de
tipos un poco confuso al principio: el concepto de tipos de tamaño dinámico . A
veces se les conoce como DST o tipos sin tamaño , estos tipos permiten escribir
código con valores cuyo tamaño solo se conoce en tiempo de ejecución.
Rust
jueves, 22 de mayo de 2025 : Página 631 de 719

Profundicemos en los detalles de un tipo de tamaño dinámico llamado str, que


hemos usado a lo largo del libro. Así es, no &str, pero strpor sí solo es un DST. No
podemos saber la longitud de la cadena hasta el tiempo de ejecución, lo que
significa que no podemos crear una variable de tipo str, ni aceptar un argumento
de tipo str. Considere el siguiente código, que no funciona:

let s1: str = "Hello there!";


let s2: str = "How's it going?";

Rust necesita saber cuánta memoria asignar a cualquier valor de un tipo


específico, y todos los valores de un tipo deben usar la misma cantidad de
memoria. Si Rust nos permitiera escribir este código, estos dos strvalores
ocuparían el mismo espacio. Sin embargo, tienen longitudes diferentes:
uno s1necesita 12 bytes de almacenamiento y s2otro 15. Por eso no es posible
crear una variable que contenga un tipo de tamaño dinámico.

Entonces, ¿qué hacemos? En este caso, ya sabes la respuesta: hacemos los tipos
de s1y s2a &stren lugar de a str. Recuerda de la sección “Segmentos de
cadenas” del Capítulo 4 que la estructura de datos del segmento solo almacena la
posición inicial y la longitud del segmento. Entonces, aunque a &Tes un valor
único que almacena la dirección de memoria de donde Tse encuentra the ,
a &strson dos valores: la dirección de the stry su longitud. Como tal, podemos
saber el tamaño de un &strvalor en tiempo de compilación: es el doble de la
longitud de a usize. Es decir, siempre sabemos el tamaño de a &str, sin importar
cuán larga sea la cadena a la que se refiere. En general, esta es la forma en que
se usan los tipos de tamaño dinámico en Rust: tienen un bit adicional de
metadatos que almacena el tamaño de la información dinámica. La regla de oro
de los tipos de tamaño dinámico es que siempre debemos poner valores de tipos
de tamaño dinámico detrás de un puntero de algún tipo.

Podemos combinar strtodo tipo de punteros: por ejemplo, Box<str>o Rc<str>. De


hecho, ya lo has visto antes, pero con un tipo de tamaño dinámico diferente: los
rasgos. Cada rasgo es un tipo de tamaño dinámico al que podemos referirnos
usando su nombre. En el capítulo 18, en la sección "Uso de objetos de rasgo que
admiten valores de diferentes tipos" , mencionamos que para usar rasgos como
objetos de rasgo, debemos colocarlos detrás de un puntero, como &dyn
Traito Box<dyn Trait>( Rc<dyn Trait>también funcionaría).
Rust
jueves, 22 de mayo de 2025 : Página 632 de 719

Para trabajar con DST, Rust proporciona la Sizedpropiedad que determina si el


tamaño de un tipo se conoce en tiempo de compilación. Esta propiedad se
implementa automáticamente para todos los tipos cuyo tamaño se conoce en
tiempo de compilación. Además, Rust añade implícitamente un límite Sizeda cada
función genérica. Es decir, una definición de función genérica como esta:

fn generic<T>(t: T) {
// --snip--
}

En realidad se trata como si hubiéramos escrito esto:

fn generic<T: Sized>(t: T) {
// --snip--
}

De forma predeterminada, las funciones genéricas solo funcionan con tipos cuyo
tamaño se conoce en tiempo de compilación. Sin embargo, puede usar la
siguiente sintaxis especial para flexibilizar esta restricción:

fn generic<T: ?Sized>(t: &T) {


// --snip--
}

Un atributo enlazado ?Sizedsignifica " Tpuede o no ser Sized" y esta notación anula
el valor predeterminado que establece que los tipos genéricos deben tener un
tamaño conocido en tiempo de compilación. La ?Traitsintaxis con este significado
solo está disponible para Sized , no para ningún otro atributo.

Tenga en cuenta también que cambiamos el tipo del tparámetro de Ta &T. Porque
el tipo podría no serSized , necesitamos usarlo detrás de algún tipo de puntero. En
este caso, hemos elegido una referencia.

¡A continuación hablaremos de funciones y cierres!

Funciones avanzadas y cierres


Esta sección explora algunas características avanzadas relacionadas con
funciones y cierres, incluidos punteros de función y cierres de retorno.

Punteros de función
Rust
jueves, 22 de mayo de 2025 : Página 633 de 719

Hemos hablado sobre cómo pasar clausuras a funciones; ¡también puedes pasar
funciones regulares a funciones! Esta técnica es útil cuando quieres pasar una
función ya definida en lugar de definir una nueva clausura. Las funciones
coaccionan al tipo fn(con f minúscula), que no debe confundirse con el Fn rasgo de
clausura. El fntipo se denomina puntero a función . Pasar funciones con punteros a
función te permitirá usar funciones como argumentos de otras funciones.

La sintaxis para especificar que un parámetro es un puntero a función es similar a


la de las clausuras, como se muestra en el Listado 20-28, donde definimos una
función add_oneque suma uno a su parámetro. La función do_twicetoma dos
parámetros: un puntero a cualquier función que tome un i32parámetro y devuelva
un i32valor y un i32valor. La do_twicefunción llama a la función fdos veces, le pasa
el argvalor y luego suma los resultados de ambas llamadas. La mainfunción
llama do_twicecon los argumentos add_oney 5.

Nombre de archivo: src/main.rs


fn add_one(x: i32) -> i32 {
x+1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {


f(arg) + f(arg)
}

fn main() {
let answer = do_twice(add_one, 5);

println!("The answer is: {answer}");


}
Listado 20-28: Uso del fntipo para aceptar un puntero de función como argumento

Este código imprime The answer is: 12. Especificamos que el


parámetro fen do_twicees un fnque toma un parámetro de tipo i32y devuelve
un i32. Luego, podemos llamar fen el cuerpo de do_twice. En main, podemos pasar
el nombre de la función add_onecomo primer argumento a do_twice.

A diferencia de los cierres, fnes un tipo en lugar de un rasgo, por lo que lo


especificamos fncomo el tipo de parámetro directamente en lugar de declarar un
parámetro de tipo genérico con uno de los Fnrasgos como un límite de rasgo.

Los punteros de función implementan los tres rasgos de cierre ( Fn, FnMut,
y FnOnce ), lo que significa que siempre se puede pasar un puntero a función
como argumento para una función que espera un cierre. Es recomendable escribir
Rust
jueves, 22 de mayo de 2025 : Página 634 de 719

funciones con un tipo genérico y uno de los rasgos de cierre para que las
funciones puedan aceptar funciones o cierres.

Dicho esto, un ejemplo de dónde solo querrías aceptar fny no cierres es cuando
interactúas con código externo que no tiene cierres: las funciones de C pueden
aceptar funciones como argumentos, pero C no tiene cierres.

Como ejemplo de dónde se puede usar una clausura definida en línea o una
función con nombre, veamos el uso del mapmétodo proporcionado por
el Iterator rasgo en la biblioteca estándar. Para usar la mapfunción y convertir un
vector de números en un vector de cadenas, podríamos usar una clausura como
esta:

let list_of_numbers = vec![1, 2, 3];


let list_of_strings: Vec<String> =
list_of_numbers.iter().map(|i| i.to_string()).collect();

O podríamos nombrar una función como argumento mapen lugar del cierre, de
esta manera:

let list_of_numbers = vec![1, 2, 3];


let list_of_strings: Vec<String> =
list_of_numbers.iter().map(ToString::to_string).collect();

Tenga en cuenta que debemos usar la sintaxis completa que explicamos


anteriormente en la sección "Características avanzadas", ya que existen varias
funciones llamadas to_string. Aquí, usamos la to_stringfunción definida en
la ToStringcaracterística, que la biblioteca estándar ha implementado para
cualquier tipo que implemente Display.

Recuerde de la sección "Valores de enumeración" del Capítulo 6 que el nombre de


cada variante de enumeración que definimos también se convierte en una función
de inicialización. Podemos usar estas funciones de inicialización como punteros de
función que implementan las características de clausura, lo que significa que
podemos especificarlas como argumentos para métodos que aceptan clausuras,
como se muestra a continuación:

enum Status {
Value(u32),
Stop,
}

let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();


Rust
jueves, 22 de mayo de 2025 : Página 635 de 719

Aquí creamos Status::Valueinstancias usando cada u32valor en el rango quemap se


llama mediante la función inicializadora de Status::Value. Algunos usuarios
prefieren este estilo, mientras que otros prefieren usar cierres. Se compilan con el
mismo código, así que use el estilo que le resulte más claro.

Cierres que regresan

Los cierres se representan mediante rasgos, lo que significa que no se pueden


devolver directamente. En la mayoría de los casos, si se desea devolver un rasgo,
se puede usar el tipo concreto que lo implementa como valor de retorno de la
función. Sin embargo, esto no es posible con los cierres, ya que no tienen un tipo
concreto retornable; no se permite usar el puntero de función. fn por ejemplo, no
se permite usar el puntero de función como tipo de retorno.

En su lugar, normalmente usará la impl Traitsintaxis que aprendimos en el Capítulo


10. Puede devolver cualquier tipo de función usando Fn, FnOncey FnMut. Por
ejemplo, este código funcionará perfectamente:

fn returns_closure() -> impl Fn(i32) -> i32 {


|x| x + 1
}

Sin embargo, como mencionamos en la sección "Inferencia y anotación de tipos


de cierre" del capítulo 13, cada cierre también tiene su propio tipo. Si necesita
trabajar con varias funciones con la misma firma, pero con diferentes
implementaciones, deberá usar un objeto de rasgo para ellas:

fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {


Box::new(|x| x + 1)
}

fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {


Box::new(move |x| x + init)
}
Rust
jueves, 22 de mayo de 2025 : Página 636 de 719

Este código compilará correctamente, pero no lo haría si hubiéramos intentado


usar impl Fn(i32) -> i32. Para más información sobre los objetos de rasgo, consulte
la sección «Uso de objetos de rasgo que admiten valores de diferentes tipos» en
el capítulo 18.

¡A continuación, veamos las macros!

Macros
Hemos usado macros println!a lo largo de este libro, pero no hemos explorado
completamente qué es una macro y cómo funciona. El término macro se refiere a
una familia de características en Rust: macros declarativasmacro_rules! y tres tipos
de macros procedimentales :

 Macros personalizadas #[derive]que especifican el código agregado con


el deriveatributo utilizado en estructuras y enumeraciones
 Macros similares a atributos que definen atributos personalizados que se
pueden utilizar en cualquier elemento
 Macros similares a funciones que parecen llamadas de función pero que
operan en los tokens especificados como su argumento

Hablaremos de cada uno de ellos por turno, pero primero veamos por qué
necesitamos macros cuando ya tenemos funciones.

La diferencia entre macros y funciones

Fundamentalmente, las macros son una forma de escribir código que a su vez
escribe otro código, lo que se conoce como metaprogramación . En el Apéndice C,
analizamos el derive atributo, que genera una implementación de varios rasgos.
También hemos utilizado las macros println!y vec!a lo largo del libro. Todas estas
macros se expanden para producir más código que el que se ha escrito
manualmente.

La metaprogramación es útil para reducir la cantidad de código que se debe


escribir y mantener, lo cual también es una de las funciones. Sin embargo, las
macros tienen algunas funciones adicionales que no tienen.

La firma de una función debe declarar el número y el tipo de parámetros que


tiene. Las macros, por otro lado, pueden aceptar un número variable de
parámetros: podemos llamar println!("hello")con un argumento o println!("hello {}",
Rust
jueves, 22 de mayo de 2025 : Página 637 de 719

name) 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 una característica en un tipo dado. Una función no puede,
ya que se llama en tiempo de ejecución y una característica debe implementarse
en tiempo de compilación.

La desventaja de implementar una macro en lugar de una función es que las


definiciones de macro son más complejas que las de función, ya que se escribe
código de Rust que escribe código de Rust. Debido a esta indirección, las
definiciones de macro suelen ser más difíciles de leer, comprender y mantener
que las de función.

Otra diferencia importante entre macros y funciones es que debes definir macros
o incluirlas en el alcance antes llamarlas en un archivo, a diferencia de las
funciones que puedes definir en cualquier lugar y llamar en cualquier lugar.

Macros declarativas macro_rules!para metaprogramación general

La forma más utilizada de macros en Rust es la macro declarativa . Estas también


se conocen como "macros de ejemplo", " macro_rules!macros" o simplemente
"macros". En esencia, las macros declarativas permiten escribir algo similar a
una matchexpresión de Rust. Como se explicó en el Capítulo 6, matchlas
expresiones son estructuras de control que toman una expresión, comparan su
valor resultante con patrones y luego ejecutan el código asociado con el patrón
correspondiente. Las macros también comparan un valor con patrones asociados
a un código específico: en este caso, el valor es el código fuente literal de Rust
que se pasa a la macro; los patrones se comparan con la estructura de ese código
fuente; y el código asociado a cada patrón, al coincidir, reemplaza el código
pasado a la macro. Todo esto ocurre durante la compilación.

Para definir una macro, se utiliza la macro_rules!construcción. Exploremos su


uso macro_rules!analizando cómo vec!se define la macro. El capítulo 8 explicó cómo
usar la vec!macro para crear un nuevo vector con valores específicos. Por
ejemplo, la siguiente macro crea un nuevo vector que contiene tres enteros:

let v: Vec<u32> = vec![1, 2, 3];

También podríamos usar la vec!macro para crear un vector de dos enteros o un


vector de cinco segmentos de cadena. No podríamos usar una función para hacer
lo mismo porque desconocemos de antemano el número o el tipo de valores.
Rust
jueves, 22 de mayo de 2025 : Página 638 de 719

El listado 20-29 muestra una definición ligeramente simplificada de la vec!macro.

Nombre de archivo: src/lib.rs


#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
Listado 20-29: Una versión simplificada de la vec!definición de macro

Nota: La definición real de la vec!macro en la biblioteca estándar incluye código para preasignar la
cantidad correcta de memoria desde el principio. Este código es una optimización que no incluimos
aquí para simplificar el ejemplo.

La #[macro_export]anotación indica que esta macro debe estar disponible siempre


que el contenedor en el que está definida se incluya en el ámbito de aplicación.
Sin esta anotación, la macro no puede incluirse en el ámbito de aplicación.

Luego, iniciamos la definición de la macro con macro_rules!y el nombre de la macro


que estamos definiendo sin el signo de exclamación. El nombre, en este caso vec,
va seguido de llaves que indican el cuerpo de la definición de la macro.

La estructura del vec!cuerpo es similar a la de una match expresión. Aquí tenemos


un brazo con el patrón ( $( $x:expr ),* ), seguido de=> y el bloque de código
asociado a este patrón. Si el patrón coincide, se emitirá el bloque de código
asociado. Dado que este es el único patrón en esta macro, solo hay una forma
válida de coincidencia; cualquier otro patrón generará un error. Las macros más
complejas tendrán más de un brazo.

La sintaxis de patrones válida en las definiciones de macros difiere de la sintaxis


de patrones descrita en el Capítulo 19, ya que los patrones de macros se
comparan con la estructura del código de Rust, no con sus valores. Analicemos el
significado de las partes del patrón en los Listados 20-29; para la sintaxis
completa de los patrones de macros, consulte la Referencia de Rust .
Rust
jueves, 22 de mayo de 2025 : Página 639 de 719

Primero, usamos un paréntesis para abarcar todo el patrón. Usamos un signo de


dólar ( $) para declarar una variable en el sistema de macros que contendrá el
código de Rust correspondiente al patrón. El signo de dólar indica que se trata de
una variable de macros, no de una variable normal de Rust. A continuación, se
incluyen paréntesis que capturan los valores que coinciden con el patrón dentro
de ellos para su uso en el código de reemplazo. Dentro de $()es $x:expr, que
coincide con cualquier expresión de Rust y le asigna el nombre $x.

La coma que sigue $()indica que debe aparecer un separador de coma literal
entre cada instancia del código que coincida con el código dentro de $().
Esto *especifica que el patrón coincide con cero o más de lo que precede a *.

Cuando llamamos a esta macro con vec![1, 2, 3];, el $xpatrón coincide tres veces
con las tres expresiones 1, 2, y 3.

Ahora veamos el patrón en el cuerpo del código asociado con este brazo:
" temp_vec.push()within" $()*se genera para cada parte que coincide $() con el
patrón cero o más veces, dependiendo de cuántas veces coincida. "The" $xse
reemplaza con cada expresión coincidente. Al llamar a esta macro con "with" vec!
[1, 2, 3];, el código generado que reemplaza esta llamada a la macro será el
siguiente:

{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}

Hemos definido una macro que puede tomar cualquier número de argumentos de
cualquier tipo y puede generar código para crear un vector que contenga los
elementos especificados.

Para obtener más información sobre cómo escribir macros, consulte la


documentación en línea u otros recursos, como “El pequeño libro de macros de
Rust”, iniciado por Daniel Keep y continuado por Lukas Wirth.

Macros de procedimiento para generar código a partir de


atributos
Rust
jueves, 22 de mayo de 2025 : Página 640 de 719

La segunda forma de macros es la macro procedimental , que actúa de forma


más similar a una función (y es un tipo de procedimiento). Las macros
procedimentales aceptan código como entrada, operan sobre él y generan código
como salida, en lugar de comparar patrones y reemplazar el código con otro,
como hacen las macros declarativas. Los tres tipos de macros procedimentales
son las de derivación personalizada, las de tipo atributo y las de tipo función, y
todas funcionan de forma similar.

Al crear macros procedimentales, las definiciones deben residir en su propia caja


con un tipo de caja especial. Esto se debe a razones técnicas complejas que
esperamos eliminar en el futuro. En los Listados 20-30, mostramos cómo definir
una macro procedimental, donde some_attributees un marcador de posición para
usar una variedad específica de macro.

Nombre de archivo: src/lib.rs


use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Listado 20-30: Un ejemplo de definición de una macro procedimental

La función que define una macro procedural toma a TokenStreamcomo entrada y


produce a TokenStreamcomo salida. El TokenStreamtipo se define mediante
el proc_macrocrate incluido en Rust y representa una secuencia de tokens. Este es
el núcleo de la macro: el código fuente sobre el que opera la macro constituye la
entrada TokenStream, y el código que produce la macro es la salida TokenStream. La
función también tiene un atributo asociado que especifica el tipo de macro
procedural que estamos creando. Podemos tener varios tipos de macros
procedurales en el mismo crate.

Analicemos los diferentes tipos de macros procedimentales. Comenzaremos con


una macro de derivación personalizada y luego explicaremos las pequeñas
diferencias que diferencian a las demás.

Cómo escribir una derivemacro personalizada

Creemos un crate llamado hello_macroque define un rasgo llamado HelloMacrocon


una función asociada llamada hello_macro. En lugar de obligar a nuestros usuarios
a implementar el HelloMacrorasgo para cada uno de sus tipos, proporcionaremos
una macro de procedimiento para que puedan anotar su tipo con [nombre del
Rust
jueves, 22 de mayo de 2025 : Página 641 de 719

tipo #[derive(HelloMacro)]] y obtener una implementación predeterminada de


la hello_macro función. La implementación predeterminada mostrará Hello, Macro!
My name is TypeName![ TypeNamenombre del tipo en el que se ha definido este
rasgo]. En otras palabras, crearemos un crate que permita a otro programador
escribir código como el del Listado 20-31 usando nuestro crate.

Nombre de archivo: src/main.rs

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
Pancakes::hello_macro();
}
Listado 20-31: El código que un usuario de nuestro cajón podrá escribir al usar nuestra macro de
procedimiento

Este código se imprimirá Hello, Macro! My name is Pancakes!al terminar. El primer


paso es crear una nueva caja de biblioteca, como esta:

$ cargo new hello_macro --lib

A continuación, definiremos el HelloMacrorasgo y su función asociada:

Nombre de archivo: src/lib.rs


pub trait HelloMacro {
fn hello_macro();
}

Tenemos un rasgo y su función. En este punto, nuestro usuario de crate podría


implementar el rasgo para lograr la funcionalidad deseada, así:

use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {


fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}

fn main() {
Pancakes::hello_macro();
Rust
jueves, 22 de mayo de 2025 : Página 642 de 719
}

Sin embargo, necesitarían escribir el bloque de implementación para cada tipo


que quisieran usar hello_macro; queremos evitarles tener que hacer este trabajo.

Además, todavía no podemos proporcionar la hello_macro función con la


implementación predeterminada que imprimirá el nombre del tipo en el que se
implementa el rasgo: Rust no tiene capacidades de reflexión, por lo que no puede
consultar el nombre del tipo en tiempo de ejecución. Necesitamos una macro para
generar código en tiempo de compilación.

El siguiente paso es definir la macro procedural. Al momento de escribir este


artículo, las macros procedurales deben estar en su propio contenedor. Con el
tiempo, esta restricción podría eliminarse. La convención para estructurar
contenedores y contenedores de macros es la siguiente: para un contenedor
llamado foo, un contenedor de macro procedural de derivación personalizado se
llama foo_derive. Iniciemos un nuevo contenedor llamado hello_macro_derivedentro
de nuestro hello_macroproyecto:

$ cargo new hello_macro_derive --lib

Nuestras dos cajas están estrechamente relacionadas, por lo que creamos la


macro procedural crate dentro del directorio de nuestra hello_macrocaja. Si
modificamos la definición del rasgo en [nombre del archivo] hello_macro, también
tendremos que modificar la implementación de la macro procedural en [nombre
del hello_macro_derivearchivo]. Ambas cajas deberán publicarse por separado, y los
programadores que las utilicen deberán añadirlas como dependencias y ajustarlas
al alcance. En lugar de eso, podríamos hello_macrousar la
caja hello_macro_derivecomo dependencia y reexportar el código de la macro
procedural. Sin embargo, la forma en que hemos estructurado el proyecto
permite que los programadores la utilicen hello_macroincluso si no desean
la derivefuncionalidad.

Necesitamos declarar la hello_macro_derivecaja como una macro procedural.


También necesitaremos la funcionalidad de las cajas syny quote, como verás en
breve, así que debemos agregarlas como dependencias. Agrega lo siguiente al
archivo Cargo.toml para hello_macro_derive:

Nombre del archivo: hello_macro_derive/Cargo.toml


[lib]
proc-macro = true
Rust
jueves, 22 de mayo de 2025 : Página 643 de 719

[dependencies]
syn = "2.0"
quote = "1.0"

Para empezar a definir la macro procedural, inserte el código del Listado 20-32 en
el archivo src/lib.rs de la hello_macro_derivecaja. Tenga en cuenta que este código
no se compilará hasta que agreguemos una definición para
la impl_hello_macrofunción.

Nombre de archivo: 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();

// Build the trait implementation


impl_hello_macro(&ast)
}
Listado 20-32: Código que la mayoría de las cajas de macros procedimentales requerirán para procesar
el código de Rust

Observe que hemos dividido el código en la hello_macro_derivefunción, responsable


de analizar el `` TokenStream`, y la impl_hello_macro función, responsable de
transformar el árbol sintáctico: esto facilita la escritura de una macro procedural.
El código de la función externa ( hello_macro_deriveen este caso) será el mismo
para casi todas las macros procedimentales que vea o cree. El código que
especifique en el cuerpo de la función interna ( impl_hello_macroen este caso)
variará según el propósito de su macro procedural.

Hemos introducido tres nuevos crates: proc_macro, syny quote. Este proc_macrocrate
viene con Rust, así que no fue necesario añadirlo a las dependencias
de Cargo.toml . El proc_macrocrate es la API del compilador que nos permite leer y
manipular código de Rust desde nuestro código.

El syncrate analiza el código de Rust desde una cadena a una estructura de datos
con la que podemos realizar operaciones. El quotecrate convierte synlas
estructuras de datos de nuevo en código de Rust. Estos crates simplifican
Rust
jueves, 22 de mayo de 2025 : Página 644 de 719

enormemente el análisis de cualquier tipo de código de Rust que queramos


manejar: escribir un analizador completo para código de Rust no es tarea fácil.

La hello_macro_derivefunción se llamará cuando un usuario de nuestra biblioteca


especifique #[derive(HelloMacro)]un tipo. Esto es posible porque hemos anotado
la hello_macro_derivefunción aquí con proc_macro_derivey especificado el
nombre HelloMacro, que coincide con el nombre de nuestro atributo; esta es la
convención que siguen la mayoría de las macros de procedimiento.

La hello_macro_derivefunción primero convierte inputde a TokenStreama una


estructura de datos que podemos interpretar y sobre la que podemos realizar
operaciones. Aquí es donde synentra en juego. La parsefunción in syntoma
a TokenStreamy devuelve una DeriveInputestructura que representa el código de
Rust analizado. El Listado 20-33 muestra las partes relevantes de
la DeriveInput estructura que obtenemos al analizar la struct Pancakes;cadena:

DeriveInput {
// --snip--

ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
Listado 20-33: La DeriveInputinstancia que obtenemos al analizar el código que tiene el atributo de la
macro en el Listado 20-31

Los campos de esta estructura indican que el código de Rust analizado es una
estructura unitaria con el identidentificador (es decir, el nombre) de Pancakes. Hay
más campos en esta estructura para describir todo tipo de código de Rust;
consulta la syn documentación paraDeriveInput obtener más información.

Pronto definiremos la impl_hello_macrofunción, donde compilaremos el nuevo


código de Rust que queremos incluir. Pero antes, tenga en cuenta que la salida de
nuestra macro deriva también es un archivo TokenStream. El
Rust
jueves, 22 de mayo de 2025 : Página 645 de 719

resultado TokenStreamse añade al código que escriben los usuarios de nuestro


crate, por lo que, al compilarlo, obtendrán la funcionalidad adicional que
proporcionamos en el archivo TokenStream.

Quizás hayas notado que estamos llamando unwrappara provocar que


la hello_macro_derivefunción entre en pánico si la llamada a la syn::parsefunción
falla. Es necesario que nuestra macro procedimental entre en pánico ante errores,
ya que proc_macro_derivelas funciones deben retornar TokenStreamen lugar
de Resultajustarse a la API de macros procedimentales. Hemos simplificado este
ejemplo usando unwrap; en código de producción, deberías proporcionar mensajes
de error más específicos sobre el problema usando panic!o expect.

Ahora que tenemos el código para convertir el código Rust anotado de una
instancia TokenStream a una DeriveInputinstancia, generemos el código que
implementa el HelloMacrorasgo en el tipo anotado, como se muestra en el Listado
20-34.

Nombre de archivo: hello_macro_derive/src/lib.rs


fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}
Listado 20-34: Implementación del HelloMacrorasgo usando el código Rust analizado

Obtenemos una Identinstancia de struct que contiene el nombre (identificador) del


tipo anotado usando ast.ident. La estructura del Listado 20-33 muestra que, al
ejecutar la impl_hello_macrofunción en el código del Listado 20-31, la identque
obtenemos tendrá el identcampo con el valor "Pancakes". Por lo tanto,
la namevariable del Listado 20-34 contendrá una Identinstancia de struct que, al
imprimirse, será la cadena "Pancakes", el nombre de la estructura del Listado 20-
31.

La quote!macro nos permite definir el código de Rust que queremos devolver. El


compilador espera algo diferente al resultado directo de la quote! ejecución de la
macro, por lo que necesitamos convertirlo a un TokenStream. Para ello, llamamos
Rust
jueves, 22 de mayo de 2025 : Página 646 de 719

al intométodo, que consume esta representación intermedia y devuelve un valor


del TokenStreamtipo requerido.

La quote!macro también ofrece una mecánica de plantillas muy interesante:


podemos introducir `` #namey quote!lo reemplazaremos con el valor de la variable
`` name. Incluso se pueden realizar repeticiones similares a las de las macros
convencionales. Consulta la quotedocumentación de la caja para obtener una
introducción completa.

Queremos que nuestra macro procedimental genere una implementación de


nuestro HelloMacro atributo para el tipo que el usuario anotó, lo cual podemos
obtener usando #name. La implementación del atributo tiene una
función hello_macro, cuyo cuerpo contiene la funcionalidad que queremos
proporcionar: imprimir Hello, Macro! My name isy, a continuación, el nombre del tipo
anotado.

La stringify!macro utilizada aquí está integrada en Rust. Toma una expresión de


Rust, como 1 + 2, y en tiempo de compilación la convierte en un literal de cadena,
como "1 + 2". Esto es diferente de las macros format!`or` println!, que evalúan la
expresión y luego convierten el resultado en un ` String. Existe la posibilidad de
que la #nameentrada sea una expresión para imprimir literalmente, por lo que
usamos ` stringify!. Usar stringify!también ahorra una asignación al
convertirla #nameen un literal de cadena en tiempo de compilación.

En este punto, cargo builddebería completarse correctamente tanto


en hello_macro como en hello_macro_derive. ¡Conectemos estos crates al código del
Listado 20-31 para ver la macro procedural en acción! Cree un nuevo proyecto
binario en su directorio de proyectos usando cargo new pancakes. Necesitamos
agregar hello_macroy hello_macro_derivecomo dependencias en el archivo
Cargo.tomlpancakes del crate . Si publica sus versiones de y en crates.io , serían
dependencias normales; de lo contrario, puede especificarlas como dependencias
de la siguiente manera:hello_macrohello_macro_derivepath

hello_macro = { path = "../hello_macro" }


hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

Coloque el código del Listado 20-31 en src/main.rs y ejecútelo cargo run: debería
imprimir Hello, Macro! My name is Pancakes!La implementación del HelloMacrorasgo de
la macro de procedimiento se incluyó sin que la pancakescaja necesitara
implementarlo;#[derive(HelloMacro)] se agregó la implementación del rasgo.
Rust
jueves, 22 de mayo de 2025 : Página 647 de 719

A continuación, exploraremos cómo los otros tipos de macros procedimentales se


diferencian de las macros derivadas personalizadas.

Macros similares a atributos

Las macros de tipo atributo son similares a las macros de derivación


personalizadas, pero en lugar de generar código para el deriveatributo, permiten
crear nuevos atributos. Además, son más flexibles: derivesolo funcionan con
estructuras y enumeraciones; los atributos también se pueden aplicar a otros
elementos, como funciones. Aquí tienes un ejemplo de uso de una macro de tipo
atributo: supongamos que tienes un atributo llamado routeque anota funciones al
usar un framework de aplicaciones web:

#[route(GET, "/")]
fn index() {

Este #[route]atributo se definiría en el marco como una macro procedimental. La


firma de la función de definición de 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 corresponde al


contenido del atributo: la GET, "/"parte. El segundo corresponde al cuerpo del
elemento al que está asociado el atributo: en este caso, fn index() {}el resto del
cuerpo de la función.

Aparte de eso, las macros de tipo atributo funcionan de la misma manera que las
macros derivadas personalizadas: creas un paquete con el proc-macrotipo de
paquete e implementas una función que genera el código que quieres.

Macros similares a funciones

Las macros de tipo función definen macros que se asemejan a llamadas a


funciones. Al igual que macro_rules!las macros, son más flexibles que las funciones;
por ejemplo, pueden aceptar un número indeterminado de argumentos. Sin
embargo, macro_rules!las macros solo se pueden definir utilizando la sintaxis de
coincidencia que explicamos en la sección "Macros declarativas macro_rules!para
metaprogramación general" . Las macros de tipo función
toman... TokenStream parámetro y su definición lo manipula TokenStream mediante
Rust
jueves, 22 de mayo de 2025 : Página 648 de 719

código Rust, al igual que los otros dos tipos de macros procedimentales. Un
ejemplo de una macro de tipo función es una sql!macro que podría llamarse así:

let sql = sql!(SELECT * FROM posts WHERE id=1);

Esta macro analizaría la declaración SQL dentro de ella y verificaría que sea
sintácticamente correcta, lo que es un procesamiento mucho más complejo que
una macro_rules! puede realizar una macro. La sql!macro se definiría así:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

Esta definición es similar a la firma de la macro derivada personalizada: recibimos


los tokens que están dentro de los paréntesis y devolvemos el código que
queríamos generar.

Resumen
¡Uf! Ahora tienes algunas funciones de Rust en tu caja de herramientas que
probablemente no uses a menudo, pero sabrás que están disponibles en
circunstancias muy particulares. Hemos presentado varios temas complejos para
que, cuando los encuentres en sugerencias de mensajes de error o en el código
de otros usuarios, puedas reconocer estos conceptos y su sintaxis. Usa este
capítulo como referencia para encontrar soluciones.

¡A continuación, pondremos en práctica todo lo que hemos comentado a lo largo


del libro y haremos un proyecto más!

Proyecto final: construcción de un


servidor web multiproceso
Ha sido un largo camino, pero hemos llegado al final del libro. En este capítulo,
crearemos un proyecto más juntos para demostrar algunos de los conceptos que
cubrimos en los capítulos finales, además de repasar algunas lecciones
anteriores.

Para nuestro proyecto final, crearemos un servidor web que diga "hola" y se vea
como la Figura 21-1 en un navegador web.
Rust
jueves, 22 de mayo de 2025 : Página 649 de 719

Figura 21-1: Nuestro proyecto compartido final

Aquí está nuestro plan para construir el servidor web:

1. Aprenda un poco sobre TCP y HTTP.


2. Escuche conexiones TCP en un socket.
3. Analizar una pequeña cantidad de solicitudes HTTP.
4. Crea una respuesta HTTP adecuada.
5. Mejore el rendimiento de nuestro servidor con un grupo de subprocesos.

Antes de empezar, debemos mencionar dos detalles: primero, el método que


usaremos no será el mejor para crear un servidor web con Rust. Los miembros de
la comunidad han publicado varios crates listos para producción disponibles
en crates.io. que ofrecen implementaciones de servidores web y grupos de hilos
más completas que las que crearemos. Sin embargo, nuestra intención en este
capítulo es ayudarte a aprender, no a tomar el camino fácil. Dado que Rust es un
lenguaje de programación de sistemas, podemos elegir el nivel de abstracción
con el que queremos trabajar e ir a un nivel inferior al que es posible o práctico en
otros lenguajes.

En segundo lugar, no usaremos async ni await aquí. Crear un grupo de


subprocesos ya es un gran desafío por sí solo, sin añadir la creación de un
entorno de ejecución asíncrono. Sin embargo, veremos cómo async y await
podrían aplicarse a algunos de los mismos problemas que veremos en este
Rust
jueves, 22 de mayo de 2025 : Página 650 de 719

capítulo. En definitiva, como mencionamos en el Capítulo 17, muchos entornos de


ejecución asíncronos utilizan grupos de subprocesos para gestionar su trabajo.

Por lo tanto, escribiremos manualmente el servidor HTTP básico y el grupo de


subprocesos para que pueda aprender las ideas y técnicas generales detrás de las
cajas que podría usar en el futuro.

Creación de un servidor web de un solo


subproceso
Comenzaremos por poner en funcionamiento un servidor web de un solo
subproceso. Antes de empezar, veamos un breve resumen de los protocolos
involucrados en la creación de servidores web. Los detalles de estos protocolos
exceden el alcance de este libro, pero un breve resumen le brindará la
información necesaria.

Los dos protocolos principales que intervienen en los servidores web son el
Protocolo de Transferencia de Hipertexto (HTTP) y el Protocolo de Control de
Transmisión (TCP) . Ambos protocolos son de solicitud-respuesta , lo que significa
que un cliente inicia las solicitudes y un servidor... las escucha y proporciona una
respuesta al cliente. El contenido de dichas solicitudes y respuestas está definido
por los protocolos.

TCP es el protocolo de nivel inferior que describe los detalles de cómo se


transmite la información de un servidor a otro, pero no especifica cuál es. 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 TCP y HTTP.

Escuchando la conexión TCP

Nuestro servidor web necesita escuchar una conexión TCP, así que esa es la
primera parte en la que trabajaremos. La biblioteca estándar ofrece
un std::netmódulo que nos permite hacerlo. Creemos un nuevo proyecto de la
forma habitual:

$ cargo new hello


Created binary (application) `hello` project
$ cd hello
Rust
jueves, 22 de mayo de 2025 : Página 651 de 719

Ahora introduzca el código del Listado 21-1 en src/main.rs para comenzar. Este
código escuchará en la dirección local 127.0.0.1:7878los flujos TCP entrantes. Al
recibir un flujo entrante, imprimirá "<sub>" (<sub>1</sub>) Connection
established!.

Nombre de archivo: src/main.rs


use std::net::TcpListener;

fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

for stream in listener.incoming() {


let stream = stream.unwrap();

println!("Connection established!");
}
}
Listado 21-1: Escuchar transmisiones entrantes e imprimir un mensaje cuando recibimos una
transmisión

Usando TcpListener, podemos escuchar conexiones TCP en la


dirección 127.0.0.1:7878. En la dirección, la sección antes de los dos puntos es una
dirección IP que representa su computadora (es la misma en todas las
computadoras y no representa específicamente la computadora de los autores)
y 7878es el puerto. Elegimos este puerto por dos razones: HTTP no se acepta
normalmente en este puerto, por lo que es poco probable que nuestro servidor
entre en conflicto con cualquier otro servidor web que pueda tener en ejecución
en su equipo, y 7878 es el tipo de rust en un teléfono.

bindEn este escenario, la función funciona de forma similar a newla anterior, ya


que devolverá una nueva TcpListenerinstancia. Se llama a esta función bind porque,
en redes, conectarse a un puerto para escuchar se conoce como "enlazar a un
puerto".

La bindfunción devuelve un Result<T, E>, lo que indica que es posible que la


vinculación falle. Por ejemplo, conectarse al puerto 80 requiere privilegios de
administrador (los usuarios sin privilegios de administrador solo pueden escuchar
en puertos superiores a 1023), por lo que si intentáramos conectarnos al puerto
80 sin ser administrador, la vinculación no funcionaría. La vinculación tampoco
funcionaría, por ejemplo, si ejecutáramos dos instancias de nuestro programa y,
por lo tanto, dos programas escucharan en el mismo puerto. Dado que estamos
desarrollando un servidor básico con fines de aprendizaje, no nos preocuparemos
Rust
jueves, 22 de mayo de 2025 : Página 652 de 719

por gestionar este tipo de errores; en su lugar, usaremos unwrappara detener el


programa si se producen errores.

El incomingmétodo on TcpListenerdevuelve un iterador que nos da una secuencia de


flujos (más específicamente, flujos de tipo TcpStream). Un solo flujo representa una
conexión abierta entre el cliente y el servidor. Una conexión es el nombre del
proceso completo de solicitud y respuesta en el que un cliente se conecta al
servidor, este genera una respuesta y el servidor cierra la conexión. Por lo tanto,
leeremos del TcpStreampara ver lo que envió el cliente y luego escribiremos
nuestra respuesta en el flujo para enviar los datos de vuelta al cliente. En
resumen, estofor bucle procesará cada conexión y generará una serie de flujos
que podremos gestionar.

Por ahora, nuestra gestión del flujo consiste en llamar unwrappara terminar
nuestro programa si el flujo presenta algún error; si no hay errores, el programa
imprime un mensaje. Añadiremos más funcionalidad para el caso de éxito en la
siguiente lista. La razón por la que podríamos recibir errores del incomingmétodo
cuando un cliente se conecta al servidor es que en realidad no estamos iterando
sobre las conexiones. En su lugar, estamos iterando sobre los intentos de
conexión . La conexión podría no ser exitosa por varias razones, muchas de ellas
específicas del sistema operativo. Por ejemplo, muchos sistemas operativos
tienen un límite en la cantidad de conexiones abiertas simultáneas que pueden
admitir; los nuevos intentos de conexión que superen ese número producirán un
error hasta que se cierren algunas de las conexiones abiertas.

¡Intentemos ejecutar este código! Invoque cargo runen la terminal y luego


cargue 127.0.0.1:7878 en un navegador web. El navegador debería mostrar un
mensaje de error como "Conexión restablecida", ya que el servidor no está
enviando datos. Sin embargo, al revisar la terminal, debería 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 varios mensajes impresos para una solicitud del navegador; el
motivo puede ser que el navegador esté realizando una solicitud para la página y
también para otros recursos, como el ícono favicon.ico que aparece en la pestaña
del navegador.
Rust
jueves, 22 de mayo de 2025 : Página 653 de 719

También podría ocurrir que el navegador intente conectarse al servidor varias


veces porque este no responde con datos. Cuando streamse sale del ámbito y se
descarta al final del bucle, la conexión se cierra como parte de
la dropimplementación. Los navegadores a veces gestionan las conexiones
cerradas reintentándolas, ya que el problema podría ser temporal. Lo importante
es que hemos conseguido un identificador para una conexión TCP.

Recuerda detener el programa presionando ctrl - c al terminar de ejecutar una


versión específica del código. Luego, reinícialo invocando el cargo runcomando
después de realizar cada conjunto de cambios para asegurarte de ejecutar el
código más reciente.

Leyendo la solicitud

Implementemos la función para leer la solicitud del navegador. Para evitar la


necesidad de obtener una conexión y luego tomar medidas con ella, crearemos
una nueva función para procesar conexiones. En esta
nueva handle_connectionfunción, leeremos datos del flujo TCP y los imprimiremos
para ver los datos que se envían desde el navegador. Modifique el código para
que se parezca al Listado 21-2.

Nombre de archivo: src/main.rs


use std::{
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
};

fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

for stream in listener.incoming() {


let stream = stream.unwrap();

handle_connection(stream);
}
}

fn handle_connection(mut stream: TcpStream) {


let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();

println!("Request: {http_request:#?}");
Rust
jueves, 22 de mayo de 2025 : Página 654 de 719
}
Listado 21-2: Lectura TcpStreame impresión de datos

Incorporamos ` std::io::preludeand` std::io::BufReaderal ámbito para acceder a los


atributos y tipos que nos permiten leer y escribir en el flujo. En el for bucle de
la mainfunción, en lugar de imprimir un mensaje que indica que se ha establecido
una conexión, ahora llamamos a la nueva handle_connectionfunción y le pasamos
`` stream.

En la handle_connectionfunción, creamos una nueva BufReaderinstancia que


encapsula una referencia a stream. Esto BufReaderañade almacenamiento en búfer
al gestionar las llamadas a los std::io::Readmétodos de rasgo.

Creamos una variable llamada http_requestpara 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 añadiendo la Vec<_>anotación de tipo.

BufReaderImplementa el std::io::BufReadatributo, que proporciona el lines método.


El linesmétodo devuelve un iterador de Result<String, std::io::Error>dividiendo el flujo
de datos cada vez que detecta un byte de nueva línea. Para obtener cada String,
asignamos y unwrapcada Result. Esto Result podría ser un error si los datos no son
UTF-8 válidos o si hubo un problema al leer el flujo. Nuevamente, un programa de
producción debería gestionar estos errores con mayor precisión, pero para
simplificar, hemos decidido detener el programa en caso de error.

El navegador indica el final de una solicitud HTTP enviando dos saltos de línea
consecutivos. Para obtener una solicitud del flujo, tomamos líneas hasta obtener
una que sea la cadena vacía. Una vez recopiladas las líneas en el vector, las
imprimimos con un formato de depuración optimizado para poder revisar las
instrucciones que el navegador web envía a nuestro servidor.

¡Probemos este código! Inicie el programa y vuelva a realizar una solicitud en un


navegador web. Tenga en cuenta que seguirá apareciendo una página de error en
el navegador, pero la salida de nuestro programa en la terminal será similar a
esta:

$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello`
Request: [
"GET / HTTP/1.1",
Rust
jueves, 22 de mayo de 2025 : Página 655 de 719
"Host: 127.0.0.1:7878",
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101
Firefox/99.0",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/
*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip, deflate, br",
"DNT: 1",
"Connection: keep-alive",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Cache-Control: max-age=0",
]

Dependiendo de tu navegador, podrías obtener un resultado ligeramente


diferente. Ahora que estamos imprimiendo los datos de la solicitud, podemos
entender por qué recibimos varias conexiones desde una misma solicitud del
navegador observando la ruta después GETde la primera línea de la solicitud. Si
todas las conexiones repetidas solicitan / , sabemos que el navegador intenta
obtener / repetidamente porque no recibe respuesta de nuestro programa.

Analicemos estos datos de solicitud para comprender qué le pide el navegador a


nuestro programa.

Una mirada más de cerca a una solicitud HTTP

HTTP es un protocolo basado en texto y una solicitud toma este formato:

Method Request-URI HTTP-Version CRLF


headers CRLF
message-body

La primera línea es la línea de solicitud , que contiene información sobre la


solicitud del cliente. La primera parte de la línea de solicitud indica
el método utilizado, como GETo POST, que describe cómo el cliente realiza la
solicitud. Nuestro cliente utilizó una GETsolicitud, lo que significa que solicita
información.

La siguiente parte de la línea de solicitud es / , que indica el Identificador


Uniforme de Recursos (URI) que el cliente solicita: un URI es prácticamente lo
mismo que un Localizador Uniforme de Recursos (URL) . La diferencia entre URI y
Rust
jueves, 22 de mayo de 2025 : Página 656 de 719

URL no es relevante para los fines de este capítulo, pero la especificación HTTP
usa el término URI, así que podemos sustituir "URI" por "URL".

La última parte corresponde a la versión HTTP que usa el cliente, y la línea de


solicitud termina con una secuencia CRLF . (CRLF significa retorno de
carro y avance de línea , términos de la época de las máquinas de escribir). La
secuencia CRLF también se puede escribir como \r\n, donde \res un retorno de
carro y \nes 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 al imprimir el CRLF, se ve
un inicio de nueva línea en lugar de \r\n.

Al observar los datos de la línea de solicitud que recibimos hasta ahora al ejecutar
nuestro programa, vemos que GETes el método, / es la URI de la solicitud
y HTTP/1.1es la versión.

Después de la línea de solicitud, las líneas restantes a partir de Host:ahora son


encabezados. GETLas solicitudes no tienen cuerpo.

Intente realizar 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 pide el navegador, ¡enviemos algunos datos!

Escribir una respuesta

Implementaremos el envío de datos en respuesta a una solicitud del cliente. Las


respuestas tienen el siguiente formato:

HTTP-Version Status-Code Reason-Phrase CRLF


headers CRLF
message-body

La primera línea es una línea de estado que contiene la versión HTTP utilizada en
la respuesta, un código de estado numérico que resume el resultado de la
solicitud y una frase de motivo que proporciona una descripción textual del código
de estado. Después de la secuencia CRLF se encuentran los encabezados, otra
secuencia CRLF y el cuerpo de la respuesta.
Rust
jueves, 22 de mayo de 2025 : Página 657 de 719

A continuación se muestra un ejemplo de respuesta que utiliza la versión HTTP


1.1, tiene un código de estado de 200, una frase de motivo OK, no tiene
encabezados ni cuerpo:

HTTP/1.1 200 OK\r\n\r\n

El código de estado 200 es la respuesta estándar de éxito. El texto es una breve


respuesta HTTP exitosa. ¡Escribámoslo en el flujo como respuesta a una solicitud
exitosa! En la handle_connectionfunción, elimine el println!que imprimía los datos de
la solicitud y reemplácelo con el código del Listado 21-3.

Nombre de archivo: src/main.rs


fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();

let response = "HTTP/1.1 200 OK\r\n\r\n";

stream.write_all(response.as_bytes()).unwrap();
}
Listado 21-3: Cómo escribir una pequeña respuesta HTTP exitosa a la transmisión

La primera línea nueva define la responsevariable que contiene los datos del
mensaje de éxito. Luego, llamamos as_bytesa `our` responsepara convertir los
datos de la cadena a bytes. El write_allmétodo `on` streamtoma `a` &[u8]y envía
esos bytes directamente a través de la conexión. Dado que la write_alloperación
podría fallar, usamos `on` unwrappara cualquier resultado de error, como antes.
En una aplicación real, se añadiría aquí la gestión de errores.

Con estos cambios, ejecutemos nuestro código y realicemos una solicitud. Ya no


imprimiremos datos en la terminal, por lo que solo veremos la salida de Cargo. Al
cargar 127.0.0.1:7878 en un navegador web, debería aparecer una página en
blanco en lugar de un error. ¡Acaba de codificar manualmente la recepción de una
solicitud HTTP y el envío de una respuesta!

Devolviendo HTML real

Implementemos la funcionalidad para devolver más de una página en blanco.


Cree el archivo hello.html en la raíz del directorio de su proyecto, no en el
Rust
jueves, 22 de mayo de 2025 : Página 658 de 719

directorio src . Puede introducir el HTML que desee; el Listado 21-4 muestra una
posibilidad.

Nombre del archivo: 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>
Listado 21-4: Un archivo HTML de muestra para devolver en una respuesta

Este es un documento HTML5 minimalista con un encabezado y texto. Para


devolverlo desde el servidor al recibir una solicitud, lo
modificaremos handle_connectioncomo se muestra en el Listado 21-5 para leer el
archivo HTML, añadirlo a la respuesta como cuerpo y enviarlo.

Nombre de archivo: src/main.rs


use std::{
fs,
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
};
// --snip--

fn handle_connection(mut stream: TcpStream) {


let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();

let status_line = "HTTP/1.1 200 OK";


let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();

let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

stream.write_all(response.as_bytes()).unwrap();
}
Listado 21-5: Envío del contenido de hello.html como cuerpo de la respuesta
Rust
jueves, 22 de mayo de 2025 : Página 659 de 719

Hemos ampliado fsla usedeclaración para incluir el módulo del sistema de archivos
de la biblioteca estándar. El código para leer el contenido de un archivo en una
cadena debería resultar familiar; lo usamos en el Capítulo 12 al leer el contenido
de un archivo para nuestro proyecto de E/S (Listado 12-4).

A continuación, format!añadimos el contenido del archivo como cuerpo de la


respuesta de éxito. Para garantizar una respuesta HTTP válida, añadimos
el Content-Lengthencabezado, que se establece con el tamaño del cuerpo de la
respuesta; en este caso, el tamaño de hello.html.

Ejecute este código cargo runy cargue 127.0.0.1:7878 en su navegador; ¡debería


ver su HTML renderizado!

Actualmente, ignoramos los datos de la solicitud http_requesty solo enviamos el


contenido del archivo HTML sin condiciones. Esto significa que si intentas
solicitar 127.0.0.1:7878/something-else en tu navegador, recibirás la misma
respuesta HTML. Actualmente, nuestro servidor es muy limitado y no realiza las
funciones de la mayoría de los servidores web. Queremos personalizar nuestras
respuestas según la solicitud y solo enviar el archivo HTML de una solicitud
correcta a / .

Validar la solicitud y responder selectivamente

Actualmente, nuestro servidor web devolverá el HTML del archivo


independientemente de la solicitud del cliente. Añadamos una función para
comprobar que el navegador solicita / antes de devolver el archivo HTML y
devolver un error si el navegador solicita algo más. Para ello, debemos
modificar handle_connection, como se muestra en el Listado 21-6. Este nuevo
código compara el contenido de la solicitud recibida con el aspecto que
conocemos de una solicitud de / y añade bloques ify elsepara tratar las solicitudes
de forma diferente.

Nombre de archivo: src/main.rs


// --snip--

fn handle_connection(mut stream: TcpStream) {


let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();

if request_line == "GET / HTTP/1.1" {


let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
Rust
jueves, 22 de mayo de 2025 : Página 660 de 719
let length = contents.len();

let response = format!(


"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);

stream.write_all(response.as_bytes()).unwrap();
} else {
// some other request
}
}
Listado 21-6: Manejo de solicitudes a / de manera diferente a otras solicitudes

Solo analizaremos la primera línea de la solicitud HTTP, así que, en lugar de leer la
solicitud completa en un vector, llamaremos a [nombre del nextiterador] para
obtener el primer elemento del iterador. El primer unwrapiterador se encarga de
[nombre del iterador] Optiony detiene el programa si no hay elementos en el
iterador. El segundo unwrapmaneja [nombre del iterador Result] y tiene el mismo
efecto que el unwrap[nombre del iterador] mapañadido en el Listado 21-2.

A continuación, comprobamos request_linesi coincide con la línea de solicitud de


una solicitud GET a la ruta / . De ser así, el ifbloque devuelve el contenido de
nuestro archivo HTML.

Si request_lineno coincide con la solicitud GET a la ruta / , significa que recibimos


otra solicitud. Agregaremos código al elsebloque en breve para responder a todas
las demás solicitudes.

Ejecute este código ahora y solicite 127.0.0.1:7878 ; debería obtener el HTML


en hello.html . Si realiza cualquier otra solicitud, como 127.0.0.1:7878/something-
else , obtendrá un error de conexión como los que vio al ejecutar el código en los
Listados 21-1 y 21-2.

Ahora, agreguemos el código del Listado 21-7 al elsebloque para devolver una
respuesta con el código de estado 404, que indica que no se encontró el
contenido de la solicitud. También devolveremos HTML para que una página se
visualice en el navegador indicando la respuesta al usuario final.

Nombre de archivo: src/main.rs


// --snip--
} else {
let status_line = "HTTP/1.1 404 NOT FOUND";
let contents = fs::read_to_string("404.html").unwrap();
let length = contents.len();
Rust
jueves, 22 de mayo de 2025 : Página 661 de 719
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);

stream.write_all(response.as_bytes()).unwrap();
}
Listado 21-7: Responder con el código de estado 404 y una página de error si se solicitó algo distinto
a/

Aquí, nuestra respuesta tiene una línea de estado con el código de estado 404 y
la frase de motivo NOT FOUND. El cuerpo de la respuesta será el HTML del
archivo 404.html . Deberá crear un archivo 404.html junto a hello.html para la
página de error; puede usar el HTML que desee o el HTML de ejemplo del Listado
21-8.

Nombre de archivo: 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>
Listado 21-8: Contenido de muestra para la página que se enviará con cualquier respuesta 404

Con estos cambios, vuelva a ejecutar el servidor. La


solicitud 127.0.0.1:7878 debería devolver el contenido de hello.html , y cualquier
otra solicitud, como 127.0.0.1:7878/foo , debería devolver el HTML de
error 404.html. .

Un toque de refactorización

Actualmente, los bloques if`y` elsepresentan mucha repetición: leen archivos y


escriben su contenido en el flujo. Las únicas diferencias son la línea de estado y el
nombre del archivo. Para simplificar el código, extraigamos estas diferencias en
líneas ` ify` independientes elseque asignarán los valores de la línea de estado y
el nombre del archivo a variables. De esta manera, podremos usar esas variables
incondicionalmente en el código para leer el archivo y escribir la respuesta. El
Listado 21-9 muestra el código resultante tras reemplazar los bloques ` ify` de
gran tamaño else.
Rust
jueves, 22 de mayo de 2025 : Página 662 de 719
Nombre de archivo: src/main.rs
// --snip--

fn handle_connection(mut stream: TcpStream) {


// --snip--

let (status_line, filename) = if request_line == "GET / HTTP/1.1" {


("HTTP/1.1 200 OK", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND", "404.html")
};

let contents = fs::read_to_string(filename).unwrap();


let length = contents.len();

let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

stream.write_all(response.as_bytes()).unwrap();
}
Listado 21-9: Refactorización de los ifbloques elsey para que contengan solo el código que difiere entre
los dos casos

Ahora los bloques ify elsesolo devuelven los valores apropiados para la línea de
estado y el nombre del archivo en una tupla; luego usamos la desestructuración
para asignar estos dos valores a status_liney filenameusando un patrón en
ellet declaración, como se analiza en el Capítulo 19.

El código previamente duplicado ahora está fuera de los ifbloques elsey y utiliza
el status_lineyfilename . Esto facilita la diferencia entre ambos casos y significa que
solo tenemos un lugar para actualizar el código si queremos cambiar el
funcionamiento de la lectura de archivos y la escritura de respuestas. El
comportamiento del código del Listado 21-9 será el mismo que el del Listado 21-
7.

¡Genial! Ahora tenemos un servidor web simple en aproximadamente 40 líneas de


código Rust que responde a una solicitud con una página de contenido y a todas
las demás con un error 404.

Actualmente, nuestro servidor funciona en un solo hilo, lo que significa que solo
puede atender una solicitud a la vez. Analicemos cómo esto puede ser un
problema simulando algunas solicitudes lentas. Luego, lo solucionaremos para
que nuestro servidor pueda procesar varias solicitudes a la vez.
Rust
jueves, 22 de mayo de 2025 : Página 663 de 719

Convertir nuestro servidor de un solo


subproceso en un servidor multiproceso
Actualmente, el servidor procesará cada solicitud por turno, lo que significa que
no procesará una segunda conexión hasta que la primera termine de procesarse.
Si el servidor recibiera cada vez más solicitudes, esta ejecución en serie sería
cada vez menos óptima. Si el servidor recibe una solicitud que tarda mucho en
procesarse, las solicitudes posteriores tendrán que esperar hasta que la solicitud
larga finalice, incluso si las nuevas solicitudes se pueden procesar rápidamente.
Tendremos que solucionar esto, pero primero, analizaremos el problema en
acción.

Simulación de una solicitud lenta en la implementación del


servidor actual

Analizaremos cómo una solicitud de procesamiento lento puede afectar a otras


solicitudes realizadas a nuestra implementación actual del servidor. El Listado 21-
10 implementa el manejo de una solicitud a /sleep con una respuesta lenta
simulada que hará que el servidor entre en suspensión durante 5 segundos antes
de responder.

Nombre de archivo: src/main.rs


use std::{
fs,
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
// --snip--

fn handle_connection(mut stream: TcpStream) {


// --snip--

let (status_line, filename) = match &request_line[..] {


"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};

// --snip--
}
Listado 21-10: Simulación de una solicitud lenta durmiendo durante 5 segundos
Rust
jueves, 22 de mayo de 2025 : Página 664 de 719

Pasamos de [a] ifa [a] y matchahora tenemos tres casos. Necesitamos hacer
coincidir explícitamente en un segmento de [ request_linea] para la coincidencia de
patrones con los valores literales de cadena; matchno realiza referencias ni
desreferencias automáticas como el método de igualdad.

El primer brazo es igual al ifbloque del Listado 21-9. El segundo brazo corresponde
a una solicitud con /sleep . Al recibirla, el servidor se suspenderá durante 5
segundos antes de renderizar la página HTML correctamente. El tercer brazo es
igual al elsebloque del Listado 21-9.

Puedes ver lo primitivo que es nuestro servidor: ¡las bibliotecas reales manejarían
el reconocimiento de múltiples solicitudes de una manera mucho menos
detallada!

Inicie el servidor usando cargo run. Luego abra dos ventanas del navegador: una
para http://127.0.0.1:7878/ y otra para http://127.0.0.1:7878/sleep . Si introduce
la URI / varias veces, como antes, verá que responde rápidamente. Pero si
introduce /sleep y luego carga / , verá que / espera hasta sleep se suspenda
durante los 5 segundos completos antes de cargarse.

Hay varias técnicas que podemos usar para evitar que las solicitudes se acumulen
detrás de una solicitud lenta, incluido el uso de async como hicimos en el Capítulo
17; lo que implementaremos es un grupo de subprocesos.

Mejorar el rendimiento con un grupo de subprocesos

Un grupo de subprocesos es un grupo de subprocesos generados, listos para


procesar una tarea. Cuando el programa recibe una nueva tarea, le asigna uno de
los subprocesos del grupo, quien la procesará. Los subprocesos restantes del
grupo están disponibles para procesar cualquier otra tarea que se reciba mientras
el primer subproceso la procesa. Cuando el primer subproceso termina de
procesar su tarea, regresa al grupo de subprocesos inactivos, listo para procesar
una nueva. Un grupo de subprocesos permite procesar conexiones
simultáneamente, lo que aumenta el rendimiento del servidor.

Limitaremos la cantidad de subprocesos en el grupo a una pequeña cantidad para


protegernos de ataques de denegación de servicio (DoS); si nuestro programa
creara un nuevo subproceso para cada solicitud a medida que llega, alguien que
realice 10 millones de solicitudes a nuestro servidor podría crear estragos al
Rust
jueves, 22 de mayo de 2025 : Página 665 de 719

utilizar todos los recursos de nuestro servidor y detener el procesamiento de


solicitudes.

En lugar de generar un número ilimitado de subprocesos, tendremos un número


fijo de subprocesos esperando en el grupo. Las solicitudes entrantes se envían al
grupo para su procesamiento. El grupo mantendrá una cola de solicitudes
entrantes. Cada subproceso del grupo extraerá una solicitud de esta cola, la
procesará y luego solicitará otra solicitud a la cola. Con este diseño, podemos
procesar hasta Nsolicitudes simultáneamente, dondeN es el número de
subprocesos. Si cada subproceso responde a una solicitud de larga duración, las
solicitudes posteriores pueden acumularse en la cola, pero hemos aumentado el
número de solicitudes de larga duración que podemos procesar antes de llegar a
ese punto.

Esta técnica es solo una de las muchas maneras de mejorar el rendimiento de un


servidor web. Otras opciones que podría explorar son el modelo de
bifurcación/unión , el modelo de E/S asíncrona de un solo subproceso o el modelo
de E/S asíncrona multiproceso . Si le interesa este tema, puede leer más sobre
otras soluciones e intentar implementarlas; con un lenguaje de bajo nivel como
Rust, todas estas opciones son posibles.

Antes de implementar un grupo de subprocesos, veamos cómo debería ser su


uso. Al diseñar código, escribir primero la interfaz de cliente puede guiar el
diseño. Escribe la API del código de forma que esté estructurada como quieres
llamarla; luego, implementa la funcionalidad dentro de esa estructura, en lugar de
implementar la funcionalidad y luego diseñar la API pública.

De forma similar a cómo usamos el desarrollo basado en pruebas en el proyecto


del Capítulo 12, aquí usaremos el desarrollo basado en compiladores.
Escribiremos el código que llama a las funciones deseadas y luego analizaremos
los errores del compilador para determinar qué debemos cambiar para que el
código funcione. Sin embargo, antes de hacerlo, exploraremos la técnica que no
usaremos como punto de partida.

Generando un hilo para cada solicitud

Primero, exploremos cómo se vería nuestro código si se creara un nuevo hilo para
cada conexión. Como se mencionó anteriormente, este no es nuestro plan final
debido a los problemas que conlleva la posibilidad de generar un número
Rust
jueves, 22 de mayo de 2025 : Página 666 de 719

ilimitado de hilos, pero es un punto de partida para obtener primero un servidor


multihilo funcional. Después, añadiremos el grupo de hilos como mejora, y
comparar las dos soluciones será más fácil. El Listado 21-11 muestra los cambios
necesarios mainpara generar un nuevo hilo que gestione cada flujo dentro
del forbucle.

Nombre de archivo: src/main.rs


fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

for stream in listener.incoming() {


let stream = stream.unwrap();

thread::spawn(|| {
handle_connection(stream);
});
}
}
Listado 21-11: Generar un nuevo hilo para cada transmisión

Como aprendiste en el Capítulo 16, thread::spawncrearás un nuevo hilo y luego


ejecutarás el código en el cierre. Si ejecutas este código y cargas /sleep en tu
navegador, y luego / en dos pestañas más, verás que las solicitudes a / no tienen
que esperar a /sleep. finalice. Sin embargo, como mencionamos, esto
eventualmente saturará el sistema, ya que estarías creando nuevos hilos sin
límite.

Quizás también recuerdes del Capítulo 17 que esta es precisamente la situación


donde async y await realmente destacan. Tenlo en cuenta al crear el grupo de
subprocesos y pensar en cómo se verían las cosas de manera diferente o similar
con async.

Creación de un número finito de subprocesos

Queremos que nuestro grupo de subprocesos funcione de forma similar y familiar,


de modo que cambiar de subprocesos a un grupo de subprocesos no requiera
grandes cambios en el código que utiliza nuestra API. El Listado 21-12 muestra la
interfaz hipotética para una ThreadPool estructura que queremos usar en lugar
de thread::spawn.

Nombre de archivo: src/main.rs

fn main() {
Rust
jueves, 22 de mayo de 2025 : Página 667 de 719
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);

for stream in listener.incoming() {


let stream = stream.unwrap();

pool.execute(|| {
handle_connection(stream);
});
}
}
Listado 21-12: Nuestra ThreadPoolinterfaz ideal

Usamos ThreadPool::newpara crear un nuevo grupo de subprocesos con un número


configurable de subprocesos, en este caso cuatro. Luego, en
el forbucle, pool.executetiene una interfaz similar: thread::spawntoma una clausura
que el grupo debe ejecutar para cada flujo. Necesitamos
implementarla pool.executepara que tome la clausura y se la asigne a un
subproceso del grupo para su ejecución. Este código aún no se compila, pero lo
intentaremos para que el compilador pueda guiarnos en la solución.

Construcción ThreadPoolmediante desarrollo impulsado por compiladores

Realice los cambios del Listado 21-12 en src/main.rs y luego usemos los errores
del compilador cargo checkpara guiar nuestro desarrollo. Este es el primer error
que recibimos:

$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
--> src/main.rs:11:16
|
11 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^ use of undeclared type `ThreadPool`

For more information about this error, try `rustc --explain E0433`.
error: could not compile `hello` (bin "hello") due to 1 previous error

¡Genial! Este error nos indica que necesitamos un ThreadPooltipo o módulo, así que
lo crearemos ahora. Nuestra ThreadPoolimplementación será independiente del
tipo de trabajo que esté realizando nuestro servidor web. Por lo tanto, cambiemos
el hellocontenedor de binarios a contenedor de biblioteca para alojar
nuestra ThreadPoolimplementación. Después de cambiar a contenedor de
biblioteca, también podríamos usar la biblioteca del grupo de subprocesos
independiente para cualquier trabajo que queramos realizar con un grupo de
subprocesos, no solo para atender solicitudes web.
Rust
jueves, 22 de mayo de 2025 : Página 668 de 719

Crea un src/lib.rs que contenga lo siguiente, que es la definición más simple de


una ThreadPoolestructura que podemos tener por ahora:

Nombre de archivo: src/lib.rs


pub struct ThreadPool;

Luego edite el archivo main.rs para traerlo ThreadPoolal alcance desde el cajón de
la biblioteca agregando el siguiente código en la parte superior de src/main.rs :

Nombre de archivo: src/main.rs


use hello::ThreadPool;

Este código aún no funciona, 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

Este error indica que, a continuación, debemos crear una función asociada
llamada newfor ThreadPool. También sabemos que newdebe tener un parámetro
que pueda aceptar 4como argumento y devolver una ThreadPoolinstancia.
Implementemos la newfunción más simple que tenga estas características:

Nombre de archivo: src/lib.rs


pub struct ThreadPool;

impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
}

Elegimos " usizecomo tipo de sizeparámetro" 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 usizesirve
este tipo, como se explica en la sección "Tipos enteros" del Capítulo 3.
Rust
jueves, 22 de mayo de 2025 : Página 669 de 719

Revisemos el código nuevamente:

$ 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

Ahora el error ocurre porque no tenemos un executemétodo en ThreadPool.


Recuerda que en la sección "Creación de un número finito de
subprocesos" decidimos que nuestro grupo de subprocesos debería tener una
interfaz similar a thread::spawn. Además, implementaremos la executefunción para
que tome la clausura dada y se la dé a un subproceso inactivo del grupo para su
ejecución.

Definiremos el executemétodo `on` ThreadPoolpara tomar un cierre como


parámetro. Recordemos que en la sección "Extracción de valores capturados del
cierre y los Fnrasgos" del capítulo 13, podemos tomar cierres como parámetros
con tres rasgos diferentes: Fn`, FnMut` y FnOnce`. Necesitamos decidir qué tipo de
cierre usar. Sabemos que terminaremos haciendo algo similar a
la thread::spawn implementación de la biblioteca estándar, así que podemos ver
qué límites thread::spawn tiene la firma de ` en su parámetro`. La documentación
nos muestra lo siguiente:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>


where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,

El Fparámetro de tipo es el que nos interesa aquí; este Tparámetro está


relacionado con el valor de retorno, y no nos interesa. Podemos ver
que spawnuses FnOncees el atributo enlazado a F. Probablemente esto también sea
lo que buscamos, ya que eventualmente pasaremos el argumento que
recibimos executea spawn. Podemos estar más seguros de que FnOncees el atributo
que queremos usar, ya que el hilo que ejecuta una solicitud solo ejecutará el
cierre de esa solicitud una vez, lo cual coincide con Oncein FnOnce.
Rust
jueves, 22 de mayo de 2025 : Página 670 de 719

El Fparámetro de tipo también tiene el límite de rasgo Sendy el límite de


duración 'static, que son útiles en nuestra situación: necesitamos Sendtransferir el
cierre de un hilo a otro y, 'staticdado que desconocemos cuánto tardará el hilo en
ejecutarse, creemos un executemétodo ThreadPoolque acepte un parámetro
genérico de tipo Fcon estos límites:

Nombre de archivo: src/lib.rs


impl ThreadPool {
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}

Todavía usamos ()after FnOnceporque esto FnOncerepresenta un cierre que no


toma parámetros y devuelve el tipo de unidad. () . Al igual que en las definiciones
de funciones, el tipo de retorno puede omitirse en la firma, pero incluso si no
tenemos parámetros, seguimos necesitando los paréntesis.

Nuevamente, esta es la implementación más simple del execute método: no hace


nada, pero solo intentamos que nuestro código compile. Comprobémoslo de
nuevo:

$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s

¡Compila! Pero ten en cuenta que si intentas cargo runrealizar una solicitud en el
navegador, verás los errores que vimos al principio del capítulo. ¡Nuestra
biblioteca execute aún no está llamando al cierre que se le pasó!

Nota: Un dicho que quizás escuches sobre lenguajes con compiladores estrictos, como Haskell y Rust,
es "si el código compila, funciona". Sin embargo, este dicho no es universalmente cierto. Nuestro
proyecto compila, ¡pero no hace absolutamente nada! Si estuviéramos desarrollando un proyecto real y
completo, este sería un buen momento para empezar a escribir pruebas unitarias para comprobar que el
código compila y se comporta como queremos.

Consideremos: ¿qué sería diferente aquí si fuéramos a ejecutar un futuro en lugar


de un cierre?

Validar el número de subprocesos ennew


Rust
jueves, 22 de mayo de 2025 : Página 671 de 719

No estamos haciendo nada con los parámetros de newy execute. Implementemos


los cuerpos de estas funciones con el comportamiento deseado. Para empezar,
pensemos en new. Anteriormente elegimos un tipo sin signo para
el size parámetro, ya que un grupo con un número negativo de subprocesos no
tiene sentido. Sin embargo, un grupo con cero subprocesos tampoco tiene
sentido, aunque cero es un perfectamente válido usize. Agregaremos código para
comprobar que sizees mayor que cero antes de devolver un .ThreadPool instancia y
para que el programa entre en pánico si recibe un cero mediante la assert!macro,
como se muestra en el Listado 21-13.

Nombre de archivo: 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--
}
Listado 21-13: ImplementaciónThreadPool::new para entrar en pánico si sizees cero

También hemos añadido documentación para nuestros ThreadPoolcomentarios de


documentación. Tenga en cuenta que seguimos las buenas prácticas de
documentación al añadir una sección que indica las situaciones en las que
nuestra función puede entrar en pánico, como se explica en el Capítulo 14.
Intente ejecutarcargo doc --open y hacer clic en la ThreadPoolestructura para ver
cómo se ve la documentación generada new.

En lugar de agregar la assert!macro como hicimos aquí, podríamos


cambiar new a buildy devolver un Resultcomo hicimos con Config::buildel proyecto
de E/S del Listado 12-9. Sin embargo, en este caso, hemos decidido que intentar
crear un grupo de subprocesos sin ningún subproceso sería un error
irrecuperable. Si se siente ambicioso, intente escribir una función buildcon la
siguiente firma para compararla con la newfunción:

pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> {


Rust
jueves, 22 de mayo de 2025 : Página 672 de 719
Creando espacio para almacenar los hilos

Ahora que sabemos que tenemos un número válido de subprocesos para


almacenar en el grupo, podemos crearlos y almacenarlos en
la ThreadPoolestructura antes de devolverla. Pero ¿cómo almacenamos un
subproceso? Analicemos la thread::spawnfirma:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>


where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,

La spawnfunción devuelve un JoinHandle<T>, donde Tes el tipo que devuelve el


cierre. Intentemos usar JoinHandle`too` y veamos qué sucede. En nuestro caso, los
cierres que pasamos al grupo de subprocesos gestionarán la conexión y no
devolverán nada, por lo que Tserán del tipo `unit`.() .

El código del Listado 21-14 se compilará, pero aún no crea ningún hilo. Hemos
modificado la definición de ThreadPoolpara que contenga un vector
de thread::JoinHandle<()>instancias, inicializado el vector con una capacidad de size,
configurado un forbucle que ejecutará código para crear los hilos y devuelto
una ThreadPoolinstancia que los contiene.

Nombre de archivo: src/lib.rs

use std::thread;

pub struct ThreadPool {


threads: Vec<thread::JoinHandle<()>>,
}

impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);

let mut threads = Vec::with_capacity(size);

for _ in 0..size {
// create some threads and store them in the vector
}

ThreadPool { threads }
}
// --snip--
}
Rust
jueves, 22 de mayo de 2025 : Página 673 de 719
Listado 21-14: Creación de un vector para ThreadPoolcontener los hilos

Hemos incluido std::threaden el alcance la biblioteca, porque estamos


usando thread::JoinHandlecomo tipo de elementos el vector en ThreadPool .

Una vez recibido un tamaño válido, ThreadPoolcrea un nuevo vector que puede
contener sizeelementos. La with_capacityfunción realiza la misma tarea
que, Vec::newpero con una diferencia importante: preasigna espacio en el vector.
Como sabemos que necesitamos almacenar sizeelementos en el vector, realizar
esta asignación por adelantado es ligeramente más eficiente que usar Vec::new,
que se redimensiona automáticamente a medida que se insertan los elementos.

Cuando lo ejecutes cargo checknuevamente, debería tener éxito.

Una Workerestructura responsable de enviar código desde ThreadPoola un


hilo

Dejamos un comentario en el forbucle del Listado 21-14 sobre la creación de hilos.


Aquí, veremos cómo crearlos. La biblioteca estándar proporciona thread::spawnuna
forma de crear hilos y thread::spawnespera obtener el código que el hilo debe
ejecutar en cuanto se crea. Sin embargo, en nuestro caso, queremos crear los
hilos y que esperen el código que enviaremos posteriormente. La implementación
de hilos de la biblioteca estándar no permite hacerlo; debemos implementarlo
manualmente.

Implementaremos este comportamiento introduciendo una nueva estructura de


datos entre los ThreadPoolhilos y que lo gestionarán. La llamaremos Worker , un
término común en las implementaciones de pooling. Worker selecciona el código
que debe ejecutarse y lo ejecuta en su hilo. Imaginemos a las personas que
trabajan en la cocina de un restaurante: los trabajadores esperan a que lleguen
los pedidos de los clientes y luego son responsables de tomarlos y prepararlos.

En lugar de almacenar un vector de JoinHandle<()>instancias en el grupo de


subprocesos, almacenaremos instancias de la Workerestructura. Cada
una Workeralmacenará una sola JoinHandle<()>instancia. Luego, implementaremos
un método Workerque tomará un cierre de código para ejecutarlo y lo enviará al
subproceso en ejecución. También asignaremos a cada trabajador un id para poder
distinguir entre los diferentes trabajadores del grupo al registrar o depurar.
Rust
jueves, 22 de mayo de 2025 : Página 674 de 719

Este es el nuevo proceso que se llevará a cabo al crear un <i> ThreadPool.


Implementaremos el código que envía el cierre al hilo después de
configurarlo Worker de esta manera:

1. Define una Workerestructura que contenga un idy un JoinHandle<()>.


2. Cambiar ThreadPoolpara contener un vector de Workerinstancias.
3. Define una Worker::newfunción que toma un idnúmero y devuelve
una Workerinstancia que contiene el idy un hilo generado con un cierre
vacío.
4. En ThreadPool::new, use el forcontador de bucle para generar un id, cree uno
nuevo Workercon ese idy almacene el trabajador en el vector.

Si está preparado para un desafío, intente implementar estos cambios por su


cuenta antes de mirar el código en el Listado 21-15.

¿Listo? Aquí está el Listado 21-15 con una forma de realizar las modificaciones
anteriores.

Nombre de archivo: src/lib.rs


use std::thread;

pub struct ThreadPool {


workers: Vec<Worker>,
}

impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);

let mut workers = Vec::with_capacity(size);

for id in 0..size {
workers.push(Worker::new(id));
}

ThreadPool { workers }
}
// --snip--
}

struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}

impl Worker {
Rust
jueves, 22 de mayo de 2025 : Página 675 de 719
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});

Worker { id, thread }


}
}
Listado 21-15: Modificación ThreadPoolpara contener Workerinstancias en lugar de contener subprocesos
directamente

Hemos cambiado el nombre del campo de ThreadPoola threadsporque workers ahora


contiene Workerinstancias en lugar de JoinHandle<()> instancias. Usamos el
contador en el forbucle como argumento de Worker::newy almacenamos cada
nuevo valor Workeren el vector llamado workers.

El código externo (como nuestro servidor en src/main.rs ) no necesita conocer los


detalles de implementación del uso de una Workerestructura dentro de [nombre
de la estructura] ThreadPool, por lo que la Workerestructura y su newfunción son
privadas. La Worker::newfunción usa la idinformación que le proporcionamos y
almacena una JoinHandle<()> instancia que se crea al generar un nuevo hilo con
una clausura vacía.

Nota: Si el sistema operativo no puede crear un hilo por falta de recursos, thread::spawnentrará en
pánico. Esto provocará un pánico en todo el servidor, aunque la creación de algunos hilos pueda ser
exitosa. Para simplificar, este comportamiento es correcto, pero en una implementación de un grupo de
hilos en producción, probablemente prefiera usar std::thread::Buildery su spawnmétodo que
retorna Result.

Este código se compilará y almacenará el número de Workerinstancias que


especificamos como argumento de ThreadPool::new. Sin embargo, aún no
procesamos el cierre obtenido en execute. Veamos cómo hacerlo a continuación.

Envío de solicitudes a hilos a través de canales

El siguiente problema que abordaremos es que los cierres


asignados thread::spawnno hacen absolutamente nada. Actualmente, obtenemos el
cierre que queremos ejecutar en el executemétodo. Pero necesitamos
asignar thread::spawnun cierre para que se ejecute al crear cada uno Workerdurante
la creación del...ThreadPool .

Queremos que las Workerestructuras que acabamos de crear obtengan el código


que se ejecutará desde una cola contenida en el ThreadPooly envíen ese código a
su hilo para ejecutarlo.
Rust
jueves, 22 de mayo de 2025 : Página 676 de 719

Los canales que aprendimos en el Capítulo 16 (una forma sencilla de comunicarse


entre dos subprocesos) serían perfectos para este caso práctico. Usaremos un
canal como cola de trabajos y executeenviaremos un trabajo desde la
instancia ThreadPoola las Workerinstancias, que a su vez lo enviarán a su
subproceso. El plan es el siguiente:

1. Crearán ThreadPoolun canal y mantendrán al remitente.


2. Cada uno Workerse quedará con el receptor.
3. Crearemos una nueva Jobestructura que contendrá los cierres que queremos
enviar por el canal.
4. El executemétodo enviará el trabajo que desea ejecutar a través del
remitente.
5. En su hilo, Workerrecorrerá su receptor y ejecutará los cierres de cualquier
trabajo que reciba.

Comencemos creando un canal ThreadPool::newy almacenando el remitente en


la ThreadPoolinstancia, como se muestra en el Listado 21-16. La Jobestructura no
contiene nada por ahora, pero será el tipo de elemento que enviaremos por el
canal.

Nombre de archivo: src/lib.rs


use std::{sync::mpsc, thread};

pub struct ThreadPool {


workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}

struct Job;

impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);

let (sender, receiver) = mpsc::channel();

let mut workers = Vec::with_capacity(size);

for id in 0..size {
workers.push(Worker::new(id));
}

ThreadPool { workers, sender }


}
// --snip--
Rust
jueves, 22 de mayo de 2025 : Página 677 de 719
}
Listado 21-16: Modificación ThreadPoolpara almacenar el remitente de un canal que
transmite Jobinstancias

En ThreadPool::new[nombre del canal], creamos nuestro nuevo canal y hacemos


que el grupo contenga al remitente. Esto se compilará correctamente.

Intentemos pasar un receptor del canal a cada trabajador a medida que el grupo
de subprocesos crea el canal. Sabemos que queremos usar el receptor en el
subproceso que generan los trabajadores, así que haremos referencia
al receiverparámetro en el cierre. El código del Listado 21-17 aún no compila del
todo.

Nombre de archivo: src/lib.rs

impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);

let (sender, receiver) = mpsc::channel();

let mut workers = Vec::with_capacity(size);

for id in 0..size {
workers.push(Worker::new(id, receiver));
}

ThreadPool { workers, sender }


}
// --snip--
}

// --snip--

impl Worker {
fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});

Worker { id, thread }


}
}
Listado 21-17: Pasando el receptor a los trabajadores

Hemos realizado algunos cambios pequeños y sencillos: pasamos el receptor


a Worker::newy luego lo usamos dentro del cierre.
Rust
jueves, 22 de mayo de 2025 : Página 678 de 719

Cuando intentamos verificar este código, obtenemos este error:

$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0382]: use of moved value: `receiver`
--> src/lib.rs:26:42
|
21 | let (sender, receiver) = mpsc::channel();
| -------- move occurs because `receiver` has type
`std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
25 | for id in 0..size {
| ----------------- inside of this loop
26 | workers.push(Worker::new(id, receiver));
| ^^^^^^^^ value moved here, in previous iteration of loop
|
note: consider changing this parameter type in method `new` to borrow instead if owning the
value isn't necessary
--> src/lib.rs:47:33
|
47 | fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
| --- in this method ^^^^^^^^^^^^^^^^^^^ this parameter takes ownership
of the value
help: consider moving the expression out of the loop so it is only moved once
|
25 ~ let mut value = Worker::new(id, receiver);
26 ~ for id in 0..size {
27 ~ workers.push(value);
|

For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello` (lib) due to 1 previous error

El código intenta pasar receivera varias Workerinstancias. Esto no funcionará, como


recordará del Capítulo 16: la implementación del canal que Rust proporciona es
de varios productores y un solo consumidor. . Esto significa que no podemos
simplemente clonar el extremo consumidor del canal para corregir este código.
Tampoco queremos enviar un mensaje varias veces a varios consumidores;
queremos una lista de mensajes con varios trabajadores, de modo que cada
mensaje se procese una sola vez.

Además, sacar un trabajo de la cola del canal implica mutar el receiver, por lo que
los subprocesos necesitan una forma segura de compartir y modificar receiver ; de
lo contrario, podríamos obtener condiciones de carrera (como se cubre en el
Capítulo 16).

Recuerde los punteros inteligentes seguros para subprocesos que se analizaron


en el Capítulo 16: para compartir la propiedad entre varios subprocesos y permitir
Rust
jueves, 22 de mayo de 2025 : Página 679 de 719

que estos modifiquen el valor, necesitamos usar Arc<Mutex<T>>. El Arctipo


permitirá que varios trabajadores sean propietarios del receptor
y Mutexgarantizará que solo un trabajador reciba una tarea del receptor a la vez.
El Listado 21-18 muestra los cambios necesarios.

Nombre de archivo: src/lib.rs


use std::{
sync::{mpsc, Arc, Mutex},
thread,
};
// --snip--

impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);

let (sender, receiver) = mpsc::channel();

let receiver = Arc::new(Mutex::new(receiver));

let mut workers = Vec::with_capacity(size);

for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}

ThreadPool { workers, sender }


}

// --snip--
}

// --snip--

impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
// --snip--
}
}
Listado 21-18: Compartir el receptor entre los trabajadores que utilizan ArcyMutex

En ThreadPool::new, colocamos el receptor en un Arcy un Mutex. Para cada nuevo


trabajador, clonamos el Arcpara aumentar el recuento de referencias, de modo
que los trabajadores puedan compartir la propiedad del receptor.

Con estos cambios, ¡el código compila! ¡Ya casi lo logramos!

Implementando el executemétodo
Rust
jueves, 22 de mayo de 2025 : Página 680 de 719

Finalmente, implementemos el executemétodo en ThreadPool. También


cambiaremos Jobde una estructura a un alias de tipo para un objeto de rasgo que
contiene el tipo de cierre que executerecibe. Como se explicó en la
sección "Creación de sinónimos de tipo con alias de tipo" del Capítulo 20, los alias
de tipo permiten acortar los tipos largos para facilitar su uso. Consulte el Listado
21-19.

Nombre de archivo: src/lib.rs


// --snip--

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
// --snip--

pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);

self.sender.send(job).unwrap();
}
}

// --snip--
Listado 21-19: Creación de un Jobalias de tipo para a Boxque contiene cada cierre y luego envío del
trabajo por el canal

Tras crear una nueva Jobinstancia con el cierre que obtenemos en [insertar
valor] execute, enviamos ese trabajo por el extremo emisor del canal.
Llamamos unwrapa send[insertar valor] en caso de que el envío falle. Esto podría
ocurrir si, por ejemplo, detenemos la ejecución de todos nuestros hilos, lo que
significa que el extremo receptor ha dejado de recibir mensajes nuevos.
Actualmente, no podemos detener la ejecución de nuestros hilos: estos continúan
ejecutándose mientras exista el grupo. La razón por la que usamos [insertar
valor] unwrapes que sabemos que el fallo no ocurrirá, pero el compilador lo
desconoce.

¡Pero aún no hemos terminado! En el trabajador, el cierre al que se


pasa thread::spawnsolo hace referencia al extremo receptor del canal. En su lugar,
necesitamos que el cierre se repita indefinidamente, solicitando un trabajo al
extremo receptor del canal y ejecutándolo cuando lo recibe. Realicemos el cambio
que se muestra en el Listado 21-20 a Worker::new.
Rust
jueves, 22 de mayo de 2025 : Página 681 de 719
Nombre de archivo: src/lib.rs
// --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();

println!("Worker {id} got a job; executing.");

job();
});

Worker { id, thread }


}
}
Listado 21-20: Recepción y ejecución de los trabajos en el hilo del trabajador

Aquí, primero llamamos locka [ nombre receiverdel hilo] para adquirir el mutex y
luego llamamos unwrapa [nombre del hilo] para entrar en pánico ante cualquier
error. La adquisición de un bloqueo podría fallar si el mutex está en estado de
envenenamiento , lo cual puede ocurrir si otro hilo entró en pánico mientras
mantenía el bloqueo en lugar de liberarlo. En esta situación, llamar unwrapa
[nombre del hilo] para que este hilo entre en pánico es la acción correcta. Puede
cambiar esto unwrapa [nombre del hilo]expect con un mensaje de error que le
resulte significativo.

Si obtenemos el bloqueo del mutex, llamamos recvpara recibir un Jobdel canal. Un


final unwraptambién ignora cualquier error, lo cual podría ocurrir si el hilo que
contiene al emisor se ha cerrado, de forma similar a cómo el send método
retorna Errsi el receptor se cierra.

La llamada a recvse bloquea, por lo que, si aún no hay un trabajo, el hilo actual
esperará hasta que haya uno disponible. Esto Mutex<T>garantiza que solo
un Workerhilo a la vez intente solicitar un trabajo.

¡Nuestro grupo de subprocesos ya está funcionando! Dale un vistazo cargo runy


realiza algunas solicitudes:

$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
warning: field `workers` is never read
--> src/lib.rs:7:5
|
6 | pub struct ThreadPool {
| ---------- field in this struct
Rust
jueves, 22 de mayo de 2025 : Página 682 de 719
7| workers: Vec<Worker>,
| ^^^^^^^
|
= note: `#[warn(dead_code)]` on by default

warning: fields `id` and `thread` are never read


--> src/lib.rs:48:5
|
47 | struct Worker {
| ------ fields in this struct
48 | id: usize,
| ^^
49 | thread: thread::JoinHandle<()>,
| ^^^^^^

warning: `hello` (lib) generated 2 warnings


Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.91s
Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.

¡Éxito! Ahora tenemos un grupo de subprocesos que ejecuta conexiones de forma


asíncrona. Nunca se crean más de cuatro subprocesos, por lo que nuestro sistema
no se sobrecargará si el servidor recibe muchas solicitudes. Si realizamos una
solicitud a /sleep , el servidor podrá atender otras solicitudes haciendo que otro
subproceso las ejecute.

Nota: Si abre /suspende varias ventanas del navegador simultáneamente, podrían cargarse una a la vez
en intervalos de 5 segundos. Algunos navegadores web ejecutan varias instancias de la misma solicitud
secuencialmente por razones de almacenamiento en caché. Esta limitación no se debe a nuestro
servidor web.

Es un buen momento para reflexionar sobre cómo cambiaría el código de los


Listados 21-18, 21-19 y 21-20 si usáramos futuros en lugar de un cierre para el
trabajo a realizar. ¿Qué tipos cambiarían? ¿Cómo cambiarían las firmas de los
métodos, si es que cambian? ¿Qué partes del código se mantendrían igual?
Rust
jueves, 22 de mayo de 2025 : Página 683 de 719

Después de aprender sobre el while letbucle en los Capítulos 17 y 18, quizás se


pregunte por qué no escribimos el código del hilo de trabajo como se muestra en
el Listado 21-21.

Nombre de archivo: src/lib.rs

// --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();
}
});

Worker { id, thread }


}
}
Listado 21-21: Una implementación alternativa del Worker::newusowhile let

Este código se compila y se ejecuta, pero no produce el comportamiento de


subprocesos deseado: una solicitud lenta seguirá provocando que otras
solicitudes esperen a ser procesadas. La razón es sutil: la Mutexestructura no
tiene unlockun método público porque la propiedad del bloqueo se basa en la vida
útil del objeto MutexGuard<T>dentro del LockResult<MutexGuard<T>>que
el lock método devuelve. En tiempo de compilación, el verificador de préstamos
puede aplicar la regla de que no se puede acceder a un recurso protegido por un
objeto Mutexa menos que mantengamos el bloqueo. Sin embargo, esta
implementación también puede provocar que el bloqueo se mantenga más
tiempo del previsto si no tenemos en cuenta la vida útil del
objeto. MutexGuard<T> .

El código del Listado 21-20 que usa let job =


receiver.lock().unwrap().recv().unwrap(); funciona porque con let, cualquier valor
temporal usado en la expresión a la derecha del signo igual se elimina
inmediatamente al letfinalizar la instrucción. Sin embargo, while let(y if lety match)
no elimina los valores temporales hasta el final del bloque asociado. En el Listado
21-21, el bloqueo permanece mientras se llama a job(), lo que significa que otros
trabajadores no pueden recibir trabajos.
Rust
jueves, 22 de mayo de 2025 : Página 684 de 719

Apagado y limpieza elegantes


El código del Listado 21-20 responde a las solicitudes de forma asíncrona
mediante un grupo de subprocesos, como era nuestra intención. Recibimos
advertencias sobre los campos workers, id, y threadque no usamos directamente, lo
que nos recuerda que no estamos limpiando nada. Al usar el método menos
elegante ctrl "-" c para detener el subproceso principal, todos los demás
subprocesos también se detienen inmediatamente, incluso si están atendiendo
una solicitud.

A continuación, implementaremos el Dropatributo para llamar joina cada uno de


los subprocesos del grupo de hilos para que puedan finalizar las solicitudes en las
que están trabajando antes de cerrar. Después, implementaremos una forma de
indicar a los subprocesos que dejen de aceptar nuevas solicitudes y se cierren.
Para ver este código en acción, modificaremos nuestro servidor para que acepte
solo dos solicitudes antes de cerrar correctamente su grupo de subprocesos.

Una cosa a tener en cuenta a medida que avanzamos: nada de esto afecta las
partes del código que manejan la ejecución de los cierres, por lo que todo aquí
sería igual si estuviéramos usando un grupo de subprocesos para un tiempo de
ejecución asincrónico.

Implementando el Droprasgo enThreadPool

Comencemos con la implementación Dropen nuestro grupo de subprocesos. Al


cerrar el grupo, todos nuestros subprocesos deben unirse para asegurar que
finalicen su trabajo. El Listado 21-22 muestra un primer intento
de Dropimplementación; este código aún no funcionará del todo.

Nombre de archivo: src/lib.rs

impl Drop for ThreadPool {


fn drop(&mut self) {
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);

worker.thread.join().unwrap();
}
}
}
Listado 21-22: Unirse a cada hilo cuando el grupo de hilos queda fuera del alcance
Rust
jueves, 22 de mayo de 2025 : Página 685 de 719

Primero, recorremos cada uno de los hilos del grupo de hilos workers. Usamos
[nombre del hilo &mut] para esto porque selfes una referencia mutable y también
necesitamos poder mutar [ workernombre del hilo]. Para cada trabajador,
imprimimos un mensaje indicando que este trabajador en particular se está
cerrando y luego llamamos joinal hilo de ese trabajador. Si la llamada joinfalla,
usamos [nombre del hilo]unwrap nombre del hilo ] para provocar que Rust entre
en pánico y se cierre de forma incorrecta.

Este es el error que obtenemos cuando compilamos este código:

$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference
--> src/lib.rs:52:13
|
52 | worker.thread.join().unwrap();
| ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call
| |
| move occurs because `worker.thread` has type `JoinHandle<()>`, which does not
implement the `Copy` trait
|
note: `JoinHandle::<T>::join` takes ownership of the receiver `self`, which moves
`worker.thread`
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/std/src/thread/
mod.rs:1763:17
|
1763 | pub fn join(self) -> Result<T> {
| ^^^^

For more information about this error, try `rustc --explain E0507`.
error: could not compile `hello` (lib) due to 1 previous error

El error indica que no podemos llamar joinporque solo tenemos un préstamo


mutable de `each` workery jointoma posesión de su argumento. Para solucionar
este problema, necesitamos mover el hilo fuera de la Workerinstancia que posee
` threadpara que joinpueda consumirlo. Una forma de hacerlo es siguiendo el
mismo enfoque que en el Listado 18-15. Si Workerse mantiene
`un` Option<thread::JoinHandle<()>>, podríamos llamar al takemétodo en
` Optionpara mover el valor fuera de la Some`variant` y dejar una ` Nonevariant`
en su lugar. En otras palabras, `un` Workerque se está ejecutando tendría una
` Somevariant` en ` thread, y cuando quisiéramos limpiar `un` Worker,
reemplazaríamos Somecon None`para que ` Workerno tenga un hilo que ejecutar`.

Sin embargo, esto solo ocurriría al eliminar el elemento Worker. A cambio,


tendríamos que lidiar con un Option<thread::JoinHandle<()>> elemento worker.thread.
Rust
jueves, 22 de mayo de 2025 : Página 686 de 719

Rust usa Optionbastantes elementos idiomáticos, pero cuando te encuentras


encapsulando algo Optioncomo solución temporal, aunque sabes que el elemento
siempre estará presente, es recomendable buscar enfoques alternativos. Estos
pueden hacer que tu código sea más limpio y menos propenso a errores.

En este caso, existe una mejor alternativa: el Vec::drainmétodo. Este acepta un


parámetro de rango para especificar qué elementos eliminar de [nombre de la
variable Vec] y devuelve un iterador de esos elementos. Al pasar la ..sintaxis de
rango, se eliminarán todos los valores de [nombre de la variable Vec].

Entonces necesitamos actualizar la ThreadPool dropimplementación de la siguiente


manera:

Nombre de archivo: src/lib.rs


impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);

worker.thread.join().unwrap();
}
}
}

Esto resuelve el error del compilador y no requiere ningún otro cambio en nuestro
código.

Señalización a los subprocesos para que dejen de escuchar


trabajos

Con todos los cambios realizados, nuestro código compila sin advertencias. Sin
embargo, la mala noticia es que aún no funciona como queremos. La clave reside
en la lógica de los cierres ejecutados por los hilos de las Worker instancias: por el
momento, llamamos a join, pero esto no cerrará los hilos, ya que loopbuscan
constantemente tareas. Si intentamos eliminar nuestro ThreadPoolcon nuestra
implementación actual de drop, el hilo principal se bloqueará indefinidamente
esperando a que el primer hilo termine.

Para solucionar este problema, necesitaremos un cambio en


la ThreadPool drop implementación y luego un cambio en el Workerbucle.

Primero, cambiaremos la ThreadPool dropimplementación para que elimine


explícitamente ``` senderantes de esperar a que finalicen los subprocesos. El
Rust
jueves, 22 de mayo de 2025 : Página 687 de 719

Listado 21-23 muestra los cambios para ThreadPooleliminar explícitamente


``` sender. A diferencia de ```` workers, aquí necesitamos usar ```` Optionpara
poder salir senderde ```` ThreadPoolcon `` Option::take``.

Nombre de archivo: src/lib.rs

pub struct ThreadPool {


workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
// --snip--
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
// --snip--

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();
}
}

impl Drop for ThreadPool {


fn drop(&mut self) {
drop(self.sender.take());

for worker in self.workers.drain(..) {


println!("Shutting down worker {}", worker.id);

worker.thread.join().unwrap();
}
}
}
Listado 21-23: Eliminar explícitamente senderantes de unirse a los subprocesos de trabajo

Al eliminar sender, se cierra el canal, lo que indica que no se enviarán más


mensajes. En este caso, todas las llamadas que recvrealizan los trabajadores en el
bucle infinito devolverán un error. En el Listado 21-24, modificamos
el Worker bucle para que salga correctamente en ese caso, lo que significa que los
subprocesos finalizarán cuando la ThreadPool dropimplementación joinlos llame.
Rust
jueves, 22 de mayo de 2025 : Página 688 de 719
Nombre de archivo: src/lib.rs
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 }


}
}
Listado 21-24: Salir explícitamente del bucle cuando recvse devuelve un error

Para ver este código en acción, modifiquémoslo mainpara aceptar solo dos
solicitudes antes de apagar correctamente el servidor, como se muestra en el
Listado 21-25.

Nombre de archivo: src/main.rs


fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);

for stream in listener.incoming().take(2) {


let stream = stream.unwrap();

pool.execute(|| {
handle_connection(stream);
});
}

println!("Shutting down.");
}
Listado 21-25: Apague el servidor después de atender dos solicitudes saliendo del bucle

No querrías que un servidor web real se apagara tras atender solo dos solicitudes.
Este código simplemente demuestra que el apagado y la limpieza correctos
funcionan correctamente.
Rust
jueves, 22 de mayo de 2025 : Página 689 de 719

El takemétodo se define en el Iteratorrasgo y limita la iteración a los dos primeros


elementos como máximo. ThreadPoolSaldrá del alcance al final de [Finalizar main]
y dropse ejecutará la implementación.

Inicie el servidor con cargo runy realice tres solicitudes. La tercera solicitud debería
generar un error y en su terminal debería ver un resultado similar a este:

$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/hello`
Worker 0 got a job; executing.
Shutting down.
Shutting down worker 0
Worker 3 got a job; executing.
Worker 1 disconnected; shutting down.
Worker 2 disconnected; shutting down.
Worker 3 disconnected; shutting down.
Worker 0 disconnected; shutting down.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3

Es posible que vea un orden diferente de trabajadores y mensajes impresos.


Podemos ver cómo funciona este código a partir de los mensajes: los trabajadores
0 y 3 recibieron las dos primeras solicitudes. El servidor dejó de aceptar
conexiones después de la segunda conexión, y
la Dropimplementación ThreadPoolcomienza a ejecutarse incluso antes de que el
trabajador 3 comience su trabajo. Al desconectar, senderse desconectan todos los
trabajadores y se les indica que se apaguen. Cada trabajador imprime un mensaje
al desconectarse, y luego el grupo de subprocesos llama joinpara esperar a que
finalice cada subproceso del trabajador.

Observe un aspecto interesante de esta ejecución en particular: ThreadPool se


eliminó el <nombre de la sendertarea> y, antes de que ningún trabajador
recibiera un error, intentamos unirnos al trabajador 0. El trabajador 0 aún no
había recibido un error de <nombre de la recvtarea>, por lo que el hilo principal
se bloqueó a la espera de que el trabajador 0 terminara. Mientras tanto, el
trabajador 3 recibió una tarea y, a continuación, todos los hilos recibieron un
error. Cuando el trabajador 0 terminó, el hilo principal esperó a que el resto de los
trabajadores terminaran. En ese momento, todos habían salido de sus bucles y se
detuvieron.
Rust
jueves, 22 de mayo de 2025 : Página 690 de 719

¡Felicidades! Hemos completado nuestro proyecto. Tenemos un servidor web


básico que usa un grupo de subprocesos para responder de forma asíncrona.
Podemos realizar un apagado ordenado del servidor, lo que limpia todos los
subprocesos del grupo.

Aquí está el código completo como referencia:

Nombre de archivo: src/main.rs


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);

for stream in listener.incoming().take(2) {


let stream = stream.unwrap();

pool.execute(|| {
handle_connection(stream);
});
}

println!("Shutting down.");
}

fn handle_connection(mut stream: TcpStream) {


let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();

let (status_line, filename) = match &request_line[..] {


"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};

let contents = fs::read_to_string(filename).unwrap();


let length = contents.len();

let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

stream.write_all(response.as_bytes()).unwrap();
Rust
jueves, 22 de mayo de 2025 : Página 691 de 719
}
Nombre de archivo: src/lib.rs
use std::{
sync::{mpsc, Arc, Mutex},
thread,
};

pub struct ThreadPool {


workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

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);

let (sender, receiver) = mpsc::channel();

let receiver = Arc::new(Mutex::new(receiver));

let mut workers = Vec::with_capacity(size);

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();
}
}

impl Drop for ThreadPool {


fn drop(&mut self) {
drop(self.sender.take());
Rust
jueves, 22 de mayo de 2025 : Página 692 de 719

for worker in &mut self.workers {


println!("Shutting down worker {}", worker.id);

if let Some(thread) = worker.thread.take() {


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í tienes
algunas ideas:

 Agregue más documentación a ThreadPooly sus métodos públicos.


 Agregar pruebas de la funcionalidad de la biblioteca.
 Cambiar las llamadas a unwrapun manejo de errores más robusto.
 Úselo ThreadPoolpara realizar alguna tarea que no sea atender solicitudes
web.
Rust
jueves, 22 de mayo de 2025 : Página 693 de 719

 Busca un contenedor de subprocesos en crates.io e implementa un servidor


web similar que lo utilice. Luego, compara su API y robustez con el
contenedor que implementamos.

Resumen
¡Bien hecho! ¡Has llegado al final del libro! Queremos agradecerte por
acompañarnos en este recorrido por Rust. Ya estás listo para implementar tus
propios proyectos en Rust y colaborar con los de otros. Recuerda que hay una
comunidad acogedora de otros Rustaceanos que estarán encantados de ayudarte
con cualquier desafío que encuentres en tu camino hacia Rust.

Apéndice
Las siguientes secciones contienen material de referencia que puede resultarle
útil en su experiencia en Rust.

Apéndice A: Palabras clave


La siguiente lista contiene palabras clave reservadas para uso actual o futuro en
el lenguaje Rust. Por lo tanto, no pueden usarse como identificadores (excepto
como identificadores sin formato, como se explicará en la sección
" Identificadores sin formato "). Los identificadores son nombres de funciones,
variables, parámetros, campos de estructura, módulos, cajas, constantes, macros,
valores estáticos, atributos, tipos, rasgos o tiempos de vida.

Palabras clave actualmente en uso

La siguiente es una lista de palabras clave actualmente en uso, con su


funcionalidad descrita.

 as- realizar conversiones primitivas, desambiguar el rasgo específico que


contiene un elemento o cambiar el nombre de elementos
en usedeclaraciones
 async- devuelve a Futureen lugar de bloquear el hilo actual
 await- suspender la ejecución hasta que el resultado de a Futureesté listo
 break- salir de un bucle inmediatamente
 const- definir elementos constantes o punteros sin procesar constantes
 continue- continuar con la siguiente iteración del bucle
Rust
jueves, 22 de mayo de 2025 : Página 694 de 719

 crate- en una ruta de módulo, hace referencia a la raíz del paquete


 dyn- envío dinámico a un objeto de rasgo
 else- construcciones de control ify respaldo de flujoif let
 enum- definir una enumeración
 extern- vincular una función o variable externa
 false- Literal falso booleano
 fn- definir una función o el tipo de puntero de función
 for- recorrer elementos de un iterador, implementar un rasgo o especificar
un tiempo de vida de mayor rango
 if- rama basada en el resultado de una expresión condicional
 impl- implementar funcionalidad inherente o de rasgo
 in- parte de forla sintaxis del bucle
 let- vincular una variable
 loop- bucle incondicional
 match- hacer coincidir un valor con patrones
 mod- definir un módulo
 move- hacer que un cierre se apropie de todas sus capturas
 mut- denota mutabilidad en referencias, punteros sin procesar o enlaces de
patrones
 pub- denota visibilidad pública en campos de estructura, implbloques o
módulos
 ref- enlazar por referencia
 return- regresar de la función
 Self- un alias de tipo para el tipo que estamos definiendo o implementando
 self- método sujeto o módulo actual
 static- variable global o duración de vida que dura toda la ejecución del
programa
 struct- definir una estructura
 super- módulo padre del módulo actual
 trait- definir un rasgo
 true- Literal booleano verdadero
 type- definir un alias de tipo o un tipo asociado
 union- define una unión ; solo es una palabra clave cuando se usa en una
declaración de unión
 unsafe- denotan código, funciones, características o implementaciones
inseguras
 use- incorporar símbolos al alcance; especificar capturas precisas para
límites genéricos y de duración
Rust
jueves, 22 de mayo de 2025 : Página 695 de 719

 where- denotan cláusulas que restringen un tipo


 while- bucle condicional basado en el resultado de una expresión

Palabras clave reservadas para uso futuro

Las siguientes palabras clave aún no tienen ninguna funcionalidad, pero Rust las
reserva para un posible uso futuro.

 abstract
 become
 box
 do
 final
 gen
 macro
 override
 priv
 try
 typeof
 unsized
 virtual
 yield

Identificadores sin procesar

Los identificadores sin formato son la sintaxis que permite usar palabras clave
donde normalmente no estarían permitidas. Se usan anteponiendo "<nombre del
identificador>" a una palabra clave r#.

Por ejemplo, matches una palabra clave. Si intenta compilar la siguiente función
que usa matchcomo nombre:

Nombre de archivo: src/main.rs

fn match(needle: &str, haystack: &str) -> bool {


haystack.contains(needle)
}

Recibirás este error:


Rust
jueves, 22 de mayo de 2025 : Página 696 de 719
error: expected identifier, found keyword `match`
--> src/main.rs:4:4
|
4 | fn match(needle: &str, haystack: &str) -> bool {
| ^^^^^ expected identifier, found keyword

El error indica que no se puede usar la palabra clave matchcomo identificador de


función. Para usarla matchcomo nombre de función, se debe usar la sintaxis del
identificador, como se muestra a continuación:

Nombre de archivo: src/main.rs

fn r#match(needle: &str, haystack: &str) -> bool {


haystack.contains(needle)
}

fn main() {
assert!(r#match("foo", "foobar"));
}

Este código se compilará sin errores. Tenga en cuenta el r#prefijo en el nombre


de la función en su definición, así como dónde se llama en main.

Los identificadores sin formato permiten usar cualquier palabra como


identificador, incluso si es una palabra clave reservada. Esto nos da mayor
libertad para elegir nombres de identificadores y nos permite la integración con
programas escritos en un lenguaje donde estas palabras no son palabras clave.
Además, permiten usar bibliotecas escritas en una edición de Rust diferente a la
que usa tu paquete. Por ejemplo, "is't" tryno es una palabra clave en la edición de
2015, pero sí en las ediciones de 2018, 2021 y 2024. Si dependes de una
biblioteca escrita en la edición de 2015 y tiene una tryfunción, deberás usar la
sintaxis de identificadores sin formato; r#tryen este caso, para llamar a esa
función desde el código de la edición de 2018. Consulta el Apéndice E para
obtener más información sobre las ediciones.

Apéndice B: Operadores y símbolos


Este apéndice contiene un glosario de la sintaxis de Rust, incluidos operadores y
otros símbolos que aparecen solos o en el contexto de rutas, genéricos, límites de
rasgos, macros, atributos, comentarios, tuplas y corchetes.

Operadores
Rust
jueves, 22 de mayo de 2025 : Página 697 de 719

La Tabla B-1 contiene los operadores en Rust, un ejemplo de cómo se vería el


operador en contexto, una breve explicación y si dicho operador es
sobrecargable. Si un operador es sobrecargable, se indica la característica
relevante para sobrecargarlo.

Tabla B-1: Operadores

Operad ¿Sobrecarga
Ejemplo Explicación
or ble?
! ident!(...), ident!{...},ident![...] Expansión macro
Complemento bit a
! !expr Not
bit o lógico
Comparación de
!= expr != expr PartialEq
desigualdades
% expr % expr resto aritmético Rem
Resto aritmético y
%= var %= expr RemAssign
asignación
& &expr,&mut expr Pedir prestado
&type, &mut type, &'a type,&'a Tipo de puntero
&
mut type prestado
& expr & expr AND bit a bit BitAnd
AND bit a bit y BitAndAssig
&= var &= expr
asignación n
Cortocircuito
&& expr && expr
lógico AND
Multiplicación
* expr * expr Mul
aritmética
Multiplicación y
*= var *= expr asignación MulAssign
aritmética
* *expr Desreferenciar Deref
Puntero sin
* *const type,*mut type
procesar
Restricción de tipo
+ trait + trait,'a + trait
compuesto
+ expr + expr Suma aritmética Add
Suma y asignación
+= var += expr AddAssign
aritmética
, expr, expr Separador de
argumentos y
Rust
jueves, 22 de mayo de 2025 : Página 698 de 719

Operad ¿Sobrecarga
Ejemplo Explicación
or ble?
elementos
Negación
- - expr Neg
aritmética
- expr - expr Resta aritmética Sub
Resta y asignación
-= var -= expr SubAssign
aritmética
Tipo de retorno de
-> fn(...) -> type,|…| -> type
función y cierre
Acceso para
. expr.ident
miembros
Literal de rango
.. .., expr.., ..expr,expr..expr exclusivo a la PartialOrd
derecha
Literal de rango
..= ..=expr,expr..=expr inclusivo a la PartialOrd
derecha
Sintaxis de
actualización de
.. ..expr
literales de
estructura
Patrón de
.. variant(x, ..),struct_type { x, .. } encuadernación “Y
el resto”
(Obsoleto,
úselo ..=en su
... expr...expr lugar) En un
patrón: patrón de
rango inclusivo
/ expr / expr División aritmética Div
División y
/= var /= expr asignación DivAssign
aritmética
: pat: type,ident: type Restricciones
Inicializador de
: ident: expr campo de
estructura
: 'a: loop {...} Etiqueta de bucle
; expr; Terminador de
declaración y
Rust
jueves, 22 de mayo de 2025 : Página 699 de 719

Operad ¿Sobrecarga
Ejemplo Explicación
or ble?
artículo
Parte de la sintaxis
; [...; len] de matriz de
tamaño fijo
Desplazamiento a
<< expr << expr Shl
la izquierda
Desplazamiento a
<<= var <<= expr la izquierda y ShlAssign
asignación
Menos que la
< expr < expr PartialOrd
comparación
Menor o igual a la
<= expr <= expr PartialOrd
comparación
Asignación/
= var = expr,ident = type
equivalencia
Comparación de
== expr == expr PartialEq
igualdad
Parte de la sintaxis
=> pat => expr del brazo de
coincidencia
Mayor que la
> expr > expr PartialOrd
comparación
Mayor o igual que
>= expr >= expr PartialOrd
la comparación
Desplazamiento a
>> expr >> expr Shr
la derecha
Desplazamiento a
>>= var >>= expr la derecha y ShrAssign
asignación
Encuadernación de
@ ident @ pat
patrones
OR exclusivo bit a
^ expr ^ expr BitXor
bit
OR exclusivo bit a
^= var ^= expr BitXorAssign
bit y asignación
Alternativas de
| pat | pat
patrones
| expr | expr OR bit a bit BitOr
|= var |= expr OR bit a bit y BitOrAssign
Rust
jueves, 22 de mayo de 2025 : Página 700 de 719

Operad ¿Sobrecarga
Ejemplo Explicación
or ble?
asignación
Cortocircuito
|| expr || expr
lógico OR
Propagación de
? expr?
errores

Símbolos que no son de operador

La siguiente lista contiene todos los símbolos que no funcionan como operadores;
es decir, no se comportan como una llamada de función o método.

La Tabla B-2 muestra símbolos que aparecen solos y son válidos en una variedad
de ubicaciones.

Tabla B-2: Sintaxis independiente

Símbolo Explicación
Etiqueta de duración de vida o de bucle con
'ident
nombre
...u8, ...i32, ...f64, ...usize, etc. Literal numérico de tipo específico
"..." literal de cadena
Literal de cadena sin formato, caracteres de escape
r"...", r#"..."#, r##"..."##, etc.
no procesados
Literal de cadena de bytes; construye una matriz
b"..."
de bytes en lugar de una cadena
Literal de cadena de bytes sin procesar,
br"...", br#"..."#, br##"..."##, etc. combinación de literal de cadena de bytes sin
procesar
'...' Carácter literal
b'...' literal de byte ASCII
|…| expr Cierre
Tipo de fondo siempre vacío para funciones
!
divergentes
Vinculación de patrones “ignorados”; también se
_
utiliza para hacer legibles los literales enteros

La Tabla B-3 muestra los símbolos que aparecen en el contexto de una ruta a
través de la jerarquía del módulo hasta un elemento.
Rust
jueves, 22 de mayo de 2025 : Página 701 de 719

Tabla B-3: Sintaxis relacionada con la ruta

Símbolo Explicación
ident::ident Ruta del espacio de nombres
Ruta relativa al preludio externo, donde se encuentran todas
::path las demás cajas (es decir, una ruta explícitamente absoluta
que incluye el nombre de la caja)
Ruta relativa al módulo actual (es decir, una ruta
self::path
explícitamente relativa).
super::path Ruta relativa al padre del módulo actual
type::ident,<type as
Constantes, funciones y tipos asociados
trait>::ident
Elemento asociado a un tipo que no se puede nombrar
<type>::...
directamente (por ejemplo, <&T>::..., <[T]>::..., etc.)
Desambiguación de una llamada de método nombrando el
trait::method(...)
rasgo que la define
Desambiguación de una llamada de método nombrando el
type::method(...)
tipo para el que está definido
<type as Desambiguación de una llamada de método nombrando el
trait>::method(...) rasgo y el tipo

La Tabla B-4 muestra los símbolos que aparecen en el contexto del uso de
parámetros de tipo genérico.

Tabla B-4: Genéricos

Símbolo Explicación
Especifica parámetros para un tipo genérico en un tipo
path<...>
(por ejemplo, Vec<u8>)
Especifica parámetros para un tipo, función o método
path::<...>,method::<...> genérico en una expresión; a menudo denominado
turbofish (por ejemplo, "42".parse::<i32>())
fn ident<...> ... Definir función genérica
struct ident<...> ... Definir estructura genérica
enum ident<...> ... Definir enumeración genérica
impl<...> ... Definir implementación genérica
for<...> type Límites de vida de mayor rango
Un tipo genérico donde uno o más tipos asociados tienen
type<ident=type> asignaciones específicas (por
ejemplo, Iterator<Item=T>)
Rust
jueves, 22 de mayo de 2025 : Página 702 de 719

La Tabla B-5 muestra los símbolos que aparecen en el contexto de restricción de


parámetros de tipo genérico con límites de rasgos.

Tabla B-5: Restricciones ligadas a rasgos

Símbolo Explicación
T: U Parámetro genérico Trestringido a tipos que implementanU
El tipo genérico Tdebe sobrevivir a la vida útil 'a(lo que significa que
T: 'a el tipo no puede contener de manera transitiva ninguna referencia con
una vida útil menor a 'a)
El tipo genérico Tno contiene referencias prestadas distintas
T: 'static
a 'staticlas
'b: 'a La vida útil genérica 'bdebe durar más que la vida útil'a
Permitir que el parámetro de tipo genérico sea un tipo de tamaño
T: ?Sized
dinámico
'a + trait,trait +
Restricción de tipo compuesto
trait

La Tabla B-6 muestra los símbolos que aparecen en el contexto de llamar o definir
macros y especificar atributos en un elemento.

Tabla B-6: Macros y atributos

Símbolo Explicación
#[meta] Atributo externo
#![meta] Atributo interno
$ident Sustitución macro
$ident:kind Captura de macro
$(…)… Repetición de macro
ident!(...), ident!{...},ident![...] Invocación de macro

La Tabla B-7 muestra símbolos que crean comentarios.

Tabla B-7: Comentarios

Símbolo Explicación
// Comentario de línea
//! Comentario del documento de línea interna
/// Comentario del documento de línea exterior
Rust
jueves, 22 de mayo de 2025 : Página 703 de 719

Símbolo Explicación
/*...*/ Bloquear comentario
/*!...*/ Comentario del documento del bloque interno
/**...*/ Comentario del documento del bloque externo

La Tabla B-8 muestra los símbolos que aparecen en el contexto del uso de tuplas.

Tabla B-8: Tuplas

Símbolo Explicación
Tupla vacía (también conocida como unidad), tanto literal como de
()
tipo
(expr) Expresión entre paréntesis
(expr,) Expresión de tupla de un solo elemento
(type,) Tipo de tupla de un solo elemento
(expr, ...) Expresión de tupla
(type, ...) Tipo de tupla
Expresión de llamada de función; también se utiliza para inicializar
expr(expr, ...)
tuplas structy enumvariantes de tuplas
expr.0, expr.1,
Indexación de tuplas
etc.

La Tabla B-9 muestra los contextos en los que se utilizan llaves.

Tabla B-9: Llaves

Contexto Explicación
{...} Expresión de bloque
Type {...} structliteral

En la Tabla B-10 se muestran los contextos en los que se utilizan corchetes.

Tabla B-10: Corchetes

Contexto Explicación
[...] literal de matriz
Matriz literal que contiene lencopias
[expr; len]
deexpr
[type; len] Tipo de matriz que
Rust
jueves, 22 de mayo de 2025 : Página 704 de 719

Contexto Explicación
contiene leninstancias detype
Indexación de colecciones.
expr[expr]
Sobrecargable ( Index, IndexMut)
Indexación de colecciones que simula
ser una segmentación de colecciones,
expr[..], expr[a..], expr[..b],expr[a..b]
utilizando Range, RangeFrom, RangeT
o, o RangeFullcomo el “índice”

Apéndice C: Rasgos derivables


En varias partes del libro, hemos hablado del deriveatributo, que se puede aplicar
a una definición de estructura o enumeración. El deriveatributo genera código que
implementará un atributo con su propia implementación predeterminada en el
tipo que se ha anotado con la derivesintaxis.

En este apéndice, proporcionamos una referencia de todos los rasgos de la


biblioteca estándar que puede usar con derive. Cada sección abarca:

 ¿Qué operadores y métodos permitirán derivar este rasgo?


 ¿Qué hace la implementación del rasgo proporcionado por derive?
 Qué significa la implementación del rasgo acerca del tipo
 Las condiciones en las que se le permite o no implementar el rasgo
 Ejemplos de operaciones que requieren el rasgo

Si desea un comportamiento diferente al proporcionado por el deriveatributo,


consulte la documentación de la biblioteca estándar de cada característica para
obtener detalles sobre cómo implementarlos manualmente.

Estos rasgos aquí listados son los únicos definidos por la biblioteca estándar que
pueden implementarse en sus tipos usando derive. Otros rasgos definidos en la
biblioteca estándar no tienen un comportamiento predeterminado sensato, por lo
que depende de usted implementarlos de la forma que mejor se adapte a su
objetivo.

Un ejemplo de un atributo que no se puede derivar es Display, que gestiona el


formato para los usuarios finales. Siempre debe considerar la forma adecuada de
mostrar un tipo a un usuario final. ¿Qué partes del tipo debería poder ver un
usuario final? ¿Qué partes le resultarían relevantes? ¿Qué formato de los datos
sería el más relevante para él? El compilador de Rust no tiene esta información,
Rust
jueves, 22 de mayo de 2025 : Página 705 de 719

por lo que no puede proporcionarle un comportamiento predeterminado


adecuado.

La lista de rasgos derivables que se proporciona en este apéndice no es


exhaustiva: las bibliotecas pueden implementar derivesus propios rasgos, lo que
hace que la lista de rasgos que se pueden usar derivesea realmente abierta. La
implementación derive implica el uso de una macro procedimental, que se trata en
la sección "Macros" del Capítulo 20.

Debugpara la salida del programador

Esta Debugcaracterística permite el formato de depuración en cadenas de


formato, lo cual se indica al agregarlas :?dentro de {}marcadores de posición.

La Debugcaracterística le permite imprimir instancias de un tipo para fines de


depuración, de modo que usted y otros programadores que usan su tipo puedan
inspeccionar una instancia en un punto particular de la ejecución de un programa.

El Debugatributo es necesario, por ejemplo, al usar la assert_eq!macro. Esta macro


imprime los valores de las instancias dadas como argumentos si la afirmación de
igualdad falla, de modo que los programadores puedan ver por qué las dos
instancias no eran iguales.

PartialEqy Eqpara comparaciones de igualdad

El PartialEqrasgo le permite comparar instancias de un tipo para verificar la


igualdad y habilita el uso de los operadores ==y .!=

La derivación PartialEqimplementa el eqmétodo. Cuando PartialEqse deriva de


estructuras, dos instancias son iguales solo si todos los campos son iguales, y las
instancias no son iguales si algún campo no lo es. Cuando se deriva de
enumeraciones, cada variante es igual a sí misma y no a las demás.

El PartialEqrasgo es necesario, por ejemplo, con el uso de la assert_eq!macro, que


debe poder comparar dos instancias de un tipo para determinar su igualdad.

El Eqrasgo no tiene métodos. Su propósito es indicar que, para cada valor del tipo
anotado, este es igual a sí mismo. El Eqrasgo solo se puede aplicar a tipos que
también implementan ``` PartialEq, aunque no todos los tipos que implementan
``` PartialEqpueden implementar `` Eq`. Un ejemplo de esto son los tipos de
Rust
jueves, 22 de mayo de 2025 : Página 706 de 719

números de punto flotante: la implementación de los números de punto flotante


establece que dos instancias del valor ``` NaN` no son iguales entre sí.

Un ejemplo de cuándo Eqes necesario es para las claves en a, HashMap<K, V>de


modo que se HashMap<K, V>pueda determinar si dos claves son iguales.

PartialOrdy Ordpara ordenar comparaciones

El PartialOrdrasgo permite comparar instancias de un tipo para ordenar. Un tipo


que implementa `` PartialOrdse puede usar con los operadores ` <`, >``, <=`` y
` >=`. Solo se puede aplicar el PartialOrdrasgo a tipos que también
implementan PartialEq``.

La derivación PartialOrdimplementa el partial_cmpmétodo, que devuelve


un Option<Ordering>valor que se generará Nonecuando los valores dados no
generen un orden. Un ejemplo de un valor que no genera un orden, aunque la
mayoría de los valores de ese tipo se pueden comparar, es el valor de NaNpunto
flotante `not-a-number()`. Al llamar partial_cmpcon cualquier número de punto
flotante y el NaN valor de punto flotante, se devolverá None`.`.

Cuando se deriva de estructuras, PartialOrdcompara dos instancias comparando el


valor de cada campo en el orden en que aparecen en la definición de la
estructura. Cuando se deriva de enumeraciones, las variantes de la enumeración
declaradas anteriormente en la definición se consideran menores que las
variantes enumeradas posteriormente.

El PartialOrdrasgo es necesario, por ejemplo, para el gen_rangemétodo del randcajón


que genera un valor aleatorio en el rango especificado por una expresión de
rango.

El Ordrasgo permite saber que, para dos valores cualesquiera del tipo anotado,
existirá un orden válido. El Ordrasgo implementa el cmpmétodo, que devuelve
un Orderingen lugar de un Option<Ordering>porque siempre será posible un orden
válido. Solo se puede aplicar el Ordrasgo a tipos que también
implementan PartialOrdy Eq(y Eqrequieren PartialEq). Cuando se deriva de
estructuras y enumeraciones, cmpse comporta igual que la implementación
derivada de for partial_cmpcon PartialOrd.
Rust
jueves, 22 de mayo de 2025 : Página 707 de 719

Un ejemplo de cuándo Ordes necesario es cuando se almacenan valores en


una BTreeSet<T>, una estructura de datos que almacena datos según el orden de
clasificación de los valores.

Cloney Copypara duplicar valores

ElClone rasgo 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 del montón. Consulte la sección "Formas en que interactúan las
variables y los datos: Clonar" en el Capítulo 4 para obtener más información
sobre Clone.

Deriving Cloneimplementa el clonemétodo, que cuando se implementa para todo el


tipo, llamaclone cada una de sus partes. Esto significa que todos los campos o
valores del tipo también deben implementarse Clonepara derivar Clone.

Un ejemplo de cuándo Clonees necesario es al llamar al to_vecmétodo en una


porción. La porción no posee las instancias de tipo que contiene, pero el vector
devuelto to_vecsí deberá poseer sus instancias, por lo que to_vec se
invoca clonecada elemento. Por lo tanto, el tipo almacenado en la porción debe
implementar Clone.

Esta Copycaracterística permite duplicar un valor copiando únicamente los bits


almacenados en la pila; no se necesita código arbitrario. Consulte sección "Datos
de solo pila: Copiar" en el Capítulo 4 para obtener más información sobre Copy.

El Copyrasgo no define ningún método para evitar que los programadores lo


sobrecarguen y violen el supuesto de que no se ejecuta código arbitrario. De esta
forma, todos los programadores pueden asumir que copiar un valor será muy
rápido.

Se puede derivar Copyde cualquier tipo cuyas partes implementen Copy. Un tipo
que implementa Copytambién debe implementar Clone, porque un tipo que
implementa Copytiene una implementación trivial deClone que realiza la misma
tarea que Copy.

El Copyrasgo rara vez se requiere; tipos que lo implementan Copy tienen


optimizaciones disponibles, lo que significa que no es necesario llamar a clone, lo
que hace que el código sea más conciso.
Rust
jueves, 22 de mayo de 2025 : Página 708 de 719

Todo lo que es posible con Copyusted también se puede lograr con Clone, pero el
código puede ser más lento o tener que usar clone en algunos lugares.

Hashpara asignar un valor a un valor de tamaño fijo

El Hashatributo permite tomar una instancia de un tipo de tamaño arbitrario y


asignarla a un valor de tamaño fijo mediante una función hash. La
derivación Hashimplementa el hashmétodo. La implementación derivada
del hash método combina el resultado de la llamada hasha cada una de las partes
del tipo, lo que significa que todos los campos o valores también deben
implementarse Hashpara derivar.Hash .

Un ejemplo de cuándo Hashes necesario es al almacenar claves para HashMap<K,


V> almacenar datos de manera eficiente.

Defaultpara valores predeterminados

El Defaultatributo permite crear un valor predeterminado para un tipo. La


derivación Defaultimplementa la defaultfunción. La implementación derivada de
la defaultfunción la llama defaulten cada parte del tipo, lo que significa que todos
los campos o valores del tipo también deben implementarse Defaultpara
derivar.Default .

La Default::defaultfunción se utiliza comúnmente en combinación con la sintaxis de


actualización de estructura analizada en la sección “Creación de instancias a
partir de otras instancias con la sintaxis de actualización de estructura” en el
Capítulo 5. Puede personalizar algunos campos de una estructura y luego
establecer y usar un valor predeterminado para el resto de los campos
usando ..Default::default().

El Defaultatributo es obligatorio al usar el


método unwrap_or_defaulten Option<T>instancias, por ejemplo. Si Option<T>es None,
el método unwrap_or_defaultdevolverá el resultado Default::defaultpara el
tipo Talmacenado en Option<T>.

Apéndice D - Herramientas de desarrollo útiles


En este apéndice, hablamos de algunas herramientas de desarrollo útiles que
ofrece el proyecto Rust. Analizaremos el formateo automático, métodos rápidos
para aplicar correcciones de advertencias, un linter y la integración con IDE.
Rust
jueves, 22 de mayo de 2025 : Página 709 de 719

Formato automático conrustfmt

La rustfmtherramienta reformatea tu código según el estilo de código de la


comunidad. Muchos proyectos colaborativos usan Rust rustfmtpara evitar
discusiones sobre el estilo: todos formatean su código con la herramienta.

Para instalar rustfmt, ingrese lo siguiente:

$ rustup component add rustfmt

Este comando te proporciona rustfmty cargo-fmt, de forma similar a cómo Rust te


proporciona rustcy cargo. Para formatear cualquier proyecto de Cargo, introduce lo
siguiente:

$ cargo fmt

Al ejecutar este comando, se reformatea todo el código de Rust en el crate actual.


Esto solo debería cambiar el estilo del código, no su semántica. Para más
información sobre rustfmt, consulta su documentación .

Arregla tu código conrustfix

La herramienta rustfix se incluye con las instalaciones de Rust y corrige


automáticamente las advertencias del compilador, lo que indica claramente que
el problema es lo que buscas. Probablemente ya hayas visto advertencias del
compilador. Por ejemplo, considera este código:

Nombre de archivo: src/main.rs

fn main() {
let mut x = 42;
println!("{x}");
}

Aquí, definimos una variable xcomo mutable, pero nunca la mutamos. Rust nos
advierte al respecto:

$ cargo build
Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: variable does not need to be mutable
--> src/main.rs:2:9
|
2| let mut x = 0;
| ----^
| |
Rust
jueves, 22 de mayo de 2025 : Página 710 de 719
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default

La advertencia sugiere eliminar la mutpalabra clave. Podemos aplicar esta


sugerencia automáticamente con la rustfixherramienta 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 volvamos a ver src/main.rs , veremos que cargo fixse ha cambiado el


código:

Nombre de archivo: src/main.rs

fn main() {
let x = 42;
println!("{x}");
}

La xvariable ahora es inmutable y la advertencia ya no aparece.

También puedes usar el cargo fixcomando para cambiar tu código entre diferentes
ediciones de Rust. Las ediciones se describen en el Apéndice E.

Más pelusas con Clippy

La herramienta Clippy es una colección de pelusas para analizar tu código para


que puedas detectar errores comunes y mejorar tu código Rust.

Para instalar Clippy, ingrese lo siguiente:

$ rustup component add clippy

Para ejecutar los lints de Clippy en cualquier proyecto de Cargo, ingrese lo


siguiente:

$ cargo clippy

Por ejemplo, digamos que escribes un programa que utiliza una aproximación de
una constante matemática, como pi, como lo hace este programa:
Rust
jueves, 22 de mayo de 2025 : Página 711 de 719

Nombre de archivo: src/main.rs

fn main() {
let x = 3.1415;
let r = 8.0;
println!("the area of the circle is {}", x * r * r);
}

Al ejecutar cargo clippyeste proyecto se produce este error:

error: approximate value of `f{32, 64}::consts::PI` found


--> src/main.rs:2:13
|
2| let x = 3.1415;
| ^^^^^^
|
= note: `#[deny(clippy::approx_constant)]` on by default
= help: consider using the constant directly
= help: for further information visit
https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant

Este error te indica que Rust ya tiene PIdefinida una constante más precisa y que
tu programa funcionaría mejor si la usaras. Entonces, modificarías tu código para
usarla PI. El siguiente código no genera errores ni advertencias de Clippy:

Nombre de archivo: 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);
}

Para obtener más información sobre Clippy, consulte su documentación .

Integración IDE usandorust-analyzer

Para facilitar la integración con IDE, la comunidad de Rust recomienda usar


[Nombre del proyecto] rust-analyzer. Esta herramienta es un conjunto de utilidades
centradas en el compilador que utiliza el Protocolo de Servidor de Lenguaje (LSP) ,
una especificación para la comunicación entre IDE y lenguajes de programación.
Diferentes clientes pueden usar [Nombre del proyecto] rust-analyzer, como el
complemento Rust Analyzer para Visual Studio Code .

Visita la página principalrust-analyzer del proyecto para obtener las instrucciones


de instalación y luego instala la compatibilidad con el servidor de idiomas en tu
Rust
jueves, 22 de mayo de 2025 : Página 712 de 719

IDE. Tu IDE incorporará funciones como autocompletado, salto a la definición y


errores en línea.

Apéndice E - Ediciones
En el Capítulo 1, viste que cargo newse agregan metadatos a tu
archivo Cargo.toml sobre una edición. ¡Este apéndice explica qué significa eso!

El lenguaje y el compilador Rust tienen un ciclo de lanzamiento de seis semanas,


lo que significa que los usuarios reciben un flujo constante de nuevas
características. Otros lenguajes de programación lanzan cambios más
importantes con menos frecuencia; Rust publica actualizaciones más pequeñas
con mayor frecuencia. Con el tiempo, todos estos pequeños cambios se
acumulan. Pero de una versión a otra, puede ser difícil mirar atrás y decir:
"¡Guau! Entre Rust 1.10 y Rust 1.31, ¡Rust ha cambiado muchísimo!".

Cada dos o tres años, el equipo de Rust publica una nueva edición de Rust . Cada
edición reúne las características implementadas en un paquete claro, con
documentación y herramientas completamente actualizadas. Las nuevas
ediciones se lanzan como parte del proceso habitual de lanzamiento de seis
semanas.

Las ediciones sirven para distintos propósitos según la persona:

 Para los usuarios activos de Rust, una nueva edición reúne cambios
incrementales en un paquete fácil de entender.
 Para los que no son usuarios, una nueva edición indica que se han
producido algunos avances importantes que podrían hacer que valga la
pena darle otra mirada a Rust.
 Para aquellos que desarrollan Rust, una nueva edición proporciona un punto
de encuentro para el proyecto en su conjunto.

Al momento de escribir este artículo, hay cuatro ediciones de Rust disponibles:


Rust 2015, Rust 2018, Rust 2021 y Rust 2024. Este libro está escrito utilizando los
modismos de la edición Rust 2024.

La editionclave en Cargo.toml indica qué edición debe usar el compilador para tu


código. Si la clave no existe, Rust usa 2015 como valor de edición por
compatibilidad con versiones anteriores.
Rust
jueves, 22 de mayo de 2025 : Página 713 de 719

Cada proyecto puede optar por una edición distinta a la predeterminada de 2015.
Las ediciones pueden contener cambios incompatibles, como la inclusión de una
nueva palabra clave que entre en conflicto con los identificadores del código. Sin
embargo, a menos que acepte estos cambios, su código seguirá compilándose
incluso si actualiza la versión del compilador de Rust que utiliza.

Todas las versiones del compilador Rust son compatibles con cualquier edición
anterior a su lanzamiento y pueden vincular crates de cualquier edición
compatible. Los cambios de edición solo afectan la forma en que el compilador
analiza el código inicialmente. Por lo tanto, si usas Rust 2015 y una de tus
dependencias usa Rust 2018, tu proyecto compilará y podrá usarla. La situación
contraria, donde tu proyecto usa Rust 2018 y una dependencia usa Rust 2015,
también funciona.

Para ser claros: la mayoría de las funciones estarán disponibles en todas las
ediciones. Los desarrolladores que usen cualquier edición de Rust seguirán viendo
mejoras a medida que se publiquen nuevas versiones estables. Sin embargo, en
algunos casos, principalmente al añadir nuevas palabras clave, es posible que
algunas funciones nuevas solo estén disponibles en ediciones posteriores.
Tendrás que cambiar de edición si quieres aprovecharlas.

Para obtener más detalles, la Guía de ediciones 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.

Apéndice F: Traducciones del libro


Para recursos en otros idiomas además del inglés. La mayoría aún están en
desarrollo; consulta la sección "Traducciones" para obtener ayuda o avísanos
sobre una nueva traducción.

 Portugués (BR)
 Portugués (PT)
 Fuentes: KaiserY/trpl-zh-cn , gnu4cn/rust-lang-Zh_CN
 Lengua china
 Ukraynska
 Español , alternativo , Español por RustLangES
 Ruso
 Coreano
Rust
jueves, 22 de mayo de 2025 : Página 714 de 719

 Japonés
 Francés
 Polaco
 Cebuano
 Tagalo
 esperanto
 elinicia
 Sueco
 Farsi , persa (FA)
 Alemán
 Hindi
 Ya
 Danés

Apéndice G - Cómo se forma el óxido y la


“oxidación nocturna”
Este apéndice trata sobre cómo se crea Rust y cómo eso le afecta a usted como
desarrollador de Rust.

Estabilidad sin estancamiento

Como lenguaje, Rust se preocupa mucho por la estabilidad de su código.


Queremos que Rust sea una base sólida sobre la que construir, y si las cosas
cambiaran constantemente, eso sería imposible. Al mismo tiempo, si no podemos
experimentar con nuevas funciones, es posible que no descubramos fallos
importantes hasta después de su lanzamiento, cuando ya no podamos modificar
nada.

Nuestra solución a este problema es lo que llamamos "estabilidad sin


estancamiento", y nuestro principio rector es este: nunca deberías tener miedo de
actualizar a una nueva versión estable de Rust. Cada actualización debería ser
sencilla, pero también debería ofrecerte nuevas funciones, menos errores y
tiempos de compilación más rápidos.

¡Chu, chu! Canales de liberación y viajes en tren

El desarrollo de Rust se basa en un programa de trenes . Es decir, todo el


desarrollo se realiza en la masterrama del repositorio de Rust. Las versiones
Rust
jueves, 22 de mayo de 2025 : Página 715 de 719

siguen un modelo de trenes de lanzamiento de software, utilizado por Cisco IOS y


otros proyectos de software. Existen tres canales de lanzamiento para Rust:

 Nocturno
 Beta
 Estable

La mayoría de los desarrolladores de Rust utilizan principalmente el canal estable,


pero aquellos que quieran probar nuevas funciones experimentales pueden
utilizar la versión nocturna o beta.

Aquí hay un ejemplo de cómo funciona el proceso de desarrollo y lanzamiento:


supongamos que el equipo de Rust está trabajando en el lanzamiento de Rust 1.5.
Este lanzamiento se realizó en diciembre de 2015, pero nos proporcionará cifras
de versión realistas. Se añade una nueva funcionalidad a Rust: se agrega una
nueva confirmación a la master rama. Cada noche, se produce una nueva versión
nocturna de Rust. Cada día es un día de lanzamiento, y nuestra infraestructura de
lanzamiento crea estas versiones automáticamente. Así, con el paso del tiempo,
nuestras versiones se ven así, una vez por noche:

nightly: * - - * - - *

Cada seis semanas, ¡es hora de preparar una nueva versión! La betarama del
repositorio de Rust se deriva de la masterrama que usa Nightly. Ahora hay dos
versiones:

nightly: * - - * - - *
|
beta: *

La mayoría de los usuarios de Rust no usan las versiones beta activamente, sino
que prueban la versión beta en su sistema de integración continua para ayudar a
Rust a detectar posibles regresiones. Mientras tanto, todavía hay una versión
nocturna cada noche:

nightly: * - - * - - * - - * - - *
|
beta: *

Supongamos que se encuentra una regresión. ¡Qué bueno que tuvimos tiempo
para probar la versión beta antes de que la regresión se colara en la versión
Rust
jueves, 22 de mayo de 2025 : Página 716 de 719

estable! La corrección se aplica a master, de modo que nightly se corrige, y luego


se retroporta a la betarama, generando una nueva versión beta:

nightly: * - - * - - * - - * - - * - - *
|
beta: *--------*

Seis semanas después de la creación de la primera beta, ¡llegó la versión estable!


La stablerama se genera a partir de la betarama:

nightly: * - - * - - * - - * - - * - - * - * - *
|
beta: *--------*
|
stable: *

¡Genial! ¡Rust 1.5 ya está listo! Sin embargo, olvidamos algo: como han pasado
seis semanas, también necesitamos una nueva beta de la próxima versión de
Rust, la 1.6. Así que, después de stableque se ramificara de beta, la próxima
versión de betase ramificara nightlyde nuevo:

nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: *--------* *
|
stable: *

Esto se llama el “modelo de tren” porque cada seis semanas, una versión “sale de
la estación”, pero todavía tiene que realizar un viaje a través del canal beta antes
de llegar como una versión estable.

Rust publica actualizaciones cada seis semanas, como un reloj. Si conoces la


fecha de una publicación de Rust, puedes saber la fecha de la siguiente: es seis
semanas después. Una ventaja de tener publicaciones programadas cada seis
semanas es que la siguiente llegará pronto. Si una característica no se publica en
una fecha específica, no hay de qué preocuparse: ¡se publicará otra pronto! Esto
ayuda a reducir la presión para introducir características posiblemente sin pulir
cerca de la fecha límite de publicación.

Gracias a este proceso, siempre puedes revisar la próxima compilación de Rust y


comprobar por ti mismo que es fácil actualizar a ella: si una versión beta no
funciona como se esperaba, puedes informar al equipo y solucionarlo antes de
Rust
jueves, 22 de mayo de 2025 : Página 717 de 719

que salga la próxima versión estable. Los fallos en una versión beta son
relativamente raros, pero rustcsiguen siendo software, y los errores existen.

Tiempo de mantenimiento

El proyecto Rust ofrece soporte para la versión estable más reciente. Cuando se
lanza una nueva versión estable, la versión anterior alcanza el fin de su ciclo de
vida (EOL). Esto significa que cada versión recibe soporte durante seis semanas.

Características inestables

Este modelo de lanzamiento tiene un inconveniente más: las características


inestables. Rust utiliza una técnica llamada "indicadores de características" para
determinar qué características están habilitadas en una versión determinada. Si
una nueva característica está en desarrollo, se incluye en [nombre del
desarrollador master] y, por lo tanto, en [nombre del desarrollador], pero detrás de
un indicador de característica [ nombre del desarrollador]. Si, como usuario,
desea probar la característica en desarrollo, puede hacerlo, pero debe usar una
versión [nombre del desarrollador] de Rust y anotar su código fuente con el
indicador correspondiente para activarla.

Si usas una versión beta o estable de Rust, no puedes usar indicadores de


características. Esta es la clave que nos permite aprovechar al máximo las nuevas
funciones antes de declararlas estables para siempre. Quienes deseen optar por
la versión más avanzada pueden hacerlo, y quienes deseen una experiencia
sólida pueden quedarse con la versión estable con la seguridad de que su código
no fallará. Estabilidad sin estancamiento.

Este libro solo contiene información sobre las funciones estables, ya que las
funciones en desarrollo siguen cambiando y seguramente serán diferentes entre
su redacción y su habilitación en las compilaciones estables. Puede encontrar
documentación sobre las funciones de actualización nocturna en línea.

Rustup y el papel de Rust Nightly

Rustup facilita el cambio entre diferentes canales de lanzamiento de Rust, a nivel


global o por proyecto. Por defecto, tendrás instalada la versión estable de Rust.
Para instalarla cada noche, por ejemplo:

$ rustup toolchain install nightly


Rust
jueves, 22 de mayo de 2025 : Página 718 de 719

También puedes ver todas las cadenas de herramientas (versiones de Rust y


componentes asociados) que tienes instaladas . Aquí tienes un ejemplo en el
ordenador Windows de uno de tus autores:rustup

> rustup toolchain list


stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc

As you can see, the stable toolchain is the default. Most Rust users use stable
most of the time. You might want to use stable most of the time, but use nightly
on a specific project, because you care about a cutting-edge feature. To do so,
you can use rustup override in that project’s directory to set the nightly toolchain as
the one rustup should use when you’re in that directory:

$ cd ~/projects/needs-nightly
$ rustup override set nightly

Now, every time you call rustc or cargo inside of ~/projects/needs-


nightly, rustup will make sure that you are using nightly Rust, rather than your
default of stable Rust. This comes in handy when you have a lot of Rust projects!

The RFC Process and Teams

So how do you learn about these new features? Rust’s development model follows
a Request For Comments (RFC) process. If you’d like an improvement in Rust, you
can write up a proposal, called an RFC.

Anyone can write RFCs to improve Rust, and the proposals are reviewed and
discussed by the Rust team, which is comprised of many topic subteams. There’s
a full list of the teams on Rust’s website, which includes teams for each area of
the project: language design, compiler implementation, infrastructure,
documentation, and more. The appropriate team reads the proposal and the
comments, writes some comments of their own, and eventually, there’s
consensus to accept or reject the feature.

If the feature is accepted, an issue is opened on the Rust repository, and someone
can implement it. The person who implements it very well may not be the person
who proposed the feature in the first place! When the implementation is ready, it
lands on the master branch behind a feature gate, as we discussed in
the “Unstable Features” section.
Rust
jueves, 22 de mayo de 2025 : Página 719 de 719

After some time, once Rust developers who use nightly releases have been able
to try out the new feature, team members will discuss the feature, how it’s
worked out on nightly, and decide if it should make it into stable Rust or not. If the
decision is to move forward, the feature gate is removed, and the feature is now
considered stable! It rides the trains into a new stable release of Rust.

También podría gustarte