Ir al contenido

5. Structs, enums y pattern matching

Ejemplo completo: examples/l05_structs_enums.rscargo run --example l05_structs_enums.

Los datos y el comportamiento se declaran por separado

Sección titulada «Los datos y el comportamiento se declaran por separado»

Una clase de C# o Java agrupa campos, constructores y métodos en un solo bloque. Rust los separa: un struct declara los datos, y uno o varios bloques impl añaden las funciones.

#[derive(Debug, Clone, PartialEq)]
struct Account {
owner: String,
balance_cents: i64,
}
impl Account {
fn new(owner: &str) -> Self {
Self { owner: owner.to_string(), balance_cents: 0 }
}
fn balance(&self) -> f64 {
self.balance_cents as f64 / 100.0
}
fn deposit(&mut self, cents: i64) {
self.balance_cents += cents;
}
fn close(self) -> i64 {
self.balance_cents
}
}
Rust C# Java
struct + impl class / record class / record
fn new(…) -> Self (una convención) constructor constructor
fn balance(&self) método de instancia método de instancia
fn deposit(&mut self, …) método que modifica this método que modifica this
fn close(self) — (sin equivalente)
fn new(…) sin self método static método static
#[derive(Debug, Clone, PartialEq)] lo que genera un record: ToString, copia, igualdad por valor lo que genera un record
  • No hay constructores: Account::new es una función asociada normal que devuelve Self. Varios «constructores» no son más que varias funciones (new, with_capacity, from_str…).
  • El receptor indica qué hace el método con el valor: leerlo (&self), modificarlo (&mut self) o consumirlo (self) — después de account.close(), account se ha movido y no se puede usar, que es como Rust modela «este objeto ha terminado».
  • No hay herencia entre structs. El comportamiento compartido va en los traits (lección 7).
let mut account = Account::new("Ada");
account.deposit(1_250);
let copy = account.clone();
println!("{account:?}");
println!("balance = {:.2}, equal to copy: {}", account.balance(), account == copy);
Account { owner: "Ada", balance_cents: 1250 }
balance = 12.50, equal to copy: true

Hay que inicializar todos los campos — no existen null por defecto:

error[E0063]: missing field `balance_cents` in initializer of `Account`
--> e05_missing_field.rs:7:19
|
7 | let account = Account { owner: String::from("Ada") };
| ^^^^^^^ missing `balance_cents`

La sintaxis de actualización de structs (struct update syntax) copia los campos restantes de otro valor, como una expresión with de C#:

let other = Account { owner: "Grace".into(), ..copy };
println!("{} has {} cents", other.owner, other.balance_cents); // Grace has 1250 cents

Un struct puede tener campos sin nombre. Con un único campo, crea un tipo distinto alrededor de uno existente — un newtype:

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Meters(f64);
let short = Meters(3.5);
let long = Meters(10.0);
println!("{short:?} < {long:?}: {}", short < long); // Meters(3.5) < Meters(10.0): true

Una función que recibe Meters no aceptará un f64 sin más ni un Feet(f64). No cuesta nada en tiempo de ejecución: en memoria, Meters es exactamente un f64.

Los enums de C# son enteros con nombre; los enums de Java son un conjunto fijo de objetos que comparten todos los mismos campos. Un enum de Rust es una unión etiquetada (tagged union): cada variante puede contener datos diferentes.

enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle(f64, f64, f64),
}

El equivalente más cercano es una jerarquía cerrada:

// C#
abstract record Shape;
record Circle(double Radius) : Shape;
record Rectangle(double Width, double Height) : Shape;
record Triangle(double A, double B, double C) : Shape;
// Java 21
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double a, double b, double c) implements Shape {}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle(a, b, c) => {
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
}

Olvida una variante y el programa no compila:

