Un método es una función asociada a un tipo que normalmente trabaja con sus datos.
En el artículo anterior definimos Structs, que son fantásticos para guardar datos. Unos datos que no hacen nada son un poco aburridos, así que vamos a darles comportamiento.
Si vienes de C# o Java, estás acostumbrado a definir la clase y, dentro de las mismas llaves, escribir las propiedades (datos) y los métodos (comportamiento).
En Rust, esto se hace por separado.
- Los datos se definen en el
struct. - El comportamiento se define en un bloque llamado
impl(implementación).
Esta separación es muy limpia y permite añadir comportamiento a un tipo incluso en diferentes archivos. Hoy vamos a ver cómo definir métodos, cómo modificar la instancia con ellos y qué son las “funciones estáticas” en Rust.
Definir métodos con impl
Vamos a recuperar un ejemplo clásico porque es el más fácil de visualizar: un Rectangulo.
#[derive(Debug)]
struct Rectangulo {
ancho: u32,
alto: u32,
}Para definir una función que pertenezca a este struct (por ejemplo, para calcular su área), abrimos un bloque impl Rectangulo:
impl Rectangulo {
fn area(&self) -> u32 {
self.ancho * self.alto
}
}El parámetro especial: &self
Fíjate en el primer parámetro de la función area: &self.
En Rust, una función se considera un método cuando su primer parámetro es alguna forma de self.
selfes una abreviatura deself: Self, mientras que&selfequivale aself: &Self.Self(con mayúscula) es un alias para el tipo de dato en el que estamos (en este caso,Rectangulo).
Al usar &self, estamos diciendo: “Para ejecutar esta función, necesito tomar prestada (lectura) la instancia sobre la que estoy trabajando”.
Para llamar al método, usamos la sintaxis de punto (.), igual que en otros lenguajes:
fn main() {
let rect1 = Rectangulo { ancho: 30, alto: 50 };
println!(
"El área del rectángulo es {} píxeles cuadrados.",
rect1.area() // Rust pasa &rect1 automáticamente
);
}¿Dónde está el operador -> de C++?
En C++, si tienes un puntero, usas -> para llamar al método. En Rust, existe la Referencia y Desreferencia Automática. Rust sabe que area espera un &self, así que cuando llamas a rect1.area(), Rust añade el & por ti automáticamente.
Tipos de self: lectura, escritura y consumo
Igual que con las funciones normales, los métodos están sujetos a las reglas de Ownership. Dependiendo de cómo definamos el primer parámetro, el método podrá hacer unas cosas u otras.
&self: préstamo inmutable
Es el más común (como en el ejemplo area).
- Significado: “Solo quiero leer los datos del struct”.
- Consecuencia: La instancia sigue siendo válida después de llamar al método.
&mut self: préstamo mutable
Lo usamos cuando el método necesita cambiar los datos de la instancia.
impl Rectangulo {
fn redimensionar(&mut self, factor: u32) {
self.ancho *= factor;
self.alto *= factor;
}
}
fn main() {
let mut rect1 = Rectangulo { ancho: 10, alto: 20 };
rect1.redimensionar(2); // Modifica rect1
}Para llamar a este método, la variable original (rect1) debe haber sido declarada como mutable (let mut).
self: consumo de la instancia
Si escribimos self sin el &, el método toma posesión de la instancia.
- Significado: “Dame la instancia entera. Ahora es mía. Yo decido si la destruyo o la transformo”.
- Consecuencia: La variable original deja de ser válida después de llamar al método.
Esto es menos común, pero muy útil para métodos que transforman un dato en otro tipo completamente diferente y queremos evitar que se use el dato antiguo.
impl Rectangulo {
fn destruir(self) {
println!("El rectángulo ha sido destruido y liberado");
} // Aquí self sale del ámbito y se destruye junto con sus recursos
}Funciones asociadas
Dentro del bloque impl también podemos definir funciones que NO toman self como primer parámetro.
Como no tienen una instancia sobre la que actuar, no son métodos, sino funciones asociadas al tipo. Cumplen un papel parecido al de los métodos estáticos de Java o C#.
El uso principal: constructores
Rust no tiene la palabra clave new. Para crear instancias, la convención es definir una función asociada llamada new.
impl Rectangulo {
// No toma &self. Devuelve una instancia de Rectangulo (Self)
fn new(ancho: u32, alto: u32) -> Rectangulo {
Rectangulo { ancho, alto }
}
}Para llamar a estas funciones, no usamos el punto (.), usamos el operador de resolución de ámbito ::.
fn main() {
// NombreStruct::FuncionAsociada
let rect1 = Rectangulo::new(30, 50);
}new es solo una convención. Podrías llamarla crear, construir o hacer_rectangulo. Pero conviene usar new, ya que es lo que todo programador de Rust espera encontrar.
Varios bloques impl
Rust te permite tener múltiples bloques impl para el mismo struct. Esto es útil para organizar el código (aunque no es obligatorio).
impl Rectangulo {
fn area(&self) -> u32 { ... }
}
impl Rectangulo {
fn new(...) -> Rectangulo { ... }
}Esto cobrará mucho sentido cuando veamos los Traits (interfaces), donde implementaremos distintos rasgos en bloques separados.
Elegir el receptor adecuado
A la hora de escribir funciones dentro de un impl, elige el receptor adecuado:
fn funcion() -> ...(Sin self): Función Asociada. Úsala para constructores (new). Se llama con::.fn metodo(&self): Método de Lectura. El 90% de los casos. Lee datos sin modificarlos.fn metodo(&mut self): Método de Escritura. Modifica la instancia. Requiere que el objeto seamut.fn metodo(self): Método de Consumo. Se queda con el ownership y destruye la instancia original.