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