impl y métodos: &self, &mut self y self
Bloques impl, los tres receptores (self prestado, mutable o por valor) y cómo cada uno codifica un contrato de ownership distinto: la primera pista de por qué el préstamo importa.
Un struct sin comportamiento es solo datos. Los métodos —definidos en un bloque impl— le dan verbos. Pero en Rust elegir el receptor (&self, &mut self o self) no es un detalle sintáctico: es la primera decisión de ownership que tomarás en cada API que diseñes.
- Escribir métodos en un bloque
implcon receptor&self. - Distinguir
&self,&mut selfyself, y qué le hace cada uno al valor. - Ver el método como azúcar sobre una función con receptor explícito.
- Entender por qué el receptor es un contrato de ownership, no un adorno.
El bloque impl y &self
Los métodos viven separados de la definición de datos, en un bloque impl:
struct Rectangulo {
ancho: f64,
alto: f64,
}
impl Rectangulo {
fn area(&self) -> f64 {
self.ancho * self.alto
}
fn es_cuadrado(&self) -> bool {
self.ancho == self.alto
}
}
let r = Rectangulo { ancho: 3.0, alto: 4.0 };
println!("{}", r.area()); // 12.0
&self significa “tomo prestado el struct de forma inmutable”. El método puede leer los campos pero no modificarlos, y —clave— no toma posesión: quien llama conserva r y puede seguir usándolo después. Es el receptor por defecto para cualquier consulta.
&mut self: mutar en el sitio
Si el método debe cambiar el struct, pide un préstamo mutable:
impl Rectangulo {
fn escalar(&mut self, factor: f64) {
self.ancho *= factor;
self.alto *= factor;
}
}
let mut r = Rectangulo { ancho: 3.0, alto: 4.0 };
r.escalar(2.0); // requiere que 'r' sea 'mut'
El compilador exige que r sea mut y que, mientras dure ese préstamo mutable, nadie más tenga acceso al struct. Aún no lo has estudiado a fondo, pero acabas de tocar la regla central de Rust: los préstamos mutables son exclusivos.
self por valor: consumir el struct
El tercer receptor, self a secas, toma posesión del struct. Tras llamarlo, el valor original ya no existe para quien llamó: se ha movido hacia dentro del método.
impl Rectangulo {
fn en_cuadrado(self) -> Rectangulo {
let lado = self.ancho.max(self.alto);
Rectangulo { ancho: lado, alto: lado }
}
}
let r = Rectangulo { ancho: 3.0, alto: 4.0 };
let c = r.en_cuadrado(); // 'r' se consume aquí
// usar 'r' de nuevo sería error de compilación
Se usa cuando el método transforma el valor en otra cosa, o cuando libera un recurso: consumir deja claro, en el propio tipo, que el original ya no sirve.
flowchart TD A[Llamada a metodo] --> B[Segun el receptor] B -->|ref self| C[Prestamo inmutable] B -->|ref mut self| D[Prestamo mutable exclusivo] B -->|self por valor| E[Toma posesion y mueve] C --> F[El llamador conserva el valor] D --> F E --> G[El llamador pierde el valor]
El método es azúcar: receptor explícito
self, &self y &mut self son abreviaturas. La forma completa revela que un método no es más que una función asociada cuyo primer parámetro es el propio tipo:
impl Rectangulo {
fn area(self: &Rectangulo) -> f64 { // equivalente a &self
self.ancho * self.alto
}
}
Y la llamada r.area() se desazucara a Rectangulo::area(&r). El operador punto hace además algo cómodo: inserta automáticamente el &, el &mut o el desreferenciado necesario para que el receptor encaje. Ese autoref/autoderef es la razón por la que no escribes (&r).area() a mano.
Cuando eliges entre &self, &mut self y self, no estás decorando una firma: estás declarando qué derecho necesita el método sobre el dato, y ese derecho queda grabado en el tipo para siempre. &self promete “solo miro”; &mut self exige “necesito exclusividad para cambiarlo”; self dice “me lo quedo, esto lo destruye o lo transforma”. El compilador convierte esas promesas en garantías: nunca podrás mutar a través de &self, ni usar un valor que un método self ya consumió. Por eso, en Rust, leer las firmas de un tipo te dice cómo fluye la propiedad por tu programa sin ejecutar una sola línea. Los diseñadores de buenas bibliotecas eligen el receptor más débil que baste —prefieren &self, suben a &mut self solo si mutan, y reservan self para transformaciones o consumo— porque cada nivel que piden restringe a quien los usa. Esta es la primera pista de por qué el ownership importa: no es una molestia del borrow checker, es el lenguaje en el que se expresan los contratos de tu código.
- Define un struct
Contador { valor: u32 }con un métodovalor(&self) -> u32. - Añade
incrementar(&mut self)y comprueba que exige un bindingmut. - Añade
consumir(self) -> u32que devuelva el valor final y “cierre” el contador. - Tras llamar a
consumir, intenta usar el contador otra vez y lee con calma el error del compilador. - Reescribe un método usando la forma explícita
self: &Contadory confirma que se comporta igual que&self.