En esta página

Structs: tipos de datos personalizados

14 min lectura TextoCap. 3 — Tipos compuestos

Structs: definiendo tipos propios

Los structs (estructuras) son la forma principal de crear tipos de datos personalizados en Rust. Te permiten agrupar campos relacionados bajo un nombre significativo, muy similar a las clases en otros lenguajes, pero sin herencia.

Definición básica

// Definir un struct
struct Rectangulo {
    ancho: u32,
    alto: u32,
}

fn main() {
    // Crear una instancia
    let rect = Rectangulo {
        ancho: 100,
        alto: 50,
    };
    
    println!("Ancho: {}", rect.ancho);
    println!("Alto: {}", rect.alto);
    println!("Área: {}", rect.ancho * rect.alto);
    
    // Instancias mutables
    let mut rect2 = Rectangulo { ancho: 200, alto: 100 };
    rect2.ancho = 300; // Modificar un campo
    println!("Nuevo ancho: {}", rect2.ancho);
}

Nota importante: En Rust, la mutabilidad aplica a toda la instancia. No puedes tener algunos campos mutables y otros no — o todo el struct es mutable o nada lo es.

Shorthand de inicialización

Cuando tienes variables con el mismo nombre que los campos del struct:

fn crear_usuario(nombre: String, email: String, edad: u32) -> Usuario {
    Usuario {
        nombre,  // Equivalente a nombre: nombre
        email,   // Equivalente a email: email
        edad,    // Equivalente a edad: edad
        activo: true, // Este sí necesita el nombre explícito
    }
}

struct Usuario {
    nombre: String,
    email: String,
    edad: u32,
    activo: bool,
}

Sintaxis de actualización de struct

Puedes crear un nuevo struct basándote en otro existente:

struct Config {
    debug: bool,
    max_conexiones: u32,
    timeout_ms: u64,
    nombre_app: String,
}

fn main() {
    let config_base = Config {
        debug: false,
        max_conexiones: 100,
        timeout_ms: 5000,
        nombre_app: String::from("MiApp"),
    };
    
    // Solo cambiamos debug, el resto viene de config_base
    // Nota: nombre_app se MUEVE (es String), no se copia
    let config_dev = Config {
        debug: true,
        ..config_base
    };
    
    println!("Debug mode: {}", config_dev.debug);
    println!("Max conexiones: {}", config_dev.max_conexiones);
    // config_base.nombre_app ya no es válido (fue movido)
}

Bloques impl: métodos y funciones asociadas

Los métodos se definen en bloques impl (implementation). La diferencia entre métodos y funciones asociadas:

  • Métodos: Primer parámetro es self, &self, o &mut self
  • Funciones asociadas: No tienen self — son como "métodos estáticos" o constructores
struct Circulo {
    radio: f64,
}

impl Circulo {
    // Función asociada (constructor)
    fn nuevo(radio: f64) -> Self {
        assert!(radio > 0.0, "El radio debe ser positivo");
        Circulo { radio }
    }
    
    fn unidad() -> Self {
        Circulo::nuevo(1.0)
    }
    
    // Métodos de solo lectura: &self
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radio * self.radio
    }
    
    fn perimetro(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.radio
    }
    
    fn es_mayor_que(&self, otro: &Circulo) -> bool {
        self.radio > otro.radio
    }
    
    // Método mutante: &mut self
    fn escalar(&mut self, factor: f64) {
        self.radio *= factor;
    }
    
    // Método consumidor: toma ownership (self, sin &)
    fn en_cuadrado(self) -> f64 {
        self.radio * 2.0 // Lado del cuadrado que lo contiene
    }
}

fn main() {
    let mut c1 = Circulo::nuevo(5.0);
    let c2 = Circulo::unidad();
    
    println!("Área de c1: {:.4}", c1.area());
    println!("Perímetro de c1: {:.4}", c1.perimetro());
    println!("¿c1 > c2? {}", c1.es_mayor_que(&c2));
    
    c1.escalar(2.0);
    println!("Radio después de escalar: {}", c1.radio);
    
    let lado = c1.en_cuadrado();
    println!("Lado del cuadrado: {}", lado);
    // c1 ya no es válido — fue consumido por en_cuadrado
}

Macros derive

Rust puede generar automáticamente implementaciones de ciertos traits con la macro #[derive(...)]:

#[derive(Debug, Clone, PartialEq, PartialOrd)]
struct Temperatura {
    celsius: f64,
}

impl Temperatura {
    fn nueva(celsius: f64) -> Self {
        Temperatura { celsius }
    }
    
    fn en_fahrenheit(&self) -> f64 {
        self.celsius * 1.8 + 32.0
    }
    
    fn en_kelvin(&self) -> f64 {
        self.celsius + 273.15
    }
}

