Structs y Métodos (bloques impl)
Los structs agrupan datos relacionados en campos con nombre, y los bloques impl les añaden comportamiento — la respuesta de Rust a una clase, sin herencia.
Todo lenguaje necesita una forma de agrupar datos relacionados en una sola unidad con sentido, y la respuesta de Rust es el struct. Mientras que una tupla simple como (f64, f64) agrupa dos números sin darles nombre, un struct le da identidad a cada campo: width y height en lugar de .0 y .1. Puede parecer una pequeña mejora de ergonomía, pero se acumula rápido — una firma de función que recibe un Rectangle documenta la intención mucho mejor que una que recibe dos floats anónimos, y el compilador jamás te dejará intercambiar accidentalmente el ancho y el alto, algo que una tupla aceptaría en silencio si invirtieras las posiciones.
Definir un struct es solo la palabra clave struct, un nombre y una lista de campos tipados; instanciarlo consiste en rellenar cada campo por nombre dentro de llaves, en el orden que prefieras. Cuando una variable local comparte nombre con un campo — let width = 30.0; seguido de Rectangle { width, height } — Rust te permite omitir la redundancia de width: width, un atajo que usarás constantemente en cuanto tus funciones empiecen a recibir parámetros que coinciden con los nombres de los campos. Leer un campo es solo sintaxis de punto, rect.width, y como los structs son valores normales, un Rectangle completo se puede mover, prestar o (si deriva Clone) copiar como cualquier otro tipo que ya conoces.
La definición de un struct solo describe los datos; el comportamiento que le pertenece vive en un bloque impl separado — impl Rectangle { ... } — que incluso puedes escribir más de una vez para el mismo tipo si te ayuda a organizar el código. Esta separación entre datos y comportamiento es deliberada: a diferencia de una clase en Java o C#, un struct de Rust nunca combina campos y métodos en una sola declaración, y no existe herencia que oscurezca de qué bloque impl viene realmente un método. Cada función dentro de un bloque impl es un método, que recibe alguna forma de self como primer parámetro, o una función asociada, que no lo recibe.
La forma que toma self es donde las reglas de propiedad de Rust se reflejan directamente en el diseño de tu API, y es una decisión de diseño real cada vez que escribes una. &self toma prestada la instancia de forma inmutable — el método puede leer campos pero no modificarlos, y quien lo llama sigue pudiendo usar el valor después, por lo que los métodos de solo lectura como area(&self) son la opción abrumadoramente más común. &mut self la toma prestada de forma mutable, permitiendo que el método actualice campos en el lugar, pero exige que quien lo llama tenga una variable mut y prohíbe cualquier otro préstamo de ese valor mientras el préstamo mutable esté activo. Tomar self por valor — sin & en absoluto — consume la instancia: el método ahora es su dueño, la variable original ya no se puede usar, y esto es exactamente lo que quieres para una transformación que produce un estado genuinamente nuevo a partir de uno anterior, como el paso final .build() de un builder o una transición de máquina de estados que no debería dejar el estado anterior dando vueltas para que alguien lo use por error.
Rust no tiene palabra clave new ni sintaxis de constructor, así que 'construir' es solo una convención: una función asociada, casi siempre llamada new, que no recibe self y devuelve Self (un alias para el tipo al que pertenece el bloque impl). Se llama con :: en lugar de . — Rectangle::new(30.0, 50.0) — porque todavía no existe ninguna instancia sobre la cual llamar un método; el trabajo entero de la función asociada es producir la primera. Aquí es también donde impones invariantes en el momento de la creación: una función new puede validar argumentos, calcular campos derivados o devolver un Result si la construcción puede fallar, nada de lo cual puede hacer por sí solo un literal de struct desnudo.
Dos parientes más ligeros completan el panorama. Un tuple struct — struct Point(f64, f64); — tiene nombre y tipo, pero campos posicionales sin nombre a los que se accede como .0 y .1; es la opción correcta cuando la posición es genuinamente autoexplicativa (un color RGB, una coordenada 2D) o cuando estás envolviendo un único valor con el 'newtype pattern' solo para darle un tipo distinto (struct UserId(u64); no es intercambiable con un u64 simple, y el compilador te impedirá confundirlos). Un unit struct — struct Marker; — no tiene campos en absoluto; más adelante los encontrarás como marcadores de tamaño cero a los que se les implementan traits, útiles cuando el tipo en sí mismo lleva todo el significado que necesitas y no hace falta que viaje ningún dato con él.
Con los structs ya tienes una forma de modelar 'esta forma de datos, siempre.' La siguiente lección presenta los enums, que modelan el caso opuesto y a menudo más realista — 'estos datos son una de estas varias formas, y no sabré cuál hasta que el programa se ejecute' — y lo combina con match, la herramienta que te obliga a manejar cada una de esas formas antes de que tu código siquiera compile.
// A classic struct: named fields, each with its own type.struct Rectangle {width: f64,height: f64,}// Tuple struct: fields have positions, not names — good for lightweight wrappers.struct Point(f64, f64);// Unit struct: no fields at all, useful as a marker type or for trait impls.struct Marker;fn main() {let rect = Rectangle { width: 30.0, height: 50.0 };println!("width = {}, height = {}", rect.width, rect.height);let origin = Point(0.0, 0.0);println!("origin = ({}, {})", origin.0, origin.1);let _m = Marker;}
Defining a classic named-field struct alongside a tuple struct and a unit struct, then instantiating and reading each one.
struct Rectangle {width: f64,height: f64,}impl Rectangle {// Associated function (constructor): no `self`, called as Rectangle::new(...).fn new(width: f64, height: f64) -> Rectangle {Rectangle { width, height }}// &self: borrows the instance immutably — just reads data.fn area(&self) -> f64 {self.width * self.height}// &mut self: borrows mutably — can change fields in place.fn scale(&mut self, factor: f64) {self.width *= factor;self.height *= factor;}// self (no &): takes ownership — the instance is consumed by this call.fn into_square(self) -> Rectangle {let side = (self.width + self.height) / 2.0;Rectangle { width: side, height: side }}}fn main() {let mut rect = Rectangle::new(30.0, 50.0);println!("area = {}", rect.area());rect.scale(2.0);println!("scaled area = {}", rect.area());let square = rect.into_square();println!("square side = {}", square.width);}
An impl block showing all three forms self can take — &self to read, &mut self to mutate, and self by value to consume — plus an associated function used as a constructor.
#[derive(Debug)]struct User {username: String,email: String,active: bool,sign_in_count: u64,}impl User {fn new(username: String, email: String) -> User {User { username, email, active: true, sign_in_count: 0 }}}fn main() {let user1 = User::new(String::from("ferris"), String::from("ferris@rust-lang.org"));// Struct update syntax: build a new instance from user1, overriding just one field.let user2 = User {email: String::from("crab@rust-lang.org"),..user1};println!("{:?}", user2);}
Deriving Debug for free printing, plus struct update syntax (..user1) to build a new instance that copies every field from an existing one except the ones you override.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. Given `fn into_square(self) -> Rectangle` defined inside `impl Rectangle`, why does `rect.into_square()` leave `rect` unusable afterward, while a call like `rect.area()` (defined as `fn area(&self) -> f64`) does not?