Capítulo 1. Tipos

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

El primer capítulo de este libro abarca consejos que giran en torno al sistema de tipos de Rust. Este sistema de tipos es más expresivo que el de otros lenguajes corrientes; tiene más en común con lenguajes "académicos" como OCaml o Haskell.

Una parte fundamental de es el tipo enum de Rust, que es considerablemente más expresivo que los tipos de enumeración de otros lenguajes y que permite tipos de datos algebraicos.

Los artículos de este capítulo cubren los tipos fundamentales que proporciona el lenguaje y cómo combinarlos en estructuras de datos que expresen con precisión la semántica de tu programa. Este concepto de codificar el comportamiento en el sistema de tipos ayuda a reducir la cantidad de código de comprobación y de ruta de errores que se necesita, porque los estados no válidos son rechazados por la cadena de herramientas en tiempo de compilación y no por el programa en tiempo de ejecución.

En este capítulo también se describen algunas de las estructuras de datos omnipresentes que proporciona la biblioteca estándar de Rust:Options, Results, Errors y Iterators. Familiarizarte con estas herramientas estándar te ayudará a escribir Rust idiomático que sea eficiente y compacto; en particular, permiten utilizar el operador de signo de interrogación de Rust, que admite un manejo de errores discreto pero seguro.

Ten en cuenta que los elementos que implican rasgos de Rust se tratan en el capítulo siguiente, pero necesariamente hay cierto grado de solapamiento con los elementos de este capítulo, porque los rasgos describen el comportamiento de los tipos.

Tema 1: Utiliza el sistema de tipos para expresartus estructuras de datos

que los llamaban programadores y no tipógrafos

@thingskatedid

Este artículo proporciona un rápido recorrido por el sistema de tipos de Rust, empezando por los tipos fundamentales que el compilador pone a tu disposición, para pasar después a las diversas formas en que los valores pueden combinarse en estructuras de datos.

El tipo enum de Rust adquiere entonces un papel protagonista. Aunque la versión básica es equivalente a la que proporcionan otros lenguajes, la posibilidad de combinar variantes de enum con campos de datos permite una mayor flexibilidad y expresividad.

Tipos fundamentales

Los fundamentos del sistema de tipos de Rust son bastante familiares para cualquiera que venga de otro lenguaje de programación de tipado estático (como C++, Go o Java). Hay una colección de tipos enteros con tamaños específicos, tanto con signo (i8, i16, i32, i64, i128) y sin signo (u8, u16, u32, u64, u128).

También hay con signo (isize) y sin signo (usize) cuyos tamaños coinciden con el tamaño del puntero en el sistema de destino. Sin embargo, con Rust no harás muchas conversiones entre punteros y enteros, por lo que esa equivalencia de tamaño no es realmente relevante. Sin embargo, las colecciones estándar devuelven su tamaño como usize (de .len()), por lo que la indexación de colecciones significa que los valores de usize son bastante comunes, lo que obviamente está bien desde el punto de vista de la capacidad, ya que no puede haber más elementos en una colección en memoria que direcciones de memoria en el sistema.

Los tipos integrales nos dan la primera pista de que Rust es un mundo más estricto que C++. En Rust, intentar meter un tipo entero mayor (i32) en un tipo entero menor (i16) genera un error de compilación:

error[E0308]: mismatched types
  --> src/main.rs:18:18
   |
18 |     let y: i16 = x;
   |            ---   ^ expected `i16`, found `i32`
   |            |
   |            expected due to this
   |
help: you can convert an `i32` to an `i16` and panic if the converted value
      doesn't fit
   |
18 |     let y: i16 = x.try_into().unwrap();
   |                   ++++++++++++++++++++

Esto es tranquilizador: Rust no se va a quedar quieto mientras el programador hace cosas arriesgadas. Aunque podemos ver que los valores implicados en esta conversión concreta estarían bien, el compilador tiene que permitir la posibilidad de que haya valores en los que la conversión no esté bien:

La salida de error también da una primera indicación de que, aunque Rust tiene reglas más estrictas, también tiene mensajes útiles del compilador que indican cómo cumplir las reglas. La solución sugerida plantea la cuestión de cómo tratar las situaciones en las que la conversión tendría que alterar el valor para ajustarlo, y tendremos más que decir tanto sobre el tratamiento de errores(Tema 4) como sobre el uso de panic! (Tema 18) más adelante.

Rust tampoco permite algunas cosas que podrían parecer "seguras", como poner un valor de un tipo entero menor en un tipo entero mayor:

error[E0308]: mismatched types
  --> src/main.rs:36:18
   |
36 |     let y: i64 = x;
   |            ---   ^ expected `i64`, found `i32`
   |            |
   |            expected due to this
   |
help: you can convert an `i32` to an `i64`
   |
36 |     let y: i64 = x.into();
   |                   +++++++

En este caso, la solución propuesta no plantea el espectro de la gestión de errores, pero la conversión debe ser explícita. Hablaremos de las conversiones de tipos con más detalle más adelante(Tema 5).

Siguiendo con los tipos primitivos no sorprendentes, Rust tiene un tipo bool tipos de coma flotante (f32, f64), y un tipo unitario () (como el de C void).

Más interesante es el tipo char que contiene unvalor Unicode (similar al tiporune de Go). Aunque internamente se almacena como cuatro bytes, tampoco hay conversiones silenciosas a o desde un entero de 32 bits.

Esta precisión en el sistema de tipos te obliga a ser explícito sobre lo que intentas expresar: un valor u32 es diferente de un char, que a su vez es diferente de una secuencia de bytes UTF-8, que a su vez es diferentede una secuencia de bytes arbitrarios, y depende de ti especificar exactamente a qué te refieres.1 La famosa entrada del blog de Joel Spolsky puede ayudarte a entender cuál necesitas.

Por supuesto, existen métodos de ayuda que te permiten convertir entre estos distintos tipos, pero sus firmas te obligan a manejar (o ignorar explícitamente) la posibilidad de fallo. Por ejemplo, un punto de código Unicode siempre puede representarse en 32 bits,2 por lo que 'a' as u32 está permitido, pero la otra dirección es más complicada (ya que hay algunos valores u32 que no son puntos de código Unicode válidos):

char::from_u32

Devuelve un Option<char>, obligando a la persona que llama a manejar el caso de fallo.

char::from_u32_unchecked

Hace la suposición de validez, pero tiene el potencial de provocar un comportamiento indefinido si esa suposición resulta no ser cierta. Como resultado, la función se marca como unsafe, obligando a quien la llama a utilizar también unsafe (Tema 16).

Tipos de áridos

Pasando a los tipos agregados, Rust dispone de diversas formas de combinar valores relacionados. La mayoría de ellas son equivalentes familiares a los mecanismos de agregación disponibles en otros lenguajes:

Matrices

Contiene varias instancias de un mismo tipo, cuyo número se conoce en tiempo de compilación. Por ejemplo, [u32; 4] son cuatro enteros de 4 bytes seguidos.

Tuplas

