Definiendo Métodos
Vamos a cambiar la función area
que tiene una instancia de Rectangle
como parámetro y, en lugar de eso, crear un método area
definido en el struct Rectangle
, como se muestra en la Lista 5-13.
Nombre de archivo: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
1 impl Rectangle {
2 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.",
3 rect1.area()
);
}
Lista 5-13: Definiendo un método area
en el struct Rectangle
Para definir la función dentro del contexto de Rectangle
, comenzamos un bloque impl
(implementación) para Rectangle
[1]. Todo lo que esté dentro de este bloque impl
estará asociado con el tipo Rectangle
. Luego movemos la función area
dentro de los corchetes impl
[2] y cambiamos el primer (y en este caso, único) parámetro para que sea self
en la firma y en todos los lugares dentro del cuerpo. En main
, donde llamamos a la función area
y pasamos rect1
como argumento, en lugar de eso podemos usar la sintaxis de método para llamar al método area
en nuestra instancia de Rectangle
[3]. La sintaxis de método va después de una instancia: agregamos un punto seguido del nombre del método, paréntesis y cualquier argumento.
En la firma de area
, usamos &self
en lugar de rectangle: &Rectangle
. El &self
es en realidad una abreviatura de self: &Self
. Dentro de un bloque impl
, el tipo Self
es un alias para el tipo al que pertenece el bloque impl
. Los métodos deben tener un parámetro llamado self
del tipo Self
como su primer parámetro, por lo que Rust te permite abreviarlo solo con el nombre self
en el primer lugar del parámetro. Tenga en cuenta que todavía necesitamos usar el &
delante de la abreviatura self
para indicar que este método presta la instancia de Self
, al igual que lo hicimos en rectangle: &Rectangle
. Los métodos pueden tomar posesión de self
, prestar self
inmutablemente, como hicimos aquí, o prestar self
mutablemente, al igual que pueden cualquier otro parámetro.
Elegimos &self
aquí por la misma razón por la que usamos &Rectangle
en la versión de función: no queremos tomar posesión y solo queremos leer los datos en el struct, no escribirlos. Si quisiéramos cambiar la instancia en la que se ha llamado al método como parte de lo que hace el método, usaríamos &mut self
como primer parámetro. Tener un método que toma posesión de la instancia usando solo self
como primer parámetro es poco común; esta técnica generalmente se utiliza cuando el método transforma self
en algo más y se desea evitar que el llamador use la instancia original después de la transformación.
La principal razón para usar métodos en lugar de funciones, además de proporcionar la sintaxis de método y no tener que repetir el tipo de self
en la firma de cada método, es por organización. Hemos puesto todas las cosas que podemos hacer con una instancia de un tipo en un bloque impl
en lugar de hacer que los futuros usuarios de nuestro código busquen las capacidades de Rectangle
en varios lugares de la biblioteca que proporcionamos.
Tenga en cuenta que podemos optar por dar a un método el mismo nombre que uno de los campos del struct. Por ejemplo, podemos definir un método en Rectangle
que también se llame width
:
Nombre de archivo: src/main.rs
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!(
"The rectangle has a nonzero width; it is {}",
rect1.width
);
}
}
Aquí, estamos eligiendo hacer que el método width
devuelva true
si el valor en el campo width
de la instancia es mayor que 0
y false
si el valor es 0
: podemos usar un campo dentro de un método con el mismo nombre para cualquier propósito. En main
, cuando seguimos rect1.width
con paréntesis, Rust sabe que nos referimos al método width
. Cuando no usamos paréntesis, Rust sabe que nos referimos al campo width
.
A menudo, pero no siempre, cuando damos a los métodos el mismo nombre que un campo, queremos que solo devuelva el valor en el campo y no haga nada más. Métodos como este se llaman getters, y Rust no los implementa automáticamente para los campos de struct como lo hacen algunos otros lenguajes. Los getters son útiles porque se puede hacer que el campo sea privado pero el método sea público, y así habilitar el acceso de solo lectura a ese campo como parte de la API pública del tipo. Discutiremos lo que es público y privado y cómo designar un campo o método como público o privado en el Capítulo 7.
¿Dónde está el operador ->?
En C y C++, se usan dos operadores diferentes para llamar a métodos: se usa .
si se está llamando a un método en el objeto directamente y ->
si se está llamando al método en un puntero al objeto y se necesita desreferenciar el puntero primero. En otras palabras, si object
es un puntero, object->
algo()
es similar a (*object).
algo()
.
Rust no tiene un equivalente al operador ->
; en cambio, Rust tiene una característica llamada referenciación y desreferenciación automática. Llamar a métodos es uno de los pocos lugares en Rust que tiene este comportamiento.
Aquí está cómo funciona: cuando se llama a un método con object.
algo()
, Rust automáticamente agrega &
, &mut
o *
para que object
coincida con la firma del método. En otras palabras, lo siguiente es lo mismo:
p1.distance(&p2);
(&p1).distance(&p2);
El primero se ve mucho más limpio. Este comportamiento de referenciación automática funciona porque los métodos tienen un receptor claro: el tipo de self
. Dado el receptor y el nombre de un método, Rust puede determinar con certeza si el método está leyendo (&self
), mutando (&mut self
) o consumiendo (self
). El hecho de que Rust haga la referencia implícita para los receptores de métodos es una gran parte de lo que hace que la posesión sea ergonomica en la práctica.