Rust
Tutorial Rust: Estructuras (Structs)
Aprende a usar estructuras en Rust: definición, métodos, tuple structs y unit structs para modelar datos eficientemente.
Aprende Rust y certifícateDefinición y campos
Las estructuras (o structs) en Rust son una herramienta fundamental para crear tipos de datos personalizados que agrupan valores relacionados bajo un único nombre. A diferencia de las colecciones estándar como vectores o hashmaps, las estructuras nos permiten definir exactamente qué campos contendrá nuestro tipo y darles nombres significativos.
Declaración de una estructura
Para definir una estructura en Rust, utilizamos la palabra clave struct
seguida del nombre que queremos darle (por convención en PascalCase) y los campos que contendrá entre llaves:
struct Persona {
nombre: String,
edad: u32,
activo: bool,
}
En este ejemplo, hemos creado una estructura llamada Persona
con tres campos:
nombre
: de tipoString
edad
: de tipou32
(entero sin signo de 32 bits)activo
: de tipobool
(booleano)
Cada campo tiene un nombre y un tipo específico, separados por dos puntos. Los campos se separan entre sí mediante comas.
Creación de instancias
Una vez definida la estructura, podemos crear instancias específicas asignando valores a cada uno de sus campos:
fn main() {
let usuario = Persona {
nombre: String::from("Ana García"),
edad: 28,
activo: true,
};
// Podemos acceder a los campos usando la notación de punto
println!("Nombre: {}", usuario.nombre);
println!("Edad: {}", usuario.edad);
println!("¿Activo?: {}", usuario.activo);
}
Al crear una instancia, debemos proporcionar valores para todos los campos de la estructura. Rust no permite crear instancias con campos faltantes.
Acceso y modificación de campos
Para acceder a los campos de una estructura, utilizamos la notación de punto (instancia.campo
). Si la instancia es mutable, también podemos modificar sus campos:
fn main() {
let mut usuario = Persona {
nombre: String::from("Ana García"),
edad: 28,
activo: true,
};
// Modificamos la edad
usuario.edad = 29;
println!("Edad actualizada: {}", usuario.edad);
}
Es importante destacar que para modificar cualquier campo, la instancia completa debe ser declarada como mutable usando mut
.
Inicialización abreviada de campos
Cuando tenemos variables con el mismo nombre que los campos de la estructura, Rust nos permite usar una sintaxis abreviada para inicializar esos campos:
fn crear_usuario(nombre: String, edad: u32) -> Persona {
// En lugar de escribir nombre: nombre, edad: edad
Persona {
nombre,
edad,
activo: true,
}
}
Esta sintaxis hace que el código sea más conciso y legible cuando los nombres coinciden.
Actualización basada en otra instancia
Rust proporciona una sintaxis de actualización de estructuras que permite crear una nueva instancia a partir de otra, cambiando solo algunos campos:
fn main() {
let usuario1 = Persona {
nombre: String::from("Ana García"),
edad: 28,
activo: true,
};
// Creamos usuario2 basado en usuario1, cambiando solo el nombre
let usuario2 = Persona {
nombre: String::from("Carlos López"),
..usuario1 // Copia el resto de campos de usuario1
};
println!("Usuario 2 - Nombre: {}, Edad: {}", usuario2.nombre, usuario2.edad);
}
La sintaxis ..usuario1
indica a Rust que debe copiar los valores de los campos restantes desde usuario1
. Esta característica es especialmente útil cuando queremos crear una nueva instancia que difiere solo en algunos campos.
Estructuras en funciones
Las estructuras pueden utilizarse como parámetros y valores de retorno en funciones:
fn cumplir_anios(persona: &mut Persona) {
persona.edad += 1;
}
fn es_mayor_de_edad(persona: &Persona) -> bool {
persona.edad >= 18
}
fn main() {
let mut usuario = Persona {
nombre: String::from("Ana García"),
edad: 17,
activo: true,
};
println!("¿Es mayor de edad?: {}", es_mayor_de_edad(&usuario));
cumplir_anios(&mut usuario);
println!("Después de cumplir años: {}", usuario.edad);
println!("¿Es mayor de edad ahora?: {}", es_mayor_de_edad(&usuario));
}
En este ejemplo, pasamos referencias a la estructura para evitar transferir la propiedad. Usamos &mut Persona
cuando necesitamos modificar la estructura y &Persona
cuando solo necesitamos leerla.
Visibilidad de campos
Por defecto, los campos de una estructura son privados fuera del módulo donde se define la estructura. Podemos hacer que los campos sean públicos usando la palabra clave pub
:
pub struct Rectangulo {
pub ancho: f64,
pub alto: f64,
}
En este caso, tanto la estructura Rectangulo
como sus campos ancho
y alto
son públicos y pueden ser accedidos desde otros módulos.
Estructuras sin campos nombrados
Rust también permite definir estructuras sin especificar nombres para sus campos, pero esto se verá en la sección de "Tuple y unit structs".
Las estructuras son una herramienta poderosa para modelar datos en Rust, permitiéndonos crear tipos personalizados que reflejan exactamente la información que necesitamos manejar en nuestros programas. En la siguiente sección, veremos cómo añadir comportamiento a nuestras estructuras mediante la implementación de métodos.
Métodos con impl
Las estructuras en Rust no solo sirven para agrupar datos relacionados, sino que también pueden tener comportamiento asociado a través de métodos. Los métodos son funciones que operan en el contexto de una estructura, permitiéndonos encapsular la lógica relacionada con nuestros datos.
Para añadir métodos a una estructura, utilizamos bloques impl
(abreviatura de "implementation"). Esta sintaxis separa claramente la definición de datos (la estructura) de su comportamiento (los métodos).
Definiendo métodos con impl
La sintaxis básica para implementar métodos en una estructura es la siguiente:
struct Rectangulo {
ancho: f64,
alto: f64,
}
impl Rectangulo {
// Método que calcula el área
fn area(&self) -> f64 {
self.ancho * self.alto
}
}
fn main() {
let rect = Rectangulo {
ancho: 5.0,
alto: 3.0,
};
println!("El área del rectángulo es: {}", rect.area());
}
En este ejemplo, hemos definido un método area()
para nuestra estructura Rectangulo
. Observa cómo:
- El método se define dentro de un bloque
impl Rectangulo { ... }
- El primer parámetro es
&self
, que representa una referencia a la instancia en la que se llama el método - Llamamos al método usando la notación de punto:
rect.area()
El parámetro self
El primer parámetro de un método siempre representa la instancia sobre la que se llama el método. Existen tres variantes principales:
&self
: Toma una referencia inmutable a la instancia. Útil cuando solo necesitamos leer datos.
fn perimetro(&self) -> f64 {
2.0 * (self.ancho + self.alto)
}
&mut self
: Toma una referencia mutable a la instancia. Necesario cuando queremos modificar la estructura.
fn duplicar_tamanio(&mut self) {
self.ancho *= 2.0;
self.alto *= 2.0;
}
self
: Toma posesión (ownership) de la instancia. Útil cuando el método consume la estructura.
fn convertir_a_cadena(self) -> String {
format!("Rectangulo de {}x{}", self.ancho, self.alto)
}
Ejemplo completo con diferentes tipos de self
Veamos un ejemplo que utiliza las tres variantes:
struct Rectangulo {
ancho: f64,
alto: f64,
}
impl Rectangulo {
// Método con &self (referencia inmutable)
fn area(&self) -> f64 {
self.ancho * self.alto
}
// Método con &mut self (referencia mutable)
fn escalar(&mut self, factor: f64) {
self.ancho *= factor;
self.alto *= factor;
}
// Método con self (toma posesión)
fn convertir_a_tupla(self) -> (f64, f64) {
(self.ancho, self.alto)
}
}
fn main() {
let mut rect = Rectangulo {
ancho: 5.0,
alto: 3.0,
};
// Usando método con &self
println!("Área: {}", rect.area());
// Usando método con &mut self
rect.escalar(2.0);
println!("Área después de escalar: {}", rect.area());
// Usando método con self (consume rect)
let dimensiones = rect.convertir_a_tupla();
println!("Dimensiones: ({}, {})", dimensiones.0, dimensiones.1);
// Después de llamar a convertir_a_tupla, rect ya no es utilizable
// El siguiente código causaría un error:
// println!("Área: {}", rect.area());
}
Es importante entender que cuando usamos self
(sin referencia), el método toma posesión de la instancia, lo que significa que después de llamar a ese método, la instancia original ya no se puede utilizar.
Funciones asociadas
Además de los métodos que operan sobre una instancia, también podemos definir funciones asociadas dentro de los bloques impl
. Estas funciones no toman self
como parámetro y se llaman usando el operador ::
en lugar de la notación de punto.
Las funciones asociadas son útiles para crear constructores u otras funciones relacionadas con la estructura pero que no necesitan una instancia:
struct Rectangulo {
ancho: f64,
alto: f64,
}
impl Rectangulo {
// Función asociada (no toma self)
fn new(ancho: f64, alto: f64) -> Rectangulo {
Rectangulo { ancho, alto }
}
// Función asociada para crear un cuadrado
fn cuadrado(lado: f64) -> Rectangulo {
Rectangulo {
ancho: lado,
alto: lado,
}
}
// Método normal
fn area(&self) -> f64 {
self.ancho * self.alto
}
}
fn main() {
// Llamamos a las funciones asociadas con ::
let rect = Rectangulo::new(5.0, 3.0);
let cuadrado = Rectangulo::cuadrado(4.0);
println!("Área del rectángulo: {}", rect.area());
println!("Área del cuadrado: {}", cuadrado.area());
}
En este ejemplo, new
y cuadrado
son funciones asociadas que crean instancias de Rectangulo
. La convención en Rust es usar new
como nombre para el constructor principal, aunque no es obligatorio.
Múltiples bloques impl
Podemos tener múltiples bloques impl
para una misma estructura. Esto es útil para organizar el código, especialmente en proyectos grandes:
struct Circulo {
radio: f64,
}
// Métodos básicos
impl Circulo {
fn new(radio: f64) -> Circulo {
Circulo { radio }
}
fn area(&self) -> f64 {
std::f64::consts::PI * self.radio * self.radio
}
}
// Métodos adicionales
impl Circulo {
fn perimetro(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radio
}
fn escalar(&mut self, factor: f64) {
self.radio *= factor;
}
}
fn main() {
let mut c = Circulo::new(2.0);
println!("Área: {}", c.area());
println!("Perímetro: {}", c.perimetro());
c.escalar(1.5);
println!("Área después de escalar: {}", c.area());
}
Esta capacidad de separar la implementación en múltiples bloques impl
facilita la organización del código y permite añadir métodos a una estructura de forma incremental.
Métodos con parámetros adicionales
Los métodos pueden tomar parámetros adicionales después de self
:
struct Punto {
x: f64,
y: f64,
}
impl Punto {
fn new(x: f64, y: f64) -> Punto {
Punto { x, y }
}
fn distancia_a(&self, otro: &Punto) -> f64 {
let dx = self.x - otro.x;
let dy = self.y - otro.y;
(dx * dx + dy * dy).sqrt()
}
fn mover(&mut self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
}
fn main() {
let p1 = Punto::new(0.0, 0.0);
let p2 = Punto::new(3.0, 4.0);
println!("Distancia: {}", p1.distancia_a(&p2));
let mut p3 = Punto::new(1.0, 1.0);
p3.mover(2.0, 3.0);
println!("Punto movido: ({}, {})", p3.x, p3.y);
}
En este ejemplo, distancia_a
toma &self
y un parámetro adicional otro
que es una referencia a otro Punto
. El método mover
toma &mut self
y dos parámetros adicionales que indican cuánto mover el punto.
Encapsulación con métodos
Los métodos nos permiten encapsular la lógica relacionada con nuestras estructuras, proporcionando una interfaz clara para interactuar con ellas:
struct Contador {
valor: u32,
paso: u32,
maximo: u32,
}
impl Contador {
fn new(inicio: u32, paso: u32, maximo: u32) -> Contador {
Contador {
valor: inicio,
paso,
maximo,
}
}
fn incrementar(&mut self) -> bool {
if self.valor + self.paso <= self.maximo {
self.valor += self.paso;
true
} else {
false
}
}
fn valor_actual(&self) -> u32 {
self.valor
}
fn reiniciar(&mut self) {
self.valor = 0;
}
}
fn main() {
let mut contador = Contador::new(0, 5, 20);
while contador.incrementar() {
println!("Valor: {}", contador.valor_actual());
}
println!("Llegamos al máximo. Reiniciando...");
contador.reiniciar();
println!("Valor después de reiniciar: {}", contador.valor_actual());
}
En este ejemplo, la estructura Contador
encapsula la lógica de un contador que se incrementa en pasos hasta un valor máximo. Los métodos proporcionan una interfaz clara para interactuar con el contador, ocultando los detalles de implementación.
Los métodos con impl
son una parte fundamental de la programación en Rust, ya que nos permiten combinar datos y comportamiento de manera organizada y segura, siguiendo principios de buena programación orientada a objetos sin necesidad de herencia o polimorfismo tradicional.
Tuple y unit structs
Además de las estructuras con campos nombrados que hemos visto hasta ahora, Rust ofrece dos variantes adicionales de estructuras que resultan útiles en situaciones específicas: las tuple structs (estructuras de tupla) y las unit structs (estructuras unitarias). Estas variantes proporcionan flexibilidad adicional para modelar nuestros datos según las necesidades concretas de nuestro programa.
Tuple structs
Las tuple structs son estructuras que contienen campos sin nombres, identificados únicamente por su posición. Combinan características de las tuplas y las estructuras tradicionales:
struct Color(i32, i32, i32);
struct Punto(f64, f64, f64);
fn main() {
let negro = Color(0, 0, 0);
let origen = Punto(0.0, 0.0, 0.0);
// Accedemos a los campos por índice, como en las tuplas
println!("Color RGB: ({}, {}, {})", negro.0, negro.1, negro.2);
println!("Coordenadas: ({}, {}, {})", origen.0, origen.1, origen.2);
}
Como puedes ver, definimos una tuple struct usando la palabra clave struct
, seguida del nombre y una lista de tipos entre paréntesis. Para acceder a los campos, utilizamos la notación de índice (.0
, .1
, etc.), igual que con las tuplas normales.
Casos de uso para tuple structs
Las tuple structs son especialmente útiles en los siguientes escenarios:
- Cuando los nombres de los campos son obvios por el contexto y darles nombres explícitos sería redundante:
struct Centimetros(f64);
struct Kilogramos(f64);
fn main() {
let longitud = Centimetros(182.5);
let peso = Kilogramos(75.3);
println!("Longitud: {} cm", longitud.0);
println!("Peso: {} kg", peso.0);
}
- Para crear tipos distintos que tengan la misma estructura interna pero representen conceptos diferentes:
// Aunque ambos son f64, el sistema de tipos los trata como tipos diferentes
struct Celsius(f64);
struct Fahrenheit(f64);
fn convertir_a_fahrenheit(temp: Celsius) -> Fahrenheit {
Fahrenheit(temp.0 * 9.0/5.0 + 32.0)
}
fn main() {
let temp_c = Celsius(25.0);
let temp_f = convertir_a_fahrenheit(temp_c);
println!("{}°C = {}°F", temp_c.0, temp_f.0);
}
Este enfoque nos ayuda a evitar errores lógicos, ya que el compilador no permitirá mezclar accidentalmente valores de Celsius
y Fahrenheit
, aunque ambos contengan un f64
.
Desestructuración de tuple structs
Al igual que las tuplas normales, las tuple structs pueden desestructurarse para extraer sus componentes:
struct RGB(u8, u8, u8);
fn main() {
let color = RGB(255, 160, 0); // Naranja
// Desestructuración
let RGB(rojo, verde, azul) = color;
println!("Componentes RGB: rojo={}, verde={}, azul={}", rojo, verde, azul);
}
También podemos usar patrones para extraer solo los componentes que necesitamos:
struct Punto3D(f64, f64, f64);
fn main() {
let punto = Punto3D(1.0, 2.0, 3.0);
// Extraemos solo las coordenadas x e y
let Punto3D(x, y, _) = punto;
println!("Coordenadas en el plano XY: ({}, {})", x, y);
}
Implementando métodos para tuple structs
Las tuple structs también pueden tener métodos, definidos de la misma manera que para las estructuras con campos nombrados:
struct Vector2D(f64, f64);
impl Vector2D {
fn longitud(&self) -> f64 {
(self.0 * self.0 + self.1 * self.1).sqrt()
}
fn normalizar(&self) -> Vector2D {
let len = self.longitud();
Vector2D(self.0 / len, self.1 / len)
}
}
fn main() {
let v = Vector2D(3.0, 4.0);
println!("Longitud del vector: {}", v.longitud());
let v_norm = v.normalizar();
println!("Vector normalizado: ({}, {})", v_norm.0, v_norm.1);
println!("Longitud del vector normalizado: {}", v_norm.longitud());
}
Unit structs
Las unit structs son estructuras que no tienen campos. Pueden parecer inútiles a primera vista, pero tienen aplicaciones prácticas importantes:
struct Saludo;
fn main() {
let saludo = Saludo;
println!("¡He creado una instancia de una unit struct!");
}
Una unit struct se define simplemente con la palabra clave struct
seguida del nombre y un punto y coma.
Casos de uso para unit structs
Las unit structs son útiles principalmente en los siguientes escenarios:
- Como marcadores de tipos cuando necesitamos un tipo distinto pero no necesitamos almacenar datos:
struct InactividadUsuario;
struct SesionExpirada;
fn manejar_error(error: InactividadUsuario) {
println!("El usuario ha estado inactivo demasiado tiempo");
}
fn otro_manejador(error: SesionExpirada) {
println!("La sesión ha expirado");
}
fn main() {
let error = InactividadUsuario;
manejar_error(error);
}
- Para implementar comportamiento sin necesidad de estado:
struct Matematicas;
impl Matematicas {
fn pi() -> f64 {
3.14159265359
}
fn e() -> f64 {
2.71828182846
}
}
fn main() {
// Llamamos a las funciones asociadas sin necesidad de crear una instancia
println!("Pi: {}", Matematicas::pi());
println!("e: {}", Matematicas::e());
}
En este ejemplo, Matematicas
actúa como un espacio de nombres para agrupar funciones relacionadas, similar a las clases estáticas en otros lenguajes.
- Como marcadores en patrones de diseño avanzados:
struct Singleton;
impl Singleton {
fn instancia() -> &'static Singleton {
static INSTANCIA: Singleton = Singleton;
&INSTANCIA
}
fn hacer_algo(&self) {
println!("Singleton haciendo algo...");
}
}
fn main() {
let s1 = Singleton::instancia();
let s2 = Singleton::instancia();
// Ambas variables apuntan a la misma instancia
s1.hacer_algo();
s2.hacer_algo();
}
Comparación entre los tipos de estructuras
Para entender mejor cuándo usar cada tipo de estructura, veamos una comparación:
- Estructuras con campos nombrados: Ideales cuando tienes múltiples campos que necesitan nombres descriptivos.
struct Persona {
nombre: String,
edad: u32,
}
- Tuple structs: Útiles cuando los nombres de los campos serían redundantes o cuando quieres crear un tipo distinto basado en tipos existentes.
struct Coordenada(f64, f64);
- Unit structs: Perfectas cuando necesitas un tipo único pero no necesitas almacenar datos.
struct NoHayResultados;
Combinando diferentes tipos de estructuras
En un programa real, es común utilizar diferentes tipos de estructuras según las necesidades:
// Estructura con campos nombrados
struct Usuario {
id: u64,
nombre: String,
activo: bool,
}
// Tuple struct para representar credenciales
struct Credenciales(String, String); // (usuario, contraseña)
// Unit struct para representar estados
struct NoAutenticado;
struct Autenticado;
impl Usuario {
fn new(id: u64, nombre: String) -> Usuario {
Usuario {
id,
nombre,
activo: true,
}
}
fn autenticar(&self, creds: Credenciales) -> Result<Autenticado, NoAutenticado> {
// Simulamos verificación de credenciales
if creds.0 == "admin" && creds.1 == "password123" {
Ok(Autenticado)
} else {
Err(NoAutenticado)
}
}
}
fn main() {
let usuario = Usuario::new(1, String::from("Carlos"));
let credenciales = Credenciales(String::from("admin"), String::from("password123"));
match usuario.autenticar(credenciales) {
Ok(_) => println!("Usuario autenticado correctamente"),
Err(_) => println!("Credenciales incorrectas"),
}
}
En este ejemplo, utilizamos:
- Una estructura con campos nombrados para
Usuario
- Una tuple struct para
Credenciales
- Unit structs para representar los estados
Autenticado
yNoAutenticado
Cada tipo de estructura se utiliza según su idoneidad para el caso específico, lo que demuestra la flexibilidad que Rust ofrece para modelar nuestros datos.
Las tuple structs y unit structs complementan a las estructuras tradicionales, proporcionando herramientas adicionales para expresar nuestras intenciones de forma clara y concisa en el código. Elegir el tipo adecuado de estructura para cada situación nos ayuda a escribir código más legible, mantenible y seguro.
Otras lecciones de Rust
Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Rust
Introducción Y Entorno
Primer Programa
Introducción Y Entorno
Instalación Del Entorno
Introducción Y Entorno
Funciones
Sintaxis
Operadores
Sintaxis
Estructuras De Control Condicional
Sintaxis
Arrays Y Strings
Sintaxis
Manejo De Errores Panic
Sintaxis
Variables Y Tipos Básicos
Sintaxis
Estructuras De Control Iterativo
Sintaxis
Colecciones Estándar
Estructuras De Datos
Option Y Result
Estructuras De Datos
Pattern Matching
Estructuras De Datos
Estructuras (Structs)
Estructuras De Datos
Enumeraciones Enums
Estructuras De Datos
El Concepto De Ownership
Ownership
Lifetimes Básicos
Ownership
Slices Y Referencias Parciales
Ownership
References Y Borrowing
Ownership
Funciones Anónimas Closures
Abstracción
Traits De La Biblioteca Estándar
Abstracción
Traits
Abstracción
Generics
Abstracción
Channels Y Paso De Mensajes
Concurrencia
Memoria Compartida Segura
Concurrencia
Threads Y Sincronización Básica
Concurrencia
Introducción A Tokio
Asincronía
Fundamentos Asíncronos Y Futures
Asincronía
Async/await
Asincronía
Ejercicios de programación de Rust
Evalúa tus conocimientos de esta lección Estructuras (Structs) con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender qué son las estructuras (structs) y cómo definirlas en Rust.
- Aprender a crear y modificar instancias de estructuras.
- Implementar métodos y funciones asociadas usando bloques impl.
- Diferenciar entre estructuras con campos nombrados, tuple structs y unit structs.
- Aplicar la encapsulación y organizar el código mediante métodos y visibilidad de campos.