En esta página
Structs: tipos de datos personalizados
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.
#[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);
}
Inicia sesión para guardar tu progreso