Actualizaciones

example workflow name

23 de mayo de 2021: Ahora disponible en indonesio gracias Ariandy/1kb.

2 de abril de 2021: Añadido enlace a BuyMeACoffee para aquellos a los que les gustaría invitarme a un café.

1 de febrero de 2021: ¡Ahora disponible en youtube! (en inglés) Dos meses después: completo el 1 de abril de 2021, 186 vídeos en total (un poco más de 23 horas).

22 de diciembre 2020: el libro en mdBook se puede encontrar aquí.

28 de noviembre de 2020: Ahora también disponible en chino simplificado ¡Gracias a kumakichi!

Introducción

Rust es un nuevo lenguaje de programación que ya tiene buenos libros de texto, pero a veces son difíciles ya que son para personas cuyo inglés es nativo. Muchas empresas y personas están aprendiendo Rust en estos días y pueden aprenderlo más rápido con un libro que use un inglés fácil1. Este libro es para que estas empresas y personas puedan aprender Rust con un español simple.

Rust es un lenguaje que bastante nuevo, pero ya es muy popular. Es popular porque te da la velocidad y control de C o C++, pero también ofrece protección del acceso a la memoria como otros lenguajes como Python. Esto lo hace usando nuevas ideas que son en parte diferentes a otros lenguajes. Lo que significa que hay que aprender cosas nuevas y no puedes simplemente "descubrirlo según vas avanzando". Rust es un lenguaje en el que tienes que pensar las cosas durante un tiempo para comprenderlo, pero que aún suena a familiar si conoces otros lenguajes y está diseñado para ayudarte a escribir código de calidad.

1

N.T.: esta traducción al español trata de seguir el mismo principio utilizando un español fácil.

¿Quién soy?

Soy un canadiense que vive en Korea y escribí Rust Fácil pensando en cómo hacer más fácil su uso para las empresas de aquí1. Espero que otras empresas que no usan el inglés como primer idioma puedan utilizarlo también.

1

N.T.: Soy un español que vive en Madrid, espero que esta traducción también facilite la comprensión de este lenguaje que tiene aspectos novedosos incluso para los que conocen C, C++, Java, Python, Ruby, Javascript o Typescript...

La escritura de Rust en inglés fácil

Rust en inglés fácil se escribió de julio a agosto de 2020 y ocupa unas 400 páginas. Puedes contactarme aquí o en LinkedIn o en Twitter si tienes cualquier pregunta. Si ves algo erróneo o tienes una petición de inserción (pull request) que hacer, adelante. Más de 20 personas ya han ayudado a corregir erratas y problemas en el código, por lo que tú también puedes[1^]. no soy el mejor experto de Rust del mundo, así que me gusta escuchar nuevas ideas o ver en qué puedo hacer el libro mejor.

1

N.T.: Esta traducción, Rust en español fácil, se inició en julio de 2021. Para cualquier asunto relacionado con ella, puedes contactarme en github o en LinkedIn.

Parte 1 - Rust en tu navegador

Este libro tiene dos partes. En la primera, aprenderás todo solo utilizando tu navegador. Puedes aprender casi todo lo que debes saber sin instalar Rust. Por eso esta primera parte es muy larga. En la segunda parte, mucho más corta, se habla de Rust en tu ordenador. Ahí aprenderás todo lo que necesitas conocer y que solo se puede hacer fuera del navegador. Algunos ejemplos son: trabajar con ficheros, obtener datos del usuario, gráficos y configuraciones personales. Espero que al finalizar la primera parte te guste Rust lo suficiente para que lo instales en tu equipo. Y si no, no hay problema, esta primera parte de enseña tanto que no te importará.

El entorno de pruebas (Playground) de Rust

Puedes ver este capítulo en inglés en YouTube

Puede que no quieras instalarte aún Rust. No pasa nada. Puedes ir a https://play.rust-lang.org/ y comenzar a escribir Rust sin salir del navegador. Puedes escribir ahí el código y pulsar Run para ver los resultados. Puedes ejecutar la mayoría de los ejemplos de este libro en este entorno. Solamente al final del libro verás ejemplos que van más allá de lo que se puede hacer aquí (como abrir ficheros.)

Algunas recomendaciones de uso de este entorno de pruebas (Playground):

  • Ejecuta el código con Run
  • Cambia de Debug a Release si quieres que el código sea más rápdio. Debug (Depuración) hace que compile más rápido, se ejecute más lento y contenga información para la depuración de errores. Release (Producción) hace que compile más lento, se ejecute más rápido y se suprima la información para la depuración de errores.
  • Pulsa en Share (Compartir) para obtener un hiperenlace. Puedes usarlo para compartir tu código si necesitas ayuda. Después de pulsar compartir, puedes pulsar en Open a new thread in de Rust user forum(Abre un nuevo hilo en el foro de usuarios de Rust) para pedir ayuda.
  • Tools (Herramientas): Rustfmt formatea el código correctamente.
  • Tools (Herramientas): Clippy da información extra sobre cómo hacer mejor el código.
  • Config (Configuración): puedes cambiar aquí el tema a modo oscuro para que puedas trabajar mejor de noche y otras muchas opciones.

Si quieres instalar Rust, ve a https://www.rust-lang.org/tools/install y sigue las instrucciones. Normalmente, usarás rustup para instalar y actualizar Rust.

🚧 y ⚠️

En ocasiones, el código de los ejemplos del libro no funciona. Si un ejemplo no funciona, veras un 🚧 o un ⚠️ junto a él. 🚧 significa "en construcción", es decir, que el código no está completo. Rust necesita una fn main() (función principal) para ejecutarse, pero algunas veces solo queremos observar una parte del código por lo que no aparecerá el código fn main (). Estos ejemplos son correctos, pero necesitan de una función fn main() para ejecutarse. En algunos ejemplos el código mostrará un problema que tendrás que resolver. Estos ejemplos podrían tener una función fn main() pero generarán un error y por eso se acompañan de un símbolo ⚠️.

Comentarios

Puedes ver este capítulo en inglés en YouTube

Los comentarios son para que los lean los programadores, no el ordenador. es bueno escribir comentarios para ayudar a otras personas a entender tu código. Es bueno, también, para ayudarte a entender tu propio código pasado un tiempo (Muchas personas escriben buen código, pero olvidan porqué lo escribieron). Para escribir comentarios en el código, en Rust se suele usar //:

fn main() {
    // Los programas Rust comienzan en fn main()
    // Pones el código en un bloque que comienza con { y termina con }
    let algun_numero = 100; // Aquí podemos esribir todo lo que queramos ya que el compilador no lo va mirar
}

Cuando lo haces así, el compilador no mirará nada que haya a la derecha de //.

Existe otra clase de comentario que se escribe con /* para iniciarlo y se termina con `*/. Este tipo de comentario es útil para escribirlo entre el código.

fn main() {
    let algun_numero/*: i16*/ = 100;
}

Para el compilador let algun_numero/*: i16*/ = 100; es igual que let algun_numero = 100;.

Esta forma /* */ también es útil para los comentarios de gran longitud que necesiten más de una línea. En este ejemplo se puede ver que necesitas escribir // en cada línea, pero si utilizas /*, el comentario dura hasta que lo finalices con */.

fn main() {
    let algun_numero = 100; /* Déjame contarte
    un poco sobre este número.
    Es el 100, que es mi número favorito.
    Se llama algun_numero pero realmente creo que...*/

    let algun_numero = 100; // Déjame contarte
    // un poco sobre este número.
    // Es el 100, que es mi número favorito.
    // Se llama algun_numero pero realmente creo que...
}

Tipos de dato

Rust tiene muchos tipos de dato que permiten trabajar con números, caracteres y otros. Algunos son simples, otros son más complicados e, incluso, puedes crear tus propios tipos de dato.

Tipos de dato primitivos

Puedes ver este capítulo en YouTube en inglés

Rust tiene tipos simples que llaman tipos primitivos (primitivo = muy básico). Comenzaremos con los números enteros y los char (caracteres). Los enteros son números sin coma decimal. Existen dos tipos de enteros:

  • Enteros con signo.
  • Enteros sin signo.

"Con signo" significa que disponen de + (signo más) y - (signo menos), por lo que los enteros con signo pueden ser positivos o negativos (por ejemplo, +8, -8). Por el contratio, los enteros sin signo solo pueden ser positivos ya que no tienen signo.

Los enteros con signo son: i8, i16, i32, i64, i128, e isize.

Los enteros sin signo son: u8, u16, u32, u64, u128, e usize.

El número tras la i o la u indica el número de bits que se usan para el entero. Así, los números con más bits pueden ser mayores. 8 bits = un byte por lo que i8 ocupa un byte, y puede contener valores entre el -128 y el 127. Por lo tanto i64 ocupa 64 bits o, lo que es lo mismo, 8 bytes y puede representar números entre el -9223372036854775808 y el 9223372036854775807.

Los tipos numéricos con mayor tamaño pueden representar valores mayores. Por ejemplo, el tipo u8puede representar del 0 al 255, el tipo u16 puede representar del 0 al 65635, y el tipo u128 puede representar a un número entre el 0 y el 340282366920938463463374607431768211455.

¿Y qué representan isize y usize? El número de bits del tipo nativo del procesador de tu ordenador (El número de bits nativo de tu procesador se denomina la arquitectura de tu procesador). Así que isize y usize en un ordenador de 32-bits son equivalentes a i32 y u32. En un ordenador de 64-bits son equivalentes a i64 y u64.

Hay muchas razones para disponer de todos estos tipos de números enteros. Una razón es el rendimiento del ordenador: es más rápido procesar un número menor de bytes. Por ejemplo, el número -10 representado como un i8 es 11110110, pero como un i128 es 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110110.

veamos otros usos:

Los caracteres en Rust se denominan char. Todo char se representa por un número: la letra A es lel número 65, mientras que el carácter ("amigo" en chino) es el número 21451. La lista de estos números que representan a caracteres se denomina "Unicode". Unicode usa números más pequeños para los caracteres que se usan más, como los de la A a la Z, los dígitos de 0 a 9 o el espacio.

fn main() {
    let primera_letra = 'A';
    let espacio = ' '; // Un espacio entre ' ' también es un char
    let char_en_otro_idioma = 'Ꮔ'; // Gracias a Unicode, otros lenguajes, como el Cherokee, también se visualizan bien
    let cara_gato = '😺'; // Emojis también son char
}

Los caracteres que se utilizan más se representan or números menores al 256 y así pueden caber en un u8. Recuerda que un u8 permite números entre el 0 y el 255, lo que facilita la representación de 256 caracteres en total. Esto significa que Rust puede convertir de forma segura del tipo de datos u8 al char, utilizando la palabra reservada del lenguaje as (Considera el dato de este tipo u8 como si fuese char).

La conversión de tipos de datos utilizando as es muy útil ya que Rust es muy estricto. Siempre necesita conocer el tipo de dato y, además, no deja utilizar de forma conjunta dos tipos de datos diferentes incluso aunque sean de la misma familia (como los enteros). Por ejemplo, este código no funcionará:

fn main() { // main() es la función a partir de la que se 
            // inician los programas Rust. El código entre las llaves {}

    let mi_numero = 100; // No hemos indicado el tipo de datos entero
                         // que Rust debe utilizar,
                         // así que Rust elige i32. Rust siempre
                         // elige i32 para los enteros si no se le indica
                         // que utilice otro tipo de datos diferente

    println!("{}", mi_numero as char); // ⚠️
}

Esta es la razón:

error[E0604]: only `u8` can be cast as `char`, not `i32`
 --> src\main.rs:10:20
  |
3 |     println!("{}", mi_numero as char);
  |                    ^^^^^^^^^^^^^^^^^ invalid cast

Afortunadamente, podemos corregir esto fácilmente con as. No podemos convertir un i32 a char, pero podemos convertir un i32 a u8. Y después, podemos realizar la conversión de u8 a char. Po tanto, en una línea usamos as para convertir mi_numero a u8 y después lo convertimos a char. Ahora sí compilará:

fn main() { 
    let mi_numero = 100; 
    println!("{}", mi_numero as u8 as char);
}

Se imprime d porque es el char que está representado en Unicode con este número.

Existe una forma más fácil, sin embargo, de conseguir este resultado: indicarle a Rust que mi_numero es de tipo u8. Así:

fn main() { 
    let mi_numero: u8 = 100; // se indica de forma expresa que
                             // el tipo de la variable mi_numero es u8
    println!("{}", mi_numero as char);
}

Las anteriores, son dos de las razones para la existencia de todos estos tipos de datos numéricos en Rust. Hay otra razón más: usize es el tamaño que Rust utiliza para indexar (Indexar significa "conocer cual elemento va primero", "cual va segundo", etc.). usize es el mejor tamaño para el indexado porque:

  • Un índice no puede ser negativo, por lo que tiene que ser uno de los tipos de dato con una u.
  • Debe ser grande porque en muchas ocasiones necesitas indexar muchas cosas, pero
  • No puede ser un u64 porque los ordenadores de 32-bits no lo pueden manejar.

Por eso Rust usa usize para indexar y facilitar que tu ordenador pueda utilizar el tipo de datos mayor de que disponga.

Vamos a aprender algo más sobre char. Ya vimos que charsiempre es un carácter y utiliza '' en lugar de "".

Todos los char usan 4 bytes de memoria, puesto que son necesarios 4 bytes para contener cualquier clase de carácter:

  • Las letras y símbolos básicos suelen necesitar solo 1 de los 4 bytes: a b 1 2 + - = $ @
  • Otras letras como las diéresis y tildes necesitan 2 de los 4 bytes: ä ö ü ß è é à ñ
  • Los caracteres coreanos, japoneses o chinos necesitan 3 de los cuatro bytes: 国 안 녕

Cuando los caracteres se usan como parte de una cadena, esta se codifica para usar la menor cantidad de memoria necesaria para cada carácter1.

1 N.T.: Rust codifica las cadenas en UTF-8.

Para observar esto podemos usar .len():

fn main() {
    println!("Tamaño de un char: {}", std::mem::size_of::<char>()); // 4 bytes
    // .len() devuelve el tamaño de una cadena de texto en bytes
    println!("Tamaño de una cadena que contiene la 'a': {}", "a".len());
    println!("Tamaño de una cadena que contiene la 'ß': {}", "ß".len());
    println!("Tamaño de una cadena que contiene la '国': {}", "国".len());
    println!("Tamaño de una cadena que contiene la '𓅱': {}", "𓅱".len());
}

Lo que imprime:

Tamaño de un char: 4
Tamaño de una cadena que contiene la 'a': 1
Tamaño de una cadena que contiene la 'ß': 2
Tamaño de una cadena que contiene la '国': 3
Tamaño de una cadena que contiene la '𓅱': 4

Puedes ver que la a ocupa un byte, la 'ß' alemana ocupa dos, la japonesa ocupa tres, y el carácter del antiguo egipto 𓅱 ocupa cuatro bytes.

fn main() {
    let fragmento = "¡Hola!";
    println!("El fragmento ocupa {} bytes.", fragmento.len());
    let fragmento2 = "안녕!"; // Coreano de "hola"
    println!("El fragmento2 ocupa {} bytes.", fragmento2.len());
}

Que imprime:

El fragmento ocupa 7 bytes.
El fragmento2 ocupa 7 bytes.

El primer fragmento consta de seis caracteres y ocupa 7 bytes (la apertura de exclamación ocupa dos bytes). El segundo fragmento consta de tres caracteres y ocupa 7 bytes (los dos primeros caracteres ocupan tres bytes cada uno).

Si .len() devuelve el tamaño en bytes, ¿cómo se puede conocer el tamaño de una cadena de texto en caracteres? Aprenderemos esto más tarde, en este momento basta con recordar que se hace con .chars().count(). La primera función .chars() devuelve los caracteres separados y luego cuenta cuántos son.

fn main() {
    let fragmento = "¡Hola!";
    println!("El fragmento ocupa {} bytes y son {} caracteres.",
        fragmento.len(), fragmento.chars().count());
    let fragmento2 = "안녕!";
    println!("El fragmento2 ocupa {} bytes y son {} caracteres.",
        fragmento2.len(), fragmento2.chars().count());
}

Que imprime:

El fragmento ocupa 7 bytes y son 6 caracteres.
El fragmento2 ocupa 7 bytes y son 3 caracteres.

Inferencia de tipos de dato

Puedes ver este capítulo en YouTube en inglés

La inferencia de tipos de datos consiste en que si no se le indica el tipo de datos al compilador, pero lo puede determinar por sí mismo, él decide que tipo utilizar. El compilador siempre necesita conocer el tipo de las variables, pero no siempre es necesario decírselo expresamente. En realidad, normalmente no necesitas indicárselo. Por ejemplo, en la sentencia let mi_numero = 8, mi_numero será de tipo i32. Esto se debe a que el compilador elige siempre el tipo i32 para los números enteros si no se le indica uno. Sin embargo, en la siguiente sentencia let mi_numero: u8 = 8, la variable mi_numero es de tipo u8 ya que así se le ha indicado.

Así que normalmente el compilador puede deducir el tipo de datos, pero en ocasiones será necesario indicárselo por una de las siguientes dos razones:

  1. Estás programando algo muy complejo y el compilador no puede deducir el tipo de datos que es necesario.
  2. Quieres usar un tipo de datos diferente (por ejemplo, quieres un i128, no el i32 que se usa por defecto).

Para especificar un tipo, se añaden dos puntos después del nombre de la variable seguido del tipo.

fn main() {
    let numerito: u8 = 10;
}

Para los números, se puede especificar el tipo después del número, no se necesita un espacio - solo teclearlo justo después del número.

fn main() {
    let numerito = 10u8; // 10u8 = 10 de tipo u8
}

También se puede añadir _ para añadir claridad a la lectura.

fn main() {
    let numerito = 10_u8; // Esto es más fácil de leer
    let numerazo = 100_000_000_i32; // 100 millones es de fácil lectura con _
}

El _ no modifica el número. Solo lo hace más fácil de leer. Y no importa el cuantos _ se utilizan.

fn main() {
    let numero = 0________u8;
    let numero2 = 1___6______2____4______i32;
    println!("{}, {}", numero, numero2);
}

Lo anterior imprime 0, 1624.

Números decimales

Los números decimales son aquellos que tienen coma decimal1. 5.5 es un número decimal y 6 es un número entero. 5.0 también es un número decimal e incluso 5. lo es.

1 N.T.: en español se usa una coma como carácter para separar la parte entera de un número de su parte decimal. En Rust, la coma decimal española se sustituye por el punto decimal que es el que se usa habitualmente en los lenguajes de programación.

fn main() {
    let mi_decimal = 5.; // Rust ve un . y sabe que es un decimal (float, en inglés)
}

Rust utiliza diversos tipos de dato para almacenar números decimales, son el f32 y el f64. Al igual que en los números enteros, el número tras f muestra el número de bits utilizados en cada caso para almacenar el dato. Si no se indica el tipo, Rust elige f64.

fn main() {
    let mi_decimal: f64 = 5.0; // Esta variable es de tipo f64
    let mi_otro_decimal: f32 = 8.5; // Esta es de tipo f32
    let tercer_decimal = mi_decimal + mi_otro_decimal; // ⚠️
}

Cuanto se intenta ejecutar el código anterior, Rust se queja diciendo:

error[E0308]: mismatched types
 --> src/main.rs:4:39
  |
5 |     let tercer_decimal = mi_decimal + mi_otro_decimal; // ⚠️
  |                                       ^^^^^^^^^^^^^^^ expected `f64`, found `f32`

El compilador indica "expected (tipo), found (type)" cuando se usa el tipo erróneo. Rust lee el código de esta forma:

fn main() {
    let mi_decimal: f64 = 5.0; // El compilador ve un f64
    let mi_otro_decimal: f32 = 8.5; // El compilador ve un f64. Es un tipo diferente.
    let tercer_decimal = mi_decimal + // Se quiere sumar mi_decimal, que es f64 a algún
                                      // otro número. Ahora espera otro f64...  
        mi_otro_decimal; // ⚠️ pero se encuentra un f32, no se pueden sumar.
}

Así que cuando veas que el compilador indica "expected (tipo), found (type)", debes buscar la causa por la que el compilador esperaba un tipo de datos diferente.

Con los números simples es fácil arreglarlo. Puedes convertir el f32 a f64 con un as.

fn main() {
    let mi_decimal: f64 = 5.0;
    let mi_otro_decimal: f32 = 8.5;
    // En la siguiente línea, se utiliza mi_otro decimal como un f64
    let tercer_decimal = mi_decimal + mi_otro_decimal as f64;
}

O simplemente, se pueden eliminar las declaraciones de tipo, Rust elegirá tipos que se puedan sumar entre sí.

fn main() {
    let mi_decimal = 5.0; // Rust elige f64
    let mi_otro_decimal = 8.5; // Rust elige de nuevo f64
    let tercer_decimal = mi_decimal + mi_otro_decimal;
}

El compilador de Rust es inteligente y no elegirá f64 si necesitas f32:

fn main() {
    let mi_decimal: f32 = 5.0; // Rust elige f64
    let mi_otro_decimal = 8.5; // Normalmente Rust elegiría f64
    // pero al conocer que lo vamos a sumar a un f32, elige un f32 para mi_otro_decimal
    let tercer_decimal = mi_decimal + mi_otro_decimal;
}

Imprimiendo '¡Hola, mundo!'

Puedes ver este capítulo en YouTube en inglés: Video 1, Video 2

Cuando creas un nuevo programa Rust, siempre contiene este código:

fn main() {
    println!("Hello, world!");
}
  • fn significa función.
  • main es el nombre de la función que inicia el programa.
  • () significa que en este caso no se le pasan variables a esta función.
  • {} es un bloque de código. Es donde se encuentra el código.
  • println! es una macro, que es como una función que sirve para escribir código por ti. Las macros siempre tienen un ! al final de su nombre. Por ahora, recuerda que ! significa que es una macro.

Para aprender lo que hace ;, crearemos otra función. Primero, en main imprimiremos el número 81.

1 N.T.: aprovechamos para cambiar el saludo a español. En Rust, cuando se utiliza la aplicación cargo para crear un programa, siempre se incorpora el código de main con "Hello, world!" de forma automática.

fn main() {
    println!("¡Hola, mundo número {}!", 8);
}

Las {} dentro de println! indican a Rust que "ponga la variable en este lugar". Este código imprime ¡Hola, mundo número 8!.

Podemos poner más cosas ampliando lo anterior.

fn main() {
    println!("¡Hola, mundos número {} y {}!", 8, 9);
}

El codigo anterior imprime ¡Hola, mundos número 8 y 9!.

Ahora vamos a crear una función.

fn numero() -> i32 {
    8
}

fn main() {
    println!("¡Hola, mundo número {}!", numero());
}

El código anterior también imprime ¡Hola, mundo número 8!. Cuando Rust encuentra numero() entiende que es una función. Esta función:

  • No toma ningún parámetro (porque tiene una llamada con ()).
  • Devuelve un i32. El símbolo de flecha -> muestra el tipo que devuelve la función.
  • La función en sí misma solo contiene un 8. Al no terminar en ; este valor es el que devuelve al terminar de ejecutarse. Si tuviera un ; detrás, la función no devolvería nada (devolvería un ()). Rust no compilaría si contuviera un ; al final ya que se ha indicado que (tras la flecha) debe devolver un valor de tipo i32 y con el ; se devuelve () que no es de tipo i32.
fn numero() -> i32 {
    8;
}

fn main() {
    println!("¡Hola, mundo número {}!", numero());
}
error[E0308]: mismatched types
 --> src/main.rs:1:16
  |
1 | fn numero() -> i32 {
  |    ------      ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
2 |     8;
  |      - help: consider removing this semicolon

Esto significa que "me dijiste que numero() devuelve un i32, pero añadiste un ; por lo que esta función no devuelve nada". Así que el compilador sugiere que se elimine el punto y coma.

También se puede escribir lo siguiente return 8;, pero en Rust lo normal es simplemente eliminar el ; para ejecutar el return.

Cuando se quiere pasar variables a una función, se deben poner dentro de (). Hay que darles un nombre e indicar su tipo.

// Entran a la función dos i32s. Se llaman num_uno y num_dos
fn multiplicar(num_uno: i32, num_dos: i32) {
    let resultado = num_uno * num_dos;
    println!("{} por {} es {}", num_uno, num_dos, resultado);
}

fn main() {
    multiplicar(8, 9); // Pasamos unos números directamente
    let algun_numero = 10; // o podemos declarar dos variables
    let algun_otro_numero = 2;
    multiplicar(algun_numero, algun_otro_numero); // y pasarlos a la función
}

También se puede devolver un i32. Basta con poner la variable resultado como la última de la función sin ; al final.

fn multiplicar(num_uno: i32, num_dos: i32) -> i32 {
    let resultado = num_uno * num_dos;
    println!("{} por {} es {}", num_uno, num_dos, resultado);
    resultado // este es el valor i32 que se retorna
}

fn main() {
    // multiplicar() imprime el resultado y lo devuelve,
    // lo que permite asignarlo a resultado_mult
    let resultado_mult = multiplicar(8, 9);
}

La declaración de variables y los bloques de código

Se usa letpara declarar una variable (para decirle a Rust que construya una variable).

fn main() {
    let mi_numero = 8;
    println!("¡Hola, mundo número {}!", mi_numero);
}

Las variables existen dentro de un bloque de código {}. En el siguiente ejemplo mi_numero desaparece antes de llamar a println! porque se encuentra dentro de su propio código.

fn main() {
    {
        let mi_numero = 8; // mi_numero se crea aquí
                           // mi_numero se extingue aquí
    }
    println!("¡Hola, mundo número {}!", mi_numero);// ⚠️  mi_numero no existe y
                                                   // println!() no lo puede encontrar
}

Se puede usar un bloque de código para devolver un valor, como en el siguiente código.

fn main() {
    let mi_numero = {
        let segundo_num = 8;
        segundo_num + 9 // sin punto y coma, por lo que el
                        // bloque de código devuelve 8 + 9 = 17
    }; 
    println!("¡Hola, mundo número {}!", mi_numero);
}

Si se añadiera un punto y coma en la sentencia final del bloque, devolvería () (nada).

fn main() {
    let mi_numero = {
        let segundo_num = 8; // declara el segundo número
        segundo_num + 9;     // suma 9 con el segundo número
                             // pero no se devuelve
                             // segundo_num desaparece aquí
    }; 
    println!("¡Hola, mundo número {:?}!", mi_numero); // mi_numero es ()
}

Si has observado bien, hemos cambiado {} por {:?}. El motivo se verá en el siguiente capítulo.

La visualización y depuración

Puedes ver este capítulo en YouTube en inglés

En Rust, las variables simples se pueden imprimir usyo {} en println!. Pero hay variables que no pueden imprimirse y es necesario usar la impresión de depuración. La impresión de depuración es para los programadores, porque habitualmente muestra más información. En ocasiones, esta impresión no es "bonita", no queda bien, porque muestra información extra para ayudarte.

¿Cómo puedes conocer si necesitas usar {:?} y no {}? El compilador te avesará. Por ejemplo:

fn main() {
    let no_imprime = ();
    println!("Esto no se imprimirá: {}", no_imprime); // ⚠️
}

Cuyo se compila el código anterior, el compilador se queja así:

error[E0277]: `()` doesn't implement `std::fmt::Desplay`
 --> src/main.rs:3:42
  |
3 |     println!("Esto no se imprimirá: {}", no_imprime); // ⚠️
  |                                          ^^^^^^^^^^ `()` cannot be formatted with el default formatter
  |
  = help: el trait `std::fmt::Desplay` es not implemented for `()`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: required by `std::fmt::Desplay::fmt`
  = note: thes error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

Esto es mucha información, pero la parte importante es you may be able to use {:?} (or {:#?} for pretty-print) instead. Esto significa que puedes intentar usar {:?} o también {:#?}. La segunda opción, {:#?} se llama "impresión atractiva". Es igual que {:?} pero usyo un formateo diferente en más líneas.

En resumen, Desplay (vesualización) supone la impresión con {}, y Debug (depuración) supone la impresión con {:?}.

Una última cosa, puedes usar print! sin ln si no quieres que haya un salto de línea al final de la impresión.

fn main() {
    print!("Esto no imprimirá un salto de línea");
    println!(" así que esto estará en la mesma línea");
}

Que imprimirá Esto no imprimirá un salto de línea así que esto estará en la mesma línea.

El número menor y el número mayor

Si se quiere ver el menor y mayor número que se puede representar, se puede usar MIN y MAX. std es la "librería estándar del lenguaje" y contiene las funciones y otros elementos importantes del lenguaje Rust. Más adelante se explicarán elementos de la librería estyar. Mientras tanto, puedes recordar que esta es la forma de obtener los numeros menor y mayor de un tipo de datos.

fn main() {
    // pista: std::i8::MIN significa
    // "el valor de MIN de la sección i8 de la librería estandar"
    println!("El menor i8 es {} y el mayor i8 es {}.", std::i8::MIN, std::i8::MAX); 
    println!("El menor u8 es {} y el mayor u8 es {}.", std::u8::MIN, std::u8::MAX);
    println!("El menor i16 es {} y el mayor i16 es {}.", std::i16::MIN, std::i16::MAX);
    println!("El menor u16 es {} y el mayor u16 es {}.", std::u16::MIN, std::u16::MAX);
    println!("El menor i32 es {} y el mayor i32 es {}.", std::i32::MIN, std::i32::MAX);
    println!("El menor u32 es {} y el mayor u32 es {}.", std::u32::MIN, std::u32::MAX);
    println!("El menor i64 es {} y el mayor i64 es {}.", std::i64::MIN, std::i64::MAX);
    println!("El menor u64 es {} y el mayor u64 es {}.", std::u64::MIN, std::u64::MAX);
    println!("El menor i128 es {} y el mayor i128 es {}.", std::i128::MIN, std::i128::MAX);
    println!("El menor u128 es {} y el mayor u128 es {}.", std::u128::MIN, std::u128::MAX);
}

Que imprimirá:

El menor i8 es -128 y el mayor i8 es 127.
El menor u8 es 0 y el mayor u8 es 255.
El menor i16 es -32768 y el mayor i16 es 32767.
El menor u16 es 0 y el mayor u16 es 65535.
El menor i32 es -2147483648 y el mayor i32 es 2147483647.
El menor u32 es 0 y el mayor u32 es 4294967295.
El menor i64 es -9223372036854775808 y el mayor i64 es 9223372036854775807.
El menor u64 es 0 y el mayor u64 es 18446744073709551615.
El menor i128 es -170141183460469231731687303715884105728 y el mayor i128 es 170141183460469231731687303715884105727.
El menor u128 es 0 y el mayor u128 es 340282366920938463463374607431768211455.

Mutabilidad (cambio)

Puedes ver este capítulo en YouTube en inglés

Cuando se declara una variable con `let, es inmutable (no se puede cambiar su valor).

El código siguiente no funciona.

fn main() {
    let mi_numero = 8;
    mi_numero = 10; // ⚠️
}

El compilador indica error[E0384]: cannot assign twice to immutable variable mi_numero.

error[E0384]: cannot assign twice to immutable variable `mi_numero`
 --> src/main.rs:3:5
  |
2 |     let mi_numero = 8;
  |         ---------
  |         |
  |         first assignment to `mi_numero`
  |         help: consider making this binding mutable: `mut mi_numero`
3 |     mi_numero = 10; // ⚠️
  |     ^^^^^^^^^^^^^^ cannot assign twice to immutable variable

Esto es porque las variables son inmutables si solo se escribe let.

Muchas veces, será necesario modificar la variable. Para ello, se debe añadir mut después de let:

fn main() {
    let mut mi_numero = 8;
    mi_numero = 10;
}

El código anterior funciona sin problema alguno.

Sin embargo, no se le puede cambiar el tipo. Esto no funciona:

fn main() {
    let mut mi_numero = 8; // La variable es i32. 
                           // El tipo no se puede cambiar.
    mi_numero = "¡Hola, mundo!"; // ⚠️
}

Si se intenta compilar el programa anterior, se obtendrá el mismo mensaje "expected" por parte del compilador: expected integer, found &str. &stres un tipo de cadena que aprenderemos pronto.

Ocultación (Shadowing)

La ocultación de una variable sucede cuando se usa let para declarar una nueva variable con el mismo nombre que otra. A primera vista se parece a la mutabilidad, pero es totalmente diferente. En el siguiente ejemplo, se oculta una variable:

fn main() {
    let mi_numero = 8; // Esta variable es i32
    println!("{}", mi_numero); // imprime 8
    let mi_numero = 9.2; // Esta variable es f64 y tiene el mismo nombre
    // pero es una variable nueva, completamente diferente.
    println!("{}", mi_numero) // imprime 9.2
}

Se dice que hemos "ocultado" mi_numerocon un nuevo "enlace".

¿Se ha destruido la anterior variable mi_numero? No, pero cuando se llama a mi_numero ahora se accede a la segunda variable de tipo f64. Y como ambas declaraciones se encuentran en el mismo bloque de código (mismo ámbito, mismo {}), se deja de tener acceso al mi_numero de tipo i32.

Si estuvieran en diferentes bloques de código, podríamos volver a acceder a la primera variable mi_numero. Por ejemplo:

fn main() {
    let mi_numero = 8; // Esta variable es i32
    println!("{}", mi_numero); // imprime 8
    {
        let mi_numero = 9.2; // Esta variable es f64 y tiene el mismo nombre
        // pero es una variable nueva, completamente diferente.
        println!("{}", mi_numero) // imprime 9.2
                                  // pero la nueva variable mi_numero
                                  // solo existe hasta aquí
                                  // la anterior ¡sigue viva!
    }
    println!("{}", mi_numero); // imprime 8
}

En resumen, cuando ocultas una variable, no la destruyes. La bloqueas.

Qué ventajas tiene el ocultar variables. Es una buena práctica cuando necesitas modificar una variable en muchas ocasiones. Imagina que quieres hacer un conjunto de cálculos matemáticos simples con una variable:

fn dos_veces(numero: i32) -> i32 {
    numero * 2
}

fn main() {
    let numero_final = {
        let y = 10;
        let x = 9; // x comienza con 9
        let x = dos_veces(x); // se oculta con el nuevo x: 18
        let x = x + y; // se oculta con el nuevo x: 28
        x // devuelve x: a numero_final se asigna este valor de x
    };
    println!("El número ahora es: {}", numero_final)
}

Sin ocultar las variables anteriores, habría sido necesario pensar diferentes nombres, incluso aunque no nos importen estos valores intermedios:

fn dos_veces(numero: i32) -> i32 {
    numero * 2
}

fn main() {
    // Ejemplo sin usar las capacidades de ocultar variables
    let numero_final = {
        let y = 10;
        let x = 9; // x comienza con 9
        let x2 = dos_veces(x); // segundo nombre para x
        let x2_y = x2 + y; // ¡tercer nombre para x!
        x2_y // qué pena no tener disponible la ocultación
             // habríamos podido usar solo una variable x
    };
    println!("El número ahora es: {}", numero_final)
}

En general, se usa la ocultación de variables en estos casos. Cuando se quiere usar una variable para un cálculo y luego otro más, sin tener mucho interés por los valores intermedios.

La pila, la memoria dinámica y los punteros

La pila ("stack" en inglés), la memoria dinámica ("heap" en inglés) y los punteros son elementos muy importantes en Rust.

La pila y la memoria dinámica son dos tipos de almacenamiento de los datos de un programa durante su ejecución. Sus diferencias más importantes son:

  • La pila es muy rápida, la memoria dinámica no lo es tanto. Tampoco es lenta, pero siempre es más rápido acceder a la pila. Aunque no es posible utilizar la pila siempre porque:
  • Rust necesita conocer el tamaño de una variable en durante su compilación para poder guardarla en la pila. Así, las variables simples como i32 van a la pila ya que se conoce su tamaño exacto. Se sabe que va a ocupar 4 bytes, 32 bits = 4 bytes. Por lo tanto, los datos de tipo i32 pueden ir siempre a la pila.
  • Algunos tipos no tienen un tamaño conocido en tiempo de compilación. No pueden guardarse en la pila. ¿Qué se puede hacer? En primer lugar, se pone la información en la memoria dinámica ya que esta puede contener datos de cualquier tamaño. En segundo lugar, se guarda un puntero en la pila. El tamaño de los punteros es conocid. Así, para recuperar un valor de una variable que está en la memoria dinámica, el ordenador va primero a la pila, obtiene el puntero y lo sigue hasta la memoria dinámica para localizr el dato que se busca.

Los punteros parecen complicados, pero no lo son. Son como una tabla de contenidos de un libro. Imagina este libro:

MI LIBRO

TABLA DE CONTENIDO

Capítulo                        Página
Capítulo 1: mi vida              1
Capítulo 2: mi gato              15
Capítulo 3: mi trabajo           23
Capítulo 4: mi familia           30
Capítulo 5: mis planes futuros   43

La tabla de contenido es como una "pila" que, en este caso, contiene cinco punteros. Puedes leerlos y encontrar la información sobre la que tratan. ¿Dónde está el capítulo sobre "mi vida"? Está en la página 1 (Apunta a la página 1). ¿Dónde está el capítulo sobre "mi trabajo"? Está en la página 23.

El puntero que se ve habitualmente en Rust se denomina referencia. Esto es lo importante que se debe saber: una referencia apunta a la memoria de otro valor. Una referencia supone que se tome prestado el valor, pero no se apropia de él. Es lo mismo que en el libro anterior: la tabla de contenidos no posee la información. Se encuentra en los capítulos que son los que la poseen. En Rust, las referencias llevan el símbolo & al principio de ellas. Así.

  • let mi_variable = 8 crea una variable normal, pero
  • let mi_referencia = &mi_variable crea una referencia. Se lee como "mi_referencia es una referencia a mi_variable" o como "mi_referencia se refiere a mi_variable".

Esto significa que mi_referencia solo mira a los datos de mi_variable. mi_variable sigue siendo propietaria de sus datos. También es posible tener una referencia que "apunte" a otra referencia. Hasta culquier número de referencias.

fn main() {
    let mi_numero = 15; // Esto es un i32
    let referencia_simple = &mi_numero; //  Esto es una &i32
    let referencia_doble = &referencia_simple; // Esto es una &&i32
    let referencia_quintuple = &&&&&mi_numero; // Esto es una &&&&&i32
}

Todos estos son tipos de dato diferentes, de la misma forma que "un amigo de un amigo" es diferente de "un amigo".

Más sobre impresión

En Rust se puede imprimir como se quiera. Por eso, interesa conocer algunas cosas más sobre este tema.

Si se añade \n se imprimirá una nueva línea. Si se añade \t se insertará un tabulador:

fn main() {
    // Observa: la función usada es print!, no println!
    print!("\tComienza con un tabulador\ny salta a una nueva línea");
}

El código anterior imprime lo siguiente:

        Comienza con un tabulador
y salta a una nueva línea

Dentro de "" se puede escribir en diferentes líneas, pero es necesario tener cuidado con los espacios:

fn main() {
    // Nota: después de la primera línea tienes que comenzar la siguiente línea
    // en la primera columna (pegado a la izquierda).
    println!("Dentro de comillas
se puede escribir
en muchas líneas
y se imprimirá correctamente.");
    // Si se escribe directamente bajo la sentencia, se añadirán los espacios
    // correspondientes de la izquierda
    println!("Si se olvida que hay que
    escribir pegado al lado izquierdo
    estos espacios se añadirán
    a la impresión.");
}

El código anterior imprime:

Dentro de comillas
se puede escribir
en muchas líneas
y se imprimirá correctamente.
Si se olvida que hay que
    escribir pegado al lado izquierdo
    estos espacios se añadirán
    a la impresión.

Si se necesitase imprimir caracteres como \n (caracteres de escape, como el del salto de línea y el tabulador que se han visto antes), se puede añadir un \ extra:

fn main() {
    println!("Se imprimen caracteres de escape, no inserta nueva línea y tabulador: \\n y \\t");
}

Lo que imprime:

Se imprimen caracteres de escape, no inserta nueva línea y tabulador: \n y \t

A veces se necesitan muchos " y caracteres de escape en el texto, por lo que Rust proporciona un método más simple para ignorarlos: se añade r# al comienzo y # al final de la cadena de caracteres.

fn main() {
    // En esta línea hemos usado \ cinco veces
    println!("Él dijo, \"Puedes encontrar el fichero en c:\\files\\my_documents\\file.txt\". Y así fue como lo encontré.");
    println!(r#"Él dijo, "Puedes encontrar el fichero en c:\files\my_documents\file.txt". Y así fue como lo encontré."#)
}

Ambas opciones imprimen lo mismo, pero el uso de r# lo hace más simple de entender.

Él dijo, "Puedes encontrar el fichero en c:\files\my_documents\file.txt". Y así fue como lo encontré.
Él dijo, "Puedes encontrar el fichero en c:\files\my_documents\file.txt". Y así fue como lo encontré.

Si se necesitara imprimir un carácter # en el texto, se puede usar r## al comienzo del texto y ## al final. Si se usaran más de dos consecutivos, se pueden seguir añadiendo # al comienzo y al final, hasta que no coincida con nada contenido en el texto.

fn main() {

    let my_string = "'Hola, mundo,' dijo."; // comilla simples
    let quote_string = r#""Hola, mundo," dijo."#; // comillas dobles
    // Contiene # se necesita al menos ##
    let hashtag_string = r##"El hasgtag #holamundo se ha hecho muy popular."##; 
    // Contiene ### se necesitan al menos ####
    let many_hashtags = r####""No se tiene que teclear ### para usar un hashtag. Solo hay que usar #.""####; 

    println!("{}\n{}\n{}\n{}\n", my_string, quote_string, hashtag_string, many_hashtags);
}

Este código imprimirá:

'Hola, mundo,' dijo.
"Hola, mundo," dijo.
El hasgtag #holamundo se ha hecho muy popular.
"No se tiene que teclear ### para usar un hashtag. Solo hay que usar #."

Existe otro uso para r#: usar palabras reservadas (como let, fn, etc.) como nombres de variable.

fn main() {
    let r#let = 6; // let como nombre de variable
    let mut r#mut = 10; // Esta variable se llama mut
}

Esta función de r# se introdujo porque las versiones más antiguas de Rust tenían menos palabras reservadas que ahora. Así se pueden evitar errores en código previo en el que se usaban nombres de variables que ahora son palabras reservadas.

Puede ser que por alguna razón realmente se necesite una función que se denomine como return. Así se puede escribir:

fn r#return() -> u8 {
    println!("Ahí va tu número.");
    8
}

fn main() {
    let mi_numero = r#return();
    println!("{}", mi_numero);
}

Que imprime:

Ahí va tu número.
8

Esto no se va a necesitar normalmente, pero si realmente hace falta, se puede usar.

Si se necesita imprimir los bytes de un &str o un char, basta con escribir la letra b delante de la cadena. Esto funciona para todos los caracteres ASCII. Estos son todos los caracteres ASCII.

☺☻♥♦♣♠♫☼►◄↕‼¶§▬↨↑↓→∟↔▲▼123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~

Así, este código:

fn main() {
    // N.T.: no se pueden poner tildes ya que en las 
    // vocales ya que las letras resultantes no forman
    // parte del conjunto de caracteres ASCII
    println!("{:?}", b"Esto aparece como una lista de numeros");
}

Da el siguiente resultado:

[69, 115, 116, 111, 32, 97, 112, 97, 114, 101, 99, 101, 32, 99, 111, 109, 111, 32, 117, 110, 97, 32, 108, 105, 115, 116, 97, 32, 100, 101, 32, 110, 117, 109, 101, 114, 111, 115]

Para un char esto se llama byte y para el tipo &str se llama byte de cadena de texto.

También existe un sistema de escape para insertar caracteres Unicode: \u{}. Así es posible imprimir cualquier carácter Unicode en una cadena de texto. Además, es posible formatear un número en hexadecimal usando {:X}. El siguiente ejemplo demuestra cómo imprimir el código hexadecimal que representa al carácter Unicode y cómo imprimirlo de nuevo como carácter.

fn main() {
    // Convierte el char a u32 para obtener su valor numérico
    println!("{:X}", '행' as u32); 
    println!("{:X}", 'H' as u32);
    println!("{:X}", '居' as u32);
    println!("{:X}", 'い' as u32);

    // Imprime los caracteres con el sistema de escape \u
    println!("\u{D589}, \u{48}, \u{5C45}, \u{3044}"); 
}

Ya se conoce que println! puede imprimir con {} (modo Display) t {:?} (para Depuración). Además de {:#?} para "impresión bonita". Pero existen otras muchas formas de imprimir.

Por ejempli, si se dispone de una referencia se puede usar {:p} para imprimir la dirección del puntero. Es decir, el lugar de la memoria del ordenador a la que apunta la referencia.

fn main() {
    let number = 9;
    let number_ref = &number;
    println!("{:p}", number_ref);
}

El código anterior imprime algo parecido a 0x7ffec9426f4c, dependerá de dónde se ejecute el programa y se almacene el número referenciado en la memoria del ordenador.

Los valores numéricos se pueden imprimir en binario, hexadecimal u octal:

fn main() {
    let number = 555;
    println!("Binario: {:b}, hexadecimal: {:x}, octal: {:o}", number, number, number);
}

Que imprime Binario: 1000101011, hexadecimal: 22b, octal: 1053.

También se pueden añadir números entre las llaves para indicar qué variable utilizar, teniendo en cuenta que la primera tiene como índice el 0, la segunda el 1 y así sucesivamente.

fn main() {
    let nombre_padre = "José Miguel";
    let nombre_hijo = "Víctor";
    let apellido = "González";
    println!("Este es {1} {2}, hijo de {0} {2}.", nombre_padre, nombre_hijo, apellido);
}

La variable nombre_padre está en la posición 0, nombre_hijoen la 1 y apellido está en la posición 2. Por eso, el código anterior imprime Este es Víctor González, hijo de José Miguel González..

Puede suceder que sea necesario imprimir una cadena de caracteres compleja con muchas variables dentro de las llaves. O puede que se necesite imprimir la misma variable dos o más veces. Para ello, se pueden añadir nombres a las llaves.

fn main() {
    println!(
        "{city1} está en {pais} y {city2} también está en {pais},
pero {city3} no está en {pais}.",
        city1 = "Seul",
        city2 = "Busan",
        city3 = "Tokio",
        pais = "Korea"
    );
}

Que imprime:

Seul está en Korea y Busan también está en Korea,
pero Tokio no está en Korea.

También es posible editar de forma compleja el formato de la impresión. Tiene esta forma:

{variable:relleno alineamiento mínimo.máximo}

Para entender esta sintaxis:

  1. ¿Se quiere usar un nombre de variable? Se escribe primero su nombre, como antes en {pais}. Lo siguiente será añadir (opcionalmente) : después si se quiere formatear de algún modo.
  2. ¿Se se necesita un carácter de relleno? Por ejemplo, 55 con tres "ceros de relleno" se imprimiría como 00055.
  3. ¿Que alineamiento se necesita para el relleno? izquierda, centro o derecha.
  4. ¿Se desea una longitud mínima? solo hay que indicar el número deseado.
  5. ¿Se desea una longitud máxima? solo hay que indicarla con un . delante.

Por ejemplo, si se quiere escribir la letra "a" con cinco caracteres - de relleno (a izquierda y derecha):

fn main() {
    let letra = "a";
    println!("{:-^11}", letra);
}

Que imprime -----a-----. El ordenador lo interpreta así:

  • ¿Hay un nombre de variable? No, ya que lo primero que aparece en {:-^11} son los dos puntos. No hay nombre de variable delante de estos dos puntos.
  • ¿Se pide un carácter de relleno? Sí, - está justo después de : y lo sigue un ^, lo que significa que el texto se inserta en el centro y el carácter de relleno se reparte en los espacios sobrantes a izquierda y derecha. Las otras dos posibilidades son: < que indica que el texto va a la izquierda y el carácter de relleno a la izquierda, y > que indica que el texto va a la derecha con el relleno a la izquierda.

A continuación, se muestran diversos ejemplos de tipos de formateo.

fn main() {
    let titulo = "NOTICIAS DE HOY";
    // sin variable, relleno con -, centrado, longitud de 30 caracteres
    println!("{:-^30}", titulo); 
    let barra = "|";
    // sin variable, relleno con espacios, 15 caracters cada uno, una barra a izquierda y otra a derecha
    println!("{: <15}{: >15}", barra, barra); 
    let a = "SEUL";
    let b = "TOKIO";
     // variables city1 y city2, relleno con -, a izquierda y a derecha
     println!("{city1:-<15}{city2:->15}", city1 = a, city2 = b);
}

El código anterior imprime:

-------NOTICIAS DE HOY--------
|                            |
SEUL---------------------TOKIO

Cadenas de caracteres

Puedes ver este capítulo en YouTube en inglés

Rust tiene dos tipos de cadenas de caracteres: String y &str. ¿Cuál es la diferencia?

  • &str es una cadena de caracteres simple que reside en la pila. Cuando se escribe let mi_variable = "¡Hola, mundo!" se crea una &str. Este tipo es muy rápido.
  • String es un tipo de dato más complejo. Es un poco más lento, pero tiene más funciones. Una String es un puntero que almacena los datos en la memoria dinámica.

Hay que destacar que &str tiene & como primer carácter debido a que es necesaria una referencia para utilizar el tipo str. Esto es por la razón que vimos anteriormente: la pila necesita conocer el tamaño, así que se le da una referencia, las referencias siempre tienen el mismo tamaño. Otro tema a tener en cuenta es que al utilizar & una referencia para interactuar con el tipo str, nunca se es dueño del tipo. Por el contrario, Stringes un tipo con dueño. Más adelante se mostrará la importancia de esta distinción.

Ambos tipos, &str y String son UTF-8. Por ejemplo, se puede escribir el siguiente código:

fn main() {
    // Nombre en coreano. No da problemas, ya que &str es UTF-8
    let nombre = "서태지";
    // Ț y ș no son un problema en UTF-8.
    let otro_nombre = String::from("Adrian Fahrenheit Țepeș");
}

En el código anterior se observa que se puede construir de forma fácil una String de una &str. Los dos tipos están muy relacionados, aunque son muy diferentes.

Gracias a UTF-8, incluso se pueden escribir emojis.

fn main() {
    let nombre = "😂";
    println!("Mi nombre real es {}", nombre);
}

Si se ejecuta este código en el terminal de comandos de tu propio ordenador tiene que aparecer Mi nombre real es 😂 a menos el terminal de comandos presente limitaciones y no lo pueda imprimir. En cuyo caso imprimirá algo así Mi nombre real es �. En todo caso, Rust es capaz de manejar todos los caracteres Unicode.

La razón para utilizar una referencia &para el tipo str es que str es un tipo de datos de tamaño dinámico, su tamaño puede ser diferente. Por ejmplo, los nombres "서태지" y "Adrian Fahrenheit Țepeș" no son del mismo tamaño:

fn main() {

    // std::mem::size_of::<Type>() devuelve el tamaño en bytes de un tipo
    println!("Una String siempre ocupa {:?} bytes. Es de tamaño fijo.",
        std::mem::size_of::<String>()); 
    println!("Y un i8 siempre ocupa {:?} bytes. Es de tamaño fijo.", 
        std::mem::size_of::<i8>());
    println!("Y un f64 siempre ocupa {:?} bytes. Es de tamaño fijo.", 
        std::mem::size_of::<f64>());
     // std::mem::size_of_val() devuelve el tamaño en bytes de una variable
    println!("¿Y un &str? Puede ocupar cualquier tamaño. '서태지' ocupa {:?} bytes. No es de tamaño fijo.",
        std::mem::size_of_val("서태지"));
    println!("Y 'Adrian Fahrenheit Țepeș' ocupa {:?} bytes. No es de tamaño fijo.",
        std::mem::size_of_val("Adrian Fahrenheit Țepeș"));
}

Lo que da como resultado:

Una String siempre ocupa 24 bytes. Es de tamaño fijo.
Y un i8 siempre ocupa 1 bytes. Es de tamaño fijo.
Y un f64 siempre ocupa 8 bytes. Es de tamaño fijo.
¿Y un &str? Puede ocupar cualquier tamaño. '서태지' ocupa 9 bytes. No es de tamaño fijo.
Y 'Adrian Fahrenheit Țepeș' ocupa 25 bytes. No es de tamaño fijo.

Por eso es necesario usar &, porque así se construye un puntero (tipo de tamaño fijo) que puede almacenarse en la pila. Si se escribiera str, Rust no sabría qué hacer al no conocer su tamaño.

Hay muchas formas de construir un elemento de tipo String. Algunas de ellas son:

  • String::from("Esta es una cadena de texto"); - String::from() es un método de Strgin que crea un String a partir de una cadena de texto.
  • "Esta es una cadena de texto".to_string() - "".to_string() es un método de &str que crea un String.
  • La macro format! - Es como println! excepto que crea un String en lugar de imprimir el texto.

A continuación se muestran algunos ejemplos:

fn main() {
    let my_name = "Billybrobby";
    let my_country = "USA";
    let my_home = "Korea";

    let together = format!(
        "Soy {} y vengo de {}, pero vivo en {}.",
        my_name, my_country, my_home
    );
}

Así se construye un objeto String denominado together, pero no se ha impreso.

Otra forma adicional para crear un String es con la función into(), pero esta forma es algo diferente ya que no solo sirve para crear String. Algunos tipos se pueden convertir de forma fácil en otros utilizando from y into(). Si el tipo tiene from, tiene también into(). from resulta más claro ya que con él conoces los tipos: al usarlo String::from("Cadena de texto") se sabe que se crea una String de &str. Sin embargo, con .into() el compilador, a veces, no lo conoce:

fn main() {
    let my_string = "Intento construir un String".into(); // ⚠️
}

Rust no conoce el tipo al que se quiere convertir la cadena de texto, porque se pueden crear muchos tipos diferentes a partir de un &str. Rust se queja: puedo convertir &str en muchos tipos diferentes. ¿Cuál es el que quieres?

error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let my_string = "Intento construir un String".into(); // ⚠️
  |         ^^^^^^^^^ consider giving `my_string` a type

Por lo que se puede corregir así:

fn main() {
    let my_string: String = "Intento construir un String".into();
}

Y ahora sí se ha creado un objeto String.

const y static

Puedes ver este capítulo en YouTube en inglés

Además de let, existen dos maneras más de declarar valores. const y static. Para estas declaraciones, Rust no realiza la inferencia de los tipos: es necesario declarar el tipo de los valores. Existen vamores que no cambian (const significa constante). La diferencia entre ellos es que:

  • const se utiliza para los valores que no cambian y el nombre se reemplaza por su valor, allí donde se usa.
  • static define una posición fija en memoria que puede actuar como una variable global.

Por ello, son casi lo mismo. Los programadores de Rust casi siempre utilizan valores constantes con const.

Por convención, las constantes se suelen escribir con todas las letras en mayúsculas, normalmente están fuera del main para que existan en todo el programas.

Dos ejemplos:

const NUMERO_DE_MESES: u32 = 12;
static ESTACIONES: [&str; 4] = ["Primavera", "Verano", "Otoño", "Invierno"];

fn main() {
    println!("{}", NUMERO_DE_MESES);
    println!("{:?}", ESTACIONES);
}

Algo más sobre referencias

Puedes ver este capítulo en YouTube en inglés

Las referencias son muy importantes en Rust. Las utiliza para asegurarse de que son seguros todos los accesos a la memoria. Ya se ha explicado anteriormente que para crear una referencia se utiliza & delante del valor:

fn main() {
    let pais = String::from("Austria");
    let ref_uno = &pais;
    let ref_dos = &pais;

    println!("{}", ref_uno);
}

El código anterior imprime Austria. pais es un String. Se crean dos referencias a pais. Estas referencias son de tipo &String, es decir son dos variables que son "referencias a String". Se pueden crear tantas referencias a paiscomo se quiera. Todas "apuntan" al mismo valor, pero son punteros diferentes.

A continuación se muestra un ejemplo sobre cómo Rust proteje el acceso a zonas de memoria erróneas:

fn return_str() -> &str {
    let pais = String::from("Austria");
    let pais_ref = &pais;
    pais_ref // ⚠️
}

fn main() {
    let pais = return_str();
}

La función return_str() crea un valor de tipo String, luego crea una referencia a dicho valor. Cuando se intenta devolver la referencia se produce un error:

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:20
  |
1 | fn return_str() -> &str {
  |                    ^ 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
  |
1 | fn return_str() -> &'static str {
  |                    ^^^^^^^^

El valor de pais solo existe dentro de la función, al terminar de ejecutarse desaparece. Una vez la variable desaparece, el ordenador libera la memoria que ocupaba y la utiliza para otra cosa. Por eso, después de que la función termina, pais_ref apunta a una zona de memoria que ya no tiene el valor esperado y eso no es correcto. Rust previene este fallo en el código e impide que el programa compile.

Este es el efecto importante de la existencia de tipos de dato "con dueño" en Rust. En el código anterior, el valor String tiene como dueño a la variable pais, por eso se puede "prestar" a otras variables (referencias), pero cuando desaparece la variable dueña del valor, pais, la referencia también desaparece, la referencia tiene "prestado" el valor, por lo que no se puede pasar a otro "dueño".

Referencias modificables (mutables)

Puedes ver este capítulo en YouTube en inglés

Si se necesita modificar un valor a través de una referencia, se debe indicar que la referencia sea modificable (mutable). Para ello, se utiliza &mut en lugar de &.

fn main() {
    // no hay que olvidar que hay que escribir mut en la variable original
    let mut mi_numero = 8;
    let num_ref = &mut mi_numero;
}

mi_numero es de tipo i32, y num_ref es de tipo &mut i32 (es una "referencia modificable/mutable a un valor ì32).

Si se desea usar esta referencia para sumar 10, no se puede usar num_ref += 10 ya que num_ref no es de tipo i32, es &i32. Para obtener el valor de la referencia, se debe usar * que significa que "no se necesita la referencia, sino el valor que al que representa". En otras palabras, * es lo opuesto a &, un * borra a un &.

fn main() {
    let mut mi_numero = 8;
    let num_ref = &mut mi_numero;
    *num_ref += 10; // Usa * para cambiar el valor i32
    println!("{}", mi_numero);

    let segundo_numero = 800;
    let triple_referencia = &&&segundo_numero;
    println!("segundo_numero = ¿triple_referencia? {}",
        segundo_numero == ***triple_referencia);
}

El código anterior, da como resultado:

18
segundo_numero = ¿triple_referencia? true

El uso del operador sobre una variable & se denomina "referenciar". El uso del operador sobre una variable de referencia * se denomina "desreferenciar".

Rust usa dos reglas para las referencias mutables e inmutables. Son muy importantes y fáciles de recordar porque tienen sentido.

  • Regla 1: Si solo existen referencias inmutables a un valor, se pueden tener tantas como se quiera.
  • Regla 2: Si existe una referencia mutable a un valor, solo puede existir una referencia. Esto último significa que no pueden existir a la vez referencias inmutables y mutables en el mismo momento.

Estas reglas son necesarias debido a que las referencias mutables pueden cambiar los datos. Sería problemático que se modificara un dato cuando otras referencias lo están usando.

Un forma de entenderlo es pensar en la creación de una presentación de Powerpoint 1.

1

No se trata de un ejemplo excesivamente bueno, ya que actualmente es posible editar simultáneamente en Office 365.

El primer caso representa el de la existencia de una referencia mutable: un empleado está editando una presentación de Powerpoint. Piode ayuda a su jefe. El empleado entrega sus credenciales de acceso a su jefe para que pueda editar el Powerpoint. Ahora el jefe tiene una "referencia mutable" a la presentación de su empleado. El jefe puede hacer los cambios que quiera y devolver el ordenador más tarde. Nadie más tiene acceso a la presentación, por lo que este caso no da problemas.

El segundo caso representa el de **la existencia únicamente de referencias inmutables": El empleado entrega la presentación a 100 personas. Todas ellas pueden verla. Tienen una "referencia inmutable" a la presentación ya que nadie puede modificarla, por lo que este caso no da problemas.

El tercer caso representa la situación problemática a evitar: El empleado entrega sus credenciales de acceso a su jefe, que a partir de aquí dispone de una "referencia mutable". Además, el empleado entrega la presentación a 100 personas. El jefe puede entrar a editar y estas modificaciones pueden verse o no por parte de las 100 personas, según el momento en que accedan sin que puedan controlar.

Se puede ver que el intento de usar una referencia mutable simultáneamente con una inmutable no es aceptado por el compilador:

fn main() {
    let mut numero = 10;
    let numero_ref = &numero;
    let numero_modif = &mut numero;
    *numero_modif += 10;
    println!("{}", numero_ref); // ⚠️
}

El compilador muestra un mensaje explicativo muy claro para mostrar el problema:

error[E0502]: cannot borrow `numero` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:24
  |
3 |     let numero_ref = &numero;
  |                      ------- immutable borrow occurs here
4 |     let numero_modif = &mut numero;
  |                        ^^^^^^^^^^^ mutable borrow occurs here
5 |     *numero_modif += 10;
6 |     println!("{}", numero_ref); // ⚠️
  |                    ---------- immutable borrow later used here

Sin embargo, este código sí funciona. ¿Por qué?

fn main() {
    let mut numero = 10;
    let numero_modif = &mut numero; // referencia modificable
    *numero_modif += 10; // suma 10
    let numero_ref = &numero; // referencia inmutable
    println!("{}", numero_ref); // imprime el valor referenciado
}

Imprime 20 sin problemas. Funciona porque el compilador es suficientemente inteligente para comprender el código. Conoce que se ha cmabiado numero a través de la referencia numero_modif, pero después, esta referencia mutable no se vuelve a usar. Por eso no hay problema aquí. No se usan "a la vez" referencias inmutables y mutables (como sí pasaba en el caso anterior).

En versiones anteriores de Rust, este código sí daba error, pero actualmente no lo hace ya que el compilador es más inteligente. Puede entender no solo lo que se ha tecleado, sino como se usa todo.

Ocultación (shadowing) de nuevo

Es necesario recordar que el ocultamiento de variables no destruye sus valores, sino que los bloquea. Con el uso de las referencias esto se ve más claro.

fn main() {
    let pais = String::from("Austria");
    let pais_ref = &pais;
    let pais = 8;
    println!("{}, {}", pais_ref, pais);
}

¿Qué imprime este código? ¿Austria, 8 o 8, 8? Imprime Austria, 8. En la primera línea, se declara una variable String con el valor Austria y denominada pais. En la segunda línea se crea una referencia al valor de esta variable. Es decir, a la String que contiene Austria. Después, se oculta la variable pais con una nueva cuyo valor es 8 y que es de tipo i32. El primer elemento pais no se destruyó (lo hará al finalizar el ámbito en el que está, en la llave de cierre de este bloque), por lo que sigue accesible a través de la referencia pais_ref. Para mayor claridad, se vuelve a mostrar el código, ahora con comentarios sobre su comportamiento.

fn main() {
    let pais = String::from("Austria"); // String denominada pais
    let pais_ref = &pais; // pais_ref es una referencia al valor
    let pais = 8; // nueva variable pais con un valor 8. Sin relación con la anterior, ni con pais_ref
    println!("{}, {}", pais_ref, pais); // pais_ref sigue "apuntando" al dato Austria
}

Paso de referencias a funciones

Puedes ver este capítulo en Youtube en inglés: referencias inmutables y referencias mutables

Las referencias son muy útiles en las funciones. La regla de Rust para todos los valores es que un valor solo puede tener una variable propietario.

Este código no funcionará:

fn print_pais(pais_nombre: String) {
    println!("{}", pais_nombre);
}

fn main() {
    let pais = String::from("Austria");
    print_pais(pais); // Se imprime "Austria"
    print_pais(pais); // ⚠️ Se intenta de nuevo, pero no funciona
}

Devuelve el siguiente error:

error[E0382]: use of moved value: `pais`
 --> src/main.rs:8:16
  |
6 |     let pais = String::from("Austria");
  |         ---- move occurs because `pais` has type `String`, which does not implement the `Copy` trait
7 |     print_pais(pais); // Se imprime "Austria"
  |                ---- value moved here
8 |     print_pais(pais); // ⚠️ Se intenta de nuevo, pero no funciona
  |                ^^^^ value used here after move

La variable pais ya no existen en la última línea. El funcionamiento es el siguiente:

  • Paso 1: Se crea el valor de tipo String cuyo dueño es la variable pais.
  • Paso 2: Se pasa pais a la función print_pais. Es una función que no tiene -> en su declaración, por lo que no retorna ningún valor. Al hacer la llamada, la variable pais_nombre (parámetro) es la nueva dueña del valor. Después de que esta función finaliza, pais_nombre desaparece, como es la dueña del valor, este también se destruye.
  • Paso 3: Se intenta pasar por segunda vez pais a la función `print_pais, pero ya no existe ya que dejó de ser dueña del valor y el valor despareció dentro de la primera llamada a la función.

Se podría hacer que la función print_pais devolviera de nuevo el valor String, pero es poco ortodoxo.

fn print_pais(pais_nombre: String) -> String {
    println!("{}", pais_nombre);
    pais_nombre // se devuelve el valor
}

fn main() {
    let pais = String::from("Austria");
    let pais = print_pais(pais); // Es necesario crear una nueva variable para recuperar el valor
    print_pais(pais);
}

Ahora sí funciona e imprime:

Austria
Austria

Es mucho mejor evitar que la variable print_nombre, parámetro de la función, sea dueña del valor, solamente se "presta", sin que la variable pais de la función main deje de ser su dueño.

fn print_pais(pais_nombre: &String) {
    println!("{}", pais_nombre);
}

fn main() {
    let pais = String::from("Austria");
    print_pais(&pais); // Se imprime "Austria"
    print_pais(&pais); // Se intenta de nuevo y funciona correctamente
}

En este caso print_pais() toma una referencia a una String: &String. Cuando se llama print_pais() se pasa una referencia con &pais. Esto significa que "la función puede acceder al valor, pero no se hace dueña de él".

A continuación, se muestra un ejemplo similar para observar el comportamiento de las referencias modificables (mutables).

fn añade_hungria(pais_nombre: &mut String) { // se pasa una referencia mutable
    pais_nombre.push_str("-Hungría"); // push_str() añade un &str a un String
    println!("Ahora dice: {}", pais_nombre);
}

fn main() {
    // es importante que la variable se declare
    // como mutable para poder crear referencias mutables
    let mut pais = String::from("Austria");
    añade_hungria(&mut pais); // hay que pasar la referencia mutable.
    println!("Y el valor se ha modificado aquí: {}", pais);
}

Que imprime:

Ahora dice: Austria-Hungría
Y el valor se ha modificado aquí: Austria-Hungría

En resumen:

  • fn nombre_de_funcion(variable: String) toma un String y se hace dueño de él.
  • fn nombre_de_funcion(variable: &String) toma prestado un String y puede acceder a su valor.
  • fn nombre_de_funcion(variable: &mut String) toma prestado un String, puede acceder a su valor y modificarlo.

El siguiente ejemplo puede parecer similar, pero es muy diferente. Sirve para mostrar cómo quien se hace dueño de un objeto puede decidir que sea modificable, aunque anteriormente no lo fuese.

fn main() {
    let pais = String::from("Austria"); // pais no es mutable, ni referencia
    añadir_hungria(pais);
}

fn añadir_hungria(mut pais: String) { // añadir_hungria declara su parámetro como mutable
    pais.push_str("-Hungría");
    println!("{}", pais);
}

La función añadir_hungria se hace dueña del valor en su variable mut pais. A partir de ahí, puede hacer con este valor lo que quiera.

Si se recuerda el ejemplo anterior sobre el empleado, su jefe y la presentación en powerpoint, esta es la situación en la que el empleado le da el control completo al jefe. El empleado no puede volver a tocar la presentación y el jefe puede hacer lo que quiera con ella.

Copia

Algunos tipos de Rust son muy simples. Se almacenan todos en la pila ya que el compilador conoce su tamaño. Esto significa que son fáciles de copiar, por lo que el compilador siempre los copia cuando se envían a una función. Son valores de tamaño fijo, conocido y pequeño. En estos casos, no hay necesidad de preocuparse por quién es el dueño de estos tipos de dato. A estos tipos, se los denomina tipos copia (Copy Types, en inglés).

Estos tipos simples incluyen a los enteros, flotantes, booleanos (true -verdadero- y false -falso-) y char.

Para que los tipos se puedan copiar tienen que implementar la posibilidad de copia (copy). Se puede consultar la documentación de cada tipo para conocerlo. Por ejemplo, esta es la documentación del tipo char:

https://doc.rust-lang.org/std/primitive.char.html

A la izquierda de esta documentación se puede ver Trait Implementations. Se muestran, entre otras: Copy, Debug y Display. Lo que permite conocer que char puede:

  • Copiarse cuando se pasa como parámetro a una función (Copy).
  • Puede usar {} para imprimir (Display).
  • Puede usar {:?} para imprimir (Debug).
fn prints_number(numero: i32) { // Esta función no tiene ->, no devuelve ningún valor
                             // Si el número no se copiara, esta función se haría au propietaria 
                             // y no se podría volver a usar
    println!("{}", numero);
}

fn main() {
    let mi_numero = 8;
    prints_number(mi_numero); // Imprime 8. prints_number obtiene una copia del número
    prints_number(mi_numero); // Imprime 8 de nuevo.
                              // No hay problema ya que mi_numero es un tipo que se copia
}

Sin embargo, si se revisa la documentación de String se ve que no es un tipo que se copie.

https://doc.rust-lang.org/std/string/struct.String.html

A la izquierda de esta documentación se puede ver Trait Implementations, en orden alfabético. En la C no está Copy. Lo que sí aparece es Clone, que es similar a *Copy, solo que es necesario invocarlo expresamente con el método clone(). Es decir, que no se clona por sí mismo, se tiene que pedir expresamente.

En el siguiente ejemplo, prints_country() imprime el nombre del país, que es de tipo String. Se quiere, como en el caso anterior, imprimir dos veces, pero no es posible:

fn prints_country(country_name: String) {
    println!("{}", country_name);
}

fn main() {
    let country = String::from("Kiribati");
    prints_country(country);
    prints_country(country); // ⚠️
}

El mensaje es autoexplicativo:

error[E0382]: use of moved value: `country`
 --> src\main.rs:4:20
  |
2 |     let country = String::from("Kiribati");
  |         ------- move occurs because `country` has type `std::string::String`, which does not implement the `Copy` trait
3 |     prints_country(country);
  |                    ------- value moved here
4 |     prints_country(country);
  |                    ^^^^^^^ value used here after move

Dice que String no implementa el rasgo necesario para copiar Copy trait. Sin embargo, se ha visto que sí implementa el rasgo Clone, por lo que se puede añadir .clone() al código para generar de forma expresa una copia del valor. De este modo, se puede enviar un clon del valor a la función. Así, después de llamar a la función, la variable country continúa vigente y se puede utilizar.

fn prints_country(country_name: String) {
    println!("{}", country_name);
}

fn main() {
    let country = String::from("Kiribati");
    prints_country(country.clone()); // crea un clon y lo pasa a la función. country sigue vigente
    prints_country(country);
}

Evidentemente, si el String es muy largo, generar un clon requiere el uso de mucha memoria. Una cadena de texto String puede ser de la longitud de un libro entero, y cada vez que se llama a .clone() se genera una copia completa. Por eso, es recomendable el uso de & para utilizar una referencia cuando sea posible. Por ejemplo, el código siguiente añade una cadena de texto &str en una String y después crea un clon cada vez que se utiliza en una función:

fn get_length(input: String) { // Se apropia de la cadena de texto
    println!("Tiene una longitud de {} palabras.", input.split_whitespace().count()); // la divide para contar el número de palabras
}

fn main() {
    let mut my_string = String::new();
    for _ in 0..50 {
        my_string.push_str("Aquí van palabras en español "); // añade las palabras
        get_length(my_string.clone()); // obtiene un nuevo clon cada vez
    }
}

Lo que imprime es:

Tiene una longitud de 5 palabras.
Tiene una longitud de 10 palabras.
...
Tiene una longitud de 250 palabras

Esto genera 50 clones. El siguiente código cumple la misma función usando referencias y es mucho más eficiente:

fn get_length(input: &String) {
    println!("Tiene una longitud de {} palabras.", input.split_whitespace().count());
}

fn main() {
    let mut my_string = String::new();
    for _ in 0..50 {
        my_string.push_str("Aquí van palabras en español ");
        get_length(&my_string);
    }
}

Con el código anterior no se genera ningún clon.

Variables sin valores

Una variables sin valor está sin inicializar. Para crear una variable en este estado se usa let y el nombre de la variable:

fn main() {
    let my_variable; // ⚠️
}

Rust no compila si hay alguna variable sin inicializar.

Sin embargo, en ocasiones pueden ser útiles. Un claro ejemplo es cuando:

  • Se tiene un bloque de código en el que se genera el valor necesario para la variable y...
  • ...la variable tiene que sobrevivir fuera del bloque de código.
fn loop_then_return(mut counter: i32) -> i32 {
    loop {
        counter += 1;
        if counter % 50 == 0 {
            break;
        }
    }
    counter
}

fn main() {
    let my_number;

    { // Este bloque es innecesario, pero se crea para documentar caso
        let number = {
            // Aquí podria haber mucho código
            // para generar un valor, por ejemplo:
            57
        };

        my_number = loop_then_return(number);
    }

    println!("{}", my_number);
}

Este ejemplo imprime 100.

Se observa que my_number se declarón en la función main(), por lo que su tiempo de vida dura hasta su finalización. Y obtiene su valor dentro del bucle loop. El valor pasa a ser propiedad de my_number antes de salir del bloque.

Si se hubiera declarado y asignado el valor en la misma línea con let my_number = loop_then_return(number) dentro del bloque, la variable hubiera desaparecido con el bloque.

De forma simplificada, ayuda a verlo el sustituir la función por su valor de retorno, 100. Se ve en el siguiente código:

fn main() {
    let my_number;
    {
        my_number = 100;
    }

    println!("{}", my_number);
}

Es casi como escribir let my_number = { 100 };.

Se debe observar que my_number no es mut. No se le asigna un valor hasta que se asigna el 100, por lo que no ha cambiado de valor. En el fondo el código real para my_number es solo let my_number = 100;.

Tipos colección

Rust tiene muchos tipos para construir una colección. La colecciones sirven para almacenar más de un valor en un mismo lugar. Por ejemplo, permiten almacenar la información sobre todas las ciudades de un país en una variable.

Se iniciará con los arrays, que son los más rápidos, pero también los que tienen la funcionalidad más limitada. En este sentido, son como el tipo &str.

Arrays

Son los datos guardados dentro de corchetes: [].

Los Arrays:

  • No pueden cambiar de tamaño.
  • Tienen datos del mismo tipo.

Sin embargo, son muy rápidos.

El tipo de un array es: [tipo; longitud]. Por ejemplo, el tipo de ["Uno", "Dos"] es [&str; 2]. Esto significa que los siguientes dos arrays tienen tipos diferentes:

fn main() {
    let array1 = ["One", "Two"]; // Este es de tipo [&str; 2]
    let array2 = ["One", "Two", "Five"]; // Y este de tipo [&str; 3]. ¡Son dos tipos diferentes!
}

Para conocer el tipo de una variable se puede "pedir" al compilador que haga algo con ella que no sea válido, por ejemplo:

fn main() {
    let seasons = ["Spring", "Summer", "Autumn", "Winter"];
    let seasons2 = ["Spring", "Summer", "Fall", "Autumn", "Winter"];
    seasons.ddd(); // ⚠️
    seasons2.thd(); // ⚠️ también
}

El compilador dice, "¿Qué haces? No existe el método .ddd() para la variable seasons y tampoco existe el método .thd() para seasons2" como puedes ver:

error[E0599]: no method named `ddd` found for array `[&str; 4]` in the current scope
 --> src\main.rs:4:13
  |
4 |     seasons.ddd(); // 
  |             ^^^ method not found in `[&str; 4]`

error[E0599]: no method named `thd` found for array `[&str; 5]` in the current scope
 --> src\main.rs:5:14
  |
5 |     seasons2.thd(); // 
  |              ^^^ method not found in `[&str; 5]`

Y dice que method not found in `[&str; 4]`, que es el tipo del array.

Si se necesita un array con el mismo valor en todos los elementos, se puede declarar de la siguiente forma:

fn main() {
    let my_array = ["a"; 10];
    println!("{:?}", my_array);
}

Que imprime ["a", "a", "a", "a", "a", "a", "a", "a", "a", "a"].

Este método se utiliza mucho para crear buffers. Por ejemplo, let mut buffer = [0;640] crea un array de 640 ceros. Posteriormente, se puede modificar el valor cero por otro dato.

Se pueden indexrar los valores (recuperarlos) con []. El primer valor es [0], el segundo [1] y así sucesivamente.

fn main() {
    let my_numbers = [0, 10, -20];
    println!("{}", my_numbers[1]); // imprime 10
}

Se puede obtener una sección (slice) de un array. Lo primero que se necesita es una referencia & porque el compilador no conoce el tamaño. Después se puede usar .. para mostrar el rango.

Por ejemplo, si se utiliza el siguiente array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].

fn main() {
    let array_of_ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let three_to_five = &array_of_ten[2..5];
    let start_at_two = &array_of_ten[1..];
    let end_at_five = &array_of_ten[..5];
    let everything = &array_of_ten[..];

    println!("Tres a cinco: {:?}, comienza en el segundo: {:?}, finaliza en el quinto: {:?}, todo: {:?}", three_to_five, start_at_two, end_at_five, everything);
}

Se debe recordar que:

  • Los números de índice comienzan en 0 (no en 1).
  • los rangos son excluyentes (es decir, no incluyen el último número).

Así, [0..2] obtiene el primer y segundo valor (0 y 1). Dicho de otro modo, el índice cero y uno. No obtiene el tercer valor, cuyo índice es dos.

Es posible establecer un rango inclusivo, que sí incluya el último número del rango. Para ello se escribe ..=, en lugar de ... Así que [0..=2] permite obtener el primer, segundo y tercer elemento del array.

Vectores

Puedes ver este capítulo en Youtube en inglés

Del mismo modo que se dispone de &str y String, se dispone de arrays y vectores. Los arrays son más rápidos, pero tienen menos funcionalidad, y los vectores son más lentos, pero tienen más funcionalidad (Rust siempre es muy rápido, solo que los vectores no son tan rápidos como los arrays). El tipo es Vec y, por lo tanto, se le puede llamar como "vec".

Existen principalemente dos formas de declarar un vector. Una es igual a como se crea un String, mediante el uso de new:

fn main() {
    let name1 = String::from("Windy");
    let name2 = String::from("Gomesy");

    let mut my_vec = Vec::new();
    // Si se compilara este programa hasta aquí, el compilador dará un error.
    // ya que no conoce el tipo de datos del vec.

    my_vec.push(name1); // Ahora sí lo conoce, es un Vec<String>
    my_vec.push(name2);
}

Los Vec siempre contienen valores y para eso sirven <> (los paréntesis angulares). Un Vec<String> es un vector que contiene elementos String. Algunos otros ejemplos son:

  • Vec<(i32, i32)> es un vector en el que cada elemento de contenido es una tupla (i32, i32).
  • Vec<Vec<String>> es un vector en el que cada elemento es otro vector de String. Por ejemplo, se puede pensar en almacenar el texto de un libro como un Vec<String>. Para almacenar varios libros haría falta crear una lista de elementos del tipo anterior y esto se puede hacer en otro Vec que contiene Vec<String>. Por lo tanto, el tipo resultante sería así Vec<Vec<String>>.

En lugar de usar .push() para llegar a deducir el tipo de elementos que contiene un vector, se puede declarar el tipo:

fn main() {
    let mut my_vec: Vec<String> = Vec::new(); // El compilador conoce el tipo
                                              // Por eso no hay error
}

Como se observa, todos los elementos de un vector tienen que tener un mismo tipo.

Otra forma sencilla de crear un vector es usando la macro vec!, cuya sintaxis recuerda a la declaración de un array.

fn main() {
    let mut my_vec = vec![8, 10, 10];
}

El tipo de los elementos, en este ejemplo, es Vec<i32>. Un vector de enteros.

Se pueden obtener secciones de un vector, igual que como se hace para un array.

fn main() {
    let vec_of_ten = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    // Todo el código es idéntico, salvo que se añade vec!.
    let three_to_five = &vec_of_ten[2..5];
    let start_at_two = &vec_of_ten[1..];
    let end_at_five = &vec_of_ten[..5];
    let everything = &vec_of_ten[..];

    println!("Three to five: {:?},
start at two: {:?}
end at five: {:?}
everything: {:?}", three_to_five, start_at_two, end_at_five, everything);
}

Puesto que un vector es más lento que un array, se pueden usar diversos métodos para hacerlo más rápido. Un vector tiene una capacidad de espacio asignada, que permite que al ir insertando nuevos elementos en el vector, se haga rápidamente. Cada vez que se hace esto, el vector se acerca al límite de su capacidad. Cuando esta se supera, Rust crea un nuevo espacio del doble del tamaño actual y copia todos los elementos al nuevo espacio. Esto se denomina relocalización. Se puede usar el método .capacity()para ver la capacidad de un vector según se le van añadiendo elementos.

fn main() {
    let mut num_vec = Vec::new();
    println!("{}", num_vec.capacity()); // 0 elementos: immprime 0
    num_vec.push('a'); // añade un carácter
    println!("{}", num_vec.capacity()); // 1 elemento: imprime 4. Vecs con 1 elemento siempre se inician con una capacidad de 4
    num_vec.push('a'); // añade uno más
    num_vec.push('a'); // añade uno más
    num_vec.push('a'); // añade uno más
    println!("{}", num_vec.capacity()); // 4 elementos: aún 4 de capacidad.
    num_vec.push('a'); // añade uno más
    println!("{}", num_vec.capacity()); // imprime 8. Son 5 elementos, pero ha doblado la capacidad de 4 a 8 para hacer espacio.
}

Esto imprime:

0
4
4
8

Así que este vector ha sufrido dos relocalizaciones: de 0 a 4 y de 4 a 8. Para que fuese más rápido se puede iniciar así:

fn main() {
    let mut num_vec = Vec::with_capacity(8); // Se crea con una capacidad inicial de 8
    num_vec.push('a'); // se añade un carácter
    println!("{}", num_vec.capacity()); // imprime 8
    num_vec.push('a'); // añade uno más
    println!("{}", num_vec.capacity()); // imprime 8
    num_vec.push('a'); // añade uno más
    println!("{}", num_vec.capacity()); // imprime 8.
    num_vec.push('a'); // añade uno más
    num_vec.push('a'); // añade uno más
    // Ahora hay 5 elementos
    println!("{}", num_vec.capacity()); // Aún 8
}

Este vector no ha sufrido ninguna relocalización, lo que es mejor. Por eso, si se conoce a priori el número de elementos que se necesitará, se puede inicializar el vector con Vec::with_capacity() para que funcione más rápido.

En el caso de las &str se podía utilizar .into() para convertirlo en una String. Igualmente, se puede convertir un array en un vector con la misma función.

fn main() {
    let my_vec: Vec<u8> = [1, 2, 3].into();
    let my_vec2: Vec<_> = [9, 0, 10].into(); // Vec<_> significa "elige el tipo del Vector por mí"
                                             // Rust elegirá Vec<i32>
}

Tuplas

Se puede ver este capítulo en Youtube en inglés

En Rust las tuplas usan (). Ya han aparecido muchas tuplas vacías ya que nada, en una función, realmente significa una tupla vacía:

fn do_something() {}

realmente es igual a:

fn do_something() -> () {}

Esta función no recibe ningún parámetro (recibe una tupla vacía) y no retorna ningún valor (una tupla vacía). Por eso, se han estado usando ya muchas tuplas en los ejemplos de capítulos anteriores. Cuando no se retorna ningún valor en una función, se está retornando una tupla vacía.

fn just_prints() {
    println!("Estoy imprimiendo"); // Al añadir un ;
    // como último dato antes de terminar la función
    // se está indicando que se debe retornar una tupla vacía
}

fn main() {}

Las tuplas pueden almacenar muchos tipos de elementos diferentes a la vez. Los elementos incluidos se indexan con números de 0 en adelante. Para acceder a ellos se utiliza el operador . en lugar de []. A continuación se incorporan datos de diferentes tipos en una única tupla:

fn main() {
    let random_tuple = ("Esto es un texto", 8, vec!['a'], 'b', [8, 9, 10], 7.7);
    println!(
        "El interior de la tupla contiene: Primer elemento: {:?}
Segundo elemento: {:?}
Tercer elemento: {:?}
Cuarto elemento: {:?}
Quinto elemento: {:?}
Sexto elemento: {:?}",
        random_tuple.0,
        random_tuple.1,
        random_tuple.2,
        random_tuple.3,
        random_tuple.4,
        random_tuple.5,
    )
}

Que imprime:

El interior de la tupla contiene: Primer elemento: "Esto es un texto"
Segundo elemento: 8
Tercer elemento: ['a']
Cuarto elemento: 'b'
Quinto elemento: [8, 9, 10]
Sexto elemento: 7.7

Esta tupla es de tipo (&str, i32, Vec<char>, char, [i32; 3], f64).

Se puede usar una tupla para crear múltiples variables. En el siguiente código:

fn main() {
    let str_vec = vec!["one", "two", "three"];
}

str_vec contiene tres elementos. ¿Cómo se pueden recuperar los tres valores de este vector en diferentes variables? Por ejemplo, con una tupla:

fn main() {
    let str_vec = vec!["one", "two", "three"];

    let (a, b, c) = (str_vec[0], str_vec[1], str_vec[2]); // las variables serán a, b, y c
    println!("{:?}", b);
}

El resultado del código anterior imprime "two", que es lo que contiene la variable b. A esta forma de extraer valores en variables se denomina desestructuramiento.

Si se necesita desestructurar un conjunto de elementos, pero no se quieren todos, se puede utilizar _.

fn main() {
    let str_vec = vec!["one", "two", "three"];

    let (_, _, variable) = (str_vec[0], str_vec[1], str_vec[2]);
}

El código anterior solo crea una variable denominada variable, pero no las crea para el resto de elementos.

Existen muchos más tipos colección y muchas formas adicionales de utilizar a los arrays, vectores y tuplas. Se enseñará más sobre ellos más adelante, pero primero, se enseñará la forma de controlar el flujo de ejecución de un programa.

Estructuras de control

Se puede ver este capítulo en YouTube en inglés: Parte 1 y Parte 2

Las estructuras de control del flujo de ejecución permiten indicar qué código debe ejecutarse en cada caso. La estructura de control de flujo más simple es if.

fn main() {
    let my_number = 5;
    if my_number == 7 {
        println!("Es el siete");
    }
}

Se utiliza == y no =. == sirve para comparar y = se utiliza para asignar un valor. También hay que destacar que se escribe if my_number == 7 y no if (my_number == 7). La estructura de control if no necesita paréntesis en Rust.

Esta estructura se completa con else if y else si resultan necesarias:

fn main() {
    let my_number = 5;
    if my_number == 7 {
        println!("Es el siete");
    } else if my_number == 6 {
        println!("Es el seis")
    } else {
        println!("Es un número diferente")
    }
}

El código anterior imprime Es un númbero diferente porque no es igual a 7 o 6.

Se pueden añadir más condiciones con && (operador y lógico) y || (operador o lógico).

fn main() {
    let my_number = 5;
    if my_number % 2 == 1 && my_number > 0 { // % 2 es el resto de la división entre dos
        println!("Es un número impar positivo");
    } else if my_number == 6 {
        println!("Es el seis")
    } else {
        println!("Es un número diferente")
    }
}

El código anterior imprime Es un número impar positivo porque cuando se divide entre 2 el resto es 1, que es mayor que 0.

Se observa que cuando hay demasiados if, else y else if el código puede resultar difícil de leer. En estos casos (y en otros muchos) se puede utilizar match, que resulta mucho más límpio. match requiere que se contemplen todos los casos posibles para evitar errores. Así que el siguiente código no funciona:

fn main() {
    let my_number: u8 = 5;
    match my_number {
        0 => println!("Es cero"),
        1 => println!("Es uno"),
        2 => println!("Es dos"),
        // ⚠️
    }
}

El compilar indica lo siguiente:

error[E0004]: non-exhaustive patterns: `3u8..=std::u8::MAX` not covered
 --> src\main.rs:3:11
  |
3 |     match my_number {
  |           ^^^^^^^^^ pattern `3u8..=std::u8::MAX` not covered

El compilador se queja de que solo conoce lo que tiene que ejecutar los casos de 0 a 2, pero u8 puede tener valores hasta el 255 (es decir std::u8::MAX). Qué debe hacer el programa para el resto de valores posibles que pueden aparecer.

fn main() {
    let my_number: u8 = 5;
    match my_number {
        0 => println!("Es cero"),
        1 => println!("Es uno"),
        2 => println!("Es dos"),
        _ => println!("Es algún otro número"),
    }
}

Este código imprime Es algún otro número.

Para el caso de match hay que recordar que:

  • A todo match le sigue un bloque de código {}
  • Se escriben los patrones a la izquierda y se usa => (flecha gruesa -fat arrow-) para indicar qué hay que hacer cuando hay una coincidencia.
  • A cada línea con un patrón se le denomina "brazo" del match.
  • Entre cada "brazo" se pone una coma de separación (no se usa el punto y coma).

Se puede declarar un valor usando match ya que retorna una valor.

fn main() {
    let my_number = 5;
    let second_number = match my_number {
        0 => 0,
        5 => 10,
        _ => 2,
    };
}

En el ejemplo anterior, second_number tendrá el valor 10. El match, en este caso acaba con un ; ya que una vez se ha finalizado su evaluación esta sentencia es como si se hubiese escrito let second_number = 10;. Que define y asigna el 10 a second_number.

match se puede utilizar para cosas más complejas. Por ejemplo, con tuplas:

fn main() {
    let sky = "nuboso";
    let temperature = "cálido";

    match (sky, temperature) {
        ("nuboso", "frío") => println!("El día es oscuro y desapacible"),
        ("despejado", "cálido") => println!("El día es agradable"),
        ("nuboso", "cálido") => println!("El día es oscuro, pero no se está mal"),
        _ => println!("No sé cómo es el día de hoy"),
    }
}

Este código imprime El día es oscuro, pero no se está mal porque coincide con "nuboso" y "cálido" para sky y temperature.

Incluso se puede utilizar if en las ramas de un match. Es lo que se llama una "guarda de coincidencia" (match guard):

fn main() {
    let children = 5;
    let married = true;

    match (children, married) {
        (children, married) if married == false => println!("Sin casar con {} niños", children),
        (children, married) if children == 0 && married == true => println!("Casado, pero sin niños"),
        _ => println!("¿Casado? {}. Número de niños: {}.", married, children),
    }
}

Este progrma imprimirá ¿Casado? true. Número de niños: 5.

Se puede usar _ tantas veces como se necesite en un match.

fn match_colours(rbg: (i32, i32, i32)) {
    match rbg {
        (r, _, _) if r < 10 => println!("No muy rojo"),
        (_, b, _) if b < 10 => println!("No muy azul"),
        (_, _, g) if g < 10 => println!("No muy verde"),
        _ => println!("Cada color tiene al menos 10"),
    }
}

fn main() {
    let first = (200, 0, 0);
    let second = (50, 50, 50);
    let third = (200, 50, 0);

    match_colours(first);
    match_colours(second);
    match_colours(third);

}

Este código imprime:

No muy azul
Cada color tiene al menos 10
No muy verde

Este código también muestra cómo funcionan las sentencias match, porque en el primer ejemplo solo imprime No muy rojo, aunque tampoco tiene mucho verde. Las sentencias match siempre se detienen cuando encuentran una coincidencia y no chequea el resto de los "brazos". Es un buen ejemplo de código que compila bien, pero no hace lo que se quiere.

Se puede construir una sentencia matchgigante para arreglar este código, pero probablemente es mejor utilizar un bucle for. Más adelante se hablará de los bucles.

La sentencia match siempre tiene que devolver el mismo tipo de datos en todas sus ramas. Por eso, este código no funciona:

fn main() {
    let my_number = 10;
    let some_variable = match my_number {
        10 => 8,
        _ => "Not ten", // ⚠️
    };
}

El compilador indica lo siguiente:

error[E0308]: `match` arms have incompatible types
  --> src\main.rs:17:14
   |
15 |       let some_variable = match my_number {
   |  _________________________-
16 | |         10 => 8,
   | |               - this is found to be of type `{integer}`
17 | |         _ => "Not ten",
   | |              ^^^^^^^^^ expected integer, found `&str`
18 | |     };
   | |_____- `match` arms have incompatible types

El código siguiente, por la misma razón, tampoco funciona:

fn main() {
    let some_variable = if my_number == 10 { 8 } else { "something else "}; // ⚠️
}

Pero el siguiente código sí funciona, porque no es un matchy son dos sentencias diferentes:

fn main() {
    let my_number = 10;

    if my_number == 10 {
        let some_variable = 8;
    } else {
        let some_variable = "Something else";
    }
}

También se puede usar @ para darle un nombre al valor de un patrón match con el fin de poder usarlo en la expresión correspondiente a ese "brazo". En este ejemplo, se guarda el valor en una variable number para pasarlo a una función. Si es 4 o 13 se usa ese number en la sentencia println!. En otro caso, no se utiliza.

fn match_number(input: i32) {
    match input {
    number @ 4 => println!("{} da mala suerte en China (suena parecido a 死)", number),
    number @ 13 => println!("{} da mala suerte en Norte América, ¡Suerte en Italia! In bocca al lupo", number),
    _ => println!("Es un número normal"),
    }
}

fn main() {
    match_number(50);
    match_number(13);
    match_number(4);
}

Este código imprime:

Es un número normal
13 da mala suerte en Norte América, ¡Suerte en Italia! In bocca al lupo
4 da mala suerte en China (suena parecido a 死)

Estructuras - struct

Se puede ver este capítulo en YouTube en inglés: Parte 1 y Parte 2

Con las estructuras se pueden crear nuevos tipos de datos. Se utilizan constantemente en Rust, puesto que son muy útiles. Las estructuras se crean con la palabra reservada struct. El nombre de las estructuras debería estar en UpperCamelCase (una letra mayúscula por cada palabra sin espacios, ni guiones bajos). Si se escribe todo en minúsculas, el compilador avisará.

Existen tres tipos de estructuras.

La primera de ellas es la estructura unitaria "unit struct", que no tiene nada. Simplemente se escribe su nombre seguido de un punto y coma.

struct FileDirectory;
fn main() {}

La segunda de ellas es la estructura tupla, o estructura sin nombres. Solo es necesario escribir los tipos de dato que contiene, sin nombres de campo. Las estructuras de tupla son indicadas cuando se necesita una estructura simple sin necesidad de utilizar nombres.

struct Colour(u8, u8, u8);

fn main() {
    let my_colour = Colour(50, 0, 50); // Crea un color RGB (red, green, blue)
    println!("La segunda parte del color (la componente verde) es: {}", my_colour.1);
}

Este código imprime La segunda parte del color (la componente verde) es: 0.

El tercer tipo es la estructura con nombres. Que es, probablemente, la más habitual. En estas estructuras se declaran los nombres de los campos y sus tipos en un bloque {}. Estos bloques no se terminan con punto y coma.

struct Colour(u8, u8, u8); // Declara la misma estructura tupla para el color

struct SizeAndColour {
    size: u32,
    colour: Colour, // y la inserta en una estructura con nombres
}

fn main() {
    let my_colour = Colour(50, 0, 50);

    let size_and_colour = SizeAndColour {
        size: 150,
        colour: my_colour
    };
}

Los campos de una estructura con nombres se separan con comas. El último campo puede llevar o no la coma. En el caso anterior, se puso una coma después de definir el campo colour: Colour,, pero no es necesario. Normalmente, se considera buena idea poner siempre la coma, porque en ocasiones resulta necesario cambiar el orden de los campos o añadir uno al final y de este modo es sistemática la modificación:

struct Colour(u8, u8, u8); 

struct SizeAndColour {
    size: u32,
    colour: Colour // Sin coma
}

fn main() {}

Se dedice cambiar su orden, cortando y pegando la fila...

struct SizeAndColour {
    colour: Colour // ⚠️ ¡Error! ya que no tiene coma.
    size: u32,
}

fn main() {}

En todo caso, no es muy importante si se usa la coma o no.

En el siguiente ejemplo, se crea una estructura Pais que tiene los campos poblacion, capitaly presidente.

struct Pais {
    poblacion: u32,
    capital: String,
    presidente: String
}

fn main() {
    let poblacion = 500_000;
    let capital = String::from("Elista");
    let presidente = String::from("Batu Khasikov");

    let kalmykia = Pais {
        poblacion: poblacion,
        capital: capital,
        presidente: presidente,
    };
}

Se observa que resuta prolijo tener que escribir el nombre del campo y su valor. Se escribe doble. poblacion: poblacion, capital: capital y presidente: presidente. Como se trata de alto habitual, Rust proporciona un atajo si tanto el campo, como la variable que contiene el valor se llaman igual. En ese caso, se puede simplificar así:

struct Pais {
    poblacion: u32,
    capital: String,
    presidente: String
}

fn main() {
    let poblacion = 500_000;
    let capital = String::from("Elista");
    let presidente = String::from("Batu Khasikov");

    let kalmykia = Pais {
        poblacion,
        capital,
        presidente,
    };
}

Enumerados - enum

Este capítulo se puede ver en YouTube en inglés: Parte 1, Parte 2, Parte 3 y Parte 4

La palabra reservada de Rust enum se usa para los tipos enumerados. Esta es la diferencia con struct:

  • Se utiliza struct cuando un tipo de datos debe representar una cosa Y otra cosa a la vez.
  • Se utiliza enum cuando un tipo de datos puede representar una cosa O alguna cosa diferente.

Las estructuras sirven para unir diferentes elementos en uno solo, mientras que los enumerados permiten que un tipo de datos represente a diferentes cosas en diferente momento.

Para declarar un enumerado se debe escribir enum seguido de un bloque {} con las diferentes opciones separadas por coma. Como en el caso de los struct la última opción puede llevar la coma o no. A continuación se crea un enumerado denominado CosasEnElCielo:

enum CosasEnElCielo {
    Sol,
    Estrellas,
}

fn main() {}

Es un enumerado, por lo tanto, cuando se cree un valor es necesario que se elija entre el Sol o las Estrellas. A cada elemento que forma parte del enumerado se le denomina variante.

// Crea el enumerado con dos variantes
enum CosasEnElCielo {
    Sol,
    Estrellas,
}

// Con esa función se usa un i32 para crear CosasEnElCielo.
fn crear_estadoEnElCielo(time: i32) -> CosasEnElCielo {
    match time {
        6..=18 => CosasEnElCielo::Sol, // Entre las 6 y 18 horas se ve el sol
        _ => CosasEnElCielo::Estrellas, // En otro caso se ven las estrellas
    }
}

// Con esta función se localiza el estado y se muestran las CosasEnElCielo.
fn comprobar_el_cielo(state: &CosasEnElCielo) {
    match state {
        CosasEnElCielo::Sol => println!("¡Puedo ver el sol!"),
        CosasEnElCielo::Estrellas => println!("¡Puedo ver las estrellas!")
    }
}

fn main() {
    let time = 8; // Son las ocho de la mañana
    let skystate = crear_estadoEnElCielo(time); // crear_estadoEnElCielo returns a CosasEnElCielo
    comprobar_el_cielo(&skystate); // Se pasa una referencia para que pueda leer el estado del cielo
}

Este código imprime ¡Puedo ver el sol!.

A cada enumerado, se le pueden añadir datos (como en los struts):

enum CosasEnElCielo {
    Sol(String), // Ahora cada variante tiene una cadena de texto
    Estrellas(String),
}

fn crear_estadoEnElCielo(time: i32) -> CosasEnElCielo {
    match time {
        6..=18 => CosasEnElCielo::Sol(String::from("¡Puedo ver el sol!")), // Da el valor aquí
        _ => CosasEnElCielo::Estrellas(String::from("¡Puedo ver las estrellas!")),
    }
}

fn comprobar_el_cielo(state: &CosasEnElCielo) {
    match state {
        CosasEnElCielo::Sol(description) => println!("{}", description), // recupera la descripción para que se pueda imprimir
        CosasEnElCielo::Estrellas(n) => println!("{}", n), // se puede usar cualquier variable n para obtener la descripción
    }
}

fn main() {
    let time = 8; // Son las ocho de la mañana
    let skystate = crear_estadoEnElCielo(time); // crear_estadoEnElCielo devuelve un elemento de CosasEnElCielo
    comprobar_el_cielo(&skystate); // Se pasa una referencia para que pueda leer el estado del cielo
}

Este código imprime lo mismo que antes ¡Puedo ver el sol!.

También se puede "importar" un enumerado para que no haya que escribir mucho. A continuación se muestra un ejemplo en se escribe Estado:: cada vez que se comprueba el "estado de ánimo":

enum Estado {
    Feliz,
    Cansado,
    NoEstoyMal,
    Enfadado,
}

fn comprueba_estado(mood: &Estado) -> i32 {
    let nivel_de_felicidad = match mood {
        Estado::Feliz => 10, // Se escribe Estado:: cada vez
        Estado::Cansado => 6,
        Estado::NoEstoyMal => 7,
        Estado::Enfadado => 2,
    };
    nivel_de_felicidad
}

fn main() {
    let my_mood = Estado::NoEstoyMal;
    let nivel_de_felicidad = comprueba_estado(&my_mood);
    println!("De 1 a 10, mi estado de felicidad es {}", nivel_de_felicidad);
}

El código anterior imprime De 1 a 10, mi estado de felicidad es 7. A continuación, el mismo código, pero importando el enumerado para tener que escribir menos. Para importar todo se utiliza *. Es el mismo carácter que para desrreferenciar, pero con un uso diferente.

enum Estado {
    Feliz,
    Cansado,
    NoEstoyMal,
    Enfadado,
}

fn comprueba_estado(mood: &Estado) -> i32 {
    use Estado::*; // Se importa el conjunto de variantes de Estado. Ahora se puede escribir menos
    let nivel_de_felicidad = match mood {
        Feliz => 10, // Ya no es necesario escribir Estado::
        Cansado => 6,
        NoEstoyMal => 7,
        Enfadado => 2,
    };
    nivel_de_felicidad
}

fn main() {
    let my_mood = Estado::NoEstoyMal;
    let nivel_de_felicidad = comprueba_estado(&my_mood);
    println!("De 1 a 10, mi estado de felicidad es {}", nivel_de_felicidad);
}

Las partes de un enumerado se pueden convertir a número entero. Esto se debe a que Rust da a cada variante de un enum un número que comienza con el 0 (para uso interno de Rust). Se puede utilizar en el código, siempre que las variantes con contengan ningún dato adicional:

enum Estacion {
    Primavera, // If this was Primavera(String) or something it wouldn't work
    Verano,
    Otoño,
    Invierno,
}

fn main() {
    use Estacion::*;
    let cuatro_estaciones = vec![Primavera, Verano, Otoño, Invierno];
    for estacion in cuatro_estaciones {
        println!("{}", estacion as u32);
    }
}

El código anterior imprime:

0
1
2
3

Es posible asignar un número entero expresamente a cada variante. A Rust no le importa el número concreto que tenga cada una de ellas. Para ello, se añade un símbolo = y el número deseado a cada variante. No es necesario indicar el número a cada variante. Si no se añade, Rust utiliza el siguiente disponible (suma 1) a partir de la variante anterior que tuviera número:

enum Estrella {
    EnanaMarron = 10,
    EnanaRoja = 50,
    EstrellaAmarilla = 100,
    GiganteRoja = 1000,
    EstrellaMuerta, // ¿Qué número tendrá?
}

fn main() {
    use Estrella::*;
    let starvec = vec![EnanaMarron, EnanaRoja, EstrellaAmarilla, GiganteRoja];
    for star in starvec {
        match star as u32 {
            size if size <= 80 => println!("No es la estrella más grande"),
            size if size >= 80 => println!("Esta estrella tiene un buen tamaño"),
            _ => println!("Esta estrella es muy grande"),
        }
    }
    println!("¿Qué número tiene EstrellaMuerta? Es el número {}.", EstrellaMuerta as u32);
}

This prints:

No es la estrella más grande
No es la estrella más grande
Esta estrella tiene un buen tamaño
Esta estrella tiene un buen tamaño
¿Qué número tiene EstrellaMuerta? Es el número 1001.

EstrellaMuerta hubiera sido el número 4 si no se hubiera expresado ningún número, pero ahora es el 1001.

Los enumerados sirven para usar tipos diferentes

Como ya se sabe, los elementos de un Vec, array, etc. tienen que ser del mismo tipo siempre (solo las tuplas permiten tipos diferentes). Los enumerados permiten incorporar diferentes tipos en las colecciones anteriores. Si se deseara tener un Vec que almacenara de forma indistinta u32 o i32 se puede declarar el Vec como que contiene un enumerado como en el siguiente ejemplo:

enum Numero {
    U32(u32),
    I32(i32),
}

fn main() {}

Así, este enumerado tiene dos variantes: la variante U32con un u32y la variante I32 con un i32. U32 y I32 son solo los nombres de cada variante. Se podrían haber llamado UTreintaYDos o ITreintaYDos o cualquier otra cosa.

Ahora es posible declarar un Vec de la siguiente forma Vec<Numero> y el compilador no se queja porque el vector es de un solo tipo. Al compilador no le preocupa si en un momento dado hay u32 o i32 porque esa diferencia está oculta por el tipo Numero. Y como es un enumerado, es necesario seleccionar una variante cada vez. En el siguiente código se usa el método .is_positive() para seleccionar la variante. Si es true se selecciona U32 y si es false se selecciona I32.

Ahora el código queda así:

enum Numero {
    U32(u32),
    I32(i32),
}

fn get_numero(input: i32) -> Numero {
    let numero = match input.is_positive() {
        true => Numero::U32(input as u32), // lo cambia a u32 si es positivo
        false => Numero::I32(input), 
    };
    numero
}


fn main() {
    let my_vec = vec![get_numero(-800), get_numero(8)];

    for item in my_vec {
        match item {
            Numero::U32(numero) => println!("Es un u32 con el valor {}", numero),
            Numero::I32(numero) => println!("Es un i32 con el valor {}", numero),
        }
    }
}

Este código imprime:

Es un i32 con el valor -800
Es un u32 con el valor 8

Bucles

Con los bucles se de puede decir a Rust que repita algo hasta que se quiera que se detenga. Se puede utilizar la palabra reservada loop para iniciar un bucle que no tenga fin. Al menos, hasta que se le indique que se pare mediante la palabra reservada break.

fn main() { // Este programa nunca se detiene
    loop {

    }
}

El siguiente programa sí acaba. En cada repetición incrementa en uno un contador, hasta que vale 5. En este momento se acaba:

fn main() {
    let mut contador = 0; // Inicia el contador a 0
    loop {
        contador +=1; // Incrementa el contador en 1
        println!("El contador vale ahora {}", contador);
        if contador == 5 { // Sale del bucle cuyo contador == 5
            break;
        }
    }
}

Que imprime:

El contador vale ahora 1
El contador vale ahora 2
El contador vale ahora 3
El contador vale ahora 4
El contador vale ahora 5

Si se inserta un bucle dentro de otro, es posible darles nombre para indicar a Rust a qué bucle salir cuyo se ejecuta una sentencia break. Para dar nombre se usa el apóstrofo ' y los dos puntos ::

fn main() {
    let mut contador = 0;
    let mut contador2 = 0;
    println!("Entryo en el primer bucle.");

    'primer_bucle: loop {
        // Da nombre al primer bucle
        contador += 1;
        println!("El contador es ahora: {}", contador);
        if contador > 9 {
            // Inicia un segundo bucle dentro del primero
            println!("Entryo en el segundo bucle.");

            'segundo_bucle: loop {
                // está dentro del 'segundo_bucle
                println!("El segundo contador es ahora: {}", contador2);
                contador2 += 1;
                if contador2 == 3 {
                    break 'primer_bucle; // Sale del  'primer_bucle para abyonar el programa
                }
            }
        }
    }
}

Este código imprimirá:

Entryo en el primer bucle.
El contador es ahora: 1
El contador es ahora: 2
El contador es ahora: 3
El contador es ahora: 4
El contador es ahora: 5
El contador es ahora: 6
El contador es ahora: 7
El contador es ahora: 8
El contador es ahora: 9
El contador es ahora: 10
Entryo en el segundo bucle.
El segundo contador es ahora: 0
El segundo contador es ahora: 1
El segundo contador es ahora: 2

Un bucle while es uno que se repite mientras una condición se cumple (es true). En cada repetición, Rust valida si la condición es aún true. Cuyo es false, Rust finaliza el bucle.

fn main() {
    let mut contador = 0;

    while contador < 5 {
        contador +=1;
        println!("El contador vale ahora: {}", contador);
    }
}

Un bucle for repite la ejecución un número determinado de veces. Este tipo de bucles suele utilizar rangos muy a menudo. Se utiliza .. y ..= para crear un rango.

  • .. crea un rango excluyente: 0..3 crea un rango con los siguientes tres números 0, 1, 2.
  • ..= crea un rango incluyente: 0..=3 crea un rango con los siguientes cuatro números 0, 1, 2, 3.
fn main() {
    for numero in 0..3 {
        println!("El numero es: {}", numero);
    }

    for numero in 0..=3 {
        println!("El siguiente numero es: {}", numero);
    }
}

Este código imprime:

El numero es: 0
El numero es: 1
El numero es: 2
El siguiente numero es: 0
El siguiente numero es: 1
El siguiente numero es: 2
El siguiente numero es: 3

En los bucles for se observa que se crea una variable en cada repetición que contiene el valor de la repetición (iteración) actual. Esta variable se podría llamar de cualquier forma. La variable se usa, en este caso, en println!.

Si no se necesitara la variable, se puede utilizar _.

fn main() {
    for _ in 0..3 {
        println!("Imprimiendo lo mismo las tres veces");
    }
}

Que imprimirá:

Imprimiendo lo mismo las tres veces
Imprimiendo lo mismo las tres veces
Imprimiendo lo mismo las tres veces

En este caso no se ha usado la variable.

Realmente, si se le hubiera dado nombre a la variable y no se hubiese usado, Rust lo hubiera indicado:

fn main() {
    for numero in 0..3 {
        println!("Imprimiendo lo mismo las tres veces");
    }
}

El código anterior imprime lo mismo que antes. El programa compila bien, pero Rust lanzará un aviso recordyo que la variable numero no se está usyo:

warning: unused variable: `numero`
 --> src\main.rs:2:9
  |
2 |     for numero in 0..3 {
  |         ^^^^^^ help: if this is intentional, prefix it with an underscore: `_number`

Rust sugiere que se escriba _numero en lugar de _. Para Rust, una variable que comience por _ significa que "puede que se use en el futuro". El uso de _ solo, significa "no importa este valor". Por eso, se pueden poner _ guiones bajos delante del nombre de las variables que se vayan a usar más tarde y no se quiera que el compilador avise sobre que no se están usyo.

break también se puede usar para devolver un valor. Para ello, se escribe un valor detrás de él y se usa ;. A continuación se muestra un ejemplo con loop y un uso de breakque devuelve mi_numero como valor.

fn main() {
    let mut contador = 5;
    let mi_numero = loop {
        contador +=1;
        if contador % 53 == 3 {
            break contador;
        }
    };
    println!("{}", mi_numero);
}

Este código imprime 56. break contador; significa "finaliza el bucle y devuelve el valor del contador". Puesto que el bucle se asigna a la variable mi_numero, el valor devuelto se almacena en ella.

Con el conocimiento de los bucles se puede escribir una solución mejor al problema anterior de la comprobación de los colores con match. Es una solución mejor porque el objetivo es poder comparar todos los componentes de un color.

fn match_colores(rbg: (i32, i32, i32)) {
    println!("Comparación de un color con {} rojo, {} azul, y {} verde:", rbg.0, rbg.1, rbg.2);
    let new_vec = vec![(rbg.0, "rojo"), (rbg.1, "azul"), (rbg.2, "verde")]; // Coloca los colores en un vec. Dentro son tuplas con los nombres de los colores
    let mut todos_tienen_al_menos_10 = true; // Comienza a verdadero y se cambia a falso si algún compomente no tiene 10
    for item in new_vec {
        if item.0 < 10 {
            todos_tienen_al_menos_10 = false; // Ahora es false
            println!("No mucho {}.", item.1) // Y se imprime el nombre del color.
        }
    }
    if todos_tienen_al_menos_10 { // Comprueba si es verdadero e imprime si lo es
        println!("Cada compomente de color tiene al menos 10.")
    }
    println!(); // Añade una línea vacía para separar
}

fn main() {
    let first = (200, 0, 0);
    let second = (50, 50, 50);
    let third = (200, 50, 0);

    match_colores(first);
    match_colores(second);
    match_colores(third);
}

Que imprime:

Comparación de un color con 200 rojo, 0 azul, y 0 verde:
No mucho azul.
No mucho verde.

Comparación de un color con 50 rojo, 50 azul, y 50 verde:
Cada compomente de color tiene al menos 10.

Comparación de un color con 200 rojo, 50 azul, y 0 verde:
No mucho verde.

Implementando funciones para structs y enums

Los struct y enum permiten que se puedan definir funciones asociadas a los tipos definidos como tal. Esto da mucha capacidad al lenguaje. Para ello, se utiliza el bloque impl sobre el tipo de datos definido con struct o enum. A estas funciones se las llama métodos.

En un bloque impl se pueden definir dos tipos diferentes de métodos:

  • Métodos: que toman como primer parámetro uno denominado self (o &self o &mut self). Estos métodos, para utilizarlos, usan . (un punto) sobre una variable del tipo struct o enum correspondiente. Por ejemplo, x.clone() es un método del tipo de la variable x.
  • Funciones asociadas (al tipo). Que en otros lenguajes se conocen como métodos estáticos: No tienen el primer parámetros self. Son funciones "relacionadas con el tipo de datos". Se llaman utilizando ::. Por ejemplo: String::from() es una llamada a una función asociada. También Vec::new(). Normalmente se utilizan para crear valores de variables del tipo correspondiente.

El ejemplo que se presenta a continuación, crea animales y los imprime.

En el siguiente ejemplo, también conviene observar que para poder usar {:?} al imprimir un tipo, este debe tener el rasgo de ser depurable, lo que se consigue mediante #derive(Debug) colocado al inicio del tipo de datos. A este tipo de etiquetado con # seguido de un nombre, se le denomina en Rust atributo. Se utilizan para indicar acciones al compilador. En este caso, para que se implemente de forma automática la posibilidad de depuración al tipo de datos correspondiente. Existen muchos atributos diferentes que se pueden utilizar en un programa Rust, más adelante se verán otros. El más común es derive y se encuentra muchas veces precediendo la definición de un struct o enum.

#[derive(Debug)]
struct Animal {
    edad: u8,
    tipo_animal: TipoAnimal,
}

#[derive(Debug)]
enum TipoAnimal {
    Gato,
    Perro,
}

impl Animal {
    fn new() -> Self {
        // Self, aquí, significa Animal.
        // También se podría haber usado Animal
        // en lugar de Self

        Self {
            // Cuando se escriba Animal::new(), se obtendrá siempre un gato de 10 años
            edad: 10,
            tipo_animal: TipoAnimal::Gato,
        }
    }

    fn cambiar_a_perro(&mut self) { // como está dentro de Animal, &mut self significa &mut Animal
                                  // usa .cambiar_a_perro() para convertir el cato en un perro
                                  // con &mut self se puede modificar
        println!("¡Cambiando el animal a perro!");
        self.tipo_animal = TipoAnimal::Perro;
    }

    fn cambiar_a_gato(&mut self) {
        // usa .cambiar_a_gato() para cambiar el perro a gato
        // con &mut self se puede modificar
        println!("¡Cambiando el animal a gato!");
        self.tipo_animal = TipoAnimal::Gato;
    }

    fn comprobar_tipo(&self) {
        // se lee a sí mismo self
        match self.tipo_animal {
            TipoAnimal::Perro => println!("El animal es un perro"),
            TipoAnimal::Gato => println!("El animal es un gato"),
        }
    }
}



fn main() {
    let mut animal_nuevo = Animal::new(); // Función asociada para crear una variable Animal
                                        // Es un gato de 10 años
    animal_nuevo.comprobar_tipo();
    animal_nuevo.cambiar_a_perro();
    animal_nuevo.comprobar_tipo();
    animal_nuevo.cambiar_a_gato();
    animal_nuevo.comprobar_tipo();
}

Esto imprime:

El animal es un gato
¡Cambiando el animal a perro!
El animal es un perro
¡Cambiando el animal a gato!
El animal es un gato

Se debe recordar que Self (el tipo Self) y self (la variable self) funcionan como abreviaturas del tipo que sea en cada momento.

En el código anterior, Self es igual a Animal. Y en fn cambiar_a_perro(&mut self) significa que el parámetro primero es un Animal. Este parámetro es la variable animal_nuevo cuando se llama de la siguiente forma animal_nuevo.cambiar_a_perro().

A continuación se muestra un ejemplo más de impl. En este caso, con enum.

enum Estado {
    Bueno,
    Malo,
    Somnoliento,
}

impl Estado {
    fn consultar(&self) {
        match self {
            Estado::Bueno => println!("¡Me siento bien!"),
            Estado::Malo => println!("Eh, no me siento tan bien"),
            Estado::Somnoliento => println!("Necesito dormir AHORA"),
        }
    }
}

fn main() {
    let mi_estado = Estado::Somnoliento;
    mi_estado.consultar();
}

This prints Necesito dormir AHORA.

Desestructurar

A continuación se presentan algunos aspectos adicionales sobre cómo desestructurar los valores de un struct o enum. Ya se vio que se podía realizar mediante let y el uso de variables que recuperan parte o todo el contenido de la estructura o enumerado. De esta forma, se tienen los valores de forma separada. Se puede observar en el siguiente ejemplo:

struct Persona { // crea una estructura simple para Persona
    nombre: String,
    real_nombre: String,
    altura: u8,
    felicidad: bool
}

fn main() {
    let papa_doc = Persona { // se crea la variable papa_doc
        nombre: "Papa Doc".to_string(),
        real_nombre: "Clarence".to_string(),
        altura: 170,
        felicidad: false
    };

    let Persona { // Se "desestructura" a  papa_doc
        nombre: a,
        real_nombre: b,
        altura: c,
        felicidad: d
    } = papa_doc;

    println!("Lo llaman {} pero su nombre real es {}. Es {} cm de alto y ¿es feliz? {}", a, b, c, d);
}

Que imprime: Lo llaman Papa Doc pero su nombre real es Clarence. Es 170 cm de alto y ¿es feliz? false

Primero se crea la estructura let papa_doc = Persona { campos }. Luego, de forma simétrica, se escribe let Persona { campos } = papa_doc para desestructurarla.

No es necesario escribir nombre: a - se puede escribir solo nombre. Pero aquí se escribe nombre: a porque se quiere usar este valor de forma independiente mediante la variable a.

A continuación se presenta un ejemplo más amplio. En este ejemplo, se usa la estructura Ciudad. Se utiliza una función asociada new para crear ciudades. Posteriormente, se utiliza la función procesar_valores_ciudad para hacer cosas con sus valores. En la función, se crea un Vec, pero se pueden imaginar muchas más operaciones sobre estos datos después de desestructurarlos.

struct Ciudad {
    nombre: String,
    nombre_antes: String,
    poblacion: u32,
    fecha_fundacion: u32,
}

impl Ciudad {
    fn new(nombre: String, nombre_antes: String, poblacion: u32, fecha_fundacion: u32) -> Self {
        Self {
            nombre,
            nombre_antes,
            poblacion,
            fecha_fundacion,
        }
    }
}

fn procesar_valores_ciudad(ciudad: &Ciudad) {
    let Ciudad {
        nombre,
        nombre_antes,
        ..
    } = ciudad;
        // se dispone de los valores separados
    let dos_nombres = vec![nombre, nombre_antes];
    println!("Los dos nombres de la ciudad son {:?}", dos_nombres);
}

fn main() {
    let tallinn = Ciudad::new("Tallinn".to_string(), "Reval".to_string(), 426_538, 1219);
    procesar_valores_ciudad(&tallinn);
}

El código anterior imprime Los dos nombres de la ciudad son ["Tallinn", "Reval"].

Referencias y el operador punto .

Se ha aprendido que cuyo se dispone de una referencia, se necesita utilizar el operador * para acceder al valor. Una referencia tiene su propio tipo, por lo que este código no funcionará:

fn main() {
    let mi_numero = 9;
    let referencia = &mi_numero;

    println!("{}", mi_numero == referencia); // ⚠️
}

El compilador imprime lo siguiente:

error[E0277]: can't comp¿Son `{integer}` with `&{integer}`
 --> src\main.rs:5:30
  |
5 |     println!("{}", mi_numero == referencia);
  |                              ^^ no implementation for `{integer} == &{integer}`

Es necesario cambiar la línea 5 a println!("{}", mi_numero == *referencia); para que funcione e imprima true.

fn main() {
    let mi_numero = 9;
    let referencia = &mi_numero;

    println!("{}", mi_numero == *referencia); // ahora funciona

Esto se debe a que ahora ya se está comparyo i32 == i32 y no i32== &i32 como sucedía antes. A esto se le llama desreferenciar.

Sin embargo, cuyo se utiliza un método sobre una variable, Rust realiza la desreferenciación de forma automática. En concreto, el operador . (punto) es quien está definido en Rust de forma que realiza la desreferenciación cuyo se utiliza sobre una variable de tipo referencia. Además, esto se realiza tantas veces como sea necesario hasta llegar al valor concreto.

A continuación, se crea un struct con un campo byte sin signo u8. Después, se crea una referencia y se intenta comparar, lo que no funcionará:

struct Item {
    numero: u8,
}

fn main() {
    let item = Item {
        numero: 8,
    };

    let referencia_numero = &item.numero; // el tipo de referencia_numero es &u8

    println!("{}", referencia_numero == 8); // ⚠️ &u8 y u8 no se pueden comparar entre sí
}

Para que funcione, es necesario desreferenciarlo. Por ejemplo, cambiyo println!("{}", *referencia_numero == 8):

struct Item {
    numero: u8,
}

fn main() {
    let item = Item {
        numero: 8,
    };

    let referencia_numero = &item.numero; // el tipo de referencia_numero es &u8

    println!("{}", *referencia_numero == 8); // así sí funciona
}

Pero no es necesario hacerlo de forma anterior. Con el operador punto, se desreferencia de forma automática;

struct Item {
    numero: u8,
}

fn main() {
    let item = Item {
        numero: 8,
    };

    let referencia_item = &item; // el tipo de referencia_item es &Item

    println!("{}", referencia_item.numero == 8); // así sí funciona
}

De forma automática, mediante el operador ., se ha realizado una desreferenciación que de otro modo habría que haber escrito así (*referencia_item).numero.

A continuación, se crea un método para la estructura Item que compara el numero con otro. Como se ve, no se necesita usar * en ningún lugar:

struct Item {
    numero: u8,
}

impl Item {
    fn compara_numero(&self, otro_numero: u8) { // tiene una referencia a self
        println!("¿Son {} y {} iguales? {}", self.numero, otro_numero, self.numero == otro_numero);
            // No se necesita escribir (*self).numero
    }
}

fn main() {
    let item = Item {
        numero: 8,
    };

    let item_referencia = &item; // De tipo &Item
    let item_referencia_dos = &item_referencia; // De tipo &&Item

    item.compara_numero(8); // El método funciona
    item_referencia.compara_numero(8); // Este método también funciona
    item_referencia_dos.compara_numero(8); // y este
}

Se concluye así que cuando se usa el operador ., no se necesita utilizar el operador * para desreferenciar.

Genéricos

En las funciones es necesario definir el tipo de cada parámetro de entrada:

fn devuelve_numero(numero: i32) -> i32 {
    println!("Ahí va tu numero.");
    numero
}

fn main() {
    let numero = devuelve_numero(5);
}

Pero si lo que se necesita es una función que haga lo mismo para cualquier tipo de datos diferente de i32, se pueden usar genéricos. Un tipo genérico sirve para indicar que algo puede ser de diferente tipo.

También se puede decir que la función tiene un parámetro de tipo.

Los parámetros de tipo genérico se definen con los símbolos de menor y mayo que encierran el nombre que representa al parámetro de tipo. Normalmente se utiliza un carácter en mayúscula para representarlos (T, U, V, etc.), aunque no es obligatorio utilizar solo una letra.

La función anterior se puede convertir en genérica así:

fn devuelve_numero<T>(numero: T) -> T {
    println!("Ahí va tu numero.");
    numero
}

fn main() {
    let numero = devuelve_numero(5);
    let numero_decimal = devuelve_numero(5.4);
}

Se observa que el parámetro de tipo genérico <T> va después del nombre de la función. Puede resultar más sencillo de comprender si se sustituye la T por un nombre más descriptivo como MiTipo:

fn devuelve_numero<MiTipo>(numero: MiTipo) -> MiTipo {
    println!("Ahí va tu numero.");
    numero
}

fn main() {
    let numero = devuelve_numero(5);
    let numero_decimal = devuelve_numero(5.4);
}

Así que la parte de después del nombre de la función es lo que hace que la función tenga un parámetro de tipo genérico que el compilador "sustituye" por el tipo concreto generyo un función diferente para cada tipo que se use en el código.

Además, como se ha visto, en Rust existen algunos tipos que implementan determinados rasgos, como son Copy, Clone, Display, Debug y otros. Si un tipo es Debug, puede usar {:?} para imprimirlo. Esto genera un posible problema si en la función genérica se quisiera imprimir:

fn imprime_numero<T>(numero: T) {
    println!("Aquí está tu numero: {:?}", numero); // ⚠️
}

fn main() {
    imprime_numero(5);
}

imprime_numero necesita que el parámetro de tipo genérico implemente Debug para poder utilizar {:?}. Con la definición actual de la función, no es posible conocer si realmente T implementa o no Debug. Por lo tanto, el compilador emite error:

error[E0277]: `T` doesn't implement `Debug`
 --> src/main.rs:2:43
  |
2 |     println!("Aquí está tu numero: {:?}", numero); // ⚠️
  |                                           ^^^^^^ `T` cannot be formatted using `{:?}` because it doesn't implement `Debug`

La solución es que se le indique a Rust que esta función genérica solo puede utilizar parámetros de tipo genérico que implementen Debug:

use std::fmt::Debug;
// Debug se encuentra en el módulo std::fmt.
// Ahora es posible usar solo Debug.

fn imprime_numero<T: Debug>(numero: T) { // Esto es lo importante <T: Debug>
    println!("Aquí está tu numero: {:?}", numero);
}

fn main() {
    imprime_numero(5);
    imprime_numero(5.4);
}

Ahora el compilador conoce que los tipos que van a usar esta función tienen definido Debug. Como tanto i32, como f64 tienen Debug definido, el código funciona.

Si se amplia el ejemplo, creyo una estructura que implemente Debug mediante #[derive(Debug)], se observa que la función también es válida:

use std::fmt::Debug;

#[derive(Debug)]
struct Animal {
    nombre: String,
    edad: u8,
}

fn imprime_elemento<T: Debug>(item: T) {
    println!("Aquí está tu elemento: {:?}", item);
}

fn main() {
    let charlie = Animal {
        nombre: "Charlie".to_string(),
        edad: 1,
    };

    let numero = 55;

    imprime_elemento(charlie);
    imprime_elemento(numero);
}

Que imprime:

Aquí está tu elemento: Animal { nombre: "Charlie", edad: 1 }
Aquí está tu elemento: 55

En ocasiones se necesita más de un tipo genérico para definir una función genérica. Para ello, se puede escribir cada tipo y pensar cómo se utiliza cada uno de ellos. En el siguiente ejemplo, se muestran dos tipos genéricos. Se desea imprimir un texto con el tipo T con {}, por lo que este tipo deberá implementar Display.

El segundo tipo es U y las dos variables num_1 y num_2 son de este tipo (U es algún tipo de número). Se trata de compararlas entre ellas, por lo que estos tipos deben implementar el rasgo PartialOrd, que es el que permite que los elementos de un tipo puedan usar <, > y ==, entre otros. También se quiere imprimir los números, por lo que el tipo U requiere Display.

use std::fmt::Display;
use std::cmp::PartialOrd;

fn compara_e_imprime<T: Display, U: Display + PartialOrd>(texto: T, num_1: U, num_2: U) {
    println!("{} ¿Es {} mayor que {}? {}", texto, num_1, num_2, num_1 > num_2);
}

fn main() {
    compara_e_imprime("¡¡Escucha!!", 9, 8);
}

El resultado es ¡¡Escucha!! ¿Es 9 mayor que 8? true.

La declaración de la función fn compara_e_imprime<T: Display, U: Display + PartialOrd>(texto: T, num_1: U, num_2: U) significa:

  • El nombre de la función es compara_e_imprime.
  • El primer tipo es T y es genérico. Y debe poder usar {} para imprimir.
  • El segundo tipo es U y es genérico. Debe ser un tipo que pueda usar {} y también se tienen que poder comparar sus elementos con <, > y ==.

De este modo, se pueden pasar parámetros de distintos tipos a la función compara_e_imprime. Por ejemplo, el parámetro texto puede ser String, &str, i32 o cualquier otro que se pueda imprimir.

Para que las funciones genéricas sean más fácil de leer, también existe la siguiente sintaxis equivalente al código anterior. Se utiliza where para simplificar la declaración de la función.

use std::fmt::Display;
use std::cmp::PartialOrd;

fn compara_e_imprime<T, U>(texto: T, num_1: U, num_2: U)
where
    T: Display,
    U: Display + PartialOrd,
{
    println!("{} ¿Es {} mayor que {}? {}", texto, num_1, num_2, num_1 > num_2);
}

fn main() {
    compara_e_imprime("¡¡Escucha!!", 9, 8);
}

Cuyo se tienen muchos tipos genéricos con rasgos concretos de implementación, resulta más legible esta sintaxis con where.

Hay que destacar también que:

  • Si se tiene un parámetro de tipo T y otro parámetro de tipo T, ambos tienen que ser del mismo tipo.
  • Si se tiene un parámetro de tipo T y otro parámetro de tipo U, pueden ser de diferente tipo, pero también pueden ser del mismo tipo.

Por ejemplo:

use std::fmt::Display;

fn diga_dos<T: Display, U: Display>(statement_1: T, statement_2: U) {
// El tipo T necesita Display, el tipo U necesita Display
    println!("Tengo dos cosas que decir: {} y {}", statement_1, statement_2);
}

fn main() {

    diga_dos("¡Hola!", String::from("Odio la arena.")); // Tipo T es &str, pero el tipo U es String.
    diga_dos(String::from("¿Dónde está Padme?"), String::from("¿Está bien?")); // Ambos tipos son String.
}

Esto imprime:

Tengo dos cosas que decir: ¡Hola! y Odio la arena.
Tengo dos cosas que decir: ¿Dónde está Padme? y ¿Está bien?

Los enumerados Option y Result

Ahora que se conocen los enumerados (enum) y los genéricos, se pueden comprender dos enumerados fundamentales en Rust que permiten hacer que el código sea más seguro: Option y Result.

En primer lugar se trata Option.

Option

Tiene dos posibilidades: Some(valor) y None y se utiliza cuando se da el caso de que un valor pueda existir o no.

Cuando el valor existe, se usa Some(valor). Cuando no existe es None.

El código siguiente, sin Option, da error (panic).

    // ⚠️
fn toma_el_quinto(valor: Vec<i32>) -> i32 {
    valor[4]
}

fn main() {
    let new_vec = vec![1, 2];
    let index = toma_el_quinto(new_vec);
}

Muestra el siguiente mensaje de error:

thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 4', src\main.rs:34:5

Que el programa "entre en pánico" (panicked) significa que se detiene de forma controlada antes de que suceda el error. Rust comprueba que la función quiere obtener un valor imposible y se detiene antes del error. Recorre la pila de llamadas de función para tratar de encontrar un lugar en que se contemple el error y si no lo encuentra, como es el caso, se detiene y viene a decir "lo siento, no puedo obtener el quinto valor de este vector".

Para evitar este error, se modificará el tipo que devuelve la función de i32 a Option<i32>. Que significa que a partir de ahora esta función puede devolver Some(i32) si existe el índice, o None cuando no existe. En este caso, el valor de retorno cuando existe, i32, estará "envuelto" en un tipo Option, es decir: la función lo devuelve dentro de un Option, en concreto Some(i32). Por lo tanto, al terminar la función, hace falta algún tipo de código para que el valor embebido en Some se pueda usar.

fn toma_el_quinto(valor: Vec<i32>) -> Option<i32> {
    if valor.len() < 5 { // .len() devuelve la longitud del vector (el número de elementos).
                         // Debería ser al menos 5 si se quiere recuperar el dato en esa posición.
        None            // cuando no lo es, devuelve None
    } else {
        Some(valor[4])
    }
}

fn main() {
    let new_vec = vec![1, 2];
    let bigger_vec = vec![1, 2, 3, 4, 5];
    println!("{:?}, {:?}", toma_el_quinto(new_vec), toma_el_quinto(bigger_vec));
}

El código anterior imprime None, Some(5). Ya no falla "en pánico". Pero, ¿cómo se recupera ahora el valor 5?.

Para obtenerlo, se puede usar alguna de las funciones que tiene el tipo Option. La función unwrap() recupera el valor contenido en el Some, pero también entra en pánico si contiene un None. Es decir, que solo se debe usar cuando se sabe que el valor retornado es un Some(valor).

fn toma_el_quinto(valor: Vec<i32>) -> Option<i32> {
    if valor.len() < 5 { 
        None            
    } else {
        Some(valor[4])
    }
}

fn main() {
    let new_vec = vec![1, 2];
    let bigger_vec = vec![1, 2, 3, 4, 5];
    println!("{:?}, {:?}", 
        toma_el_quinto(new_vec).unwrap(), // esta fila falla ya que contiene None.
        toma_el_quinto(bigger_vec).unwrap());
}

El mensaje que devuelve el código anterior es:

thread 'main' panicked at 'called `Option::unwrap()` on a `None` valor', src/main.rs:13:33

La forma correcta de recuperar el valor de un Option, contemplando previamente ambas posibilidades es utilizar match. De este forma, se puede decidir qué acción realizar, imprimir o no el valor, en función del resultado. Por ejemplo:

fn toma_el_quinto(valor: Vec<i32>) -> Option<i32> {
    if valor.len() < 5 {
        None
    } else {
        Some(valor[4])
    }
}

fn manejar_opcion(mi_opcion: Vec<Option<i32>>) {
  for item in mi_opcion {
    match item {
      Some(numero) => println!("¡Encontré un {}!", numero),
      None => println!("¡Encontré un None!"),
    }
  }
}

fn main() {
    let new_vec = vec![1, 2];
    let bigger_vec = vec![1, 2, 3, 4, 5];
    let mut option_vec = Vec::new(); // se crea un vector para guardar los valores option
                                     // El vector es de tipo: Vec<Option<i32>>. Es decir, un vector de Option<i32>.

    option_vec.push(toma_el_quinto(new_vec)); // guarda "None" en el vec
    option_vec.push(toma_el_quinto(bigger_vec)); // guarda "Some(5)" en el vec

    manejar_opcion(option_vec); // revisa el vector y realiza la acción que corresponda
                               // Imprime el valor si es un Some. Y no lo toca, y lo indica, si es un None.
}

Da como resultado:

¡Encontré un None!
¡Encontré un 5!

Option utiliza genéricos en su definición, para poder definir el contenido de Some en cada caso:

enum Option<T> {
    None,
    Some(T),
}

fn main() {}

El punto importante a recordar es que con Some se incluye un valor de tipo T (cualquier tipo). Se observa que los símbolos de < y > después del nombre del enumerado Option contienen el parámetro de tipo, que es lo que le indica al compilador que este enumerado es genérico. En este caso, el enumerado puede ser uno de los dos structs: None o Some(T). Además, se observa que T no tiene ningún trait (rasgo) obligatorio como Display u otro que limite los posibles tipos de datos que se puedan incluir dentro de Some. Además, en el caso de None, ni siquiera existe un tipo de datos (ni siquiera se usa el parámetro de tipo T).

Por esto último, no se puede usar lo siguiente en la sentencia match:

#![allow(unused)]
fn main() {
// 🚧
Some(valor) => println!("El valor es {}", valor),
None(valor) => println!("El valor es {}", valor),
}

None solo es None...

Hay formas más fáciles de usar Option. En el código siguiente, se usa el método .is_some() para preguntar si el tipo del Option es Some (también hay otro método complementario denominado .is_none()).

fn toma_el_quinto(valor: Vec<i32>) -> Option<i32> {
    if valor.len() < 5 {
        None
    } else {
        Some(valor[4])
    }
}

fn main() {
    let new_vec = vec![1, 2];
    let bigger_vec = vec![1, 2, 3, 4, 5];
    let vec_of_vecs = vec![new_vec, bigger_vec];
    for vec in vec_of_vecs {
        let numero_interno = toma_el_quinto(vec);
        if numero_interno.is_some() {
            // .is_some() devuelve true si es Some, false si es None
            println!("Tenemos: {}", numero_interno.unwrap()); // ahora es seguro usar .unwrap() ya que es seguro que es Some
        } else {
            println!("No tenemos nada.");
        }
    }
}

Esto imprime:

No tenemos nada.
Tenemos: 5

Result

Es similar a Option, pero su uso es diferente:

  • Option trata sobre Some o None. La existencia o no de un valor.
  • Result trata sobre Ok o Error. La existencia de un resultado correcto o no (la existencia de un error).

Por eso, se debe usar Option cuando el razonamiento es "puede que haya un valor o puede que no". Pero se debe usar Result cuando el razonamiento es "puede que esto falle".

Si se comparan las definiciones de ambos enumerados:

enum Option<T> {
    None,
    Some(T),
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

fn main() {}

Se observa que Result tiene un valor dentro de Ok y también el caso de Err. Se trata de poder gestionar la información específica que describe el error que haya sucedido cuando este se produzca.

Al tener dos parámetros de tipo, Result<T, E>, se debe indicar qué se devuelve cuando el resultado es correcto, Ok(T), y qué se devuelve cuando el resultado es erróneo, Err(E). Puede ser cualquier cosa que se decida, incluso:

fn check_error() -> Result<(), ()> {
    Ok(())
}

fn main() {
    check_error();
}

check_error indica que se devuelva (), tanto en el caso de Ok, como en el caso de Err. Aunque en el ejemplo, siempre devuelve Ok(()).

En todo caso, con este código, el compilador da un aviso interesante:

warning: unused `std::result::Result` that must be used
 --> src\main.rs:6:5
  |
6 |     check_error();
  |     ^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: this `Result` may be an `Err` variant, which should be handled

El compilador avisa de que se prevé que devuelva Result, por lo que el código podría haber devuelto un Err, pero no se está tratando dicha posibilidad de error.

A continuación se presenta un código que trata el posible caso de error:

fn dar_resultado(input: i32) -> Result<(), ()> {
    if input % 2 == 0 {
        return Ok(())
    } else {
        return Err(())
    }
}

fn main() {
    if dar_resultado(5).is_ok() {
        println!("Es correcto")
    } else {
        println!("Es un error")
    }
}

Este código imprime Es un error. Así se ha gestionado el error.

Se puede recordar que para Option y para Result, dos métodos, respectivamente, para chequear de forma sencilla el tipo concreto, son: .is_some(), is_none(), is_ok() y is_err().

En ocasiones, una función con Result usará String para el valor contenido en Err. Aunque no es la mejor forma, es mejor que lo visto hasta el momento:

fn comprueba_si_es_cinco(numero: i32) -> Result<i32, String> {
    match numero {
        5 => Ok(numero),
        _ => Err("Lo siento, el número no era cinco.".to_string()), // Este es el mensaje de error
    }
}

fn main() {
    let mut result_vec = Vec::new(); // Crea un vector para contener el resultado

    for numero in 2..7 {
        result_vec.push(comprueba_si_es_cinco(numero)); // guarda cada resultado
    }

    println!("{:?}", result_vec);
}

El vector imprime lo siguiente:

[Err("Lo siento, el numero no era cinco."), Err("Lo siento, el numero no era cinco."), Err("Lo siento, el numero no era cinco."), Ok(5),
Err("Lo siento, el numero no era cinco.")]

Igual que en el caso de Option, .unwrap() sobre un valor de tipo Err "provoca el pánico" del programa.

    // ⚠️
fn main() {
    let valor_error: Result<i32, &str> = Err("Hubo un error"); // Crea un Result que es un Err
    println!("{}", valor_error.unwrap()); // Intenta recuperar el valor del resultado (asumiendo que sería correcto, cosa que no es)
}

El programa falla (panic) e imprime:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "Hubo un error"', src/main.rs:4:32

Esta información, src/main.rs:4:32 significa que el pánico se ha producido en "main.rs en el directorio src, en la línea 4 y columna 32". Por lo que se puede mirar ahí para ver cuál es el problema y resolverlo.

También se pueden crear nuevos tipos de error, que es lo habitual. Las funciones de la librería estándar y otras librerías lo suelen hacer. Por ejemplo, esta función de la librería estándar:

#![allow(unused)]
fn main() {
// 🚧
pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>
}

Esta función toma un vector de bytes (u8) e intenta convertirlo en una String. Como puede ser que los bytes no se correspondan con códigos válidos en UTF8, en lugar de devolver un String, devuelve un Result con un String en el caso de éxito y un error FromUtf8Error en el caso de que falle la conversión. Se puede usar cualquier nombre para el caso de fallo.

En ocasiones, el uso de match con Option y Result requiere mucho código. Por ejemplo, el método .get() devuelve un Option sobre un Vec.

fn main() {
    let my_vec = vec![2, 3, 4];
    let get_one = my_vec.get(0); // recupera el primer número
    let get_two = my_vec.get(10); // recupera None
    println!("{:?}", get_one);
    println!("{:?}", get_two);
}

Que imprime:

Some(2)
None

Para obtener los valores, se puede usar match.

fn main() {
    let my_vec = vec![2, 3, 4];

    for index in 0..10 {
      match my_vec.get(index) {
        Some(number) => println!("El número es: {}", number),
        None => {}
      }
    }
}

Este código es correcto, pero queremos hacer nada en el caso de None. En estos casos, se puede optar por escribir un código más compacto mediante el uso de if let que permite "hacer algo si coincide un valor" y "no hacer nada en el resto de casos".

fn main() {
    let my_vec = vec![2, 3, 4];

    for index in 0..10 {
      if let Some(number) = my_vec.get(index) {
        println!("The número es: {}", number);
      }
    }
}

Importante, a recordar: if let Some(number) = my_vec(index) significa que "compruebe si el valor de my_vec(index) es compatible con Some(number).

Además, hay que tener en cuenta que se usa =.

También existe while let que es un bucle for en el que se comprueba como en if let. Por ejemplo, si se dispone de los siguientes datos del tiempo procedentes de estaciones meteorológicas:

["Berlin", "cloudy", "5", "-7", "78"]
["Athens", "sunny", "not humid", "20", "10", "50"]

Y se desea obtener solo los números. Para ello, se puede usar el método parse::<i32>(). Este método intenta convertir un &str en un i32 y lo entrega en un valor de tipo Resultya que podría no funcionar si se le pasa algo que no es un número entero.

En el siguiente ejemplo, también se usará .pop() para extraer el último elemento del vector.

fn main() {
    let weather_vec = vec![
        vec!["Berlin", "cloudy", "5", "-7", "78"],
        vec!["Athens", "sunny", "not humid", "20", "10", "50"],
    ];
    for mut city in weather_vec {
        println!("Para la ciudad de {}:", city[0]); // En los datos, el primer elemento siempre es el nombre de la ciudad
        while let Some(information) = city.pop() {
            // Esto significa: continua mientras hay valores
            // Cuando no queden valores pop retorna None
            // y se saldrá del bucle while
            if let Ok(number) = information.parse::<i32>() {
                // Intenta obtener un entero
                // Devuelve un Result. Si es Ok(number), se imprimirá
                println!("El número es: {}", number);
            }  // No se hace nada, si no era un número, casos en los que se devuelve Err
        }
    }
}

Que imprimirá:

Para la ciudad de Berlin:
El número es: 78
El número es: -7
El número es: 5
Para la ciudad de Athens:
El número es: 50
El número es: 10
El número es: 20

Otras colecciones

Rust tiene muchos tipos de colección más. Se pueden consultar en la librería estándar: https://doc.rust-lang.org/beta/std/collections/. Esta página dispone de buenas explicaciones en cuanto a cuándo cada tipo, así que es el lugar en que consultar cuando no se tiene claro qué tipo usar. Todas estas colecciones se encuentran dentro de std::collections en la librería estándar de Rust. La mejor forma de usarlas es mediante use, como se hizo con los enumerados. Se presenta primero HashMap por ser de uso común.

HashMap (y BTreeMap)

Un HashMap es una colección compuesta por claves y valores. Se puede usar la clave para recuperar el valor que se almacenó con ella. Se puede crear un HashMap con HashMap::new() y se pueden insertar nuevos elementos mediante .insert(clave, valor).

Los HashMap no están ordenados, por lo que si se imprimen todas las claves almacenadas, probablemente saldrán en cualquier orden. Se puede ver con un ejemplo:

use std::collections::HashMap; // Así, bastará con escribir
// HashMap cada vez, en lugar de std::collections::HashMap

struct Ciudad {
    nombre: String,
    poblacion: HashMap<u32, u32>, // Almacenará el año
    //y la población de cada año
}

fn main() {

    let mut tallinn = Ciudad {
        nombre: "Tallinn".to_string(),
        poblacion: HashMap::new(), // En este momento el HashMap está vacío
    };

    tallinn.poblacion.insert(1372, 3_250); // inserta tres fechas
    tallinn.poblacion.insert(1851, 24_000);
    tallinn.poblacion.insert(2020, 437_619);


    for (año, poblacion) in tallinn.poblacion { // El tipo del Hashmap es HashMap<u32, u32>. Obtiene en cada iteración un par clave/valor
        println!("En el año {} la ciudad de {} tenía una población de {}.", año, tallinn.nombre, poblacion);
    }
}

Que puede imprimir:

En el año 1372 la ciudad de Tallinn tenía una población de 3250.
En el año 2020 la ciudad de Tallinn tenía una población de 437619.
En el año 1851 la ciudad de Tallinn tenía una población de 24000.

Pero también podría imprimir:

En el año 1851 la ciudad de Tallinn tenía una población de 24000.
En el año 2020 la ciudad de Tallinn tenía una población de 437619.
En el año 1372 la ciudad de Tallinn tenía una población de 3250.

Se puede observar que no está en orden.

Si se necesita una colección para almacenar parejas de clave y valor, se puede utilizar BTreeMap, que funciona igual que HashMap, pero mantiene el orden por clave.

use std::collections::BTreeMap; // Así, bastará con cambiar HashMap a BTreeMap

struct Ciudad {
    nombre: String,
    poblacion: BTreeMap<u32, u32>, 
}

fn main() {

    let mut tallinn = Ciudad {
        nombre: "Tallinn".to_string(),
        poblacion: BTreeMap::new(), 
    };

    tallinn.poblacion.insert(1372, 3_250); 
    tallinn.poblacion.insert(1851, 24_000);
    tallinn.poblacion.insert(2020, 437_619);


    for (año, poblacion) in tallinn.poblacion {
        println!("En el año {} la ciudad de {} tenía una población de {}.", año, tallinn.nombre, poblacion);
    }
}

Ahora, siempre se imprime:

En el año 1372 la ciudad de Tallinn tenía una población de 3250.
En el año 1851 la ciudad de Tallinn tenía una población de 24000.
En el año 2020 la ciudad de Tallinn tenía una población de 437619.

Volviendo a los HashMap. Se puede recuperar un valor determinado simplemente escribiendo la clave entre [] corchetes. En el siguiente ejemplo se recuperará el valor de la clave Bielefeld que está en Alemania. La aplicación fallará si no existe la clave. Si se escribe println!(("{:?}", ciudad_hashmap["Bielefeldd"]);, fallará, porque Bielefelddno existe.

Si no se está seguro de que exista una clave determinada, se puede usar get() que devuelve un tipo Option. Si existe sera Some(value) y si no, contendrá None, pero no fallará la aplicación. Por eso, la forma adecuada de recuperar un valor de un HashMap es usar get().

use std::collections::HashMap;

fn main() {
    let ciudades_canadieneses = vec!["Calgary", "Vancouver", "Gimli"];
    let ciudades_alemanas = vec!["Karlsruhe", "Bad Doberan", "Bielefeld"];

    let mut ciudad_hashmap = HashMap::new();

    for ciudad in ciudades_canadieneses {
        ciudad_hashmap.insert(ciudad, "Canadá");
    }
    for ciudad in ciudades_alemanas {
        ciudad_hashmap.insert(ciudad, "Alemania");
    }

    println!("{:?}", ciudad_hashmap["Bielefeld"]);
    println!("{:?}", ciudad_hashmap.get("Bielefeld"));
    println!("{:?}", ciudad_hashmap.get("Bielefeldd"));
}

Que imprime:

"Alemania"
Some("Alemania")
None

Esto sucede porque Bielefeld existe, pero Bielefeldd no.

Si un HashMapya contiene una clave y se intenta insertar un nuevo valor, el antiguo se sobreescribe.

use std::collections::HashMap;

fn main() {
    let mut book_hashmap = HashMap::new();

    book_hashmap.insert(1, "L'Allemagne Moderne");
    book_hashmap.insert(1, "Le Petit Prince");
    book_hashmap.insert(1, "섀도우 오브 유어 스마일");
    book_hashmap.insert(1, "Eye of the World");

    println!("{:?}", book_hashmap.get(&1));
}

Que imprime Some("Eye of the World"), porque fue el último valor utilizado en al insertar con .insert().

Es fácil comprobar si un valor existe cotejando el enumerado Option que devuelve .get().

use std::collections::HashMap;

fn main() {
    let mut book_hashmap = HashMap::new();

    book_hashmap.insert(1, "L'Allemagne Moderne");

    if book_hashmap.get(&1).is_none() { // is_none() devuelve un bool: true si es None, false si es Some
        book_hashmap.insert(1, "Le Petit Prince");
    }

    println!("{:?}", book_hashmap.get(&1));
}

Que imprime Some("L\'Allemagne Moderne") porque existía ya una clave 1, por lo que no se llegó a insertar Le Petit Prince.

HashMap tiene un método muy interesante denominado .entry() que se puede utilizar. Con el resultado de este método (que devuelve un valor de tipo enumerado Entry) se puede utilizar el método .or_entry() para insertar un valor solo si no existe una clave. La parte interesante es que devuelve una referencia modificable por polo que se puede modificar si se quiere. En el siguiente ejemplo e inserta true cada vez que se inserta un libro en el HashMap.

Se quiere llevar el seguimiento de los libros de un biblioteca.

use std::collections::HashMap;

fn main() {
    let book_collection = vec!["L'Allemagne Moderne", "Le Petit Prince", "Eye of the World", "Eye of the World"]; // Eye of the World aparece dos veces

    let mut book_hashmap = HashMap::new();

    for book in book_collection {
        book_hashmap.entry(book).or_insert(true);
    }
    for (book, true_or_false) in book_hashmap {
        println!("¿Tenemos el libro {}? {}", book, true_or_false);
    }
}

Que imprime:

¿Tenemos el libro Eye of the World? true
¿Tenemos el libro Le Petit Prince? true
¿Tenemos el libro L'Allemagne Moderne? true

Pero esto no es exactamente lo que se quiere. Sería mejor contar el número de copias de cada libro para que se pueda conocer que existen dos copias de Eye of the world.

En primer lugar, se va a estudiar lo que hace el método .entry() y el método .or_insert(). .entry() devuelve un enum llamado Entry:

#![allow(unused)]
fn main() {
pub fn entry(&mut self, key: K) -> Entry<K, V> // 🚧
}

Esta es la página de Entry. Esa es una versión simplificada de su código. K representa el tipo de la clave y V representa el tipo del valor.

#![allow(unused)]
fn main() {
// 🚧
use std::collections::hash_map::*;

enum Entry<K, V> {
    Occupied(OccupiedEntry<K, V>),
    Vacant(VacantEntry<K, V>),
}
}

Cuando se llama a .or_insert() se observa el tipo concreto del enumerado y se decide qué hacer:

#![allow(unused)]
fn main() {
fn or_insert(self, default: V) -> &mut V { // 🚧
    match self {
        Occupied(entry) => entry.into_mut(),
        Vacant(entry) => entry.insert(default),
    }
}
}

Lo más interesante es que se devuelve una referencia modificable: &mut V. Esto significa que se puede usar let para asignarla a una variable y cambiar la variable para cambiar el valor del HashMap. Así, para cada libro se insertará un 0 si no hay una entrada. Si hay una, se utilizará +=1 en la referencia para incrementar la cuenta. El código queda así:

use std::collections::HashMap;

fn main() {
    let book_collection = vec!["L'Allemagne Moderne", "Le Petit Prince", "Eye of the World", "Eye of the World"];

    let mut book_hashmap = HashMap::new();

    for book in book_collection {
        let return_value = book_hashmap.entry(book).or_insert(0); // return_value es una referencia mutable.
        // Si no contiene nada, se asigna un cero.
        *return_value +=1; // Ahora return_value vale al menos 1.
        // Y si ya tenía algún valor, lo incrementa en uno
    }

    for (book, numero) in book_hashmap {
        println!("{}, {}", book, numero);
    }
}

Lo important es let return_value = book_hashmap.entry(book).or_insert(0);. Si no se asignara el valor a una variable, se asignaría el 0 cuando no hubiera valor, pero se perdería la referencia modificable. Al conservarla en la variable return_value, se puede modificar el valor sumándole 1 en este caso. Cuando esto sucede por segunda vez para un mismo valor, no se crea ninguna entrada nueva con un valor 0, sino que simplemente se devuelve el valor para que se pueda incrementar. Así el resultado de este programa es:

L'Allemagne Moderne, 1
Le Petit Prince, 1
Eye of the World, 2

También se pueden hacer otras cosas con .or_insert() como insertar un vector y luego insertar en el vector. Por ejemplo, si se supone que se pregunta a hombres y mujeres qué opinan de un político para que les asignen una valoración de 0 a 10, se pueden clasificar juntos los puntos para saber si un político es más popular entre los hombres o entre las mujeres, el código podría ser así:

use std::collections::HashMap;

fn main() {
    let data = vec![ // Estos son los datos puros
        ("hombre", 9),
        ("mujer", 5),
        ("hombre", 0),
        ("mujer", 6),
        ("mujer", 5),
        ("hombre", 10),
    ];

    let mut survey_hash = HashMap::new();

    for item in data { // Devuelve una tupla de (&str, i32)
        survey_hash.entry(item.0)
            .or_insert(Vec::new())
            .push(item.1); // Añade el número al vector contenido en el valor correspondiente del HashMap
    }

    for (hombre_or_mujer, numeros) in survey_hash {
        println!("{:?}: {:?}", hombre_or_mujer, numeros);
    }
}

Que imprime:

"mujer", [5, 6, 5]
"hombre", [9, 0, 10]

La línea de código importante es survey_hash.entry(item.0).or_insert(Vec::new()).push(item.1); que si recibe una "mujer" comprobará si ya existe en el HashMap. Si no existe, insertará Vec::new() y después insertará el número en el vector. Si existe, no insertará ningún vector nuevo, lo recuperará e insertará el número en el vector.

HashSet y BTreeSet

Un HashSet es un HashMap que solo tiene claves. En la página para HashSet explica lo siguiente:

"Es un hash set implementado con HashMap en el que el valor es ()." Es un HashMap con claves y sin valores.

Se utiliza frecuentemente para saber si una clave existe o no.

Por ejemplo, si se tienen 100 números aleatorios y cada uno de ellos se encuentra entre el 1 y el 100, habrá números entre el 1 y el 100 que aparezcan varias veces y algunos que no aparecerán. Si se insertan en un HashSet se obtendrá una lista de todos los números que sí han aparecido sin tener en cuenta el número de veces que lo han hecha.

use std::collections::HashSet;

fn main() {
    let many_numeros = vec![
        94, 42, 59, 64, 32, 22, 38, 5, 59, 49, 15, 89, 74, 29, 14, 68, 82, 80, 56, 41, 36, 81, 66,
        51, 58, 34, 59, 44, 19, 93, 28, 33, 18, 46, 61, 76, 14, 87, 84, 73, 71, 29, 94, 10, 35, 20,
        35, 80, 8, 43, 79, 25, 60, 26, 11, 37, 94, 32, 90, 51, 11, 28, 76, 16, 63, 95, 13, 60, 59,
        96, 95, 55, 92, 28, 3, 17, 91, 36, 20, 24, 0, 86, 82, 58, 93, 68, 54, 80, 56, 22, 67, 82,
        58, 64, 80, 16, 61, 57, 14, 11];

    let mut numero_hashset = HashSet::new();

    for numero in many_numeros {
        numero_hashset.insert(numero);
    }

    let hashset_length = numero_hashset.len(); // Cuántos números contiene
    println!("Hay {} números únicos, por lo que faltan {}.", hashset_length, 100 - hashset_length);

    // Veamos cuáles son los que faltan
    let mut missing_vec = vec![];
    for numero in 0..100 {
        if numero_hashset.get(&numero).is_none() { // Si .get() devuelve None,
            missing_vec.push(numero);
        }
    }

    print!("No contiene: ");
    for numero in missing_vec {
        print!("{} ", numero);
    }
}

Este código imprime:

Hay 66 números únicos, por lo que faltan 34.
No contiene: 1 2 4 6 7 9 12 21 23 27 30 31 39 40 45 47 48 50 52 53 62 65 69 70 72 75 77 78 83 85 88 97 98 99

Un BTreeSet es similar a un HashSet de la misma manera que un BTreeMap lo es a un HashMap. Si se imprimen los elementos de un HashSet lo harán en cualquier orden:

#![allow(unused)]
fn main() {
for entry in numero_hashset { // 🚧
    print!("{} ", entry);
}
}

Puede que imprima: 67 28 42 25 95 59 87 11 5 81 64 34 8 15 13 86 10 89 63 93 49 41 46 57 60 29 17 22 74 43 32 38 36 76 71 18 14 84 61 16 35 90 56 54 91 19 94 44 3 0 68 80 51 92 24 20 82 26 58 33 55 96 37 66 79 73. Pero casi nunca lo imprimirá en el mismo orden en distintas repeticiones.

De nuevo, es muy fácil cambiar un HashSet a BTreeSet si se decide que se necesita mantenerlo ordenado. En el código anterior, basta con cambiar en dos sitios de HashSet a BTreeSet.

use std::collections::BTreeSet; // HashSet a BTreeSet

fn main() {
    let many_numeros = vec![
        94, 42, 59, 64, 32, 22, 38, 5, 59, 49, 15, 89, 74, 29, 14, 68, 82, 80, 56, 41, 36, 81, 66,
        51, 58, 34, 59, 44, 19, 93, 28, 33, 18, 46, 61, 76, 14, 87, 84, 73, 71, 29, 94, 10, 35, 20,
        35, 80, 8, 43, 79, 25, 60, 26, 11, 37, 94, 32, 90, 51, 11, 28, 76, 16, 63, 95, 13, 60, 59,
        96, 95, 55, 92, 28, 3, 17, 91, 36, 20, 24, 0, 86, 82, 58, 93, 68, 54, 80, 56, 22, 67, 82,
        58, 64, 80, 16, 61, 57, 14, 11];

    let mut numero_btreeset = BTreeSet::new(); // HashSet a BTreeSet

    for numero in many_numeros {
        numero_btreeset.insert(numero);
    }
    for entry in numero_btreeset {
        print!("{} ", entry);
    }
}

Que lo imprimirá en orden: 0 3 5 8 10 11 13 14 15 16 17 18 19 20 22 24 25 26 28 29 32 33 34 35 36 37 38 41 42 43 44 46 49 51 54 55 56 57 58 59 60 61 63 64 66 67 68 71 73 74 76 79 80 81 82 84 86 87 89 90 91 92 93 94 95 96.

BinaryHeap

Un BinaryHeap es un tipo de colección mayormente desordenada, pero que tiene un bit de orden. Mantiene el elemento mayor al comienzo, pero los demás elementos están en cualquier orden.

Se usará otra lista de elementos para el ejemplo, pero esta vez, será más pequeña.

use std::collections::BinaryHeap;

fn muestra_contenido(input: &BinaryHeap<i32>) -> Vec<i32> {
    // Esta función recupera el contenido de un BinaryHeap.
    // Un iterador sería más rápido que esta función
    // se aprenderán más adelante
    let mut remainder_vec = vec![];
    for numero in input {
        remainder_vec.push(*numero)
    }
    remainder_vec
}

fn main() {
    let many_numeros = vec![0, 5, 10, 15, 20, 25, 30]; // Estos números están ordenados

    let mut my_heap = BinaryHeap::new();

    for numero in many_numeros {
        my_heap.push(numero);
    }

    while let Some(numero) = my_heap.pop() { // .pop() devuelve Some(numero) si está, None si no está. Lo recupera del comienzo
        println!("Se extrae el {}. Los restantes números son: {:?}", numero, muestra_contenido(&my_heap));
    }
}

Que imprime:

Se extrae el 30. Los restantes números son: [25, 15, 20, 0, 10, 5]
Se extrae el 25. Los restantes números son: [20, 15, 5, 0, 10]
Se extrae el 20. Los restantes números son: [15, 10, 5, 0]
Se extrae el 15. Los restantes números son: [10, 0, 5]
Se extrae el 10. Los restantes números son: [5, 0]
Se extrae el 5. Los restantes números son: [0]
Se extrae el 0. Los restantes números son: []

Se observa que siempre está el número mayor en el índice 0. A partir del índice 1 no existe orden.

Un buen uso para BinaryHeap es como colección de cosas a hacer. Se puede crear un BinaryHeap<(u8, &str)> en el que el u8 indica la importancia de la tarea. La cadena de texto &str es la descripción de lo que hay que hacer:

use std::collections::BinaryHeap;

fn main() {
    let mut tareas = BinaryHeap::new();

    // Añade las tareas a hacer durante el día
    tareas.push((100, "Contestar correo al CEO"));
    tareas.push((80, "Finalizar el informe hoy"));
    tareas.push((5, "Ver algo en YouTube"));
    tareas.push((70, "Dar las gracias a tu equipo por trabajar siempre duro"));
    tareas.push((30, "Planear a quién contratar parar el equipo"));

    while let Some(job) = tareas.pop() {
        println!("Tienes que hacer: {}", job.1);
    }
}

Lo que siempre imprimirá:

Tienes que hacer: Contestar correo al CEO
Tienes que hacer: Finalizar el informe hoy
Tienes que hacer: Dar las gracias a tu equipo por trabajar siempre duro
Tienes que hacer: Planear a quién contratar parar el equipo
Tienes que hacer: Ver algo en YouTube

VecDeque

Un VecDeque es un Vec que tiene buen rendimiento extrayendo elementos tanto por el inicio, como por el final. Rust tiene VecDeque porque Vec solo tiene buen rendimiento extrayendo elementos por el final. Cuando se usa .pop() en un Vec, solamente se tiene que recuperar el último elemento de la derecha y nada más se mueve. Pero si se recupera cualquier otro elemento, todos los que quedan a su derecha se tienen que mover hacia la izquierda. Se puede ver esto en la descripción del método .remove() de Vec:

Quita y devuelve el elemento en la posición con el índice indicado, desplazando todos los elementos posteriores hacia la izquierda.

Por eso, si se hace lo siguiente:

fn main() {
    let mut my_vec = vec![9, 8, 7, 6, 5];
    my_vec.remove(0);
}

Se eliminará el 9. El 8 en el índice 1 se moverá al índice 0, el 7 en el índice 2 se moverá al 1 y así sucesivamente. Se puede imaginar lo complejo que sería que un aparcamiento de vehículos funcionara así cada vez que un coche sale de él...

Esta forma de eliminar el primer elemento supone mucho trabajo para el ordenador. De hecha, si se ejecuta esto en el Playground de rust, problamente lo abanhecha debido a que es mucho esfuerzo:

fn main() {
    let mut my_vec = vec![0; 600_000];
    for i in 0..600000 {
        my_vec.remove(0);
    }
}

El código anterior construye un Vec de 600.000 ceros. Cada vez que usa .remove(0) se tiene que mover cada cero una posición a la izquierda. Y esto se tiene que hacer hasta 600.000 veces.

Esto no es un problema para VecDeque. En general, puede ser un poco más lento que un Vec, pero si es necesario realizar operaciones en ambos lados es muchísimo más eficiente. Se puede construir a partir de un vector mediante VecDeque::from. El código anterior quedaría modificado así:

use std::collections::VecDeque;

fn main() {
    let mut my_vec = VecDeque::from(vec![0; 600_000]);
    for i in 0..600000 {
        my_vec.pop_front();
    }
}

Que ahora es mucho más rápido y en el Playground de Rust se acaba en menos de un segundo en lugar de fallar.

En el siguiente ejemplo se dispone de un Vec de cosas para hacer. Se convierte a un VecDeque y se usa .push_front() para añadir elementos al inicio. Así el primer elemento añadido estará a la derecha. Cada elemento que se inserta es una tupla (&str, bool): &str es la descripción de la tarea y false significa que no se ha ejecutado aún. Se usa la función .hecha() para extraer un elemento del final, pero sin eliminarlo. En su lugar, se cambia de false a true y se inserta al inicio para conservarlo.

Queda así:

use std::collections::VecDeque;

fn comprueba_restantes(input: &VecDeque<(&str, bool)>) { // Cada elemento es un (&str, bool)
    for item in input {
        if item.1 == false {
            println!("Tienes que: {}", item.0);
        }
    }
}

fn hecha(input: &mut VecDeque<(&str, bool)>) {
    let mut tarea_hecha = input.pop_back().unwrap(); // extrae del final
    tarea_hecha.1 = true;                            // ahora está hecha - se marca como true
    input.push_front(tarea_hecha);                   // y se inserta al inicio
}

fn main() {
    let mut my_vecdeque = VecDeque::new();
    let things_to_do = vec!["enviar correo al cliente", "añadir un producto a la lista", "devolver la llamada a Loki"];

    for thing in things_to_do {
        my_vecdeque.push_front((thing, false));
    }

    hecha(&mut my_vecdeque);
    hecha(&mut my_vecdeque);

    comprueba_restantes(&my_vecdeque);

    for tarea in my_vecdeque {
        print!("{:?} ", tarea);
    }
}

Que imprime:

Tienes que: devolver la llamada a Loki
("añadir un producto a la lista", true) ("enviar correo al cliente", true) ("devolver la llamada a Loki", false)

El operador ?

Existe una forma más corta de gestionar un valor de tipo Result (y Option). más corta que match y que if let. Es el "operador interrogación" y es ?. Después de una función devuelve un resultado, se puede añadir ? para:

  • devolver el valor contenido en Ok, si es el tipo concreto de Result.
  • o elevar el error si es Err.

En otras palabras, hace casi todo por ti.

Por ejemplo, se puede probar esto con .parse(). En el siguiente ejemplo se presenta la función parse_str que intenta convertir un &str a i32:

use std::num::ParseIntError;

fn parse_str(input: &str) -> Result<i32, ParseIntError> {
    let parsed_number = input.parse::<i32>()?; // Aquí está el ?
    Ok(parsed_number)
}

fn main() {}

Esta función toma un &str. Si es Ok, devuelve un i32 envuelto en un Ok. Si es un Err, devuelve ParseIntError. Al intentar obtener el número y haberse añadido ? lo que se hace es comprobar si esta llamada a parse ha devuelto Ok y obtener el contenido de este. Si no es correcto, devolverá un error y finalizará en la linea de parse. Pero si es correcto avanza hasta la línea siguiente para devolver Ok(i32). Es necesario envolver el número para que sea compatible con el valor de retorno de la función Result<i32, ParseIntError>, no i32.

Ahora, se puede probar a ejecutar la función. Se observa a continuación lo que sucede con un vector de &str.

fn parse_str(input: &str) -> Result<i32, std::num::ParseIntError> {
    let parsed_number = input.parse::<i32>()?;
    Ok(parsed_number)
}

fn main() {
    let str_vec = vec!["Siete", "8", "9.0", "bien", "6060"];
    for item in str_vec {
        let parsed = parse_str(item);
        println!("{:?}", parsed);
    }
}

Que imprime:

Err(ParseIntError { kind: InvalidDigit })
Ok(8)
Err(ParseIntError { kind: InvalidDigit })
Err(ParseIntError { kind: InvalidDigit })
Ok(6060)

¿Cómo se puede conocer que el error de esta función parse era std::num::ParseIntError? Una forma fácil es "pedírselo" al compilador:

fn main() {
    let failure = "No es un número".parse::<i32>();
    failure.rbrbrb(); // ⚠️ Compilador: "¿Qué es rbrbrb()???"
}

El compilador se "queja" con:

error[E0599]: no method named `rbrbrb` found for enum `std::result::Result<i32, std::num::ParseIntError>` in the current scope
 --> src\main.rs:3:13
  |
3 |     failure.rbrbrb();
  |             ^^^^^^ method not found in `std::result::Result<i32, std::num::ParseIntError>`

Así se deduce que la definición de tipo que devuelve parse es std::result::Result<i32, std::num::ParseIntError>.

No se necesita escribir std::result::Result porque Result está siempre visible en el alcance (alcance = listo para su uso). Rust hace esto para todos los tipos que se utilizan mucho para que no sea necesario escribir o usar std::result::Result, std::collections::Vec, etc.

Aún no se han presentado recursos como los ficheros, por lo que el operador ? no parece aún muy útil. El siguiente ejemplo muestra cómo se puede ilustrar este uso en una sola línea. En lugar de construir un i32 con .parse(), se hace mucho más. Se construye un u16, luego se convierte a String, después se convierte a u32 y de nuevo se pasa a String para, finalmente, convertirlo en un i32.

use std::num::ParseIntError;

fn parse_str(input: &str) -> Result<i32, ParseIntError> {
    let parsed_number = input.parse::<u16>()?.to_string().parse::<u32>()?.to_string().parse::<i32>()?; // Se añade ? a cada función para validar la corrección y pasar el valor
    Ok(parsed_number)
}

fn main() {
    let str_vec = vec!["Siete", "8", "9.0", "bien", "6060"];
    for item in str_vec {
        let parsed = parse_str(item);
        println!("{:?}", parsed);
    }
}

Esto imprime lo mismo, pero después de haber procesado tres Result en una sola línea. Después, se mostrará un ejemplo parecido con ficheros, puesto que las funciones asociadas siempre devuelven Result ya que pueden ir mal muchas cosas cuando se accede a ellos.

Se puede imaginar lo siguiente: se quiere abrir un fichero, escribir y cerrarlo. Lo primero es encontrar el fichero con éxito (eso es un Result); después, escribir en él sin fallos (esto es otro Result). Con ? se puede hacer todo en una sola línea.

Cuando panic y unwrap son buenos

Rust dispone de una macro panic! que se puede utilizar para que "entre en pánico".

fn main() {
    panic!("¡hora de entrar en pánico!");
}

Cuando se ejecuta este programa se muestra el siguiente mensaje: thread 'main' panicked at '¡hora de entrar en pánico!', src/main.rs:2:5.

Se muestra el programa, la línea de código y columna 2:5 en el que se "entró en pánico". Con esta información, se puede buscar la línea de código y arreglarla.

panic! tiene utilidad para asegurar que algo no cambia. Por ejemplo, la siguiente función imprime_tres_cosas siempre imprime los índices [0], [1] y [2] de un vector. Esto está bien siempre que reciba un vector con tres elementos.

fn imprime_tres_cosas(vector: Vec<i32>) {
    println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
    let my_vec = vec![8, 9, 10];
    imprime_tres_cosas(my_vec);
}

Esto imprime 8, 9 y 10 correctamente.

Si posteriormente se escribe más código y se olvida que my_vec tiene que ser siempre de tres elementos, se puede acabar un vector de seis elementos como en el caso siguiente:

fn imprime_tres_cosas(vector: Vec<i32>) {
  println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
  let my_vec = vec![8, 9, 10, 10, 55, 99]; // Ahora tiene seis elementos
  imprime_tres_cosas(my_vec);
}

Con este ejemplo, parece que todo está bien, no hay error. Pero si es importante que el vector solo tenga tres cosas, no se detectaría el fallo. Se podría hacer lo siguiente:

fn imprime_tres_cosas(vector: Vec<i32>) {
    if vector.len() != 3 {
        panic!("my_vec siempre tiene que tener tres elementos") // entrará en pánico si tiene longitud 3
    }
    println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
    let my_vec = vec![8, 9, 10];
    imprime_tres_cosas(my_vec);
}

Ahora, si el vector tuviera seis elementos, el programa fallaría (N.T. a las condiciones que se deben cumplir siempre en una función -en sus parámetros o valores de retorno o durante su ejecución- se le denomina "invariante". Rust proporciona macros específicas para ello assert! y assert_debug!):

    // ⚠️
fn imprime_tres_cosas(vector: Vec<i32>) {
    if vector.len() != 3 {
        panic!("my_vec siempre tiene que tener tres")
    }
    println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
    let my_vec = vec![8, 9, 10, 10, 55, 99];
    imprime_tres_cosas(my_vec);
}

Este programa devuelve thread 'main' panicked at 'my_vec siempre tiene que tener tres', src/main.rs:4:9. Gracias a panic!, se alerta de que solo debería tener tres elementos el vector my_vec. Es una macro útil para meter recordatorios en el código.

Existen otras tres macros que son similares y que se utilizan mucho durante las pruebas. Son: assert!, assert_eq! y assert_ne!. Esto es lo que hacen:

  • assert!(): el programa entra en pánico cuando la evaluación del contenido entre () no es cierta.
  • assert_eq!(): el programa entra en pánico cuando los dos elementos contenidos entre () no son iguales.
  • assert_ne!(): el programa entra en pánico cuando los dos elementos contenidos entre () son iguales.

Algunos ejemplos son:

fn main() {
    let my_name = "Loki Laufeyson";

    assert!(my_name == "Loki Laufeyson");
    assert_eq!(my_name, "Loki Laufeyson");
    assert_ne!(my_name, "Mithridates");
}

El código anterior funciona sin problemas ya que en este caso todas las macros comprueban con éxito los valores que reciben como parámetro.

También es posible añadir un mensaje a cada macro:

fn main() {
    let my_name = "Loki Laufeyson";

    assert!(
        my_name == "Loki Laufeyson",
        "{} should be Loki Laufeyson",
        my_name
    );
    assert_eq!(
        my_name, "Loki Laufeyson",
        "{} and Loki Laufeyson should be equal",
        my_name
    );
    assert_ne!(
        my_name, "Mithridates",
        "You entered {}. Input must not equal Mithridates",
        my_name
    );
}

Estos mensajes se mostrarán solamente si el programa entra en pánico (no se cumple la condición que establece la macro). Por eso, si se ejecuta lo siguiente:

fn main() {
    let my_name = "Mithridates";

    assert_ne!(
        my_name, "Mithridates",
        "You enter {}. Input must not equal Mithridates",
        my_name
    );
}

Se mostrará:

thread 'main' panicked at 'assertion failed: `(left != right)`
  left: `"Mithridates"`,
 right: `"Mithridates"`: You entered Mithridates. Input must not equal Mithridates', src\main.rs:4:5

Esta respuesta está diciendo que se esperaba que los valores fuesen distintos left != right, pero se han recibido valores iguales.

También es útil el uso de unwrap cuando se escribe un programa y se quiere que falle (entre en pánico) cuando haya un problema. Posteriormente, cuando el código esté completo, conviene eliminar unwrap por una alternativa más robusta que impida que el programa se pare.

También se puede usar expect, que es como unwrap, pero permite que se le pase un mensaje que se mostrará en el momento del error. Los libros de texto suelen tener el siguiente aviso: "si se utiliza unwrap() mucho, al menos se debería usar expect() para tener mensajes de error adecuados.

Esto fallará:

   // ⚠️
fn get_cuatro(input: &Vec<i32>) -> i32 {
    let cuatro = input.get(3).unwrap();
    *cuatro
}

fn main() {
    let my_vec = vec![9, 0, 10];
    let cuatro = get_cuatro(&my_vec);
}

El mensaje de error es: thread 'main' panicked at 'called Option::unwrap() on a None value', src\main.rs:7:18.

   // ⚠️
fn get_cuatro(input: &Vec<i32>) -> i32 {
    let cuatro = input.get(3).expect("Input vector needs at least 4 items");
    *cuatro
}

fn main() {
    let my_vec = vec![9, 0, 10];
    let cuatro = get_cuatro(&my_vec);
}

Vuelve a fallar, pero el mensaje de error es mejor: thread 'main' panicked at 'Input vector needs at least 4 items', src\main.rs:7:18. .expect() es un poco mejor que .unwrap(), pero sigue fallando con None. A continuación se muestra un ejemplo de una mala práctica una función que intenta unwrap dos veces. Toma como parámetros un Vec<Option<i32>>, por lo que puede que cada parte contenta un Some<i32> o un None.

fn intenta_dos_unwraps(input: Vec<Option<i32>>) {
    println!("Índice 0 es: {}", input[0].unwrap());
    println!("Índice 1 es: {}", input[1].unwrap());
}

fn main() {
    let vector = vec![None, Some(1000)]; // Este vector tiene un None, por lo que entrará en pánico
    intenta_dos_unwraps(vector);
}

El mensaje es "thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:2:42. No se puede saber en qué unwrap sucedió hasta revisar la línea. Sería mejor valorar la longitud y no unwrap. Pero con .expect() al menos será un poco mejor:

fn intenta_dos_unwrap(input: Vec<Option<i32>>) {
    println!("Index 0 is: {}", input[0].expect("¡El primer unwrap contenía un None!"));
    println!("Index 1 is: {}", input[1].expect("¡El segundo unwrap contenía un None!"));
}

fn main() {
    let vector = vec![None, Some(1000)];
    intenta_dos_unwrap(vector);
}

Ahora el resultado es algo mejor: thread 'main' panicked at '¡El primer unwrap contenía un None!', src/main.rs:2:41. Y se dispone también de la línea en la que ha sucedido para encontrarlo.

También se puede utilizar unwrap_or si siempre existe un valor a usar por defecto cuando se devuelve None. Esta función no entra en pánico. Esto es:

  1. Bueno, porque el programa no entra en pánico, pero...
  2. no tan bueno si se quiere detectar un problema en el código.

En todo caso, lo habitual es que no se quiera que el programa entre en pánico, por lo que unwrap_or es muy útil.

fn main() {
    let my_vec = vec![8, 9, 10];

    let cuatro = my_vec.get(3).unwrap_or(&0); // Si .get no funciona, 
    // se toma por defecto el &0.
    // .get devuelve una referencia, por eso se necesita &0

    println!("{}", cuatro);
}

Esto imprime 0 porque .unwrap_or(&0) devuelve 0 cuando el Option es un None.

Traits (Rasgos)

Anteriormente, se han visto algunos traits1 (rasgos). Debug, Copy y Clone son traits (rasgos). Para que un tipo tenga un trait (rasgo) hay que implementarlo. Puesto que algunos traits son tan comunes (como Debug), existen atributos en Rust que los implementan automáticamente (con una implementación por defecto). Esto es lo que sucede cuando se escribe #[derive(Debug)]: se implementa de forma automática el trait Debug.

#[derive(Debug)]
struct MiStruct {
    numero: usize,
}

fn main() {}

Pero hay otros traits que son más difíciles de implementar y hay que hacerlo a mano con impl. Por ejemplo, el trait Add, que se encuentra en std::ops::Add y se utiliza para sumar dos cosas. Pero Rust no puede adivinar cómo se pueden sumar dos cosas cualquiera, por lo que hay qué codificarlo.

struct CosasASumar {
    primera_cosa: u32,
    segunda_cosa: f32,
}

fn main() {}

Se pueden sumar primera_cosa y segunda_cosa, pero hay que dar más información. Puede que se quiera sumar f32, algo así:

#![allow(unused)]
fn main() {
// 🚧
let resultado = self.segunda_cosa + self.primera_cosa as f32
}

O puede que se quiera poner self.primera_cosa junto a self.segunda_cosa y que sea así como se quiera sumar. Así la suma de 55 a 33.4 sería 5533.4 y no 88.4.

A continuación, se analiza en primer lugar como se crea un trait. Lo importante es recordar que los trait sirven para describir un comportamiento determinado de quien los implementen. Para crear un trait, se escribe trait y se crean algunas funciones (o ninguna).

struct Animal { // Un simple struct - un Animal que solo contiene su nombre
    nombre: String,
}

trait Perro { // El trait Perro asigna alguna funcionalidad
    fn ladrar(&self) { // Puede ladrar
        println!("¡Guau, guau!");
    }
    fn correr(&self) { // y puede correr
        println!("¡El perro está corriendo!");
    }
}

impl Perro for Animal {} // Ahora el Animal implementa el trait Perro

fn main() {
    let rover = Animal {
        nombre: "Rover".to_string(),
    };

    rover.ladrar(); // El Animal puede usar ladrar()
    rover.correr();  // y puede usar correr()
}

Así está bien, pero no se quiere imprimir "¡El perro está corriendo!". Se pueden modificar los métodos que implementa por defecto un trait, para ello la nueva implementación tiene que tener la misma declaración. Esto significa que tiene que tomar los mismos parámetros y devolver el mismo tipo de resultado. Por ejemplo, se puede modificar el método .correr(). La declaración indica:

#![allow(unused)]
fn main() {
// 🚧
fn correr(&self) {
    println!("¡El perro está corriendo!");
}
}

fn correr(&self) significa que la función correr() tiene de parámetro &self y no devuelve ningún valor. Por lo que no se puede definir una nueva implementación así:

#![allow(unused)]
fn main() {
fn correr(&self) -> i32 { // ⚠️
    5
}
}

Rust se quejará así:

   = note: expected fn pointer `fn(&Animal)`
              found fn pointer `fn(&Animal) -> i32`

Pero sí se puede hacer esto:

struct Animal { // Un simple struct - un Animal que solo contiene su nombre
    nombre: String,
}

trait Perro { // El trait Perro asigna alguna funcionalidad
    fn ladrar(&self) { // Puede ladrar
        println!("¡Guau, guau!");
    }
    fn correr(&self) { // y puede correr
        println!("¡El perro está corriendo!");
    }
}

impl Perro for Animal { // Ahora el Animal implementa el trait Perro
    fn correr(&self) {
        println!("¡{} está corriendo!", self.nombre);
    }
}

fn main() {
    let rover = Animal {
        nombre: "Rover".to_string(),
    };

    rover.ladrar(); // El Animal puede usar ladrar()
    rover.correr();  // y puede usar correr()
}

Ahora imprime ¡Rover está corriendo!. Esta función es correcta porque devuelve () o nada, que es lo que indica la implementación del trait.

Cuando se define un trait, se puede escribir solo la declaración de las funciones. Esto obliga a que quien lo quiera implementar tenga que escribir el código que desea para las funciones que solo están declaradas en el trait (sin implementar). En el código siguiente el trait solo declara las funciones sin aportar una definición por defecto de ladrar() y correr(), por eso se obliga a escribir el código en la implementación del trait por parte del Perro.

struct Animal {
    nombre: String,
}

trait Perro {
    fn ladrar(&self); // Solo se indica que necesita el parámetro &self y que no devuelva nada

    fn correr(&self); // necesita &self y que no devuelva nada
    // Ahora se tiene que escribir el código en la implementación del Perro
}

impl Perro for Animal {

    fn ladrar(&self) {
        println!("¡{}, para de ladrar!", self.nombre);
    }
    
    fn correr(&self) {
        println!("¡{} está corriendo!", self.nombre);
    }

}

fn main() {
    let rover = Animal {
        nombre: "Rover".to_string(),
    };

    rover.ladrar();
    rover.correr();
}

Cuando se crea un trait (rasgo), se debe pensar: ¿Qué funciones deberá tener? ¿qué funciones se pueden implementar en el propio trait? y ¿qué funciones deberá implementar el propio usuario?

A continuación se implementa el trait Display para el siguiente struct simple:

struct Gato {
    nombre: String,
    edad: u8,
}

fn main() {
    let mr_mantle = Gato {
        nombre: "Reggie Mantle".to_string(),
        edad: 4,
    };
}

Se quiere imprimir mr_mantle. Debug es fácil de derivar:

#[derive(Debug)]
struct Gato {
    nombre: String,
    edad: u8,
}

fn main() {
    let mr_mantle = Gato {
        nombre: "Reggie Mantle".to_string(),
        edad: 4,
    };

    println!("Mr. Mantle es un {:?}", mr_mantle);
}

Pero la implementación de Debug no es muy bonita. Este es el resultado:

Mr. Mantle es un Gato { nombre: "Reggie Mantle", edad: 4 }

Por eso, se tiene que implementar Display para un Gato si se quiere que la impresión sea bonita. En https://doc.rust-lang.org/std/fmt/trait.Display.html se puede ver la información detallada para Display con un ejemplo que dice:

use std::fmt;

struct Position {
    longitude: f32,
    latitude: f32,
}

impl fmt::Display for Position {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.longitude, self.latitude)
    }
}

fn main() {}

Hay algunas cosas que aún no se entienden en este código, como <'_> y lo que hace f. Pero se entiende que el struct Position tiene únicamente dos valores f32. Los campos de este struct son self.longitude y self.latitude. Así que posiblemente se pueda utilizar este código ara implementar la versión que se necesita para Gato, cambiando los campos por self.nombre y self.edad. Por último, write! se parece mucho a println!. Así, el código queda:

use std::fmt;

struct Gato {
    nombre: String,
    edad: u8,
}

impl fmt::Display for Gato {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} es un gato de {} años.", self.nombre, self.edad)
    }
}

fn main() {}

Si ahora se añade una función fn main(), el código queda así:

use std::fmt;

struct Gato {
    nombre: String,
    edad: u8,
}

impl fmt::Display for Gato {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} es un gato de {} años.", self.nombre, self.edad)
    }
}

fn main() {
    let mr_mantle = Gato {
        nombre: "Reggie Mantle".to_string(),
        edad: 4,
    };

    println!("{}", mr_mantle);
}

¡Estupendo! Ahora, cuando se usa {} para imprimir a un Gato, se obtiene Reggie Mantle es un gato de 4 años., que queda mucho mejor.

Por cierto, cuando se implementa el trait Display se dispone del trait ToString sin nada que hacer adicionalmente. Esto pasa al usar la macro format! que facilita la creación de un String con la función .to_string(). Así que se puede hacer algo como lo siguiente cuando se pasa la variable reggie_mantle a una función que necesite un String:

use std::fmt;
struct Gato {
    nombre: String,
    edad: u8,
}

impl fmt::Display for Gato {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} es un gato de {} años.", self.nombre, self.edad)
    }
}

fn print_gatos(mascota: String) {
    println!("{}", mascota);
}

fn main() {
    let mr_mantle = Gato {
        nombre: "Reggie Mantle".to_string(),
        edad: 4,
    };

    print_gatos(mr_mantle.to_string()); // Lo convierte en un String
    println!("La cadena de caracteres de Mr. Mantle tiene {} letras.",
        mr_mantle.to_string().chars().count()); // Los convierte en caracteres y los cuenta
}

Lo anterior imprime:

Reggie Mantle es un gato de 4 años.
La cadena de caracteres de Mr. Mantle tiene 35 letras.

Lo que se debe recordar sobre los rasgos es que tratan sobre el comportamiento de algo. ¿Cómo se comporta un determinado struct? ¿Qué puede hacer? Para eso son los rasgos. Si se revisan los rasgos que se han visto hasta el momento, todos tratan sobre algún comportamiento: Copyes algo que un tipo determinado puede hacer. Display también es algo que un tipo puede hacer. ToString es otro rasgo y también es algo que un tipo puede hacer: convertirse en una String. En el rasgo Perro la palabra perro no significa algo que se pueda hacer, pero proporciona algunos métodos que le permiten hacer cosas. Se puede implementar para una estructura Caniche o para una Chucho y así ambas podrían disponer de los métodos de Perro.

A continuación se presenta otro ejemplo aún más conectado con algo que solo es comportamiento. Se imaginará un juego de fantasía con algunos personajes simples. Uno es un Monstruo, los otros dos son Mago y Cazador. El Monstruo tiene salud y se le puede atacar, los otros dos no tendrán nada aún. Se crearán dos rasgos (traits). Uno llamado LuchaCercana que permite luchar de cerca y otro LuchaADistancia que permite lugar desde lejos. Solo el Cazador puede usar LuchaADistancia. Este es el código resultante:

struct Monstruo {
    salud: i32,
}

struct Mago {}
struct Cazador {}

trait LuchaCercana {
    fn atacar_con_espada(&self, oponente: &mut Monstruo) {
        oponente.salud -= 10;
        println!(
            "Atacaste con espada. Tu oponente tiene ahora {} de salud.",
            oponente.salud
        );
    }
    fn atacar_con_la_mano(&self, oponente: &mut Monstruo) {
        oponente.salud -= 2;
        println!(
            "Atacaste con la mano. tu oponente tiene ahora {} de salud.",
            oponente.salud
        );
    }
}
impl LuchaCercana for Mago {}
impl LuchaCercana for Cazador {}

trait LuchaADistancia {
    fn atacar_con_arco(&self, oponente: &mut Monstruo, distancia: u32) {
        if distancia < 10 {
            oponente.salud -= 10;
            println!(
                "Atacaste con el arco. Tu oponente tiene ahora {} de salud.",
                oponente.salud
            );
        }
    }
    fn atacar_con_piedra(&self, oponente: &mut Monstruo, distancia: u32) {
        if distancia < 3 {
            oponente.salud -= 4;
        }
        println!(
            "Atacaste con una piedra. Your oponente tiene ahora {} de salud.",
            oponente.salud
        );
    }
}
impl LuchaADistancia for Cazador {}

fn main() {
    let radagast = Mago {};
    let aragorn = Cazador {};

    let mut uruk_hai = Monstruo { salud: 40 };

    radagast.atacar_con_espada(&mut uruk_hai);
    aragorn.atacar_con_arco(&mut uruk_hai, 8);
}

Lo anterior imprime:

Atacaste con espada. Tu oponente tiene ahora 30 de salud.
Atacaste con el arco. Tu oponente tiene ahora 20 de salud.

En este ejemplo, se pasa constantemente self a los métodos de los rasgos, pero solo se conoce que se trata de un tipo que implementa el propio rasgo. Puede ser un Mago o un Cazador. O podría ser una nueva struct llamada Toefocfgetobjtnode o cualquier otra que implementara el rasgo correspondiente. Para facilitar que self tenga más funcionalidad, se puede añadir restricciones a un trait. Se puede añadir otros trait que son necesarios para el trait que se define. Por ejemplo, si que se quiere que en los métodos de un trait se pueda usar {:?} para imprimir, se necesita que implemente el trait Debug. Eso se puede indicar mediante la escritura de : (dos puntos). Así quedaría el código:

struct Monstruo {
    salud: i32,
}

#[derive(Debug)] // Ahora el mago tiene Debug
struct Mago {
    salud: i32, // Ahora el mago tiene salud
}

#[derive(Debug)] // Ahora el Cazador tiene Debug
struct Cazador {
    salud: i32, // Ahora el cazador tiene salud
}

trait LuchaCercana: std::fmt::Debug {
    // Ahora el tipo necesita tener Debug 
    // para poder implementar LuchaCercana
    fn atacar_con_espada(&self, oponente: &mut Monstruo) {
        oponente.salud -= 10;
        println!(
            "Atacaste con espada. Tu oponente tiene ahora {} de salud. Ahora tienes {:?}",
            // se puede usar {:?} porque se dispone de Debug
            oponente.salud, &self
        );
    }
    fn atacar_con_la_mano(&self, oponente: &mut Monstruo) {
        oponente.salud -= 2;
        println!(
            "Atacaste con la mano. tu oponente tiene ahora {} de salud. Ahora tienes {:?}",
            oponente.salud, &self
        );
    }
}
impl LuchaCercana for Mago {}
impl LuchaCercana for Cazador {}

trait LuchaADistancia: std::fmt::Debug {
    // también se podría haber escrito LuchaADistancia: LuchaCercana
    // porque LuchaCercana implementa ya Debug
    fn atacar_con_arco(&self, oponente: &mut Monstruo, distancia: u32) {
        if distancia < 10 {
            oponente.salud -= 10;
            println!(
                "Atacaste con el arco. Tu oponente tiene ahora {} de salud. Ahora tienes {:?}",
                oponente.salud, &self
            );
        }
    }
    fn atacar_con_piedra(&self, oponente: &mut Monstruo, distancia: u32) {
        if distancia < 3 {
            oponente.salud -= 4;
        }
        println!(
            "Atacaste con una piedra. Your oponente tiene ahora {} de salud. Ahora tienes {:?}",
            oponente.salud, &self
        );
    }
}
impl LuchaADistancia for Cazador {}

fn main() {
    let radagast = Mago { salud: 60 };
    let aragorn = Cazador { salud: 80 };

    let mut uruk_hai = Monstruo { salud: 40 };

    radagast.atacar_con_espada(&mut uruk_hai);
    aragorn.atacar_con_arco(&mut uruk_hai, 8);
}

Ahora, se imprime:

Atacaste con espada. Tu oponente tiene ahora 30 de salud. Ahora tienes Mago { salud: 60 }
Atacaste con el arco. Tu oponente tiene ahora 20 de salud. Ahora tienes Cazador { salud: 80 }

En un juego real sería mejor reescribir esto para cada tipo, ya que Ahora tienes Mago { salud: 60 } queda muy raro. Por eso, los métodos predefinidos de los rasgos suelen ser muy simples, porque no se conoce el tipo que lo va a usar. No se puede escribir algo como self.0 += 10, por ejemplo. En cualquier caso, el ejemplo anterior muestra cómo se puede usar otro trait dentro de uno que se esté escribiendo y disponer así de los métodos de ese otro.

Otra forma de usar un rasgo es mediante lo que se llama trait bounds (N.T.: límites de un rasgo). Son fáciles de implementar ya que no necesitan nada. A continuación, se reescribe el ejemplo anterior sin que los rasgos implementen ningún método, a cambio, se dispone de funciones que necesitan rasgos para que se puedan usar.

use std::fmt::Debug;  // para no tener que escribir todo el camino al rasgo

struct Monstruo {
    salud: i32,
}

#[derive(Debug)]
struct Mago {
    salud: i32,
}
#[derive(Debug)]
struct Cazador {
    salud: i32,
}

trait Magia{} // todos los traits sin métodos. Son límites de rasgo


trait LuchaCercana {}
trait LuchaADistancia {}

impl LuchaCercana for Cazador{} // Cada tipo tiene LuchaCercana,
impl LuchaCercana for Mago {}
impl LuchaADistancia for Cazador{} // Pero solo el cazador lucha a distancia
impl Magia for Mago{}  // y solo el mago hace magia

fn atacar_con_flecha<T: LuchaADistancia + Debug>(personaje: &T, oponente: &mut Monstruo, distancia: u32) {
    if distancia < 10 {
        oponente.salud -= 10;
        println!(
            "Atacaste con el arco. Tu oponente tiene ahora {} de salud. Ahora tienes {:?}",
            oponente.salud, personaje
        );
    }
}

fn atacar_con_espada<T: LuchaCercana + Debug>(personaje: &T, oponente: &mut Monstruo) {
    oponente.salud -= 10;
    println!(
        "Atacaste con la espada. Tu oponente tiene ahora {} de salud. Ahora tienes {:?}",
        oponente.salud, personaje
    );
}

fn atacar_con_bola_de_fuego<T: Magia + Debug>(personaje: &T, oponente: &mut Monstruo, distancia: u32) {
    if distancia < 15 {
        oponente.salud -= 20;
        println!("¡Levantas las manos y conjuras una bola de fuego! Tu oponente tiene ahora {} de salud. Ahora tienes: {:?}",
    oponente.salud, personaje);
    }
}

fn main() {
    let radagast = Mago { salud: 60 };
    let aragorn = Cazador { salud: 80 };

    let mut uruk_hai = Monstruo { salud: 40 };

    atacar_con_espada(&radagast, &mut uruk_hai);
    atacar_con_flecha(&aragorn, &mut uruk_hai, 8);
    atacar_con_bola_de_fuego(&radagast, &mut uruk_hai, 8);
}
Atacaste con la espada. Tu oponente tiene ahora 30 de salud. Ahora tienes Mago { salud: 60 }
Atacaste con el arco. Tu oponente tiene ahora 20 de salud. Ahora tienes Cazador { salud: 80 }
¡Levantas las manos y conjuras una bola de fuego! Tu oponente tiene ahora 0 de salud. Ahora tienes: Mago { salud: 60 }

Se puede ver que hay varias formas de obtener el mismo resultado cuando se usan rasgos. Todo depende de lo que tenga más sentido en el programa que se está escribiendo.

A continuación, se echa un vistazo a cómo implementar algunos de los rasgos más usados en Rust.

El rasgo From

From es un rasgo muy útil que ya se ha usado. Con From se puede crear una String a partir de una &str, pero se puede usar para convertir cualquier tipo en otro. Por ejemplo, Vec utiliza From para lo siguiente:

From<&'_ [T]>
From<&'_ mut [T]>
From<&'_ str>
From<&'a Vec<T>>
From<[T; N]>
From<BinaryHeap<T>>
From<Box<[T]>>
From<CString>
From<Cow<'a, [T]>>
From<String>
From<Vec<NonZeroU8>>
From<Vec<T>>
From<VecDeque<T>>

Implementa un montón de Vec::from() que no se han usado aún en este manual. Se pueden usar algunos para ver qué pasa.

use std::fmt::Display; // se usará Display para crear una función genérica y mostrar el resultado

fn print_vec<T: Display>(input: &Vec<T>) { // Toma cualquier Vec<T> cuyo tipo T que tenga Display
    for item in input {
        print!("{} ", item);
    }
    println!();
}

fn main() {

    let array_vec = Vec::from([8, 9, 10]); // "from" un array
    print_vec(&array_vec);

    let str_vec = Vec::from("¿Qué clase de vector seré?"); // Array "from" un &str
    print_vec(&str_vec);

    let string_vec = Vec::from("Un String ¿Qué clase de vector será?".to_string()); // También "from" un String
    print_vec(&string_vec);
}

Se imprime lo siguiente:

8 9 10
87 104 97 116 32 107 105 110 100 32 111 102 32 118 101 99 32 119 105 108 108 32 73 32 98 101 63
87 104 97 116 32 107 105 110 100 32 111 102 32 118 101 99 32 119 105 108 108 32 97 32 83 116 114 105 110 103 32 98 101 63

Si se observa el tipo, los segundo y tercer vector son de tipo Vec<u8>, lo que significa que contiene los bytes de &str y String. Se ve así que From es muy flexible y se usa mucho.

El siguiente ejemplo implemente From en tipos desarrollados en el código. Se construyen dos structs y luego implementan From para una de ellas. Una será una Ciudad y la otra un Pais. Se quiere que sea posible hacer esto: let country_nombre = Pais::from(vector_de_ciudades).

Queda así el código:

#[derive(Debug)] // Para poder imprimir la Ciudad
struct Ciudad {
    nombre: String,
    poblacion: u32,
}

impl Ciudad {
    fn new(nombre: &str, poblacion: u32) -> Self { // función new
        Self {
            nombre: nombre.to_string(),
            poblacion,
        }
    }
}
#[derive(Debug)] // Pais necesita imprimirse
struct Pais {
    ciudades: Vec<Ciudad>, // Las ciudades van aquí
}

impl From<Vec<Ciudad>> for Pais { // Note: no se crea From<Ciudad>, se crea
    // From<Vec<Ciudad>>. Es decir, sobre un tipo, Vec, que no se ha creado en el código
    fn from(ciudades: Vec<Ciudad>) -> Self {
        Self { ciudades }
    }
}

impl Pais {
    fn imprimir_ciudades(&self) { // función para imprimir las ciudades del Pais
        for ciudad in &self.ciudades {
            // & porque Vec<Ciudad> no es Copy
            println!("{:?} tiene una población de {:?}.", ciudad.nombre, ciudad.poblacion);
        }
    }
}

fn main() {
    let helsinki = Ciudad::new("Helsinki", 631_695);
    let turku = Ciudad::new("Turku", 186_756);

    let finland_ciudades = vec![helsinki, turku]; // Esto es  el Vec<Ciudad>
    let finland = Pais::from(finland_ciudades); // En este punto se usa From

    finland.imprimir_ciudades();
}

Que imprime:

"Helsinki" tiene una población de 631695.
"Turku" tiene una población de 186756.

Se observa que es fácil implementar From para tipos que no se han creado por el desarrollador, por ejemplo, Vec, i32 o cualquier otro.

A continuación, se muestra otro ejemplo en el que se crea un vector que contiene dos vectores. El primer vector contiene números pares y el segundo, impares. Con From se puede tomar un vector de i32 y convertirlo en Vec<Vec<i32>>: un vector que contiene vectores de i32.

use std::convert::From;

struct ParImparVec(Vec<Vec<i32>>);

impl From<Vec<i32>> for ParImparVec {
    fn from(input: Vec<i32>) -> Self {
        let mut par_impar_vec: Vec<Vec<i32>> = vec![vec![], vec![]]; // Un vec con dos vecs vacíos dentro
        // se rellena para después retornarlo 
        for item in input {
            if item % 2 == 0 {
                par_impar_vec[0].push(item);
            } else {
                par_impar_vec[1].push(item);
            }
        }
        Self(par_impar_vec) // una vez relleno se devuelve como Self (Self = ParImparVec)
    }
}

fn main() {
    let varios_numero = vec![8, 7, -1, 3, 222, 9787, -47, 77, 0, 55, 7, 8];
    let new_vec = ParImparVec::from(varios_numero);

    println!("Números pares: {:?}\nNúmeros impares: {:?}", new_vec.0[0], new_vec.0[1]);
}

Lo anterior imprime:

Números pares: [8, 222, 0, 8]
Números impares: [7, -1, 3, 9787, -47, 77, 55, 7]

Un tipo como ParImparVec es mejor definirlo como genérico T para poder usarlo en muchos tipos de números. Se deja esta idea como práctica.

Paso de parámetros String y &str a funciones

En ocasiones se necesita que una función pueda recibir tanto String como &str en el mismo parámetro. Esto se puede conseguir con genéricos y con el rasgo AsRef. AsRef se utiliza para pasar una referencia de un tipo a otro tipo. Si se observa la documentación de String, se ve que implementa AsRef para muchos tipos:

https://doc.rust-lang.org/std/string/struct.String.html

Estas son algunas de las declaraciones de función:

AsRef<str>:

#![allow(unused)]
fn main() {
// 🚧
impl AsRef<str> for String

fn as_ref(&self) -> &str
}

AsRef<[u8]>:

#![allow(unused)]
fn main() {
// 🚧
impl AsRef<[u8]> for String

fn as_ref(&self) -> &[u8]
}

AsRef<OsStr>:

#![allow(unused)]
fn main() {
// 🚧
impl AsRef<OsStr> for String

fn as_ref(&self) -> &OsStr
}

Se observa que implementa la función as_ref que recibe como parámetro &self y devuelve una referencia a otro tipo. Esto permite que si se dispone de un tipo genérico T, se pueda indicar que ese tipo necesita AsRef<str>. Así, será posible que se pase como un &str o un String.

El código siguiente implementa la función genérica, pero da error:

fn imprimelo<T>(input: T) {
    println!("{}", input) // ⚠️
}

fn main() {
    imprimelo("Por favor, imprímeme");
}

Rust dice error[E0277]: T doesn't implement std::fmt::Display. Se necesita que lo implemente:

use std::fmt::Display;

fn imprimelo<T: Display>(input: T) {
    println!("{}", input)
}

fn main() {
    imprimelo("Por favor, imprímeme");
}

Ahora funciona e imprime Por favor, imprímeme. Eso es mejor, pero T puede ser muchas cosas aún. Puede ser i8, f32 o cualquier cosa que tenga Display. Si se añade ahora AsRef<str>, T necesita ambos rasgos.

use std::fmt::Display;

fn imprimelo<T: AsRef<str> + Display>(input: T) {
    println!("{}", input)
}

fn main() {
    imprimelo("Por favor, imprímeme");
    imprimelo("También, por favor, imprímeme".to_string());
    // imprimelo(7); <- Esto no compilará
}

Ahora no dejará pasar tipos como i8.

No se puede olvidar que se puede utilizar where ara escribir de forma más compacta la declaración de la función cuando esta se hace muy larga. Si se añadiera Debug la línea quedaría muy larga, fn imprimelo<T: AsRef<str> + Display + Debug>(input: T), así que se puede escribir de la siguiente forma.

use std::fmt::{Debug, Display}; // añade Debug

fn imprimelo<T>(input: T) // Ahora la línea es más fácil de leer
where
    T: AsRef<str> + Debug + Display, // y también se leen bien los rasgos del tipo
{
    println!("{}", input)
}

fn main() {
    imprimelo("Por favor, imprímeme");
    imprimelo("También, por favor, imprímeme".to_string());
}
1

N.T.: Este concepto de Rust se puede traducir como rasgo. Está relacionado con la programación orientada a aspectos. Los rasgos de Rust describen determinados aspectos de los tipos, funciones o variables que los implementen.

Encadenando Métodos

Rust es un lenguaje de programación de sistemas como C y C++ y su código se puede escribir mediante órdenes (comandos) separados en líneas separadas, pero también es un lenguaje que permite un estilo de programación funcional. Ambos estilos de programación son correctos, pero el estilo funcional suele ser más sintético. Produce código más conciso y breve. A continuación se muestra un ejemplo del estilo no funcional (denominado estilo imperativo) en el que se construye un Vec que contiene números de 1 a 10:

fn main() {
    let mut new_vec = Vec::new();
    let mut counter = 1;

    while counter < 11 {
        new_vec.push(counter);
        counter += 1;
    }

    println!("{:?}", new_vec);
}

Esto imprime [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].

A continuación, se muestra cómo se codificaría mediante un estilo funcional:

fn main() {
    let new_vec = (1..=10).collect::<Vec<i32>>();
    // También se podría haber escrito así
    // let new_vec: Vec<i32> = (1..=10).collect();
    println!("{:?}", new_vec);
}

.collect() crea colecciones de muchos tipos posibles, por eso hay que indicar el tipo.

Con el estilo funcional de programación se pueden encadenar métodos. Encadenar métodos significa que se pueden unir para formar una única sentencia. A continuación se muestra un ejemplo de muchos métodos unidos:

fn main() {
    let my_vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let new_vec = my_vec.into_iter().skip(3).take(4).collect::<Vec<i32>>();

    println!("{:?}", new_vec);
}

El ejemplo anterior crea un Vec que contiene [3, 4, 5, 6]. La sentencia contiene mucha información en una sola línea. Si se separan en diferentes líneas para hacerlo más fácil de leer (y se añaden comentarios explicativos):

fn main() {
    let my_vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let new_vec = my_vec
        .into_iter() // Itera a través de todos los elementos.
                     // Esta función devuelve los elementos, no referencias a ellos.
        .skip(3) // se salta tres elementos, en este caso: 0, 1 y 2
        .take(4) // obtiene los cuatro primeros que quedan: 3, 4, 5 y 6
        .collect::<Vec<i32>>(); // los coloca en un nuevo Vec<i32>

    println!("{:?}", new_vec);
}

Se puede utilizar este estilo funcional de forma amplia cuando se estudien los closures (cierres) y los iteradores. Por ello, estos conceptos se ven a continuación.

Iteradores

Un iterador es una construcción del lenguaje Rust que recupera los elementos contenidos en una colección uno a uno. En realidad, ya se han utilizado de forma frecuente los iteradores en los capítulos anteriores: el bucle for entrega un iterador. Para obtener un iterador para otros usos, es necesario indicar el tipo que se requiere:

  • .iter() para iterar a través de referencias a los elementos.
  • .iter_mut() para iterar mediante referencias modificables (mutables).
  • .into_iter() para obtener un iterador sobre valores, no referencias.

Un bucle for es solo un iterador que es propietario de sus valores. Es por eso que se puede hacer modificable y se pueden cambiar los valores cuando se utiliza.

Los iteradores se pueden usar como sigue:

fn main() {
    let vector1 = vec![1, 2, 3]; // Se usará .iter() y .into_iter() sobre este vector
    let vector1_a = vector1.iter().map(|x| x + 1).collect::<Vec<i32>>();
    let vector1_b = vector1.into_iter().map(|x| x * 10).collect::<Vec<i32>>();

    let mut vector2 = vec![10, 20, 30]; // Se usará .iter_mut() sobre este otro vector
    vector2.iter_mut().for_each(|x| *x +=100);

    println!("{:?}", vector1_a);
    println!("{:?}", vector2);
    println!("{:?}", vector1_b);
}

Que imprime:

[2, 3, 4]
[110, 120, 130]
[10, 20, 30]

En los primeros dos casos se ha utilizado el método map() que permite ejecutar algún código sobre cada elemento antes de pasarlo a la siguiente función. En el último caso se hay utilizado el método for_each(). Este método ejecuta el código que contiene sobre cada elemento que recibe. El uso de .iter_mut() junto con for_each() equivalen a un bucle for. Dentro de cada método se puede dar un nombre a cada elemento (en el ejemplo, se ha utilizado x) y se ha modificado. A este tipo de funciones se las denomina closures y se ven en la siguiente sección.

Si se revisa el código anterior paso a paso:

  1. En primer lugar se usa .iter() en el vector1 para obtener unas referencias a los elementos. Después se ha usado map() para sumar 1 y devolver nuevos valores en un nuevo vector. El vector1 aún está disponible ya que solo se han usado referencias a sus elementos: se ha tomado ningún valor. Al final, resulta necesario utilizar collect() para crear un nuevo vector con los nuevos elementos que map() ha ido generando.

  2. En segundo lugar, se usa .into_iter() para obtener un iterador sobre los valores (directamente, sin referencias). Esto destruye vector1. Así que después de construir el nuevo vector1_b, no se puede volver a usar vector1.

  3. Por último, se usa .iter_mut() sobre el vector2. Es mutable, por lo que no se necesita utilizar .collect() para crear un nuevo Vec. En vez de ello, se modifican difectamente los valores del Vec con referencias modificables. Por eso, vector2 sigue existiendo con los valores modificados. Por ello, se utiliza for_each en lugar de map().

Cómo funciona un iterador

Un iterador funciona mediante el uso de un método denominado .next(), que devuelve un Option. Cuando se usa un iterador, Rust llama a la función .next() una y otra vez. Sigue haciéndolo siempre que obtenga un elemento de tipo Some. Se detiene cuando recupera un elemento de tipo None.

Se recordará por un apartado anterior que existe la macro assert_eq!. En la documentación de las librerías de Rust se usa mucho. A continuación, se muestra un ejemplo que la usa para mostrar cómo funciona un iterador:

fn main() {
    let my_vec = vec!['a', 'b', '거', '柳']; // Es un Vector "normal"

    let mut my_vec_iter = my_vec.iter(); // Esta variable contiene un objeto iterador
                                         // que aún no se ha usado

    assert_eq!(my_vec_iter.next(), Some(&'a'));  // Recupera el primer elemento con next()
    assert_eq!(my_vec_iter.next(), Some(&'b'));  // Recupera el siguiente
    assert_eq!(my_vec_iter.next(), Some(&'거')); // Y el siguiente
    assert_eq!(my_vec_iter.next(), Some(&'柳')); // Y el siguiente
    assert_eq!(my_vec_iter.next(), None);        // Ya no queda ninguno, recupera None
    assert_eq!(my_vec_iter.next(), None);        // Si se sigue llamando a .next() se sigue obteniendo None
}

Se puede implementar el rasgo Iterator en cualquier struct o enum sin mucha dificultad. En primer lugar, se crea una biblioteca de libros como base para demostrar cómo se puede hacer.

#[derive(Debug)] // Se quiere usar print con {:?}
struct Library {
    library_type: LibraryType, // Este es el enum
    books: Vec<String>, // lista de libros
}

#[derive(Debug)]
enum LibraryType { // las bibliotecas pueden ser de la ciudad o del país
    City,
    Country,
}

impl Library {
    fn add_book(&mut self, book: &str) { // Se usa add_book para añadir nuevos libros
        self.books.push(book.to_string()); // Se toma un &str y lo convierte en String, para añadirlo después a Vec
    }

    fn new() -> Self { // Esta función crea una nueva biblioteca
        Self {
            library_type: LibraryType::City, // La mayoría son de ciudades, por lo qu ese usa como valor por defecto
                                             // la mayor parte de las veces.
            books: Vec::new(),
        }
    }
}

fn main() {
    let mut my_library = Library::new(); // crea una nueva biblioteca
    my_library.add_book("The Doom of the Darksword"); // se añaden algunos libros
    my_library.add_book("Demian - die Geschichte einer Jugend");
    my_library.add_book("구운몽");
    my_library.add_book("吾輩は猫である");

    println!("{:?}", my_library.books); // se puede imprimir la lista de libros
}

Ahora se puede implementar Iterator para la biblioteca para que se pueda utilizar en un bucle for. Con la implementación anterior, antes de implementar Iterator, el bucle for no funciona.

#![allow(unused)]
fn main() {
for item in my_library {
    println!("{}", item); // ⚠️
}
}

Devuelve lo siguente:

error[E0277]: `Library` is not an iterator
  --> src\main.rs:47:16
   |
47 |    for item in my_library {
   |                ^^^^^^^^^^ `Library` is not an iterator
   |
   = help: the trait `std::iter::Iterator` is not implemented for `Library`
   = note: required by `std::iter::IntoIterator::into_iter`

Pero se puede hacer que la biblioteca implemente un iterador con impl Iterator for Library. La información sobre el rasgo Iterator se encuentra en la documentación de la librería estándar: https://doc.rust-lang.org/std/iter/trait.Iterator.html

En la parte superior izquierda de esta página indica Associated Types: Item y Required Methods: next. Un tipo asociado es otro tipo que va junto con este. En este caso, el tipo asociado será String ya que se quiere que el iterador a implementar devuelva cadenas de texto.

En la página de la documentación hay un ejemplo:

// an iterator which alternates between Some and None
struct Alternate {
    state: i32,
}

impl Iterator for Alternate {
    type Item = i32;

    fn next(&mut self) -> Option<i32> {
        let val = self.state;
        self.state = self.state + 1;

        // if it's even, Some(i32), else None
        if val % 2 == 0 {
            Some(val)
        } else {
            None
        }
    }
}

fn main() {}

Se puede observa que bajo impl Iterator for Alternate dice type Item = i32. Este es el tipo asociado para este ejemplo. En el caso del ejemplo de la Librería, el iterador devolverá libros de la lista que es de tipo Vec<String>. Cuando se llame a .next() devolverá un String de los incluidos en el vector. Por lo tanto, el tipo asociado será type Item = String.

Para implementar Iterator se debe escribir la función fn next(). En ella se indicará lo que debe hacer el iterador. Para este caso, se quiere que entregue primero los últimos libros del vector. En el ejemplo que sigue, se usa match para ver el tipo de elemento que ha devuelvo .pop() y seleccionar qué debe devolver next(). Además, se imprimirá " encontrado" para cada elemento:

#[derive(Debug, Clone)]
struct Library {
    library_type: LibraryType,
    books: Vec<String>,
}

#[derive(Debug, Clone)]
enum LibraryType {
    City,
    Country,
}

impl Library {
    fn add_book(&mut self, book: &str) {
        self.books.push(book.to_string());
    }

    fn new() -> Self {
        Self {
            library_type: LibraryType::City,
            // la mayor parte de las veces, valor por defecto
            books: Vec::new(),
        }
    }
}

impl Iterator for Library {
    type Item = String;

    fn next(&mut self) -> Option<String> {
        match self.books.pop() {
            Some(book) => Some(book + " encontrado"), // Rust permite String + &str
            None => None,
        }
    }
}

fn main() {
    let mut my_library = Library::new();
    my_library.add_book("The Doom of the Darksword");
    my_library.add_book("Demian - die Geschichte einer Jugend");
    my_library.add_book("구운몽");
    my_library.add_book("吾輩は猫である");

    for item in my_library.clone() { // ahora sí funciona el bucle for. Se le pasa un clon de la biblioteca para que no se destruya
        println!("{}", item);
    }
}

Esto imprime:

吾輩は猫であるencontrado
구운몽 encontrado
Demian - die Geschichte einer Jugend encontrado
The Doom of the Darksword encontrado

Closures - Cierres

Los cierres o closures (en inglés) son una especie de funciones rápidas que no necesitan un nombre. En ocasiones se les denomina funciones lambdas. Son fáciles de encontrar en el código debido a que utilizan || en lugar de (). Son muy habituales en Rust y una vez se aprenden a utilizar uno se pregunta como ha podido vivir sin ellos.

Se puede asociar un closure a una variable y en ese caso funciona y parece exactamente como una función:

fn main() {
    let my_closure = || println!("Esto es un cierre");
    my_closure();
}

Este closure no recibe nada coomo parámetro || e imprime el mensaje Esto es un cierre.

Entre los símbolos || se pueden añadir variables de entrada y tipos, como se hace en las funciones dentro de ():

fn main() {
    let my_closure = |x: i32| println!("{}", x);

    my_closure(5);
    my_closure(5+5);
}

Imprime:

5
10

Cuando un cierre se hace más complejo, se puede escribir con un bloque de código que puede ser tan largo como se requiera:

fn main() {
    let my_closure = || {
        let number = 7;
        let other_number = 10;
        println!("Los dos números son {} y {}.", number, other_number);
          // Este cierre puede ser tan largo como se necesite, como sucede con las funciones.
    };

    my_closure();
}

Pero los cierres son especiales porque pueden guardarse valores de variables que se encuentren fuera del ellos incluso aunque no reciban parámetros (es decir, incluso habiendo escrito ||). Por ello, se puede escribir:

fn main() {
    let number_one = 6;
    let number_two = 10;

    let my_closure = || println!("{}", number_one + number_two);
    my_closure();
}

Este código imprime 16. No ha sido necesario añadir parámetros a || porque se pueden usar las variables dentro del código del cierre y sumarlas ahí.

Por cierto, por eso se llaman closures. Porque pueden tomar unas variables y encerrarlas dentro. Esto significa que si se quiere hablar con precisión:

  • Una || que no encierra ninguna variable exterior en su interior es una función anónima. No es, estrictamente, un closure.
  • Una || que sí encierra una variable exterior en su interior sí es un cierre.

Sin embargo, esta precisión no se suele hacer en el habla coloquial. Así que en este texto simplemente se llaman cierres a todos los ||.

¿Por qué es bueno conocer la diferencia? Porque las funciones anónimas generan realmente el mismo código que el de las funciones con nombre. Aunque pueda parecer que el código máquina de una función anónima será más complejo, no lo es. Es igual (y, por lo tanto, igual de rápido) que el código de una función habitual.

Para qué pueden servir los cierres. Por ejemplo:

fn main() {
    let number_one = 6;
    let number_two = 10;

    let my_closure = |x: i32| println!("{}", number_one + number_two + x);
    my_closure(5);
}

Este cierre guarda en su interior a dos variables number_one y number_two. Pero también recibe como parámetro una nueva variable x y cuando se ejecuta, se le pasa 5 al parámetro x. Suma los tres valores y, en este caso, devuelve 21.

Normalmente, se usan los cierres para pasarlos como parámetros a determinados métodos. En la última sección se usaron cierres con .map()y .for_each(). En esa sección, se escribió |x| para obtener el siguiente elemento en un iterador; eso era un cierre.

Otro ejemplo: el método unwrap_or que se usa para asignar un valor cuando no funciona unwrap. Igual que se puede escribir la siguiente función let fourth = my_vec.get(3).unwrap_or(&0);, existe también el método unwrap_or_else que recibe un cierre como parámetro. Por ejemplo:

fn main() {
    let my_vec = vec![8, 9, 10];

    let fourth = my_vec.get(3).unwrap_or_else(|| { // Intenta "desenvolver". Si no funciona:
        if my_vec.get(0).is_some() {               // observa si el vector contiene algo en el índice [0]
            &my_vec[0]                             // Utiliza ese valor si existe algo.
        } else {
            &0 // en otro caso, usa el &0
        }
    });

    println!("{}", fourth);
}

Por supuesto, los cierres pueden ser muy simples. Se puede escribir, simplemente let fourth = my_vec.get(3).unwrap_or_else(|| &0);. No siempre se necesita usar {} y escribir código complejo solo porque es un cierre. Solo con escribir || el compilador conoce que hay un cierre.

Quizás el uso más habitual de un cierre sea en la función .map(). A continuación se muestra otro ejemplo:

fn main() {
    let num_vec = vec![2, 4, 6];

    let double_vec = num_vec        // usa el vector
        .iter()                     // para recorrerlo (iterar)
        .map(|number| number * 2)   // para cada elemento, lo multiplica por 2
        .collect::<Vec<i32>>();     // y crea un nuevo vector a partir de ello
    println!("{:?}", double_vec);
}

Otro buen ejemplo es su uso con .for_each() después de .enumerate(). El método .enumerate() devuelve un iterador con el número de índice del elemento y el propio elemento. Por ejemplo, de [10, 9, 8 ] se obtiene (0, 10), (1, 9), (2, 8). Con los tipos (usize, i32).

fn main() {
    let num_vec = vec![10, 9, 8];

    num_vec
        .iter()      // itera sobre num_vec
        .enumerate() // obtiene pares de (índice, valor)
        .for_each(|(index, number)| println!("El índice {} contiene el valor {}", index, number)); // imprime para cada elemento
}

Este código imprime:

El índice 0 contiene el valor 10
El índice 1 contiene el valor 9
El índice 2 contiene el valor 8

En este caso se usa for_each en lugar de map. map es para ejecutar algo en cada elemento y pasarlo al siguiente paso (es un iterador en sí mismo) y actua de forma perezosa, es decir, si no hay nadie a quien pasarle el valor, no hace nada. for_each realiza la operación cuando recibe cada elemento. Por eso en caso de map hace falta usar un método como collect para obtener un resultado.

Esta es una característica muy interesante de los iteradores. Si se intenta ejecutar un map sin utilizar un método como collect, el compilador observará que ese código no hace nada. No fallará, pero lo indicará:

fn main() {
    let num_vec = vec![10, 9, 8];

    num_vec
        .iter()
        .enumerate()
        .map(|(index, number)| println!("El índice {} contiene el valor {}", index, number));

}

Mostrará:

warning: unused `std::iter::Map` that must be used
 --> src\main.rs:4:5
  |
4 | /     num_vec
5 | |         .iter()
6 | |         .enumerate()
7 | |         .map(|(index, number)| println!("Index number {} has number {}", index, number));
  | |_________________________________________________________________________________________^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

Se trata de un aviso warning, no es un error. El programa funciona correctamente. Pero no suma nada. Si se observa el tipo del resultado de cada fila, se entenderá:

  • let num_vec = vec![10, 9, 8]; En este momento se tiene un Vec<i32>.
  • .iter() El valor de retorno de esta función es un Iter<i32>. Es decir, un iterador sobre elementos de tipo i32.
  • .enumerate() Ahora es de tipo enumerado Enumerate<Iter<i32>>. Es decir, un Enumerate sobre un Iter de i32s.
  • .map() Y ahora el tipo es Map<Enumerate<Iter<i32>>>. Es decir, un Map sobre un Enumerate sobre un Iter de i32s.

Hasta el momento, todo lo que han hecho las funciones es ir construyendo una estructura compleja que está lista para usarse, solo que hay que decirle de algún modo que lo haga. Rust hace todo esto porque necesita ejecutarse rápido. No quiere hacer:

  • itera sobre todos los i32 en el vector
  • luego enumera todos los i32 del iterador
  • luego ejecuta la función sobre todos los i32

Rust realiza un único "bucle" solo cuando lo necesita. En el momento que se ejecuta una función como .collect::<Vec<i32>>() Rust sabe que se le está pidiendo construir un vector y comienza a pedir los diferentes valores. Esto es lo que significa que los iteradores son perezosos y no hacen nada a no ser que se consuman.

Se pueden crear cosas muy complejas como HashMap mediante el uso de .collect(). A continuación se muestra un ejemplo sobre como construir un HashMap a partir de dos vectores. En primer lugar, se construyen ambos vectores. Después se usará into_iter() para obtener un interador sobre los valores. Posteriormente se usará .zip() para unir ambos iteradores como un una cremallera, emparejando cada elemento de un iterador con el obtenido del otro. Por último, se utilizará .collect() para crear el HashMap:

use std::collections::HashMap;

fn main() {
    let some_numbers = vec![0, 1, 2, 3, 4, 5]; // un Vec<i32>
    let some_words = vec!["cero", "uno", "dos", "tres", "cuatro", "cinco"]; // un Vec<&str>

    let number_word_hashmap = some_numbers
        .into_iter()                 // obtiene un iter
        .zip(some_words.into_iter()) // se combinan con .zip() dos iter.
        .collect::<HashMap<_, _>>();

    println!("Para la clave {} se obtiene {}.", 2, number_word_hashmap.get(&2).unwrap());
}

Que imprime:

Para la clave 2 se obtiene dos.

Se observa que se escribió <HashMap<_, _>>, que es información suficiente para que Rust sepa decidir el tipo del objeto como <HashMap<i32, &str>>. También habría sido posible haber escrito .collect::<HashMap<i32, &str>>();. E incluso así:

use std::collections::HashMap;

fn main() {
    let some_numbers = vec![0, 1, 2, 3, 4, 5]; // un Vec<i32>
    let some_words = vec!["cero", "uno", "dos", "tres", "cuatro", "cinco"]; // un Vec<&str>
    let number_word_hashmap: HashMap<_, _> = some_numbers  // Puesto que se indica el tipo aquí...
        .into_iter()
        .zip(some_words.into_iter())
        .collect(); // no es necesario indicarlo aquí
}

Hay otro método que es como .enumerate(), pero para el tipo char: .char_indices(). Se usa de la misma forma. Si se supone que se tiene una cadena que está formada por números de tres dígitos:

fn main() {
    let numbers_together = "140399923481800622623218009598281";

    for (index, number) in numbers_together.char_indices() {
        match (index % 3, number) {
            (0..=1, number) => print!("{}", number), // solo imprime el número si hay resto
            _ => print!("{}\t", number), // en otro caso, imprime el número y un tabulador
        }
    }
}

Esto imprime: 140 399 923 481 800 622 623 218 009 598 281.

|_| en un cierre (closure)

En ocasiones se puede ver |_| en un cierre. Esto significa que el cierre (closure) necesita un argumento (como podría ser una variable x), pero no se va a usar. Así que esta expresión en los parámetros de un cierre significa: vale, el cierre recibe un parámetro, pero no le doy un nombre porque no lo uso".

A continuación, se muestra un ejemplo que da error por no expresar el parámetro:

fn main() {
    let my_vec = vec![8, 9, 10];

    println!("{:?}", my_vec.iter().for_each(|| println!("No usamos la variable que se necesita"))); // ⚠️
}

Rust da un error que dice:

error[E0593]: closure ¿Está expected to take 1 argument, but it takes 0 arguments
 --> src/main.rs:4:36
  |
4 |     println!("{:?}", my_vec.iter().for_each(|| println!("No usamos la variable que se necesita"))); // ⚠️
  |                                    ^^^^^^^^ -- takes 0 arguments
  |                                    |
  |                                    expected closure that takes 1 argument

El propio compilador da alguna pista:

help: consider changing the closure to take and ignore the expected argument
  |
4 |     println!("{:?}", my_vec.iter().for_each(|_| println!("No usamos la variable que se necesita"))); // ⚠️
  |                                             ~~~

Es decir, que se debe pasar el argumento que necesita cualquier cierre que se pase como parámetro a la función for_each.

Si se cambia de este modo, funcionará:

fn main() {
    let my_vec = vec![8, 9, 10];

    println!("{:?}", my_vec.iter().for_each(|_| println!("No usamos la variable que se necesita")));
}

Métodos útiles para su uso con cierres e iteradores

Rust es un lenguaje muy divertido cuando se dominan los cierres. Con ellos se pueden encadenar métodos para hacer mucho con poco código. A continuación, se muestrans algunos métodos que se utilizan con cierres que aún no se habían visto:

.filter(): a partir de un iterador, permite conservar los elementos que se deseen. El siguiente ejemplo filtra los meses del año:

fn main() {
    let meses = vec!["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];

    let filtered_meses = meses
        .into_iter()                         // crea un iterador
        .filter(|month| month.len() <= 5)     // Solo los meses con cinco o menos caracters (byte)
                                             // En este caso, todos los caracteres son de un byte, por eso funciona usar .len()
        .filter(|month| month.contains("u")) // Se seleccionan solo los meses que contengan la letra u
        .collect::<Vec<&str>>();

    println!("{:?}", filtered_meses);
}

Esto imprime: ["Junio", "Julio"].

.filter_map(): este método realiza de una vez la función de .filter() y .map(). El cierre que se pasa como parámetro,debe devolver un Option<T>. El método filter_map() extraerá T para aquellos elementos que tengan valor (que sean de tipo Some<T>) y desechará los que sean de tipo None. Por ejemplo, el uso de .filter_map() con un vector como vec![Some(2), None, Some(3)] devuelve [2, 3].

A continuación se muestra un ejemplo con un struct de una Empresa. Cada empresa tiene un campo nombre de tipo String, y un campo ceo para describir a su responsable. El ceo puede haber dimitido, por lo que es de tipo Option<String> En el ejemplo, se filtran las empresas para mostrar el valor de los CEO.

struct Empresa {
    nombre: String,
    ceo: Option<String>,
}

impl Empresa {
    fn new(nombre: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            ceo => Some(ceo.to_string()),
        }; // Se crea el valor del campo ceo y debajo se crea el elemento struct
        Self {
            nombre: nombre.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone() // Devuelve un clone del CEO (struct no es tipo Copy)
    }
}

fn main() {
    let empresa_vec = vec![
        Empresa::new("Umbrella Corporation", "Unknown"),
        Empresa::new("Ovintiv", "Doug Suttles"),
        Empresa::new("The Red-Headed League", ""),
        Empresa::new("Stark Enterprises", ""),
    ];

    let all_the_ceos = empresa_vec
        .into_iter()
        .filter_map(|empresa| empresa.get_ceo()) // filter_map recibe Option<T> que es lo que necesita
        .collect::<Vec<String>>();

    println!("{:?}", all_the_ceos);
}

Esto imprime: ["Unknown", "Doug Suttles"]

Para facilitar el uso de .filter_map() con valores Result<T, E>, este último tiene el método .ok() que lo convierte en Option<T> usando None para los casos de error.

En el siguiente ejemplo se utiliza .parse() para convertir la entrada de usuario en números f32. En el resultado se registran los errores que pueda haber en la entrada del usuario, al usar .filter_map() junto con .ok() se descartan los valores erróneos.

fn main() {
    let user_input = vec!["8.9", "Nine point nine five", "8.0", "7.6", "eleventy-twelve"];

    let actual_numbers = user_input
        .into_iter()
        .filter_map(|input| input.parse::<f32>().ok())
        .collect::<Vec<f32>>();

    println!("{:?}", actual_numbers);
}

Lo anterior imprime: [8.9, 8.0, 7.6]

En sentido contrario de .ok(), los Option<T> disponen de los métodos .ok_or() y .ok_or_else(). Estas funcones convierten un Option<T> en Resutl<T, E>. La función .ok_or() devuelve un Ok o un Err, por lo que necesita que se le indique de qué tipo debe ser este error. Esto se debe a que los casos None de Option no disponen de información adicionales.

En estas funciones, .ok_or() y .ok_or_else(), el parámetro closure se utiliza para obtener el valor para los casos en los que el Option sea None.

En el siguiente ejemplo, se toma el Option del struct de Empresa para convertirlo en un Result. Para el manejo de errores a largo plazo, es bueno que la aplicación tenga sus propios tipos de error. En este caso solo se incluye un mensaje de error, por lo que el tipo del resultado es Result<String, &str>.

// El código es idéntico, salvo en la función main()
struct Empresa {
    nombre: String,
    ceo: Option<String>,
}

impl Empresa {
    fn new(nombre: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            ceo => Some(ceo.to_string()),
        };
        Self {
            nombre: nombre.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone()
    }
}

fn main() {
    let empresa_vec = vec![
        Empresa::new("Umbrella Corporation", "Unknown"),
        Empresa::new("Ovintiv", "Doug Suttles"),
        Empresa::new("The Red-Headed League", ""),
        Empresa::new("Stark Enterprises", ""),
    ];

    let mut results_vec = vec![]; // Para guardar los valores resultantes

    empresa_vec
        .iter()
        .for_each(|empresa| results_vec.push(empresa.get_ceo().ok_or("No CEO found")));

    for item in results_vec {
        println!("{:?}", item);
    }
}

El resultado de este código es el siguiente:

Ok("Unknown")
Ok("Doug Suttles")
Err("No CEO found")
Err("No CEO found")

En este caso, se dispone de las cuatro entradas del usuario y se indica cuáles son erróneas.

A continuación se usa .ok_or_else() para que se pueda pasar como parámetro un cierre y obtener un mensaje de error mejor incluyendo el nombre de la empresa.

// El código es idéntico, salvo en la función main()
struct Empresa {
    nombre: String,
    ceo: Option<String>,
}

impl Empresa {
    fn new(nombre: &str, ceo: &str) -> Self {
        let ceo = match ceo {
            "" => None,
            ceo => Some(ceo.to_string()),
        };
        Self {
            nombre: nombre.to_string(),
            ceo,
        }
    }

    fn get_ceo(&self) -> Option<String> {
        self.ceo.clone()
    }
}

fn main() {
    let empresa_vec = vec![
        Empresa::new("Umbrella Corporation", "Unknown"),
        Empresa::new("Ovintiv", "Doug Suttles"),
        Empresa::new("The Red-Headed League", ""),
        Empresa::new("Stark Enterprises", ""),
    ];

    let mut results_vec = vec![]; // Para guardar los valores resultantes

    empresa_vec
        .iter()
        .for_each(|empresa| {
            results_vec.push(empresa.get_ceo().ok_or_else(|| {
                let mensaje_error = format!("No CEO found for {}", empresa.nombre);
                mensaje_error
            }))
        });

    for item in results_vec {
        println!("{:?}", item);
    }
}

El código anterior, imprime:

Ok("Unknown")
Ok("Doug Suttles")
Err("No CEO found for The Red-Headed League")
Err("No CEO found for Stark Enterprises")

.and_then(): es un método que toma como parámetro un Option y permite realizar una operación con su valor pasando el resultado al siguiente método que exista. El resultado también debe ser de tipo Option. Es una forma segura de obtener el contenido del elemento Some, realizar la operación deseada y encapsular el resultado de nuevo en un Option.

Un ejemplo sencillo se puede ver a partir del resultado de la operación .get() sobre un vector, puesto que devuelve un Option. En este caso, se obtiene un número, se realiza un cálculo y se vuelve a convertir a Option. Los valores None se pasan sin procesar, siguen siendo None.

fn main() {
    let new_vec = vec![8, 9, 0]; // solo un vec con números
    let number_to_add = 5;       // Se usa en el cálculo más adelante
    let mut empty_vec = vec![];  // El resultado va aquí


    for index in 0..5 {
        empty_vec.push(
            new_vec
               .get(index)
                .and_then(|number| Some(number + 1))
                .and_then(|number| Some(number + number_to_add))
        );
    }
    println!("{:?}", empty_vec);
}

Esto imprime: [Some(14), Some(15), Some(6), None, None]. Se observa que no se filtran los None.

.and(): es parecido a bool para Option. Se pueden encadenar muchos y si todos son Some el resultado será el del último. Si existe algún None, el resultado será None.

Se muestra en primer lugar un ejemplo con booleanos para ayudar a entender el funcionamiento de .and() en base al funcionamiento de &&(and). Un solo false lo convierte todo en false.

fn main() {
    let one = true;
    let two = false;
    let three = true;
    let four = true;

    println!("{}", one && three); // prints true
    println!("{}", one && two && three && four); // prints false
}

A continuación, se muestra el mismo tipo de ejemplo con la función .and(). Se puede imagina que se ejecutaron cinco operaciones y para cada una de ellas, si el resultado fue correcto se guardar en un Vec<Option<&str>> un valor Some("éxito"). Se repiten tres veces estas cinco operaciones. Después se usa .and() para comparar si en cada operación se obtubo resultado en todas las ocasiones.

fn main() {
    let first_try = vec![Some("éxito"), None, Some("éxito"), Some("éxito"), None];
    let second_try = vec![None, Some("éxito"), Some("éxito"), Some("éxito"), Some("éxito")];
    let third_try = vec![Some("éxito"), Some("éxito"), Some("éxito"), Some("éxito"), None];

    for i in 0..first_try.len() {
        println!("{:?}", first_try[i].and(second_try[i]).and(third_try[i]));
    }
}

El resultado es el siguiente:

None
None
Some("éxito")
Some("éxito")
None

La cadena de .and() de la primera operación en los tres intentos devuelve None ya que el segundo intento tiene None en la primera posición (índice cero). La segunda operación también es None debido a que el primer intento dio un resultado None. La tercera operación sí tiene éxito debido a que los tres intentos tiene un resultado distinto de None.

.any() y .all(). Se utilizan de forma sencilla en iteradores. Devuelven un bool en función del parámetro de entrada. En el siguiente ejemplo, se crea un vector muy grande, de unos 20.000 elementos, con toods los caracteres desde la 'a' a la '働'. Después, se comprueba si contiene un determinado carácter.

A continuación, se crea un vector más pequeño y se comprueba si es totalmente alfabético (con .is_alphabetic()). POr último, se comprueba si todos los caracteres son menores que el carácter coreano '행'.

Se debe observar que se usan referencias allí donde es necesario ya que es lo que entrega .iter().

fn in_char_vec(char_vec: &Vec<char>, check: char) {
    println!("¿Está {} dentro? {}", check, char_vec.iter().any(|&char| char == check));
}

fn main() {
    let char_vec = ('a'..'働').collect::<Vec<char>>();
    in_char_vec(&char_vec, 'i');
    in_char_vec(&char_vec, '뷁');
    in_char_vec(&char_vec, '鑿');

    let smaller_vec = ('A'..'z').collect::<Vec<char>>();
    println!("¿Es todo alfabético? {}", smaller_vec.iter().all(|&x| x.is_alphabetic()));
    println!("¿Es todo menor que el carácter 행? {}", smaller_vec.iter().all(|&x| x < '행'));
}

Lo anterior imprime:

¿Está i dentro? true
¿Está 뷁 dentro? false
¿Está 鑿 dentro? false
¿Es todo alfabético? false
¿Es todo menor que el carácter 행? true

Por cierto, .any() solo comprueba hasta que encuentra un elemento que coincida. Funciona en modo cortocircuito. No sigue comprobando si ya ha encontrado una coincidencia. Por este motivo, si se va a usar esta función .any() con un Vec, puede ser buena idea que los elementos que puedan coincidir estén lo más cerca del inicio del iterador. Por ejemplo, usando .rev() después de .iter() si pueden estar por el final. Por ejemplo:

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);
}

Que es un vector con 1000 números 6 seguidos de un 5. Si se quiere comprobar si contiene el número 5, lo óptimo sería obtener un iterador en sentido inverso con .rev().

En primer lugar, se puede revisar el funcionamiento esta función .rev()con el siguiente código:

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    let mut iterator = big_vec.iter().rev();
    println!("{:?}", iterator.next());
    println!("{:?}", iterator.next());
}

Que imprime:

Some(5)
Some(6)

Por lo tanto, lo eficiente, en este caso, para buscar un 5 sería buscarlo a través del iterador que comienza por el final:

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    println!("{:?}", big_vec.iter().rev().any(|&number| number == 5));
}

Así, solo necesita iterar a través del primer elemento y luego se para. Si no se usara, en este ejemplo se iteraría a través de los 1001 elementos. El siguiente código permite observarlo:

fn main() {
    let mut big_vec = vec![6; 1000];
    big_vec.push(5);

    let mut counter = 0; // Inicia la cuenta
    let mut big_iter = big_vec.into_iter(); // Crea un Iterator

    loop {
        counter +=1;
        if big_iter.next() == Some(5) { // Continua llamando a .next() hasta que obtiene un Some(5)
            break;
        }
    }
    println!("Final counter is: {}", counter);
}

Esto imprime Final counter is: 1001, por lo que queda claro que llamó a .next() 1001 veces antes de encontrar el 5.

.find() indica si un iterador contiene algo y .position() indica dónde está. .find() es diferente de .any() en que devuelve un Option que cotiene el valor buscado en un Some o None. .position(), por otra parte devuelve un Option con un Someque contiene el número de posición o None. En otras palabras:

  • .find(): intentará extraer el valor.
  • .position(): intentará indicar la posición en la que está el valor.

A continuación, se muestra un ejemplo simple:

fn main() {
    let num_vec = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100];

    println!("{:?}", num_vec.iter().find(|&number| number % 3 == 0)); // find takes a reference, so we give it &number
    println!("{:?}", num_vec.iter().find(|&number| number * 2 == 30));

    println!("{:?}", num_vec.iter().position(|&number| number % 3 == 0));
    println!("{:?}", num_vec.iter().position(|&number| number * 2 == 30));
}

Este código imprime:

Some(30) // Devuelve el primer valor que cumple la condición
None // No hay ningún número que multplicado por 2 == 30
Some(2) // Esta es la posición del elemneto
None

.cycle() puede crear un iterador infinito que nunca termina. Este tipo de iterador funciona muy bien con .zip() para crear un objeto nuevo Vec<(i32, &str)>.

fn main() {
    let even_odd = vec!["par", "impar"];
    let even_odd_vec = (0..6)
        .zip(even_odd.into_iter().cycle())
        .collect::<Vec<(i32, &str)>>();
    println!("{:?}", even_odd_vec);
}

Aunque .cycle() devuelve un iterador que no acaba nunca, en este caso, el iterador de .zip() solo se ejecuta seis veces antes de acabarse (cuando se agota el iterable de mayor longitud). La salida de este programa es:

[(0, "par"), (1, "impar"), (2, "par"), (3, "impar"), (4, "par"), (5, "impar")]

Algo parecido se puede consguir que no tiene fin. Si se escribe 0.. se crea un rango con un límite superior infinito y que nunca se completa. Es fácil de utilizar:

fn main() {
    let ten_chars = ('a'..).take(10).collect::<Vec<char>>();
    let skip_then_ten_chars = ('a'..).skip(1300).take(10).collect::<Vec<char>>();

    println!("{:?}", ten_chars);
    println!("{:?}", skip_then_ten_chars);
}

Ambos imprimen 10 caracteres, pero el segundo se salta 1.300 lugares e imprime diez letras en Armenio.

['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
['յ', 'ն', 'շ', 'ո', 'չ', 'պ', 'ջ', 'ռ', 'ս', 'վ']

.fold() es otro popular método que se utliza para sumar elementos de un iterador, pero se puede hacer mucho más. Es similar en cierta medida a .for_each(). En .fold() primero se añade un valor inicial (que puede ser 0 para poder sumar los valores), seguido del cierre. El cierre recibe dos parámetros, el valor total hasta el momento y el siguiente elemento. Se ve a continuación un ejemplo simple para utlizar .fold() para sumar elementos.

fn main() {
    let some_numbers = vec![9, 6, 9, 10, 11];

    println!("{}", some_numbers
        .iter()
        .fold(0, |total_so_far, next_number| total_so_far + next_number)
    );
}

Así:

  • En el paso 1, comienza con 0 y suma el siguiente número: 9.
  • En el paso 2, toma el 9 y le suma el 6: 15.
  • En el paso 3, toma el 15 y le suma el 9: 24.
  • En el paso 4, toma el 24 y le suma el 10: 34.
  • En el paso 5, toma el 34 y le suma el 11: 45, se imprime 45.

Pero no es necesario usarlo solo para sumar. En el siguiente ejemplo se añade '-' a cada carácter para construir una cadena de caracteres de tipo String.

fn main() {
    let a_string = "No tengo guiones.";

    println!(
        "{}",
        a_string
            .chars() // iterator sobre cada carácter
            .fold("-".to_string(), |mut string_so_far, next_char| { // Comienza con una "-"
                string_so_far.push(next_char); // Incorpora el siguiente caracter, seguido de '-'
                string_so_far.push('-');
                string_so_far} // y se pasa al siguiente bucle
            ));
}

Que imprime:

-N-o- -t-e-n-g-o- -g-u-i-o-n-e-s-.-

Hay muchos otros métodos de gran utilidad como:

  • .take_while() que va leyendo elementos de un iterador mientras retorna true.
  • .cloned() que crea un clones de los elementos del iterador. Convierte una referencia en un valor.
  • .by_ref() que convierte en referencias los elementos del iterador. Es bueno para asegurarse de que se puede usar un Vec o similar después de que se haya iterado a través de él.
  • Existen muchos métodos acabados en _while: .skip_while(), .map_while().
  • .sum(): simplemente suma todos los elementos en un solo valor.

.chunks() y .windows() son dos formas de partir un vector en el tamaño que se desee. Se indica el tamñao que se desea como parámetro. Por ejemplo, si se tiene un vector de 10 elementos [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], y se pasa un parámetro de 3, se debe trabajar de la siguiente forma:

  • .chunk() dará lugar a cuatro fragmentos (slices): [0, 1, 2]. [3, 4, 5], [6, 7, 8] y [9]. El parámetro fija el número de elementos en cada fragmento. Con un posible fragmento de tamaño inferior al final.
  • .windows() dará lugar a más fragmentos en este caso ya que creará fragmentos del tamaño indicado y se desplazará de uno en uno: [0, 1, 2], [1, 2, 3], [2, 3, 4], ...., [7, 8, 9].

Se observa el funcionamiento con el siguiente código:

fn main() {
    let num_vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0];

    for chunk in num_vec.chunks(3) {
        println!("{:?}", chunk);
    }
    println!();
    for window in num_vec.windows(3) {
        println!("{:?}", window);
    }
}

Que imprime:

[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[0]

[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
[4, 5, 6]
[5, 6, 7]
[6, 7, 8]
[7, 8, 9]
[8, 9, 0]

Hay que tener en cuenta que .chunks() lanzará panic! si no se le indica ningún tamaño. Se puede escribir .chunks(1000) para un vector de un solo elemento, pero no se puede escribir .chunks() con algo de longitud 0. La función valida assert!(chunk_size != 0).

.match_indices() permite extraer de un String o &str todo lo que coincida con el parámetro e indica el índice también. Es similar a .enumerate() en que retorna una tupla con dos elementos.

fn main() {
    let rules = "Regla número 1: No luchar. Regla número 2: ve a la cama a las 8 pm. Regla número 3: levántate a las 6 am.";
    let rule_locations = rules.match_indices("Regla").collect::<Vec<(_, _)>>(); // Es Vec<usize, &str>
    println!("{:?}", rule_locations);
}

Que imprime:

[(0, "Regla"), (28, "Regla"), (70, "Regla")]

.peekable() crea un iterador que permite echar un vistazo al siguiente elemento. Es como llmar a .next()(devuelve un Option) solo que sin avanzar el iterador. Se puede usar tanto como se necesite. A continuación, se muestra un ejemplo de uso de .peek() tres veces para cada elemento.

fn main() {
    let just_numbers = vec![1, 5, 100];
    let mut number_iter = just_numbers.iter().peekable(); // Esto crea un tipo de iterator denominado Peekable

    for _ in 0..3 {
        println!("Me encanta el número {}", number_iter.peek().unwrap());
        println!("Me encanta tanto el número {}", number_iter.peek().unwrap());
        println!("{} es un número tan bonito", number_iter.peek().unwrap());
        number_iter.next();
    }
}

Que imprime:

Me encanta el número 1
Me encanta tanto el número 1
1 es un número tan bonito
Me encanta el número 5
Me encanta tanto el número 5
5 es un número tan bonito
Me encanta el número 100
Me encanta tanto el número 100
100 es un número tan bonito

A continuación se muestra otro ejemplo que usa .peek() para contrastar con match previamente a llamar a .next().

fn main() {
    let locations = vec![
        ("Nevis", 25),
        ("Taber", 8428),
        ("Markerville", 45),
        ("Cardston", 3585),
    ];
    let mut location_iter = locations.iter().peekable();
    while location_iter.peek().is_some() {
        match location_iter.peek() {
            Some((name, number)) if *number < 100 => { // .peek() da una referencia, se necesita *
                println!("Encontrada una aldea: {} con {} personas", name, number)
            }
            Some((name, number)) => println!("Encontrado un pueblo: {} con {} personas", name, number),
            None => break,
        }
        location_iter.next();
    }

El código anterior da como resultado:

Encontrada una aldea: Nevis con 25 personas
Encontrado un pueblo: Taber con 8428 personas
Encontrada una aldea: Markerville con 45 personas
Encontrado un pueblo: Cardston con 3585 personas

Finalmente, se muestra un ejemplo en que se se usa también .match_indices(). En este caso, introducen nombres en un struct dependiendo del número de espacios de la &str.

#[derive(Debug)]
struct Nombres {
    una_palabra: Vec<String>,
    dos_palabras: Vec<String>,
    tres_palabras: Vec<String>,
}

fn main() {
    let vec_of_Nombres = vec![
        "Caesar",
        "Frodo Baggins",
        "Bilbo Baggins",
        "Jean-Luc Picard",
        "Data",
        "Rand Al'Thor",
        "Paul Atreides",
        "Barack Hussein Obama",
        "Bill Jefferson Clinton",
    ];

    let mut iter_of_Nombres = vec_of_Nombres.iter().peekable();

    let mut all_Nombres = Nombres { // inicia un struct sin nombres
        una_palabra: vec![],
        dos_palabras: vec![],
        tres_palabras: vec![],
    };

    while iter_of_Nombres.peek().is_some() {
        let next_item = iter_of_Nombres.next().unwrap(); // Se puede usar .unwrap() ya que se sabe que contiene algo
        match next_item.match_indices(' ').collect::<Vec<_>>().len() { // Crea un vector utilizando .match_indices y comprueba la longitud
            0 => all_Nombres.una_palabra.push(next_item.to_string()),
            1 => all_Nombres.dos_palabras.push(next_item.to_string()),
            _ => all_Nombres.tres_palabras.push(next_item.to_string()),
        }
    }

    println!("{:?}", all_Nombres);
}

El código anterior imprime:

Nombres { una_palabra: ["Caesar", "Data"], dos_palabras: ["Frodo Baggins", "Bilbo Baggins", "Jean-Luc Picard", "Rand Al'Thor", "Paul Atreides"], tres_palabras: ["Barack Hussein Obama", "Bill Jefferson Clinton"] }

La macro dbg! e .inspect()

dbg!

dbg! es una macro muy útil para volcar información. Es una buena alternativa a println! porque es más fácil de teclear y da más información.

fn main() {
    let my_number = 8;
    dbg!(my_number);
}

Esto imprime [src\main.rSs:4] my_number = 8.

Además, dbg! se puede poner en muchos otros lugares, incluso envolviendo código en ella. Por ejemplo, en el siguiente código:

fn main() {
    let mut my_number = 9;
    my_number += 10;

    let new_vec = vec![8, 9, 10];

    let double_vec = new_vec.iter().map(|x| x * 2).collect::<Vec<i32>>();
}

Se crea una variable mutable y se modifica después. A continuación se crea un vec y se utiliza iter, map y collect para crear un nuevo vec. Se puede añadir dbg! en casi cualquier sitio del código anterior. Lo que hace esta macro es pedirle al compilador que "le diga qué está haciendo en este momento" y este se lo dice.

fn main() {
    let mut my_number = dbg!(9);
    dbg!(my_number += 10);

    let new_vec = dbg!(vec![8, 9, 10]);

    let double_vec = dbg!(new_vec.iter().map(|x| x * 2).collect::<Vec<i32>>());

    dbg!(double_vec);
}

El código anterior imprime primero:

[src\main.rs:3] 9 = 9

y después:

[src\main.rs:4] my_number += 10 = ()

y sigue con:

[src\main.rs:6] vec![8, 9, 10] = [
    8,
    9,
    10,
]

y la siguiente muestra incluso el valor de la expresión:

[src\main.rs:8] new_vec.iter().map(|x| x * 2).collect::<Vec<i32>>() = [
    16,
    18,
    20,
]

y:

[src\main.rs:10] double_vec = [
    16,
    18,
    20,
]

.inspect()

.inspect() es parecido a dbg!, pero se usa como .map en un iterador. Recibe un elemento iterador y se puede hacer lo que se quiera con él, el valor continuará en la cadena de funciones sin modificar. Por ejemplo, en el siguiente código:

fn main() {
    let new_vec = vec![8, 9, 10];

    let double_vec = new_vec
        .iter()
        .map(|x| x * 2)
        .collect::<Vec<i32>>();
}

Se quiere tener información sobre lo que va haciendo el código. Así que se puede añadir .inspect() en dos puntos:

fn main() {
    let new_vec = vec![8, 9, 10];

    let double_vec = new_vec
        .iter()
        .inspect(|first_item| println!("El elemento contiene: {}", first_item))
        .map(|x| x * 2)
        .inspect(|next_item| println!("Y después vale: {}", next_item))
        .collect::<Vec<i32>>();
}

Que imprime:

El elemento contiene: 8
Y después vale: 16
El elemento contiene: 9
Y después vale: 18
El elemento contiene: 10
Y después vale: 20

Como .inspect() recibe de parámetro un cierre (closure), se puede codificar todo lo que se necesite:

fn main() {
    let new_vec = vec![8, 9, 10];

    let double_vec = new_vec
        .iter()
        .inspect(|first_item| {
            println!("El elemento es: {}", first_item);
            match **first_item % 2 { // eL primer elemento es un &i32 **
                0 => println!("Es par."),
                _ => println!("Es impar."),
            }
            println!("En binario es {:b}.", first_item);
        })
        .map(|x| x * 2)
        .collect::<Vec<i32>>();
}

Que imprime:

El elemento es: 8
Es par.
En binario es 1000.
El elemento es: 9
Es impar.
En binario es 1001.
El elemento es: 10
Es par.
En binario es 1010.

Tipos de &str

Existe más de un tipo de &str:

  • Cadenas de caracteres literales: que se construyen cuando se escribe código como let mi_cadena = "Soy un &str". Este tipo de &str dura toda la ejecución del programa ya que están escritas directamente en el código fuente y así se traspasa al código binario. El tipo real que tienen estas variables es &'static str. El apostrofo ' indica el tiempo o ciclo de vida (lifetime) de este valor. Las cadenas de caracteres literales tienen un tiempo de vida llamado 'static, que sirve para expresar que la cadena existe durante toda la ejecución del programa.
  • &str prestados: Es la forma habitual del tipo &str. No tiene un tiempo de vida 'static, su tiempo de vida es menor a la duración de todo el programa. Por ejemplo, si se crea un String y se obtiene una referencia a ella, Rust la convertirá a &str si se necesita. Por ejemplo:
fn prints_str(my_str: &str) { // Esta función puede recibir &String y &str
    println!("{}", my_str);
}

fn main() {
    let my_string = String::from("I am a string");
    prints_str(&my_string); // Se pasa a prints_str u &String
}

En Rust, todas las referencias tienen un tiempo de vida asociado. ¿Y qué es el tiempo de vida? Se verá a continuación.

Tiempos de vida (lifetimes)

El tiempo de vida asociado a todos los valores y variables indica "cuánto vive una variable". Solo es necesario pensar en ellos cuando se trabaja con referencias. Esto se debe a que las referencias no pueden vivir más tiempo que el propio objeto al que referencian. Por ejemplo, esta función no compila:

fn returns_reference() -> &str {
    let my_string = String::from("I am a string");
    &my_string // ⚠️
}

fn main() {}

El problema es que my_string solo vive dentro de la propia función, pero la función intenta devolver una referencia &my_string y esta no podrá existir cuando se libere my_string al terminar de ejecutarse la función. Por eso el compilador falla.

Este otro código tampoco funciona:

fn returns_str() -> &str {
    let my_string = String::from("I am a string");
    "I am a str" // ⚠️
}

fn main() {
    let my_str = returns_str();
    println!("{}", my_str);
}

Aunque por poco. El compilador indica:

error[E0106]: missing lifetime specifier
 --> src\main.rs:6:21
  |
6 | fn returns_str() -> &str {
  |                     ^ 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
  |
6 | fn returns_str() -> &'static str {
  |                     ^^^^^^^^

El mensaje missing lifetieme specifier significa que tenemos que añadir ' con un tiempo de vida.

Después indica contains a borrowed value, but there is no value for it to be borrowed from. Esto signfiica que I am a str no se obtiene de ningún sitio. Además, indica consider using the 'static lifetime escribiendo &'static str. Por lo que el compilador piensa que se debe indicar que la variable es de tipo cadena de caracteres literal (como es el caso).

Con la modificación, lo siguiente funciona:

fn returns_str() -> &'static str {
    let my_string = String::from("I am a string");
    "I am a str"
}

fn main() {
    let my_str = returns_str();
    println!("{}", my_str);
}

Que funcione, se debe a que se ha indicado al compilador que esta función devuelve un &str con un tiempo de vida estático. Si se quisiera evolver my_string solo podría hacerse como String, no como referencia. El fallo del paso por referencia del primer ejemplo de este apartado, se debe a que la propiedad no se traspasaría al código que llamara a la función y en la siguiente línea se eliminaría el valor de my_string. Rust evita que exista una referencia cuyo tiempo de vida sea mayor que el del valor al que referencia.

Ahora fn returns_str() -> &'static str le dice a Rust que no debe preocuparse, se retorna una cadena de caracteres literal. La cadena de caracteres literal dura toda la ejecución del programa. Se observa que se asemeja a los genéricos. Cuando se indica al compilador algo como <T: Display>, se le está diciendo que solo se va a usar este código con tipos Display. Los tiempos de vida son similares: no se está cambiando nada en las propias variables, solo se está indicando al compilador cuales serán los tiempos de vida de cada variable de entrada y de salida.

Lógicamente, 'static no es el único tiempo de vida posible. Cada variable tiene su tiempo de vida, aunque normalmente no es necesario indicarlo en el código. El compilador es inteligente y puede deducirlo por sí mismo. Solo es necesario expresarlo en el código cuando el compilador no puede hacerlo.

A continuación se muestra un ejemplo de otro tiempo de vida. Si se quiere crear un struct Ciudad y pasarle un &str para el nombre (por ejemplo, para que su rendimiento sea mejor que con un String). Se puede intentar así (este código no funciona):

#[derive(Debug)]
struct Ciudad {
    name: &str, // ⚠️
    date_founded: u32,
}

fn main() {
    let mi_ciudad = Ciudad {
        name: "Ichinomiya",
        date_founded: 1921,
    };
}

El compilador indica:

error[E0106]: missing lifetime specifier
 --> src\main.rs:3:11
  |
3 |     name: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
2 | struct Ciudad<'a> {
3 |     name: &'a str,
  |

Rust necesita un tiempo de vida para &str porque &str es una referencia. ¿Qué sucede cuando el valor al que apunta name se libera? No sería seguro usar este código.

¿Qué pasa si se pone 'static? ¿funcionará como en los casos anteriores? Si se prueba:

#[derive(Debug)]
struct Ciudad {
    name: &'static str, 
    date_founded: u32,
}

fn main() {
    let mi_Ciudad = Ciudad {
        name: "Ichinomiya",
        date_founded: 1921,
    };

println!("{} se fundó en {}", mi_Ciudad.name, mi_Ciudad.date_founded);

}

En este caso funciona. Sin embargo, solo se le pueden pasar cadenas de caracteres literales, no referencias a otro tipo de valores. Por eso, este otro código no funciona:

#[derive(Debug)]
struct Ciudad {
    name: &'static str, // debe existir el valor durante todo el programa
    date_founded: u32,
}

fn main() {
    let Ciudad_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()]; // Ciudad_names no vive durante todo el programa

    let my_Ciudad = Ciudad {
        name: &Ciudad_names[0], // ⚠️ Es un &str, pero no &'static str. Es una referencia a un valor interno de Ciudad_names
        date_founded: 1921,
    };

    println!("{} se fundó en {}", my_Ciudad.name, my_Ciudad.date_founded);
}

El compilador dice:

error[E0597]: `Ciudad_names` does not live long enough
  --> src\main.rs:12:16
   |
12 |         name: &Ciudad_names[0],
   |                ^^^^^^^^^^
   |                |
   |                borrowed value does not live long enough
   |                requires that `Ciudad_names` is borrowed for `'static`
...
18 | }
   | - `Ciudad_names` dropped here while still borrowed

Este ejemplo es importante entenderlo, ya que la referencia que se pasa sí que vive lo suficiente. Pero lo que se ha indicado en el código es que solo se le va a pasar 'static str' y ese es el problema.

Así que se va a intentar lo que el compilador sugería antes: escribir struct Ciudad<'a> y name: &'a str. Que significa que la referencia a name solo existe mientras exista el valor struct Ciudad.

#[derive(Debug)]
struct Ciudad<'a> { // City has lifetime 'a
    name: &'a str, // and name also has lifetime 'a.
    date_founded: u32,
}

fn main() {
    let ciudad_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()];

    let my_Ciudad = Ciudad {
        name: &ciudad_names[0],
        date_founded: 1921,
    };

    println!("{} se fundó en {}", my_Ciudad.name, my_Ciudad.date_founded);
}

Es necesario recordar que se puede escribir cualquier cosa en lugar de 'a. Vuelve a ser similar a los genéricos en los que se escribe T y U, pero se puede escribir cualquier palabra.

#[derive(Debug)]
struct City<'city> { // El tiempo de vida se llama ahora 'city
    name: &'city str, // y name vive solo lo que 'city
    date_founded: u32,
}

fn main() {}

En todo caso, se suelen usar por convención 'a, 'b, 'c, etc. En caso de usar otros nombres, es conveniente aprovechar para que tengan sentido para los humanos.

Se presenta de nuevo una comparación con los rasgos y genéricos. Por ejemplo:

use std::fmt::Display;

fn prints<T: Display>(input: T) {
    println!("T is {}", input);
}

fn main() {}

T:Display significa que solo se puede usar un valor si este implementa Display. No significa que doy Display a T.

Lo mismo sucede con los tiempos de vida. Si se escribe:

#[derive(Debug)]
struct City<'a> {
    name: &'a str,
    date_founded: u32,
}

fn main() {}

Significa que solo se permiten valores de referencia para name que duren al menos lo mismo que el struct de City que se está creando. No significa que el valor asignado a name pase a dura lo mismo que el struct de City creado.

Ahora se puede entender <'_> que se apareció anteriormente. Se denomina "tiempo de vida anónimo" y es un indicador de que se están usando referencias. Rust lo sugiere cuando se implementan struct. Por ejemplo: este es un código que no funciona:

    // ⚠️
struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

fn main() {}

Se ha hecho todo lo necesario para que la referencia en el name requira tener el tiempo de vida del struct como mínimo. Sin embargo, Rust se queja de la parte de la implementación.

error[E0726]: implicit elided lifetime not allowed here
 --> src\main.rs:6:6
  |
6 | impl Adventurer {
  |      ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

Pide que se añada una tiempo de vida anónimo para que quede claro en la implementación que existe una referencia en este código que se está usando. Basta con atender a la sugerencia:

struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer<'_> {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

fn main() {}

Este tiempo de vida anónimo simplifica la forma general que tendría que haber tenido en este caso impl<'a> Adventurer<'a>.

Los tiempos de vida son uno de los temas que puede ser más difícil en Rust. A continuación se muestran algunas sugerencias para que sirvan de ayuda:

  • Se pueden evitar las referencias pasando clones y con objetos copy, etc.
  • La mayor parte de las veces en las que el compilador necesita un tiempo de vida, simplemente habrá que escribir 'a en un par de sitios y funcionará.
  • Se puede ir aprendiendo este tema en pequeñas dosis. Se puede escribir el código con valores propietarios, sin referencias, luego convertir uno de ellos en referencia. El compilador empezará a quejarse y a dar sugerencias. Si se complica mucho, se puede deshacer el cambio e intentarlo más tarde.

Se presenta aquí un ejemplo que ya contiene una referencia y no indica tiempo de vida alguno. Se seguirán las indicaciones para subsanarlo.

// ⚠️
struct Adventurer {
    name: &str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {}

La primera queja es:

error[E0106]: missing lifetime specifier
 --> src\main.rs:2:11
  |
2 |     name: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 | struct Adventurer<'a> {
2 |     name: &'a str,
  |

Sugiere qué es lo que hay que hacer:

// ⚠️
struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {}

Ahora no tiene problema con esa parte de código, pero se queja de otra parte:

error[E0726]: implicit elided lifetime not allowed here
 --> src\main.rs:6:6
  |
6 | impl Adventurer {
  |      ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

error[E0726]: implicit elided lifetime not allowed here
  --> src\main.rs:12:28
   |
12 | impl std::fmt::Display for Adventurer {
   |                            ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

Así que se sigue la sugerencia y se modifica como pide:

struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer<'_> {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer<'_> {

        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {
    let mut billy = Adventurer {
        name: "Billy",
        hit_points: 100_000,
    };
    println!("{}", billy);
    billy.take_damage();
}

Esto da como salida:

Billy has 100000 hit points.
Billy has 99980 hit points left!

Así se puede observar que los tiempos de vida en muchas ocasiones sirven para que el compilador se asegure de que no se está cometiendo un error. Normalmente es lo suficientemente inteligente para determinar qué tiempo de vida tiene cada valor y solo "pregunta" cuando no lo puede saber con seguridad.

Mutabilidad interior

Cell

La mutabilidad interior permite modificar el interior de un elemento sin necesidad de que la variable sea mut.

Rust permite hacer esto en algunos casos de forma segura, modificando los valores internos de un struct que es inmutable. Cada uno de los mecanismos que se tienen a disposición, sigue unas reglas que aseguran que la modificación es segura.

En primer lugar, se presenta el siguiente ejemplo:

struct PhoneModel {
    company_name: String,
    model_name: String,
    screen_size: f32,
    memory: usize,
    date_issued: u32,
    on_sale: bool,
}

fn main() {
    let super_phone_3000 = PhoneModel {
        company_name: "YY Electronics".to_string(),
        model_name: "Super Phone 3000".to_string(),
        screen_size: 7.5,
        memory: 4_000_000,
        date_issued: 2020,
        on_sale: true,
    };

}

En este ejemplo, es mejor que los campos de PhoneModel sean inmutables. Por ejemplo, date_issued y screen_size nunca cambian.

Pero en su interior, también existe un campo denominado on_sale. Un modelo de teléfono empieza su vida estando a la venta (true en este campo), pero más tarde se deja de vender. ¿Se puede hacer que solo este campo sea modificable sin que se tenga que hacer todo el struct modificable? Es decir, sin tener que hacer let mut super_phone_3000 = ..., ya que de hacerlo así, todos los campos serían modificables.

Rust tiene varias formas de permitir la modificación segura dentro de un lugar que es inmutable en general. La forma más simple es el uso de Cell. Se tiene que incluir use std::cell::Cell para poder usarlo y a partir de ahí usar normalmente Cell.

use std::cell::Cell;

struct PhoneModel {
    company_name: String,
    model_name: String,
    screen_size: f32,
    memory: usize,
    date_issued: u32,
    on_sale: Cell<bool>,
}

fn main() {
    let super_phone_3000 = PhoneModel {
        company_name: "YY Electronics".to_string(),
        model_name: "Super Phone 3000".to_string(),
        screen_size: 7.5,
        memory: 4_000_000,
        date_issued: 2020,
        on_sale: Cell::new(true),
    };

    // 10 years later, super_phone_3000 is not on sale anymore
    super_phone_3000.on_sale.set(false);
}

Cellfunciona para todos los tipos, pero funciona mejor para los tipos simples Copy porque Cell no usa referencias. Tiene un método denominado get() que solo funciona en tipos Copy.

Otro tipo que se puede usar es RefCell.

RefCell

Un RefCell es otra forma de cambiar valores sin necesidad de declararlos mut. Es como Cell, pero utiliza referencias en lugar de copias.

El ejemplo a continuación permite ver cómo es similar a Cell.

use std::cell::RefCell;

#[derive(Debug)]
struct User {
    id: u32,
    year_registered: u32,
    username: String,
    active: RefCell<bool>,
    // Many other fields
}

fn main() {
    let user_1 = User {
        id: 1,
        year_registered: 2020,
        username: "User 1".to_string(),
        active: RefCell::new(true),
    };

    println!("{:?}", user_1.active);
}

Este ejemplo imprime RefCell { value: true }.

RefCell tiene muchos métodos. Dos de ellos son .borrow() y .borrow_mut(). Con estos métodos, se puede hacer lo mismo que con & y &mut. Las reglas son las mismas:

  • Se pueden hacer muchos préstamos simultáneos.
  • Solo se puede hacer un préstamo modificable.
  • No se pueden hacer préstamos simultáneos de ambos tipos.

Modificar el valor de un RefCell es así de fácil:

#![allow(unused)]
fn main() {
// 🚧
user_1.active.replace(false);
println!("{:?}", user_1.active);
}

Dispone también de otros métodos como replace_with que utiliza un cierre (closure):

#![allow(unused)]
fn main() {
// 🚧
let date = 2020;

user_1
    .active
    .replace_with(|_| if date < 2000 { true } else { false });
println!("{:?}", user_1.active);
}
}

Es necesario prestar atención al uso de RefCell ya que valida si los préstamos son correctos en tiempo de ejecución. No en tiempo de compilación. Es decir, cuando el programa ya se está ejecutando. Por eso, cosas como esta compilarán, aunque sean erróneas:

use std::cell::RefCell;

#[derive(Debug)]
struct User {
    id: u32,
    year_registered: u32,
    username: String,
    active: RefCell<bool>,
    // Many other fields
}

fn main() {
    let user_1 = User {
        id: 1,
        year_registered: 2020,
        username: "User 1".to_string(),
        active: RefCell::new(true),
    };

    let borrow_one = user_1.active.borrow_mut(); // primer préstamo modificable - correcto
    let borrow_two = user_1.active.borrow_mut(); // segundo préstamo modificable (sigue el primero) - incorrectos
}

Si se ejecuta el código anterior, Rust entrará en pánico:

thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:21:36
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\rust_book.exe` (exit code: 101)

Lo destacable del mensaje de error es already borrowed: BorrowMutError. Así que cuando se usan RefCell resulta conveniente ejecutar el código y pasar un conjunto de pruebas.

Mutex

Los Mutex posibilitan otra forma de modificar valores sin declararlos como mut. Significa exclusión mutua, lo que significa que "solo puede modificarlo un proceso cada vez". Por eso, el uso de mutex es seguro, porque permite la modificación interna, pero solo a un proceso cada vez. Es útil en la programación concurrente. Para ello, este tipo utiliza .lock(). Esta función es como cerrar la puerta con un candado desde dentro: se entra en una habitación, se cierra con llave y a partir de ahí, se puede modificar lo que contenga la habitación. Ningún otro proceso puede entrar a detener o contradecir los cambios.

Los mutex son más fáciles de explicar a través de ejemplos:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5); // Un nuevo Mutex<i32>. No se dice que sea mut
    let mut mutex_changer = my_mutex.lock().unwrap(); // mutex_changer es un MutexGuard
                                // Esta veriable tiene que ser  mut para cambiarla
                                // Con ella se tiene acceso al contenido de Mutex
                                // Si se intenta imprimir el mutex:

    println!("{:?}", my_mutex); // Se imprime "Mutex { data: <locked> }"
                                // El mensaje indica que no es posible acceder al valor,
                                // está bloquedo y solo puede acceder mutex_changer
                                // mientras mantenga el bloqueo

    println!("{:?}", mutex_changer); // Esto sí funciona, imprime 5. Se cambiara a 6.

    *mutex_changer = 6; // mutex_changer es un MutexGuard<&i32> se usa * para cambiar el valor i32

    println!("{:?}", mutex_changer); // Ahora indica 6
}

¿Cómo se puede "reabrir la puerta"? ¿Cómo se puede liberar el bloqueo del valor que aún mantiene mutex_changer para que otro lo pueda usar? Es necesario que la variable MutexGuard salga fuera de ámbito. Que su código finalice. Por ejemplo:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);
    {
        let mut mutex_changer = my_mutex.lock().unwrap();
        *mutex_changer = 6;
    } // mutex_changer sale de ámbito - desaparece. Ya no está bloquedo el valor.

    println!("{:?}", my_mutex); // Ahora indica: Mutex { data: 6 }
}

Es necesario tener cuidado en la programación concurrente ya que se pueden producir interbloqueos debido a que un segundo intento de bloqueo sobre un mutex mientras está bloquedo por otra variable producirá la parada y espera del segundo intento. Sin programación concurrente, se puede ver en el siguiente ejemplo:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);
    let mut mutex_changer = my_mutex.lock().unwrap(); // mutex_changer tiene el bloqueo
    let mut other_mutex_changer = my_mutex.lock().unwrap(); // other_mutex_changer quiere bloquear
                                    // el programa se queda parado esperando
                                    // y esperando
                                    // y esperará para siempre.

    println!("Esto nunca se imprimirá...");
}

Existe otro método try_lock() que intenta obtener el bloqueo una vez y si no lo consigue abando. En este caso, no se debe usar try_lock().unwrap() ya que si no consigue el bloqueo, la aplicación entrará en pánico. En este caso no se puede renunciar a let o match para validar correctamente:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);
    let mut mutex_changer = my_mutex.lock().unwrap();
    let mut other_mutex_changer = my_mutex.try_lock(); // intenta obtener el bloqueo

    if let Ok(value) = other_mutex_changer {
        println!("El  MutexGuard contiene: {}", value)
    } else {
        println!("No se pudo obtener el bloqueo")
    }
}

Tampoco es necesario crear una variable para modificar el valor:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);

    *my_mutex.lock().unwrap() = 6;

    println!("{:?}", my_mutex);
}

La línea de código *my_mutex.lock().unwrap() = 6; significa que *bloquea para mi uso el valor contenido en el mutex y conviértela en el 6". De esta forma, no existe variable y, por lo tanto, no es necesario desbloquear el mutex saliendo del ámbito de una variable. Esto se puede hacer tantas veces como se quiera:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);

    for _ in 0..100 {
        *my_mutex.lock().unwrap() += 1; // bloquea y desbloquea 100 veces
    }

    println!("{:?}", my_mutex);
}

RwLock

RwLock significa "bloqueo de lectura y escritura". Es como un Mutex, pero también como un RefCell. Se utiliza .write().unwrap() para modificarlo, en lugar de .lock().unwrap(). Y también se puede usar .read().unwrap() para obtener acceso de lectura. Es como RefCellya que sigue las reglas:

  • se pueden tener muchas varaibles .read().
  • se puede tener una sola variable .write().
  • no se puede tener a la vez una variable .write() con otras variables.

El programa se quedará bloqueado para siempre si se intenta .write() cuando no se puede obtener acceso.

use std::sync::RwLock;

fn main() {
    let my_rwlock = RwLock::new(5);

    let read1 = my_rwlock.read().unwrap(); // un .read() es correcto
    let read2 = my_rwlock.read().unwrap(); // dos.read() también es correcto

    println!("{:?}, {:?}", read1, read2);

    let write1 = my_rwlock.write().unwrap(); // uh oh, aquí el programa se ejecutará para siempre
}

Se debe liberar el bloqueo previo. Para ello se usa std::mem::drop. Igual que se podía hacer con mutex.

use std::sync::RwLock;
use std::mem::drop; // We will use drop() many times

fn main() {
    let my_rwlock = RwLock::new(5);

    let read1 = my_rwlock.read().unwrap();
    let read2 = my_rwlock.read().unwrap();

    println!("{:?}, {:?}", read1, read2);

    drop(read1);
    drop(read2); // descartamos las dos variables, para oder usar .write()

    let mut write1 = my_rwlock.write().unwrap();
    *write1 = 6;
    drop(write1);
    println!("{:?}", my_rwlock);
}

También se puede usar try_read() y try_write().

use std::sync::RwLock;

fn main() {
    let my_rwlock = RwLock::new(5);

    let read1 = my_rwlock.read().unwrap();
    let read2 = my_rwlock.read().unwrap();

    if let Ok(mut number) = my_rwlock.try_write() {
        *number += 10;
        println!("Ahora el núemro es {}", number);
    } else {
        println!("No se puede obtener acceso, lo siento.")
    };
}

Cow

Cow es un tipo de enumerado que significa "clone on write" (clonq en escritura). Permite devolver una referencia &str si no se necesita un String. Y devuelve un String, cuando se necesita. Lo mismo sucede para cualquier otro tipo, como arrays vs. Vec, etc.

Para entenderlo, se puede observar su definición:

pub enum Cow<'a, B>
where
    B: 'a + ToOwned + ?Sized,
 {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

fn main() {}

En el momento en que se aparece una indicación de tiempo de vida como 'a, se tiene conocimiento de que este tipo funciona con referencias, ya que, como se ha visto, es para lo que tienen sentido las indicaciones de tiempo de vida.

El rasgo ToOwned significa que B debe ser un tipo que puede convertirse en un tipo "adueñado", que tiene "una variable propietario". Por ejemplo, str que normalmente se usa a través de préstamo por referencia, se puede convertir en una String que tendrá una variable propietario.

Lo siguiente que aparece es ?Sized, que significa que "puede ser Sized o no". Casi todos los tipos de Rust son Sized, pero hay algunos, como str, que no. Es el motivo por el que este tipo se debe usar siempre por referencia, porque el compilador no conoce su tamaño. Por ello, si se quiere que una definición use tipos como str, resulta necesario que admita ?Sized.

A continuación, el código presenta las variantes del enumerado. Que son Borrowed y Owned.

Por ejemplo, si se imagina una función que devolviera Cow<'static, str>. Si a esa función se le dice que devuelva "Mi mensaje".into(), Rust mirará el tipo de "Mi mensaje", que es un str. Se trata de un tipo Borrowed, por lo que el tipo seleccionado del enumerado será Borrowed(&'a, B) y el tipo concreto que se devuelve es Cow::Borrowed(&'static str).

Si en vez de lo anterior, se le pidiera devolver format!("{}", "Mi mensaje").into(), entonces el tipo es String, porque format! crea String. Y el tipo enumerado seleccionado será Owned.

A continuación se presenta un ejemplo para probar Cow. Se pasará un número a una función que devuelve Cow<'static, str>. Dependiendo del número, se crear un &str o una String. Después, se usa .into() para convertirlo en un Cow. De esta forma, Rust seleccionará Cow::Borrowed o Cow::Owned. Se usa un match al finalizar, para ver qué se ha seleccionado:

use std::borrow::Cow;

fn modulo_3(input: u8) -> Cow<'static, str> {
    match input % 3 {
        0 => "El resto es 0".into(),
        1 => "El resto es 1".into(),
        remainder => format!("El resto es {}", remainder).into(),
    }
}

fn main() {
    for number in 1..=6 {
        match modulo_3(number) {
            Cow::Borrowed(message) => println!("{}, es el valor que se pasa. El Cow es prestado en este mensaje: {}", number, message),
            Cow::Owned(message) => println!("{}, es el valor que se pasa. El Cow es propietario en este mensaje: {}", number, message),
        }
    }
}

Que imprime:

1, es el valor que se pasa. El Cow es prestado en este mensaje: El resto es 1
2, es el valor que se pasa. El Cow es propietario en este mensaje: El resto es 2
3, es el valor que se pasa. El Cow es prestado en este mensaje: El resto es 0
4, es el valor que se pasa. El Cow es prestado en este mensaje: El resto es 1
5, es el valor que se pasa. El Cow es propietario en este mensaje: El resto es 2
6, es el valor que se pasa. El Cow es prestado en este mensaje: El resto es 0

Cow tiene otros métodos como into_ownedo into_borrowed para cambiar el tipo manualmente si resulta necesario.

Alias de tipos

Un alias para un tipo permite darle un nuevo nombre. Normalmente, se usan cuando se tiene un nombre de tipo muy largo que no se quiere escribir cada vez. También sirve para cuando se quiere dar un nombre más apropiado o fácil de recordar en el contexto de la aplicación. A continuación se muestran dos ejemplos de alias.

El siguiente ejemplo sirve para darle un nombre más fácil de comprender en el código:

type CharacterVec = Vec<char>;

fn main() {}

EL siguiente ejemplo sirve para mostrar un caso con un tipo difícil de entender:

// this return type is extremely long
fn returns<'a>(input: &'a Vec<char>) -> std::iter::Take<std::iter::Skip<std::slice::Iter<'a, char>>> {
    input.iter().skip(4).take(5)
}

fn main() {}

Con un alias queda mucho más claro:

type SkipFourTakeFive<'a> = std::iter::Take<std::iter::Skip<std::slice::Iter<'a, char>>>;

fn returns<'a>(input: &'a Vec<char>) -> SkipFourTakeFive {
    input.iter().skip(4).take(5)
}

fn main() {}

Lógicamente, también se puede importar un tipo para hacer las definiones más simples. El ejemplo anterior quedaría así:

use std::iter::{Take, Skip};
use std::slice::Iter;

fn returns<'a>(input: &'a Vec<char>) -> Take<Skip<Iter<'a, char>>> {
    input.iter().skip(4).take(5)
}

fn main() {}

Así que se puede decidir qué es mejor en cada caso.

Los alias no crean un tipo nuevo. Solo se trata de un nuevo nombre que representa al tipo ya existente. Por ello, si se escribe el siguiente código type File = String;, el compilador solo ve el tipo String. Por ello, imprimirá true:

type File = String;

fn main() {
    let my_file = File::from("I am file contents");
    let my_string = String::from("I am file contents");
    println!("{}", my_file == my_string);
}

¿Cómo se haría si realmente se quisiera un tipo nuevo? Lo más simple es usar el tipo de struct que representa tuplas incluyendo el tipo preexistente (Se trata de un uso idiomático de Rust que se denomina newtype).

struct File(String); // File es un envoltorio de String

fn main() {
    let my_file = File(String::from("I am file contents"));
    let my_string = String::from("I am file contents");
}

Ahora, el siguiente código ya no funciona debido a que son dos tipos diferentes:

struct File(String); // File es un envoltorio de String

fn main() {
    let my_file = File(String::from("I am file contents"));
    let my_string = String::from("I am file contents");
    println!("{}", my_file == my_string);  // ⚠️ no se puede comparar File con String
}

Para poder compararlos, hay que recuperar la cadena de texto incluida en File con my_file.0:

struct File(String);

fn main() {
    let my_file = File(String::from("I am file contents"));
    let my_string = String::from("I am file contents");
    println!("{}", my_file.0 == my_string); // my_file.0 es String, así que imprime true
}

El nuevo tipo creado, no tienen ningún rasgo, pero se pueden implementar como siempre.

#![allow(unused)]
fn main() {
#[derive(Clone, Debug)]
struct File(String);
}

En el código anterior, el tipo File se puede clonar y depurar (imprimir sus valores), pero no dispone de los demás rasgos de String. Quien sí sigue teniendo dichos rasgos es la cadena interior .0. Si este código se usara como librería en otros programas, esta cadena interior no estaría accesible, salvo en el caso de que se marcara como pub. Por eso, en esta clase de tipos es habitual el uso del rasgo Deref. Ambos conceptos, pub y Deref, se verán más adelante.

Importar y redenominar dentro de una función

Normalmente, se escribe use al inicio del programa, de esta forma:

use std::cell::{Cell, RefCell};

fn main() {}

Pero es posible, como se ha visto, usarlo en cualquier parte del código. Especialmente en funciones con enumerados que tienen nombres largos. Por ejemplo:

enum MapDirection {
    North,
    NorthEast,
    East,
    SouthEast,
    South,
    SouthWest,
    West,
    NorthWest,
}

fn main() {}

fn give_direction(direction: &MapDirection) {
    match direction {
        MapDirection::North => println!("You are heading north."),
        MapDirection::NorthEast => println!("You are heading northeast."),
        // Hay que teclear mucho... Y seguir tecleando el resto de opciones
    }
}

Sin embargo, es posible simplificar escribiendo un use dentro de la propia función.

enum MapDirection {
    North,
    NorthEast,
    East,
    SouthEast,
    South,
    SouthWest,
    West,
    NorthWest,
}

fn main() {}

fn give_direction(direction: &MapDirection) {
    use MapDirection::*; // Se importa todo en MapDirection
    let m = "You are heading";

    match direction {
        North => println!("{} north.", m),
        NorthEast => println!("{} northeast.", m),
        // Es un poco menos a escribir
        // ⚠️
    }
}

Se ha visto ya que ::* significa que "se importa todo lo que venga después de ::". En este caso, significa que se importa North, NorthEast, ... y así hasta NorthWest. También se puede hacer la importar código de terceros. Esto puede dar lugar a problemas cuando parte de nuestro código tiene nomenclatura idéntica a la de otras librerías. Por eso, no es recomendable usar ::* a menos que se esté seguro de ello.

Algunas veces se observará que existe una sección denominada prelude en el código de otras librerías. Por convenio, esta es la forma de agrupar los elementos que se necesitarán de forma habitual. En este caso, la forma de importarlo recomendada, sí suele ser con *, así: name::prelude::*. Se hablará más de ello en las secciones dedicadas a modules y crates.

También se puede usar as para cambiar los nombres. Por ejemplo, se puede dar el caso de estar utilizando el código de otro desarrollador y no se pueden cambiar los nombres en un enum:

enum FileState {
    CannotAccessFile,
    FileOpenedAndReady,
    NoSuchFileExists,
    SimilarFileNameInNextDirectory,
}

fn main() {}

Así que se puede importar todo y luego cambiarle los nombres.

enum FileState {
    CannotAccessFile,
    FileOpenedAndReady,
    NoSuchFileExists,
    SimilarFileNameInNextDirectory,
}

fn give_filestate(input: &FileState) {
    use FileState::{
        CannotAccessFile as NoAccess,
        FileOpenedAndReady as Good,
        NoSuchFileExists as NoFile,
        SimilarFileNameInNextDirectory as OtherDirectory
    };
    match input {
        NoAccess => println!("Can't access file."),
        Good => println!("Here is your file"),
        NoFile => println!("Sorry, there is no file by that name."),
        OtherDirectory => println!("Please check the other directory."),
    }
}

fn main() {}

Por lo que ahora se puede escribir OtherDirectory en lugar de FileState::SimilarFileNameInNextDirectory.

La macro todo!

En ocasiones, se necesita escribir código para ayudar a imaginar el proyecto que se está desarrollando. Por ejemplo, se puede imaginar un proyecto para hacer algo con libros. Esto es lo que se puede ir pensando mientras se va escribiendo:

struct Libro {} // Primero necesitaré una estructura para los libros.
               // Nada aquí aún - se añadirá más tarde

enum TipoLibro { // Un Libro puede ser de TapaDura o de TapaBlanda: necesito un enumerado
    TapaDura,
    TapaBlanda,
}

fn get_libro(libro: &Libro) -> Option<String> {} // ⚠️ get_libro debería recibir un &Libro y devolver un Option<String>

fn delete_libro(libro: Libro) -> Result<(), String> {} // delete_libro debería recibir un Libro y devolver un Result...
                        // TODO: bloque impl con el cntenido de estas funciones...
fn check_tipo_libro(tipo_libro: &TipoLibro) { // Me aseguro de que este match funciona
    match tipo_libro {
        TipoLibro::TapaDura => println!("Es TapaDura"),
        TipoLibro::TapaBlanda => println!("Es TapaBlanda"),
    }
}

fn main() {
    let tipo_libro = TipoLibro::TapaDura;
    check_tipo_libro(&tipo_libro); // Vamos a probarlo
}

Rust no puede compilar el código anterior debido a get_libro y delete_libro. Dice:

error[E0308]: mismatched types
 --> src/main.rs:9:32
  |
9 | fn get_libro(libro: &Libro) -> Option<String> {} // ⚠️ get_libro debería recibir un &Libro y devolver un Option<String>
  |    ---------                   ^^^^^^^^^^^^^^ expected enum `Option`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
  |
  = note:   expected enum `Option<String>`
          found unit type `()`

error[E0308]: mismatched types
  --> src/main.rs:11:34
   |
11 | fn delete_libro(libro: Libro) -> Result<(), String> {} // delete_libro debería recibir un Libro y devolver un Result...
   |    ------------                  ^^^^^^^^^^^^^^^^^^ expected enum `Result`, found `()`
   |    |
   |    implicitly returns `()` as its body has no tail or `return` expression
   |
   = note:   expected enum `Result<(), String>`
           found unit type `()`

En este momento no es posible completar el código de get_libro, ni de delete_libro. Para esto se puede usar todo!(). Si se añade a la función, se evitan los fallos que encuentra Rust.

struct Libro {}

fn get_libro(libro: &Libro) -> Option<String> {
    todo!() // todo! significa "Lo añadiré después. Por favor, dalo por bueno mientras tanto."
}

fn delete_libro(libro: Libro) -> Result<(), String> {
    todo!()
}

fn main() {}

Ahora el código compila y se puede ver el resultado de check_tipo_libro: Es TapaDura.

Es necesario tener cuidado, ya que aunque compile no se pueden usar las funciones que contienen todo. Si se llamaa una de estas funciones, el programa entrará en pánico.

Adicionalmente, las funciones que contienen todo! siguen necesitando tipos de entrada y salida existentes. Si se escribe un tipo inexistente, el programa no compilará. Por ejemplo:

struct Libro {}

fn get_libro(libro: &Libro) -> WorldsBestType { // ⚠️
    todo!()
}

fn main() {}

Dirá al compilar:

error[E0412]: cannot find type `WorldsBestType` in this scope
 --> src/main.rs:3:32
  |
3 | fn get_libro(libro: &Libro) -> WorldsBestType { // ⚠️
  |                                ^^^^^^^^^^^^^^ not found in this scope

La macro todo! funciona de forma igual a unimplemented!(). Simplemente unimplemented!() es demasiado largo de escribir, por lo que se creó todo!() que es más corto.

Rc

Rc significa "contador de referencias". Ya se ha visto que en Rust cada valor solo puede ter un dueño. Por eso, el siguiente código no funciona:

fn takes_a_string(input: String) {
    println!("It is: {}", input)
}

fn also_takes_a_string(input: String) {
    println!("It is: {}", input)
}

fn main() {
    let user_name = String::from("User MacUserson");

    takes_a_string(user_name);
    also_takes_a_string(user_name); // ⚠️
}

Después de que takes_a_string reciba user_name, no se puede volver a usar. En este caso, se podría solventar utilizando user_name.clone(). Pero en ocasiones, un valor forma parte de un struct y puede que no se pueda clonar ese struct. O puede que sea un valor de gran tamaño que no sea eficiente clonar. Por estas razones existe Rc, que sirve para permitir que un valor tenga más de un dueño de forma simultánea. Rc anota quienes tienen la propiedad y cuántos. Posteriormente, cuando el número de dueños baja a cero, el valor asociado se liberará.

En el siguiente ejemplo se usa Rc. Se crean dos struct: uno denominado City y otro CityData. City contiene la información de una ciudad y CityData reúne todas las ciudades usando Vec.

#[derive(Debug)]
struct City {
    name: String,
    population: u32,
    city_history: String,
}

#[derive(Debug)]
struct CityData {
    names: Vec<String>,
    histories: Vec<String>,
}

fn main() {
    let calgary = City {
        name: "Calgary".to_string(),
        population: 1_200_000,
           // Pretend that this string is very very long
        city_history: "Calgary began as a fort called Fort Calgary that...".to_string(),
    };

    let canada_cities = CityData {
        names: vec![calgary.name], // This is using calgary.name, which is short
        histories: vec![calgary.city_history], // But this String is very long
    };

    println!("Calgary's history is: {}", calgary.city_history);  // ⚠️
}

Esto no funciona debido a que canada_cities es el dueño de los datos al final y calgary ya no lo es. Por lo que el error es el siguiente:

error[E0382]: borrow of moved value: `calgary.city_history`
  --> src/main.rs:27:42
   |
24 |         histories: vec![calgary.city_history], // But this String is very long
   |                         -------------------- value moved here
...
27 |     println!("Calgary's history is: {}", calgary.city_history);  // ⚠️
   |                                          ^^^^^^^^^^^^^^^^^^^^ value borrowed here after move
   |
   = note: move occurs because `calgary.city_history` has type `String`, which does not implement the `Copy` trait

Se podría clonar el nombre con names: vec![calgary.name.clone()], pero no se desea con city_history debido a que puede ser un texto muy largo. Por ello, se usa Rc. Para ello, se debe declarar su uso previamente:

use std::rc::Rc;

fn main() {}

Y después, usar Rc como tipo de la variable que se quiera usar:

´´´rust use std::rc::Rc;

#[derive(Debug)] struct City { name: String, population: u32, city_history: Rc, }

#[derive(Debug)] struct CityData { names: Vec, histories: Vec<Rc>, }

fn main() {} ´´´

Para añadir una nueva referencia, se debe usar clone sobre el elemento Rc. En los Rc, esta función solo clona el puntero y no duplica la variable contenida. Por ello, su coste es mínimo.

En el ejemplo anterior, al clonar la historia de una ciudad, esta tendrá dos dueños. Se puede comprobar el número de dueños en un momento dado mediante Rc::strong_count(&item). El código quedará así:

use std::rc::Rc;

#[derive(Debug)]
struct City {
    name: String,
    population: u32,
    city_history: Rc<String>, // String dentro de un Rc
}

#[derive(Debug)]
struct CityData {
    names: Vec<String>,
    histories: Vec<Rc<String>>, // Un Vec de Strings dentro de Rcs
}

fn main() {
    let calgary = City {
        name: "Calgary".to_string(),
        population: 1_200_000,
        city_history: Rc::new("Calgary began as a fort called Fort Calgary that...".to_string()), // Rc::new() para crear el Rc
    };

    let canada_cities = CityData {
        names: vec![calgary.name],
        histories: vec![calgary.city_history.clone()], // .clone() para incrementar la cuenta
    };

    println!("Calgary's history is: {}", calgary.city_history);
    println!("{}", Rc::strong_count(&calgary.city_history));
    let new_owner = calgary.city_history.clone();
}

Este código imprime 2 como el número de dueños de la historia de Calgary. Como la última línea de código añade otro dueño, new_owner, si se imprimiera después la cuenta de dueños, se mostraría ya 3.

Se observa que esta función habla de strong pointers (punteros fuertes). ¿Existen los punteros débiles? Sí, existen. Se utilizan cuando se quiere evitar referencias circulares entre elementos. Por ejemplo, si dos Rc se apuntan entre sí, si ambos punteros son fuertes, nunca alcanzarán la cuenta de cero dueños, ya que cada uno es dueño del otro, y no se podrán liberar. Rc mantiene la cuenta de los punteros débiles, pero no la tiene en cuenta para liberar la memoria. Es decir, cuando un elemento Rc no tiene punteros fuertes, se libera su espacio aunque la cuenta de punteros débiles no sea cero.

Se puede utilizar Rc::downgrade(&item) en lugar de Rc::clone(&item) para construir una referencia débil. Para ver la cuenta de referencias débiles se usa Rc::weak_count(&item).

Múltiples hilos

Para ejecutar diferentes tareas al mismo tiempo, se usan los hilos (threads). Los ordenadores modernos suelen tener más de un núcleo de proceso por lo que pueden ejecutar más de una cosa a la vez. Rust permite aprovechar esto. Para ello, Rust utiliza hilos, llamados hilos de sistema operativo. Esto significa que el sistema operativo crea este hilo y lo asigna a un núcleo de proceso. Otros lenguajes de programación utilizan hilos verdes (green threads) que son menos potentes.

Se pueden crear hilos con std::thread::spawn al que se le pasa un cierre para indicarle qué tiene que hacer. Los hilos son interesantes porque se ejecutan a la vez. Se puede comprobar con el siguiente ejemplo.

fn main() {
    std::thread::spawn(|| {
        println!("I am printing something");
    });
}

Si se ejecuta este código, en ocasiones se imprimirá algo y otras veces no. Dependerá también de la velocidad del ordenador en que se ejecute. Esto sucede porque main() se ejecuta en el hilo principal del programa y el cierre en un hilo secundario. Cuando el hilo principal, main(), finaliza, el programa se para.

Para verlo mejor, un bucle for resulta más práctico:

fn main() {
    for _ in 0..10 { // lanzará 10 hilos
        std::thread::spawn(|| {
            println!("I am printing something");
        });
    }   // Se inicia un hilo.
}       // ¿Cuántos hilos pueden terminar antes de que main() finalice aquí?

Variará en cada caso, unas veces 1, otras 4, otras 5. Si el ordenador es muy rápido, podría no llegarse a imprimir nada. A veces, pdoría darse este error:

#![allow(unused)]
fn main() {
thread 'thread 'I am printing something
thread '<unnamed><unnamed>thread '' panicked at '<unnamed>I am printing something
' panicked at 'thread '<unnamed>cannot access stdout during shutdown' panicked at '<unnamed>thread 'cannot access stdout during
shutdown
}

Que sucede cuando el hilo intenta ejecutar algo mientras el programa está finalizando.

Se le puede pedir al hilo principal (el que está ejecutando la función main()) que ejecute algo que lo entretenga mientras se ejecutan los hilos:

fn main() {
    for _ in 0..10 {
        std::thread::spawn(|| {
            println!("I am printing something");
        });
    }
    for _ in 0..1_000_000 { // declarar "let x = 9" un millón de veces
                            // Tiene que hacerlo antes de poder acabar la función main()
        let _x = 9;
    }
}

Pero este código anterior es una mala práctica. Para dar tiempo a acabar a los hilos, lo que se debe hacer es conservarlas en una variable. Si se añade let se asigna un valor de tipo JoinHandle. Esto se ve claramente en la definición de la función spawn:

#![allow(unused)]
fn main() {
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
}

f es el cierre que se ejecuta por este hilo. JoinHandle es el tipo de retorno.

Ahora se puede escribir:

fn main() {
    for i in 0..10 {
        let handle = std::thread::spawn(|| {
            println!("Hilo, imprimo algo");
        });
        println!("Hilo {} creado.", i);
    }
}

handle es de tipo JoinHandle. ¿Qué se hace con él? Se puede usar el método .join() que hace que el hilo en el que se ejecute (el principal), se pare para esperar a que este hilo haya terminado. Así:

fn main() {
    for i in 0..10 {
        let handle = std::thread::spawn(|| {
            println!("Hilo, imprimo algo");
        });
        println!("Hilo {} creado.", i);
        handle.join(); // Espera a que acabe este hilo 
    }
}

El código anterior no es muy correcto ya que su orden de ejecución es: crea un hilo, espera a que termine, luego crea otro, etc. El resultado siempre es:

Hilo 0 creado.
Hilo, imprimo algo
Hilo 1 creado.
Hilo, imprimo algo
Hilo 2 creado.
Hilo, imprimo algo
Hilo 3 creado.
Hilo, imprimo algo
Hilo 4 creado.
Hilo, imprimo algo
Hilo 5 creado.
Hilo, imprimo algo
Hilo 6 creado.
Hilo, imprimo algo
Hilo 7 creado.
Hilo, imprimo algo
Hilo 8 creado.
Hilo, imprimo algo
Hilo 9 creado.
Hilo, imprimo algo

Con lo que no se aprovecha para paralelizar todo lo que sea posible por los nucleos de proceso que tenga el ordenador. Lo correcto sería lanzar todos los hilos y luego esperar a que acaben todos, así:

fn main() {
    let mut handles = Vec::new();

    for i in 0..10 {
        handles.push(std::thread::spawn(|| {
            println!("Hilo, imprimo algo");
        }));
        println!("Hilo {} creado.", i);
    }

    for handle in handles {
        handle.join();
    }
}

De esta forma, se crean los hilos y la ejecución puede producirse en diversos ordenes. Pero de forma simultánea puede haber hasta un máximo de 10 hilos.

A continuación se explican los tres tipos de cierres que existen:

  • FnOnce: que toma el cierre completo.
  • FnMut: que toma una referencia modificable.
  • Fn: que toma una referencia.

Un cierre intentará usar Fn, si es posible. Pero si necesita modificar algún valor utilizará FnMut. Y si necesita apropiarse del valor, usará FnOnce. Este último es un buen nombre, porque explica lo que hace: tomar el valor una vez y luego ya no puede volver a usarlo.

A continuación se observa un ejemplo:

fn main() {
    let my_string = String::from("I will go into the closure");
    let my_closure = || println!("{}", my_string);
    my_closure();
    my_closure();
}

Que imprime:

I will go into the closure
I will go into the closure

String no es de tipo Copy, por lo que el cierre es Fn y Rust crea una referencia al valor.

Si se modificara el valor de la variable, el cierre pasaría a ser de tipo FnMut.

fn main() {
    let mut my_string = String::from("I will go into the closure");
    let mut my_closure = || {
        my_string.push_str(" now");
        println!("{}", my_string);
    };
    my_closure();
    my_closure();
}

Que imprime:

I will go into the closure now
I will go into the closure now now

Si la variable se pasa por valor, entonces el cierre será FnOnce.

fn main() {
    let my_vec: Vec<i32> = vec![8, 9, 10];
    let my_closure = || {
        my_vec
            .into_iter() // into_iter toma la propiedad
            .map(|x| x as u8) // lo convierte en u8
            .map(|x| x * 2) // lo multiplica por 2
            .collect::<Vec<u8>>() // y lo guarda en un Vec
    };
    let new_vec = my_closure();
    println!("{:?}", new_vec);
}

En este último caso, solo se puede ejecutar una vez este cierre ya que my_vec se pasa por valor.

De vuelta a los hilos. Si se intenta usar un valor así:

fn main() {
    let mut my_string = String::from("¿Puede pasarlo a un hilo?");

    let handle = std::thread::spawn(|| {
        println!("{}", my_string); // ⚠️
    });

    handle.join();
}

El compilador dice que esto no es posible:

error[E0373]: closure may outlive the current function, but it borrows `my_string`, which is owned by the current function
 --> src/main.rs:4:37
  |
4 |     let handle = std::thread::spawn(|| {
  |                                     ^^ may outlive borrowed value `my_string`
5 |         println!("{}", my_string); // ⚠️
  |                        --------- `my_string` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:4:18
  |
4 |       let handle = std::thread::spawn(|| {
  |  __________________^
5 | |         println!("{}", my_string); // ⚠️
6 | |     });
  | |______^
help: to force the closure to take ownership of `my_string` (and any other referenced variables), use the `move` keyword
  |
4 |     let handle = std::thread::spawn(move || {
  |                                     ++++

Es un mensaje muy largo, pero explicativo: dice que es necesario usar la palabra move. El problema es que en el hilo principal la variable es mut y, por lo tanto, se puede modificar mientras los demás hilos tienen acceso a ella. Esto no es seguro.

Se puede intentar algo más que tampoco funciona:

fn main() {
    let mut my_string = String::from("Can I go inside the thread?");

    let handle = std::thread::spawn(|| {
        println!("{}", my_string); // now my_string is being used as a reference
    });

    std::mem::drop(my_string);  // ⚠️ We try to drop it here. But the thread still needs it.

    handle.join();
}

Lo correcto, para poder usarlo, es pasar la variable con move para hacer al cierreo propietario de tipo FnOnce.

fn main() {
    let mut my_string = String::from("Can I go inside the thread?");

    let handle = std::thread::spawn(move|| {
        println!("{}", my_string);
    });

    std::mem::drop(my_string);  // ⚠️ No se puede hacer drop, ya que se ha transferido al hilo anterior.

    handle.join();
}

Si se quita el std::mem::drop funciona correctamente ya que el código es seguro:

fn main() {
    let my_string = String::from("Can I go inside the thread?");

    let handle = std::thread::spawn(move|| {
        println!("{}", my_string);
    });

    handle.join().unwrap();
}

Es necesario recordar: si se necesita pasar por valor un elemento, es necesario usar move.

Cierres en funciones

Los cierres (closures) son muy útiles. ¿Cómo los pasamos como parámetro a nuestras propias funciones?

Es posible hacerlo, pero dentro de ellas es necesario definir el tipo de cierre (de los tres tipos posibles vistos en el apartardo anterior). Fuera de una función, Rust decide por sí mismo qué tipo de cierre debe usar: Fn, FnMut o FnOnce. Sin embargo, dentro de la función resulta necesario seleccionar qué tipo se admite. Loa mejor forma de comprenderlo es revisar varias definiciones de funión. Por ejemplo, la siguiente de .all(). Si se recuerda, esta función comprueba si un iterador cumple una condición en todos sus elementos. Parte de la definición dice:

#![allow(unused)]
fn main() {
    fn all<F>(&mut self, f: F) -> bool    // 🚧
    where
        F: FnMut(Self::Item) -> bool,
}
  1. fn all<F> indica que existe un tipo genérico F. Un cierre siempre es un tipo genérico ya que cada función es en sí misma un tipo diferente (con un solo valor).
  2. (&mut self, f: F): &mut self revela que esta función es un método. f: F es lo que por convenio se suele escribir para representar un parámetro que debe recibir un cierre: este es el nombre de la variable y el tipo genérico. No es obligatorio, lógicamente, usar estas letras f y F.
  3. La siguiente parte es la que define que el genérico tiene que ser uno de los tres tipos de cierre: where F: FnMut(Self::Item) -> bool. Se requiere, en este caso, que el cierre sea modificable para que se puedan modificar sus parámetros. En este caso, necesita cambiar el iterador (Self::Item). Se devuelve un booleano: true o false.

A continuación se muestar una definición más simple con un cierre:

#![allow(unused)]
fn main() {
fn do_something<F>(f: F)    // 🚧
where
    F: FnOnce(),
{
    f();
}
}

En este caso, se toma un cierre como parámetro (se hace propietario de él ya que es de tipo FnOnce) y no devuelve ningún valor. El cierre, según se define en la cláusula where, no tiene parámetros y no devuelve ningún valor.

Ampliando el ejemplo anterior, se crea un Vec y se itera a través de él para mostrar lo que se puede hacer:

fn do_something<F>(f: F)
where
    F: FnOnce(),
{
    f();
}

fn main() {
    let some_vec = vec![9, 8, 10];
    do_something(|| {
        some_vec
            .into_iter()
            .for_each(|x| println!("The number is: {}", x));
    })
}

Para ver un ejemplo más realista, se crea a continuación un struct City. Esta vez tendrá más datos sobre años y población. Dispone de un Vec<u32> para contener todos los años y otro Vec<u32>igual para el número de habitantes.

City tiene dos funciones: new() para crear una nueva ciudad y .city_data() que recibe un cierre. Cuando se usa .city_data() devuelve los años, los habitantes y un cierre, para que se pueda realizar la operación que se solicite con los datos. El tipo del cierre es FnMut para poder modificar los valores. El ejemplo es así:

#[derive(Debug)]
struct City {
    name: String,
    years: Vec<u32>,
    populations: Vec<u32>,
}

impl City {
    fn new(name: &str, years: Vec<u32>, populations: Vec<u32>) -> Self {

        Self {
            name: name.to_string(),
            years,
            populations,
        }
    }

    fn city_data<F>(&mut self, mut f: F) // self. Solo f es genérico de tipo F. f es el cierre

    where
        F: FnMut(&mut Vec<u32>, &mut Vec<u32>), // El cierre toma como parámetrso dos vectors de u32
                                // que representan el año y la poblicación.
                                // no devuelve ningún valor
    {
        f(&mut self.years, &mut self.populations) // Finalmente este es el código de la función
            // simplemente usa el cierre en los dos parámetros year y habitantes"
            // se puede hacer lo que se quiera dentro del cierre. No devuelve ningún valor
    }
}

fn main() {
    let years = vec![
        1372, 1834, 1851, 1881, 1897, 1925, 1959, 1989, 2000, 2005, 2010, 2020,
    ];
    let populations = vec![
        3_250, 15_300, 24_000, 45_900, 58_800, 119_800, 283_071, 478_974, 400_378, 401_694,
        406_703, 437_619,
    ];
    // Se crea la ciudad
    let mut tallinn = City::new("Tallinn", years, populations);

    // Ahora se tiene el método .city_data() que utiliza un cierre.
    // Se puede hacer lo que se quiera.

    // En primer lugar se unen los 5  primeros años y los habitantes para imprimirlos.
    tallinn.city_data(|city_years, city_populations| { // Los parámetros se pueden llamar como se quiera
        let new_vec = city_years
            .into_iter()
            .zip(city_populations.into_iter()) // Zip los dos valores juntos
            .take(5)                           // pero se queda con los 5 primeros
            .collect::<Vec<(_, _)>>(); // Deja a Rust decidir el tipo de la tupla
        println!("{:?}", new_vec);
    });

    // Ahora se va a añadir algún dato para el año 2030
    tallinn.city_data(|x, y| { // Esta vez se les llama a los parámetros: x, y.
        x.push(2030);
        y.push(500_000);
    });

    // Ahora no se quieren los datos de 1834
    tallinn.city_data(|x, y| {
        let position_option = x.iter().position(|x| *x == 1834);
        if let Some(position) = position_option {
            println!(
                "Going to delete {} at position {:?} now.",
                x[position], position
            ); // Confirma que se está borrando el elemento apropiado
            x.remove(position);
            y.remove(position);
        }
    });

    println!(
        "Years left are {:?}\nPopulations left are {:?}",
        tallinn.years, tallinn.populations
    );
}

Esto imprimirá el valor de todas las veces que se ha llamado a .city_data()::

[(1372, 3250), (1834, 15300), (1851, 24000), (1881, 45900), (1897, 58800)]
Going to delete 1834 at position 1 now.
Years left are [1372, 1851, 1881, 1897, 1925, 1959, 1989, 2000, 2005, 2010, 2020, 2030]
Populations left are [3250, 24000, 45900, 58800, 119800, 283071, 478974, 400378, 401694, 406703, 437619, 500000]

impl Trait

imple Trait es similar a los genéricos. Los genéricos usan un tipo, representado (por convenio) por T o similar, que se decide en tiempo de compilación.

En primer lugar, se presenta un ejemplo con un tipo concreto:

fn gives_higher_i32(one: i32, two: i32) {
    let higher = if one > two { one } else { two };
    println!("{} is higher.", higher);
}

fn main() {
    gives_higher_i32(8, 10);
}

Que imprime: 10 is higher..

En este caso, la función solo recibe el tipo i32. A continuación, se modifica el código para hacerlo genérico. El código de la función necesita comparar y necesita imprimir con {}, por lo que el tipo T necesita implementar los rasgos PartialOrd y Display. Se debe recordar que esto significa que solo es posible implementar esta función para aquellos tipos que tengan estos rasgos.

use std::fmt::Display;

fn gives_higher_i32<T: PartialOrd + Display>(one: T, two: T) {
    let higher = if one > two { one } else { two };
    println!("{} is higher.", higher);
}

fn main() {
    gives_higher_i32(8, 10);
}

El caso de impl Trait es similar, pero decide en tiempo de ejecución. Permite pasar variables de los tipos que implementen el rasgo, pero el código de la función es uno solo (no se desarrolla uno para cada tipo en tiempo de ejecución). El código es un poco menos eficiente, ya que necesita determinar en tiempo de ejecución si el tipo que se pasa implementa o no los rasgos necesarios.

fn prints_it(input: impl Into<String> + std::fmt::Display) { // Takes anything that can turn into a String and has Display
    println!("You can print many things, including {}", input);
}

fn main() {
    let name = "Tuon";
    let string_name = String::from("Tuon");
    prints_it(name);
    prints_it(string_name);
}

Sin embargo, lo más interesante de esto es que se puede devolver también impl Trait y que esto permite devolver cierres (hay que recordar que hay tres tipos de cierres, pero cada implementación concreta tiene un tipo concreto único que es de uno de los tres generales). La definición de cada cierre es un rasgo en sí (trait).

Para entenderlo mejor, se puede observar algún método que funciona así en la librería estándar. Por ejemplo, esta es la definición de .map():

#![allow(unused)]
fn main() {
fn map<B, F>(self, f: F) -> Map<Self, F>     // 🚧
    where
        Self: Sized,
        F: FnMut(Self::Item) -> B,
    {
        Map::new(self, f)
    }
}

fn map<B, F>(self, f: F) significa que esta función necesita dos tipos genéricos. F es una función que recibe como parámetro un elemento del contenedor que implementa .map() y B es el valor de retorno de esta función. En el where se observan los ragos que deben tener las diferentes variables y tipos. Self tiene que ser un tipo Sized y F tiene que ser una función/cierre FnMut.

Así, se puede hacer lo mismo para devolver un cierre. Se usa impl y la definición del cierre. Una vez devuelto, se puede usar como cualquier otra función. A continuación se presenta un ejemplo de una función que devuelve diferentes cierres en función del texto que reciba. Si se pasa "double" o "triple" devuelve un cierre que multiplica por dos o por tres. En todos los demás casos, usa un cierre que devuelve el mismo valor:

fn returns_a_closure(input: &str) -> impl FnMut(i32) -> i32 {
    match input {
        "double" => |mut number| {
            number *= 2;
            println!("Doubling number. Now it is {}", number);
            number
        },
        "triple" => |mut number| {
            number *= 40;
            println!("Tripling number. Now it is {}", number);
            number
        },
        _ => |number| {
            println!("Sorry, it's the same: {}.", number);
            number
        },
    }
}

fn main() {
    let my_number = 10;

    // Make three closures
    let mut doubles = returns_a_closure("double");
    let mut triples = returns_a_closure("triple");
    let mut quadruples = returns_a_closure("quadruple");

    doubles(my_number);
    triples(my_number);
    quadruples(my_number);
}

Por último, se presenta un ejemplo más largo. Se imagina un juego en el que el personaje se enfrenta a monstruos que son más fuertes por la noche. Se puede crear un enumerado denominado TimeOfday para conocer el momento del día. El personaje se llama Simón y tiene un número que se denomina character_fear, que es un f64. El número sube por la noche (tiene más miedo por la noche) y baja durante el día. Se construye una función change_fear que cambia el el miedo en función del momento del día y hace otras cosas como imprimir mensajes:

enum TimeOfDay { // enum
    Dawn,
    Day,
    Sunset,
    Night,
}

fn change_fear(input: TimeOfDay) -> impl FnMut(f64) -> f64 { // la función recibe TimeOfDay. Devuelve un closure.
                // Se usa impl FnMut(64) -> f64 para indicar la función que
                // cambiará el valor
    use TimeOfDay::*; // Para que sea más corto de escribir Dawn, Day, Sunset, Night
                      // En lugar de TimeOfDay::Dawn, TimeOfDay::Day, etc.
    match input {
        Dawn => |x| { // x representa a la variable character_fear que se pasará después
            println!("The morning sun has vanquished the horrible night. You no longer feel afraid.");
            println!("Your fear is now {}", x * 0.5);
            x * 0.5
        },
        Day => |x| {
            println!("What a nice day. Maybe put your feet up and rest a bit.");
            println!("Your fear is now {}", x * 0.2);
            x * 0.2
        },
        Sunset => |x| {
            println!("The sun is almost down! This is no good.");
            println!("Your fear is now {}", x * 1.4);
            x * 1.4
        },
        Night => |x| {
            println!("What a horrible night to have a curse.");
            println!("Your fear is now {}", x * 5.0);
            x * 5.0
        },
    }
}

fn main() {
    use TimeOfDay::*;
    let mut character_fear = 10.0; // Comienza Simon a 10

    let mut daytime = change_fear(Day); // Crea cuatro cierres que cambian el nivel de miedo de Simon.
    let mut sunset = change_fear(Sunset);
    let mut night = change_fear(Night);
    let mut morning = change_fear(Dawn);

    character_fear = daytime(character_fear); // Llama a los cierres. Cambian el miedo y escriben un mensaje
    character_fear = sunset(character_fear);
    character_fear = night(character_fear);
    character_fear = morning(character_fear);
}

Que imprime:

What a nice day. Maybe put your feet up and rest a bit.
Your fear is now 2
The sun is almost down! This is no good.
Your fear is now 2.8
What a horrible night to have a curse.
Your fear is now 14
The morning sun has vanquished the horrible night. You no longer feel afraid.
Your fear is now 7

Arc

Según se ha visto, para permitir que una variable tuviera más de un dueño de forma segura, se utiliza Rc. Si se quiere hacer lo mismo compartiendo la propiedad de una variable entre diferentes hilos, se debe usar Arc. Arc significa: contador de referencias atómico. En este contexto, atómico se refiere a que solo un proceso de los existentes puede escribir en él cada vez. Esto es importante ya que si dos hilos escribieran a la vez, podrían darse resultados erróneos. Por ejemplo, si se pudiera hacer esto en Rust:

#![allow(unused)]
fn main() {
// 🚧
let mut x = 10;

for i in 0..10 { // Hilo 1
    x += 1
}
for i in 0..10 { // Hilo 2
    x += 1
}
}

Si el hilo 1 y el hilo 2 comienzan a la vez y se permitiera escribir simultáneamente, podría darse lo siguiente:

  • El hilo 1 lee 10 y escribe 11. Posteriormente, el hilo 2 lee 11 y escribe 12. Esto no causa problema.
  • El hilo 1 lee 12 y a la vez el hilo 2 lee 12. El hilo 1 escribe 13 y el hilo 2 escribe 13. Se ha perdido un incremento, lo que es un problema grave.

El tipo Arc se asegura de que esto no suceda y es lo que se debe usar para compartir valores entre hilos. Si no hay hilos, es suficiente con usar Rc que, además, es ligeramente más rápido.

Para poder modificar los valores de un Arc no es suficiente con él. Se necesita envolver los datos en un Mutex que es lo que se compartirá entre hilos con Arc.

En el siguiente ejemplo, se va a usar un Mutex dentro de un Arc para modificar un valor de un número entre hilos. En primer lugar, se muestra el primer hilo:

fn main() {

    let handle = std::thread::spawn(|| {
        println!("The thread is working!") // Just testing the thread
    });

    handle.join().unwrap(); // Make the thread wait here until it is done
    println!("Exiting the program");
}

Por ahora, solo imprime:

The thread is working!
Exiting the program

Bien, a continuación se incluye un bucle en el hilo:

fn main() {

    let handle = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("The thread is working!")
        }
    });

    handle.join().unwrap();
    println!("Exiting the program");
}

También funciona, su ejecución resulta en:

The thread is working!
The thread is working!
The thread is working!
The thread is working!
The thread is working!
Exiting the program

Ahora se crea otro hilo que hará lo mismo. La forma en que se ordena la impresión puede ser diferente cada vez, según sea la ejecución paralela de ambos hilos. Se ejecutan de forma concurrente, que significa que se ejecutan a la vez.

fn main() {

    let thread1 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 1 is working!")
        }
    });

    let thread2 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 2 is working!")
        }
    });

    thread2.join().unwrap();
    thread1.join().unwrap();
    println!("Exiting the program");
}

Que podría imprimir diferentes resultados cada vez (dependiendo de la velocidad de ejecución de cada hilo), como por ejemplo:

Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Exiting the program

Ahora se trata de que cada hilo cambie el valor de my_number que puede ser un i32. Para que se pueda compartir entre hilos, se debe definir así:

#![allow(unused)]
fn main() {
// 🚧
let my_number = Arc::new(Mutex::new(0));
}

Después, se deben clonar (que realmente solo clona el puntero), para pasar cada clon a cada hilo.

#![allow(unused)]
fn main() {
// 🚧
let my_number = Arc::new(Mutex::new(0));

let my_number1 = Arc::clone(&my_number); // Este clon va al hilo 1
let my_number2 = Arc::clone(&my_number); // Este clon va al hilo 2
}

Ahora se pueden mover (move) a cada hilo:

use std::sync::{Arc, Mutex};

fn main() {
    let my_number = Arc::new(Mutex::new(0));

    let my_number1 = Arc::clone(&my_number);
    let my_number2 = Arc::clone(&my_number);

    let thread1 = std::thread::spawn(move || { // El clon va al hilo 1
        for _ in 0..10 {
            *my_number1.lock().unwrap() +=1; // Bloquea el Mutex y cambia el valor
        }
    });

    let thread2 = std::thread::spawn(move || { // El clon va al hilo 2
        for _ in 0..10 {
            *my_number2.lock().unwrap() += 1;
        }
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
    println!("Value is: {:?}", my_number);
    println!("Exiting the program");
}

El programa da como resultado:

Value is: Mutex { data: 20 }
Exiting the program

Para simplificar el código, se puede unificar el código de cada hilo (ya que es idéntico):

use std::sync::{Arc, Mutex};

fn main() {
    let my_number = Arc::new(Mutex::new(0));
    let mut handle_vec = vec![]; // los JoinHandles irán aquí

    for _ in 0..2 { // se hace dos veces
        let my_number_clone = Arc::clone(&my_number); // se clona antes de iniciar el hilo
        let handle = std::thread::spawn(move || { // se mueve el clon
            for _ in 0..10 {
                *my_number_clone.lock().unwrap() += 1;
            }
        });
        handle_vec.push(handle); // se guarda el manejador para poder hacer join cuando estén lanzados los dos hilos.
            //si no lo hiciéramos, este manejador se perdería
    }

    handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // se llama al join de todos los hilos lanzados (dos, en este caso)
    println!("{:?}", my_number);
}

Esto imprime: { data: 20 }.

Aunque parece complejo Arc<Mutex<AlgunTipo>> es algo que se usa mucho en Rust y se vuelve natural. Siempre se puede escribir el código para que quede más claro. A continuación, se muestra el mismo código, pero añadiendo un use y dos funciones. Así el código de main() queda conciso y claro:

use std::sync::{Arc, Mutex};
use std::thread::spawn; // Así solo hace falta escribir spawn

fn make_arc(number: i32) -> Arc<Mutex<i32>> { // una función para crear un Mutex en un Arc
    Arc::new(Mutex::new(number))
}

fn new_clone(input: &Arc<Mutex<i32>>) -> Arc<Mutex<i32>> { // para crear clones
    Arc::clone(&input)
}

// Ahora main() se lee más fácil
fn main() {
    let mut handle_vec = vec![]; // los manejadores de hilos se guardan aquí
    let my_number = make_arc(0);

    for _ in 0..2 {
        let my_number_clone = new_clone(&my_number);
        let handle = spawn(move || {
            for _ in 0..10 {
                let mut value_inside = my_number_clone.lock().unwrap();
                *value_inside += 1;
            }
        });
        handle_vec.push(handle);    // se guarda el manejador
    }

    handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // se espera a la finalización de los hilos

    println!("{:?}", my_number);
}

En todo caso, siempre se puede reescribir el código que parezca difícl de leer.

Canales

Un canal es una forma fácil de utilizar muchos hilos que envían información a un único punto. Son muy populares porque son simples de usar. En Rust, se puede crear un canal mediante std::sync::mpsc. mpsc significa "múltiple productor, solo un consumidor". Es decir, "muchos hilos enviando a un único lugar". Para iniciar un canal se utiliza channel(), que sirve para rear un Sender (un proceso que envía) y un Receiver (un proceso que recibe) que están unidos. Se puede observar en la declaración de la función:


#![allow(unused)]
fn main() {
// 🚧
pub fn channel<T>() -> (Sender<T>, Receiver<T>)
}

Normalmente, se asignan a unas variables cada elemento de la tupla con nombres como sender y receiver. O, en español, emisor y receptor. Como se trata de una función genérica, si se escribe solo lo siguiente:

use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel(); // ⚠️
}

Rust no conocerá el tipo que se envía y recibe. Así que el compilador indica:

error[E0282]: type annotations needed for `(std::sync::mpsc::Sender<T>, std::sync::mpsc::Receiver<T>)`
  --> src\main.rs:30:30
   |
30 |     let (sender, receiver) = channel();
   |         ------------------   ^^^^^^^ cannot infer type for type parameter `T` declared on the function `channel`
   |         |
   |         consider giving this pattern the explicit type `(std::sync::mpsc::Sender<T>, std::sync::mpsc::Receiver<T>)`, where
the type parameter `T` is specified

Que indica que es necesario añadir un tipo para el Sender y el Receiver. Se puede hacer de la siguiente forma:

use std::sync::mpsc::{channel, Sender, Receiver};

fn main() {
    let (sender, receiver): (Sender<i32>, Receiver<i32>) = channel();
}

o

use std::sync::mpsc::{channel, Sender, Receiver};

fn main() {
    let (sender, receiver) = channel::<i32>();
}

o simplemente, se envía algo a través del Sender en el código, para que Rust pueda inferir su tipo:

use std::sync::mpsc::{channel, Sender, Receiver};

fn main() {
    let (sender, receiver) = channel();
    sender.send(5);
    receiver.recv(); //recv es la función que sirve para recibir el valor. En este caso, no se usa.
}

Así, el compilador conoce el tipo. sender es de tipo Result<(), SendError<i32>> y receiver es de tipo Result<(), RecvError>. Se puede usar unwrap() para ver si el envío ha funcionado. Por ejemplo, se el siguiente código comprueba si ha funcionado el envío:

use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel();

    sender.send(5).unwrap();
    println!("{}", receiver.recv().unwrap());
}

Que imprime 5.

Un canal es como un Arc ya que se puede clonar y enviar los clones a otros hilos. En el siguiente ejemplo se crean dos hilos que envían valores a un receiver. Este código funcionará, aunque no exactamente como se pretende:

use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel();
    let sender_clone = sender.clone();

    std::thread::spawn(move|| { // mueve un sender 
        sender.send("Envía un &str esta vez").unwrap();
    });

    std::thread::spawn(move|| { // mueve un sender_clone
        sender_clone.send("Y aquí otro &str").unwrap();
    });

    println!("{}", receiver.recv().unwrap());   
}

Los dos hilos comienzan a enviar (en cualquier orden) y después se imprime lo primero que se recibe. Por lo tanto, el resultado de la impresión puede variar cada vez, dependiendo del hilo que envíe primero.

Para ver el resultado completo de cada hilo, resulta conveniente guardar los manejadores de ejecutar tantas recepciones como sean necesarias.

use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel();
    let sender_clone = sender.clone();
    let mut handle_vec = vec![]; // Se guardan los manejadores

    handle_vec.push(std::thread::spawn(move|| {  // push del primero
        sender.send("Envía un &str esta vez").unwrap();
    }));

    handle_vec.push(std::thread::spawn(move|| {  // y push del segundo
        sender_clone.send("Y aquí otro &str").unwrap();
    }));

    for _ in handle_vec { // ahora handle_vec tiene 2 elementos. Como cada uno envía un dato, se hacen tantos print como manejadores
        println!("{:?}", receiver.recv().unwrap());
    }
}

El orden de los str puede variar en función del orden en que se ejecutan los envíos.

En el siguiente ejemplo, se crea un vector con los resultados, en lugar de imprimirlos directamente:

use std::sync::mpsc::channel;

fn main() {
    let (sender, receiver) = channel();
    let sender_clone = sender.clone();
    let mut handle_vec = vec![]; // Se guardan los manejadores
    let mut results_vec = vec![];

    handle_vec.push(std::thread::spawn(move|| {  // push del primero
        sender.send("Envía un &str esta vez").unwrap();
    }));

    handle_vec.push(std::thread::spawn(move|| {  // y push del segundo
        sender_clone.send("Y aquí otro &str").unwrap();
    }));

    for _ in handle_vec { // ahora handle_vec tiene 2 elementos. Como cada uno envía un dato, alamcenan tantos como manejadores
        results_vec.push(receiver.recv().unwrap());
    }

    println!("{:?}", results_vec);
}

El resultado del vector es ["Envía un &str esta vez", "Y aquí otro &str"].

El siguiente ejemplo utiliza diez hilos para modificar un vector de un millón de elementos. Al comienzo, todos los elementos del vector valen cero. Se modifican todos los valores a 1. Cada hilo hará la décima parte del trabjo. Se creará un nuevo vector y se usará .extend() para guardar el resultado de cada hilo.

use std::sync::mpsc::channel;
use std::thread::spawn;

fn main() {
    let (sender, receiver) = channel();
    let hugevec = vec![0; 1_000_000];
    let mut newvec = vec![];
    let mut handle_vec = vec![];

    for i in 0..10 {
        let sender_clone = sender.clone();
        let mut work: Vec<u8> = Vec::with_capacity(hugevec.len() / 10); // el nuevo vec en el que guardar el resultado de la décima parte del trabajo
        work.extend(&hugevec[i*100_000..(i+1)*100_000]); // el primero trabaja de 0..100_000, el siguiente de 100_000..200_000, etc.
        let handle =spawn(move || { // crea un manejador

            for number in work.iter_mut() { // efectúa el trabajo
                *number += 1;
            };
            sender_clone.send(work).unwrap(); // utiliza el sender_clone para enviar el trabajo al receptor
        });
        handle_vec.push(handle);
    }
    
    for handle in handle_vec { // espera a que todos los hilos terminen
        handle.join().unwrap();
    }
    
    while let Ok(results) = receiver.try_recv() {
        newvec.push(results); // guarda los resultados en receiver.recv() en el vector final
    }

    // Ahora se tiene un Vec<Vec<u8>>. se debe aplanar con .flatten()
    let newvec = newvec.into_iter().flatten().collect::<Vec<u8>>(); // Ahora es un vector de 1_000_000 u8 números. El orden de cada décima parte puede no coincidir aunque en este caso no se nota debido a que todos valen cero inicialmente
    
    println!("{:?}, {:?}, longitud total: {}", // se imprimen algunos para comprobar que contienen un 1
        &newvec[0..10], &newvec[newvec.len()-10..newvec.len()], newvec.len() // y se muestra que la longitud es de  1_000_000 de elementos
    );
    
    for number in newvec { // Si algúnvalor no fuese 1 se entra en pánico
        if number != 1 {
            panic!();
        }
    }
}

Para que este ejemplo mantuviese el orden de los elementos originales (en caso de no fuesen todos con el mismo valor), sería necesario que también se trabajara con un índice de cada sección para reordenar en el resultado antes de aplanar el vector final.

Entender la documentación de Rust

Es importante conocer cómo leer la documentación de Rust para poder comprender lo que otras personas han escrito. A continuación, se observan algunas cosas que es es conveniente conocer de la documentación de Rust:

assert_eq!

Como se ha visto, assert_eq! se utiliza durante las pruebas. Se pasan dos elementos y la aplicación entra en pánico si no son iguales entre sí. A continuación, se muestra un ejemplo simple en el que se necesita un número par.

fn main() {
    prints_number(56);
}

fn prints_number(input: i32) {
    assert_eq!(input % 2, 0); // el resto debe ser cero para que sea par.
                              // Si el resultado de number % 2 si no es 0, entra en pánico
    println!("El número no es impar. Es {}", input);
}

Puede que no se quiera usar assert_eq! en el código que se escriba, pero está en todas partes de la documentación de Rust. Esto es debido a que en un documento se requiera mucho espacio para usar println! para todo. Además, se necesitaría que los elementos dispusieran del rasgo Display o Debug para todo lo que se quiera imprimir. Por eso la documentación utiliza assert_eq!. El siguiente es un ejmpleo de https://doc.rust-lang.org/std/vec/struct.Vec.html que muestra cómo utilizar Vec:

fn main() {
    let mut vec = Vec::new();
    vec.push(1);
    vec.push(2);

    assert_eq!(vec.len(), 2);
    assert_eq!(vec[0], 1);

    assert_eq!(vec.pop(), Some(2));
    assert_eq!(vec.len(), 1);

    vec[0] = 7;
    assert_eq!(vec[0], 7);

    vec.extend([1, 2, 3].iter().copied());

    for x in &vec {
        println!("{}", x);
    }
    assert_eq!(vec, [7, 1, 2, 3]);
}

En estos ejemplos basta con leer assert_eq(a, b) como que "a es b". A continuación, se muestra el mismo ejemplo con comentarios que indican lo que significa cada fila.

fn main() {
    let mut vec = Vec::new();
    vec.push(1);
    vec.push(2);

    assert_eq!(vec.len(), 2); // "La longitud del vec es 2"
    assert_eq!(vec[0], 1); // "vec[0] es 1"

    assert_eq!(vec.pop(), Some(2)); // "Cuando se usa .pop(), se obtiene un Some()"
    assert_eq!(vec.len(), 1); // "La longitud del vec ahora es 1"

    vec[0] = 7;
    assert_eq!(vec[0], 7); // "Vec[0] es 7"

    vec.extend([1, 2, 3].iter().copied());

    for x in &vec {
        println!("{}", x);
    }
    assert_eq!(vec, [7, 1, 2, 3]); // "El vec contiene ahora [7, 1, 2, 3]"
}

Búsquedas

La barra superior en la documentación de Rust es la barra de búsqueda. Muestra los resultados según se teclea. Cuando se avanza en una página, la barra de búsqueda desaparece, pero si se pulsa la tecla s en el teclado vuelve a aparecer. Esto permite buscar algo de forma rápida.

El enlace [source]

Normalmente, el código de un método, struct, etc. No estará completo. Esto se debe a que normalmente no se necesita ver todo el código para conocer como funciona. De hecho, el código completo puede hacer más difícil entender el objetivo de una explicación. Pero si se quiere ver, se puede pulsar en [source] y se mostrará todo el código. Por ejemplo, en la página dedicada a String se ver la declaración de .with_capacity():

#![allow(unused)]
fn main() {
// 🚧
pub fn with_capacity(capacity: usize) -> String
}

Este método recibe un número y obtiene una String. Si se quiere conocer más, se puede pulsar en el enlace [source] que muestra:

#![allow(unused)]
fn main() {
// 🚧
pub fn with_capacity(capacity: usize) -> String {
    String { vec: Vec::with_capacity(capacity) }
}
}

Ahora se puede observar que una String es un tipo de Vec. Realmente, un String es un vector de u8 bytes, lo que resulta interesante conocer. Para utilizar el método with_capacity no es necesario conocer este detalle, por eso solo se ve si se pulsa [source]. Así que es una buena idea pulsarlo cuando se quiere conocer los detalles de algo en la documentación.

Información sobre rasgos

La parte importante de la documentación de los rasgos es la de los "Métodos necesitados" (Required Methods) a la izquierda. Cuando en un rasgo existe un apartado a la izquierda con métodos necesitados, es probable que signifique que es necesario escribirlos. Por ejemplo, para Iterator se necesita escribir el método .next(). Y para el rasgo From se necesita escribir el método .from(). Algunos rasgos se pueden implementar con solo utilizar un atributo, como sucede con el rasgo Debug que basta con usar #[derive(Debug)]. Debug necesita el método .fmt(), pero normalmente no es necesario escribirlo ya que el atributo #[derive(Debug)] resulta suficiente para ello. Por eso, la página de std::fmt::Debug dice que "en general, debería ser suficiente con derivar la implementación de Debug".

Atributos

Ya se ha visto anteriormente código como este #[derive(Debug)]. Este tipo de código es un atributo. Los atributos son pequeñas piezas de código que dan información al compilador. No son fáciles de crear, pero son muy fáciles de usar. Un atributo puede comenzar con solo #, lo que significa que solo afecta al código de la siguiente línea. Sin embargo, si comienza con #! afectará a todo lo que esté en su espacio.

Hay atributos que aparecen mucho:

#[allow(dead_code)] y #[allow(unused_variables)]. Si el fichero contiene código que no se utiliza, Rust compilará, pero avisará. Por ejemplo, el siguiente código contiene un struct vacío y una variable. Ninguno se usa en el código:

struct JustAStruct {}

fn main() {
    let some_char = 'ん';
}

Rust se queja e indica que no se usan:

warning: unused variable: `some_char`
 --> src\main.rs:4:9
  |
4 |     let some_char = 'ん';
  |         ^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_some_char`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: struct is never constructed: `JustAStruct`
 --> src\main.rs:1:8
  |
1 | struct JustAStruct {}
  |        ^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

Se ha visto que se puede utilizar un guión bajo para indicar que no se usa _:

struct _JustAStruct {}

fn main() {
    let _some_char = 'ん';
}

Pero también se puede utilizar un atributo. El propio mensaje de Rust indica que tiene activo los atributos #[warn(unused_variables)] y #[warn(dead_code)]. En el código anterior, JustAStruct es código muerto y some_char es una variable sin usar. El atributo opuesto a warn es allow, por lo que se puede escribire en el código y Rust dejará de avisar para estos casos:

#![allow(dead_code)]
#![allow(unused_variables)]

struct Struct1 {} // Crea cinco structs
struct Struct2 {}
struct Struct3 {}
struct Struct4 {}
struct Struct5 {}

fn main() {
    let char1 = 'ん'; // y cuatro variables. No se usa ninguno de ellos, pero el compilador ya no da ningún mensaje
    let char2 = ';';
    let some_str = "I'm just a regular &str";
    let some_vec = vec!["I", "am", "just", "a", "vec"];
}

Es importante tener en cuenta el código muerto y las variables sin uso, pero en ocasiones puede ser necesario que el compilador deje de avisar durante un tiempo. Se puede necesitar desarrollar el código o enseñar a alguien y no se quiere confundir con excesivos mensajes.

El atributo #[derive(NombreDeRasfgo)] permite derivar algunos rasgos para los struct y enum que se creen. Diversos rasgos de uso común, como Debug, pueden derivarse de esta forma. Otros, como Display, no se pueden derivar. En el caso de Display es necesari que se dedica por parte del desarrollador cómo se quiere mostrar el elemento.

// ⚠️
#[derive(Display)]
struct HoldsAString {
    the_string: String,
}

fn main() {
    let my_string = HoldsAString {
        the_string: "¡Aquí estoy!".to_string(),
    };
}

El mensaje de error lo indica bien claro:

error: cannot find derive macro `Display` in this scope
 --> src/main.rs:2:10
  |
2 | #[derive(Display)]
  |          ^^^^^^^

Pero en los casos de rasgos que se pueden derivar, se pueden indicar tantos como se necesite. En el siguiente ejemplo se le dan siete rasgos diferentes a un struct, solo por gusto, aunque solo se necesite uno en este caso.

#[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Clone)]
struct HoldsAString {
    the_string: String,
}

fn main() {
    let my_string = HoldsAString {
        the_string: "¡Aquí estoy!".to_string(),
    };
    println!("{:?}", my_string);
}

Se puede derivar Copy, pero solo en los casos que un elemento solo contiene elementos que también tienen el rasgo Copy. En el ejemplo anterior no es posible puesto que String no tiene dicho rasgo y el struct HodsAString contiene una variable de dicho tipo. Sin embargo en el siguiente ejemplo sí se puede derivar Copy al contener tipos que disponen de dicho rasgo:

#[derive(Clone, Copy)] // Se necesita Clone para usar Copy
struct NumberAndBool {
    number: i32, // i32 es Copy
    true_or_false: bool // bool es también Copy. Por lo que no es problema
}

fn does_nothing(input: NumberAndBool) {

}

fn main() {
    let number_and_bool = NumberAndBool {
        number: 8,
        true_or_false: true
    };

    does_nothing(number_and_bool);
    does_nothing(number_and_bool); // Sin Copy, esta fila daría error
}

El atributo #[cfg()] significa configuración e indica al compilador si ejecutar código o no. Normalmente, se encuentra de la siguiente forma #[cfg(test)]. Se usa cuando se escriben funciones de prueba para que el compilador solo las ejecute cuando se está probando. Así, el código de prueba puede estar junto al código del programa sin que el compilador lo ejecute, salvo cuando se le indica.

Otro ejemplo del uso de cfg es #[cfg(target_os = "windows")]. Que indica que solo se ejecute el código en windows (o linux u otro sistema).

El atributo #![no_std] indica a Rust que no incorpore la librería estándar. Esto implica que no se dispone de Vec, String y todo lo que aporta esta librería. Es útil cuando no es necesaria y el código tiene que ejecutarse en pequeños dispositivos con poca memoria.

Los diferentes atributos disponibles se pueden consultar aquí.

Box

Box se trata de un tipo muy util en Rust. Permite almacenar en el heap (el montón) un valor, en lugar de almacenarlo en la pila. Para crear un elemento de este tipo se usa Box::new() con el elemento como parámetro.

fn just_takes_a_variable<T>(item: T) {} // Toma cualquier variable y la olvida.

fn main() {
    let my_number = 1; // Este es de tipo i32
    just_takes_a_variable(my_number);
    just_takes_a_variable(my_number); // no hay problema en usarla dos veces porque el tipo permite Copy

    let my_box = Box::new(1); // Este es de tipo Box<i32>
    just_takes_a_variable(my_box.clone()); // Sin .clone() la segunda función daría error
    just_takes_a_variable(my_box); // debido a que Box no dispone de Copy
}

Al principio, resulta difícil pensar en la utilidad de este tipo, pero se usa mucho en Rust. Si se recuerda, & se usa para los str debido a que el compilador no conoce su tamaño: str puede ser de cualquier longitud. Y la referencia & sí que tiene un tamaño conocido, siempre igual. Box se comporta de forma similar. Además, * sirve para extraer el valor de un Box. Igual que sucede con &:

fn main() {
    let my_box = Box::new(1); // Este es Box<i32>
    let an_integer = *my_box; // Este es i32
    println!("{:?}", my_box);
    println!("{:?}", an_integer);
}

Por este motivo se denomina "puntero inteligente" a Box, porque es como una referencia (un tipo de puntero), pero facilita otro conjunto de cosas.

Se puede usar Box para crear un struct con la misma estructura en su interior. A esto se le denomina estructura recursiva. Es decir, que dentro de un Struc A puede haber otro Struct A. En ocasiones, se puede necesitar Box para crear listas enlazadas, aunque este tipo de listas no son muy populares en Rust. A continuación se muestra un ejemplo que muestra lo que sucede si se intenta crear una estructura recursiva sin Box:

#![allow(unused)]
fn main() {
struct List {
    item: Option<List>, // ⚠️
}
}

Este struct simple contiene un único elemento que puede ser Some<List> o None. Debido a que se puede incorporar este último, el struct no es recursivo hasta el infinito. Pero el compilador no puede conocer su tamaño para poder depositar sus elementos en la pila:

error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:3:1
  |
3 | struct List {
  | ^^^^^^^^^^^ recursive type has infinite size
4 |     item: Option<List>, // ⚠️
  |           ------------ recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable

Se observa que el propio compilador sugiere que se use Box. Si se aplica este cambio:

struct List {
    item: Option<Box<List>>,
}
fn main() {}

Ahora el compilador acepta el struct List, debido a que la recursividad se encuentra detrás de un Box y el tamaño de este es conocido.

Un lista simple podría definirse así:

struct List {
    item: Option<Box<List>>,
}

impl List {
    fn new() -> List {
        List {
            item: Some(Box::new(List { item: None })),
        }
    }
}

fn main() {
    let mut my_list = List::new();
}

Incluso sin datos es un poco complejo. Rust no usa este tipo de patrón muy a menudo. Esto se debe a las estrictas reglas de propiedad y préstamo que tiene Rust. En todo caso, si se necesita un tipo cualquiera de lista enlazada, Box puede ayudar.

Box permite el uso de std::mem::drop sobre un elemento de este tipo ya que el elemento se encuentra en el heap (montón). Esto puede ser útil en ocasiones.

Box y los rasgos

Box es muy útil para devolver rasgos. Como ya se conoce, se pueden escribir rasgos en funciones genéricas como la del siguiente ejemplo:

use std::fmt::Display;

struct DoesntImplementDisplay {}

fn displays_it<T: Display>(input: T) {
    println!("{}", input);
}

fn main() {}

Como la función displays_it solo recibe un elemento que implemente Display, no puede aceptar elementos del tipo struct DoesntImplementDisplay. Sin embargo, puede recibir como parámetro otros elementos como String.

También se ha visto que se puede usar impl Trait para devolver otros rasgos o cierres. Box se puede usar de una forma similar ya que permite que el compilador conozca el tamaño del elemento a devolver. El siguiente ejemplo muestra que un rasgo puede usarse en un elemento de cualquier tamaño:

#![allow(dead_code)] // Le dice al compilador que no se queje con código muerto
use std::mem::size_of; // Es función devuelve el tamaño de un tipo determinado

trait JustATrait {} // Se implementará este rasgo en todos los tipos

enum EnumOfNumbers {
    I8(i8),
    AnotherI8(i8),
    OneMoreI8(i8),
}
impl JustATrait for EnumOfNumbers {}

struct StructOfNumbers {
    an_i8: i8,
    another_i8: i8,
    one_more_i8: i8,
}
impl JustATrait for StructOfNumbers {}

enum EnumOfOtherTypes {
    I8(i8),
    AnotherI8(i8),
    Collection(Vec<String>),
}
impl JustATrait for EnumOfOtherTypes {}

struct StructOfOtherTypes {
    an_i8: i8,
    another_i8: i8,
    a_collection: Vec<String>,
}
impl JustATrait for StructOfOtherTypes {}

struct ArrayAndI8 {
    array: [i8; 1000], // Este va a ser muy largo
    an_i8: i8,
    in_u8: u8,
}
impl JustATrait for ArrayAndI8 {}

fn main() {
    println!(
        "{}, {}, {}, {}, {}",
        size_of::<EnumOfNumbers>(),
        size_of::<StructOfNumbers>(),
        size_of::<EnumOfOtherTypes>(),
        size_of::<StructOfOtherTypes>(),
        size_of::<ArrayAndI8>(),
    );
}

Cuando se imprime el resultado del tamaño de estos objetos se obtiene 2, 3, 32, 1002. Por ello, si se devolviera lo siguiente, daría error:

#![allow(unused)]
fn main() {
// ⚠️
fn returns_just_a_trait() -> JustATrait {
    let some_enum = EnumOfNumbers::I8(8);
    some_enum
}
}

El compilador dice:

error[E0746]: return type cannot have an unboxed trait object
  --> src\main.rs:53:30
   |
53 | fn returns_just_a_trait() -> JustATrait {
   |                              ^^^^^^^^^^ doesn't have a size known at compile-time

Debido a que el rasgo puede implementarse por objetos de diverso tamaño, el compilador se queja. Para que funcione, es necesario guardar el objeto en un Box. Además, hace falta indicar dyn, que sirve para indicar que se está devolviendo en tiempo de ejecución un elemento de diverso tipo, que está representado por un rasgo que sí que tienen que tener los valores a devolver (N.T.: Es una forma de polimorfismo).

La nueva función queda así:

#![allow(unused)]
fn main() {
// 🚧
fn returns_just_a_trait() -> Box<dyn JustATrait> {
    let some_enum = EnumOfNumbers::I8(8);
    Box::new(some_enum)
}
}

Ahora funciona. En la pila se puede incorporar el elemento Box de tamaño fijo.

Esta forma de código se encuentra mucho en la forma de Box<dyn Error>, debido a que en ocasiones puede haber diversos tipos de error.

El siguiente ejemplo muestra dos tipos de error. Para crear un elemento de tipo Error que sea oficial, se debe implementar el rasgo std::error::Error. Esto es fácil, simplemente se escribe impl std::error::Error {}. Los tipos de error también tienen que implementar Debug y Display para que puedan mostrar información sobre el problema. Debug es fácil de implementar, basta con #[derive(Debug)], pero Display necesita el método .fmt(). Esto ya se ha codificado antes:

use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct ErrorOne;

impl Error for ErrorOne {} // Ahora ya es un tipo de eroro con Debug. Falta Display:

impl fmt::Display for ErrorOne {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "¡Se ha producido el primer error!") // Todo lo que hace es escribir este mensaje
    }
}


#[derive(Debug)] // Se hace lo mismo con ErrorTwo
struct ErrorTwo;

impl Error for ErrorTwo {}

impl fmt::Display for ErrorTwo {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "¡Se ha producido el segundo error!")
    }
}

// Se crea una función que devuelve un String o Errores
fn returns_errors(input: u8) -> Result<String, Box<dyn Error>> { // Con Box<dyn Error> se puede devolver cualquier elemento que implemente el rasgo Error

    match input {
        0 => Err(Box::new(ErrorOne)), // no se puede olvidar que debe estar en una Box
        1 => Err(Box::new(ErrorTwo)),
        _ => Ok("Parece correcto".to_string()), // Este es el tipo de retorno correcto
    }

}

fn main() {

    let vec_of_u8s = vec![0_u8, 1, 80]; // Tres números de prueba

    for number in vec_of_u8s {
        match returns_errors(number) {
            Ok(input) => println!("{}", input),
            Err(message) => println!("{}", message),
        }
    }
}

Este código imprime:

¡Se ha producido el primer error!
¡Se ha producido el segundo error!
Parece correcto

Si no se devolviera Box<dyn Error> se produciría un error al compilar:

#![allow(unused)]
fn main() {
// ⚠️
fn returns_errors(input: u8) -> Result<String, Error> {
    match input {
        0 => Err(ErrorOne),
        1 => Err(ErrorTwo),
        _ => Ok("Parece correcto".to_string()),
    }
}
}

Producirá el siguiente error durante la compilación:

21  | fn returns_errors(input: u8) -> Result<String, Error> {
    |                                 ^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time

Lo que no es sorprendente ya que los rasgos se pueden implementar por muchos tipos diferentes que pueden tener diferente tamaño.

Default y el patrón constructor (builder)

Se puede implementar el rasgo Default para inicializar un struct o enum a los valores que se consideren más habituales. El patrón constructor (builder) se usa de forma combinada con esto para facilitar a los usuarios realizar aquellos cambios que quieran. En primer lugar se va a ver Default. Los tipos generales de Rust ya implementan Default. Son valores bastante habituales: 0, "" (cadenas de caracteres vacía), false, etc.

fn main() {
    let default_i8: i8 = Default::default();
    let default_str: String = Default::default();
    let default_bool: bool = Default::default();

    println!("'{}', '{}', '{}'", default_i8, default_str, default_bool);
}

Lo anterior imprime '0', ''. 'false'.

Así, Default es como la función new, salvo que no hay que darle nigún valor. En primer lugar, se va a mostrar un struct uqe no implemente Default. Tendrá una función new que se usa para crear un personaje denominado Billy con algunas características.

struct Personaje {
    nombre: String,
    edad: u8,
    estatura: u32,
    peso: u32,
    estadovital: EstadoVital,
}

enum EstadoVital {
    Vivo,
    Muerto,
    NuncaVivo,
    Desconocido
}

impl Personaje {
    fn new(nombre: String, edad: u8, estatura: u32, peso: u32, vivo: bool) -> Self {
        Self {
            nombre,
            edad,
            estatura,
            peso,
            estadovital: if vivo { EstadoVital::Vivo } else { EstadoVital::Muerto },
        }
    }
}

fn main() {
    let personaje_1 = Personaje::new("Billy".to_string(), 15, 170, 70, true);
}

Puede que en este mundo, la mayoría de los personajes se llamen Billy, tengan edad de 15 años, estatura de 170, peso de 70 y estén vivos. Para ello, se puede implementar Default de forma que se pueda crear un personaje estándar con solo Personaje::default(). Se haría de la siguiente forma:

#[derive(Debug)]
struct Personaje {
    nombre: String,
    edad: u8,
    estatura: u32,
    peso: u32,
    estadovital: EstadoVital,
}

#[derive(Debug)]
enum EstadoVital {
    Vivo,
    Muerto,
    NuncaVivo,
    Desconocido
}

impl Personaje {
    fn new(nombre: String, edad: u8, estatura: u32, peso: u32, vivo: bool) -> Self {
        Self {
            nombre,
            edad,
            estatura,
            peso,
            estadovital: if vivo { EstadoVital::Vivo } else { EstadoVital::Muerto },
        }
    }
}

impl Default for Personaje {
    fn default() -> Self {
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
        }
    }
}

fn main() {
    let personaje_1 = Personaje::default();

    println!(
        "El personaje {:?} tiene {:?} años.",
        personaje_1.nombre, personaje_1.edad
    );
}

El código anterior imprime El personaje "Billy" tiene 15 años..

El patrón constructor (builder)

A continuación se mostrará cómo utilizar el patrón constructor. Se mantiene el valor por defecto usando el rasgo Default. A veces, algunos personajes solo serán ligeramente diferentes a "Billy". El patrón constructor establece una cadena de métodos para cambiar un valor cada vez. A continuación se muestra uno de estos métodos para el Personaje:

#![allow(unused)]
fn main() {
fn estatura(mut self, estatura: u32) -> Self {    // 🚧
    self.estatura = estatura;
    self
}
}

Se debe prestar atención a que toma como parámetro un mut self. Esto ya se vio anteriormente. No es una referencia &mut self). Toma propiedad de Self y con mut es modificable, aunque no lo fuese antes. Esto es debido a que .estatura() obtiene la completa propiedad y nadie lo puede tocar, por lo que es seguro usar mut y el compilador no se queja. Luego solo le cambia el valor self.estatura y devuelve Self (que es de tipo Personaje).

En el ejemplo, se crean tres métodos constructores. Son muy parecidos:

#![allow(unused)]
fn main() {
fn estatura(mut self, estatura: u32) -> Self {     // 🚧
    self.estatura = estatura;
    self
}

fn peso(mut self, peso: u32) -> Self {
    self.peso = peso;
    self
}

fn nombre(mut self, nombre: &str) -> Self {
    self.nombre = nombre.to_string();
    self
}
}

Cada uno de estos métodos modifica una variable y devuelve Self: esto es lo que define a un patrón constructor. Así, se puede escribir lo siguiente para construir un personaje: let personaje_1 = Personaje::default().estatura(180).peso(60).nombre("Bobby");- Si se está construyendo una librería, esto hace que sea más fácil el crear nuevos elementos ya que queda legible lo que se está haciendo. Con estos cambios, el código queda así:

#[derive(Debug)]
struct Personaje {
    nombre: String,
    edad: u8,
    estatura: u32,
    peso: u32,
    estadovital: EstadoVital,
}

#[derive(Debug)]
enum EstadoVital {
    Vivo,
    Muerto,
    NuncaVivo,
    Desconocido
}

impl Personaje {
    fn new(nombre: String, edad: u8, estatura: u32, peso: u32, vivo: bool) -> Self {
        Self {
            nombre,
            edad,
            estatura,
            peso,
            estadovital: if vivo { EstadoVital::Vivo } else { EstadoVital::Muerto },
        }
    }
    fn estatura(mut self, estatura: u32) -> Self {     
        self.estatura = estatura;
        self
    }

    fn peso(mut self, peso: u32) -> Self {
        self.peso = peso;
        self
    }

    fn nombre(mut self, nombre: &str) -> Self {
        self.nombre = nombre.to_string();
        self
    }
}

impl Default for Personaje {
    fn default() -> Self {
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
        }
    }
}

fn main() {
    let personaje_1 = Personaje::default().estatura(180).peso(60).nombre("Bobby");;

    println!("{:?}", personaje_1);
}

Un último método que se suele añadir es .build(). Este método es una especie de validación final. Por ejemplo, para comprobar que la estatura no contenga un valor de 5000. Con este método build() que debería devolver un Result, se pueden establecer validaciones para comprobar si es correcto el elemento que se está construyendo. Si lo es, se puede devolver Ok(Self).

En primer lugar, se modifica el método new() para que no reciba parámetros. Se evita que los usuarios sean libres de crear cualquier persona con cualquier valor. Los valores que utiliza la implementación de Default se pasan a .new(), que queda así:

#![allow(unused)]
fn main() {
    fn new() -> Self {    // 🚧
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
        }
    }
}

Así, deja de ser necesaria la implementación del rasgo Default. Por tanto, se puede eliminar del código, que queda así:

#[derive(Debug)]
struct Personaje {
    nombre: String,
    edad: u8,
    estatura: u32,
    peso: u32,
    estadovital: EstadoVital,
}

#[derive(Debug)]
enum EstadoVital {
    Vivo,
    Muerto,
    NuncaVivo,
    Desconocido
}

impl Personaje {
    fn new() -> Self {    
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
        }
    }

    fn estatura(mut self, estatura: u32) -> Self {     
        self.estatura = estatura;
        self
    }

    fn peso(mut self, peso: u32) -> Self {
        self.peso = peso;
        self
    }

    fn nombre(mut self, nombre: &str) -> Self {
        self.nombre = nombre.to_string();
        self
    }
}

fn main() {
    let personaje_1 = Personaje::new().estatura(180).peso(60).nombre("Bobby");;

    println!("{:?}", personaje_1);
}

Lo anterior, imprime lo mismo que antes: Personaje { nombre: "Bobby", edad: 15, estatura: 180, peso: 60, estadovital: Vivo }.

Con el código anterior, casi se puede implementar el método build(), pero hay un problema: ¿cómo se fuerza al usuario a que lo utilice después de poner todos los valores de los diferentes atributos? En este momento, el usuario puede escribir solo let x = Personaje::new().estatura(76767); y obtener un Personaje con una estatura imposible. Hay diversas formas de hacerlo. Una de ellas consiste en añadir a Personaje un atributo se_puede_usar: bool.

#![allow(unused)]
fn main() {
    fn new() -> Self {    // 🚧
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
            se_puede_usar: true, // .new() devuelve un elemento válido, se puede usar.
        }
    }
}

Los demás métodos constructores como .estatura(), establecerán se_puede_usar a false. Solo .build() lo volverá a establecer a true. Así, el usuario se verá obligado a llamar a este método de comprobación. Se validará que la estatura no es superior a 250 y el peso no es superior a 300. También se comprobará que los personajes no puedan llamarse smurf.

El método .build() queda como sigue:

#![allow(unused)]
fn main() {
fn build(mut self) -> Result<Personaje, String> {      // 🚧
    if self.estatura < 250 && self.peso < 300 && !self.nombre.to_lowercase().contains("smurf") {
        self.se_puede_usar = true;
        Ok(self)
    } else {
        Err("No se puede crear el Personaje. Los Personajes deben tener:
1) estatura menor a 250
2) peso menor a 300
3) nombre diferente a Smurf (es una palabra fea)"
            .to_string())
    }
}
}

!self.nombre.to_lowercase().contains("smurf") sirve para asegurar que el usuario no escribe variaciones de esta palabra como "SMURF" o "SoySmurf". Pasa la cadena de caracteres a minúscula y comprueba que no contenga el texto. La ! significa no.

Si el personaje es correcto, se activa se_puede_usar a true y se devuelve el personaje dentro del elemento Ok.

Así queda el código completo. Se muestra creando tres personajes que no cumplen los requisitos y un personaje que sí.

#[derive(Debug)]
struct Personaje {
    nombre: String,
    edad: u8,
    estatura: u32,
    peso: u32,
    estadovital: EstadoVital,
    se_puede_usar: bool,
}

#[derive(Debug)]
enum EstadoVital {
    Vivo,
    Muerto,
    NuncaVivo,
    Desconocido
}

impl Personaje {
    fn new() -> Self {    
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
            se_puede_usar: true,
        }
    }

    fn estatura(mut self, estatura: u32) -> Self {     
        self.estatura = estatura;
        self.se_puede_usar = false;
        self
    }

    fn peso(mut self, peso: u32) -> Self {
        self.peso = peso;
        self.se_puede_usar = false;
        self
    }

    fn nombre(mut self, nombre: &str) -> Self {
        self.nombre = nombre.to_string();
        self.se_puede_usar = false;
        self
    }

    fn build(mut self) -> Result<Personaje, String> { 
        if self.estatura < 250 && self.peso < 300 && !self.nombre.to_lowercase().contains("smurf") {
            self.se_puede_usar = true;
            Ok(self)
        } else {
            Err("No se puede crear el Personaje. Los Personajes deben tener:
1) estatura menor a 250
2) peso menor a 300
3) nombre diferente a Smurf (es una palabra fea)"
                .to_string())
        }
    }
}

fn main() {
 let personaje_con_smurf = Personaje::new().nombre("jeje, soy Smurf!!").build(); // Este contiene "smurf" - no es correcto
    let personaje_demasiado_alto = Personaje::new().estatura(400).build(); // Demasiado alto - no es correcto
    let personaje_demasiado_pesado = Personaje::new().peso(500).build(); // Demasiado pesado - no es correcto
    let personaje_correcto = Personaje::new()
        .nombre("Billybrobby")
        .estatura(180)
        .peso(100)
        .build();   // Este es correcto, peso, estatura y nombre

    // Las cuatro variables no son Personaje, son Result<Personaje, String>. Se meten en un vector y se ve el resultado:
    let personaje_vec = vec![personaje_con_smurf, personaje_demasiado_alto, personaje_demasiado_pesado, personaje_correcto];

    for Personaje in personaje_vec { // Se imprimirá para ver si hay un error o no
        match Personaje {
            Ok(personaje_info) => println!("{:?}", personaje_info),
            Err(err_info) => println!("{}", err_info),
        }
        println!(); // Añade una línea separadora
    }
}

El código anterior da como resultado:

No se puede crear el Personaje. Los Personajes deben tener:
1) estatura menor a 250
2) peso menor a 300
3) nombre diferente a Smurf (es una palabra fea)

No se puede crear el Personaje. Los Personajes deben tener:
1) estatura menor a 250
2) peso menor a 300
3) nombre diferente a Smurf (es una palabra fea)

No se puede crear el Personaje. Los Personajes deben tener:
1) estatura menor a 250
2) peso menor a 300
3) nombre diferente a Smurf (es una palabra fea)

Personaje { nombre: "Billybrobby", edad: 15, estatura: 180, peso: 100, estadovital: Vivo, se_puede_usar: true }

Deref y DerefMut

Deref es un rasgo que permite utilizar * para desreferenciar una variable. Deref ha aparecido anteriormente cuando se usaba una estructura de tupla para crear un nuevo tipo. Ahora es el momento de aprender su uso.

Se conoce ya que una referencia no es lo mismo que un valor:

// ⚠️
fn main() {
    let valor = 7; // Esto es un i32
    let referencia = &7; // Esto es un &i32
    println!("{}", valor == referencia);
}

En este caso, Rust no devuelve false en la comparación. Da error de compilación:

error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src\main.rs:4:26
  |
4 |     println!("{}", valor == referencia);
  |                          ^^ no implementation for `{integer} == &{integer}`

La solución en este caso es el uso de *. Que dará como resultado true.

fn main() {
    let valor = 7;
    let referencia = &7;
    println!("{}", valor == *referencia);
}

A continuación, se construye un tipo simple que solamente contiene un número. Este tipo será parecido al tipo Box. Si solamente se crea el tipo con el número, no se podrá hacer mucho con él.

No se puede usar *, como sí se puede con Box:

// ⚠️
struct GuardaUnNumero(u8);

fn main() {
    let mi_numero = GuardaUnNumero(20);
    println!("{}", *mi_numero + 20);
}

El error dice:

error[E0614]: type `GuardaUnNumero` cannot be dereferenced
 --> src/main.rs:6:20
  |
6 |     println!("{}", *mi_numero + 20);
  |                    ^^^^^^^^^^

Sí se podría hacer algo como esto println!("{:?}", mi_numero.0 + 20); , pero entonces solo se están sumando un u8 a 20. Lo que sería práctico es sumar la variable con el valor. El mensaje cannot be dereferenced da una pista: es necesario implementar Deref. A los elementos simples que implementan este rasgo se los suele llamar punteros inteligentes- Un punterio inteligente puede apuntar a su elemento, tiene información adicional sobre él y puede tener y usar diversos métodos. El que se está construyendo puede hacer poca cosa en este momento, sumar un número e imprimirlo en println! ya que implementa Debug.

Cabe destacar un hecho interesante: String es un puntero inteligente a &str y Vec es un puntero inteligente a un array u otro tipo. Desde el comienzo, se han estado presentando punteros inteligentes.

La implementación de Deref no es difícil y los ejemplos de la librería estándar son fáciles. (Este es un código de ejemplo de la librería estándar)[https://doc.rust-lang.org/std/ops/trait.Deref.html]:

use std::ops::Deref;

struct DerefExample<T> {
    value: T
}

impl<T> Deref for DerefExample<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

fn main() {
    let x = DerefExample { value: 'a' };
    assert_eq!('a', *x);
}

Siguiendo el ejemplo, el tipo que se está creando necesita:

#![allow(unused)]
fn main() {
// 🚧
impl Deref for GuardaUnNumero {
    type Target = u8; // Recuerda, este es el "tipo asociado": el tipo al que va unido.
                      // Hay que poner el tipo correcot de retorno para Target

    fn deref(&self) -> &Self::Target { // Rust llama a .deref() cuando se usa *. solo se ha definido el Target como u8
        &self.0   // Se elige &self.0 porque estamos en un struct tupla. En una struct con campos con nombres se usaría "&self.numero"
    }
}
}

Ahora sí se puede usar *:

use std::ops::Deref;
#[derive(Debug)]
struct GuardaUnNumero(u8);

impl Deref for GuardaUnNumero {
    type Target = u8;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let mi_numero = GuardaUnNumero(20);
    println!("{:?}", *mi_numero + 20);
}

El código anterior imprime 40 sin que haya habido necesidad de referirse al valor almacenado en el struct con mi_numero.0. Así se dispone de los métodos de u8 y se pueden añadir otros métodos a GuardaUnNumero. A continuación, se observa esto mediante el uso de un método denominado .checked_sub(). Se trata de una resta segura que devuelve un Option. Si puede hacer la resta, devuelve un Some con el resultado en su interior. Si no puede hacerla, devuelve None. Se debe recordar que u8 no puede guardar números negativos. Por ello, es más seguro usar .checked_sub() evitando que el programa entre en pánico.

use std::ops::Deref;

struct GuardaUnNumero(u8);

impl GuardaUnNumero {
    fn imprime_el_doble_del_numero(&self) {
        println!("{}", self.0 * 2);
    }
}

impl Deref for GuardaUnNumero {
    type Target = u8;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let mi_numero = GuardaUnNumero(20);
    println!("{:?}", mi_numero.checked_sub(100)); // Este método viene de u8
    mi_numero.imprime_el_doble_del_numero(); // Este viene del struct GuardaUnNumero
}

Este código imprime:

None
40

DerefMut

También se puede implementar DerefMut que permite modificar los valores a través de *. Es muy parecido. Es necesario haber implementado Deref para poder implementar DerefMut.

use std::ops::{Deref, DerefMut};

struct GuardaUnNumero(u8);

impl GuardaUnNumero {
    fn imprime_el_doble_del_numero(&self) {
        println!("{}", self.0 * 2);
    }
}

impl Deref for GuardaUnNumero {
    type Target = u8;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for GuardaUnNumero { // Aquí no se necesita Target = u8; ya que se conoce debido a Deref
    fn deref_mut(&mut self) -> &mut Self::Target { // es igual que en Deref, pero con mut
        &mut self.0
    }
}

fn main() {
    let mut mi_numero = GuardaUnNumero(20);
    *mi_numero = 30; // DerefMut permite que se pueda hacer esto
    println!("{:?}", mi_numero.checked_sub(100));
    mi_numero.imprime_el_doble_del_numero();
}

Esto imprime:

#![allow(unused)]
fn main() {
None
60
}

Se observa que Deref permite muchas posibilidades.

Por ello, la librería estándar dice que Con el fin de evitar confusiones, Deref solo debería implementarse para punteros inteligentes. Esto se debe a que se pueden hacer cosas extrañas con Deref si un tipo es compuesto. A continuación se muestra un ejemplo desconcertante. Se crea un struct Personaje para un juego. Este personaje tiene diversos atributos.

struct Personaje {
    nombre: String,
    fuerza: u8,
    destreza: u8,
    salud: u8,
    inteligencia: u8,
    sabiduria: u8,
    encanto: u8,
    puntos_de_golpeo: i8,
    alineamiento: Alineamiento,
}

impl Personaje {
    fn new(
        nombre: String,
        fuerza: u8,
        destreza: u8,
        salud: u8,
        inteligencia: u8,
        sabiduria: u8,
        encanto: u8,
        puntos_de_golpeo: i8,
        alineamiento: Alineamiento,
    ) -> Self {
        Self {
            nombre,
            fuerza,
            destreza,
            salud,
            inteligencia,
            sabiduria,
            encanto,
            puntos_de_golpeo,
            alineamiento,
        }
    }
}

enum Alineamiento {
    Bueno,
    Neutral,
    Malvado,
}

fn main() {
    let billy = Personaje::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alineamiento::Bueno);
}

A continuación, se modifica para mantener los puntos de golpeo en un vector. Puede que se incorpore en este vector los datos de los monstruos, para mantenerlos juntos. Puesto que puntos_de_golpeo es un i8, se implementa Deref para poder hacer toda clase de operaciones matemáticas con él. Pero queda muy extraño su uso en main():

use std::ops::Deref;

// todo el código es igual hasta el enum Alineamiento
struct Personaje {
    nombre: String,
    fuerza: u8,
    destreza: u8,
    salud: u8,
    inteligencia: u8,
    sabiduria: u8,
    encanto: u8,
    puntos_de_golpeo: i8,
    alineamiento: Alineamiento,
}

impl Personaje {
    fn new(
        nombre: String,
        fuerza: u8,
        destreza: u8,
        salud: u8,
        inteligencia: u8,
        sabiduria: u8,
        encanto: u8,
        puntos_de_golpeo: i8,
        alineamiento: Alineamiento,
    ) -> Self {
        Self {
            nombre,
            fuerza,
            destreza,
            salud,
            inteligencia,
            sabiduria,
            encanto,
            puntos_de_golpeo,
            alineamiento,
        }
    }
}

enum Alineamiento {
    Bueno,
    Neutral,
    Malvado,
}

impl Deref for Personaje { // impl Deref en Personaje. Ahora hace cualquier cómputo integer sobre los puntos de golpeo sin mostralo!
    type Target = i8;

    fn deref(&self) -> &Self::Target {
        &self.puntos_de_golpeo
    }
}

fn main() {
    let billy = Personaje::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alineamiento::Bueno);
    let brandy = Personaje::new("Brandy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alineamiento::Bueno);

    let mut vec_puntos_de_golpeo = vec![];
    vec_puntos_de_golpeo.push(*billy);     // ¿Push de *billy qué está guardando?
    vec_puntos_de_golpeo.push(*brandy);    // ¿Push de *brandy qué está guardando?
    
    println!("{:?}", vec_puntos_de_golpeo);
}

Esto imprime solo [5, 5]. El código resulta extraño al leerlo. Es necesario ir a mirar que el Deref se está haciendo sobre un atributo concreto de Personaje. Si este conocimiento, no es posible entender qué se está guardando cuando se usa el *.

En este caso, el lugar de implementar Deref, resulta mejor implementar un método .get_puntos_de_golpeo(). Deref ofrece mucha potencia, pero es necesario usarlo solo donde resulte lógico hacerlo.

Crates (cajones) y módulos

Todo el código que se escribe en Rust se encuentra en un crate (N.T.: es el término que utiliza Rust para describir lo que en otros lenguajes de programación son las librerías o bibliotecas). Un crate es un fichero o conjunto de ficheros que debe ir unido. Dentro de un fichero, se pueden crear mod (módulos). Un módulo es un espacio de nombres único para funciones, estructuras, etc. y tiene sentido para:

  • Contruir el código: facilita estructurar el código en piezas lógicas según crece.
  • Leer el código: facilita la comprensión del código. Por ejemplo, el nombre std::collections::Hashmap expresa que el Hashmap se encuentra en std, dentro del módulo collections. Así se dispone de una pista sobre la existencia de un conjunto de tipos que son colecciones dentro de collections.
  • Privacidad: todo lo que se escribe en un módulo es privado por defecto. Se impide el uso de las funciones incluidas por parte de otros usuarios.

Para crear un módulo, solo se necesita escribir mod e iniciar un bloque de código con {}. A continuación, se crea un módulo denominado imprimir_cosas para incluir diversas funciones relacionadas con la impresión.

mod imprimir_cosas {
    use std::fmt::Display;

    fn imprimir_una_cosa<T: Display>(input: T) { // Imprime cualquier cosa que tenga definido Display
        println!("{}", input)
    }
}

fn main() {}

Cabe destacar que se ha escrito use std::fmt::Display dentro del módulo imprimir_cosas. Es un espacio separado. Si estuviera definido en main() no estaría disponible en el módulo. De esta forma, se puede usar en el módulo, pero no está accesible en main(). Además, la función imprimir_una_cosa() no puede usarse en este momento fuera del módulo, ya que no se ha precedido de la palabra reservada pub. Es decir, imprimir_una_cosa() es de uso privado del módulo mientras no se declare pública. Si se intenta el siguiente código:

mod imprimir_cosas {
    use std::fmt::Display;

    fn imprimir_una_cosa<T: Display>(input: T) { // Imprime cualquier cosa que tenga definido Display
        println!("{}", input)
    }
}

fn main() {
    crate::imprimir_cosas::imprimir_una_cosa(6);
}

Falla con el siguiente error:

error[E0603]: function `imprimir_una_cosa` is private
  --> src/main.rs:10:28
   |
10 |     crate::imprimir_cosas::imprimir_una_cosa(6);
   |                            ^^^^^^^^^^^^^^^^^ private function
   |

Aparte del error, en el código anterior, para llamar a la función se ha usado una sintaxis crate que significa desde la raíz de este proyecto. Esto se puede escribir cada vez que sea necesario usar la función o se puede usar use para importar la función:

mod imprimir_cosas {
    use std::fmt::Display;

    fn imprimir_una_cosa<T: Display>(input: T) { // Imprime cualquier cosa que tenga definido Display
        println!("{}", input)
    }
}

fn main() {
    use crate::imprimir_cosas::imprimir_una_cosa;
    
    imprimir_una_cosa(6);
    imprimir_una_cosa("Intento imprimir una cadena de caracteres".to_string());
}

Este código falla de la misma forma que el anterior:

error[E0603]: function `imprimir_una_cosa` is private
  --> src/main.rs:10:32
   |
10 |     use crate::imprimir_cosas::imprimir_una_cosa;
   |                                ^^^^^^^^^^^^^^^^^ private function
note: the function `imprimir_una_cosa` is defined here
  --> src/main.rs:4:5
   |
4  |     fn imprimir_una_cosa<T: Display>(input: T) { // Imprime cualquier cosa que tenga definido Display
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

En este ejemplo, se ha añadido el resto del mensaje de error, que indica donde encontrar la función que es privada en este momento. Esto puede llegar a ser muy útil cuando se tiene mucho código en diferentes ficheros.

Solo se necesita escribir pub fn en lugar de fn para que la función sea pública al módulo. Con este cambio, el código funciona:

mod imprimir_cosas {
    use std::fmt::Display;

    pub fn imprimir_una_cosa<T: Display>(input: T) { // Imprime cualquier cosa que tenga definido Display
        println!("{}", input)
    }
}

fn main() {
    use crate::imprimir_cosas::imprimir_una_cosa;
    
    imprimir_una_cosa(6);
    imprimir_una_cosa("Intento imprimir una cadena de caracteres".to_string());
}

E imprime lo siguiente:

6
Intento imprimir una cadena de caracteres

En el caso de estructuras, enumerados, rasgos o módulos, pub funciona de la siguiente forma:

  • Para un struct: hace que la estructura sea pública, per sus elementos no. Para hacer público un elemento de un struct hay que usar pub con él.
  • Para un enum o trait: todo su contenido se vuelve público. Tiene sentido ya que los rasgos tienen que ver con dar un comportamiento a algo y los enumerados sirven para elegir entre varios tipos, por lo que deben estar accesibles.
  • Para un módulo: un módulo de nivel superior siempre es público dado que es la única forma de poder usarlo, pero los módulos interiores, solo serán accesibles desde fuera si se utiliza pub ante ellos.

A continuación se incorpora una estructura de nominada Billy dentro del módulo imprimir_cosas. La estructura será pública, dentro tendrá un nombre y veces_a_imprimir. El primero no será público. El usuario podrá seleccionar el número de veces a imprimir, así que este último si será público. Queda así:

mod imprimir_cosas {
    use std::fmt::{Display, Debug};

    #[derive(Debug)]
    pub struct Billy { // Billy es público
        nombre: String, // pero nombre es privado.
        pub veces_a_imprimir: u32, // y veces_a_imprimir es público
    }

    impl Billy {
        pub fn new(veces_a_imprimir: u32) -> Self { // El usuario debe utilizar new para crear a Billy. Y solo decide el número de veces a imprimir
            Self {
                nombre: "Billy".to_string(), // El nombre se pone internamente
                veces_a_imprimir,
            }
        }

        pub fn imprimir_a_billy(&self) { // Esta función imprime a Billy
            for _ in 0..self.veces_a_imprimir {
                println!("{:?}", self.nombre);
            }
        }
    }

    pub fn imprimir_una_cosa<T: Display>(input: T) {
        println!("{}", input)
    }
}

fn main() {
    use crate::imprimir_cosas::*; // Ahora se usa *. Esto hace accesible todos los elementos públicos del módulo

    let my_billy = Billy::new(3);
    my_billy.imprimir_a_billy();
}

Esto imprimirá:

"Billy"
"Billy"
"Billy"

Por cierto, el * para importar todo se denomina operador glob. Glob es por global, que significa aquí importar todo.

Dentro de un módulo se pueden crear otros módulos. Un módulo hijo de otro (el módulo que está dentro de otro se llama hijo del que lo contiene) siempre puede usar cualquier cosa del módulo padre. Esto se observa con el siguiente ejemplo en el que existe un mod ciudad dentro de un mod provincia dentro de un mod pais.

Se puede pensar en esta estructura del modo en el que cuando estás ejecutando código de un país, puede que no estés dentro de código de un provinica, pero si estás ejecutando código de una provincia es que has pasado por un país. Si estás ejecutando código de ciudad, es que estás dentro de provincia y dentro de país, por lo que tienes acceso a todo su código.

mod pais { // El módulo principal no necesita pub
    fn imprimir_pais(pais: &str) { // Observa que esta función no es pública
        println!("Estamos en el pais de {}", pais);
    }
    pub mod provincia { // Este módulo es público

        fn imprimir_provincia(provincia: &str) { // Observa que esta función no es pública
            println!("en la provincia de {}", provincia);
        }

        pub mod ciudad { // Este módulo es público
            pub fn imprimir_ciudad(pais: &str, provincia: &str, ciudad: &str) {  // Esta función sí es pública
                crate::pais::imprimir_pais(pais);
                crate::pais::provincia::imprimir_provincia(provincia);
                println!("en la ciudad de {}", ciudad);
            }
        }
    }
}

fn main() {
    crate::pais::provincia::ciudad::imprimir_ciudad("Canadá", "New Brunswick", "Moncton");
}

Esto imprime:

Estamos en el pais de Canadá
en la provincia de New Brunswick
en la ciudad de Moncton

Lo reseñable es que imprimir_ciudad puede acceder a imprimir_provincia e imprimir_pais aunque no sean públicas para todo el código que esté fuera de estos módulos. Esto se debe a que el módulo ciudad sí está dentro de los otros dos, por lo que tiene acceso a ellos de forma completa, incluido todo aquello que no sea público.

Se habrá observado que crate::pais::provincia::imprimir_provincia(provincia); es un código muy largo. Dentro de un módulo hijo, es posible utilizar la palabra super para acceder al módulo padre de este. Por lo tanto, se puede reescribir el código de otra forma, incluso importando los módulos para simplificar usos variados:

mod pais { 
    fn imprimir_pais(pais: &str) { 
        println!("Estamos en el pais de {}", pais);
    }
    pub mod provincia {

        fn imprimir_provincia(provincia: &str) { 
            println!("en la provincia de {}", provincia);
        }

        pub mod ciudad { 
            use super::super::*; // usa todo lo del módulo "abuelo": pais
            use super::*; // usa todo lo del módulo "padre": provincia

            pub fn imprimir_ciudad(pais: &str, provincia: &str, ciudad: &str) {  
                imprimir_pais(pais);
                imprimir_provincia(provincia);
                println!("en la ciudad de {}", ciudad);
            }
        }
    }
}

fn main() {
    use crate::pais::provincia::ciudad::imprimir_ciudad; // importa solo esta función pública
    
    imprimir_ciudad("Canadá", "New Brunswick", "Moncton");
    imprimir_ciudad("Korea", "Gyeonggi-do", "Gwangju"); // Ahora es menos trabajo utilizarlo de nuevo
}

Pruebas (testing)

Ahora que ya se entiende el funcionamiento de los módulos es un buen momento para aprender sobre las pruebas. En Rust es muy fácil probar el código porque se pueden escribir los tests junto al propio código.

La forma más sencilla de empezar a probar es añadir #[test] a una función. Por ejemplo:

#![allow(unused)]
fn main() {
#[test]
fn dos_es_dos() {
    assert_eq!(2, 2);
}
}

Pero si se intenta ejecutar en Playground, da un error: error[E0601]: `main` function not found in crate `playground. Esto se debe a que no se utiliza Run para ejecutar las pruebas. En playground se puede pulsar junto a RUN en los ··· y cambiar a TEST. Ahora, si se pulsa, ejecutará las pruebas. Si se tiene ya instalado Rust en el ordenador, se debe ejecutar cargo test en lugar de cargo run.

Este es el resultado de ejecutar el test anterior:

running 1 test
test dos_es_dos ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Si se modifica assert_eq!(2, 2) a assert_eq!(2, 3) el test falla y devuelve una información mucho más detallada:

running 1 test
test dos_es_dos ... FAILED

failures:

---- dos_es_dos stdout ----
thread 'dos_es_dos' panicked at 'assertion failed: `(left == right)`
  left: `2`,
 right: `3`', src/lib.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    dos_es_dos

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

En Rust, assert_eq!(left, right) es el principal modo de probar una función. Si no funciona, mostará los dos valores que son diferentes: left has 2, but right has 3.

¿Qué significa RUST_BACKTRACE=1? Es una variable del sistema que se puede activar para dar más información sobre el error. En Playground se puede activar pulsando ··· junto a STABLEy establecer la traza (backtrace) a ENABLED. Si se hace así, se mostrará mucha información:

running 1 test
test dos_es_dos ... FAILED

failures:

---- dos_es_dos stdout ----
thread 'dos_es_dos' panicked at 'assertion failed: 2 == 3', src/lib.rs:3:5
stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/libunwind.rs:86
   1: backtrace::backtrace::trace_unsynchronized
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/mod.rs:66
   2: std::sys_common::backtrace::_print_fmt
             at src/libstd/sys_common/backtrace.rs:78
   3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
             at src/libstd/sys_common/backtrace.rs:59
   4: core::fmt::write
             at src/libcore/fmt/mod.rs:1076
   5: std::io::Write::write_fmt
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/io/mod.rs:1537
   6: std::io::impls::<impl std::io::Write for alloc::boxed::Box<W>>::write_fmt
             at src/libstd/io/impls.rs:176
   7: std::sys_common::backtrace::_print
             at src/libstd/sys_common/backtrace.rs:62
   8: std::sys_common::backtrace::print
             at src/libstd/sys_common/backtrace.rs:49
   9: std::panicking::default_hook::{{closure}}
             at src/libstd/panicking.rs:198
  10: std::panicking::default_hook
             at src/libstd/panicking.rs:215
  11: std::panicking::rust_panic_with_hook
             at src/libstd/panicking.rs:486
  12: std::panicking::begin_panic
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:410
  13: playground::dos_es_dos
             at src/lib.rs:3
  14: playground::dos_es_dos::{{closure}}
             at src/lib.rs:2
  15: core::ops::function::FnOnce::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libcore/ops/function.rs:232
  16: <alloc::boxed::Box<F> as core::ops::function::FnOnce<A>>::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/liballoc/boxed.rs:1076
  17: <std::panic::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panic.rs:318
  18: std::panicking::try::do_call
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:297
  19: std::panicking::try
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:274
  20: std::panic::catch_unwind
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panic.rs:394
  21: test::run_test_in_process
             at src/libtest/lib.rs:541
  22: test::run_test::run_test_inner::{{closure}}
             at src/libtest/lib.rs:450
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


failures:
    dos_es_dos

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

No es necesario disponer de toda la traza, salvo que no se pueda encontrar cuál es el problema. Normalmente, tampoco es necesario comprender toda la traza. En ella, se observa que en la línea 13, se habla de dos_es_dos. A partir de ahí es donde empieza a hablar del código de la aplicación. Todo lo demás es sobre lo que Rust está haciendo en otras librerías para ejecutar el programa. Estas dos líneas (13 y 14) muestran que son errores en las líneas 2 y 3 de playground, es ahí donde está el error.

  13: playground::dos_es_dos
             at src/lib.rs:3
  14: playground::dos_es_dos::{{closure}}
             at src/lib.rs:2

Nota: Rust mejoró los mensajes de traza a comienzos de 2021 para mostrar solo la información más útil. Ahora es más fácil de leer:

failures:

---- dos_es_dos stdout ----
thread 'dos_es_dos' panicked at 'assertion failed: `(left == right)`
  left: `2`,
 right: `3`', src/lib.rs:3:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/std/src/panicking.rs:493:5
   1: core::panicking::panic_fmt
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/panicking.rs:92:14
   2: playground::dos_es_dos
             at ./src/lib.rs:3:5
   3: playground::dos_es_dos::{{closure}}
             at ./src/lib.rs:2:1
   4: core::ops::function::FnOnce::call_once
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/ops/function.rs:227:5
   5: core::ops::function::FnOnce::call_once
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


failures:
    dos_es_dos

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s

En el siguiente ejemplo se desactiva la traza y se añaden algunas funciones que se probarán a través de unas funciones de prueba:

#![allow(unused)]
fn main() {
fn return_two() -> i8 {
    2
}
#[test]
fn it_returns_two() {
    assert_eq!(return_two(), 2);
}

fn return_six() -> i8 {
    4 + return_two()
}
#[test]
fn it_returns_six() {
    assert_eq!(return_six(), 6)
}
}

Ahora se ejecutan los dos tests:

running 2 tests
test it_returns_two ... ok
test it_returns_six ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Normalmente, los tests se llevarán a un módulo específico. Para ello, se usa mod como ya se conoce y se antecede con #[cfg(test)]. Delante de las funciones de prueba se sigue poniendo #[test]. Desde la línea de comando de Rust, esto permitirá realizar diferentes tipos de prueba, ejecutar solo una función de prueba, todas o unas algunas de ellas. En el módulo de prueba será necesario escribir use super::* para que pueda tener acceso sencillo a todas las funciones del módulo principal que se está probando. Queda así:


#![allow(unused)]
fn main() {
fn return_two() -> i8 {
    2
}
fn return_six() -> i8 {
    4 + return_two()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_returns_six() {
        assert_eq!(return_six(), 6)
    }
    #[test]
    fn it_returns_two() {
        assert_eq!(return_two(), 2);
    }
}
}

Desarrollo dirigido por pruebas

En inglés se denomina TDD, test driven development. Es una forma de escribir programas en la que primero se escribe el código de prueba de una función y luego se escribe su código. Así, siempre se dispone de pruebas para todo el código que se escribe. Normalmente, se escriben las pruebas, se ejecutan y fallan, puesto que no se ha escrito el código. Después, se escribe el código de la función hasta que pasan todas las pruebas existentes. Esto es muy sencillo en Rust ya que el compilador da mucha información sobre lo que hay que arreglar. A continuación, se escribe un pequeño ejemplo de desarrollo orientado por pruebas para ver cómo se hace.

Se va a suponer que se desarrolla una calculadora que suma y resta. Si el usuario escribe "5 + 6" debería dar como resultado 11. Si el usuario escribe "5 + 6 - 7", debería devolver 4, y así para cada entrada. En primer lugar, se escriben las funciones de prueba. Los nombres de las funciones de prueba suelen ser largos, para dejar claro cual es cada prueba y conocer bien qué significa si falla.

El código real de esta calculadora se encontrará en una función math() que realiza los cálculos. Devolverá un i32 (no realiza cálculos con decimales). En primer lugar, esta función solo devolverá un número fijo, el 6. Evidentemente, esto hace que el código de prueba falle:

#![allow(unused)]
fn main() {
fn math(input: &str) -> i32 {
    6
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
}
}

La ejecución de las pruebas, en este momento, da el siguiente resultado:

running 3 tests
test tests::one_minus_minus_one_is_two ... FAILED
test tests::one_minus_two_is_minus_one ... FAILED
test tests::one_plus_one_is_two ... FAILED

El texto anterior, está resumido. Además, se dispone de toda la información del motivo de cada fallo, pero no se reproduce aquí.

Ahora se va a diseñar la calculadora. Esta aceptará los dígitos, los símbolos + y - y el espacio en blanco. Y nada más. En primer lugar, se crea un const con todos los caracteres posibles. La función math() iterará a través de todos los caracteres que tenga el parámetro y comprobará si están incluidos entre los caracteres válidos.

Este es el momento para añadir una prueba que tenga que fallar debido a que se le pase algún carácter no válido. Para que el resultado de una prueba que falla sea considerado que es el resultado que debe valoes se debe añadir el atributo #[should_panic] a la prueba. Es decir, que la función de prueba entre en pánico es lo correcto en este caso.

El código queda así:

#![allow(unused)]
fn main() {
const OKAY_CHARACTERS: &str = "1234567890+- "; // Con el espacio entre los caracteres válidos

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) {
        panic!("Please only input numbers, +-, or spaces");
    }
    6 // por ahora se sigue devolviendo un 6
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }

    #[test]
    #[should_panic]  // Esta es la nueva prueba - que debe lanzar panic como resultado correcto
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

Ahora, el resultado de ejecutar los tests es:

running 4 tests
test tests::one_minus_two_is_minus_one ... FAILED
test tests::one_minus_minus_one_is_two ... FAILED
test tests::panics_when_characters_not_right ... ok
test tests::one_plus_one_is_two ... FAILED

Una prueba ha funcionado, la función solo acepta caracteres válidos.

El siguiente paso consiste en escribir el código que calcula los resultados. La lógica de la calculadora será así:

  • Todos los espacios se eliminarán. Esto se hace con .filter().
  • La entrada se convertirá en un Vec con todas las entradas. + no necesita ser una entrada, pero cuando el programa vea un + debería saber que el número está completo. Por ejemplo, para la entrada 11+1 debería hacer algo así:
    1. Encuentra el 1 y lo inserta en una cadena de caracteres vacía.
    2. Encuentra el siguiente 1 y lo inserta en la cadena de caracteres que ahora contendrá "11".
    3. Encuentra el carácter +, entiende que el número se ha terminado y guarda la cadena de caracteres en el vec y limpia la cadena de caracteres.
  • El programa debe contar el núemro de -. Un número impar (1,3,5,...) significará restar, un número par significará sumar. Así, 1--9 debería dar 10, no -8. Es decir, 1 menos el -9.
  • El programa debería eliminar todos lo que aparezca después del último número. 5+5+++++----- está compuesto de todos los caracteres en OKAY_CHARACTERS, pero debería convertirlo a 5+5. Es fácil hacer esto con .trim_end_matches(). Esta función elimina todo lo que coincida con ella al final de una &str.

(Por cierto, .trim_end_matches() y .trim_start_matches() se denominaban antes .trim_right_matches() y .trim_left_matches(). Pero se observó que algunos lenguajes se leen de derecha a izquierda (persa, hebreo, etc), por lo que la denominación de izquierda y derecha era errónea. En algún código antiguo, aún pueden estar usándose las funciones antiguas).

En primer lugar, se debe conseguir que la función pase todas las pruebas. Después, se puede refactorizar el código para hacerlo mejor:

#![allow(unused)]
fn main() {
const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric())
    {
        panic!("Please only input numbers, +-, or spaces.");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)) // Elimina +, - y espacios al final
        .chars().filter(|x| *x != ' ') // Elimina todos los espacios restantes
        .collect::<String>(); 
    let mut result_vec = vec![]; // Los resultados van aquí
    let mut push_string = String::new(); // Esta cadena es para hacer push de lo que se va encontrando. Se reutiliza entre diferentes números
    for character in input.chars() {
        match character {
            '+' => {
                if !push_string.is_empty() { // Si la cadena es vacía, no se quiere añadir "" al result_vec
                    result_vec.push(push_string.clone()); // Pero si no es vacía, es un número que se añade al vector
                    push_string.clear(); // Y se limpia la cadena temporal
                }
            },
            '-' => { // Si llega un símbolo -,
                if push_string.contains('-') || push_string.is_empty() { // Hay que ver si está vacía o ya tiene -
                    push_string.push(character) // si es así, se añade.
                } else { // en otro caso, contendrá un número
                result_vec.push(push_string.clone()); // añade el valor a result_vec
                push_string.clear(); // limpia la cadena temporal
                push_string.push(character); // y añade el -
                }
            },
            number => { // cualquier otra cosa que venga, que serán dígitos, pasa por aquí
                if push_string.contains('-') { // Su hay - en la cadena temporal, hay que añadirlos.
                    result_vec.push(push_string.clone());
                    push_string.clear();
                    push_string.push(number);
                } else { // si no es así, se añade a la cadena temporal
                    push_string.push(number);
                }
            },
        }
    }
    result_vec.push(push_string); // Cuando se acaba el bucle, se añade lo que quedara en a cadena. No es necesario .clone() porque no se usa la variable más.

    let mut total = 0; // Ahora es el momento de hacer los cálculos
    let mut adds = true; // true = sumar, false = restar
    let mut math_iter = result_vec.into_iter();
    while let Some(entry) = math_iter.next() { // Itera a través de los elementos
        if entry.contains('-') { // Si contiene -, se comprueba si son par o impar
            if entry.chars().count() % 2 == 1 {
                adds = match adds {
                    true => false,
                    false => true
                };
                continue; // va al siguiente elemento
            } else {
                continue;
            }
        }
        if adds == true {
            total += entry.parse::<i32>().unwrap(); // Si llega aquí, tiene que ser un número, por lo que es seguro hacer unwrap
        } else {
            total -= entry.parse::<i32>().unwrap();
            adds = true;  // Después de restar, se resetea a suma.
        }
    }
    total // Finalmente, devuelve el total
}
   /// Se añaden más tests para añadir seguridad

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0); // Este es nuevo
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8); // Este es nuevo
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

Y ahora, las pruebas pasan:

running 6 tests
test tests::one_minus_minus_one_is_two ... ok
test tests::nine_plus_nine_minus_nine_minus_nine_is_zero ... ok
test tests::one_minus_two_is_minus_one ... ok
test tests::eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end ... ok
test tests::one_plus_one_is_two ... ok
test tests::panics_when_characters_not_right ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Se observa que, durante el proceso de desarrollo orientado a pruebas, existe un continuo ir y venir que sigue el siguiente patrón:

  • Primero se escriben todos los casos de prueba que se puedan imaginar.
  • Luego se comienza a escribir código.
  • Según se escribe el código, aparecen ideas para hacer otras pruebas.
  • Se añaden estas otras pruebas. Así, las pruebas van creciendo según se programa. Cuantos más pruebas (NT: pruebas que cubran casos nuevos) se codifiquen, más veces pruebas el código.

En todo caso, las pruebas no aseguran que todo esté correcto. Pero sí son muy útiles cuando se va a modificar el código más tarde. Facilitan encontrar posibles fallos introducidos por el nuevo código.

Ahora se va a reescribir un poco el código (refactorizar). Una buena forma de comenzar es usar clippy. Si se ha instalado Rust, se puede usar cargo clippy. En Playgroudn se puede pulsar en TOOLS y seleccionar Clippy. Clippy analizará el código y dará pistas para hacerlo más simple.

En este caso, Clippy dice dos cosas:

warning: this loop could be written as a `for` loop
  --> src/lib.rs:44:5
   |
44 |     while let Some(entry) = math_iter.next() { // Iter through the items
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `for entry in math_iter`
   |
   = note: `#[warn(clippy::while_let_on_iterator)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#while_let_on_iterator

warning: equality checks against true are unnecessary
  --> src/lib.rs:53:12
   |
53 |         if adds == true {
   |            ^^^^^^^^^^^^ help: try simplifying it as shown: `adds`
   |
   = note: `#[warn(clippy::bool_comparison)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#bool_comparison

Esto es cierto: for entry in math_iter es mucho más simple que while let Some(entry) = math_iter(next). Un bucle for ya realiza la iteración, por lo que no hay que escribir .iter(). Y tampoco se necesitaba usar math_iter, se puede esribir for entry in result_vec.

Además, se va a refactorizar parte del código. En lugar de variables separadas, se crea una estructura Calculator que une las variables necesarias. Se cambiarán dos nombres por claridad. result_vec se convierte en results y push_string en current_input (la entrada actual). En este momento, esta estructura solo tiene un método: new().

#![allow(unused)]
fn main() {
// 🚧
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }
}
}

Ahora el código es un poco más largo, pero más fácil de leer. Por ejemplo, if adds ahora es if calculator.adds, que es casi como leer inglés. Queda así:

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }
}

const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric()) {
        panic!("Please only input numbers, +-, or spaces");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>();
    let mut calculator = Calculator::new();

    for character in input.chars() {
        match character {
            '+' => {
                if !calculator.current_input.is_empty() {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.current_input.clear();
                }
            },
            '-' => {
                if calculator.current_input.contains('-') || calculator.current_input.is_empty() {
                    calculator.current_input.push(character)
                } else {
                calculator.results.push(calculator.current_input.clone());
                calculator.current_input.clear();
                calculator.current_input.push(character);
                }
            },
            number => {
                if calculator.current_input.contains('-') {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.current_input.clear();
                    calculator.current_input.push(number);
                } else {
                    calculator.current_input.push(number);
                }
            },
        }
    }
    calculator.results.push(calculator.current_input);

    for entry in calculator.results {
        if entry.contains('-') {
            if entry.chars().count() % 2 == 1 {
                calculator.adds = match calculator.adds {
                    true => false,
                    false => true
                };
                continue;
            } else {
                continue;
            }
        }
        if calculator.adds {
            calculator.total += entry.parse::<i32>().unwrap();
        } else {
            calculator.total -= entry.parse::<i32>().unwrap();
            calculator.adds = true;
        }
    }
    calculator.total
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0);
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8);
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

Finalmente, se añaden dos métodos nuevos. Uno se denomina .clear() que vacía la entrada actual. El otro se denomina push_char() que añade una caracter a la entrada actual. Así queda el código refactorizado.

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }

    fn clear(&mut self) {
        self.current_input.clear();
    }

    fn push_char(&mut self, character: char) {
        self.current_input.push(character);
    }
}

const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric()) {
        panic!("Please only input numbers, +-, or spaces");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>();
    let mut calculator = Calculator::new();

    for character in input.chars() {
        match character {
            '+' => {
                if !calculator.current_input.is_empty() {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.clear();
                }
            },
            '-' => {
                if calculator.current_input.contains('-') || calculator.current_input.is_empty() {
                    calculator.push_char(character)
                } else {
                calculator.results.push(calculator.current_input.clone());
                calculator.clear();
                calculator.push_char(character);
                }
            },
            number => {
                if calculator.current_input.contains('-') {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.clear();
                    calculator.push_char(number);
                } else {
                    calculator.push_char(number);
                }
            },
        }
    }
    calculator.results.push(calculator.current_input);

    for entry in calculator.results {
        if entry.contains('-') {
            if entry.chars().count() % 2 == 1 {
                calculator.adds = match calculator.adds {
                    true => false,
                    false => true
                };
                continue;
            } else {
                continue;
            }
        }
        if calculator.adds {
            calculator.total += entry.parse::<i32>().unwrap();
        } else {
            calculator.total -= entry.parse::<i32>().unwrap();
            calculator.adds = true;
        }
    }
    calculator.total
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0);
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8);
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

Se pueden escribir más métodos, pero líneas como calculator.results.push(calculator.current_input.clone()); ya quedan suficientemente claras. Cuando se refactoriza, es bueno que el código quede legible, no se trata de hacerlo más corto: clc.clr() es peor que calculator.clear(), por ejemplo.

Crates externas

Un crate (Cajón -librería o biblioteca-) externa es aquella que se ha desarrollado fuera del proyecto en desarrollo.

Para esta sección casi se necesita ya tener instalado Rust, pero aún puede realizarse usando solo Playground. Se va a aprender cómo importar crates que otras personas hayan escrito para utilizarlas en un proyecto de Rust. Esto es importante en Rust por dos motivos:

  • Es muy fácil importar otras crates.
  • La librería de Rust estándar es muy pequeña.

Esto significa que es muy normal tener que importar crates externas para funciones aparentemente básicas. La idea es que si es fácil usar librerías (crates) externas, se puede elegir la mejor existente.

En este libro solo aparecen algunas de las crates más populares. Aquellas que todo el mundo que usa Rust conoce.

Para comentar a aprender las crates externas, se comenzará con una de las más comunes: rand.

rand

Hasta el momento, no se han utilizado números aleatorios. No se encuentran en la librería estándar. Si se ha instalado Rust en el ordenador y se ha iniciado un proyecto con cargo init, existe un fichero Cargo.toml en el directorio del proyecto. En este fichero reside, entre otra, la información de los crates que esté utilizando el proyecto. Un fichero Cargo.toml tiene la siguiente apariencia:

[package]
name = "rust_book"
version = "0.1.0"
authors = ["David MacLeod"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Para añadir el crate (la librería) rand, se puede buscar en crates.io, que es el almacén online de todas las crates publicadas: https://crates.io/crates/rand. En la página anterior, dedicada a rand se observa a la derecha una explicación que indica cómo añadir este crate al fichero Cargo.toml: rand = "0.7.8" (NT: el número de versión puede ser mayor ya que se va actualizando periódicamente). Esta línea, se debe añadir bajo el apartado [dependencies] del fichero Cargo.toml:

[package]
name = "rust_book"
version = "0.1.0"
authors = ["David MacLeod"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.7.3"

Con esta línea añadida, Cargo se encarga de todo. A partir de este momento, se puede escribir código como el de ejemplo de este crate en la documentación de rand. Para llegar a la página de documentación de una librería se puede pulsar el enlace a docs de la página de crates.io.

Esto es suficiente por ahora sobre Cargo. Aún se está usando Playground. Afortunadamente, Playground tiene las 100 crates más usada precargadas. Por ello, aún no es necesario escribirlas en Cargo.toml. En Playground se debe pensar que existe una lista de 100 crates como:

[dependencies]
rand = "0.7.3"
some_other_crate = "0.1.0"
another_nice_crate = "1.7"

Esto significa que para usar rand en Playground basta con escribirlo en el código así:

extern crate rand; // Esto significa que se use toda la librería rand.
          // En un proyecto propio en un ordenador no se puede solo escribir esto,
          // es necesario haber añadido primero la librería en Cargo.toml.

fn main() {
    for _ in 0..5 {
        let random_u16 = rand::random::<u16>();
        print!("{} ", random_u16);
    }
}

Este código imprime un número u16 diferente cada vez, como 42266 52873 56528 46927 6867.

Las funciones principales de rand son random y thread_rng (rng significa "random number generator" -generador de números aleatorios-)-. La propia función random es un atajo a thread_rng().gen(). Por lo que es esta última thread_rng quien en realidad hace casi todo el trabajo.

A continuación, se muestra un ejemplo de números del 1 al 10. Para obtener estos números se utiliza .get_range() entre 1 y 11.

extern crate rand;
use rand::{thread_rng, Rng}; // O simplemente rand::*; con pereza

fn main() {
    let mut number_maker = thread_rng();
    for _ in 0..5 {
        print!("{} ", number_maker.gen_range(1..11));
    }
}

Que imprime cosas como 7 2 4 8 6.

Los números aleatorios permiten cosas divertidas, como crear los personajes de un juego. Con rand y algunas otras herramientas se puede simular un dado d6 (con seis caras: 1, 2, 3, 4, 5, 6). En este juego el personaje tiene seis tipos de características. Para establecer cada característica, se podría lanzar tres veces el dado de seis caras y sumar el resultado. De esta forma, cada característica podría tener un valor entre 3 y 18.

En ocasiones puede no ser justo que un personaje tenga algo tan bajo como 3 o 4. Si la fuerza es 3 no se puede transportar nada, por ejemplo. Para evitar esto, se puede usar un método que lanza el dado d6 cuatro veces y descarta el valor más bajo. Así si el resultado es 3, 3, 1, 6, se conservan los siguientes 3, 3, 6, y el resultado es 12. Este será el método que se desarrollará en este ejemplo:

extern crate rand;
use rand::{thread_rng, Rng}; // O rand::*; si se está perezoso
use std::fmt; // Se va a impl Display para el personaje


struct Character {
    strength: u8,
    dexterity: u8,    // Esto signfica "velocidad del cuerpo"
    constitution: u8, // Esto signfica "salud"
    intelligence: u8,
    wisdom: u8,
    charisma: u8, // Esto signfica "popularidad con la gente"
}

fn three_die_six() -> u8 { // Un "die" es lo que lanzas para obtener el número
    let mut generator = thread_rng(); // Crea un generador de números aleatorios
    let mut stat = 0; // Estadísticas totales
    for _ in 0..3 {
        stat += generator.gen_range(1..=6); // Suma el resultado cada vez
    }
    stat // Devuelve el total
}

fn four_die_six() -> u8 {
    let mut generator = thread_rng();
    let mut results = vec![]; // Primero guarda los números en un vec
    for _ in 0..4 {
        results.push(generator.gen_range(1..=6));
    }
    results.sort(); // Con esto, ordena [4, 3, 2, 6] en [2, 3, 4, 6]
    results.remove(0); // Ahora sería [3, 4, 6]
    results.iter().sum() // Devuelve el resultado
}

enum Dice {
    Three,
    Four
}

impl Character {
    fn new(dice: Dice) -> Self { // Crea el personaje con tres o cuatro tiradas
        match dice {
            Dice::Three => Self {
                strength: three_die_six(),
                dexterity: three_die_six(),
                constitution: three_die_six(),
                intelligence: three_die_six(),
                wisdom: three_die_six(),
                charisma: three_die_six(),
            },
            Dice::Four => Self {
                strength: four_die_six(),
                dexterity: four_die_six(),
                constitution: four_die_six(),
                intelligence: four_die_six(),
                wisdom: four_die_six(),
                charisma: four_die_six(),
            },
        }
    }
    fn display(&self) { // Esto funciona gracias a implementar Display a continuación
        println!("{}", self);
        println!();
    }
}

impl fmt::Display for Character { // Basta con seguir https://doc.rust-lang.org/std/fmt/trait.Display.html y cambiarlo un poco
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "Tu personaje tiene las siguientes características:
fuerza: {}
destreza: {}
constitución: {}
inteligencia: {}
sabiduría: {}
carisma: {}",
            self.strength,
            self.dexterity,
            self.constitution,
            self.intelligence,
            self.wisdom,
            self.charisma
        )
    }
}



fn main() {
    let weak_billy = Character::new(Dice::Three);
    let strong_billy = Character::new(Dice::Four);
    weak_billy.display();
    strong_billy.display();
}

Normalmente el personaje generado con cuatro tiradas de dado tendrá mejores características.

rayon

rayon es una librería muy popular que permite acelerar el código Rust. Es popular porque crea hilos sin necesidad de hacer cosas como thread::spawn. Es efectiva y fácil de usar. Por ejemplo:

  • .iter(), .iter_mut(), .into_iter() en esta librería se escriben así:
  • .par_iter(), .par_iter_mut(), .par_into_iter() y permite ejecutar las operaciones en paralelo.

Así sucede con otros métodos: .chars() es .par_chars() y así con todos.

A continuación, se muestra un ejemplo de un código muy simple que obliga a trabajar mucho al ordenador:

fn main() {
    let mut my_vec = vec![0; 200_000];
    my_vec.iter_mut().enumerate().for_each(|(index, number)| *number+=index+1);
    println!("{:?}", &my_vec[5000..5005]);
}

Crea un vector de 200.000 elementos: cada uno de ellos vale 0. Luego llama a .enumerate() para obtener el índice de cada uno de ellos y cambia el valor 0 por el número de índice. Como es un vector muy largo, se imprimen solo algunos valores, del 5000, al 5004. Esto es rápido en Rust, pero con rayon es aún más rápido y el código es casi igual:

extern crate rayon;
use rayon::prelude::*; // Import rayon

fn main() {
    let mut my_vec = vec![0; 200_000];
    my_vec.par_iter_mut().enumerate().for_each(|(index, number)| *number+=index+1); // add par_ to iter_mut
    println!("{:?}", &my_vec[5000..5005]);
}

Esto es todo. Con rayon se dispone de un conjunto de métodos muy amplio que se ajustan a cada necesidad. En general es tan sencillo como añadir par_ al nombre de la función...

serde

Esta librería se usa de forma habitual para convertir desde y hacia formatos como JSON, YAML, etc. Normalmente se crea un struct dos atributos específicos (enlace a la documentación de la librería) como en el siguiente código:



#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}
}

Los rasgos Serialize y Deserialize hacen fácil la conversión (de ellos viene el nombre serde). Añaden métodos para convertir desde y hacia los diferentes formatos disponibles.

regex

La librería regex permite buscar en un texto mediante expresiones regulares. Con ella se pueden encontrar referencias a cosas parecidas, pero diferentes, como color, colores, colours en una sola búsqueda. Las expresiones regulares son un un lenguaje completo que es necesario conocer para poder usar esta librería.

chrono

chrono es la librería principal que se utiliza para disponer de funcionalidad avanzada sobre el tiempo. Lo que no esté disponible en la librería estándar, se puede buscar aquí.

Un paseo por la librería estándar

Ahora que ya se conoce bastante sobre Rust, se puede entender la mayor parte de lo que contiene la librería estándar. El código que contiene no debe asustar ya. Se muestran en este capítulo algunas partes de lo que aún no se ha aprendido. Se revisitarán conceptos que ya se conocen, para aprenderlos en mayor detalle.

Arrays

En el pasado (antes de Rust 1.53), los arrays no implementaban Iterator y se necesitaba usar métodos como .iter() en bucles for (O se usaba & para obtener una sección que usar en un bucle for). En resume, este código no funcionaba en el pasado:

fn main() {
    let my_cities = ["Beirut", "Tel Aviv", "Nicosia"];

    for city in my_cities {
        println!("{}", city);
    }
}

El compilador daba el siguiente error:

error[E0277]: `[&str; 3]` is not an iterator
 --> src\main.rs:5:17
  |
  |                 ^^^^^^^^^ borrow the array with `&` or call `.iter()` on it to iterate over it

Afortunadamente, esto ya no es un problema. Por lo que las siguientes tres versiones funcionan sin problema:

fn main() {
    let my_cities = ["Beirut", "Tel Aviv", "Nicosia"];

    for city in my_cities {
        println!("{}", city);
    }
    for city in &my_cities {
        println!("{}", city);
    }
    for city in my_cities.iter() {
        println!("{}", city);
    }
}

Que imprime:

Beirut
Tel Aviv
Nicosia
Beirut
Tel Aviv
Nicosia
Beirut
Tel Aviv
Nicosia

Si se quiere recuperar elementos de un array para guardarlos en una variable se puede usar [] para desestructurarlo (como en las tuplas en sentencias match):

fn main() {
    let my_cities = ["Beirut", "Tel Aviv", "Nicosia"];
    let [city1, city2, city3] = my_cities;
    println!("{}", city1);
}

El código anterior imprime Beirut.

char

Se puede usar .escape_unicode() para recuperar el código unicode de un carácter char:

fn main() {
    let korean_word = "청춘예찬";
    for character in korean_word.chars() {
        print!("{} ", character.escape_unicode());
    }
}

El código anterior imprime \u{ccad} \u{cd98} \u{c608} \u{cc2c}.

Se puede obtener un char de un u8 usando el rasgo From, pero para obtenerlo de un u32 resulta necesario usar TryFrom ya que no todos los valores u32 son caracteres unicode y puede fallar la conversión:

extern crate rand;
use std::convert::TryFrom; // Se necesita importar TryFrom para usarlo
use rand::prelude::*;      // También se usarán números aleatorios

fn main() {
    let some_character = char::from(99); // Este es fácil - no necesita TryFrom
    println!("{}", some_character);

    let mut random_generator = rand::thread_rng();
    // Se intenta esto 40,000 times: crear un char de un u32.
    // El rango entre 0 (std::u32::MIN) to u32's highest number (std::u32::MAX). Si no funciona, devolverá '-'.
    for _ in 0..40_000 {
        let bigger_character = char::try_from(random_generator.gen_range(std::u32::MIN..std::u32::MAX)).unwrap_or('-');
        print!("{}", bigger_character)
    }
}

Casi siempre se genera un -. La salida del programa anterior se parecerá a lo siguiente:

------------------------------------------------------------------------𤒰---------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-------------------------------------------------------------춗--------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
------------򇍜----------------------------------------------------

A partir de agosto de 2020 se puede crear un String a partir de char. (String implementa From<char>). Para ello, se usa String::from() pasando como parámetro un char.

Enteros

Los tipos de dato enteros tienen a su disposición muchos métodos matemáticos y algunos otros. A continuación se muestran algunos de los más útiles.

.checked_add(), .checked_sub(), .checked_mul(), .checked_div(). Son métodos que validan que el resultado "cabe" en el tipo. Devuelven Option para que se puda validar de forma fácil el resultado sin que el programa entre en pánico.

fn main() {
    let some_number = 200_u8;
    let other_number = 200_u8;

    println!("{:?}", some_number.checked_add(other_number));
    println!("{:?}", some_number.checked_add(1));
}

Este código imprime:

None
Some(201)

Se habrá observado que la página de documentación de los tipos enteros dice mucho rhs que significa "right hand side" (lado de la derecha). Por ejemplo, en 5 + 6, el lado izquierdo es 5 y el derecho es 6. Es decir, 6 es el rhs.

Es posible implementar la suma para cualquier tipo. Para ello, se usa el rasgo correspondiente Add. Después de implementarlo, se puede usar el operador + en el tipo en que se haya codificado. Este es el ejemplo de la documentación oficial:


#![allow(unused)]
fn main() {
use std::ops::Add; // añade acceso al rasgo Add

#[derive(Debug, Copy, Clone, PartialEq)] // PartialEq es importante para poder comparar números
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Self; // Recuerda, este es el tipo asociado. El tipo "que va" con este otro
                        // En este caso es otro Point

    fn add(self, other: Self) -> Self {
        Self {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}
}

A continuación se implementa para un tipo propio. Se van a sumar dos países para comparar sus economías.

use std::fmt;
use std::ops::Add;

#[derive(Clone)]
struct Country {
    name: String,
    population: u32,
    gdp: u32, // This is the size of the economy
}

impl Country {
    fn new(name: &str, population: u32, gdp: u32) -> Self {
        Self {
            name: name.to_string(),
            population,
            gdp,
        }
    }
}

impl Add for Country {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self {
            name: format!("{} y {}", self.name, other.name), // Se unen los nombres,
            population: self.population + other.population, // se suma la población,
            gdp: self.gdp + other.gdp,   // y el producto interior bruto (gross domestic product)
        }
    }
}

impl fmt::Display for Country {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "En {} hay {} personas y un producto interior bruto de {}€", // Así se puede imprimir con solo {}
            self.name, self.population, self.gdp
        )
    }
}

fn main() {
    let nauru = Country::new("Nauru", 10_670, 160_000_000);
    let vanuatu = Country::new("Vanuatu", 307_815, 820_000_000);
    let micronesia = Country::new("Micronesia", 104_468, 367_000_000);

    // Si se hubiera usado &str en lugar de String para el name, habría que haber usado ciclos de vida
    // y era demasiado para un ejemplo. Mejor usar clone cuando se llama a println!.
    println!("{}", nauru.clone());
    println!("{}", nauru.clone() + vanuatu.clone());
    println!("{}", nauru + vanuatu + micronesia);
}

El código anterior imprime:

En Nauru hay 10670 personas y un producto interior bruto de 160000000€
En Nauru y Vanuatu hay 318485 personas y un producto interior bruto de 980000000€
En Nauru y Vanuatu y Micronesia hay 422953 personas y un producto interior bruto de 1347000000€

Más adelante en este código se puede cambiar .fmt() para mostrar un número de forma que sea más sencillo de leer (formateándolo).

Hay otros rasgos Sub, Mul y Div para implementar la resta, multiplicación y división. Para poder usar +=, -=, *= y /= se deben añadir los rassgos: AddAssign, SubAssign, MulAssign y DivAssign. La lista completa existente se puede consultar aquí. Hay muchos más, como %, que se llama Rem, o como - (operador unario), que se denomina Neg.

Números flotantes

f32 y f64 tienen un amplio número de métodos matemáticos. Hay otros más que se pueden usar. Por ejemplo .floor(), .ceil(), .round() y .trunc(). Estos métodos devuelven f32 o f64 solo que con la parte decimal con valor 0. Esta es su función:

  • .floor(): devuelve el valor entero inmediatamente anterior.
  • .ceil(): devuelve el valor entero inmediatamente siguiente.
  • .round(): devuelve el valor .ceil() si la parte decimal es 0.5 o superior. Devuelve el valor .floor() si no es así. A esto se le llama redondeo, porque devuelve un número "redondo".
  • .trunc(): simplemente elimina la parte decimal. (N.T.: en los números positivos funciona igual que .floor(), en los negativos no.)

A continuación, se muestra un ejemplo:

fn four_operations(input: f64) {
    println!(
"Para el número {}:
floor: {}
ceiling: {}
rounded: {}
truncated: {}\n",
        input,
        input.floor(),
        input.ceil(),
        input.round(),
        input.trunc()
    );
}

fn main() {
    four_operations(9.1);
    four_operations(100.7);
    four_operations(-1.1);
    four_operations(-19.9);
}

Que imprime:

For the number 9.1:
floor: 9
ceiling: 10
rounded: 9 // because less than 9.5
truncated: 9

For the number 100.7:
floor: 100
ceiling: 101
rounded: 101 // because more than 100.5
truncated: 100

For the number -1.1:
floor: -2
ceiling: -1
rounded: -1
truncated: -1

For the number -19.9:
floor: -20
ceiling: -19
rounded: -20
truncated: -19

f32 y f64 tienen métodos denominados .max() y .min() que devuelven el menor y mayor número de dos (para otros tipos se puede usar std::cmp::max y std::cmp::min). A continuación se muestra una forma de obtener el número mayor y el menor usando .fold():

fn main() {
    let my_vec = vec![8.0_f64, 7.6, 9.4, 10.0, 22.0, 77.345, 10.22, 3.2, -7.77, -10.0];
    let maximum = my_vec.iter().fold(f64::MIN, |current_number, next_number| current_number.max(*next_number)); // Nota: inicia con el menor f64 existente.
    let minimum = my_vec.iter().fold(f64::MAX, |current_number, next_number| current_number.min(*next_number)); // Y en este se inicia con el mayor posible
    println!("{}, {}", maximum, minimum);
}

bool

En Rust, se pueden convertir los valores bool a enteros: es seguro hacerlo. true se convierte en 1 y `false' en '0'. Sin embargo, no es posible hacer la conversión en sentido opuesto.

fn main() {
    let true_false = (true, false);
    println!("{} {}", true_false.0 as u8, true_false.1 as i32);
}

Que imprime 1 0. O se puede usar .into(), diciéndole al compilador el tipo:

fn main() {
    let true_false: (i128, u16) = (true.into(), false.into());
    println!("{} {}", true_false.0, true_false.1);
}

Que imprime lo mismo.

Desde Rust 1.50 (liberado en Febrero 2021), existe un método denominado then(), que convierte un bool en Option. En el método .then() se pasa como parámetro un cierre (closure) que solo se llama si el elemento es true. El valor de retorno del cierre se guarda como valor de retorno en el Option. Por ejemplo:

fn main() {

    let (tru, fals) = (true.then(|| 8), false.then(|| 8));
    println!("{:?}, {:?}", tru, fals);
}

Que imprime Some(8), None.

A continuación un ejemplo un poco más largo:

fn main() {
    let bool_vec = vec![true, false, true, false, false];
    
    let option_vec = bool_vec
        .iter()
        .map(|item| {
            item.then(|| { // Lo incluye en un map para poder pasarlo
                println!("¡Tengo un {}!", item);
                "Tiene valor true" // Esto va dentro de Some si es true
                                      // En otro caso, pasa None
            })
        })
        .collect::<Vec<_>>();

    println!("Tenemos este resultado: {:?}", option_vec);

    // Esto imprime Nones también. Se filtran para pasarlo a un nuevo Vec.
    let filtered_vec = option_vec.into_iter().filter_map(|c| c).collect::<Vec<_>>();

    println!("Y sin los None: {:?}", filtered_vec);
}

Vec

Vec tiene muchos métodos que aún no se han revisado. En primer lugar, .sort() necesita una variable mut self para poder ordenar.

fn main() {
    let mut my_vec = vec![100, 90, 80, 0, 0, 0, 0, 0];
    my_vec.sort();
    println!("{:?}", my_vec);
}

Esto imprime [0, 0, 0, 0, 0, 80, 90, 100]. Existe otro método que suele ser más rápido denominado .sort_unstable(). En este caso, no se preocupa del orden de los números si son el mismo. En el caso de .sort() se sabe que el último 0, 0, 0, 0, 0 estará en el mismo orden después. En el caso de .sort_unstable() podría pasar que el último cero estuviese en la posición 0, el tercero inicialmente en la posición 2, etc.

.dedup() significa "quitar duplicados". Elimina los elementos iguales que están en un vector, pero solo si están uno junto a otro. El código siguiente no solo imprime "sun", "moon", sino que mantiene repetidos, siempre que no estuvieran juntos.

fn main() {
    let mut my_vec = vec!["sun", "sun", "moon", "moon", "sun", "moon", "moon"];
    my_vec.dedup();
    println!("{:?}", my_vec);
}

El resultado es ["sun", "moon", "sun", "moon"].

Si se quieren eliminar todos los duplicados es necesario usar .sort() antes:

fn main() {
    let mut my_vec = vec!["sun", "sun", "moon", "moon", "sun", "moon", "moon"];
    my_vec.sort();
    my_vec.dedup();
    println!("{:?}", my_vec);
}

Así, el resultado es ["moon", "sun"].

String

Se recordará que un String es un tipo de Vec, por lo tanto se pueden usar muchos de los métodos de los vectores. POr ejemplo, se puede iniciar una cadena de caracteres String::with_capacity(). Por ejemplo, puede ser util para ser eficiente cuando se prevé que se van a añadir caracteres con .push() o .push_str() (cuando se va a insertar un &str).

El siguiente ejemplo, es poco eficiente:

fn main() {
    let mut push_string = String::new();
    let mut capacity_counter = 0; // la capacidad se inicia a 0
    for _ in 0..100_000 { // Hace esto 100,000 veces
        if push_string.capacity() != capacity_counter { // Comprueba si ha variado la capacidad
            println!("{}", push_string.capacity()); // Si ha variado, se muestra la nueva
            capacity_counter = push_string.capacity(); // y se guarda en el contador
        }
        push_string.push_str("I'm getting pushed into the string!"); // y añade esto a la cadena cada vez
    }
}

Esto imprime

35
70
140
280
560
1120
2240
4480
8960
17920
35840
71680
143360
286720
573440
1146880
2293760
4587520

Durante esta ejecución, ha habido que mover la cadena de sitio en memoria 18 veces. Y se conoce la capacidad final. Se puede crear la cadena de caracteres con la capacidad necesaria desde el inicio.

fn main() {
    let mut push_string = String::with_capacity(4587520); // Capacidad necesaria
    let mut capacity_counter = 0;
    for _ in 0..100_000 {
        if push_string.capacity() != capacity_counter {
            println!("{}", push_string.capacity());
            capacity_counter = push_string.capacity();
        }
        push_string.push_str("I'm getting pushed into the string!");
    }
}

Esto imprime 4587520 una sola vez. No ha habido que mover la cadena de caracteres ni una sola vez.

En este caso, la longitud real es un poco menor. Esto se debe a que Rust duplica la capacidad de una cadena de caracteres cada vez que necesita moverla. Existe el método .shrink_to_fit() (igual que en Vec). Así se puede reducir el tamaño al espacio realmente ocupado.

fn main() {
    let mut push_string = String::with_capacity(4587520);
    let mut capacity_counter = 0;
    for _ in 0..100_000 {
        if push_string.capacity() != capacity_counter {
            println!("{}", push_string.capacity());
            capacity_counter = push_string.capacity();
        }
        push_string.push_str("I'm getting pushed into the string!");
    }
    push_string.shrink_to_fit();
    println!("{}", push_string.capacity());
    push_string.push('a');
    println!("{}", push_string.capacity());
    push_string.shrink_to_fit();
    println!("{}", push_string.capacity());
}

Que imprime:

4587520
3500000
7000000
3500001

La primera vez, una vez completa, la cadena de caracteres ocupa 4587520, pero no se está usando todo. Se usa .shrink_to_fit() y pasa a ocupar 35000000. Después se añade un a al final. En ese momento, Rust determina que necesita más espacio y dobla la capacidad anterior a 7000000. Una nueva ejecución de .shrink_to_fit() lo reduce a 3500001.

.pop() funciona con una String igual que en un Vec.

fn main() {
    let mut my_string = String::from(".daer ot drah tib elttil a si gnirts sihT");
    loop {
        let pop_result = my_string.pop();
        match pop_result {
            Some(character) => print!("{}", character),
            None => break,
        }
    }
}

Esto imprime This string is a little bit hard to read, porque está dada la vuelta.

.retain() es un método que usa un cierre com parámetro (lo cual es raro en este tipo String). Funciona como .filter en un iterador.

fn main() {
    let mut my_string = String::from("Age: 20 Height: 194 Weight: 80");
    my_string.retain(|character| character.is_alphabetic() || character == ' '); // consérvalo si es una letra o espacio
    dbg!(my_string); // Solo por variar, se usa dbg!() esta vez en lugar de println!
    // Se imprime solo en la consola de error
}

Esto imprime:

[src\main.rs:4] my_string = "Age  Height  Weight "

OsString y CString

std::ffi es la parte de la librería std que ayuda a usar Rust con otros lenguajes o sistemas operativos. Tiene tipos como OsString y CString que son como String del sistema operativo o String del lenguaje C. Cada uno tiene sus propias versiones de &str: OsStr y CStr. ffi significa "foreign function interface" (interfaz para funciones externas).

Se puede usar OsString para trabajar con un Sistema Operativo que no tenga unicode. Todas las cadenas de caracteres de Rust son unicode, pero no todos los sistemas operativos las usan. La explicación de la librería estándar dice lo siguiente:

  • Una cadena de caracteres de Unix (Linux, etc) podrían ser un conjunto de bytes juntos sin ceros. En ocasiones es necesario leerla como Unicode UTF-8.
  • Una cadena de caracteres de Windows podría estar compuesta de valores de 16 bits que no tengan ceros. También puede ser necesario leerla como Unicode UTF-16.
  • En Rust, las cadenas de caracteres siempre están en UTF-8, que sí puede contener ceros.

Con OsString se pueden hacer las cosas habituales que se hacen con String como OsString::from("Escribe algo aquí"). También dispone de un método interesante .into_string() que intenta convertirla en una String de Rust. Devuelve un Result en el que la parte Err es la cadena original OsString.

#![allow(unused)]
fn main() {
// 🚧
pub fn into_string(self) -> Result<String, OsString>
}

Si no funciona, simplemente se dispone de la cadena original del sistema opertivo. En este Result no es posible ejecutar .unwrap() porque el sistema entra en pánico, pero se puede recuperar usando match. Se prueba llamando a métodos que no existen.

use std::ffi::OsString;

fn main() {
    // ⚠️
    let os_string = OsString::from("This string works for your OS too.");
    match os_string.into_string() {
        Ok(valid) => valid.thth(),           // Compilador: "Qué es .thth()??"
        Err(not_valid) => not_valid.occg(),  // Compilador: "Qué es .occg()??"
    }
}

El compilador indica exactamente lo que se necesita conocer. Al haber puesto métodos que no existen, el compilador indica el tipo de dato que no tiene este elemento.

error[E0599]: no method named `thth` found for struct `std::string::String` in the current scope
 --> src/main.rs:6:28
  |
6 |         Ok(valid) => valid.thth(),
  |                            ^^^^ method not found in `std::string::String`

error[E0599]: no method named `occg` found for struct `std::ffi::OsString` in the current scope
 --> src/main.rs:7:37
  |
7 |         Err(not_valid) => not_valid.occg(),
  |                                     ^^^^ method not found in `std::ffi::OsString`

Se ve que en el primer caso, el caso correcto, devolvería String y en el caso incorrecto devolvería OsString:

mem

std::mem tiene métodos muy interesantes. Ya se han visto algunos, como .size_of(), .size_of_val() y .drop()-

use std::mem;

fn main() {
    println!("{}", mem::size_of::<i32>());
    let my_array = [8; 50];
    println!("{}", mem::size_of_val(&my_array));
    let mut some_string = String::from("Se puede hacer drop de String porque está en el heap");
    mem::drop(some_string);
    // some_string.clear();   Esto podría entrar en pánico

Esto imprime:

4
200

A continuación, se presentan algunos métodos de mem:

swap(): intercambia valors entre dos variables. Necesita una referencia mutable para cada una de ellas. Es útil cuando tienes dos tienes dos cosas que se quieren intercambiar y Rust no deja por las reglas de préstamo. O simplemente, cuando se necesita hacer un intercambio rápido entre dos variables.

A continuación, se muestra un ejemplo:

use std::{mem, fmt};

struct Ring { // Crea un anillo del Señor de los Anillos
    owner: String,
    former_owner: String,
    seeker: String, // es la persona buscándolo
}

impl Ring {
    fn new(owner: &str, former_owner: &str, seeker: &str) -> Self {
        Self {
            owner: owner.to_string(),
            former_owner: former_owner.to_string(),
            seeker: seeker.to_string(),
        }
    }
}

impl fmt::Display for Ring { // Para mostrar quién lo tiene y quién lo busca
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{} tiene el anillo, {} solía tenerlo, {} lo quiere", self.owner, self.former_owner, self.seeker)
        }
}

fn main() {
    let mut one_ring = Ring::new("Frodo", "Gollum", "Sauron");
    println!("{}", one_ring);
    mem::swap(&mut one_ring.owner, &mut one_ring.former_owner); // Gollum tiene el anillo fugazmente
    println!("{}", one_ring);
}

Esto imprimirá:

Frodo tiene el anillo, Gollum solía tenerlo, Sauron lo quiere
Gollum tiene el anillo, Frodo solía tenerlo, Sauron lo quiere

replace(): se parece a swap y lo usa internamente, como se puede ver:

#![allow(unused)]
fn main() {
pub fn replace<T>(dest: &mut T, mut src: T) -> T {
    swap(dest, &mut src);
    src
}
}

Lo único que hace es conmutar los valores y devolver el antiguo elemento. Así, se puede usar con let. Como por ejemplo:

use std::mem;

struct City {
    name: String,
}

impl City {
    fn change_name(&mut self, name: &str) {
        let old_name = mem::replace(&mut self.name, name.to_string());
        println!(
            "La ciudad llamada en el pasado {} ahora se llama {}.",
            old_name, self.name
        );
    }
}

fn main() {
    let mut capital_city = City {
        name: "Constantinopla".to_string(),
    };
    capital_city.change_name("Estambul");
}

Esto imprime La ciudad llamada en el pasado Constantinopla ahora se llama Estambul..

.take() es una como .replace(), pero lo sustituye por el valor por defecto en el elemento. Se recordará que los valores por defecto suelen ser cosas como 0, "" o similar. Esta es su declaración:

#![allow(unused)]
fn main() {
// 🚧
pub fn take<T>(dest: &mut T) -> T
where
    T: Default,
}

Se pueden hacer cosas como:

use std::mem;

fn main() {
    let mut number_vec = vec![8, 7, 0, 2, 49, 9999];
    let mut new_vec = vec![];

    number_vec.iter_mut().for_each(|number| {
        let taker = mem::take(number);
        new_vec.push(taker);
    });

    println!("{:?}\n{:?}", number_vec, new_vec);
}

Como se puede ver en el resultado:

[0, 0, 0, 0, 0, 0]
[8, 7, 0, 2, 49, 9999]

Reemplaza todos los números con 0, pero no elimina ningún elemento.

En el caso de tipos propios, Default puede implementar lo que se necesite. En el siguiente ejemplo se dispone de un Banco y un Ladron. Cada vez que roba el Banco, se lleva el dinero del mostrador. Pero el mostrador puede tomar dinero del interior del bano siempre que se necesita, por lo que siempre tiene 50. Se va a construir este tipo para que ese sea su valor por defecto.

use std::mem;
use std::ops::{Deref, DerefMut}; // Se usa esto para obtener la potencia de u32

struct Banco {
    dinero_dentro: u32,
    dinero_en_mostrador: DineroMostrador, // Es un "puntero inteligente". tiene su propio Default, pero usa u32
}

struct DineroMostrador(u32);

impl Default for DineroMostrador {
    fn default() -> Self {
        Self(50) // Siempre 50, no 0
    }
}

impl Deref for DineroMostrador { // Con este rasgo se puede acceder al u32 usando *
    type Target = u32;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for DineroMostrador { // Y con esto se puede restar, sumar, etc.
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl Banco {
    fn comprobar_dinero(&self) {
        println!(
            "Hay {}€ en el banco y {}€ en el mostrador.\n",
            self.dinero_dentro, *self.dinero_en_mostrador // usa * para imprimir el u32
        );
    }
}

struct Ladron {
    dinero_en_bolsillo: u32,
}

impl Ladron {
    fn comprobar_dinero(&self) {
        println!("El ladrón tiene {}€ en este momento.\n", self.dinero_en_bolsillo);
    }

    fn robar_banco(&mut self, Banco: &mut Banco) {
        let nuevo_dinero = mem::take(&mut Banco.dinero_en_mostrador); // Toma el dinero, pero deja siempre 50€ que es el valor por defecto.
        self.dinero_en_bolsillo += *nuevo_dinero; // Usa * porque solo se pueden sumar u32. DineroMostrador no puede sumar
        Banco.dinero_dentro -= *nuevo_dinero;    // Igual aquí
        println!("¡Ha robado el banco, ahora tiene {}€!\n", self.dinero_en_bolsillo);
    }
}

fn main() {
    let mut banco_de_klezkavania = Banco { // Prepara el banco
        dinero_dentro: 5000,
        dinero_en_mostrador: DineroMostrador(50),
    };
    banco_de_klezkavania.comprobar_dinero();

    let mut ladron = Ladron { // Prepara al Ladron
        dinero_en_bolsillo: 50,
    };
    ladron.comprobar_dinero();

    ladron.robar_banco(&mut banco_de_klezkavania); // Roba, después comprueba el dinero
    ladron.comprobar_dinero();
    banco_de_klezkavania.comprobar_dinero();

    ladron.robar_banco(&mut banco_de_klezkavania); // Vuelve a robar
    ladron.comprobar_dinero();
    banco_de_klezkavania.comprobar_dinero();

}

Que imprime:

Hay 5000€ en el banco y 50€ en el mostrador.

El ladrón tiene 50€ en este momento.

¡Ha robado el banco, ahora tiene 100€!

El ladrón tiene 100€ en este momento.

Hay 4950€ en el banco y 50€ en el mostrador.

¡Ha robado el banco, ahora tiene 150€!

El ladrón tiene 150€ en este momento.

Hay 4900€ en el banco y 50€ en el mostrador.

Se puede obsverar que siempre hay 50€ en el mostrador.

prelude

La librería estándar tiene también un preludio, que es lo que hace que no haya que escribir cosas como use std::vec::Vec para crear un Vec. Se pueden ver todos los elementos que contiene aquí. Ya han aparecido casi todos ellos:

  • std::marker::{Copy, Send, Sized, Sync, Unpin}. No se ha visto Unpin antes. Se usa en casi cualquier tipo (como Sized, que también es muy común). Pin significa que no se puede mover de su lugar en la memoria, pero la mayoría de los elementos implementan Unpin, por lo que es posible moverlos. Por ello funcionan métodos como std::mem::replace.
  • std::ops::{Drop, Fn, FnMut, FnOnce}.
  • std::mem::drop
  • std::boxed::Box.
  • std::borrow::ToOwned. Se ha visto antes con Cow, que puede tomar contenido prestado y convertirlo en propiedad suya. Utiliza .to_owned() para hacerlo. También se puede usar .to_owned() en un &str para convertirlo en String y lo mismo para otros valores prestados.
  • std::clone::Clone
  • std::cmp::{PartialEq, PartialOrd, Eq, Ord}.
  • std::convert::{AsRef, AsMut, Into, From}.
  • std::default::Default.
  • std::iter::{Iterator, Extend, IntoIterator, DoubleEndedIterator, ExactSizeIterator}. Anteriormente, se usó .rev() en un iterador: en realidad esta función crea un DoubleEndedIterator. Un ExactSizeIterator es algo como 0..10: se sabe al inicio que tiene una longitud de 10 elementos. Otros iteradores no conocen su tamaño con seguridad.
  • std::option::Option::{self, Some, None}.
  • std::result::Result::{self, Ok, Err}.
  • std::string::{String, ToString}.
  • std::vec::Vec.

Si por alguna razón no se deseara cargar el preludio, se debe añadir el atributo #![no_implicit_prelude]. Se prueba lo siguiente y se observa que el compilador se queja:

// ⚠️
#![no_implicit_prelude]
fn main() {
    let my_vec = vec![8, 9, 10];
    let my_string = String::from("Esto no funciona");
    println!("{:?}, {}", my_vec, my_string);
}

Ahora, Rust desconoce qué son determinados elementos y no compila.

error: cannot find macro `println` in this scope
 --> src/main.rs:5:5
  |
5 |     println!("{:?}, {}", my_vec, my_string);
  |     ^^^^^^^

error: cannot find macro `vec` in this scope
 --> src/main.rs:3:18
  |
3 |     let my_vec = vec![8, 9, 10];
  |                  ^^^

error[E0433]: failed to resolve: use of undeclared type or module `String`
 --> src/main.rs:4:21
  |
4 |     let my_string = String::from("This won't work");
  |                     ^^^^^^ use of undeclared type or module `String`

error: aborting due to 3 previous errors

Para este código tan simple, manteniendo que Rust no cargue el preludio, solo hay que añadir la librería estándar con extern crate std y luego añadir los elementos que se usan realmente. Quedaría como sigue:

#![no_implicit_prelude] 
extern crate std; // Hay que decirle a Rust que se quiere usar la librería std
use std::vec; // se necesita la macro vec
use std::string::String; // y string
use std::convert::From; // Y este rasgo para convertir de &str a String
use std::println; // y esto para imprimir

fn main() {
    let my_vec = vec![8, 9, 10];
    let my_string = String::from("Esto sí funciona");
    println!("{:?}, {}", my_vec, my_string);
}

Ahora sí funciona e imprime [8, 9, 10], Esto sí funciona.

Además, se puede llegar a utilizar un atributo #![no_std] (se vio anteriormente) cuando no se puede usar ni la pila. La mayor parte del tiempo no es necesario quitar el preludio o std.

En el pasado se usaba mucho más la palabra clave extern. Era necesario para cualquier librería externa que se quisiera usar. Por ejemplo, para usar rand era necesario escribir:


#![allow(unused)]
fn main() {
extern crate rand;
}

y después las sentencias use que fuesen necesarias para incorporar módulos, rasgos, etc. El compilador ya no lo necesita, es suficiente con expresar los diferentes use y el propio compilador se encarga de encontrarlos en las librerías a que correspondan.

time

std::time es donde se encuentran las funciones relacionadas con la fecha y hora (si son necesarias más, se puede usar la librería chrono). La función más sencilla es la que recupera la hora del sistema Instant::now().

use std::time::Instant;

fn main() {
    let time = Instant::now();
    println!("{:?}", time);
}

Si se imprime, se obtiene algo como esto: Instant { tv_sec: 432756, tv_nsec: 504281663 }. Muestra segundos y nanosegundos, lo que no resulta muy útil. La página de documentación de Instant indica ques "opaco y solo es útil con Duration". Solo es útil, comparando distintos momentos del tiempo.

Si se observan los rasgos de este tipo, uno de ellos es Sub<Instant>. Se pueden restar unos de otros. Si se ve su código fuente:

#![allow(unused)]
fn main() {
impl Sub<Instant> for Instant {
    type Output = Duration;

    fn sub(self, other: Instant) -> Duration {
        self.duration_since(other)
    }
}
}

Toma un Instant y usa .duration_since() para obtener una Duration. Para probarlo, se van a tomar dos instantes separados por un cierto intervalo:

use std::time::Instant;

fn main() {
    let time1 = Instant::now();
    let time2 = Instant::now(); // Estos dos instantes están muy cerca entre sí

    let mut new_string = String::new();
    loop {
        new_string.push('წ'); // Se va a crear un String añadiendo una letra 100.000 veces
        if new_string.len() > 100_000 { //  hasta que es de longitud 100.000 
            break;
        }
    }
    let time3 = Instant::now();
    println!("{:?}", time2 - time1);
    println!("{:?}", time3 - time1);
}

Esto imprimirá un Duration:

1.025µs
683.378µs

En este ejemplo, uno representa un poco más de 1 microsegundo vs. 683 microsegundos. Se observa que construir la cadena de caracteres llevó su tiempo.

Hay una última cosa que se puede hacer con un único Instant. Convertirlo a String con format!("{:?}", Instant::now());:

use std::time::Instant;

fn main() {
    let time1 = format!("{:?}", Instant::now());
    println!("{}", time1);
}

Esto imprime algo así como Instant { tv_sec: 433468, tv_nsec: 406649320 }. Si se usa .iter(), .rev() y .skip(2), es posible saltar la llave } final y se puede crear un generador de números aleatorios.

use std::time::Instant;

fn bad_random_number(digits: usize) {
    if digits > 9 {
        panic!("El número debe ser como máximo de 9 dígitos");
    }
    let now = Instant::now();
    let output = format!("{:?}", now);

    output
        .chars()
        .rev()
        .skip(2)
        .take(digits)
        .for_each(|character| print!("{}", character));
    println!();
}

fn main() {
    bad_random_number(1);
    bad_random_number(1);
    bad_random_number(3);
    bad_random_number(3);
}

Esto imprimirá algo así como:

#![allow(unused)]
fn main() {
6
4
967
180
}

No es un buen generador de números aleatorios. Rust tiene librerías mucho mejores para ello, como rand y fastrand. Pero es un ejemplo de lo que se puede hacer con Instant y cierta imaginación.

En un hilo es posible parar durante un tiempo con std::thread::sleep. Cuando se hace esto hay que darle una duración. Para obtener las unidades necesarias, se puede usar Duration::from_millis(), Duration::from_secs(), etc. Por ejemplo:

use std::time::Duration;
use std::thread::sleep;

fn main() {
    let three_seconds = Duration::from_secs(3);
    println!("Me voy a dormir.");
    sleep(three_seconds);
    println!("¿Me he perdido algo?");
}

Esto imprimirá:

Me voy a dormir.
¿Me he perdido algo?

Pero el hilo no hará nada durante esos tres segundos. Normalmente, se usa .sleep() cuando existen diversos hilos que tienen que repetir o intentar algo varias veces, como conectarse a un servicio. En estos casos, se intenta o repite la acción, después se duerme duratne un tiempo establecido, hasta que se completa y se vuelve ha realizar la acción programada. Y así cada vez que se despierta el hilo.

Otras macros

A continuación, se recorren algunas otras macros de Rust.

unreachable!()

Es una especie de todo!() salvo por ser para código que nunca se usará. Es posible que exista un match en la que se sepa que una de las ramas nunca se alcanza. Si es así, se escribe esta macro para que el compilador lo sepa y pueda ignorar esa parte.

Por ejemplo, se escribe un programa que imprime algo cada vez que se elige un país para vivir. Están en Ucrania y todos son lugares elegibles salvo Chernobyl. El código puede ser como sigue:

enum UkrainePlaces {
    Kiev,
    Kharkiv,
    Chernobyl, 
    Odesa,
    Dnipro,
}

fn choose_city(place: &UkrainePlaces) {
    use UkrainePlaces::*;
    match place {
        Kiev => println!("Vivirás en Kiev"),
        Kharkiv => println!("Vivirás en Kharkiv"),
        Chernobyl => unreachable!(),
        Odesa => println!("Vivirás en Odesa"),
        Dnipro => println!("Vivirás en Dnipro"),
    }
}

fn main() {
    let user_input = UkrainePlaces::Kiev; // El usuario introduciría el lugar de algún modo sin poder elegir Chernobyl
    choose_city(&user_input);
}

Este código imprimirá Vivirás en Kiev.

unreachable!() es útil para recordar que esa parte del programa no se va a ejecutar nunca. Hay que estar seguro de ello. Si el compilador llega a entrar en esta función, el programa entrará en pánico.

El compilador avisará en los casos en que tenga claro que el programador se ha equivocado y hay código inalcanzable que no se haya marcado como tal. Por ejemplo:

fn main() {
    let true_or_false = true;

    match true_or_false {
        true => println!("It's true"),
        false => println!("It's false"),
        true => println!("It's true"), // Vaya, se ha vuelto a escribir true
    }
}

Lo anterior, dará el siguiente aviso:

warning: unreachable pattern
 --> src/main.rs:7:9
  |
7 |         true => println!("It's true"),
  |         ^^^^
  |

column!, line!, file!, module_path!

Estas cuatro macros son parecidas a dbg!() porque solo se incorporan para obtener información de depuración. No necesitan parámetros, se uan con los paréntesis, sin nada más. Son fáciles de aprender juntas:

  • column!() muestra la columna en la que se escribió.
  • file!() muestra el nombre del fichero en el que se escribió.
  • line!() muestra el número de lína en la que se escribió.
  • module_path!() muestra el módulo en el que se encuentra.

El código siguiente muestra su uso. Se simula la creación de diversos módulos (unos dentro de otros) para que se vea el resultado de estas macros:

pub mod something {
    pub mod third_mod {
        pub fn print_a_country(input: &mut Vec<&str>) {
            println!(
                "The last country is {} inside the module {}",
                input.pop().unwrap(),
                module_path!()
            );
        }
    }
}

fn main() {
    use something::third_mod::*;
    let mut country_vec = vec!["Portugal", "Czechia", "Finland"];
    
    // do some stuff
    println!("Hello from file {}", file!());

    // do some stuff
    println!(
        "On line {} we got the country {}",
        line!(),
        country_vec.pop().unwrap()
    );

    // do some more stuff

    println!(
        "The next country is {} on line {} and column {}.",
        country_vec.pop().unwrap(),
        line!(),
        column!(),
    );

    // lots more code

    print_a_country(&mut country_vec);
}

Este código imprime:

Hello from file src/main.rs
On line 23 we got the country Finland
The next country is Czechia on line 32 and column 9.
The last country is Portugal inside the module playground::something::third_mod

cfg!

Se sabe que se pueden usar atributos como #[cfg(test)] y #[cfg(windows)] para informar al compilador qué hacer en ciertos casos. En el caso de test se ejecuta el código marcado por el atributo si se está ejecutando el programa en modo test (usando cargo test). Cuando se usa windows, se ejecuta el código cuando el sistema operativo es Windows. Pero puede ser que solo se quiera cambiar una pequeña parte de código dependiendo del sistema operativo en que se ejecute. Es en este caso, cuando esta macro es útil. Devuelve un bool.

fn main() {
    let helpful_message = if cfg!(target_os = "windows") { "backslash" } else { "slash" };

    println!(
        "...then in your hard drive, type the directory name followed by a {}. Then you...",
        helpful_message
    );
}

Este trozo de código imprimirá diferente texto en función del sistema operativo en que se encuentre. El playground de Rust se ejecuta en Linux, por lo que se imprime:

...then in your hard drive, type the directory name followed by a slash. Then you...

cfg!() funciona para cualquier configuración disponible. El código siguiente muestra cómo ejecutar algo cuando se está en un test.

#[cfg(test)] // cfg! buscará la existencia de este atributo de configuración
mod testing {
    use super::*;
    #[test]
    fn check_if_five() {
        assert_eq!(bring_number(true), 5); // Esta función comprueba que bring_number() devolverá 5
    }
}

fn bring_number(should_run: bool) -> u32 { // esta función comprueba si debe ejecutarse o no
    if cfg!(test) && should_run { // Si se tiene que ejecutar y está en test, devuelve 5
        5
    } else if should_run { // Si no es test devuelve 5 e imprime
        println!("Returning 5. This is not a test");
        5
    } else {
        println!("This shouldn't run, returning 0."); // en otro caso devuelve 0
        0
    }
}

fn main() {
    bring_number(true);
    bring_number(false);
}

Este código devolverá:

Returning 5. This is not a test
This shouldn't run, returning 0.

Cuando no está en modo test. Cuando se ejecuta en modo test (cargo test), ejecutará la preuba y en este caso, siempre devuelve 5, por lo que pasará el test.

running 1 test
test testing::check_if_five ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

La escritura de macros

Escribir una macro puede ser mucho complejo. Casi nunca necesitarás escribir una, pero en ocasiones puede ser necesario. Sus reglas de lenguaje pueden ser muy diferentes. Una forma de escribir una macro nueva es usar la macro macro_rules!, dándole un nombre y seguida de un bloque {}. Dentro del bloque, se comporta como una especie de sentencia match.

A continuación, se muestra un ejemplo que solo toma () como parámetro y devuelve únicamente el número 6.

macro_rules! give_six {
    () => {
        6
    };
}

fn main() {
    let six = give_six!();
    println!("{}", six);
}

Pero no es lo mismo que una sentencia match, debido a que en realidad no compila nada. Solo usa la entrada y devuelve una salida. Con esa salida, el compilador comprueba si tiene sentido. Por eso se dice que las macros son "código que escribe código". En una macro, además, los valores de retorno tienen que ser del mismo tipo, por lo que el siguiente código no funcionaría:

fn main() {
// ⚠️
    let my_number = 10;
    match my_number {
        10 => println!("You got a ten"),
        _ => 10,
    }
}

Dará error indicando que una de las ramas devuelve (), mientras que otra devuelve un 10.

error[E0308]: `match` arms have incompatible types
 --> src\main.rs:5:14
  |
3 | /     match my_number {
4 | |         10 => println!("You got a ten"),
  | |               ------------------------- this is found to be of type `()`
5 | |         _ => 10,
  | |              ^^ expected `()`, found integer
6 | |     }
  | |_____- `match` arms have incompatible types

En el caso de una macro, sí se puede "devolver" diferente código, no es algo compilado, es código lo que se devuelve. Por lo que se puede hacer esto:

macro_rules! six_or_print {
    (6) => {
        6
    };
    () => {
        println!("You didn't give me 6.");
    };
}

fn main() {
    let my_number = six_or_print!(6);
    six_or_print!();
}

Lo anterior es correcto e imprime You didn't give me 6.. Tampoco existe una rama _ porque funciona como un match. Solo se le puede pasar como parámetro (6) o (). Cualquier otra cosa dará error. El 6 que se está pasando, no es de tipo i32, no es de ningún tipo, solo es el código de Rust 6. Esto hace que la entrada de una macro pueda ser cualquier cosa. Se puede pasar cualquier cosa, porque internamente solo se mira lo que se pasa, para determinar qué hacer. Por ejemplo:

macro_rules! might_print {
    (THis is strange input 하하はは哈哈 but it still works) => {
        println!("You guessed the secret message!")
    };
    () => {
        println!("You didn't guess it");
    };
}

fn main() {
    might_print!(THis is strange input 하하はは哈哈 but it still works);
    might_print!();
}

La macro anterior solo responde a dos entradas () y (THis is strange input 하하はは哈哈 but it still works). A nada más. En este caso imprime:

You guessed the secret message!
You didn't guess it

En conclusión, una macro no sigue exactamente la sintaxis habitual de Rust. Pero una macro sí puede comprender diferentes tipos de entrada. Por ejemplo:

macro_rules! might_print {
    ($input:expr) => {
        println!("You gave me: {}", $input);
    }
}

fn main() {
    might_print!(6);
}

Esto imprime You gave me: 6. La parte $input:expr es importante. Significa: si hay una expresión de entrada, asígnala a la variable $input. En las macros, las variables comienzan con el símbolo $. En esta macro, si se le pasa una expresión, la imprimirá. El siguiente ejemplo intenta algo más:

macro_rules! might_print {
    ($input:expr) => {
        println!("You gave me: {:?}", $input); // Now we'll use {:?} because we will give it different kinds of expressions
    }
}

fn main() {
    might_print!(()); // give it a ()
    might_print!(6); // give it a 6
    might_print!(vec![8, 9, 7, 10]); // give it a vec
}

Que imprimirá:

You gave me: ()
You gave me: 6
You gave me: [8, 9, 7, 10]

Se debe observar que se escribió {:?}, pero en el momento de la macro no se comprueba si &input implementa Debug. Solo generará el código correspondiente println!("You gave me: {:?}", ()) y lo intenta compilar y si no compila da error.

Aparte de expr, una macro puede recibir estos tipos de parámetro: block, expr, ident, item, lifetime, literal, meta, pat, path, stmt, tt, ty y vis. Esta parte es compleja, se puede ver lo que significa cada una aquí, que dice:

item: an Item
block: a BlockExpression
stmt: a Statement without the trailing semicolon (except for item statements that require semicolons)
pat: a Pattern
expr: an Expression
ty: a Type
ident: an IDENTIFIER_OR_KEYWORD
path: a TypePath style path
tt: a TokenTree (a single token or tokens in matching delimiters (), [], or {})
meta: an Attr, the contents of an attribute
lifetime: a LIFETIME_TOKEN
vis: a possibly empty Visibility qualifier
literal: matches -?LiteralExpression

Hay otro lugar denominado cheats.rs que los explica con ejemplos.

Sin embargo, para la mayoría de macros, será suficiente usar expr, ident y tt. ident significa identificador y sirve para pasar variables o nombres de función. tt significa árbol de elementos (token tree), que permite cualquier tipo de entrada. A continuación se muestra un ejemplo usando ambos:

macro_rules! check {
    ($input1:ident, $input2:expr) => {
        println!(
            "Is {:?} equal to {:?}? {:?}",
            $input1,
            $input2,
            $input1 == $input2
        );
    };
}

fn main() {
    let x = 6;
    let my_vec = vec![7, 8, 9];
    check!(x, 6);
    check!(my_vec, vec![7, 8, 9]);
    check!(x, 10);
}

Esta macro recibe un ident (el nombre de una variable, por ejemplo) y una expr expresión y comprueban si son iguales. En el ejemplo se imprime:

Is 6 equal to 6? true
Is [7, 8, 9] equal to [7, 8, 9]? true
Is 6 equal to 10? false

A continuación se muestra una macro que recibe un tt y lo imprime. Usa otra macro denominada stringify! que construye una cadena de lo que recibe:

macro_rules! print_anything {
    ($input:tt) => {
        let output = stringify!($input);
        println!("{}", output);
    };
}

fn main() {
    print_anything!(ththdoetd);
    print_anything!(87575oehq75onth);
}

Que imprime:

ththdoetd
87575oehq75onth

Pero no imprimirá nada si se le pasa algo con espacios, comas, etc. La macro creerá que se le está pasando más de un elemento u otra información extra y no se cumplirá la selección.

Esto es lo que hace que las macros comiencen a ser difíciles.

Para pasar más de un elemento a una macro es necesario utilizar una sintaxis diferente. En lugar de $input. debería ser $($input1),*. Esto signfica cero o más (es para lo que está el *), seperados por una coma. Si lo que se quiere es una o más, se utilizar + en lugar de *.

Ahora, la macro quedaría así:

macro_rules! print_anything {
    ($($input1:tt),*) => {
        let output = stringify!($($input1),*);
        println!("{}", output);
    };
}


fn main() {
    print_anything!(ththdoetd, rcofe);
    print_anything!();
    print_anything!(87575oehq75onth, ntohe, 987987o, 097);
}

Recibe un árbol de tokens separado por comas y utiliza stringify! para convertirlo en una cadena de caracteres. Luego, la imprime. El resultado es:

ththdoetd, rcofe

87575oehq75onth, ntohe, 987987o, 097

Si se usara + en lugar de * daría error debido a que en uno de los usos no se pasa ningún parámetro. Por ello, es más seguro * para este uso.

Ahora se puede empezar a ver la potencia de las macros. En el siguiente ejemplo se usa una macro para construir una función:

macro_rules! make_a_function {
    ($name:ident, $($input:tt),*) => { // En primer lugar se le da nombre a la función y luego se comprueba todo lo demás
        fn $name() {
            let output = stringify!($($input),*); // La función imprimirá el resto de los parámetros
            println!("{}", output);
        }
    };
}


fn main() {
    make_a_function!(print_it, 5, 5, 6, I); // Crea la función print_it() que imprime todo lo que se pasa como parámetro
    print_it();
    make_a_function!(say_its_nice, this, is, really, nice); // igual, pero con otro nombre de función
    say_its_nice();
}

Esto imprime:

5, 5, 6, I
this, is, really, nice

Ahora se puede empezar a comprender otras macros. Se puede ver que algunas de las macros que se han usado anteriormente son bastante sencillas. La macro write! que se ha usado para escribir a ficheros es como sigue:

#![allow(unused)]
fn main() {
macro_rules! write {
    ($dst:expr, $($arg:tt)*) => ($dst.write_fmt($crate::format_args!($($arg)*)))
}
}

Para usarla, se introduce:

  • Una expresión (expr) que se asigna a la variable $dst.
  • Todo lo que siga a esa expresión se asigna a $($arg)*, ya que se ha escrito que el parámetro será $($arg:tt)*, que significa que puede seguir desde cero a cualquier número de parámetros.

Posteriormente se usa el método write_fmt de $dst para volcarlo al fichero. Aunque previamente, ser usa otra madro llamada format_args! que toma $($arg)*. Es decir, todos los parámetros recibidos.

A continuación se echa un vistazo a la macro todo!. Que se usa para que el programa compile cuando aún no se ha escrito el código. El código de esta macro es como sigue:

#![allow(unused)]
fn main() {
macro_rules! todo {
    () => (panic!("not yet implemented"));
    ($($arg:tt)+) => (panic!("not yet implemented: {}", $crate::format_args!($($arg)+)));
}
}

Esta macro tiene dos opciones de uso. Se puede usar con () o con unúmero de árboles de token ( tt ).

  • Si se usa con (), simplemente lo sustituye por panic! con un mensaje.
  • Si se introducien algunos parámetros, se intentará imprimirlos. Como en el caso anterior, se usa la macro format_args! que funciona como println!.

El siguiente código también funciona:

fn not_done() {
    let time = 8;
    let reason = "lack of time";
    todo!("Not done yet because of {}. Check back in {} hours", reason, time);
}

fn main() {
    not_done();
}

Que imprime:

thread 'main' panicked at 'not yet implemented: Not done yet because of lack of time. Check back in 8 hours', src/main.rs:4:5

Una macro se puede llamar a sí misma. Por ejemplo:

macro_rules! my_macro {
    () => {
        println!("Let's print this.");
    };
    ($input:expr) => {
        my_macro!();
    };
    ($($input:expr),*) => {
        my_macro!();
    }
}

fn main() {
    my_macro!(vec![8, 9, 0]);
    my_macro!(toheteh);
    my_macro!(8, 7, 0, 10);
    my_macro!();
}

Esta macro toma () o una expresión o muchas expresiones. Pero ignora esas posibles expresiones al volver a llamar a my_macro! sin parámetros (). POr eso, la salida es Let's print this, four times.

Se puede ver lo mismo en la macro dbg! que se llama a sí misma:

#![allow(unused)]
fn main() {
macro_rules! dbg {
    () => {
        $crate::eprintln!("[{}:{}]", $crate::file!(), $crate::line!()); //$crate significa la librería en la que se encuentra
    };
    ($val:expr) => {
        // el uso de `match` aquí es intencionado ya que afecta los ciclos de vida        
        // de los temporales - https://stackoverflow.com/a/48732525/1063961
        match $val {
            tmp => {
                $crate::eprintln!("[{}:{}] {} = {:#?}",
                    $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp);
                tmp
            }
        }
    };
    // Se ignora la coma final con un solo argumento
    ($val:expr,) => { $crate::dbg!($val) };
    ($($val:expr),+ $(,)?) => {
        ($($crate::dbg!($val)),+,)
    };
}
}

eprintln! es igual que println!, salvo que imprime a io::stderr en lugar de a io::stdout. Existe también una macro eprint! que no añade una nueva línea.

Se puede intentar esto:

fn main() {
    dbg!();
}

El código anterior, coincide con la primera rama, por lo que imprimirá el nombre del fichero y la línea usando las macros file! y line!. Imprime [src/main.rs:2].

Si se prueba con este otro código:

fn main() {
    dbg!(vec![8, 9, 10]);
}

Este código coincide con la siguiente rama, ya que es una expresión. Llamará a la entrada tmp que usa el siguiente código $crate::eprintln!("[{}:{}] {} = {:#?}", $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp);. Por lo que imprimirá file!, line! y $val convertido a una cadena de caracteres. Usa {:#?} para imprimir "bonito" tmp. Para el ejemplo del código anterior, se imprime:

[src/main.rs:2] vec![8, 9, 10] = [
    8,
    9,
    10,
]

Las ramas restantes permiten que funcione la macro incluso aunque se añada una coma de más.

Como se puede ver, la programación de macros es un tema complejo. Normalmente, se usan macros para resolver de forma automática algo que una simple función no puede hacer bien. La mejor forma de aprender a usarlas es mirar otros ejemplos de uso de macros. No es fácil escribirlas sin entrar en problemas, pero tampoco es necesario usarlas de forma perfecta para usar Rust. Partiendo de otras macros, se puede aprender mucho y programar otras de forma más sencilla, aprovechando el conocimiento de ellas.

Parte 2 - Rust en tu ordenador

Como se ha visto hasta el momento, Rust se puede aprender utilizando Playground. Pero lo normal, será desarrollar con Rust en un ordenador personal. Además, existen cosas, como utilizar ficheros o crear librerías y programas de más de un fichero, que no se pueden hacer en Playground.

Lo más importante es poder usar todo tipo de librerías (crates) para construir programas completos.

cargo

rustc signifca compilador de Rust, y es el programa que compila el código fuente. Los ficheros de rust se escriben con la extensión .rs. En todo caso, la mayoría de las personas no escriben rustc main.rs para compilar un programa. Normalmente, se usa cargo que es el programa gestor de paquetes de Rust.

Una nota sobre el nombre: se llama cargo porque cuando se unen diversas "crates" (cajas, librerías) se tiene toda una carga de cajas. Un "crate" es una caja como las que se ven en los barcos o en los camiones trailer. Pero todo proyecto Rust también se llama así "crate". Cuando se unen todas las librerías, se consigue una carga completa.

A continuación se prueba cargo para ejecutar un proyecto que utiliza la librería (crate) rand para elegir de forma aleatoria entre ocho letras posibles.

use rand::seq::SliceRandom; // Use this for .choose over slices

fn main() {

    let my_letters = vec!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];

    let mut rng = rand::thread_rng();
    for _ in 0..6 {
        print!("{} ", my_letters.choose(&mut rng).unwrap());
    }
}

El código anterior imprime algo como b c g h e a. Pero lo que se quiere ver es cómo funciona cargo. Para usarlo y ejecutar este programa, normalmente se usará cargo run. Esto compilará, construirá y ejecutará el programa. Pero cuando se empieza a compilar, se observará algo como esto:

   Compiling getrandom v0.1.14
   Compiling cfg-if v0.1.10
   Compiling ppv-lite86 v0.2.8
   Compiling rand_core v0.5.1
   Compiling rand_chacha v0.2.2
   Compiling rand v0.7.3
   Compiling rust_book v0.1.0 (C:\Users\mithr\OneDrive\Documents\Rust\rust_book)
    Finished dev [unoptimized + debuginfo] target(s) in 13.13s
     Running `C:\Users\mithr\OneDrive\Documents\Rust\rust_book\target\debug\rust_book.exe`
g f c f h b

Se ve que no solo usa rand, sino otras librerías. Esto es debido a que rand también necesita a otras librerías. cargo se encarga de encontrar todas las librerías necesarias y las compila juntas. En este caso solo se necesitan siete librerías, pero en proyectos grande pueden aparecer 200 o más librerías.

Este es un inconveniente de Rust. Es muy rápido, porque se compila por adelantado. Lo hace mirando el código y viendo qué hace. Si se escribe el siguiente código:

use std::fmt::Display;

fn print_and_return_thing<T: Display>(input: T) -> T {
    println!("You gave me {} and now I will give it back.", input);
    input
}

fn main() {
    let my_name = print_and_return_thing("Windy");
    let small_number = print_and_return_thing(9.0);
}

Esta función puede tomar cualquier tipo que tenga el rasgo Display, así que se le pasa un &str y en el siguiente uso un f64 y no falla. Pero el compilador no usa genéricos, porque no deja nada para la posterior ejecución del programa. El compilador construye un programa que pueda ejecutarse lo más rápido posible. Por eso cuando mura a la primer parte con "Windy", genera una función particular como fn print_and_return_thing(input: &str) -> &str. Y cuando se usa un f64, construye, a partir del código genérico, otra función fn print_and_return_thing(input: f64) -> f64. Todo los chequeos sobre los rasgos se hace durante la compilación y esta es la razón por la que se tarda más en compilar el programa, porque necesita analizar el código y construir las versiones concretas de cada código genérico que hay.

Otra cosa más: En 2020 se está trabajando en reducir el tiempo de compilación de Rust, puesto que es lo que más tiempo lleva. Cada versión de Rust es un poco más rápida al compilar y existen diversos planes para acelerarla más. Mientras tanto, esto es lo más importante a conocer:

  • cargo build compila el programa para que se pueda ejecutar.
  • cargo run lo compila y ejecuta.
  • cargo build --release y cargo run --release hace lo mismo que los anteriores, pero optimizado para distribuir. ¿Qué es esto? Es el modo en el que se compila el programa cuando ya se ha terminado. En este modo, Rust tarda aún más en compilar, pero lo hace para que el programa resultante sea lo más rápido posible. El modo "release" es mucho más rápido en ejecución que el modo normal, que se llama modo de depuración. Este otro modo se llama así porque es más rápido en compilar y tiene más información de depuración. El modo cargo build normal se llama "construcción de depuración" (debug build) y cargo build --release se llama construcción para distribución (release build).
  • cargo check sirve para validar el código. Es como compilar, pero sin que genere el código ejecutable. Es una forma de validar si no se quiere ejecutar el código, ya que es más rápida que build o run.

Por cierto, la parte --release de la sentencia de cargo es un flag. Es información extra que recibe la sentencia cargo que sirve para cambiar su comportamiento.

Algunas otras cosas que son necesarias conocer sobre cargo:

  • cargo new sirve para crear un nuevo proyecto de Rust. Después de la palabra new se escribe el nombre del proyecto y cargo creará una carpeta y los ficheros mínimos necesarios para ese proyecto.
  • cargo clean sirve para eliminar las librerías que se hayan descargado por ser necesarias para el proyecto. Estas librerías se habrán ido añadido como filas en el fichero Cargo.toml y se habrán ido descargando como consecuencia de haber compilado el proyecto con cargo build o cargo run.

Una última cosa a conocer sobre el compilador: siempre tarda más la primera vez que se ejecuta cargo build o carga run. Después de esta primera vez, recordará las librerías usadas y compilará más rápido. Eso sí, después de un cargo clean, volverá a tardar mucho en compilar ya que es como si fuese la primera vez que lo hace.

Entrada de datos de usuario

Una forma fácil de permitir que el usuario "teclee" información para el programa es usar la librería std::io::stdin. Esta es la librería "estándar de entrada", que recibe las entradas del teclado. Con stdin() se puede obtener la entrada del usuario. Esto se guardará en una &mut String con .read_line(). A continuación se muestra un ejemplo simple que no funciona del todo:

use std::io;

fn main() {
    println!("Por favor, teclea algo o x para terminar:");
    let mut input_string = String::new();

    while input_string != "x" { // Esto es lo que no funciona bien
        input_string.clear(); // Primero vacía la cadena de caracteres. En otro caso, se estaría añadiendo sin parar información
        io::stdin().read_line(&mut input_string).unwrap(); // Obtiene la entrada del usuario y la pone en  read_string
        println!("Escribiste: {}", input_string);
    }
    println!("¡Adios!");
}

Esta es es la salida de este programa:

Por favor, teclea algo o x para terminar:
algo
Escribiste: algo

Algo más
Escribiste: Algo más

x
Escribiste: x

x
Escribiste: x

x
Escribiste: x

Toma el valor de entrada y lo escribe en pantalla, pero continúa a pesar de haber escrito x. La única forma de salir es cerrar la ventana o pulsar ctrl y c simultáneamente. Si se modifica el {} del println! por {:?} para obtener más información (o se usa dbg!(&input_string)) el programa imprime lo siguiente:

Por favor, teclea algo o x para terminar:
algo
Escribiste: "algo\r\n"
algo más
Escribiste: "algo más\r\n"
x
Escribiste: "x\r\n"
x
Escribiste: "x\r\n"

Esto se debe a que la entrada que se recibe del teclado no es la palabra "algo", sino que es "algo" y el resultado de la tecla Intro (Enter en inglés). La forma más fácil para resolver esto es usar el método .trim() que elimina todos los espacios en blanco que rodean al texto por ambos lados. En esta caso por espacio en blanco .trim() entiende estos caracteres:

U+0009 (tabulador horizontal, '\t')
U+000A (salto de línea, '\n')
U+000B (tabulador vertical)
U+000C (salto de página)
U+000D (retorno de carro, '\r')
U+0020 (espacio, ' ')
U+0085 (siguiente línea)
U+200E (marca de izquierda a derecha)
U+200F (marca de derecha a izquierda)
U+2028 (separador de línea)
U+2029 (separador de párrafo)

De esta forma, la cadena x\r\n se convierte en x. Con este cambio, la aplicación funciona:

use std::io;

fn main() {
    println!("Por favor, teclea algo o x para terminar:");
    let mut input_string = String::new();

    while input_string.trim() != "x" {
        input_string.clear();
        io::stdin().read_line(&mut input_string).unwrap();
        println!("Escribiste: {}", input_string);
    }
    println!("¡Adiós!");
}

Ahora imprimirá:

Por favor, teclea algo o x para terminar:
algo
Escribiste: algo

algo
Escribiste: algo

x
Escribiste: x

¡Adiós!

Hay otra clase de entrada de usuario denominada std::env::Args (env significa entorno). Args es lo que teclea el usuario cuando inicia el programa (en la misma línea de arranque). Siempre existe un Arg en todos los programas.

fn main() {
    println!("{:?}", std::env::args());
}

Si se teclea cargo run el código anterior imprime:

Args { inner: ["target\\debug\\rust_book.exe"] }

A continuación, se ejecuta el mismo programa, pero pasándole más argumentos, por ejemplo: cargo run but with some extra words. En este caso, imprime:

Args { inner: ["target\\debug\\rust_book.exe", "but", "with", "some", "extra", "words"] }

Es interesante el resultado. Cuando se observa la página de Args se ve que implementa IntoIterator. Esto significa que se puede leer con un iterador:

use std::env::args;

fn main() {
    let input = args();

    for entry in input {
        println!("You entered: {}", entry);
    }
}

Que imprime:

You entered: target\debug\rust_book.exe
You entered: but
You entered: with
You entered: some
You entered: extra
You entered: words

El primer argumento siempre es el nombre del programa, por lo que lo habitual es saltárselo:

use std::env::args;

fn main() {
    let input = args();

    input.skip(1).for_each(|item| {
        println!("You wrote {}, which in capital letters is {}", item, item.to_uppercase());
    })
}

Que imprimirá:

You wrote but, which in capital letters is BUT
You wrote with, which in capital letters is WITH
You wrote some, which in capital letters is SOME
You wrote extra, which in capital letters is EXTRA
You wrote words, which in capital letters is WORDS

Un uso habitual de Args es para las configuraciones de usuario. A continuación, se muestra un programa que pone las palabas que se teclean en mayúsculas (capital) o minúsculas (lowercase), según sea la configuración inicial:

use std::env::args;

enum Letters {
    Capitalize,
    Lowercase,
    Nothing,
}

fn main() {
    let mut changes = Letters::Nothing;
    let input = args().collect::<Vec<_>>();

    if input.len() > 2 {
        match input[1].as_str() {
            "capital" => changes = Letters::Capitalize,
            "lowercase" => changes = Letters::Lowercase,
            _ => {}
        }
    }

    for word in input.iter().skip(2) {
      match changes {
        Letters::Capitalize => println!("{}", word.to_uppercase()),
        Letters::Lowercase => println!("{}", word.to_lowercase()),
        _ => println!("{}", word)
      }
    }
    
}

Algunos ejemplos de su ejecución:

Entrada cargo run please make capitals:

make capitals

Entrada cargo run capital:

// No imprime nada...

Entrada cargo run capital I think I understand now:

I
THINK
I
UNDERSTAND
NOW

ENtrada cargo run lowercase Does this work too?:

does
this
work
too?

Además de los argumentos tecleados por el usuario al arrancar el programa, std::env tiene también Vars que son las variables del sistema. Estas son diversas configuraciones que el usuario no tiene que teclera. Para acceder a ellas se usa std::env::vars() que devuelve una lista de tuplas (String, String). Suelen existir muchas. POr ejemplo:

fn main() {
    for item in std::env::vars() {
        println!("{:?}", item);
    }
}

El programa anterior muestra la información completa de las variables de sesión del usaurio:

("CARGO", "/playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo")
("CARGO_HOME", "/playground/.cargo")
("CARGO_MANIFEST_DIR", "/playground")
("CARGO_PKG_AUTHORS", "The Rust Playground")
("CARGO_PKG_DESCRIPTION", "")
("CARGO_PKG_HOMEPAGE", "")
("CARGO_PKG_NAME", "playground")
("CARGO_PKG_REPOSITORY", "")
("CARGO_PKG_VERSION", "0.0.1")
("CARGO_PKG_VERSION_MAJOR", "0")
("CARGO_PKG_VERSION_MINOR", "0")
("CARGO_PKG_VERSION_PATCH", "1")
("CARGO_PKG_VERSION_PRE", "")
("DEBIAN_FRONTEND", "noninteractive")
("HOME", "/playground")
("HOSTNAME", "f94c15b8134b")
("LD_LIBRARY_PATH", "/playground/target/debug/build/backtrace-sys-3ec4c973f371c302/out:/playground/target/debug/build/libsqlite3-sys-fbddfbb9b241dacb/out:/playground/target/debug/build/ring-cadba5e583648abb/out:/playground/target/debug/deps:/playground/target/debug:/playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib:/playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib")
("PATH", "/playground/.cargo/bin:/playground/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
("PLAYGROUND_EDITION", "2018")
("PLAYGROUND_TIMEOUT", "10")
("PWD", "/playground")
("RUSTUP_HOME", "/playground/.rustup")
("RUSTUP_TOOLCHAIN", "stable-x86_64-unknown-linux-gnu")
("RUST_RECURSION_COUNT", "1")
("SHLVL", "1")
("SSL_CERT_DIR", "/usr/lib/ssl/certs")
("SSL_CERT_FILE", "/usr/lib/ssl/certs/ca-certificates.crt")
("USER", "playground")
("_", "/usr/bin/timeout")

La forma más sencilla de acceder a una de ellas de forma independiente es usar la macro env!. Se le da el nombre de una variable y devolverá un &str con el valor. No funcionará si la variable no existe, por lo que es mejor usar option_env!. Si se escribe el siguiente código en Playground:

fn main() {
    println!("{}", env!("USER"));
    println!("{}", option_env!("ROOT").unwrap_or("Can't find ROOT"));
    println!("{}", option_env!("CARGO").unwrap_or("Can't find CARGO"));
}

Se obtiene:

playground
Can't find ROOT
/playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo

Se ve que es mejor usar option_env!, salvo que realmente se quiera que el programa falle si no existe determinada variable de entorno de sistema/usuario, entonces sería mejor env!.

Utilizando ficheros

Ahora que se está usando Rust en el propio ordenador, se puede comenzar a trabajar con ficheros. Ahora se van a ver muchos Result en el código. Esto es debido a que muchas cosas pueden fallar. El fichero puede no existir, o el ordenador no lo puede leer.

Se puede usar el operador ? que espera recibir un Result de la función a la que se aplique. Si no se recuerda el error que se devuelve, se dejar vacío y dejar que el compilador lo indique. El programa siguiente contiene una función que intenta obtener un número con .parse().

// ⚠️
fn give_number(input: &str) -> Result<i32, ()> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88"));
    println!("{:?}", give_number("5"));
}

El compilador dice lo que hay que hacer:

error[E0308]: mismatched types
 --> src\main.rs:4:5
  |
3 | fn give_number(input: &str) -> Result<i32, ()> {
  |                                --------------- expected `std::result::Result<i32, ()>` because of return type
4 |     input.parse::<i32>()
  |     ^^^^^^^^^^^^^^^^^^^^ expected `()`, found struct `std::num::ParseIntError`
  |
  = note: expected enum `std::result::Result<_, ()>`
             found enum `std::result::Result<_, std::num::ParseIntError>`

Así, el compilador indica cuál es el tipo de retorno esperado y se puede corregir:

use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88"));
    println!("{:?}", give_number("5"));
}

Ahora el programa funciona:

Ok(88)
Ok(5)

Ahora se quiere utilizar el operador ? para que se devuelva el valor recuperado o el error en caso de que lo haya habido, pero este código no funciona:

// ⚠️
use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88")?);
    println!("{:?}", give_number("5")?);
}

El compilador dice:

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
  --> src\main.rs:8:22
   |
7  | / fn main() {
8  | |     println!("{:?}", give_number("88")?);
   | |                      ^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
9  | |     println!("{:?}", give_number("5")?);
10 | | }
   | |_- this function should return `Result` or `Option` to accept `?`

Pero main() si puede devolver un Result, como cualquier otra función. Si la función va bien, no se debe devolver nada en main, pero si falla, debe dar el mismo error. Por lo tanto, el código queda así:

use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() -> Result<(), ParseIntError> {
    println!("{:?}", give_number("88")?);
    println!("{:?}", give_number("5")?);
    Ok(())
}

Es importante ahora añadir el Ok(()) como valor de retorno final de main. Esto es muy común en Rust. Significa Ok, dentro del cual está () que es el valor de retorno. Ahora imprime:

88
5

Esto no es muy útil cuando solo se usa .parse(), pero lo será con ficheros. Esto de debe a que ? también modifica los tipos de error. Según indica la página del operador ?:

If you get an `Err`, it will get the inner error. Then `?` does a conversion using `From`. With that it can change specialized errors to more general ones. The error it gets is then returned.

Es decir, si el operador obtiene un Err, se recuperará el error interno. Así con From, ? realiza la conversión de errores especializados a más general. El error que obtiene así, es el que se devuelve.

Además, Rust tiene un tipo Result especializado para los Files (ficheros) o similar. Es el tipo std::io::Result y es lo típico que se observará como retorno de una función main() cuando se están manipulando ficheros y usando ?. Este tipo Result es un alias de:

#![allow(unused)]
fn main() {
type Result<T> = Result<T, Error>;
}

Es un Result<T, Error>, pero no es necesario escribir la parte del errro, solo Result<T>.

A continuación, se presenta el primer ejemplo que trabaja con ficheros. std::fs es el módulo que contiene los métodos para trabajar con ficheros. std::io::Write permite escribir contenido a ficheros. Se puede usar el método .write_all() para escribir valores a un ficehro.

use std::fs;
use std::io::Write;

fn main() -> std::io::Result<()> {
    let mut file = fs::File::create("myfilename.txt")?; // Crea un fichero con este nombre.
                                                        // CUIDADO: si existe un fichero previamente con ese nombre,
                                                        // se eliminará todo su contenido previo.
    file.write_all(b"Let's put this in the file")?;     // No se debe olvidar la b delante de ". Esto se debe a que un fichero recibe bytes.
    Ok(())
}

Este código consiste en: intentar crear un fichero y comprobar si ha funcionado. Si funciona, utiliza .write_all() y comprueba que haya funcionado bien.

De hecho, existe una función que hace las dos cosas juntas. Crea un fichero y escribe el contenido que se indique. Se denomina std::io::write. Se le pasa como parámetros el nombre del fichero deseado y el contenido que se quiere incorporar. Como antes, es importante tener cuidado ya que si el fichero existiera previamente, se borraría todo su contenido anterior. Además, permite escribir un &str sin necesidad de utilizar b delante. Esto se debe a que la función se define así:

#![allow(unused)]
fn main() {
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()>
}

El segundo parámetro, el contenido, debe implementar un array de binarios AsRef<u8>. Por eso se puede pasar un &str sin hacer conversión explícita a binario.

Se puede ver en el siguiente ejemplo:

use std::fs;
fn main() -> std::io::Result<()> {
    fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    Ok(())
}

En este programa se crea un fichero que será el que se usará en los siguientes ejemplos, es una conversión de un personaje de un libro de comic llamado Calvin que habla con su padre, que no se toma la pregunta en serio. Con este código, se puede crera un fichero que se usará en varios ejemplos.

Abrir un fichero, leer de él, es tan sencillo como crearlo. Solo se necesita usar open() en lugar de create(). Después de esto, si el fichero existía, se pueden usar funciones como read_to_string(). Para esto es necesario disponer de una mut String y leer del fichero a ella. Por ejemplo:

use std::fs;
use std::fs::File;
use std::io::Read; // Este módulo es necesario para disponer de la función .read_to_string()

fn main() -> std::io::Result<()> {
     fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;


    let mut calvin_file = File::open("calvin_with_dad.txt")?; // Abre el fichero recién creado
    let mut calvin_string = String::new(); // Esta String guardará el contenido
    calvin_file.read_to_string(&mut calvin_string)?; // Lee el fichero a la String

    calvin_string.split_whitespace().for_each(|word| print!("{} ", word.to_uppercase())); // Pasa a mayúsculas e imprime cada palabra de la cadena de caracteres

    Ok(())
}

El resultado del programa es:

CALVIN: DAD, HOW COME OLD PHOTOGRAPHS ARE ALWAYS BLACK AND WHITE? DIDN'T THEY HAVE COLOR FILM BACK THEN? DAD: SURE THEY DID. IN 
FACT, THOSE PHOTOGRAPHS *ARE* IN COLOR. IT'S JUST THE *WORLD* WAS BLACK AND WHITE THEN. CALVIN: REALLY? DAD: YEP. THE WORLD DIDN'T TURN COLOR UNTIL SOMETIMES IN THE 1930S...

Si se quiere crear un fichero, pero solo si no existe otro con el mismo nombre, existe una struct denominada OpenOptions. En realidad, ya se ha estado usando este struct. Por ejemplo, la definición de File::open dice:

#![allow(unused)]
fn main() {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().read(true).open(path.as_ref())
    }
}

Esta definición parece seguir el patrón del constructor que se aprendió antes. Lo mismo sucede para File::create:

#![allow(unused)]
fn main() {
pub fn create<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().write(true).create(true).truncate(true).open(path.as_ref())
    }
}

Si se consulta página de OpenOptions, se pueden ver todos los métodos que existen. La mayoría necesitan un valor bool:

  • append(): este método indica que se añada al contenido anterior, en lugar de borrarlo.
  • create(): crea un fichero.
  • create_new(): solo crea el fichero si no existe previamente.
  • read(): para poder leer del fichero, se necesario ponerlo a true.
  • truncate(): si se pone a true se elimina todo el contenido del fichero cuando se abre.
  • write(): permite escribir en un fichero.

Como último método se usa .open() con el nombre del fichero que devuelve un Result. Por ejemplo:

// ⚠️
use std::fs;
use std::fs::OpenOptions;

fn main() -> std::io::Result<()> {
     fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    let calvin_file = OpenOptions::new().write(true).create_new(true).open("calvin_with_dad.txt")?;

    Ok(())
}

En primer lugar se crea un OpenOptions con new. Después se le indica que se puede escribir con write. Después de eso create_new() a true e intenta abrir el fichero. En este caso no funciona ya que ya existe el fichero por lo que lanza el siguiente error:

#![allow(unused)]
fn main() {
Error: Os { code: 80, kind: AlreadyExists, message: "The file exists." }
}

Para que sí funcione, se debe usar .append() para este caso. Para escribir al fichero, se puede usar .write_all(), que intenta escribir todo lo que se le pase.

También se va a usar la macro write! para hacer lo mismo. Esta macro ya se ha usado cuando se usó impl Display para construir struct. Ahora se usará en un fichero en lugar de un buffer.

use std::fs;
use std::fs::OpenOptions;
use std::io::Write;

fn main() -> std::io::Result<()> {
    fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    let mut calvin_file = OpenOptions::new()
        .append(true) // Ahora se puede escribir sin borrar el contenido anterior
        .read(true)
        .open("calvin_with_dad.txt")?;
    calvin_file.write_all(b"And it was a pretty grainy color for a while too.\n")?;
    write!(&mut calvin_file, "That's really weird.\n")?;
    write!(&mut calvin_file, "Well, truth is stranger than fiction.")?;

    println!("{}", fs::read_to_string("calvin_with_dad.txt")?);

    Ok(())
}

Que imprime:

Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...And it was a pretty grainy color for a while too.
That's really weird.
Well, truth is stranger than fiction.

cargo doc

Se puede haber observado que la documentación de Rust siempre es parecida. En el lado izquierdo aparecen los struct y trait y los ejemplos de código aparecen a la derecha. Esto se debe a que se puede generar de forma automática la documentación mediante el uso de cargo doc.

Incluso con un programa que no hace nada, se puede aprender sobre los rasgos en Rust. Por ejemplo, a continuación hay dos struct que no hacen casi nada y una función fn main() que casi tampoco hace nada:

struct DoesNothing {}
struct PrintThing {}

impl PrintThing {
    fn prints_something() {
        println!("I am printing something");
    }
}

fn main() {}

Pero si se teclea cargo doc --open, se puede observa que existe mucha información, más de la que puede que se esperara. Lo primero que muestra es:

Crate rust_book

Structs
DoesNothing
PrintThing

Functions
main

Pero si se teclea en una de las struct mostrará muchos rasgos que implementa sin haberse hecho por parte del desarrollador:

Struct rust_book::DoesNothing
[+] Show declaration
Auto Trait Implementations
impl RefUnwindSafe for DoesNothing
impl Send for DoesNothing
impl Sync for DoesNothing
impl Unpin for DoesNothing
impl UnwindSafe for DoesNothing
Blanket Implementations
impl<T> Any for T
where
    T: 'static + ?Sized,
[src]
[+]
impl<T> Borrow<T> for T
where
    T: ?Sized,
[src]
[+]
impl<T> BorrowMut<T> for T
where
    T: ?Sized,
[src]
[+]
impl<T> From<T> for T
[src]
[+]
impl<T, U> Into<U> for T
where
    U: From<T>,
[src]
[+]
impl<T, U> TryFrom<U> for T
where
    U: Into<T>,
[src]
[+]
impl<T, U> TryInto<U> for T
where
    U: TryFrom<T>,

Esto se debe a que Rust los implementa de forma automática para todos los tipos que se crean.

Si, además, se añade algo de comentarios de documentación \\\, aparecerán en cargo doc.

/// This is a struct that does nothing
struct DoesNothing {}
/// This struct only has one method.
struct PrintThing {}
/// It just prints the same message.
impl PrintThing {
    fn prints_something() {
        println!("I am printing something");
    }
}

fn main() {}

Que mostrará ahora:

Crate rust_book
Structs
DoesNothing This is a struct that does nothing
PrintThing  This struct only has one method.
Functions
main

cargo doc es muy útil cuando se usan librerías de otras personas. Porque normalmente estas librerías están en múltiples sitios web. Puede necesitarse tiempo para buscarlas todas. Con cargo doc se mostrará la documentación de todas ellas desde el propio ordenador personal.

¿El final?

Este es el final de Rust en español fácil. Pero yo estoy aún aquí, y puedes hacerme llegar cualquier pregunta. Siéntete libre de contactarme en Twitter1 o añadir una "pull request", incidencia, etc. También puedes hacerme llegar qué partes son las más difíciles de comprender. Rust en español fácil aspira a ser muy comprensible, por lo que necesito conocer en qué partes el texto resulta difícil. Por supuesto, Rust en sí mismo puede ser difícil de comprender, pero al menos que esté explicado de forma fácil.

1

N.T.: para la traducción al español puedes contactarme en jmgaguilera.com.