Las tuple structs son una variante de las estructuras que carecen de nombres para sus campos, utilizando en su lugar la posición para definirlos. Los valores se acceden mediante índices numéricos (empezando desde .0). Son ideales cuando los nombres de los campos no aportan claridad adicional o cuando se busca implementar el Newtype pattern. Este último es una técnica fundamental para la seguridad de tipos: al envolver un tipo existente (como un f64) en una nueva estructura (como Metros(f64)), creamos tipos distintos que el compilador no confundirá, evitando errores lógicos como sumar accidentalmente kilogramos con metros.

Es vital no confundir una tupla común (T, U) con una tuple struct. La tupla es un tipo anónimo, mientras que la tuple struct define un tipo nominal único en el sistema de tipos de Rust, permitiendo implementar traits específicos para ella.

Las unit structs son estructuras que no contienen campos. Al no tener datos, son tipos de tamaño cero (Zero-Sized Types o ZST), lo que significa que no ocupan memoria en tiempo de ejecución. Se utilizan principalmente para implementar traits en tipos que actúan como marcadores (markers), para representar estados en una máquina de estados, o para cumplir con una interfaz de un trait sin necesidad de almacenar datos internos.

Finalmente, el patrón Newtype es esencial cuando necesitamos implementar un trait de la biblioteca estándar (std) en un tipo que ya está definido por otro módulo o biblioteca externa. Dado que Rust no permite la implementación de traits en tipos externos (regla de la coherencia), envolvemos el tipo externo en una tuple struct propia para ganar control sobre su comportamiento.

// 1. Tuple Struct: Usada para agrupaciones simples sin nombres de campos
struct Color(u8, u8, u8);

// 2. Newtype Pattern: Diferencia tipos semánticamente distintos
struct Metros(f64);
struct Kilos(f64);

// 3. Unit Struct: Actúa como un marcador (no ocupa memoria)
struct Marcador;

trait Saludar {
    fn saludar(&self);
}

impl Saludar for Marcador {
    fn saludar(&self) {
        println!("Hola desde una unit struct!");
    }
}

// 4. Newtype Pattern para implementar traits externos en tipos externos
use std::fmt;

struct MiVec<T>(Vec<T>);

impl<T: fmt::Display> fmt::Display for MiVec<T> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Contenido: {:?}", self.0)
    }
}

fn main() {
    // Uso de Color (acceso por índice)
    let rojo = Color(255, 0, 0);
    println!("Rojo: {}, {}, {}", rojo.0, rojo.1, rojo.2);

    // Newtype: Prevención de errores de tipo
    let distancia = Metros(10.5);
    let peso = Kilos(5.0);
    // let error = distancia + peso; // Esto fallaría al compilar
    println!("Distancia: {}m, Peso: {}kg", distancia.0, peso.0);

    // Uso de Unit Struct
    let m = Marcador;
    m.saludar();

    // Uso de Newtype para extender comportamiento
    let mi_lista = MiVec(vec![1, 2, 3]);
    println!("{}", mi_lista);
}

Explicación del Código

  • struct Color(u8, u8, u8): Define una tuple struct donde los componentes se acceden mediante rojo.0, rojo.1 y rojo.2.
  • struct Metros(f64) y struct Kilos(f64): Implementan el Newtype pattern. Aunque ambos contienen un f64, distancia (de tipo Metros) y peso (de tipo Kilos) son tipos incompatibles para el compilador, evitando errores de mezcla de unidades.
  • struct Marcador: Es una unit struct. No almacena datos y se usa para implementar el trait Saludar. En main, m es una instancia que no consume memoria.
  • struct MiVec<T>(Vec<T>): Es un envoltorio (wrapper) utilizado para el Newtype pattern. Permite implementar el trait fmt::Display sobre un Vec<T> (un tipo de la librería estándar), algo que no sería posible directamente debido a las reglas de coherencia de Rust.
  • self.0 dentro de la implementación de fmt::Display: Accede al campo interno del MiVec para poder imprimir el contenido del Vec original.
  • println!("{}", mi_lista): Llama a la implementación personalizada de Display que definimos para MiVec.

36