Box y los rasgos

Box es muy útil para devolver rasgos. Como ya se conoce, se pueden escribir rasgos en funciones genéricas como la del siguiente ejemplo:

use std::fmt::Display;

struct DoesntImplementDisplay {}

fn displays_it<T: Display>(input: T) {
    println!("{}", input);
}

fn main() {}

Como la función displays_it solo recibe un elemento que implemente Display, no puede aceptar elementos del tipo struct DoesntImplementDisplay. Sin embargo, puede recibir como parámetro otros elementos como String.

También se ha visto que se puede usar impl Trait para devolver otros rasgos o cierres. Box se puede usar de una forma similar ya que permite que el compilador conozca el tamaño del elemento a devolver. El siguiente ejemplo muestra que un rasgo puede usarse en un elemento de cualquier tamaño:

#![allow(dead_code)] // Le dice al compilador que no se queje con código muerto
use std::mem::size_of; // Es función devuelve el tamaño de un tipo determinado

trait JustATrait {} // Se implementará este rasgo en todos los tipos

enum EnumOfNumbers {
    I8(i8),
    AnotherI8(i8),
    OneMoreI8(i8),
}
impl JustATrait for EnumOfNumbers {}

struct StructOfNumbers {
    an_i8: i8,
    another_i8: i8,
    one_more_i8: i8,
}
impl JustATrait for StructOfNumbers {}

enum EnumOfOtherTypes {
    I8(i8),
    AnotherI8(i8),
    Collection(Vec<String>),
}
impl JustATrait for EnumOfOtherTypes {}

struct StructOfOtherTypes {
    an_i8: i8,
    another_i8: i8,
    a_collection: Vec<String>,
}
impl JustATrait for StructOfOtherTypes {}

struct ArrayAndI8 {
    array: [i8; 1000], // Este va a ser muy largo
    an_i8: i8,
    in_u8: u8,
}
impl JustATrait for ArrayAndI8 {}

fn main() {
    println!(
        "{}, {}, {}, {}, {}",
        size_of::<EnumOfNumbers>(),
        size_of::<StructOfNumbers>(),
        size_of::<EnumOfOtherTypes>(),
        size_of::<StructOfOtherTypes>(),
        size_of::<ArrayAndI8>(),
    );
}

Cuando se imprime el resultado del tamaño de estos objetos se obtiene 2, 3, 32, 1002. Por ello, si se devolviera lo siguiente, daría error:

#![allow(unused)]
fn main() {
// ⚠️
fn returns_just_a_trait() -> JustATrait {
    let some_enum = EnumOfNumbers::I8(8);
    some_enum
}
}

El compilador dice:

error[E0746]: return type cannot have an unboxed trait object
  --> src\main.rs:53:30
   |
53 | fn returns_just_a_trait() -> JustATrait {
   |                              ^^^^^^^^^^ doesn't have a size known at compile-time

Debido a que el rasgo puede implementarse por objetos de diverso tamaño, el compilador se queja. Para que funcione, es necesario guardar el objeto en un Box. Además, hace falta indicar dyn, que sirve para indicar que se está devolviendo en tiempo de ejecución un elemento de diverso tipo, que está representado por un rasgo que sí que tienen que tener los valores a devolver (N.T.: Es una forma de polimorfismo).

La nueva función queda así:

#![allow(unused)]
fn main() {
// 🚧
fn returns_just_a_trait() -> Box<dyn JustATrait> {
    let some_enum = EnumOfNumbers::I8(8);
    Box::new(some_enum)
}
}

Ahora funciona. En la pila se puede incorporar el elemento Box de tamaño fijo.

Esta forma de código se encuentra mucho en la forma de Box<dyn Error>, debido a que en ocasiones puede haber diversos tipos de error.

El siguiente ejemplo muestra dos tipos de error. Para crear un elemento de tipo Error que sea oficial, se debe implementar el rasgo std::error::Error. Esto es fácil, simplemente se escribe impl std::error::Error {}. Los tipos de error también tienen que implementar Debug y Display para que puedan mostrar información sobre el problema. Debug es fácil de implementar, basta con #[derive(Debug)], pero Display necesita el método .fmt(). Esto ya se ha codificado antes:

use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct ErrorOne;

impl Error for ErrorOne {} // Ahora ya es un tipo de eroro con Debug. Falta Display:

impl fmt::Display for ErrorOne {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "¡Se ha producido el primer error!") // Todo lo que hace es escribir este mensaje
    }
}


#[derive(Debug)] // Se hace lo mismo con ErrorTwo
struct ErrorTwo;

impl Error for ErrorTwo {}

impl fmt::Display for ErrorTwo {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "¡Se ha producido el segundo error!")
    }
}

// Se crea una función que devuelve un String o Errores
fn returns_errors(input: u8) -> Result<String, Box<dyn Error>> { // Con Box<dyn Error> se puede devolver cualquier elemento que implemente el rasgo Error

    match input {
        0 => Err(Box::new(ErrorOne)), // no se puede olvidar que debe estar en una Box
        1 => Err(Box::new(ErrorTwo)),
        _ => Ok("Parece correcto".to_string()), // Este es el tipo de retorno correcto
    }

}

fn main() {

    let vec_of_u8s = vec![0_u8, 1, 80]; // Tres números de prueba

    for number in vec_of_u8s {
        match returns_errors(number) {
            Ok(input) => println!("{}", input),
            Err(message) => println!("{}", message),
        }
    }
}

Este código imprime:

¡Se ha producido el primer error!
¡Se ha producido el segundo error!
Parece correcto

Si no se devolviera Box<dyn Error> se produciría un error al compilar:

#![allow(unused)]
fn main() {
// ⚠️
fn returns_errors(input: u8) -> Result<String, Error> {
    match input {
        0 => Err(ErrorOne),
        1 => Err(ErrorTwo),
        _ => Ok("Parece correcto".to_string()),
    }
}
}

Producirá el siguiente error durante la compilación:

21  | fn returns_errors(input: u8) -> Result<String, Error> {
    |                                 ^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time

Lo que no es sorprendente ya que los rasgos se pueden implementar por muchos tipos diferentes que pueden tener diferente tamaño.