Contener instancias de múltiples tipos heterogéneos, donde el número de elementos y sus tipos se conocen en tiempo de compilación, por ejemplo,(WidgetOffset, WidgetSize, WidgetColor). Si los tipos de la tupla no son distintivos -por ejemplo, (i32, i32, &'static str, bool)-, es mejor dar un nombre a cada elemento y utilizar una estructura.

Estructuras

También mantienen instancias de tipos heterogéneos conocidos en tiempo de compilación, pero permiten referirse por su nombre tanto al tipo global como a los campos individuales.

Rust también incluye la estructura tupla, que es un cruce de struct y tupla: hay un nombre para el tipo general, pero no para los campos individuales, a los que se hace referencia por números: s.0 s.1 , etc:

/// Struct with two unnamed fields.
struct TextMatch(usize, String);

// Construct by providing the contents in order.
let m = TextMatch(12, "needle".to_owned());

// Access by field number.
assert_eq!(m.0, 12);

enums

Esto nos lleva a la joya de la corona del sistema de tipos de Rust, el enum. Con la forma básica de un enum, es difícil ver por qué hay que entusiasmarse. Como en otros lenguajes, el enum te permite especificar un conjunto de valores mutuamente excluyentes, posiblemente con un valor numérico adjunto:

enum HttpResultCode {
    Ok = 200,
    NotFound = 404,
    Teapot = 418,
}

let code = HttpResultCode::NotFound;
assert_eq!(code as i32, 404);

Dado que cada definición de enum crea un tipo distinto, esto puede utilizarse para mejorar la legibilidad y mantenibilidad de las funciones que toman argumentos de bool. En lugar de

print_page(/* both_sides= */ true, /* color= */ false);

una versión que utiliza un par de enums:

pub enum Sides {
    Both,
    Single,
}

pub enum Output {
    BlackAndWhite,
    Color,
}

pub fn print_page(sides: Sides, color: Output) {
    // ...
}

es más seguro para los tipos y más fácil de leer en el punto de invocación:

print_page(Sides::Both, Output::BlackAndWhite);

A diferencia de la versión bool, si un usuario de la biblioteca invirtiera accidentalmente el orden de los argumentos, el compilador se quejaría inmediatamente:

error[E0308]: arguments to this function are incorrect
   --> src/main.rs:104:9
    |
104 | print_page(Output::BlackAndWhite, Sides::Single);
    | ^^^^^^^^^^ ---------------------  ------------- expected `enums::Output`,
    |            |                                    found `enums::Sides`
    |            |
    |            expected `enums::Sides`, found `enums::Output`
    |
note: function defined here
   --> src/main.rs:145:12
    |
145 |     pub fn print_page(sides: Sides, color: Output) {
    |            ^^^^^^^^^^ ------------  -------------
help: swap these arguments
    |
104 | print_page(Sides::Single, Output::BlackAndWhite);
    |             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Utilizar el patrón newtype -ver punto 6- paraenvolver un bool también consigue seguridad de tipos y facilidad de mantenimiento; por lo general, es mejor utilizar el patrón newtype si la semántica siempre será booleana, y utilizar un enum si existe la posibilidad de que surja una nueva alternativa -por ejemplo, Sides::BothAlternateOrientation- en el futuro.

La seguridad de tipos de enums de Rust continúa con la expresión match:

error[E0004]: non-exhaustive patterns: `HttpResultCode::Teapot` not covered
  --> src/main.rs:44:21
   |
44 |     let msg = match code {
   |                     ^^^^ pattern `HttpResultCode::Teapot` not covered
   |
note: `HttpResultCode` defined here
  --> src/main.rs:10:5
   |
7  | enum HttpResultCode {
   |      --------------
...
10 |     Teapot = 418,
   |     ^^^^^^ not covered
   = note: the matched value is of type `HttpResultCode`
help: ensure that all possible cases are being handled by adding a match arm
      with a wildcard pattern or an explicit pattern as shown
   |
46 ~         HttpResultCode::NotFound => "Not found",
47 ~         HttpResultCode::Teapot => todo!(),
   |

El compilador obliga al programador a considerar todas las posibilidades que representa el enum,3 aunque el resultado sea sólo añadir un brazo por defecto _ => {}. (Ten en cuenta que los compiladores modernos de C++ también pueden advertir, y de hecho lo hacen, sobre la falta de brazos switch para enums.)

enums con Campos

La verdadera potencia de la función enum de Rust proviene del hecho de que cada variante puede tener datos que la acompañan, lo que la convierte en un tipo agregado que actúa como un tipo de datos algebraico (ADT). Esto resulta menos familiar a los programadores de lenguajes convencionales; en términos de C/C++, es como una combinación de un enum con un union-sólo de tipo seguro.

Esto significa que las invariantes de las estructuras de datos del programa pueden codificarse en el sistema de tipos de Rust; los estados que no cumplan esas invariantes ni siquiera compilarán. Un enum bien diseñado deja clara la intención del creador tanto para los humanos como para el compilador:

use std::collections::{HashMap, HashSet};

pub enum SchedulerState {
    Inert,
    Pending(HashSet<Job>),
    Running(HashMap<CpuId, Vec<Job>>),
}

Sólo por la definición del tipo, es razonable suponer que los Jobse ponen en cola en el estado Pending hasta que el programador está totalmente activo, momento en el que se asignan a algún pool por CPU.

Esto pone de relieve el tema central de este Tema, que es utilizar el sistema de tipos de Rust para expresar los conceptos asociados al diseño de tu software.

Una señal inequívoca de que esto no ocurre es un comentario que explique cuándo es válido algún campo o parámetro:

Es un candidato ideal para sustituirlo por un enum que contenga datos:

pub enum Color {
    Monochrome,
    Foreground(RgbColor),
}

pub struct DisplayProps {
    pub x: u32,
    pub y: u32,
    pub color: Color,
}

Este pequeño ejemplo ilustra un consejo clave: haz que los estados no válidos sean inexpresables en tus tipos. Los tipos que sólo admiten combinaciones válidas de valores significan que clases enteras de errores son rechazadas por el compilador, lo que conduce a un código más pequeño y seguro.

Ubicua enum Tipos

Volviendo al poder del enum, hay dos conceptos que son tan comunes que la biblioteca estándar de Rust incluye tipos enum incorporados para expresarlos; estos tipos son omnipresentes en el código Rust.

Option<T>

El primer concepto es el de Option: o hay un valor de un tipo determinado (Some(T)) o no lo hay (None). Utiliza siempreOption para los valores que pueden estar ausentes; no recurras nunca al uso de valores centinela (-1, nullptr, ...) para intentar expresar el mismo concepto en banda.

Sin embargo, hay un punto sutil a tener en cuenta. Si estás tratando con una colección de cosas, tienes que decidir si tener cero cosas en la colección es lo mismo que no tener una colección. En la mayoría de las situaciones, la distinción no se plantea y puedes seguir adelante y utilizar (digamos) Vec<Thing>: un recuento de cero cosas implica una ausencia de cosas.

Sin embargo, existen sin duda otros escenarios poco frecuentes en los que es necesario distinguir ambos casos conOption<Vec<Thing>>-por ejemplo, un sistema criptográfico podría necesitar distinguir entre "carga útil transportada por separado" y "carga útil vacía proporcionada". (Esto está relacionado con los debates en torno al marcadorNULL para las columnas en SQL).

Del mismo modo, ¿cuál es la mejor opción para un String que podría estar ausente? ¿Tiene más sentido "" o None para indicar la ausencia de un valor? Cualquiera de las dos formas funciona, pero Option<String> comunica claramente la posibilidad de que este valor pueda estar ausente.

Result<T, E>

El segundo concepto común surge del procesamiento de errores: si una función falla, ¿cómo debe informarse de ese fallo? Históricamente, se utilizaban valores centinela especiales (por ejemplo, -errno valores de retorno de llamadas al sistema Linux) o variables globales (errno para sistemas POSIX). Más recientemente, los lenguajes que admiten valores de retorno múltiples o tuplas (como Go) de funciones pueden tener la convención de devolver un par (result, error), asumiendo la existencia de algún valor "cero" adecuado para el result cuando el error no es "cero".

En Rust, existe un tipo enum precisamente para esto: codifica siempre el resultado de una operación que pueda fallar como Result<T, E>. El tipo T contiene el resultado satisfactorio (en la variante Ok ), y el tipo E contiene los detalles del error (en la variante Err ) en caso de fallo.

Utilizar el tipo estándar deja clara la intención del diseño. También permite el uso de transformaciones estándar(punto 3) y el tratamiento de errores(punto 4), lo que a su vez permite agilizar también el tratamiento de errores con el operador ?.

Tema 2: Utilizar el sistema de tipos para expresar comportamientos comunes

En el punto1 se trató cómo expresar estructuras de datos en el sistema de tipos; en este punto se pasa a tratar la codificación delcomportamiento en el sistema de tipos de Rust.

En general, los mecanismos descritos en este Tema te resultarán familiares, ya que todos tienen análogos directos en otros idiomas:

Funciones

El mecanismo universal para asociar un trozo de código con un nombre y una lista de parámetros.

Métodos

Funciones que se asocian a una instancia de una determinada estructura de datos. Los métodos son habituales en los lenguajes de programación creados después de que surgiera la orientación a objetos como paradigma de programación.

Punteros de función

Soportado por la mayoría de los lenguajes de la familia C, incluidos C++ y Go, como mecanismo que permite aun nivel extra de indirección al invocar otro código.

Cierres

Originalmente eran más comunes en la familia de lenguajes Lisp, pero se han adaptado a muchos lenguajes de programación populares de, incluidos C++ (desde C++11) y Java (desde Java 8).

Rasgos

Describen colecciones de funcionalidades relacionadas que se aplican todas al mismo elemento subyacente. Los rasgos tienen equivalentes aproximados en muchos otros lenguajes, como las clases abstractas en C++ y las interfaces en Go y Java.

Por supuesto, todos estos mecanismos tienen detalles específicos de Rust que se tratarán en este artículo.

De la lista anterior, los rasgos son los más importantes para este libro, ya que describen gran parte del comportamiento proporcionado por el compilador y la biblioteca estándar de Rust. El capítulo 2 se centra en artículos que dan consejos sobre el diseño y la implementación de rasgos, pero su omnipresencia significa que también aparecen con frecuencia en los demás artículos de este capítulo.

Funciones y métodos

Como cualquier otro lenguaje de programación, Rust utiliza funciones para organizar el código en trozos con nombre para su reutilización, con entradas al código expresadas como parámetros. Como en cualquier otro lenguaje de tipado estático, los tipos de los parámetros y del valor de retorno se especifican explícitamente:

/// Return `x` divided by `y`.
fn div(x: f64, y: f64) -> f64 {
    if y == 0.0 {
        // Terminate the function and return a value.
        return f64::NAN;
    }
    // The last expression in the function body is implicitly returned.
    x / y
}

/// Function called just for its side effects, with no return value.
/// Can also write the return value as `-> ()`.
fn show(x: f64) {
    println!("x = {x}");
}

Si una función está íntimamente relacionada con una estructura de datos concreta, se expresa como método. Un método actúa sobre un elemento de ese tipo, identificado por self, y se incluye dentro de un bloque impl DataStructure. Esto encapsula juntos datos y código relacionados de una forma orientada a objetos que es similar a la de otros lenguajes; sin embargo, en Rust, los métodos pueden añadirse a los tipos enum así como a los tipos struct, en consonancia con la naturaleza omnipresente de enum de Rust(Tema 1):

enum Shape {
    Rectangle { width: f64, height: f64 },
    Circle { radius: f64 },
}

impl Shape {
    pub fn area(&self) -> f64 {
        match self {
            Shape::Rectangle { width, height } => width * height,
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
        }
    }
}

El nombre de un método crea una etiqueta para el comportamiento que codifica, y la firma del método da información de tipo para sus entradas y salidas. La primera entrada de un método será alguna variante de self, indicando lo que el método puede hacer a la estructura de datos:

  • Un parámetro &self indica que se puede leer el contenido de la estructura de datos, pero no se modificará.

  • Un parámetro &mut self indica que el método podría modificar el contenido de la estructura de datos.

  • Un parámetro self indica que el método consume la estructura de datos.

Punteros de función

En el apartado anterior se ha descrito cómo asociar un nombre (y una lista de parámetros) a un código. Sin embargo, al invocar una función siempre se ejecuta el mismo código; lo único que cambia de una invocación a otra son los datos sobre los que opera la función. Eso cubre muchos escenarios posibles, pero ¿qué ocurre si el código debe variar en tiempo de ejecución?

La abstracción de comportamiento más sencilla que permite esto es el puntero a función: un puntero a (sólo) código, con un tipo que refleja la firma de la función:

fn sum(x: i32, y: i32) -> i32 {
    x + y
}
// Explicit coercion to `fn` type is required...
let op: fn(i32, i32) -> i32 = sum;

El tipo se comprueba en tiempo de compilación, de modo que cuando se ejecuta el programa, el valor es sólo el tamaño de un puntero. Los punteros de función no tienen ningún otro dato asociado, por lo que pueden tratarse como valores de varias formas:

// `fn` types implement `Copy`
let op1 = op;
let op2 = op;
// `fn` types implement `Eq`
assert!(op1 == op2);
// `fn` implements `std::fmt::Pointer`, used by the {:p} format specifier.
println!("op = {:p}", op);
// Example output: "op = 0x101e9aeb0"

Un detalle técnico a tener en cuenta en: es necesaria la coerción explícita a un tipo fn, porque el mero uso del nombre de una función no te da algo del tipo fn:

error[E0369]: binary operation `==` cannot be applied to type
              `fn(i32, i32) -> i32 {main::sum}`
   --> src/main.rs:102:17
    |
102 |     assert!(op1 == op2);
    |             --- ^^ --- fn(i32, i32) -> i32 {main::sum}
    |             |
    |             fn(i32, i32) -> i32 {main::sum}
    |
help: use parentheses to call these
    |
102 |     assert!(op1(/* i32 */, /* i32 */) == op2(/* i32 */, /* i32 */));
    |                ++++++++++++++++++++++       ++++++++++++++++++++++

En su lugar, el error del compilador indica que el tipo es algo como fn(i32, i32) -> i32 {main::sum}, un tipo que es totalmente interno al compilador (es decir, que no podría escribirse en código de usuario) y que identifica la función específica, así como su firma. Dicho de otro modo, el tipo de sum codifica tanto la firma de la función como su ubicación por razones de optimización; este tipo puede coaccionarse automáticamente(punto 5) a un tipo fn.

Cierres

Los punteros de función desnudos son limitantes, porque las únicas entradas disponibles para la función invocada son las que se pasan explícitamente como valores de los parámetros. Por ejemplo, considera un código que modifica cada elemento de una rebanada utilizando un puntero a función:

// In real code, an `Iterator` method would be more appropriate.
pub fn modify_all(data: &mut [u32], mutator: fn(u32) -> u32) {
    for value in data {
        *value = mutator(*value);
    }
}

Esto funciona para una simple mutación de la rebanada:

fn add2(v: u32) -> u32 {
    v + 2
}
let mut data = vec![1, 2, 3];
modify_all(&mut data, add2);
assert_eq!(data, vec![3, 4, 5]);

Sin embargo, si la modificación depende de algún estado adicional, no es posible pasarlo implícitamente al puntero de la función:

error[E0434]: can't capture dynamic environment in a fn item
   --> src/main.rs:125:13
    |
125 |         v + amount_to_add
    |             ^^^^^^^^^^^^^
    |
    = help: use the `|| { ... }` closure form instead

El mensaje de error apunta a la herramienta adecuada para el trabajo: un cierre. Un cierre es un trozo de código que se parece al cuerpo de una definición de función (una expresión lambda), excepto en lo siguiente:

  • Puede construirse como parte de una expresión, por lo que no necesita tener un nombre asociado.

  • Los parámetros de entrada se indican en barras verticales |param1, param2| (normalmente, el compilador puede deducir automáticamente sus tipos asociados ).

  • Puede captar partes del entorno que le rodea:

    let amount_to_add = 3;
    let add_n = |y| {
        // a closure capturing `amount_to_add`
        y + amount_to_add
    };
    let z = add_n(5);
    assert_eq!(z, 8);

Para entender (a grandes rasgos) cómo funciona la captura, imagina que el compilador crea un tipo efímero interno que contiene todas las partes del entorno que se mencionan en la expresión lambda. Cuando se crea el cierre, se crea una instancia de este tipo efímero para contener los valores relevantes, y cuando se invoca el cierre, esa instancia se utiliza como contexto adicional:

let amount_to_add = 3;
// *Rough* equivalent to a capturing closure.
struct InternalContext<'a> {
    // references to captured variables
    amount_to_add: &'a u32,
}
impl<'a> InternalContext<'a> {
    fn internal_op(&self, y: u32) -> u32 {
        // body of the lambda expression
        y + *self.amount_to_add
    }
}
let add_n = InternalContext {
    amount_to_add: &amount_to_add,
};
let z = add_n.internal_op(5);
assert_eq!(z, 8);

Los valores que se mantienen en este contexto nocional suelen ser referencias(Elemento 8), como aquí, pero también pueden ser referencias mutables a cosas del entorno, o valores que se mueven fuera del entorno por completo (utilizando la palabra clave moveantes de los parámetros de entrada).

Volviendo al ejemplo de modify_all, no se puede utilizar un cierre donde se espera un puntero a función:

error[E0308]: mismatched types
   --> src/main.rs:199:31
    |
199 |         modify_all(&mut data, |y| y + amount_to_add);
    |         ----------            ^^^^^^^^^^^^^^^^^^^^^ expected fn pointer,
    |         |                                           found closure
    |         |
    |         arguments to this function are incorrect
    |
    = note: expected fn pointer `fn(u32) -> u32`
                  found closure `[closure@src/main.rs:199:31: 199:34]`
note: closures can only be coerced to `fn` types if they do not capture any
      variables
   --> src/main.rs:199:39
    |
199 |         modify_all(&mut data, |y| y + amount_to_add);
    |                                       ^^^^^^^^^^^^^ `amount_to_add`
    |                                                     captured here
note: function defined here
   --> src/main.rs:60:12
    |
60  |     pub fn modify_all(data: &mut [u32], mutator: fn(u32) -> u32) {
    |            ^^^^^^^^^^                   -----------------------

En su lugar, el código que recibe el cierre tiene que aceptar una instancia de uno de los rasgos Fn*:

pub fn modify_all<F>(data: &mut [u32], mut mutator: F)
where
    F: FnMut(u32) -> u32,
{
    for value in data {
        *value = mutator(*value);
    }
}

Rust tiene tres rasgos diferentes Fn*, que entre ellos expresan algunas distinciones en torno a este comportamiento de captura del entorno:

FnOnce

Describe un cierre que sólo puede invocarseuna vez. Si alguna parte del entorno se moved en el contexto de la clausura, y el cuerpo de la clausura la mueve posteriormente fuera del contexto de la clausura, entonces esos movimientos sólo pueden ocurrir una vez -no hay otra copia del elemento fuente desde la que move - y, por tanto, la clausura sólo puede invocarse una vez.

FnMut

Describe un cierre que puede llamarse repetidamente y que puede realizar cambios en su entorno porque toma prestado mutablemente del entorno.

Fn

Describe un cierre que puede llamarse repetidamente y que sólo toma prestados valores del entorno de forma inmutable.

El compilador implementa automáticamente en el subconjunto adecuado de estos rasgos Fn* para cualquier expresión lambda del código; no es posible implementar manualmente ninguno de estos rasgos (a diferencia de la sobrecarga operator() de C++).4

Volviendo al modelo mental aproximado anterior de los cierres, cuál de los rasgos autoimplementa el compilador corresponde aproximadamente aa si el contexto ambiental capturado tiene estos elementos:

FnOnce

Cualquier valor desplazado

FnMut

Cualquier referencia mutable a valores (&mut T)

Fn

Sólo referencias normales a valores (&T)

Los dos últimos traits de esta lista tienen cada uno un trait bound del trait anterior, lo que tiene sentido si tienes en cuenta las cosas que utilizan los cierres:

  • Si algo espera llamar a un cierre una sola vez (lo que se indica recibiendo un FnOnce), está bien pasarle un cierre que sea capaz de ser llamado repetidamente (FnMut).

  • Si algo espera llamar repetidamente a un cierre que podría mutar su entorno (indicado mediante la recepción de unFnMut), está bien pasarle un cierre que no necesite mutar su entorno (Fn).

El tipo de puntero de función desnudo fn también pertenece teóricamente al final de esta lista; cualquier tipo (nounsafe) fn implementa automáticamente todos los rasgos Fn*, porque no toma prestado nada del entorno.

En consecuencia, cuando escribas código que acepte cierres, utiliza el rasgo Fn* más general que funcione, para permitir la mayor flexibilidad a los invocadores; por ejemplo, acepta FnOnce para los cierres que sólo se utilicen una vez. El mismo razonamiento lleva también a aconsejar que se prefieran los límites del rasgo Fn* a los punteros de función desnudos (fn).

Rasgos

Los rasgos Fn* son más flexibles que los punteros de función desnudos, pero aún así sólo pueden describir el comportamiento de una única función, e incluso entonces sólo en términos de la firma de la función.

Sin embargo, son en sí mismos ejemplos de otro mecanismo para describir el comportamiento en el sistema de tipos de Rust, el rasgo. Un trait define un conjunto de funciones relacionadas que algún elemento subyacente pone a disposición del público; además, las funciones son típicamente (pero no tienen por qué serlo) métodos, que toman como primer argumento alguna variante de self.

Cada función de un rasgo también tiene un nombre, que proporciona una etiqueta que permite al compilador desambiguar funciones con la misma firma y, lo que es más importante, que permite a los programadores deducir la intención de la función.

Un rasgo de Rust es aproximadamente análogo a una "interfaz" en Go y Java, o a una "clase abstracta" (todos los métodos virtuales, sin miembros de datos) en C++. Las implementaciones del rasgo deben proporcionar todas las funciones (pero ten en cuenta que la definición del rasgo puede incluir una implementación por defecto; Tema 13) y también pueden tener datos asociados que esas implementaciones utilicen. Esto significa que el código y los datos se encapsulan juntos en una abstracción común, de una manera un tanto orientada a objetos (OO).

El código que acepta un struct y llama a funciones sobre él está limitado a trabajar sólo con ese tipo específico. Si hay varios tipos que implementan un comportamiento común, entonces es más flexible definir un rasgo que encapsule ese comportamiento común, y hacer que el código utilice las funciones del rasgo en lugar de las funciones que implican a un struct específico.

Esto lleva al mismo tipo de consejo que aparece para otros lenguajes con influencia OO:5 prefiere aceptar tipos trait en lugar de tipos concretos si seprevé flexibilidad en el futuro.

A veces, hay algún comportamiento que quieres distinguir en el sistema de tipos, pero no puede expresarse como una firma de función específica en una definición de rasgo. Por ejemplo, considera un rasgo Sort para ordenar colecciones; una implementación podría ser estable (los elementos que se comparen igual aparecerán en el mismo orden antes y después de la ordenación), pero no hay forma de expresarlo en los argumentos del método sort.

En este caso, sigue mereciendo la pena utilizar el sistema de tipos para hacer un seguimiento de este requisito, utilizando un rasgo marcador:

pub trait Sort {
    /// Rearrange contents into sorted order.
    fn sort(&mut self);
}

/// Marker trait to indicate that a [`Sort`] sorts stably.
pub trait StableSort: Sort {}

Un rasgo marcador no tiene funciones, pero una implementación sigue teniendo que declarar que implementa el rasgo, lo que actúa como una promesa del implementador: "Juro solemnemente que mi implementación ordena de forma estable". El código que depende de una ordenación estable puede entonces especificar el límite del rasgo StableSort, confiando en el sistema de honor para preservar sus invariantes. Utiliza rasgos marcadores para distinguir comportamientos que no puedan expresarse en las firmas de función de los rasgos.

Una vez que el comportamiento se ha encapsulado en el sistema de tipos de Rust como un rasgo, se puede utilizar de dos formas:

  • Como un límite de rasgo, que restringe qué tipos son aceptables para un tipo de datos o función genéricos en tiempo de compilación

  • Como un objeto trait, que restringe qué tipos pueden almacenarse o pasarse a una función en tiempo de ejecución

Las secciones siguientes describen estas dos posibilidades, y el Tema 12 da más detalles sobre las compensaciones entre ellas.

Límites de rasgo

Un trait bound indica que el código genérico parametrizado por algún tipo T sólo puede utilizarse cuando ese tipo T implementa algún trait específico. La presencia del límite de rasgo significa que la implementación del genérico puede utilizar las funciones de ese rasgo, con la seguridad de que el compilador se asegurará de que cualquier T que compile tenga efectivamente esas funciones. Esta comprobación se realiza en tiempo de compilación, cuando el genérico se monomorfiza, es decir,se conviertedel código genérico que trata un tipo arbitrario T en código específico que trata un tipo concreto SomeType (lo que C++ llamaría instanciación de plantilla).

Esta restricción sobre el tipo objetivo T es explícita, codificada en los límites del rasgo: el rasgo sólo puede ser implementado por tipos que satisfagan los límites del rasgo. Esto contrasta con la situación equivalente en C++, donde las restricciones sobre el tipo T utilizado en un rasgotemplate<typename T> son implícitas:6 El código de plantilla C++ sólo se compila si todas las funciones referenciadas están disponibles en tiempo de compilación, pero las comprobaciones se basan únicamente en el nombre y la firma de la función. (Esta "tipificación de pato" puede llevar a confusión; una plantilla C++ que utilice t.pop()podría compilarse para un parámetro de tipo T de Stack oBalloon-lo que probablemente no sea el comportamiento deseado).

La necesidad de límites de rasgo explícitos también significa que una gran parte de los genéricos utilizan límites de rasgo. Para ver por qué ocurre esto, dale la vuelta a la observación y considera lo que se puede hacer con un struct Thing<T> en el que no hay límites de rasgo en T. Sin un límite de rasgo, el Thing sólo puede realizar operaciones que se apliquen a cualquier tipo T-básicamente, sólo mover o soltar el valor-. Esto, a su vez, permite contenedores genéricos, colecciones y punteros inteligentes, pero no mucho más. Cualquier cosa que utilice el tipo T necesitará un trait bound:

pub fn dump_sorted<T>(mut collection: T)
where
    T: Sort + IntoIterator,
    T::Item: std::fmt::Debug,
{
    // Next line requires `T: Sort` trait bound.
    collection.sort();
    // Next line requires `T: IntoIterator` trait bound.
    for item in collection {
        // Next line requires `T::Item : Debug` trait bound
        println!("{:?}", item);
    }
}

Así que el consejo aquí es utilizar trait bounds para expresar requisitos sobre los tipos utilizados en los genéricos, pero es un consejo fácil de seguir: el compilador te obligará a cumplirlo a pesar de todo.

Objetos rasgo

Un objeto trait es la otra forma de hacer uso de la encapsulación definida por un trait, pero aquí, las diferentes implementaciones posibles del trait se eligen en tiempo de ejecución en lugar de en tiempo de compilación. Este envío dinámico es análogo al uso de funciones virtuales en C++, y bajo las cubiertas, Rust tiene objetos "vtable" que sonaproximadamente análogos a los de C++.

Este aspecto dinámico de los objetos trait también significa que siempre tienen que manejarse indirectamente, mediante una referencia (por ejemplo, &dyn Trait) o un puntero (por ejemplo, Box<dyn Trait>) de algún tipo. La razón es que el tamaño del objeto que implementa el rasgo no se conoce en tiempo de compilación -podría ser un struct gigante o un enumdiminuto-, por lo que no hay forma de asignar la cantidad adecuada de espacio a un objeto rasgo desnudo.

No conocer el tamaño del objeto concreto también significa que los traits utilizados como objetos trait no pueden tener funciones que devuelvan el tipo Self ni argumentos (distintos del receptor, elobjeto sobre el que se invoca el método) que utilicen Self. La razón es que el código compilado por adelantado que utiliza el objeto trait no tendría ni idea de lo grande que podría ser ese Self.

Un trait que tiene una función genérica fn some_fn<T>(t:T) permite la posibilidad de un número infinito de funciones implementadas, para todos los tipos diferentes T que puedan existir. Esto está bien para un trait utilizado como límite de trait, porque el conjunto infinito de funciones genéricas posiblemente invocadas se convierte en un conjunto finito de funciones genéricas realmente invocadas en tiempo de compilación. No ocurre lo mismo con un objeto trait: el código disponible en tiempo de compilación tiene que hacer frente a todos los posibles Ts que puedan llegar en tiempo de ejecución.

Estas dos restricciones -no utilizar Self y no utilizar funciones genéricas- se combinan en el concepto de seguridad de los objetos. Sólo los rasgos seguros para los objetos pueden utilizarse como objetos rasgo.

Tema 3: Prefiere las transformaciones Option y Result a las expresiones explícitas match

El punto 1 expuso las virtudes de enum y mostró cómo las expresiones match obligan al programador a tener en cuenta todas las posibilidades. El Tema 1 también introdujo las dos omnipresentes enums que proporciona la biblioteca estándar de Rust :

Option<T>

Para expresar que un valor (del tipoT) puede o no estar presente

Result<T, E>

Para cuando una operación para devolver un valor (de tipo T) no tenga éxito y en su lugar devuelva un error (de tipo E)

Este Artículo explora situaciones en las que deberías intentar evitar expresiones match explícitas para estos enums particulares, prefiriendo en su lugar utilizar varios métodos de transformación que la biblioteca estándar proporciona para estos tipos. El uso de estos métodos de transformación (que normalmente se implementan como expresiones match ocultas) conduce a un código más compacto e idiomático y con una intención más clara.

La primera situación en la que un match es innecesario es cuando sólo el valor es relevante y la ausencia de valor (y cualquier error asociado) puede simplemente ignorarse:

struct S {
    field: Option<i32>,
}

let s = S { field: Some(42) };
match &s.field {
    Some(i) => println!("field is {i}"),
    None => {}
}

Para esta situación, una if let expresión es una línea más corta y, lo que es más importante, más clara:

if let Some(i) = &s.field {
    println!("field is {i}");
}

Sin embargo, la mayoría de las veces el programador tiene que proporcionar el correspondiente brazo else: la ausencia de un valor (Option::None), posiblemente con un error asociado (Result::Err(e)), es algo con lo que el programador tiene que lidiar. Diseñar software para hacer frente a rutas de fallo es difícil, y la mayor parte de ello es complejidad esencial con la que ninguna cantidad de soporte sintáctico puede ayudar, en concreto, decidir qué debe ocurrir si falla una operación.

En algunas situaciones, la decisión correcta es realizar una maniobra de avestruz: meter la cabeza en la arena y no enfrentarse explícitamente al fallo. No puedes ignorar completamente el brazo de error, porque Rust requiere que el código se ocupe de las dos variantes del Error enum , pero puedes elegir tratar un fallo como fatal. Realizar un panic! en caso de fallo significa que el programa termina, pero el resto del código puede escribirse asumiendo que ha tenido éxito.Hacer esto con un match explícito sería innecesariamenteverboso:

let result = std::fs::File::open("/etc/passwd");
let f = match result {
    Ok(f) => f,
    Err(_e) => panic!("Failed to open /etc/passwd!"),
};
// Assume `f` is a valid `std::fs::File` from here onward.

Tanto Option como Result proporcionan un par de métodos que extraen su valor interno y panic! si está ausente: unwrap y expect. Este último permite personalizar el mensaje de error en caso de fallo, pero en cualquier caso, el código resultante es más corto y sencillo: la gestión del error se delega en el sufijo .unwrap() (pero sigue estando presente):

let f = std::fs::File::open("/etc/passwd").unwrap();

Pero tenlo claro: estas funciones de ayuda siguen siendo panic!, por lo que elegir utilizarlas es lo mismo que elegir panic!(Tema 18).

Sin embargo, en muchas situaciones, la decisión correcta para la gestión de errores es aplazar la decisión a otra persona. Esto es especialmente cierto cuando se escribe una biblioteca, en la que el código puede utilizarse en todo tipo de entornos diferentes que el autor de la biblioteca no puede prever. Para facilitar el trabajo a esa otra persona, prefiere Result a Option para expresar los errores, aunque ello implique conversiones entre distintos tipos de error(Tema 4).

Por supuesto, esto abre la pregunta: ¿Qué se considera un error? En este ejemplo, no poder abrir un archivo es definitivamente un error, y los detalles de ese error (¿no existe tal archivo? ¿permiso denegado?) pueden ayudar al usuario a decidir qué hacer a continuación. En cambio, no poder recuperar el elemento first() de una porción porque está vacía no es realmente un error, por lo que se expresa como un tipo de retorno Option en la biblioteca estándar. Elegir entre las dos posibilidades requiere juicio, pero inclínate por Result si un error puede comunicar algo útil.

Result también tiene unatributo #[must_use]para empujar a los usuarios de la biblioteca en la direccióncorrecta : siel código que utiliza el Result devuelto lo ignora, el compilador generará una advertencia:

warning: unused `Result` that must be used
  --> src/main.rs:63:5
   |
63 |     f.set_len(0); // Truncate the file
   |     ^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
   |
63 |     let _ = f.set_len(0); // Truncate the file
   |     +++++++

Utilizar explícitamente un match permite que se propague un error, pero a costa de cierta burocracia visible (que recuerda a Go):

pub fn find_user(username: &str) -> Result<UserId, std::io::Error> {
    let f = match std::fs::File::open("/etc/passwd") {
        Ok(f) => f,
        Err(e) => return Err(From::from(e)),
    };
    // ...
}

El ingrediente clave para reducir la burocracia es el operador de signo de interrogación de Rust , ?. Este trozo de azúcar sintáctico se encarga de hacer coincidir el brazo Err, transformar el tipo de error si es necesario y construir la expresión return Err(...), todo en un solocarácter:

pub fn find_user(username: &str) -> Result<UserId, std::io::Error> {
    let f = std::fs::File::open("/etc/passwd")?;
    // ...
}

Los recién llegados a Rust a veces encuentran esto desconcertante: el signo de interrogación puede ser difícil de detectar a primera vista, lo que provoca inquietud sobre cómo es posible que funcione el código. Sin embargo, incluso con un solo carácter, el sistema de tipos sigue funcionando, garantizando que se cubran todas las posibilidades expresadas en los tipos relevantes(Elemento 1), dejando que el programador se centre en la ruta principal del código sin distracciones.

Además, por lo general, estas aparentes invocaciones a métodos no tienen ningún coste: son todas funciones genéricas marcadas como #[inline]por lo que el código generado suele compilar código máquina idéntico a la versión manual.

Estos dos factores tomados en conjunto significan que debes preferir las transformaciones Option y Result a las expresiones explícitas match.

En el ejemplo anterior, los tipos de error se alineaban: tanto el método interno como el externo expresaban errores comostd::io::Error. A menudo no es así: una función puede acumular errores de varias sublibrerías distintas, cada una de las cuales utiliza tipos de error diferentes.

El mapeo de errores en general se trata en el Tema 4, pero por ahora, ten en cuenta que un mapeo manual de:

pub fn find_user(username: &str) -> Result<UserId, String> {
    let f = match std::fs::File::open("/etc/passwd") {
        Ok(f) => f,
        Err(e) => {
            return Err(format!("Failed to open password file: {:?}", e))
        }
    };
    // ...
}

podría expresarse de forma más sucinta e idiomática con la siguiente .map_err() transformación:

pub fn find_user(username: &str) -> Result<UserId, String> {
    let f = std::fs::File::open("/etc/passwd")
        .map_err(|e| format!("Failed to open password file: {:?}", e))?;
    // ...
}

Mejor aún, incluso esto puede no ser necesario: si el tipo de error externo puede crearse a partir del tipo de error interno mediante una implementación del rasgo estándar From (punto 10), entonces el compilador realizará automáticamente la conversión sin necesidad de llamar a .map_err().

Este tipo de transformaciones se generalizan más ampliamente. El operador interrogación es un gran martillo; utiliza métodos de transformación en los tipos Option y Result para maniobrarlos hasta una posición en la que puedan ser un clavo.

La biblioteca estándar proporciona una amplia variedad de estos métodos de transformación para que esto sea posible. La Figura 1-1muestra algunos de los métodos más comunes (rectángulos claros) que transforman entre los tipos correspondientes (rectángulos oscuros). De acuerdo con el punto 18, los métodos que pueden panic! están marcados con un asterisco.

The diagram shows mappings between Result, Option and related types.  Gray boxes show types, and white rounded boxes show methods that transform between types.  Methods that can panic are marked with an asterisk. In the middle are the Result<T, E> and Option<T> types, with methods ok, ok_or and ok_or_else that convert between them. To one side of Result<T, E> are the or and or_else methods that transform back to the same type. To one side of Option<T> are various methods that transform back to the same type: filter, xor, or, or_else and replace. Across the top and bottom of the diagram are various related types that can covert to or from Result and Option. For Result<T, E>, the map method reaches Result<T, F>, the map, and and and_then methods reach Result<U, E>, and the map_or and map_or_else methods reach U, with all of the destinations at the bottom of the diagram. At the top of the diagram, Result<T, E> maps to Option<E> via err, to E via unwrap_err and expect_err (both of which can panic), and to T via a collection of methods: unwrap, expect, unwrap_or, unwrap_or_else, unwrap_or_default (where unwrap and expect might panic).  The E and T types map back to Result<T, E> via the Err(e) and Ok(t) enum variants.  For Option<T>, the map, and and and_then methods reach Option<U>, and the map_or and map_or_else methods reach U at the bottom of the diagram. At the top of the diagram, Option<T> maps to T via the same collection of methods as for Result: unwrap, expect, unwrap_or, unwrap_or_else, unwrap_or_default (where unwrap and expect might panic).  The T type maps back to Option<T> via the Some(t) enum; the () type also maps to Option<T> via None.
Figura 1-1. TransformacionesOption y Result 7

Una situación común que el diagrama no cubre tiene que ver con las referencias. Por ejemplo, considera una estructura que contiene opcionalmente algunos datos:

struct InputData {
    payload: Option<Vec<u8>>,
}

Un método en este struct que intente pasar la carga útil a una función de encriptación con firma (&[u8]) -> Vec<u8>falla si hay un intento ingenuo de tomar una referencia:

error[E0507]: cannot move out of `self.payload` which is behind a shared
              reference
  --> src/main.rs:15:18
   |
15 |     encrypt(&self.payload.unwrap_or(vec![]))
   |              ^^^^^^^^^^^^ move occurs because `self.payload` has type
   |                           `Option<Vec<u8>>`, which does not implement the
   |                           `Copy` trait

La herramienta adecuada para ello es el as_ref() método en Option.8 Este método convierte una referencia a unaOption en una Option-de-una-referencia:

pub fn encrypted(&self) -> Vec<u8> {
    encrypt(self.payload.as_ref().unwrap_or(&vec![]))
}

Cosas para recordar

  • Acostúmbrate a las transformaciones de Option y Result, y prefiere Result a Option. Utiliza .as_ref() cuando sea necesario cuando las transformaciones impliquen referencias.

  • Utiliza estas transformaciones con preferencia a las operaciones explícitas de match en Option y Result.

  • En concreto, utiliza estas transformaciones para convertir los tipos de resultado a una forma en la que se aplique el operador ?.

Tema 4: Prefiere tipos idiomáticos Error

El Tema 3 describía cómo utilizar las transformaciones que la biblioteca estándar proporciona para los tipos Option y Result para permitir un manejo conciso e idiomático de los tipos de resultado utilizando el operador ?. No llegó a discutir la mejor manera de tratar la variedad de tipos de error E que surgen como segundo argumento de tipo de un Result<T, E>; ése es el tema de este Tema.

Esto sólo es relevante cuando hay varios tipos de error diferentes en juego. Si todos los errores diferentes que encuentra una función ya son del mismo tipo, puede limitarse a devolver ese tipo. Cuando hay errores de distintos tipos, hay que decidir si debe conservarse la información del tipo de suberror.

El rasgo Error

Siempre es bueno entender lo que implican los rasgos estándar(Tema 10), y el rasgo relevante aquí es std::error::Error. El parámetro de tipo E para unResult no tiene por qué ser un tipo que implemente Error, pero es una convención común que permite a las envolturas expresar límites de rasgo apropiados, así que prefiere implementar Error para tus tipos de error.

Lo primero que hay que observar es que el único requisito estricto para los tipos Error son los límites del rasgo : cualquier tipo que implemente Error también tiene que implementar los rasgos siguientes:

  • El rasgo Display, lo que significa que se puede format!ed con {}

  • El rasgo Debug, lo que significa que se puede format!ed con {:?}

En otras palabras, debería ser posible mostrar los tipos Error tanto al usuario como al programador.

El único método del rasgo essource(),9 que permite a un tipo Error exponer un error interno anidado. Este método es opcional: viene con una implementación por defecto(Elemento 13) que devuelve None, lo que indica que la información sobre el error interno no está disponible.

Una última cosa a tener en cuenta: si estás escribiendo código para un entorno no_std (Tema 33), puede que no sea posible implementar Error-el rasgo Error está actualmente implementado enstd, no en core, por lo que no está disponible.10

Errores mínimos

Si no se necesita información de error anidada, entonces una implementación del tipo Error no necesita ser mucho más que unString-una rara ocasión en la que una variable "stringly typed" podría ser apropiada-. Sin embargo, sí necesita seralgo más que un String; aunque es posible utilizar String como parámetro del tipo E:

pub fn find_user(username: &str) -> Result<UserId, String> {
    let f = std::fs::File::open("/etc/passwd")
        .map_err(|e| format!("Failed to open password file: {:?}", e))?;
    // ...
}

a String no implementa Error, lo que preferiríamos para que otras áreas de código puedan ocuparse de Errors. No es posible impl Error para String, porque ni el rasgo ni el tipo nos pertenecen (la llamada regla del huérfano):

error[E0117]: only traits defined in the current crate can be implemented for
              types defined outside of the crate
  --> src/main.rs:18:5
   |
18 |     impl std::error::Error for String {}
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^------
   |     |                          |
   |     |                          `String` is not defined in the current crate
   |     impl doesn't use only types from inside the current crate
   |
   = note: define and implement a trait or new type instead

Un alias de tipo tampoco ayuda, porque no crea un nuevo tipo y, por tanto, no cambia el mensaje de error:

error[E0117]: only traits defined in the current crate can be implemented for
              types defined outside of the crate
  --> src/main.rs:41:5
   |
41 |     impl std::error::Error for MyError {}
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^-------
   |     |                          |
   |     |                          `String` is not defined in the current crate
   |     impl doesn't use only types from inside the current crate
   |
   = note: define and implement a trait or new type instead

Como de costumbre, el mensaje de error del compilador da una pista para resolver el problema. Definir una estructura de tupla que envuelva el tipoString (el "patrón newtype", punto 6) permite implementar el rasgo Error, siempre que Debug y Display también estén implementados:

#[derive(Debug)]
pub struct MyError(String);

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl std::error::Error for MyError {}

pub fn find_user(username: &str) -> Result<UserId, MyError> {
    let f = std::fs::File::open("/etc/passwd").map_err(|e| {
        MyError(format!("Failed to open password file: {:?}", e))
    })?;
    // ...
}

Por comodidad de , puede tener sentido implementar el rasgo From<String> para permitir que los valores de cadena se conviertan fácilmente en instancias de MyError (Tema 5):

impl From<String> for MyError {
    fn from(msg: String) -> Self {
        Self(msg)
    }
}

Cuando encuentre el operador interrogación (?), el compilador aplicará automáticamente las implementaciones de rasgos From pertinentes que sean necesarias para alcanzar el tipo de retorno de error de destino. Esto permite una mayor minimización:

pub fn find_user(username: &str) -> Result<UserId, MyError> {
    let f = std::fs::File::open("/etc/passwd")
        .map_err(|e| format!("Failed to open password file: {:?}", e))?;
    // ...
}

La ruta del error cubre aquí los siguientes pasos:

  • File::open devuelve un error de tipo std::io::Error.

  • format! lo convierte en un String, utilizando la implementación Debug de std::io::Error.

  • ? hace que el compilador busque y utilice una implementación de From que pueda llevarlo de String a MyError.

Errores anidados

El escenario alternativo es aquel en el que el contenido de los errores anidados es lo suficientemente importante como para que deba conservarse y ponerse a disposición de la persona que llama.

Considera una función de biblioteca que intenta devolver la primera línea de un archivo como una cadena, siempre que la línea no sea demasiado larga. Un momento de reflexión revela (al menos) tres tipos distintos de fallo que podrían producirse:

  • Puede que el archivo no exista o que sea inaccesible para la lectura.

  • El archivo puede contener datos de que no sean UTF-8 válidos y, por tanto, no puedan convertirse a String.

  • Puede que el archivo tenga una primera línea demasiado larga.

De acuerdo con el punto 1, puedes utilizar el sistema de tipos para expresar y englobar todas estas posibilidades como enum:

#[derive(Debug)]
pub enum MyError {
    Io(std::io::Error),
    Utf8(std::string::FromUtf8Error),
    General(String),
}

Esta definición enum incluye una derive(Debug), pero para satisfacer el rasgo Error, también se necesita unaDisplay implementación:

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MyError::Io(e) => write!(f, "IO error: {}", e),
            MyError::Utf8(e) => write!(f, "UTF-8 error: {}", e),
            MyError::General(s) => write!(f, "General error: {}", s),
        }
    }
}

También tiene sentido anular la implementación por defecto de source() para acceder fácilmente a los errores anidados:

use std::error::Error;

impl Error for MyError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            MyError::Io(e) => Some(e),
            MyError::Utf8(e) => Some(e),
            MyError::General(_) => None,
        }
    }
}

