Aprende Rust

Lección 09 de 26 Structs, enums y patrones

Structs: agrupa tus datos

  • 4 min de lectura
  • 4 ejercicios
  1. Un struct crea tu propio tipo agrupando datos con nombre. Jugador es la plantilla y tiene tres campos.
  2. Para crear una instancia das un valor a todos los campos. Este jugador se llama "Ferris".
  3. Con el punto accedes a cada campo: jugador.nivel vale 3.
  4. Si la instancia es let mut, puedes cambiar sus campos: con jugador.nivel += 1, Ferris sube al nivel 4.
1/4

Hasta ahora has agrupado datos con tuplas, como (String, u32). Funcionan, pero sus elementos no tienen nombre: ¿qué era .1? Un struct te permite crear tu propio tipo y ponerle nombre a cada dato.

Si vienes de JavaScript, piensa en un objeto con una forma fija; si vienes de otros lenguajes, en una clase que solo tiene datos. El comportamiento (los métodos) lo añadirás en la siguiente lección.

Definir un struct

struct Jugador {
    nombre: String,
    nivel: u32,
    activo: bool,
}
  • struct va seguido del nombre del tipo, en PascalCase.
  • Entre llaves, los campos: cada uno con su nombre (en snake_case) y su tipo, separados por comas.

La definición es solo un molde: describe la forma de los datos, pero todavía no crea ninguno.

Crear una instancia

Para crear un valor de ese tipo, una instancia, escribe el nombre del struct y da un valor a todos sus campos, en el orden que quieras. Con el punto accedes a cada campo:

struct Jugador {
    nombre: String,
    nivel: u32,
    activo: bool,
}

fn main() {
    let jugador = Jugador {
        nombre: String::from("Ferris"),
        activo: true,
        nivel: 3,
    };

    println!("{} está en el nivel {}", jugador.nombre, jugador.nivel);
    println!("¿Activo? {}", jugador.activo);
}

Mutabilidad

Para cambiar un campo, la instancia entera tiene que ser mutable. Rust no permite marcar como mut solo algunos campos:

struct Jugador {
    nombre: String,
    nivel: u32,
}

fn main() {
    let mut jugador = Jugador {
        nombre: String::from("Ferris"),
        nivel: 1,
    };

    jugador.nivel += 1;
    jugador.nombre.push_str(" el Cangrejo");

    println!("{} sube al nivel {}", jugador.nombre, jugador.nivel);
}

Sintaxis abreviada de campos

Es habitual crear instancias desde una función. Si un parámetro se llama igual que un campo, basta con escribir su nombre una vez (field init shorthand):

fn crear_jugador(nombre: String, nivel: u32) -> Jugador {
    Jugador {
        nombre, // equivale a nombre: nombre
        nivel,  // equivale a nivel: nivel
        activo: true,
    }
}

Crear una instancia a partir de otra

Con la sintaxis de actualización ..otra creas una instancia nueva que toma de otra todos los campos que no indiques:

struct Jugador {
    nombre: String,
    nivel: u32,
    activo: bool,
}

fn main() {
    let mario = Jugador {
        nombre: String::from("Mario"),
        nivel: 5,
        activo: true,
    };

    let luigi = Jugador {
        nombre: String::from("Luigi"),
        ..mario // nivel y activo se toman de mario
    };

    println!("{}: nivel {}, activo {}", luigi.nombre, luigi.nivel, luigi.activo);
    println!("{} sigue disponible", mario.nombre);
}

..mario siempre va al final. Y ojo con el ownership: los campos que se toman de mario se mueven o se copian igual que en una asignación. Aquí solo se toman nivel y activo, que son Copy, así que mario sigue intacto. Si luigi tomara también el nombre de mario, ese String se movería y ya no podrías usar mario.nombre.

Tuple structs

Los tuple structs tienen campos sin nombre, solo con tipo. Son útiles cuando el nombre del tipo ya lo dice todo:

struct Color(u8, u8, u8);
struct Punto(i32, i32);

fn main() {
    let rojo = Color(255, 0, 0);
    let origen = Punto(0, 0);

    println!("Rojo: {}, {}, {}", rojo.0, rojo.1, rojo.2);

    let Punto(x, y) = origen; // desestructurar, como en una tupla
    println!("Origen: ({x}, {y})");
}

