En este post vamos a ahondar un poco en el tema de los structs. Como ya vimos, un struct es una serie de datos organizados y agrupados dentro de un ámbito definido. También hacía referencia a este tema de los métodos y su similaridad con la POO. Bueno, pues desarrollaremos más los métodos en Rust y cómo se usan en los structs.
Un método es similar a una función: se declaran con el keyword fn
y pueden recibir o devolver valores. Digamos que la diferencia está en que los métodos se definen en el ámbito de los structs
, enums
o traits
y siempre su primer parámetro es self
.
Definir un método en Rust
Vamos a tomar como base nuestro ejemplo anterior:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
Desmenuzando esto: tenemos nuestro struct
llamado Rectangle
. Luego está el bloque definido por la keyword impl
(implementation). Todo lo que está dentro de este bloque estará asociado con Rectangle
.
La función area
recibe &self
, que es un parámetro que siempre será el primero en las funciones dentro de impl
. self
es una manera corta de escribir self: &Self
. Que hace referencia al mismo struct que invoca a Rectangle
mediante impl
, lo mismo que hace la función que vimos en el post anterior, donde la función área llamaba a rectangle: &Rectangle
. Lo hacemos de esa manera porque no queremos tomar el Ownership, solo queremos leer los valores del struct para procesar los datos y obtener un resultado.
Fields y métodos son el mismo nombre
Rust nos permite usar el mismo nombre para un field y para un método, algo así como los Getters en otros lenguajes de programación, por ejemplo:
...
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}
En la función width
tomamos, como ya dijimos, el parámetro &self
y regresamos un valor bool
. Lo que hace es regresar true
o false
dependiendo si el field width
es mayor a cero o no. Dentro de main
evaluamos con un if el método width()
mediante los paréntesis si nuestro field es mayor a cero. OJO, podríamos hacerlo sencillamente quitando la función width
y evaluando solo el field (if rect1.width > 0
), pero para el ejemplo dejamos el método porque en otros escenarios, este método procesaría la información de otra manera o tomaría más variables para generar el resultado.
Más de un parámetro en las funciones
De nuevo, las funciones en impl
reciben siempre como primer valor self
. Ahora, para ejemplificar funciones que reciben más de un valor vamos a escribir una función que reciba una instancia de Rectangle
, compare los valores de base y altura, y si los valores del rectángulo recibido son menores al de origen regresará true
de lo contrario será false
.
La función se llamará can_hold
y recibirá una referencia inmutable de Rectangle
. Recordemos que debe ser una referencia porque no queremos tomar el ownsership y será inmutable porque no queremos modificar los valores de la referencia, solo leerlos. La función devolverá un valor bool
de acuerdo a las condiciones que mencionamos el párrafo anterior.
impl Rectangle {
...
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Para usarla hacemos lo siguiente:
fn main() {
let rect1 = Rectangle {
width: 9,
height: 50,
};
let rect2 = Rectangle {
width: 6,
height: 49,
};
if rect1.can_hold(&rect2) {
println!("The rectangles fix");
}
}
Funciones asociadas
Las funciones asociadas (associated functions) se llaman así porque están relacionadas con el tipo que después de la palabra impl
y es otra manera de generar métodos en Rust. Y ya vimos que siempre su primer parámetro es self
(que sí, ya sabemos), pero también podemos definir associated functions sin que su valor tenga que ser obligatoriamente self
. Esto es porque tal vez no necesitamos instanciarlas, tal como hacemos con String::from("mi texto")
. Esta es de tipo String
pero no tenemos que hacer nada más para usar from
, algo así como métodos estáticos en otros lenguajes. Es decir, no tenemos que hacer algo como let st = String();
.
La definición es así:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
Ahora podemos usarla asignando directamente a una variable let sq = Rectangle::square(3);
. Como vemos ahora usamos ::
para llamar a Square
y no instanciamos el struct. Estos pueden ser usados para associated functions y para módulos, de los que hablaremos después.
Multiples bloques impl
Es posible que alguna vez veas algo como esto:
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Aunque es poco probable que en realidad lo veas, esta sintaxis es válida y no tiene nada diferente, funciona de la misma manera en que declararías todo dentro de un bloque impl
.
Hasta aquí llegamos con los structs y los métodos en Rust. En el siguiente veremos la creación de enums
, otra manera de crear tipos personalizados en Rust.
Gracias por leer.