Sintaxis de Métodos

Los métodos son similares a las funciones: los declaramos con la palabra clave fn y un nombre, pueden tener parámetros y un valor de retorno, y contienen alguno código que se ejecuta cuando el método es llamado desde otro lugar. A diferencia de las funciones, los métodos se definen dentro del contexto de una estructura (o un enum o un objeto de tipo trait, que cubriremos en el Capítulo 6 y el Capítulo 17, respectivamente), y su primer parámetro siempre es self, que representa la instancia de la estructura en la que se está llamando al método.

Definiendo Metodos

Vamos a cambiar la función area que tiene una instancia de Rectangle como parámetro y en vez de eso definamos un método area en el struct Rectangle, como se muestra en el Listado 5-13.

Filename: src/main.rs

#[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()
    );
}

Listing 5-13: Definición de un método area en el struct Rectangle

Para definir la función dentro del contexto de Rectangle, iniciamos un bloque impl (implementación). Todo lo que esté dentro de este bloque impl estará asociado al tipo Rectangle. Luego movemos la función area dentro de las llaves del bloque impl y cambiamos el primer (y en este caso, único) parámetro para ser self en la firma y en todas partes dentro del cuerpo. En main, donde llamamos a la función area y pasamos rect1 como argumento, podemos en vez de eso usar la sintaxis de método para llamar al método area en nuestra instancia de Rectangle. La sintaxis de método va después de una instancia: se agrega un punto seguido del nombre del método, paréntesis y cualquier argumento.

En la firma para area, usamos &self en vez de rectangle: &Rectangle. El &self es en realidad una abreviatura para 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 de tipo Self para su primer parámetro, por lo que Rust nos permite abreviar esto con solo el nombre self en el primer parámetro. Ten en cuenta que aún necesitamos usar el & antes de la abreviatura self para indicar que este método toma prestada la instancia Self, al igual que hicimos en rectangle: &Rectangle. Los métodos pueden tomar la propiedad de self, tomarlo prestado de forma inmutable, como hemos hecho aquí, o tomarlo prestado de forma mutable, al igual que pueden hacerlo con cualquier otro parámetro.

Elegimos &self aquí por la misma razón que usamos &Rectangle en la versión de la función: no queremos tomar la propiedad, y solo queremos leer los datos en la estructura, no escribir en ella. Si quisiéramos cambiar la instancia en la que hemos llamado al método como parte de lo que el método hace, usaríamos &mut self como primer parámetro. Tener un método que tome la propiedad de la instancia usando solo self como primer parámetro es raro; esta técnica se usa normalmente cuando el método transforma self en otra cosa y quieres evitar que el que llama al método use la instancia original después de la transformación.

La razón principal para usar métodos en vez de funciones, además de proveer la sintaxis de método y no tener que repetir el tipo de self en cada firma de método, es para la organización. Hemos puesto todas las cosas que podemos hacer con una instancia de un tipo en un bloque impl en vez de hacer que los usuarios futuros de nuestro código busquen las capacidades de Rectangle en varios lugares en la biblioteca que proveemos.

Nota que podemos elegir darle al método el mismo nombre que uno de los campos del struct. Por ejemplo, podemos definir un método en Rectangle que se llame width:

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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 que el método width retorne 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 del mismo nombre para cualquier propósito. En main, cuando seguimos rect1.width con paréntesis, Rust sabe que queremos decir el método width. Cuando no usamos paréntesis, Rust sabe que queremos decir el campo width.

A veces, pero no siempre, cuando damos un método el mismo nombre que un campo queremos que solo retorne el valor en el campo y no haga nada más. Los métodos como este se llaman getters, y Rust no los implementa automáticamente para los campos de un struct como lo hacen otros lenguajes. Los getters son útiles porque puedes hacer que el campo sea privado, pero el método sea público, y así permitir acceso de solo lectura a ese campo como parte de la API pública del tipo. Hablaremos de qué es público y privado y cómo designar un campo o método como público o privado en el Capítulo 7.

¿Donde esta 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->something() es similar a (*object).something().

Rust no tiene un equivalente al operador ->; en su lugar, Rust tiene una característica llamada referenciación y desreferenciación automáticas. Llamar a métodos es uno de los pocos lugares en Rust donde se tiene este comportamiento.

Así es como funciona: cuando llamas a un método con object.something(), Rust automáticamente agrega &, &mut, o * para que object coincida con la firma del método. En otras palabras, lo siguiente es lo mismo:

#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}

