Definir structs: campos, tuplas y unidades
Las tres formas de un struct en Rust —campos nombrados, tuple structs y unit structs—, la sintaxis de construcción, el atajo de campos y la actualización con ..otro, vistas como tipos producto.
Un struct es la herramienta con la que Rust te deja nombrar y agrupar datos que van juntos. Pero bajo la sintaxis hay una idea de teoría de tipos: un struct es un tipo producto, la combinación de sus campos. Entender eso —y las tres formas que adopta— es el primer ladrillo del sistema de tipos que sostiene todo Rust.
- Definir structs con campos nombrados y construir instancias.
- Distinguir tuple structs y unit structs, y saber cuándo usar cada uno.
- Dominar el atajo de campos y la sintaxis de actualización
..otro. - Ver el struct como tipo producto y el patrón newtype como abstracción de coste cero.
Structs con campos nombrados
La forma canónica agrupa campos con nombre y tipo:
struct Usuario {
nombre: String,
email: String,
activo: bool,
inicios_sesion: u64,
}
let u = Usuario {
nombre: String::from("Ada"),
email: String::from("ada@example.com"),
activo: true,
inicios_sesion: 0,
};
println!("{}", u.email);
El nombre de cada campo hace la construcción independiente del orden: da igual en qué secuencia escribas los campos, porque los emparejas por nombre, no por posición. Para mutar un campo, la instancia entera debe ser mut —Rust no permite marcar campos individuales como mutables; la mutabilidad es una propiedad del binding, no del dato—:
let mut u = Usuario { /* ... */ };
u.activo = false; // requiere que 'u' sea 'mut'
El atajo de campos y la actualización
Cuando una variable local ya se llama igual que el campo, el field init shorthand elimina la repetición:
fn construir(nombre: String, email: String) -> Usuario {
Usuario {
nombre, // en vez de 'nombre: nombre'
email, // en vez de 'email: email'
activo: true,
inicios_sesion: 0,
}
}
Y para crear una instancia que es casi igual a otra, la sintaxis de actualización ..otro toma el resto de campos de una instancia existente:
let u2 = Usuario {
email: String::from("ada2@example.com"),
..u // el resto de campos vienen de 'u'
};
..otro mueve los campos que no sean Copy. Tras ..u, el campo nombre (un String) se ha movido a u2, y ya no puedes usar u.nombre —aunque sí seguirás pudiendo usar los campos Copy como activo—. La sintaxis de actualización no es una copia superficial mágica: obedece exactamente las reglas de ownership que dominarás en el nivel 8.
Tuple structs: el patrón newtype
Cuando los nombres de campo no aportan nada pero sí quieres un tipo nuevo y distinto, usa un tuple struct: campos por posición, accesibles con .0, .1:
struct Punto(f64, f64);
struct Metros(f64);
struct Segundos(f64);
let p = Punto(3.0, 4.0);
println!("{}", p.0); // 3.0
Metros y Segundos envuelven el mismo f64, pero son tipos incompatibles: pasar segundos donde se esperan metros es un error de compilación. Esto es el newtype pattern, y no cuesta ni un byte ni un ciclo en tiempo de ejecución.
Unit structs: tipos de tamaño cero
Un struct sin campos —un unit struct— no ocupa memoria: es un zero-sized type (ZST). Sirve como marcador o para colgar comportamiento de algo que no lleva datos:
struct SiempreValido;
let _ = SiempreValido; // no reserva ni un byte
En teoría de tipos, struct Usuario { activo: bool, rol: Rol } es el producto de sus campos: el conjunto de sus valores posibles es el producto cartesiano bool × Rol. Por eso se llama tipo producto. Su hermano, el enum (nivel 6), es el tipo suma: uno u otro. Juntos forman los tipos algebraicos de datos, y con ellos modelas un dominio haciendo que los estados imposibles sean, literalmente, imposibles de escribir. Cuando eliges qué campos lleva un struct no estás “guardando variables”: estás definiendo el espacio de estados legales de tu programa. El programador experto piensa el tipo primero —qué combinaciones deben existir y cuáles no— y deja que el compilador imponga esa álgebra. El newtype (Metros(f64)) es el caso mínimo de esta idea: un producto de un solo factor que, aun así, levanta una frontera de tipo que el compilador vigila gratis, sin coste en memoria ni en velocidad.
- Define un struct con campos nombrados para un
Libro(título, autor, páginas) y construye una instancia. - Usa el atajo de campos en una función que reciba
tituloyautory devuelva unLibro. - Crea un segundo libro con la sintaxis de actualización
..otro, cambiando solo las páginas. Observa qué campos se mueven y cuáles no. - Envuelve un
u32en dos tuple structs distintos (IdyCantidad) y comprueba que el compilador no los deja intercambiar. - Declara un unit struct y confirma con
std::mem::size_ofque su tamaño es 0.