Structs en Rust
Un struct (estructura) agrupa varios valores relacionados en un solo tipo con nombre. Una tupla es un paquete rápido y sin nombres; un struct, en cambio, le da un nombre a cada campo, y eso hace que el código que lo usa sea mucho más fácil de leer.
Definir y crear un struct
Sección titulada «Definir y crear un struct»struct Person { name: String, age: u32,}
fn main() { let alice = Person { name: String::from("Alicia"), age: 30, };
println!("{} tiene {} años", alice.name, alice.age);}Salida:
Alicia tiene 30 añosCompáralo con una tupla ("Alicia", 30): con un struct, alice.name y alice.age se explican solos. No tienes que acordarte de que el campo 0 es el nombre y el campo 1 es la edad.
Hacer un struct mutable
Sección titulada «Hacer un struct mutable»Igual que las variables, las instancias de un struct son inmutables por defecto. Añade mut si quieres cambiar un campo después de crearla:
struct Person { name: String, age: u32,}
fn main() { let mut alice = Person { name: String::from("Alicia"), age: 30, };
alice.age += 1; println!("{} ahora tiene {} años", alice.name, alice.age);}Añadir comportamiento con métodos
Sección titulada «Añadir comportamiento con métodos»Puedes asociar funciones a un struct con un bloque impl. Dentro de él, &self se refiere a la instancia concreta sobre la que se llamó el método:
struct Person { name: String, age: u32,}
impl Person { fn greet(&self) { println!("Hola, soy {} y tengo {} años", self.name, self.age); }}
fn main() { let alice = Person { name: String::from("Alicia"), age: 30 }; alice.greet();}Salida:
Hola, soy Alicia y tengo 30 añosFunciones constructoras
Sección titulada «Funciones constructoras»Un patrón muy común es escribir una función new dentro del impl que construye la instancia por ti. Así, quien la usa no tiene que escribir cada campo a mano:
struct Person { name: String, age: u32,}
impl Person { fn new(name: &str, age: u32) -> Person { Person { name: name.to_string(), age } }}
fn main() { let bob = Person::new("Roberto", 25); println!("{} tiene {} años", bob.name, bob.age);}Fíjate en que age aparece solo dentro de Person { name: name.to_string(), age }. Cuando una variable se llama exactamente igual que el campo, no hace falta escribir age: age: basta con age. Rust llama a esto field init shorthand (forma abreviada de inicializar campos).
Imprimir un struct con derive
Sección titulada «Imprimir un struct con derive»Si pruebas println!("{:?}", alice) con el struct tal como lo escribimos arriba, no compila: Rust no sabe cómo mostrar un tipo propio en modo depuración a menos que se lo indiques. Añade #[derive(Debug)] encima del struct y lo tendrás gratis:
#[derive(Debug)]struct Person { name: String, age: u32,}
fn main() { let alice = Person { name: String::from("Alicia"), age: 30 }; println!("{:?}", alice);}Salida:
Person { name: "Alicia", age: 30 }#[derive(...)] es la forma que tiene Rust de generar automáticamente funcionalidad común para un tipo. Verás Debug, y más adelante Clone y otros, añadidos así todo el tiempo.
Práctica
Sección titulada «Práctica»1. Métodos de un rectángulo
Sección titulada «1. Métodos de un rectángulo»Crea un struct Rectangle con los campos width y height (u32). Dale dos métodos: area, que devuelve el área, e is_square, que devuelve true cuando los dos lados son iguales. Pista: los dos métodos solo leen los campos, así que reciben &self.
Ver solución
struct Rectangle { width: u32, height: u32,}
impl Rectangle { fn area(&self) -> u32 { self.width * self.height }
fn is_square(&self) -> bool { self.width == self.height }}
fn main() { let wide = Rectangle { width: 8, height: 3 }; let square = Rectangle { width: 5, height: 5 };
println!("ancho: área {}, ¿cuadrado? {}", wide.area(), wide.is_square()); println!("cuadrado: área {}, ¿cuadrado? {}", square.area(), square.is_square());}Salida:
ancho: área 24, ¿cuadrado? falsecuadrado: área 25, ¿cuadrado? true2. Una cuenta bancaria
Sección titulada «2. Una cuenta bancaria»Crea un struct Account con un owner (String) y un balance en centavos (u32). Escribe Account::new("Ana"), que empieza con el saldo en 0. Después deposita 50 y 25 centavos cambiando el campo, e imprime la cuenta de dos formas: con una frase normal y con {:?}. Pista: la variable tiene que ser let mut para poder cambiar account.balance, y {:?} necesita #[derive(Debug)].
Ver solución
#[derive(Debug)]struct Account { owner: String, balance: u32,}
impl Account { fn new(owner: &str) -> Account { Account { owner: owner.to_string(), balance: 0 } }}
fn main() { let mut account = Account::new("Ana"); account.balance += 50; account.balance += 25;
println!("{} tiene {} centavos", account.owner, account.balance); println!("{:?}", account);}Salida:
Ana tiene 75 centavosAccount { owner: "Ana", balance: 75 }new pone balance: 0 por su cuenta, así que quien la llama no puede olvidarlo. Esa es una gran parte de la razón por la que las funciones constructoras son tan comunes.
3. Una lista de personas
Sección titulada «3. Una lista de personas»Mete tres valores Person en un vector: Ana (34), Ben (16) y Chen (52). Imprime el nombre de todos los que tienen 18 años o más y luego quién es el mayor. Pista: recorre la lista con for person in &people y lleva la cuenta de la persona mayor hasta el momento con let mut oldest = &people[0];.
Ver solución
struct Person { name: String, age: u32,}
fn main() { let people = vec![ Person { name: String::from("Ana"), age: 34 }, Person { name: String::from("Ben"), age: 16 }, Person { name: String::from("Chen"), age: 52 }, ];
let mut oldest = &people[0]; for person in &people { if person.age >= 18 { println!("{} es mayor de edad", person.name); } if person.age > oldest.age { oldest = person; } }
println!("El mayor: {} ({})", oldest.name, oldest.age);}Salida:
Ana es mayor de edadChen es mayor de edadEl mayor: Chen (52)oldest es una referencia (&Person) a un elemento del vector, no una copia, así que no se clona nada. Cada vez que encontramos a alguien mayor, simplemente hacemos que oldest apunte a esa persona.
Resumen
Sección titulada «Resumen»- Un struct agrupa campos con nombre en un solo tipo:
struct Person { name: String, age: u32 }. - Crea una instancia con
Person { name: ..., age: ... }; márcala conmutsi vas a cambiar sus campos. - Añade comportamiento con un bloque
impl;&selfes la instancia sobre la que se llamó el método. - Añade
#[derive(Debug)]para poder imprimir un struct con{:?}.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Cuál es la principal ventaja de un struct frente a una tupla?
2. ¿Qué te permite hacer añadir #[derive(Debug)] a un struct?
3. En Person { name: name.to_string(), age }, ¿qué hace el age suelto?
Puntuación: 0 / 3
Para leer más
Sección titulada «Para leer más»- Defining and Instantiating Structs — The Rust Book (en inglés): también explica la sintaxis de actualización de structs (
..other), los tuple structs y los unit-like structs. - An Example Program Using Structs — The Rust Book (en inglés): reescribe un programa de rectángulos pasando de variables sueltas a una tupla y después a un struct, para que veas por qué ayudan los structs.
- Method Syntax — The Rust Book (en inglés):
&selffrente a&mut self, métodos con parámetros extra y funciones asociadas comonew. - Structures — Rust by Example (en inglés): ejemplos ejecutables de todos los tipos de struct, con un pequeño ejercicio al final.
- Derive — Rust by Example (en inglés): lo que
#[derive(...)]puede generar por ti, comoDebug,CloneyPartialEq.