Métodos en Rust – Aprende Rust 9/x

metodos en rust, funciones asociadas, associated functions

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.


Posted

in

, , ,

by