5. Structs, enums y pattern matching
Ejemplo completo: examples/l05_structs_enums.rs — cargo 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::newes una función asociada normal que devuelveSelf. 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 deaccount.close(),accountse 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: trueHay 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 centsTuple structs y newtypes
Sección titulada «Tuple structs y newtypes»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): trueUna 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 llevan datos
Sección titulada «Los enums llevan datos»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 21sealed 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 {}match es exhaustivo
Sección titulada «match es exhaustivo»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.
Patrones
Sección titulada «Patrones»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 3move by (1, -2)say "hi"quitRangos, 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.
if let y let … else
Sección titulada «if let y let … else»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") == 8080Some y Ok son a su vez variantes de enums — el tema de la próxima lección.
Puntos clave
Sección titulada «Puntos clave»structcontiene los datos,implañade funciones asociadas y métodos;newes una convención, no un constructor.&self,&mut selfyselfindican si un método lee, modifica o consume el valor.- Las variantes de un
enumllevan datos distintos: un enum de Rust es una jerarquía sellada de records en una sola declaración. matchdebe ser exhaustivo;if letylet … elsecubren los casos de un solo patrón.
Ejercicios
Sección titulada «Ejercicios»- 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"), }}- Añades
Crypto { wallet: String }aPayment. ¿Qué le pasa adescribey 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.)
- Escribe un
Rectangle { width: f64, height: f64 }confn square(size: f64) -> Self,fn area(&self) -> f64yfn 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 3let wide = Rectangle { width: r.width * 2.0, ..r }; // 6 x 3assert_eq!(wide.area(), 18.0);