Ir al contenido

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.

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ños
▶ Pruébalo tú mismo

Compá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.

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);
}
▶ Pruébalo tú mismo

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ños
▶ Pruébalo tú mismo

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);
}
▶ Pruébalo tú mismo

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).

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 }
▶ Pruébalo tú mismo

#[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.

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? false
cuadrado: área 25, ¿cuadrado? true
▶ Pruébalo tú mismo

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 centavos
Account { owner: "Ana", balance: 75 }
▶ Pruébalo tú mismo

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.

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 edad
Chen es mayor de edad
El mayor: Chen (52)
▶ Pruébalo tú mismo

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.

  • 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 con mut si vas a cambiar sus campos.
  • Añade comportamiento con un bloque impl; &self es la instancia sobre la que se llamó el método.
  • Añade #[derive(Debug)] para poder imprimir un struct con {:?}.

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