El uso de enum permite que la gestión de errores sea concisa y, al mismo tiempo, conserva toda la información del tipo en las distintas clases de error:

use std::io::BufRead; // for `.read_until()`

/// Maximum supported line length.
const MAX_LEN: usize = 1024;

/// Return the first line of the given file.
pub fn first_line(filename: &str) -> Result<String, MyError> {
    let file = std::fs::File::open(filename).map_err(MyError::Io)?;
    let mut reader = std::io::BufReader::new(file);

    // (A real implementation could just use `reader.read_line()`)
    let mut buf = vec![];
    let len = reader.read_until(b'\n', &mut buf).map_err(MyError::Io)?;
    let result = String::from_utf8(buf).map_err(MyError::Utf8)?;
    if result.len() > MAX_LEN {
        return Err(MyError::General(format!("Line too long: {}", len)));
    }
    Ok(result)
}

También es una buena idea implementar el rasgo From para todos los tipos de suberror(Tema 5):

impl From<std::io::Error> for MyError {
    fn from(e: std::io::Error) -> Self {
        Self::Io(e)
    }
}
impl From<std::string::FromUtf8Error> for MyError {
    fn from(e: std::string::FromUtf8Error) -> Self {
        Self::Utf8(e)
    }
}

Esto evita que los propios usuarios de la biblioteca sufran bajo las reglas de orfandad: no se les permite implementarFrom en MyError, porque tanto el trait como el struct son externos a ellos.

