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 es0.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 vistoUnpin
antes. Se usa en casi cualquier tipo (comoSized
, 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 implementanUnpin
, por lo que es posible moverlos. Por ello funcionan métodos comostd::mem::replace
.std::ops::{Drop, Fn, FnMut, FnOnce}
.std::mem::drop
std::boxed::Box
.std::borrow::ToOwned
. Se ha visto antes conCow
, 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 enString
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 unDoubleEndedIterator
. UnExactSizeIterator
es algo como0..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