Structs en Rust – Aprende Rust 8/x

structs en Rust

Una estructura o struct es un tipo de dato personalizado que nos sirve para agrupar datos en un determinado ámbito y pertenezcan a una misma entidad. Si conoces POO te resultará familiar, ya que los structs en Rust tienen funciones que se llaman métodos (methods) o variables que son llamadas campos (fields), así que esto no será complicado de entender.

Structs en Rust

Para declarar un struct hacemos lo siguiente:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

Como vemos, usamos la palabra struct seguida por el nombre del struct que vayamos a usar.

NOTA: si compilamos en este momento, nos dará un error:

struct `User` is never constructed
`#[warn(dead_code)]` on by default

O sea, si ya declaramos la estructura debemos crear una instancia de ella (un objeto):

fn main() {
    let user1 = User {
        active: true,
        username: String::from("username123"),
        email: String::from("[email protected]"),
        sign_in_count: 1,
    };
}

Esto sigue la regla de la mutabilidad en Rust. Es decir, tal como declaramos el struct solo estamos fijando los valores y no podemos modificarlos. Si queremos modificar alguno de los fields del struct, debemos declararlo usando la palabra mut.

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("username123"),
        email: String::from("[email protected]"),
        sign_in_count: 1,
    };

    user1.email = String::from("[email protected]");
}

De esta manera ya seremos capaces de modificar un valor cualquiera del struct.

En Rust no es posible hacer mutable solo un valor del struct, por lo que toda la estructura debe ser mutable.

En una función también podemos retornar una instancia del struct:

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}

De esta manera estamos recibiendo dos valores, email y username para dárselos a nuestro struct. Una vez que asigna los valores retornamos la instancia.

Una forma de ahorrarnos un poco de código es asignar directamente el nombre variables en el struct. Por ejemplo, aquí estamos usando algo como username: username y email: email. Bueno, pues esto, como en Javascript, podemos usarlo sin el uso de los dos puntos, solo poniendo el mismo nombre del field, separándolo del siguiente valor con una coma.

NOTA: esto solo es posible porque el parámetro de que recibe build_user se llama email o username. Si el field email se llamara user_email no podríamos usarlo y tendríamos que volver a la notación anterior: email: user_email. Esto es porque no es posicional como veremos adelante.

Crear instancias de otra instancia

También podemos usar los valores una instancia de nuestro struct para asignarlos en otra instancia:

fn main() {
    // --snip--

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("[email protected]"),
        sign_in_count: user1.sign_in_count,
    };
}

Aquí vemos que estamos usando tres valores exactamente iguales, solo cambia el email. Usando una sintaxis corta, podemos decirle a nuestro nuevo struct que primero queremos el campo que no será igual y luego usamos una especie de operador spread (..) para rellenar los siguientes fields.

    let user2 = User {
        email: String::from("[email protected]"),
        ..user1
    };

Primero deberán ir siempre los fields que cambian y luego el operador para establecer los mismos valores.

Tuple struct

Existe una manera de declarar struct como si fueran tuples, es decir, sin nombrar los fields, solo indicando su tipo. Es especialmente útil cuando es demasiado obvia la declaración y nombrar los fields es redundante, por ejemplo, la fecha.

struct MiFecha(i32, i32, i32);

fn main() {
    let fecha = MiFecha(2024, 02, 29);
}

Con esto declaramos e instanciamos nuestro struct.

Hay dos formas de acceder a sus valores. La más obvia es usando la misma notación que usamos en las tuplas. Para esto usaríamos lo siguiente:

struct MiFecha(i32, i32, i32);

fn main() {
    let fecha = MiFecha(2024, 2, 29);

    let (year, month, day) = fecha;
    println!("{}-{}-{}", year, month, day);
}

Sin embargo obtenemos lo siguiente:

error[E0308]: mismatched types
 --> src/main.rs:6:9
  |
6 |     let (year, month, day) = fecha;
  |         ^^^^^^^^^^^^^^^^^^   ----- this expression has type `MiFecha`
  |         |
  |         expected `MiFecha`, found `(_, _, _)`
  |
  = note: expected struct `MiFecha`
              found tuple `(_, _, _)`

For more information about this error, try `rustc --explain E0308`.

Esto es porque el compilador espera que los tipos coincidan y nosotros no hemos declarado simplemente tipos nativos, que sí, lo son, pero están dentro de un struct, es por ello que debemos hacer el destructuring de la siguiente manera:

    let MiFecha(year, month, day) = fecha;

Con esto funcionará, ya que le indicamos que mis datos i32 los obtendrá de un struct del mismo tipo.

Otra manera de acceder a los fields del struct es por su index, de la siguiente manera:

println!("año:{} día:{} mes:{}", fecha.0, fecha.2, fecha.1);

Structs vacíos en Rust

También puedes declarar structs vacíos en Rust, que básicamente son como tuplas vacías, es decir, que regresan (), son llamados unit-like structs y sirven para implementar “algo”, pero no necesitas tener fields asignados. En otro post hablaremos de traits, que es donde cobran relevancia.

Su declaración es de esta manera;

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Ejemplo de structs en Rust

Acá vemos un ejemplo para calcular el área, usando los structs:

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

Como curiosos que somos, qué pasa si yo quiero imprimir la instancia de Rectangle, rect1. Si queremos hacerlo directamente no funcionará:

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );

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

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

No da el siguiente error:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
  --> src/main.rs:17:20
   |
17 |     println!("{}", rect1);
   |                    ^^^^^ `Rectangle` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.

Tenemos que hacer un par de modificaciones para ver nuestra instancia pretty-print.

Asignamos la directiva #[derive(Debug)] antes de la declaración del struct.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

Y para usar la macro println!, agregamos lo siguiente.

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

Ahora ya podemos ver la instancia de Rectangle:

Rectangle { width: 30, height: 50 }

#[derive(Debug)] es un atributo que se usa para generar una implementación del trait Debug para una estructura. Este trait es usado para imprimir la salida de los valores de un objeto. Como dije antes, veremos los traits en otro momento.

Acá vamos a dejar este tema por ahora. Espero les haya servido.

Gracias por leer.


Posted

in

, , ,

by