El primer ejemplo es más limpio. Este comportamiento de referencia y desreferenciació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 que el préstamo sea implícito para los receptores de método es una gran parte de hacer que la propiedad sea ergonómica en la práctica.

Métodos con más parámetros

Practiquemos usando métodos implementando un segundo método en la estructura Rectangle. Esta vez queremos que una instancia de Rectangle tome otra instancia de Rectangle y retorne true si el segundo Rectangle puede completamente caber dentro de self (el primer Rectangle); de lo contrario, debería retornar false. Es decir, una vez que hayamos definido el método can_hold, queremos poder escribir el programa mostrado en el Listing 5-14.

Filename: src/main.rs

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-14: Uso del método can_hold aún no escrito

La salida esperada se vería como la siguiente porque ambas dimensiones de rect2 son más pequeñas que las dimensiones de rect1, pero rect3 es más ancha que rect1:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

Sabemos que queremos definir un método, por lo que estará dentro del bloque impl Rectangle. El nombre del método será can_hold, y tomará un préstamo inmutable de otro Rectangle como parámetro. Podemos decir cuál será el tipo del parámetro mirando el código que llama al método: rect1.can_hold(&rect2) pasa &rect2, que es un préstamo inmutable a rect2, una instancia de Rectangle. Esto tiene sentido porque solo necesitamos leer rect2 (en lugar de escribir, lo que significaría que necesitaríamos un préstamo mutable), y queremos que main conserve la propiedad de rect2 para que podamos usarlo nuevamente después de llamar al método can_hold. El valor de retorno de can_hold será un Booleano, y la implementación verificará si el ancho y alto de self son mayores que el ancho y alto del otro Rectangle, respectivamente. Agreguemos el nuevo método can_hold al bloque impl del Listing 5-13 que se muestra en el Listing 5-15.

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-15: Implementando el método can_hold en Rectangle que toma otra instancia de Rectangle como un parámetro

Cuando ejecutamos este código con la función main en el Listing 5-14, obtendremos el resultado deseado. Los métodos pueden tomar múltiples parámetros que agregamos a la firma después del parámetro self, y esos parámetros funcionan igual que los parámetros en las funciones.

Funciones asociadas

Todas las funciones definidas dentro de un bloque impl se llaman funciones asociadas porque están asociadas con el tipo nombrado después del impl. Podemos definir funciones asociadas que no tengan self como su primer parámetro (y, por lo tanto, no sean métodos) porque no necesitan una instancia del tipo con el que trabajar. Ya hemos usado una función como esta: la función String::from que está definida en el tipo String.

Las funciones asociadas que no son métodos son a menudo utilizadas para constructores que devolverán una nueva instancia de la estructura. Estás a menudo se llaman new, pero new no es un nombre especial y no está incorporado en el lenguaje. Por ejemplo, podríamos elegir proporcionar una función asociada llamada square que tendría un parámetro de dimensión y lo usaría como ancho y alto, de modo que sea más fácil crear un Rectangle cuadrado en lugar de tener que especificar el mismo valor dos veces:

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}

La palabra clave Self en el tipo de retorno y en el cuerpo de la función es un alias para el tipo que aparece después de la palabra clave impl, que en este caso es Rectangle.

Para llamar a esa función asociada, usamos la sintaxis :: con el nombre de la estructura; let sq = Rectangle::square(3); es un ejemplo. Esta función está dentro del namespace de la estructura. La sintaxis :: se usa tanto para las funciones asociadas como para los namespaces creados por los módulos. Discutiremos los módulos en el Capítulo 7.

Bloques impl múltiples

Cada struct es permitido tener múltiples bloques impl. Por ejemplo, el Listing 5-15 es equivalente al código mostrado en el Listing 5-16, que tiene cada método en su propio bloque impl.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-16: Reescribiendo Listing 5-15 usando múltiples bloques impl

No hay razón para separar estos métodos en múltiples bloques impl aquí, pero esta es una sintaxis válida. Veremos un caso en el que los múltiples bloques impl son útiles en el Capítulo 10, donde discutiremos los tipos genéricos y los traits.

Resumen

Los structs te permiten crear tipos personalizados que son significativos para su dominio. Al usar structs, puede mantener piezas de datos asociadas entre sí y nombrar cada pieza para hacer que su código sea claro. En los bloques impl, puede definir funciones que están asociadas con su tipo, y los métodos son un tipo de función asociada que le permite especificar el comportamiento que tienen las instancias de sus structs.

Pero los structs no son la única forma de crear tipos personalizados: pasemos a la función enum de Rust para agregar otra herramienta a su toolbox.