Accedes a los campos por su posición (.0, .1…) o desestructurando. Aunque Color y Punto se parezcan, son tipos distintos: una función que espera un Punto no acepta un Color.

Unit structs

También puedes definir un struct sin campos, un unit struct. Sirve cuando necesitas un tipo, pero no datos que guardar; le sacarás partido cuando veas los traits:

struct SiempreIgual;

Se crea escribiendo solo su nombre: let valor = SiempreIgual;.

Imprimir un struct con Debug

Si intentas imprimir un struct con {}, no compila: Rust no sabe cómo quieres mostrarlo.

struct Jugador {
    nombre: String,
    nivel: u32,
}

fn main() {
    let jugador = Jugador { nombre: String::from("Ferris"), nivel: 3 };
    println!("{}", jugador);
}
error[E0277]: `Jugador` doesn't implement `std::fmt::Display`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

Para depurar, añade #[derive(Debug)] encima del struct: le pide al compilador que genere automáticamente el formato de depuración. Después usa {:?}, o {:#?} para verlo en varias líneas:

#[derive(Debug)]
struct Jugador {
    nombre: String,
    nivel: u32,
}

fn main() {
    let jugador = Jugador { nombre: String::from("Ferris"), nivel: 3 };
    println!("{} juega en el nivel {}", jugador.nombre, jugador.nivel);
    println!("{jugador:?}");
    println!("{jugador:#?}");
}
Ferris juega en el nivel 3
Jugador { nombre: "Ferris", nivel: 3 }
Jugador {
    nombre: "Ferris",
    nivel: 3,
}

Aviso del compilador: Si un campo solo se lee a través de Debug, Rust te avisará con field is never read: imprimir con {:?} no cuenta como usarlo. Es una advertencia, no un error, pero te recuerda que ese dato no se usa en ningún sitio.

La macro dbg!

Para depurar es todavía más cómoda la macro dbg!: imprime el fichero, la línea, la expresión y su valor, y además devuelve el valor, así que puedes meterla en medio de una expresión:

#[derive(Debug)]
struct Rectangulo {
    ancho: u32,
    alto: u32,
}

fn main() {
    let escala = 2;
    let rect = Rectangulo {
        ancho: dbg!(30 * escala), // imprime la expresión y devuelve 60
        alto: 50,
    };

    dbg!(&rect); // con & para prestarlo en lugar de moverlo
    let area = dbg!(rect.ancho * rect.alto);
    println!("Área: {area}");
}
[src/main.rs:10:16] 30 * escala = 60
[src/main.rs:14:5] &rect = Rectangulo {
    ancho: 60,
    alto: 50,
}
[src/main.rs:15:16] rect.ancho * rect.alto = 3000
Área: 3000

Dos detalles: dbg! escribe en la salida de errores (stderr), no en la estándar, y toma la propiedad de lo que le pasas. Por eso se usa dbg!(&rect): así solo lo presta.

Resumen

Qué quieres hacerCómo
Definir un tipo con camposstruct Jugador { nombre: String, nivel: u32 }
Crear una instanciaJugador { nombre, nivel: 1 }
Leer o cambiar un campojugador.nivel, jugador.nivel += 1 (con let mut)
Copiar el resto de campos de otra instanciaJugador { nivel: 2, ..otro }
Campos sin nombrestruct Color(u8, u8, u8); y color.0
Tipo sin datosstruct SiempreIgual;
Imprimir para depurar#[derive(Debug)] con {:?}, {:#?} o dbg!

Hora de practicar

Ejercicios

0/4 resueltos

Reto 1 de 4

Define el struct Libro con tres campos: titulo (String), autor (String) y paginas (u32). Después, en main, crea un libro con título El Quijote, autor Cervantes y 1250 páginas, e imprime sus campos así: El Quijote, de Cervantes (1250 páginas).
Salida esperada
El Quijote, de Cervantes (1250 páginas)
src/main.rs
$ cargo run Listo

Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.

¿Algo no cuadra en este reto? Avísanos