Mejor aún, la implementación de From permite aún más concisión, porque el operador de signo de interrogación realizará automáticamente cualquier conversión necesaria de From, eliminando la necesidad de .map_err():

use std::io::BufRead; // for `.read_until()`

/// Maximum supported line length.
pub const MAX_LEN: usize = 1024;
/// Return the first line of the given file.
pub fn first_line(filename: &str) -> Result<String, MyError> {
    let file = std::fs::File::open(filename)?; // `From<std::io::Error>`
    let mut reader = std::io::BufReader::new(file);
    let mut buf = vec![];
    let len = reader.read_until(b'\n', &mut buf)?; // `From<std::io::Error>`
    let result = String::from_utf8(buf)?; // `From<string::FromUtf8Error>`
    if result.len() > MAX_LEN {
        return Err(MyError::General(format!("Line too long: {}", len)));
    }
    Ok(result)
}

Escribir un tipo de error completo puede implicar una buena cantidad de repeticiones, lo que lo convierte en un buen candidato para la automatización mediante una macroderive (Tema 28). Sin embargo, no es necesario que escribas tú mismo dicha macro:considera la posibilidad de utilizar la caja thiserror de David Tolnay, que proporciona una implementación de alta calidad y ampliamente utilizada de dicha macro. El código generado porthiserror también tiene cuidado de evitar que los tipos de this​er⁠ror sean visibles en la API generada, lo que a su vez significa que las preocupaciones asociadas al punto 24 no son aplicables.

Objetos Rasgo

El primer enfoque de los errores anidados desechaba todos los detalles de los suberrores, limitándose a conservar algunas cadenas de salida (format!("{:?}", err)). El segundo enfoque conservaba la información completa del tipo de todos los posibles suberrores, pero requería una enumeración completa de todos los posibles tipos de suberror.

Esto plantea la pregunta: ¿Existe un término medio entre estos dos enfoques, que preserve la información de los suberrores sin necesidad de incluir manualmente todos los tipos de error posibles?

Codificar la información del suberror como un objeto de rasgo evita la necesidad de una variante de enum para cada posibilidad, pero borra los detalles de los tipos de error específicos subyacentes. El receptor de un objeto de este tipo tendría acceso a los métodos del rasgo Error y a sus límites de rasgo -source(), Display::fmt(), y Debug::fmt(), a su vez - pero no conocería el tipo estático original del suberror:

Resulta que esto es posible, pero es sorprendentemente sutil. Parte de la dificultad proviene de las restricciones de seguridad de los objetos trait(Tema 12), pero también entran en juego las reglas de coherencia de Rust, que (a grandes rasgos) dicen que puede haber como máximo una implementación de un trait para un tipo.

Ingenuamente, cabría esperar que un tipo putativo de WrappedError pusiera en práctica ambascosas:

  • El rasgo Error, porque es un error en sí mismo.

  • El rasgo From<Error>, para permitir que los suberrores se envuelvan fácilmente.

Eso significa que se puede crear un WrappedError from internoWrappedError, ya que WrappedError implementa Error, y eso choca con la implementación reflexiva general de From:

error[E0119]: conflicting implementations of trait `From<WrappedError>` for
              type `WrappedError`
   --> src/main.rs:279:5
    |
279 |     impl<E: 'static + Error> From<E> for WrappedError {
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: conflicting implementation in crate `core`:
            - impl<T> From<T> for T;

David Tolnay anyhow es una crate que ya ha resuelto estos problemas (añadiendo un nivel adicional de indirección a través de Box) y que, además, añade otras funciones útiles (como las trazas de pila). Como resultado, se está convirtiendo rápidamente en la recomendación estándar para el tratamiento de errores, una recomendación que secundamos aquí: considera el uso del crate anyhow para el tratamiento de errores en las aplicaciones.

Bibliotecas frente a aplicaciones

El último consejo de la sección anterior incluía el calificativo "...para la gestión de errores en las aplicaciones", porque a menudo hay una distinción entre el código que se escribe para reutilizarlo en una biblioteca y el código que forma una aplicación de nivel superior.11

El código que se escribe para una biblioteca no puede predecir el entorno en el que se utiliza el código, por lo que es preferible emitir información de error concreta y detallada y dejar que la persona que llama averigüe cómo utilizar esa información. Esto se inclina hacia los errores anidados al estilo de enum descritos anteriormente (y también evita una dependencia de anyhow en la API pública de la biblioteca, véase el Tema 24).

Sin embargo, el código de la aplicación suele tener que concentrarse más en cómo presentar los errores al usuario. También tiene que hacer frente potencialmente a todos los tipos de error diferentes emitidos por todas las bibliotecas que están presentes en su gráfico de dependencias(Tema 25). Por ello, un tipo de error más dinámico (comoanyhow::Error) hace que la gestión de errores sea más sencilla y coherente en toda la aplicación.

Cosas para recordar

  • El rasgo estándar Error requiere poco de ti, así que prefiere implementarlo para tus tipos de error.

  • Cuando trates con tipos de error subyacentes heterogéneos, decide si es necesario conservar esos tipos.

    • Si no es así, considera la posibilidad de utilizar anyhow para envolver suberrores en el código de la aplicación.

    • Si es así, codifícalos en un enum y proporciona las conversiones. Considera la posibilidad de utilizarthiserror como ayuda.

  • Considera la posibilidad de utilizar la caja anyhow para una cómoda gestión idiomática de errores en el código de la aplicación.

  • Es tu decisión, pero decidas lo que decidas, codifícalo en el sistema de tipos(Tema 1).

Tema 5: Comprender las conversiones de tipo

Conversiones de tipo de óxido se dividen en tres categorías:

Manual

Conversiones de tipo definidas por el usuario mediante la implementación de los rasgos From y Into

Semiautomático

Elaboraciones explícitas entre valores utilizando la palabra clave as

Automático

Coerción implícita en un nuevo tipo

La mayor parte de este Tema se centra en la primera de ellas, las conversiones manuales de tipos, porque las dos últimas no se aplican a las conversiones de tipos definidos por el usuario. Hay un par de excepciones, por lo que las secciones del final del Tema tratan sobre el moldeado y la coerción, incluyendo cómo pueden aplicarse a un tipo definido por el usuario.

Ten en cuenta que, a diferencia de muchos lenguajes antiguos, Rust no realiza conversiones automáticas entre tipos numéricos. Esto se aplica incluso a las transformaciones "seguras" de tipos integrales:

error[E0308]: mismatched types
  --> src/main.rs:70:18
   |
70 |     let y: u64 = x;
   |            ---   ^ expected `u64`, found `u32`
   |            |
   |            expected due to this
   |
help: you can convert a `u32` to a `u64`
   |
70 |     let y: u64 = x.into();
   |                   +++++++

Conversiones de tipo definidas por el usuario

Como ocurre con otras características del lenguaje(Tema 10), la capacidad de realizar conversiones entre valores de distintos tipos definidos por el usuario se encapsula como un rasgo estándar -o, mejor dicho, como un conjunto de rasgos genéricos relacionados-.

Los cuatro rasgos relevantes de que expresan la capacidad de convertir valores de un tipo sonlos siguientes:

From<T>

Los elementos de este tipo pueden construirse a partir de elementos del tipo T, y la conversión siempre tiene éxito.

TryFrom<T>

Los elementos de este tipo pueden construirse a partir de elementos del tipo T, pero es posible que la conversión no tenga éxito.

Into<T>

Los elementos de este tipo pueden convertirse en elementos del tipo T, y la conversión siempre tiene éxito.

TryInto<T>

Los elementos de este tipo pueden convertirse en elementos del tipo T, pero es posible que la conversión no tenga éxito.

Dada la discusión del punto 1 sobre expresar cosas en el sistema de tipos, no sorprende descubrir que la diferencia con las variantes de Try... es que el único método de rasgo devuelve un Result en lugar de un nuevo elemento garantizado. Las definiciones de rasgo Try... también requieren un tipo asociado que dé el tipo del error Eemitido para situaciones de fallo.

Por tanto, el primer consejo es implementar (sólo) el rasgo Try... si es posible que falle una conversión, de acuerdo con el punto 4. La alternativa es ignorar la posibilidad de error (por ejemplo, con.unwrap()), pero esa debe ser una elección deliberada, y en la mayoría de los casos es mejor dejar esa elección al que llama.

Los rasgos de conversión de tipos tienen una simetría obvia: si un tipo T puede transformarse into un tipo U (a través de Into<U>), ¿no es lo mismo que sea posible crear un elemento de tipo U transformando from un elemento de tipo T (a través deFrom<T>)?

Esto es así, y nos lleva al segundo consejo: implementa el rasgo From para las conversiones. La biblioteca estándar de Rust tuvo que elegir sólo una de las dos posibilidades, para evitar que el sistema girara en círculos vertiginosos,12 y se decantó por proporcionar automáticamente Into a partir de una implementación de From.

Si estás consumiendo uno de estos dos rasgos, como un rasgo ligado a un nuevo genérico propio, entonces el consejo es el inverso:utiliza el rasgo Into para los rasgos ligados. De este modo, el límite será satisfecho tanto por cosas que implementen directamente Into como por cosas que sólo implementen directamente From.

Esta conversión automática se destaca en la documentación de From y Into, pero también merece la pena leer la parte correspondiente del código de la biblioteca estándar, que es una implementación de rasgos generalizada:

impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

Traducir a palabras la especificación de un rasgo puede ayudar a comprender límites de rasgo más complejos. En este caso, es bastante sencillo: "Puedo implementar Into<U> para un tipo T siempre que U ya implemente From<T>."

La biblioteca estándar también incluye varias implementaciones de estos rasgos de conversión para tipos de la biblioteca estándar. Como era de esperar, existen implementaciones de From para conversiones integrales en las que el tipo de destino incluye todos los valores posibles del tipo de origen (From<u32> for u64), e implementaciones de TryFrom cuando el origen puede no caber en el destino (TryFrom<u64> for u32).

Además de la versión Into mostrada anteriormente, existen otras implementaciones de rasgos generales, principalmente para los tipos de puntero inteligente, que permiten que el puntero inteligente se construya automáticamente a partir de una instancia del tipo que contiene. Esto significa que los métodos genéricos que aceptan parámetros de puntero inteligente también pueden invocarse con elementos simples; más sobre esto próximamente y en el Tema 8.

El rasgo TryFrom también tiene una implementación general para cualquier tipo que ya implemente el rasgo Into en la direcciónopuesta, lo que incluye automáticamente (como se ha mostrado anteriormente) cualquier tipo que implemente From en la misma dirección. En otras palabras, si puedes convertir infaliblemente un T en un U, también puedes obtener infaliblemente un U a partir de unT; como esta conversión siempre tendrá éxito, el tipo de error asociado es el útilmente denominado Infallible.13

En también destaca una implementación genérica muy específica de From, la implementación reflexiva:

impl<T> From<T> for T {
    fn from(t: T) -> T {
        t
    }
}

Traducido a palabras, esto sólo dice que "dado un T, puedo obtener un T." Es un "bueno, duh" tan obvio que merece la pena pararse a entender por qué es útil.

Considera un simple newtype struct (Elemento 6) y una función que opere sobre él (ignorando que esta función se expresaría mejor como un método):

/// Integer value from an IANA-controlled range.
#[derive(Clone, Copy, Debug)]
pub struct IanaAllocated(pub u64);

/// Indicate whether value is reserved.
pub fn is_iana_reserved(s: IanaAllocated) -> bool {
    s.0 == 0 || s.0 == 65535
}

Esta función puede invocarse con instancias de struct:

let s = IanaAllocated(1);
println!("{:?} reserved? {}", s, is_iana_reserved(s));
// output: "IanaAllocated(1) reserved? false"

pero aunque se implemente From<u64> para la envoltura newtype:

impl From<u64> for IanaAllocated {
    fn from(v: u64) -> Self {
        Self(v)
    }
}

la función no puede invocarse directamente para los valores u64:

error[E0308]: mismatched types
  --> src/main.rs:77:25
   |