error[E0004]: non-exhaustive patterns: `&Shape::Triangle(_, _, _)` not covered
--> e05_nonexhaustive.rs:8:11
|
8 | match shape {
| ^^^^^ pattern `&Shape::Triangle(_, _, _)` not covered
|
note: `Shape` defined here
--> e05_nonexhaustive.rs:1:6
|
1 | enum Shape {
| ^^^^^
...
4 | Triangle(f64, f64, f64),
| -------- not covered
Cuando falta un caso Resultado
expresión switch de C# advertencia CS8509, y luego SwitchExpressionException en tiempo de ejecución
switch de Java 21 sobre una interfaz sellada error de compilación
match de Rust error de compilación E0004

Esto es lo que hace que los enums sean tan útiles para refactorizar: añade una variante y el compilador enumera cada lugar que debe tratarla.

Los patrones desestructuran valores y se pueden refinar:

match command {
Command::Move { dx, dy: 0 } => println!("horizontal move by {dx}"), // literal dentro de un patrón
Command::Move { dx, dy } => println!("move by ({dx}, {dy})"),
Command::Say(text) => println!("say {text:?}"),
Command::Quit => println!("quit"),
}
horizontal move by 3
move by (1, -2)
say "hi"
quit

Rangos, guardas (if) y el comodín _:

fn describe(temperature: i32) -> &'static str {
match temperature {
i32::MIN..=0 => "freezing",
1..=15 => "cold",
t if t > 30 => "hot",
_ => "mild",
}
}

Los brazos se prueban en orden, así que pon primero los más específicos — la misma regla que en los patrones de switch de C# y Java.

Cuando solo importa un patrón, if let evita un match completo:

if let Some(Shape::Circle { radius }) = shapes.first() {
println!("first shape is a circle of radius {radius}");
}

let … else enlaza un patrón o sale del bloque actual — perfecto para las cláusulas de guarda:

fn parse_port(text: &str) -> u16 {
let Ok(port) = text.parse::<u16>() else {
return 8080;
};
port
}
// parse_port("3000") == 3000, parse_port("oops") == 8080

Some y Ok son a su vez variantes de enums — el tema de la próxima lección.

  • struct contiene los datos, impl añade funciones asociadas y métodos; new es una convención, no un constructor.
  • &self, &mut self y self indican si un método lee, modifica o consume el valor.
  • Las variantes de un enum llevan datos distintos: un enum de Rust es una jerarquía sellada de records en una sola declaración.
  • match debe ser exhaustivo; if let y let … else cubren los casos de un solo patrón.
  1. Traduce esta jerarquía de C# a un enum de Rust y escribe fn describe(payment: &Payment) -> String, que devuelva "card ending 4242", "transfer from FR76…" o "cash".
abstract record Payment;
record Card(string Last4) : Payment;
record Transfer(string Iban) : Payment;
record Cash : Payment;
Solución
enum Payment {
Card { last4: String },
Transfer { iban: String },
Cash,
}
fn describe(payment: &Payment) -> String {
match payment {
Payment::Card { last4 } => format!("card ending {last4}"),
Payment::Transfer { iban } => format!("transfer from {iban}"),
Payment::Cash => String::from("cash"),
}
}
  1. Añades Crypto { wallet: String } a Payment. ¿Qué le pasa a describe y por qué es mejor que el comportamiento de C#?
Solución

describe deja de compilar con E0004: non-exhaustive patterns: &Payment::Crypto { .. } not covered. Cada match que debe tratar el nuevo caso se señala en tiempo de compilación, en lugar de una advertencia seguida de una SwitchExpressionException en producción. (Si existe un brazo _ =>, la nueva variante cae en él en silencio — un motivo para evitar los comodines en los enums que te pertenecen.)

  1. Escribe un Rectangle { width: f64, height: f64 } con fn square(size: f64) -> Self, fn area(&self) -> f64 y fn scale(&mut self, factor: f64). Después crea una copia el doble de ancha usando la sintaxis de actualización de structs.
Solución
#[derive(Debug, Clone, Copy, PartialEq)]
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
fn square(size: f64) -> Self {
Self { width: size, height: size }
}
fn area(&self) -> f64 {
self.width * self.height
}
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
}
let mut r = Rectangle::square(2.0);
r.scale(1.5); // 3 x 3
let wide = Rectangle { width: r.width * 2.0, ..r }; // 6 x 3
assert_eq!(wide.area(), 18.0);