fn main() {
    let t1 = Temperatura::nueva(100.0);
    let t2 = t1.clone(); // Clone derivado
    let t3 = Temperatura::nueva(0.0);
    
    println!("{:?}", t1);     // Debug derivado: Temperatura { celsius: 100.0 }
    println!("{:#?}", t1);    // Debug pretty-print
    println!("¿t1 == t2? {}", t1 == t2); // PartialEq derivado
    println!("¿t1 > t3? {}", t1 > t3);   // PartialOrd derivado
    
    println!("{}°C = {}°F = {}K", t1.celsius, t1.en_fahrenheit(), t1.en_kelvin());
}

Tuple structs y unit structs

Tuple structs: Son structs sin nombres de campo, solo tipos posicionales:

struct Metros(f64);
struct Kilogramos(f64);
struct Color(u8, u8, u8);  // RGB

fn main() {
    let distancia = Metros(42.5);
    let peso = Kilogramos(70.0);
    let rojo = Color(255, 0, 0);
    
    println!("Distancia: {} metros", distancia.0);
    println!("Peso: {} kg", peso.0);
    println!("Color RGB: ({}, {}, {})", rojo.0, rojo.1, rojo.2);
    
    // El sistema de tipos previene mezclar unidades
    // let suma = distancia.0 + peso.0; // No tiene sentido semánticamente
    // Aunque funcionaría numéricamente, mejor usar tipos distintos
}

Unit structs: Structs sin campos. Útiles para implementar traits sin datos:

struct Agente;
struct Contador;

impl Agente {
    fn ejecutar(&self) {
        println!("Ejecutando agente...");
    }
}

fn main() {
    let agente = Agente;
    agente.ejecutar();
    
    // Unit structs son útiles con generics y traits
    let _marcador = Contador;
}

Múltiples bloques impl

Puedes dividir la implementación en varios bloques impl. Son equivalentes a uno solo:

struct Pila<T> {
    datos: Vec<T>,
}

impl<T> Pila<T> {
    fn nueva() -> Self {
        Pila { datos: Vec::new() }
    }
    
    fn empujar(&mut self, valor: T) {
        self.datos.push(valor);
    }
    
    fn sacar(&mut self) -> Option<T> {
        self.datos.pop()
    }
}

impl<T> Pila<T> {
    fn esta_vacia(&self) -> bool {
        self.datos.is_empty()
    }
    
    fn tamanio(&self) -> usize {
        self.datos.len()
    }
    
    fn cima(&self) -> Option<&T> {
        self.datos.last()
    }
}

fn main() {
    let mut pila: Pila<i32> = Pila::nueva();
    
    pila.empujar(1);
    pila.empujar(2);
    pila.empujar(3);
    
    println!("Tamaño: {}", pila.tamanio());
    println!("Cima: {:?}", pila.cima());
    
    while let Some(valor) = pila.sacar() {
        println!("Sacado: {valor}");
    }
    
    println!("¿Vacía? {}", pila.esta_vacia());
}

Los structs son fundamentales en Rust. Se usan en conjunto con enums, que veremos en la próxima lección, para modelar todos los tipos de datos de tu aplicación con máxima expresividad y seguridad.

Shorthand de inicialización de campos
Cuando el nombre de la variable local coincide con el campo del struct, puedes usar la sintaxis abreviada: Punto { x, y } en lugar de Punto { x: x, y: y }. Esto es idéntico al shorthand de propiedades en JavaScript.
Sintaxis de actualización de struct
Puedes crear un nuevo struct basándote en otro con la sintaxis ..otro_struct: let p2 = Punto { x: 1.0, ..p }; — esto copia todos los campos de p excepto x. Los campos copiados se mueven o copian según si implementan Copy.
rust
#[derive(Debug, Clone, PartialEq)]
struct Punto {
    x: f64,
    y: f64,
}

impl Punto {
    // Función asociada (constructor)
    fn nuevo(x: f64, y: f64) -> Self {
        Punto { x, y }
    }

    fn origen() -> Self {
        Punto { x: 0.0, y: 0.0 }
    }

    // Método: toma &self (solo lectura)
    fn distancia_al_origen(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }

    // Método mutable: toma &mut self
    fn trasladar(&mut self, dx: f64, dy: f64) {
        self.x += dx;
        self.y += dy;
    }
}

fn main() {
    let mut p = Punto::nuevo(3.0, 4.0);
    println!("Punto: {p:?}");
    println!("Distancia al origen: {:.2}", p.distancia_al_origen());

    p.trasladar(1.0, -1.0);
    println!("Después de traslación: {p:?}");

    let origen = Punto::origen();
    println!("¿Es el origen? {}", p == origen);
}