77 |     if is_iana_reserved(42) {
   |        ---------------- ^^ expected `IanaAllocated`, found integer
   |        |
   |        arguments to this function are incorrect
   |
note: function defined here
  --> src/main.rs:7:8
   |
7  | pub fn is_iana_reserved(s: IanaAllocated) -> bool {
   |        ^^^^^^^^^^^^^^^^ ----------------
help: try wrapping the expression in `IanaAllocated`
   |
77 |     if is_iana_reserved(IanaAllocated(42)) {
   |                         ++++++++++++++  +

Sin embargo, una versión genérica de la función que acepta (y convierte explícitamente) cualquier cosa que satisfaga Into<IanaAllocated>:

pub fn is_iana_reserved<T>(s: T) -> bool
where
    T: Into<IanaAllocated>,
{
    let s = s.into();
    s.0 == 0 || s.0 == 65535
}

permite este uso:

if is_iana_reserved(42) {
    // ...
}

Con este vínculo de rasgo, la implementación reflexiva de rasgo de From<T> tiene más sentido: significa que la función genérica se ocupa de elementos que ya son instancias de IanaAllocated, sin necesidad de conversión.

Este patrón también explica por qué (y cómo) a veces parece que el código Rust realiza conversiones implícitas entre tipos: la combinación de las implementaciones de From<T> y los límites de los rasgos de Into<T> hace que el código parezca convertir mágicamente en el lugar de la llamada (pero sigue realizando conversiones seguras y explícitas bajo cuerda). Este patrón se vuelve aún más poderoso cuando se combina con tipos de referencia y sus rasgos de conversión relacionados; más información en el Tema 8.

Reparto

Rust incluye la palabra clave as para realizar conversiones explícitas entre algunos pares de tipos.

Los pares de tipos que pueden convertirse de este modo constituyen un conjunto bastante limitado, y los únicos tipos definidos por el usuario que incluye son los enum"tipo C" (los que sólo tienen un valor entero asociado). Sin embargo, se incluyen conversiones integrales generales, lo que supone una alternativa a into():

let x: u32 = 9;
let y = x as u64;
let z: u64 = x.into();

La versión as también permite conversiones con pérdida:14

let x: u32 = 9;
let y = x as u16;

que sería rechazado por las versiones from/into:

error[E0277]: the trait bound `u16: From<u32>` is not satisfied
   --> src/main.rs:136:20
    |
136 |     let y: u16 = x.into();
    |                    ^^^^ the trait `From<u32>` is not implemented for `u16`
    |
    = help: the following other types implement trait `From<T>`:
              <u16 as From<NonZeroU16>>
              <u16 as From<bool>>
              <u16 as From<u8>>
    = note: required for `u32` to implement `Into<u16>`

Por coherencia y seguridad de , deberías preferir las conversiones from/ into a los vaciados as , a menos que entiendas y necesites la semántica precisa del vaciado (por ejemplo, para la interoperabilidad con C). Este consejo puede verse reforzado por Clippy(Tema 29), que incluye varias indicaciones sobre las conversionesas ; sin embargo, estas indicaciones están desactivadas por defecto.

Coacción

Las conversiones explícitas as descritas en el apartado anterior son un superconjunto de lascoerciones implícitas que el compilador realizará silenciosamente: cualquier coerción puede forzarse con una as explícita, pero lo contrario no es cierto. En particular, las conversiones integrales realizadas en la sección anterior no son coerciones, por lo que siempre requerirán as.

La mayoría de las coerciones implican conversiones silenciosas de los tipos de puntero y referencia de de formas que sean sensatas y convenientes para el programador, como la conversión de lo siguiente:

  • Una referencia mutable a una referencia inmutable (así puedes utilizar un &mut T como argumento de una funciónque toma un &T)

  • Una referencia a un puntero sin procesar (esto no es unsafe- la inseguridad se produce en el momento en que eres lo suficientemente tonto como para desreferenciar un puntero sin procesar)

  • Un cierre que resulta que no captura ninguna variable en un puntero de función desnudo(Elemento 2)

  • De una matriz a unaporción

  • Un elemento concreto a un objeto rasgo, para un rasgo que el elemento concreto implementa

  • Un artículo de por vida a otro "más corto"(Tema 14)15

Sólo hay dos coerciones cuyo comportamiento puede verse afectado por los tipos definidos por el usuario. La primera ocurre cuando un tipo definido por el usuario implementa la coerción Deref o el rasgo DerefMut rasgo. Estos rasgos indican que el tipo definido por el usuario está actuando como un puntero inteligente de algún tipo(Elemento 8), y en este caso el compilador coaccionará una referencia al elemento puntero inteligente para que sea una referencia a un elemento del tipo que contiene el puntero inteligente (indicado por su rasgo Target).

La segunda coerción de un tipo definido por el usuario se produce cuando un elemento concreto se convierte en un objeto trait. Esta operación construye un puntero gordo al elemento; este puntero es gordo porque incluye tanto un puntero a la ubicación del elemento en memoria como un puntero a la vtable para la implementación del rasgo del tipo concreto -ver Tema 8.

Tema 6: Adoptar el patrón newtype

El Tema 1 describía los structs de tupla, en los que los campos de un struct no tienen nombre y en su lugar se hace referencia a ellos mediante un número (self.0). Este Tema se centra en los structs de tupla que tienen una única entrada de algún tipo existente, creando así un nuevo tipo que puede contener exactamente el mismo rango de valores que el tipo adjunto. Este patrón está tan extendido en Rust que merece su propio Artículo y tiene su propio nombre: el patrón newtype.

El uso más sencillo del patrón newtype es indicar semántica adicional para un tipo, por encima de su comportamiento normal. Para ilustrarlo, imagina un proyecto que va a enviar un satélite a Marte.16 Es un proyecto grande, así que diferentes grupos han construido diferentes partes del proyecto. Un grupo se ha encargado del código de los motores del cohete:

/// Fire the thrusters. Returns generated impulse in pound-force seconds.
pub fn thruster_impulse(direction: Direction) -> f64 {
    // ...
    return 42.0;
}

mientras que otro grupo se encarga del sistema de guiado inercial:

/// Update trajectory model for impulse, provided in Newton seconds.
pub fn update_trajectory(force: f64) {
    // ...
}

Con el tiempo, estas diferentes partes deben unirse:

let thruster_force: f64 = thruster_impulse(direction);
let new_direction = update_trajectory(thruster_force);

Ruh-roh.17

Rust incluye una función de alias de tipo, que permite a los distintos grupos dejar más claras sus intenciones:

/// Units for force.
pub type PoundForceSeconds = f64;

/// Fire the thrusters. Returns generated impulse.
pub fn thruster_impulse(direction: Direction) -> PoundForceSeconds {
    // ...
    return 42.0;
}
/// Units for force.
pub type NewtonSeconds = f64;

/// Update trajectory model for impulse.
pub fn update_trajectory(force: NewtonSeconds) {
    // ...
}

Sin embargo, los alias de tipo no son más que documentación; son una indicación más clara que los comentarios de la versión anterior, pero nada impide que se utilice un valor PoundForceSeconds cuando se espera un valorNewtonSeconds:

let thruster_force: PoundForceSeconds = thruster_impulse(direction);
let new_direction = update_trajectory(thruster_force);

Ruh-roh una vez más.

Este es el punto en el que ayuda el patrón newtype:

/// Units for force.
pub struct PoundForceSeconds(pub f64);

/// Fire the thrusters. Returns generated impulse.
pub fn thruster_impulse(direction: Direction) -> PoundForceSeconds {
    // ...
    return PoundForceSeconds(42.0);
}
/// Units for force.
pub struct NewtonSeconds(pub f64);

/// Update trajectory model for impulse.
pub fn update_trajectory(force: NewtonSeconds) {
    // ...
}

Como su nombre indica, un newtype es un tipo nuevo y, como tal, el compilador se opone cuando hay un desajuste de tipos: aquí se intenta pasar un valor PoundForceSeconds a algo que espera un valor NewtonSeconds:

error[E0308]: mismatched types
  --> src/main.rs:76:43
   |
76 |     let new_direction = update_trajectory(thruster_force);
   |                         ----------------- ^^^^^^^^^^^^^^ expected
   |                         |        `NewtonSeconds`, found `PoundForceSeconds`
   |                         |
   |                         arguments to this function are incorrect
   |
note: function defined here
  --> src/main.rs:66:8
   |
66 | pub fn update_trajectory(force: NewtonSeconds) {
   |        ^^^^^^^^^^^^^^^^^ --------------------
help: call `Into::into` on this expression to convert `PoundForceSeconds` into
      `NewtonSeconds`
   |
76 |     let new_direction = update_trajectory(thruster_force.into());
   |                                                         +++++++

Como se describe en el punto 5, añadir una implementación del rasgo estándar From:

impl From<PoundForceSeconds> for NewtonSeconds {
    fn from(val: PoundForceSeconds) -> NewtonSeconds {
        NewtonSeconds(4.448222 * val.0)
    }
}

permite realizar la necesaria conversión de unidad y tipo con .into():

let thruster_force: PoundForceSeconds = thruster_impulse(direction);
let new_direction = update_trajectory(thruster_force.into());

La misma pauta de utilizar un nuevo tipo para marcar la semántica adicional de "unidad" de un tipo también puede ayudar a que los argumentos puramente booleanos sean menos ambiguos. Volviendo al ejemplo del punto 1, el uso de los tipos nuevos aclara el significado de los argumentos:

struct DoubleSided(pub bool);

struct ColorOutput(pub bool);

fn print_page(sides: DoubleSided, color: ColorOutput) {
    // ...
}
print_page(DoubleSided(true), ColorOutput(false));

Si la eficiencia de tamaño o la compatibilidad binaria son una preocupación, el atributo#[repr(transparent)] garantiza que un newtype tenga la misma representación en memoria que el tipo interno.

Ése es el uso simple de newtype, y es un ejemplo específico de la semántica de codificación del punto 1en el sistema de tipos, de modo que el compilador se encarga de vigilar esa semántica.

Eludir la regla de orfandad de los rasgos

El otro escenariocomún, pero más sutil, que requiere el patrón newtype gira en torno a la regla de orfandad de Rust. A grandes rasgos, dice que un crate puede implementar un rasgo para un tipo sólo si se cumple una de las siguientes condiciones:

  • La caja ha definido el rasgo

  • El cajón ha definido el tipo

Intentar implementar un rasgo ajeno para un tipo ajeno:

conduce a un error del compilador (que a su vez señala el camino de vuelta a newtypes):

error[E0117]: only traits defined in the current crate can be implemented for
              types defined outside of the crate
   --> src/main.rs:146:1
    |
146 | impl fmt::Display for rand::rngs::StdRng {
    | ^^^^^^^^^^^^^^^^^^^^^^------------------
    | |                     |
    | |                     `StdRng` is not defined in the current crate
    | impl doesn't use only types from inside the current crate
    |
    = note: define and implement a trait or new type instead

La razón de esta restricción se debe al riesgo de ambigüedad: si dos cajas diferentes en el gráfico de dependencias(punto 25) fueran ambas a (digamos) impl std::fmt::Display for rand::rngs::StdRng, entonces el compilador/enlazador no tiene forma de elegir entre ellas.

Con frecuencia, esto puede llevar a la frustración: por ejemplo, si intentas serializar datos que incluyen un tipo de otro crate, la regla de orfandad te impide escribir impl serde::Serialize for somecrate::SomeType.18

Pero el patrón newtype significa que estás definiendo un nuevo tipo, que forma parte de la caja actual, por lo que se aplica la segunda parte de la regla del rasgo huérfano. Ahora es posible implementar un rasgo ajeno:

struct MyRng(rand::rngs::StdRng);

impl fmt::Display for MyRng {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        write!(f, "<MyRng instance>")
    }
}

Limitaciones de los nuevos tipos

El patrón newtype resuelve estas dos clases de problemas -evita las conversiones de unidades y elude la regla de orfandad-, pero conlleva cierta incomodidad: toda operación que implique al newtype debe reenviarse al tipo interno.

A nivel trivial, eso significa que el código tiene que utilizar thing.0 en todo el código, en lugar de sólo thing, pero eso es fácil, y el compilador te dirá dónde es necesario.

El inconveniente más importante es que se pierde cualquier implementación de rasgos en el tipo interno: como el newtype es un tipo nuevo, no se aplica la implementación interna existente.

En el caso de los rasgos derivables, esto sólo significa que la declaración newtype acaba con un montón de derives:

#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct NewType(InnerType);

Sin embargo, en el caso de rasgos más sofisticados, es necesario algún tipo de código de reenvío para recuperar la implementación del tipo interno, por ejemplo :

use std::fmt;
impl fmt::Display for NewType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        self.0.fmt(f)
    }
}

Tema 7: Utilizar constructores para tipos complejos

Este Artículo describe el patrón constructor, en el que las estructuras de datos complejas tienen un tipo constructor asociado que facilita a los usuarios la creación de instancias de laestructura de datos.

Rust insiste en que todos los campos de un struct deben rellenarse cuando se crea una nueva instancia de ese struct. Esto mantiene el código seguro, garantizando que nunca haya valores no inicializados, pero da lugar a un código repetitivo más verboso de lo que sería ideal.

Por ejemplo, cualquier campo opcional tiene que estar explícitamente marcado como ausente con None:

/// Phone number in E164 format.
#[derive(Debug, Clone)]
pub struct PhoneNumberE164(pub String);

#[derive(Debug, Default)]
pub struct Details {
    pub given_name: String,
    pub preferred_name: Option<String>,
    pub middle_name: Option<String>,
    pub family_name: String,
    pub mobile_phone: Option<PhoneNumberE164>,
}

// ...

let dizzy = Details {
    given_name: "Dizzy".to_owned(),
    preferred_name: None,
    middle_name: None,
    family_name: "Mixer".to_owned(),
    mobile_phone: None,
};

Este código repetitivo también es frágil, en el sentido de que un cambio futuro que añada un nuevo campo a la struct requiere una actualización en cada lugar que construye la estructura.

La repetición puede reducirse significativamente implementando y utilizando el rasgoDefault como se describe enel punto 10:

let dizzy = Details {
    given_name: "Dizzy".to_owned(),
    family_name: "Mixer".to_owned(),
    ..Default::default()
};

Utilizar Default también ayuda a reducir los cambios necesarios cuando se añade un nuevo campo, siempre que el nuevo campo sea a su vez de un tipo que implemente Default.

Se trata de un problema más general: la implementación derivada automática de Default sólo funciona si todos los tipos de campo implementan el rasgo Default. Si hay un campo que no sigue el juego, el paso derive no funciona:

error[E0277]: the trait bound `Date: Default` is not satisfied
  --> src/main.rs:48:9
   |
41 |     #[derive(Debug, Default)]
   |                     ------- in this derive macro expansion
...
48 |         pub date_of_birth: time::Date,
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Default` is not
   |                                       implemented for `Date`
   |
   = note: this error originates in the derive macro `Default`

El código no puede implementar Default para chrono::Utc debido a la regla de orfandad; pero aunque pudiera, no sería útil: utilizar un valor por defecto para la fecha de nacimiento va a ser incorrecto casi siempre.

La ausencia de Default significa que todos los campos deben rellenarse manualmente:

let bob = Details {
    given_name: "Robert".to_owned(),
    preferred_name: Some("Bob".to_owned()),
    middle_name: Some("the".to_owned()),
    family_name: "Builder".to_owned(),
    mobile_phone: None,
    date_of_birth: time::Date::from_calendar_date(
        1998,
        time::Month::November,
        28,
    )
    .unwrap(),
    last_seen: None,
};

Esta ergonomía puede mejorarse si implementas el patrón constructor para estructuras de datos complejas.

La variante más sencilla del patrón constructor es un struct independiente que contiene la información necesaria para construir el elemento. Para simplificar, el ejemplo contendrá una instancia del propio elemento:

pub struct DetailsBuilder(Details);

impl DetailsBuilder {
    /// Start building a new [`Details`] object.
    pub fn new(
        given_name: &str,
        family_name: &str,
        date_of_birth: time::Date,
    ) -> Self {
        DetailsBuilder(Details {
            given_name: given_name.to_owned(),
            preferred_name: None,
            middle_name: None,
            family_name: family_name.to_owned(),
            mobile_phone: None,
            date_of_birth,
            last_seen: None,
        })
    }
}

El tipo constructor puede entonces equiparse con métodos ayudantes que rellenen los campos del elemento naciente. Cada uno de estos métodos consume self pero emite un nuevo Self, lo que permite encadenar distintos métodos de construcción:

/// Set the preferred name.
pub fn preferred_name(mut self, preferred_name: &str) -> Self {
    self.0.preferred_name = Some(preferred_name.to_owned());
    self
}

/// Set the middle name.
pub fn middle_name(mut self, middle_name: &str) -> Self {
    self.0.middle_name = Some(middle_name.to_owned());
    self
}

Estos métodos de ayuda pueden ser más útiles que los simples definidores:

/// Update the `last_seen` field to the current date/time.
pub fn just_seen(mut self) -> Self {
    self.0.last_seen = Some(time::OffsetDateTime::now_utc());
    self
}

El método final que se invoca para el constructor consume el constructor y emite el elemento construido:

/// Consume the builder object and return a fully built [`Details`]
/// object.
pub fn build(self) -> Details {
    self.0
}

En general, esto permite a los clientes del constructor tener unaexperiencia de construcción más ergonómica:

let also_bob = DetailsBuilder::new(
    "Robert",
    "Builder",
    time::Date::from_calendar_date(1998, time::Month::November, 28)
        .unwrap(),
)
.middle_name("the")
.preferred_name("Bob")
.just_seen()
.build();

La naturaleza omnipresente de este estilo de constructor conlleva un par de inconvenientes. La primera es que separar las fases del proceso de construcción no puede hacerse por sí solo:

error[E0382]: use of moved value: `builder`
   --> src/main.rs:256:15
    |
247 |     let builder = DetailsBuilder::new(
    |         ------- move occurs because `builder` has type `DetailsBuilder`,
    |                 which does not implement the `Copy` trait
...
254 |         builder.preferred_name("Bob");
    |                 --------------------- `builder` moved due to this method
    |                                       call
255 |     }
256 |     let bob = builder.build();
    |               ^^^^^^^ value used here after move
    |
note: `DetailsBuilder::preferred_name` takes ownership of the receiver `self`,
      which moves `builder`
   --> src/main.rs:60:35
    |
27  |     pub fn preferred_name(mut self, preferred_name: &str) -> Self {
    |                               ^^^^

Esto puede solucionarse asignando de nuevo el constructor consumido a la mismavariable:

let mut builder = DetailsBuilder::new(
    "Robert",
    "Builder",
    time::Date::from_calendar_date(1998, time::Month::November, 28)
        .unwrap(),
);
if informal {
    builder = builder.preferred_name("Bob");
}
let bob = builder.build();

El otro inconveniente de la naturaleza omnipresente de este constructor es que sólo se puede construir un elemento; intentar crear varias instancias llamando repetidamente a build() en el mismo constructor es una falta del compilador, como cabría esperar:

error[E0382]: use of moved value: `smithy`
   --> src/main.rs:159:39
    |
154 |   let smithy = DetailsBuilder::new(
    |       ------ move occurs because `smithy` has type `base::DetailsBuilder`,
    |              which does not implement the `Copy` trait
...
159 |   let clones = vec![smithy.build(), smithy.build(), smithy.build()];
    |                            -------  ^^^^^^ value used here after move
    |                            |
    |                            `smithy` moved due to this method call

Un enfoque alternativo es que los métodos del constructor tomen un &mut self y emitan un &mut Self:

/// Update the `last_seen` field to the current date/time.
pub fn just_seen(&mut self) -> &mut Self {
    self.0.last_seen = Some(time::OffsetDateTime::now_utc());
    self
}

Esto elimina la necesidad de autoasignación en etapas de construcción separadas:

let mut builder = DetailsBuilder::new(
    "Robert",
    "Builder",
    time::Date::from_calendar_date(1998, time::Month::November, 28)
        .unwrap(),
);
if informal {
    builder.preferred_name("Bob"); // no `builder = ...`
}
let bob = builder.build();

Sin embargo, esta versión hace imposible encadenar la construcción del constructor junto con la invocación de sus métodos setter:

error[E0716]: temporary value dropped while borrowed
   --> src/main.rs:265:19
    |
265 |       let builder = DetailsBuilder::new(
    |  ___________________^
266 | |         "Robert",
267 | |         "Builder",
268 | |         time::Date::from_calendar_date(1998, time::Month::November, 28)
269 | |             .unwrap(),
270 | |     )
    | |_____^ creates a temporary value which is freed while still in use
271 |       .middle_name("the")
272 |       .just_seen();
    |                   - temporary value is freed at the end of this statement
273 |       let bob = builder.build();
    |                 --------------- borrow later used here
    |
    = note: consider using a `let` binding to create a longer lived value

letComo indica el error del compilador , puedes solucionarlo haciendo que el elemento constructor tenga un nombre:

let mut builder = DetailsBuilder::new(
    "Robert",
    "Builder",
    time::Date::from_calendar_date(1998, time::Month::November, 28)
        .unwrap(),
);
builder.middle_name("the").just_seen();
if informal {
    builder.preferred_name("Bob");
}
let bob = builder.build();

Esta variante de constructor mutante también permite construir varios elementos. La firma del método build() no debe consumir self, por lo que debe ser como sigue:

/// Construct a fully built [`Details`] object.
pub fn build(&self) -> Details {
    // ...
}

La implementación de este método build() repetible tiene entonces que construir un elemento nuevo en cada invocación. Si el elemento subyacente implementa Clone, esto es fácil: el constructor puede mantener una plantilla y clone() para cada construcción. Si el elemento subyacente no implementa Clone, entonces el constructor necesita tener suficiente estado para poder construir manualmente una instancia del elemento subyacente en cada llamada a build().

Con cualquier estilo de patrón constructor, el código repetitivo se limita ahora a un lugar -el constructor- en lugar de ser necesario en cada lugar que utilice el tipo subyacente.

El boilerplate que queda puede reducirse potencialmente aún más mediante el uso de una macro(punto 28), pero si sigues este camino, también debes comprobar si hay una crate existente (como la crate derive_builder en particular) que proporcione lo que necesitas, suponiendo que estés dispuesto a depender de ella en (punto 25).

Tema 8: Familiarízate conlos tipos de referenciay puntero

Para la programación en general, una referencia es una forma de acceder indirectamente a alguna estructura de datos, independientemente de la variable que posea esa estructura de datos. En la práctica, suele implementarse como un puntero: un número cuyo valor es la dirección en memoria de la estructura de datos.

Una CPU moderna suele imponer algunas restricciones a los punteros: la dirección de memoria debe estar en un intervalo válido de memoria (ya sea virtual o física) y puede ser necesario que esté alineada (por ejemplo, un valor entero de 4 bytes puede ser accesible sólo si su dirección es múltiplo de 4).

Sin embargo, los lenguajes de programación de alto nivel suelen codificar más información sobre los punteros en sus sistemas de tipos. En los lenguajes derivados de C, incluido Rust, los punteros tienen un tipo que indica qué tipo de estructura de datos se espera que esté presente en la dirección de memoria apuntada. Esto permite al código interpretar el contenido de la memoria en esa dirección y en la memoria siguiente a esa dirección.

Este nivel básico de información sobre punteros -localización de memoria y disposición prevista de la estructura de datos- se representa en Rust como un puntero sin procesar. Sin embargo, el código Rust seguro no utiliza punteros en bruto, porque Rust proporciona tipos de referencia y puntero más ricos que proporcionan garantías y restricciones de seguridad adicionales. Estos tipos de referencia y puntero son el tema de este Tema; los punteros en bruto quedan relegados al Tema 16 (que trata del código unsafe ).

Referencias de óxido

El tipo tipo puntero más ubicuo en Rust es la referencia, con un tipo que se escribe como &T para algún tipoT. Aunque se trata de un valor de puntero encubierto, el compilador se asegura de que se respeten varias reglas en torno a su uso: siempre debe apuntar a una instancia válida y correctamente alineada del tipo correspondiente T, cuyo tiempo de vida(Punto 14) se extiende más allá de su uso, y debe satisfacer las reglas de comprobación de préstamo(Punto 15). Estas restricciones adicionales siempre están implícitas en el término referencia en Rust, por lo que el término puntero desnudo suele ser poco frecuente.

La restricción de que una referencia Rust debe apuntar a un elemento válido y correctamente alineado es compartida por los tipos de referencia de C++. Sin embargo, C++ no tiene el concepto de tiempo de vida y, por tanto, permite estribos con referencias colgantes:19

Las comprobaciones de préstamo y tiempo de vida de Rust significan que el código equivalente ni siquiera compila:

error[E0515]: cannot return reference to local variable `x`
   --> src/main.rs:477:5
    |
477 |     &x
    |     ^^ returns a reference to data owned by the current function

Una referencia Rust &T permite el acceso de sólo lectura al elemento subyacente (aproximadamente equivalente a const T&de C++ ). Una referencia mutable que también permite modificar el elemento subyacente se escribe como &mut T y también está sujetaa las reglas de comprobación de préstamos comentadas en el Tema 15. Este patrón de nomenclatura refleja una mentalidad ligeramente diferente entre Rust y C++:

  • En Rust, la variante por defecto es de sólo lectura, y los tipos escribibles se marcan especialmente (con mut).

  • En C++, la variante por defecto es escribible, y los tipos de sólo lectura se marcan especialmente (con const).

El compilador convierte el código Rust que utiliza referencias en código máquina que utiliza punteros simples, que tienen un tamaño de ocho bytes en una plataforma de 64 bits (que este Artículo asume en todo momento). Por ejemplo, un par de variables locales junto con referencias a ellas:

pub struct Point {
    pub x: u32,
    pub y: u32,
}

let pt = Point { x: 1, y: 2 };
let x = 0u64;
let ref_x = &x;
let ref_pt = &pt;

pueden acabar dispuestos en la pila como se muestra en la Figura 1-2.

Representation of a stack with 4 entries, each shown as a rectangle representing 8 bytes. Starting from the bottom, the first entry is labelled pt, and the 8 bytes it represents is split into two 4-byte values, 1 and 2. Above that, the next entry is labelled x and holds the value 0.  Above that is an entry labelled ref_x, whose contents are just an arrow that points to the x entry below it on the stack. At the top is a ref_pt entry, whose contents are an arrow that points to the pt entry at the bottom of the stack.
Figura 1-2. Disposición de la pila con punteros a variables locales

Una referencia Rust puede referirse a elementos que se encuentran en la pila o en el montón.Rust asigna elementos en la pila por defecto, pero el tipo de puntero Box<T> (aproximadamente equivalente al std::unique_ptr<T> de C++) obliga a que la asignación se produzca en el montón, lo que a su vez significa que el elemento asignado puede superar el alcance del bloque actual. Bajo la cubierta, Box<T> es también un simple valor de puntero de ocho bytes:

    let box_pt = Box::new(Point { x: 10, y: 20 });

Esto se representa en la Figura 1-3.

The figure shows a representation of the stack on the left, with a single entry labelled box_pt. The contents of this entry is the start of an arrow that points to a rectangle on the right hand side, inside a cloud labelled 'Heap'. The rectangle on the right hand side is split into two 4-byte components, holding the values 10 and 20.
Figura 1-3. Pila Box puntero a struct en el montón

Rasgos del puntero

Un método que espera un argumento de referencia como &Point también puede ser alimentado con un &Box<Point>:

fn show(pt: &Point) {
    println!("({}, {})", pt.x, pt.y);
}
show(ref_pt);
show(&box_pt);
(1, 2)
(10, 20)

Esto es posible porque Box<T> implementa el Deref con Target = T. Una implementación de este rasgo para algún tipo significa que el métododeref() puede utilizarse para crear una referencia al tipo Target. También existe un DerefMut que emite una referencia mutable al tipo Target.

Los rasgos Deref/DerefMut son algo especiales, porque el compilador de Rust tiene un comportamiento específico cuando trata con tipos que los implementan. Cuando el compilador encuentra una expresión de desreferencia (por ejemplo, *x), busca y utiliza una implementación de uno de estos rasgos, dependiendo de si la desreferencia requiere acceso mutable o no.Esta coerción deDeref permite que varios tipos de punteros inteligentes se comporten como referencias normales y es uno de los pocos mecanismos que permiten la conversión implícita de tipos en Rust (como se describe en el punto 5).

Como apunte técnico, merece la pena entender por qué los rasgos Deref no pueden ser genéricos (Deref<Target>) para el tipo de destino. Si lo fueran, entonces sería posible que algún tipo ConfusedPtr implementara tanto Deref<TypeA>como Deref<TypeB>, y eso dejaría al compilador incapaz de deducir un único tipo único para una expresión como *x. Así que, en su lugar, el tipo de destino se codifica como el tipo asociado denominadoTarget.

Este inciso técnico contrasta con otros dos rasgos estándar de los punteros, el AsRef y AsMut . Estos rasgos no inducen un comportamiento especial en el compilador, sino que permiten conversiones a una referencia o a una referencia mutable mediante una llamada explícita a sus funciones de rasgo (as_ref() yas_mut()respectivamente). El tipo de destino de estas conversiones se codifica como un parámetro de tipo (por ejemplo, AsRef<Point>), lo que significa que un único tipo de contenedor puede admitir varios destinos.

Por ejemplo, el tipo String implementa el rasgoDeref con Target = str, lo que significa que una expresión como &my_string puede coaccionarse al tipo &str. Pero también implementa lo siguiente:

  • AsRef<[u8]>permitiendo la conversión a una porción de byte &[u8]

  • AsRef<OsStr>permitiendo la conversión a una cadena OS

  • AsRef<Path>permitiendo la conversión a una ruta del sistema de archivos

  • AsRef<str>, permitiendo la conversión a una rebanada de cadena &str (como con Deref)

Tipos de puntero gordo

Rust tiene dos tipos de punteros gordos incorporados: las rebanadas y los objetos trait. Son tipos que actúan como punteros pero contienen información adicional sobre aquello a lo que apuntan.

Rebanadas

El primer tipo de puntero gordo es la rebanada: una referencia a un subconjunto de alguna colección contigua de valores. Se construye a partir de un puntero simple (no propietario), junto con un campo de longitud, lo que hace que tenga el doble de tamaño que un puntero simple (16 bytes en una plataforma de 64 bits). El tipo de una porción se escribe como &[T]-una referencia a [T], que es el tipo nocional de una colección contigua de valores de tipo T.

El tipo nocional [T] no puede instanciarse, pero hay dos contenedores comunes que lo encarnan. El primero es la matriz: una colección contigua de valores con un tamaño conocido en el momento de la compilación: una matriz con cinco valores siempre tendrá cinco valores. Por tanto, una porción puede referirse a un subconjunto de una matriz (como se muestra en la Figura 1-4):

let array: [u64; 5] = [0, 1, 2, 3, 4];
let slice = &array[1..3];
Representation of a stack holding seven 8-byte quantities, divided into two groups.  The bottom group is labelled array and covers the top 5 entries in the stack, which hold the values 0 to 4.  The top group is labelled slice and covers the bottom 2 entries in the stack. Of these 2 entries, the top one holds an arrow that points to the second element in the array chunk, counting from the bottom; the bottom entry holds a value labelled len=2.
Figura 1-4. Rebanada de pila apuntando a una matriz de pila

El otro contenedor común de valores contiguos es un Vec<T>. Contiene una colección contigua de valores como una matriz, pero a diferencia de ésta, el número de valores en Vec puede crecer (por ejemplo, con push(value)) o reducirse (por ejemplo, con pop()).

El contenido del Vec se guarda en el montón (lo que permite esta variación de tamaño), pero siempre es contiguo, por lo que una rebanada puede referirse a un subconjunto de un vector, como se muestra en la Figura 1-5:

let mut vector = Vec::<u64>::with_capacity(8);
for i in 0..5 {
    vector.push(i);
}
let vslice = &vector[1..3];
The diagram shows a stack on the left, and a heap on the right, both arranged as vertically stacked rectangles where each rectangle represents an 8-byte quantity.  The heap on the right has 8 entries within it: from bottom to top the first 5 contain values from 0 to 4 consecutively; the top 3 entries are all labelled (uninit).  The stack holds five 8-byte quantities, divided into two groups.  The top group is labelled slice and holds two entries.  Of these 2 entries, the top one holds an arrow that points to the second element in the heap chunk, counting from the bottom; the bottom entry holds a value labelled len=2. The bottom group of the stack is labelled vec and hols three entries. The top entry holds an arrow that points to the bottom element of the heap chunk; the middle entry has a value capacity=8; the bottom entry has a value len=5.
Figura 1-5. Corte de pila apuntando al contenido de Vec en el montón

La expresión &vector[1..3] encierra muchas cosas, así que vale la pena desglosarla en sus componentes:

  • La parte 1..3 es una expresión de rango; el compilador la convierte en una instancia del tipo Range<usize> que contiene un límite inferior inclusivo y un límite superior exclusivo.

  • El tipo Rangeimplementael rasgo SliceIndex<T> trait, que describe operaciones de indexación en trozos de un tipo arbitrario T (por lo que el tipo Output es [T]).

  • La parte vector[ ] es una expresión de indexación; el compilador convierte en una invocación a la función Index método index en vector, junto con una desreferencia (es decir, *vector.index( )).20

  • vector[1..3] por tanto, invoca laimplementación de Vec<T>de Index<I>, que requiere que I sea una instancia de SliceIndex<[u64]>. Esto funciona porque Range<usize> implementaSliceIndex<[T]> para cualquier T, incluido u64.

  • &vector[1..3] deshace la desreferencia, dando como resultado un tipo de expresión final de &[u64].

Objetos rasgo

El segundo tipo incorporado de puntero gordo es un objeto trait: una referencia a algún elemento que implementa un trait concreto. Se construye a partir de un puntero simple al elemento, junto con un puntero interno a la tabla virtual del tipo, lo que da un tamaño de 16 bytes (en una plataforma de 64 bits). La tabla v de la implementación de un rasgo en un tipo contiene punteros a funciones para cada una de las implementaciones de métodos, lo que permite el envío dinámico en tiempo de ejecución(elemento 12).21

Así que un rasgo sencillo:

trait Calculate {
    fn add(&self, l: u64, r: u64) -> u64;
    fn mul(&self, l: u64, r: u64) -> u64;
}

con un struct que lo implemente:

struct Modulo(pub u64);

impl Calculate for Modulo {
    fn add(&self, l: u64, r: u64) -> u64 {
        (l + r) % self.0
    }
    fn mul(&self, l: u64, r: u64) -> u64 {
        (l * r) % self.0
    }
}

let mod3 = Modulo(3);

puede convertirse en un objeto trait de tipo &dyn Trait. La palabra clavedyn resalta el hecho de que se trata de un envío dinámico:

// Need an explicit type to force dynamic dispatch.
let tobj: &dyn Calculate = &mod3;
let result = tobj.add(2, 2);
assert_eq!(result, 1);

La disposición de la memoria equivalente a se muestra en la Figura 1-6.

The diagram shows a stack layout on the left, with a single entry labelled mod3 with value 3 at the top, and below that a pair of entries jointly labelled tobj.  The top entry in tobj holds an arrow that points to the mod3 entry on the stack; the bottom entry in tobj points to a composite rectangle on the right hand side of the diagram labelled Calculate for Modulo vtable.  This representable of a vtable contains two entries, labelled add and mul. The first of these holds an arrow that leads to a box representing the code of Modulo::add(); the second holds an arrow that leads to a box representing the code of Modulo::mul().
Figura 1-6. Objeto trait con punteros a elemento concreto y vtable

El código que contiene un objeto trait puede invocar los métodos del trait a través de los punteros de función de la vtable, pasando el puntero del elemento como parámetro &self; consulta el Tema 12 para obtener más información y consejos.

Más rasgos de puntero

"Rasgos depuntero" describe dos pares de rasgos (Deref/DerefMut, AsRef/AsMut) que se utilizan cuando se trabaja con tipos que pueden convertirse fácilmente en referencias. Hay algunos rasgos estándar más que también pueden entrar en juego al trabajar con tipos tipo puntero, ya sean de la biblioteca estándar o definidos por el usuario.

El más sencillo de ellos es el Pointer que formatea un valor de puntero para la salida. Esto puede ser útil para la depuración de bajo nivel, y el compilador buscará este trait automáticamente cuando encuentre el especificador de formato {:p}.

Más intrigantes son los Borrow y BorrowMut que tienen un único método (borrow yborrow_mutrespectivamente). Este métodotiene la misma firma que los métodos equivalentes de los rasgos AsRef/AsMut.

La diferencia clave en las intenciones entre estos rasgos es visible a través de las implementaciones globales que proporciona la biblioteca estándar. Dada una referencia Rust arbitraria &T, existe una implementación general tanto de AsRef como deBorrow; del mismo modo, para una referencia mutable &mut T, existe una implementación general tanto de AsMut como de BorrowMut.

Sin embargo, Borrow también tiene una implementación general para tipos (sin referencia): impl<T> Borrow<T> for T.

Esto significa que un método que acepte el rasgo Borrow puede tratar tanto casos de T como referencias aT:

fn add_four<T: std::borrow::Borrow<i32>>(v: T) -> i32 {
    v.borrow() + 4
}
assert_eq!(add_four(&2), 6);
assert_eq!(add_four(2), 6);

Los tipos contenedores de la biblioteca estándar tienen usos más realistas de Borrow. Por ejemplo,HashMap::get utiliza Borrow para permitir una recuperación cómoda de las entradas, tanto si están codificadas por valor como por referencia.

El rasgo ToOwned se basa en el rasgo Borrowy añade un método to_owned() que produce un nuevo elemento propio del tipo subyacente. Se trata de una generalización del rasgo Clone: donde Clonerequiere específicamente una referencia Rust &T, ToOwned se ocupa en cambio de cosas que implementan Borrow.

Esto ofrece un par de posibilidades para tratar de forma unificada tanto las referencias como los elementos movidos:

  • Una función que opera sobre referencias a algún tipo puede aceptar Borrow para que también pueda ser llamada con elementos desplazados además de referencias.

  • Una función que opere sobre elementos poseídos de algún tipo puede aceptar ToOwned, de modo que también puede ser llamada con referencias a elementos, así como con elementos movidos; cualquier referencia que se le pase se replicará en un elemento poseído localmente.

Aunque no es un tipo puntero, merece la pena mencionar el tipo Cow merece la pena mencionarlo en este punto, porque proporciona una forma alternativa de tratar el mismo tipo de situación. Cow es un enum que puede contener datos propios o una referencia a datos prestados. Su peculiar nombre significa "clone-on-write" (clonar al escribir): una entrada Cow puede permanecer como dato prestado hasta el momento en que haya que modificarlo, pero se convierte en una copia propia en el momento en que haya que alterar el dato.

Tipos de puntero inteligente

La biblioteca estándar de Rust incluye una variedad de tipos que actúan como punteros en un grado u otro, mediados por los rasgos de la biblioteca estándar descritos anteriormente. Cada uno de estos tipos de puntero inteligente viene con una semántica y unas garantías particulares, lo que tiene la ventaja de que la combinación adecuada de ellos puede proporcionar un control minucioso sobre el comportamiento del puntero, pero tiene el inconveniente de que los tipos resultantes pueden parecer abrumadores al principio (Rc<RefCell<Vec<T>>>, ¿alguien?).

El primer tipo de puntero inteligente es Rc<T>que es un puntero contado por referencia a un elemento (aproximadamente análogo al puntero C std::shared_ptr<T>). Implementa todos los rasgos relacionados con los punteros y, por tanto, actúa como Box<T> en muchos aspectos.

Esto es útil para estructuras de datos en las que se puede llegar al mismo elemento de distintas formas, pero elimina una de las reglas básicas de Rust sobre la propiedad: que cada elemento sólo tiene un propietario. Relajar esta regla significa que ahora es posible filtrar datos: si el elemento A tiene un puntero Rc al elemento B, y el elemento B tiene un puntero Rc a A, entonces el par nunca se abandonará.22 Dicho de otro modo: necesitas Rc para soportar estructuras de datos cíclicas, pero el inconveniente es que ahora hay ciclos en tus estructuras de datos.

El riesgo de fugas puede reducirse en algunos casos mediante el tipo relacionado Weak<T> que contiene una referencia no propietaria al elemento subyacente (más o menos análogo al tipo std::weak_ptr<T>). Mantener una referencia débil no impide que se elimine el elemento subyacente (cuando se eliminan todas las referencias fuertes), por lo que hacer uso del Weak<T>implica una actualización a un Rc<T>-que puede fallar.

Bajo el capó, Rc se implementa (actualmente) como un par de recuentos de referencia junto con el elemento referenciado, todo ello almacenado en el montón (como se muestra en la Figura 1-7):

use std::rc::Rc;
let rc1: Rc<u64> = Rc::new(42);
let rc2 = rc1.clone();
let wk = Rc::downgrade(&rc1);
The diagram shows a stack on the left and a heap on the right. The stack holds three entries, labelled rc1, rc2 and wk.  All three of these entries hold arrows that point to an object in the heap, however the arrow from the wk entry is dashed rather than solid.  The object on the heap is a composite rectangle holding three component values: an entry labelled strong=2, and entry labelled weak=1 and an entry labelled 42.
Figura 1-7. Rc y Weak punteros todos ellos referidos al mismo elemento del montón

El elemento subyacente se elimina cuando el recuento de referencias fuertes llega a cero, pero la estructura de contabilidad sólo se elimina cuando el recuento de referencias débiles también llega a cero.

Un Rc por sí solo te da la posibilidad de llegar a un elemento de distintas formas, pero cuando llegas a ese elemento, sólo puedes modificarlo (mediante get_mut) sólo si no hay otras formas de llegar al elemento, es decir, si no existen otras referencias Rc o Weak al mismo elemento. Esto es difícil de arreglar, por lo que Rc se combina a menudo con RefCell.

El siguiente tipo de puntero inteligente RefCell<T>relaja la regla(Tema 15) de que un elemento sólo puede ser mutado por su propietario o por el código que posee la (única) referencia mutable al elemento. Esta mutabilidad interior permite una mayor flexibilidad, por ejemplo, permitiendo implementaciones de rasgos que mutan elementos internos incluso cuando la firma del método sólo permite &self. Sin embargo, también conlleva costes: además de la sobrecarga de almacenamiento adicional (un isize adicional para realizar un seguimiento de los préstamos actuales, como se muestra en la Figura 1-8), las comprobaciones normales de los préstamos se trasladan del tiempo de compilación al tiempo de ejecución:

use std::cell::RefCell;
let rc: RefCell<u64> = RefCell::new(42);
let b1 = rc.borrow();
let b2 = rc.borrow();
The diagram shows a representation of a stack, with three entries in it, each containing two 8-byte values. The top entry is labelled rc, and holds the value borrow=2 above a value 42. The middle entry is labelled b1, and holds two values with arrows: the top arrow leads to the 42 value in rc, the bottom arrow leads to the rc entry as a whole. The bottom entry is labelled b2 and holds the same contents as b1: a top arrow to 42 and a bottom arrow to rc.
Figura 1-8. Ref hace referencia a un contenedor RefCell

La naturaleza en tiempo de ejecución de estas comprobaciones significa que el usuario de RefCell tiene que elegir entre dos opciones, ninguna agradable:

  • Acepta que pedir prestado es una operación de que puede fallar, y haz frente a Result valores de try_borrow[_mut]

  • Utiliza los métodos de préstamo supuestamente infalibles borrow[_mut], y acepta el riesgo de un panic! en tiempo de ejecución(Tema 18) si no se han cumplido las normas de préstamo

En cualquier caso, esta comprobación en tiempo de ejecución significa que la propia RefCell no implementa ninguno de los rasgos estándar de los punteros; en su lugar, sus operaciones de acceso devuelven un puntero Ref<T> o RefMut<T> que sí implementa esos rasgos.

Si el tipo subyacente T implementa el rasgo Copy (que indica que una copia rápida bit a bit produce un elemento válido; véase el punto 10), entonces el tipo Cell<T> permite la mutación interior con menos sobrecarga: el método get(&self) copia el valor actual y el método set(&self, val) copia un nuevo valor. El tipo Cell se utiliza internamente en las implementaciones de Rc yRefCell, para el seguimiento compartido de contadores que pueden mutarse sin &mut self.

Los tipos de punteros inteligentes descritos hasta ahora sólo son adecuados para su uso con un único hilo; sus implementaciones asumen que no hay acceso concurrente a sus partes internas. Si no es así, se necesitan punteros inteligentes que incluyan una sobrecarga de sincronización adicional.

El equivalente seguro para hilos de Rc<T> es Arc<T>que utiliza contadores atómicos para garantizar la exactitud del recuento de referencias. Al igual que Rc, Arc implementa todos los rasgos relacionados con los punteros.

Sin embargo, Arc por sí solo no permite ningún tipo de acceso mutable al elemento subyacente. Esto lo cubre el tipo Mutex que garantiza que sólo un hilo tengaacceso -mutableo inmutable- al elemento subyacente. Al igual que RefCell, Mutex no implementaningún rasgo de puntero, pero su operación lock() devuelve un valor de un tipo que sí lo hace: MutexGuard, que implementa Deref[Mut].

Si es probable que haya más lectores que escritores, es preferible el tipo RwLock es preferible, ya que permite que varios lectores accedan al elemento subyacente en paralelo, siempre que no haya en ese momento un (único) escritor.

En cualquier caso, las reglas de préstamo e hilado de Rust obligan a utilizar uno de estos contenedores de sincronización en el código multihilo (pero esto sólo protege contra algunos de los problemas de la concurrencia de estado compartido; véase el Tema 17).

La misma estrategia -ver qué rechaza el compilador y qué sugiere en su lugar- puede aplicarse a veces con los otros tipos de punteros inteligentes. Sin embargo, es más rápido y menos frustrante entender lo que implica el comportamiento de los distintos punteros inteligentes. Tomando prestado (juego de palabras) un ejemplo de la primera edición del libro de Rust:

  • Rc<RefCell<Vec<T>>> tiene un vector (Vec) con propiedad compartida (Rc), en el que el vector puede mutar, pero sólo como vector completo.

  • Rc<Vec<RefCell<T>>> también contiene un vector con propiedad compartida, pero aquí cada entrada individual del vector puede mutar independientemente de las demás.

Los tipos implicados describen con precisión estos comportamientos.

Tema 9: Considera la posibilidad de utilizar transformaciones de iteradoresen lugar de bucles explícitos

El humilde bucle ha tenido un largo recorrido de creciente comodidad y creciente abstracción. El lenguaje B (precursor de C) sólo tenía while (condition) { ... }, pero con la llegada de C, el escenario habitual de iterar a través de los índices de una matriz se hizo más cómodo con la adición del bucle for:

// C code
int i;
for (i = 0; i < len; i++) {
  Item item = collection[i];
  // body
}

Las primeras versiones de C++ mejoraron aún más la comodidad y el alcance al permitir que la declaración de la variable de bucle seincrustara en la declaración for (esto también lo adoptó C en C99):

// C++98 code
for (int i = 0; i < len; i++) {
  Item item = collection[i];
  // ...
}

La mayoría de los lenguajes modernos abstraen aún más la idea del bucle: la función principal de un bucle suele ser pasar al siguiente elemento de algún contenedor. El seguimiento de la logística necesaria para llegar a ese elemento (index++ o ++it) es, en la mayoría de los casos, un detalle irrelevante. Esta realización produjo dos conceptos centrales:

Iteradores

Un tipo cuya finalidad es emitir repetidamente el siguiente elemento de un contenedor, hasta que se agote23

Bucles For-each

Una expresión de bucle compacta para iterar sobre todos los elementos de un contenedor, vinculando una variable de bucle al elemento en lugar de a los detalles para llegar a ese elemento.

Estos conceptos permiten que el código del bucle sea más corto y (lo que es más importante) más claro sobre lo que se pretende:

// C++11 code
for (Item& item : collection) {
  // ...
}

Una vez que estos conceptos estuvieron disponibles, eran tan evidentemente potentes que se adaptaron rápidamente a los lenguajes que aún no los tenían (por ejemplo, los bucles for-each se añadieron a Java 1.5 y C++11).

Rust incluye iteradores y bucles de tipo "para cada uno", pero también incluye el siguiente paso en la abstracción: permitir que todo el bucle se exprese como una transformación de iterador (a veces también denominada adaptador de iterador). Al igual que en el Tema 3sobre Option y Result, este Tema intentará mostrar cómo pueden utilizarse estas transformaciones de iterador en lugar de bucles explícitos, y dará orientaciones sobre cuándo es una buena idea. En concreto, las transformaciones de iterador pueden ser más eficientes que un bucle explícito, porque el compilador puede saltarse las comprobaciones de límites que de otro modo tendría que realizar.

Al final de este Tema, un bucle explícito tipo C para sumar los cuadrados de los cinco primeros elementos pares de un vector:

let values: Vec<u64> = vec![1, 1, 2, 3, 5 /* ... */];

let mut even_sum_squares = 0;
let mut even_count = 0;
for i in 0..values.len() {
    if values[i] % 2 != 0 {
        continue;
    }
    even_sum_squares += values[i] * values[i];
    even_count += 1;
    if even_count == 5 {
        break;
    }
}

debería empezar a sentirse más natural expresado como una expresión de estilo funcional:

let even_sum_squares: u64 = values
    .iter()
    .filter(|x| *x % 2 == 0)
    .take(5)
    .map(|x| x * x)
    .sum();

Las expresiones de transformación de un iterador como esta pueden dividirse aproximadamente en tres partes:

  • Un iterador fuente inicial, a partir de una instancia de un tipo que implemente uno de los rasgos de iterador de Rust

  • Una secuencia de transformaciones de iteradores

  • Un método consumidor final para combinar los resultados de la iteración en un valor final

Las dos primeras partes desplazan la funcionalidad del cuerpo del bucle a la expresión for; la última elimina por completo la necesidad de la declaración for.

Rasgos del iterador

El núcleo Iterator tiene una interfaz muy sencilla: un único método next que emite elementos Somehasta que no lo hace (None). El tipo de los elementos emitidos viene dado por el tipo Itemasociado al rasgo.

Las colecciones que permiten iterar sobre su contenido -lo que en otros lenguajes se llamaría iterables- implementan el rasgo IntoIterator el método into_iter de este rasgo consume Self y emite en su lugar un Iterator. El compilador utilizará automáticamente este rasgo para expresiones de la forma

for item in collection {
    // body
}

convirtiéndolos efectivamente en un código más o menos así:

let mut iter = collection.into_iter();
loop {
    let item: Thing = match iter.next() {
        Some(item) => item,
        None => break,
    };
    // body
}

o de forma más sucinta e idiomática:

let mut iter = collection.into_iter();
while let Some(item) = iter.next() {
    // body
}

Para que todo vaya sobre ruedas, también hay una implementación de IntoIterator para cualquier Iterator, que sólo devuelve self; después de todo, ¡es fácil convertir un Iterator en un Iterator!

Esta forma inicial es un iterador que consume , consumiendo la colección a medida que se crea:

let collection = vec![Thing(0), Thing(1), Thing(2), Thing(3)];
for item in collection {
    println!("Consumed item {item:?}");
}

Cualquier intento de utilizar la colección después de haber iterado sobre ella falla:

println!("Collection = {collection:?}");
error[E0382]: borrow of moved value: `collection`
   --> src/main.rs:171:28
    |
163 |   let collection = vec![Thing(0), Thing(1), Thing(2), Thing(3)];
    |       ---------- move occurs because `collection` has type `Vec<Thing>`,
    |                  which does not implement the `Copy` trait
164 |   for item in collection {
    |               ---------- `collection` moved due to this implicit call to
    |                           `.into_iter()`
...
171 |   println!("Collection = {collection:?}");
    |                          ^^^^^^^^^^^^^^ value borrowed here after move
    |
note: `into_iter` takes ownership of the receiver `self`, which moves
      `collection`

Aunque es sencillo de entender, este comportamiento que todo lo consume suele ser indeseable; se necesita algún tipo de préstamo de los elementos iterados.

Para garantizar que el comportamiento sea claro, en los ejemplos se utiliza un tipo Thing que no implementa Copy (punto 10), ya que eso ocultaría cuestiones de propiedad(punto 15): el compilador haría copias silenciosamente en todas partes:

// Deliberately not `Copy`
#[derive(Clone, Debug, Eq, PartialEq)]
struct Thing(u64);

let collection = vec![Thing(0), Thing(1), Thing(2), Thing(3)];

Si la colección sobre la que se está iterando lleva el prefijo &:

for item in &collection {
    println!("{}", item.0);
}
println!("collection still around {collection:?}");

el compilador de Rust buscará una implementación deIntoIterator para el tipo &Collection. Los tipos de colección correctamente diseñados proporcionarán dicha implementación; esta implementación seguirá consumiendo Self, pero ahora Self es &Collection en lugar de Collection, y el tipo Item asociado será una referencia &Thing.

Esto deja la colección intacta después de la iteración, y el código expandido equivalente es el siguiente:

let mut iter = (&collection).into_iter();
while let Some(item) = iter.next() {
    println!("{}", item.0);
}

Si tiene sentido proporcionar iteración sobre referencias mutables,24 entonces se aplica un patrón similar para for item in &mut collection: el compilador busca y utiliza una implementación de IntoIterator para &mut Collection, siendo cada Item de tipo &mut Thing.

Por convención, los contenedores estándar también proporcionan un método iter() que devuelve un iterador sobre referencias al elemento subyacente, y un método iter_mut() equivalente, si procede, con el mismo comportamiento que el que acabamos de describir. Estos métodos pueden utilizarse en bucles for, pero tienen un beneficio más evidente cuando se utilizan como inicio de una transformación de iterador:

let result: u64 = (&collection).into_iter().map(|thing| thing.0).sum();

se convierte:

let result: u64 = collection.iter().map(|thing| thing.0).sum();

Iterador Transformaciones

El rasgo Iterator tiene un único método obligatorio (next), pero también proporciona implementaciones por defecto(Elemento 13) de un gran número de otros métodos que realizan transformaciones en un iterador.

Algunas de estas transformaciones afectan al proceso global de iteración de:

take(n)

Restringe un iterador a emitir como máximo n elementos.

skip(n)

Salta los primeros nelementos del iterador.

step_by(n)

Convierte un iterador para que sólo emita cada enésimo elemento.

chain(other)

Pega dos iteradores, para construir un iterador combinado que se mueva por uno y luego por el otro.

cycle()

Convierte un iterador que termina en uno que se repite para siempre, volviendo a empezar desde el principio cada vez que llega al final. (El iterador debe soportar Clone para permitir esto).

rev()

Invierte la dirección de un iterador. (El iterador debe implementar el Double​En⁠ded​Iterator que tiene un método adicional next_back adicional).

Otras transformaciones afectan a la naturaleza del Item que es el sujeto del Iterator:

map(|item| {...})

Aplica repetidamente un cierre para transformar cada elemento sucesivamente. Ésta es la versión más general; varias de las siguientes entradas de esta lista son variantes de conveniencia que podrían implementarse de forma equivalente como map.

cloned()

Produce un clon de todos los elementos del iterador original; este es especialmente útil con iteradores sobre referencias &Item. (Obviamente, esto requiere que el tipo Item subyacente implemente Clone.)

copied()

Produce una copia de todos los elementos del iterador original; esto es especialmente útil con iteradores sobre referencias &Item. (Obviamente, esto requiere que el tipo Item subyacente implemente Copy, pero es probable que sea más rápido que cloned(), si ese es el caso).

enumerate()

Convierte un iterador sobre elementos en un iterador sobre pares (usize, Item), proporcionando un índice a los elementos del iterador.

zip(it)

Une un iterador con un segundo iterador, para producir un iterador combinado que emita pares de elementos, uno de cada uno de los iteradores originales, hasta que termine el más corto de los dos iteradores.

Sin embargo, otras transformaciones realizan un filtrado de los Items emitidos por elIterator:

filter(|item| {...})

Aplica un cierre bool-returning a cada referencia de elemento para determinar si debe pasarse por él.

take_while()

Emite un subrango inicial del iterador, basado en un predicado. Imagen especular de skip_while.

skip_while()

Emite un subrango final del iterador, basado en un predicado. Imagen especular de take_while.

El método flatten() trata con un iterador cuyos elementos son a su vez iteradores, aplanando el resultado. Por sí solo, esto no parece muy útil, pero lo es mucho más cuando se combina con la observación de que tanto Option y Result actúan como iteradores: producen cero (para None, Err(e)) o uno (para Some(v), Ok(v)) elementos. Esto significa que flattening un flujo de valoresOption/Result es una forma sencilla de extraer sólo los valores válidos, ignorando el resto.

En conjunto, estos métodos permiten transformar los iteradores para que produzcan exactamente la secuencia de elementos que se necesita en la mayoría de las situaciones.

Iteradores Consumidores

Las dos secciones anteriores describían cómo obtener un iterador y cómo transformarlo en la forma exacta para una iteración precisa. Esta iteración precisa podría producirse como un bucle for-each explícito:

let mut even_sum_squares = 0;
for value in values.iter().filter(|x| *x % 2 == 0).take(5) {
    even_sum_squares += value * value;
}

Sin embargo, la gran colección de métodosIterator incluye muchos que permiten consumir una iteración en una sola llamada al método, eliminando la necesidad de un bucle for explícito.

El más general de estos métodos es for_each(|item| {...})que ejecuta un cierre para cada elemento producido por Iterator. Puede hacer la mayoría de las cosas que puede hacer un bucle explícito de for (las excepciones se describen en una sección posterior), pero su generalidad también hace que sea un poco incómodo de utilizar: el cierre tiene que utilizar referencias mutables al estado externo para poder emitir algo:

let mut even_sum_squares = 0;
values
    .iter()
    .filter(|x| *x % 2 == 0)
    .take(5)
    .for_each(|value| {
        // closure needs a mutable reference to state elsewhere
        even_sum_squares += value * value;
    });

Sin embargo, si el cuerpo del bucle for coincide con alguno de los patrones habituales, existen métodos más específicos de consumo de iteradores que son más claros, cortos e idiomáticos.

Estos patrones incluyen atajos para construir un único valor a partir de la colección:

sum()

Suma una colección de valores numéricos (enteros o flotantes).

product()

Multiplica una colección de valores numéricos.

min()

Busca el valor mínimo de una colección, en relación con la implementación Ord del elemento (véase el elemento 10).

max()

Busca el valor máximo de una colección, en relación con la implementación Ord del artículo (véase el artículo 10).

min_by(f)

Encuentra el valor mínimo de una colección, en relación con una función de comparación especificada por el usuario f.

max_by(f)

Busca el valor máximo de una colección, en relación con una función de comparación especificada por el usuario f.

reduce(f)

Construye un valor acumulado del tipo Item ejecutando en cada paso un cierre que toma el valor acumulado hasta el momento y el elemento actual. Se trata de una operación más general que engloba los métodos anteriores.

fold(f)

Construye un valor acumulado de un tipo arbitrario (no sólo del tipo Iterator::Item ) ejecutando en cada paso un cierre que toma el valor acumulado hasta el momento y el elemento actual. Se trata de una generalización de reduce.

scan(init, f)

Construye un valor acumulado de un tipo arbitrario ejecutando en cada paso un cierre que toma una referencia mutable a algún estado interno y al elemento actual. Se trata de una generalización ligeramente diferente de reduce.

También hay métodos para seleccionar un único valor de la colección:

find(p)

Busca el primer elemento que satisfaga un predicado.

position(p)

También encuentra el primer elemento que satisface un predicado, pero esta vez devuelve el índice del elemento.

nth(n)

Devuelve el elemento ndel iterador, si está disponible.

Hay métodos para comprobar cada elemento de la colección:

any(p)

Indica si un predicado es true para cualquier elemento de la colección.

all(p)

Indica si un predicado es true para todos los elementos de la colección.

En cualquier caso, la iteración terminará antes si se encuentra el contraejemplo pertinente.

Hay métodos que permiten la posibilidad de fallo en los cierres utilizados con cada elemento. En cada caso, si un cierre devuelve un fallo para un elemento, se termina la iteración y la operación en su conjunto devuelve el primer fallo:

try_for_each(f)

Se comporta como for_each, pero el cierre puede fallar

try_fold(f)

Se comporta comofold, pero el cierre puede fallar

try_find(f)

Se comporta comofind, pero el cierre puede fallar

Por último, hay métodos que acumulan todos los elementos iterados en una nueva colección. El más importante es collect()que puede utilizarse para construir una nueva instancia de cualquier tipo de colección que implemente el rasgoFromIterator rasgo.

El rasgo FromIterator se implementa para todos los tipos de colección de la biblioteca estándar (Vec, HashMap, BTreeSetetc.), pero esta ubicuidad también significa que a menudo tienes que utilizar tipos explícitos, porque de lo contrario el compilador no puede averiguar si estás intentando ensamblar (digamos) un Vec<i32> o un HashSet<i32>:

use std::collections::HashSet;

// Build collections of even numbers.  Type must be specified, because
// the expression is the same for either type.
let myvec: Vec<i32> = (0..10).into_iter().filter(|x| x % 2 == 0).collect();
let h: HashSet<i32> = (0..10).into_iter().filter(|x| x % 2 == 0).collect();

Este ejemplo también ilustra el uso de expresiones de rango para generar los datos iniciales sobre los que se va a iterar.

Otros métodos (más oscuros) de producción de colecciones son los siguientes:

unzip()

Divide un iterador de pares en dos colecciones

partition(p)

Divide un iterador en dos colecciones en función de un predicado que se aplica a cada elemento

En este artículo se ha tratado una amplia selección de métodos de Iterator, pero esto es sólo un subconjunto de los métodos disponibles; para más información, consulta la documentación de los iteradores o lee el Capítulo 15 de Programming Rust, 2ª edición (O'Reilly), que cubre ampliamente las posibilidades.

Esta rica colección de transformaciones de iteradores está ahí para ser utilizada. Produce código más idiomático, más compacto y con una intención más clara.

Expresar los bucles como transformaciones de iteradores también puede producir código más eficiente. En aras de la seguridad, Rust realiza comprobaciones de límites en el acceso a contenedores contiguos como vectores y rebanadas; un intento de acceder a un valor más allá de los límites de la colección desencadena un pánico en lugar de un acceso a datos no válidos. Un bucle de estilo antiguo que accede a valores contenedores (por ejemplo, values[i]) podría estar sujeto a estas comprobaciones en tiempo de ejecución, mientras que un iterador que produce un valor tras otro ya se sabe que está dentro de los límites.

Sin embargo, también puede darse el caso de que un bucle de estilo antiguo no esté sujeto a comprobaciones de límites adicionales en comparación con la transformación equivalente de un iterador. El compilador y el optimizador de Rust son muy buenos a la hora de analizar el código que rodea el acceso a un trozo para determinar si es seguro omitir las comprobaciones de límites; el artículo 2023 de Sergey "Shnatsel" Davidoff explora las sutilezas implicadas.

Construir colecciones a partir de Result Valores

La sección anterior describía el uso de collect() para construir colecciones a partir de iteradores, pero collect() también tiene una característica especialmente útil cuando se trata de valores de Result.

Considera un intento de convertir un vector de valores i64 en bytes (u8), con la optimista expectativa de que quepan todos:

Esto funciona hasta que aparece alguna entrada inesperada:

let inputs: Vec<i64> = vec![0, 1, 2, 3, 4, 512];

y provoca un fallo de ejecución:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
TryFromIntError(())', iterators/src/main.rs:266:36
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Siguiendo el consejo dado en el punto 3, queremos mantener el tipo Result en juego y utilizar el operador ? para que cualquier fallo sea problema del código llamante. La modificación obvia de emitir el Result no ayuda mucho:

let result: Vec<Result<u8, _>> =
    inputs.into_iter().map(|v| <u8>::try_from(v)).collect();
// Now what?  Still need to iterate to extract results and detect errors.

Sin embargo, existe una versión alternativa de collect(), que puede ensamblar un Result que sujeta un Vec, en lugar de unVec que sujeta Results.

Forzar el uso de esta versión requiere el turbofish (::<Result<Vec<_>, _>>):

let result: Vec<u8> = inputs
    .into_iter()
    .map(|v| <u8>::try_from(v))
    .collect::<Result<Vec<_>, _>>()?;

Combinando esto con el operador signo de interrogación se obtiene un comportamiento útil:

  • Si la iteración encuentra un valor de error, ese valor de error se emite a la persona que llama y la iteración se detiene.

  • Si no se encuentra ningún error, el resto del código puede tratar con una colección sensata de valores del tipo correcto.

Transformación en bucle

El objetivo de este Tema es convencerte de que muchos bucles explícitos pueden considerarse transformaciones de iteradores. Esto puede resultar algo antinatural para los programadores que no estén acostumbrados, así que vamos a recorrer una transformación paso a paso.

Comienza con un bucle explícito muy similar a C para sumar los cuadrados de los cinco primeros elementos pares de un vector:

let mut even_sum_squares = 0;
let mut even_count = 0;
for i in 0..values.len() {
    if values[i] % 2 != 0 {
        continue;
    }
    even_sum_squares += values[i] * values[i];
    even_count += 1;
    if even_count == 5 {
        break;
    }
}

El primer paso es sustituir la indexación de vectores por el uso directo de un iterador en un bucle for-each:

let mut even_sum_squares = 0;
let mut even_count = 0;
for value in values.iter() {
    if value % 2 != 0 {
        continue;
    }
    even_sum_squares += value * value;
    even_count += 1;
    if even_count == 5 {
        break;
    }
}

Un brazo inicial del bucle que utiliza continue para saltarse sobre algunos elementos se expresa naturalmente como filter():

let mut even_sum_squares = 0;
let mut even_count = 0;
for value in values.iter().filter(|x| *x % 2 == 0) {
    even_sum_squares += value * value;
    even_count += 1;
    if even_count == 5 {
        break;
    }
}

A continuación, la salida anticipada del bucle una vez que se han localizado cinco elementos pares se asigna a un take(5):

let mut even_sum_squares = 0;
for value in values.iter().filter(|x| *x % 2 == 0).take(5) {
    even_sum_squares += value * value;
}

Cada iteración del bucle utiliza sólo el elemento al cuadrado, en la combinación value * value, lo que hace que sea un objetivo ideal para un map():

let mut even_sum_squares = 0;
for val_sqr in values.iter().filter(|x| *x % 2 == 0).take(5).map(|x| x * x)
{
    even_sum_squares += val_sqr;
}

Estas refactorizaciones del bucle original dan como resultado un cuerpo de bucle que es el clavo perfecto para encajar bajo el martillo del método sum():

let even_sum_squares: u64 = values
    .iter()
    .filter(|x| *x % 2 == 0)
    .take(5)
    .map(|x| x * x)
    .sum();

Cuando lo explícito es mejor

Este artículo ha puesto de relieve las ventajas de las transformaciones de iteradores, sobre todo en cuanto a concisión y claridad. Entonces, ¿cuándo no son apropiadas o idiomáticas las transformaciones de iteradores?

  • Si el cuerpo del bucle es grande y/o multifuncional, tiene sentido mantenerlo como un cuerpo explícito en lugar de apretujarlo en un cierre.

  • Si el cuerpo del bucle implica condiciones de error que provocan la finalización anticipada de la función circundante, a menudo es mejor mantener explícitos estos -los métodos try_..() sólo ayudan un poco. Sin embargo, la capacidad de collect()de convertir una colección de valores Result en una Result que contenga una colección de valores, a menudo permite seguir gestionando las condiciones de error con el operador ?.

  • Si el rendimiento es vital, una transformación de iterador que implique un cierre debe optimizarse para que sea tan rápida como el código explícito equivalente. Pero si el rendimiento de un bucle central es tan importante, mide diferentes variantes y afina adecuadamente:

    • Ten cuidado de que tus mediciones reflejen el rendimiento en el mundo real: el optimizador del compilador puede dar resultados demasiado optimistas con datos de prueba (como se describe en el punto 30).

    • El explorador del compilador Godbolt es una herramienta increíble para explorar lo que escupe el compilador.

Y lo que es más importante, no conviertas un bucle en una transformación de iteración si la conversión resulta forzada o incómoda. Se trata de una cuestión de gustos, pero ten en cuenta que es probable que tus gustos cambien a medida que te familiarices con el estilo funcional.

1 La situación se enturbia aún más si interviene el sistema de archivos, ya que los nombres de archivo en las plataformas más populares están a medio camino entre los bytes arbitrarios y las secuencias UTF-8: consulta la documentación std::ffi::OsString documentación.

2 Técnicamente, un valor escalar Unicode en lugar de un punto de código.

3 La necesidad de considerar todas las posibilidades también significa que añadir una nueva variante a un enum existente en una biblioteca es un cambio de ruptura(Tema 21): los clientes de la biblioteca tendrán que cambiar su código para adaptarse a la nueva variante. Si un enum es realmente una lista de valores numéricos relacionados, similar a C, este comportamiento puede evitarse marcándolo como un non_exhaustive enum ver punto 21.

4 Al menos no en Rust estable en el momento de escribir esto. Los enlaces unboxed_closures y fn_traits pueden cambiar esto en el futuro.

5 Por ejemplo, el libro de Joshua Bloch Effective Java (3ª edición, Addison-Wesley) incluye el punto 64: Refiérete a los objetos por sus interfaces.

6 La adición de conceptos en C++20 permite la especificación explícita de restricciones en los tipos de plantilla, pero las comprobaciones siguen realizándose sólo cuando se instancian las plantillas, no cuando se declaran.

7 La versión online de este diagrama es clicable; cada recuadro enlaza con la documentación pertinente.

8 Observa que este método es independiente del rasgo AsRef, aunque el nombre del método sea el mismo.

9 O, al menos, el único método no deprecado y estable.

10 En el momento de escribir esto, Error se ha trasladado a core, pero aún no está disponible en Rust estable.

11 Esta sección está inspirada en el artículo de Nick Groenen "Rust: Estructuración y gestión de errores en 2020".

12 Más propiamente conocidas como reglas de coherencia de rasgos.

13 Por ahora, es probable que se sustituya por el tipo! "nunca" en una futura versión de Rust.

14 Permitir las conversiones con pérdida en Rust fue probablemente un error, y ha habido discusiones en torno a intentar eliminar este comportamiento.

15 Rust se refiere a estas conversiones como "subtipado", pero es bastante diferente de la definición de "subtipado" utilizada en los lenguajes orientados a objetos.

16 Concretamente, el Orbitador Climático de Marte.

17 Véase "Mars Climate Orbiter" en Wikipedia para más información sobre la causa del fallo.

18 Éste es un problema lo suficientemente común para serde como para que incluya un mecanismo de ayuda.

19 Aunque con una advertencia de los compiladores modernos.

20 El rasgo equivalente para expresiones mutables es IndexMut.

21 Esto es algo simplificado; una vtabla completa también incluye información sobre el tamaño y la alineación del tipo, junto con un puntero a la función drop() para que el objeto subyacente pueda abandonarse con seguridad.

22 Ten en cuenta que esto no afecta a las garantías de seguridad de memoria de Rust: los elementos siguen siendo seguros, sólo que inaccesibles.

23 De hecho, el iterador puede ser más general: no es necesario asociar a un contenedor la idea de emitir los siguientes elementos hasta su finalización.

24 Este método no se puede proporcionar si una mutación del elemento puede invalidar las garantías internas del contenedor. Por ejemplo, cambiar el contenido del elemento de forma que se altere su Hash invalidaría las estructuras de datos internas de HashMap.

Get Óxido efectivo now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.