Práctica de Sintaxis de Métodos en Rust

RustRustBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a Sintaxis de Métodos. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, los métodos se declaran con la palabra clave fn y un nombre, pueden tener parámetros y un valor de retorno, y se definen en el contexto de un struct, con el primer parámetro siempre siendo self para representar la instancia del struct en el que se está llamando.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") subgraph Lab Skills rust/variable_declarations -.-> lab-100397{{"Práctica de Sintaxis de Métodos en Rust"}} rust/integer_types -.-> lab-100397{{"Práctica de Sintaxis de Métodos en Rust"}} rust/boolean_type -.-> lab-100397{{"Práctica de Sintaxis de Métodos en Rust"}} rust/function_syntax -.-> lab-100397{{"Práctica de Sintaxis de Métodos en Rust"}} rust/expressions_statements -.-> lab-100397{{"Práctica de Sintaxis de Métodos en Rust"}} rust/method_syntax -.-> lab-100397{{"Práctica de Sintaxis de Métodos en Rust"}} rust/traits -.-> lab-100397{{"Práctica de Sintaxis de Métodos en Rust"}} end

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 un código que se ejecuta cuando se llama al método desde otro lugar. A diferencia de las funciones, los métodos se definen en el contexto de un struct (o un enum o un objeto de trato, que cubriremos en los Capítulos 6 y 17, respectivamente), y su primer parámetro siempre es self, que representa la instancia del struct en el que se está llamando al método.

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.

Métodos con más parámetros

Vamos a practicar el uso de métodos implementando un segundo método en el struct Rectangle. Esta vez queremos que una instancia de Rectangle tome otra instancia de Rectangle y devuelva true si el segundo Rectangle puede caber completamente dentro de self (el primer Rectangle); de lo contrario, debe devolver false. Es decir, una vez que hayamos definido el método can_hold, queremos poder escribir el programa mostrado en la Lista 5-14.

Nombre de archivo: 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));
}

Lista 5-14: Usando el método can_hold aún no escrito

La salida esperada se vería como la siguiente porque ambas dimensiones de rect2 son menores 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 determinar el tipo del parámetro viendo 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 posesión 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 comprobará si el ancho y la altura de self son mayores que el ancho y la altura del otro Rectangle, respectivamente. Agreguemos el nuevo método can_hold al bloque impl de la Lista 5-13, como se muestra en la Lista 5-15.

Nombre de archivo: src/main.rs

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

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

Cuando ejecutamos este código con la función main de la Lista 5-14, obtendremos la salida deseada. 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 exactamente como 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 tienen self como su primer parámetro (y por lo tanto no son métodos) porque no necesitan una instancia del tipo para funcionar. Ya hemos usado una función así: la función String::from que está definida en el tipo String.

Las funciones asociadas que no son métodos a menudo se usan para constructores que devolverán una nueva instancia del struct. A menudo se llaman new, pero new no es un nombre especial y no está integrado 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 tanto ancho como alto, lo que haría más fácil crear un Rectangle cuadrado en lugar de tener que especificar el mismo valor dos veces:

Nombre de archivo: src/main.rs

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

Las palabras clave Self en el tipo de retorno [1] y en el cuerpo de la función [2] son alias para el tipo que aparece después de la palabra clave impl, que en este caso es Rectangle.

Para llamar a esta función asociada, usamos la sintaxis :: con el nombre del struct; let sq = Rectangle::square(3); es un ejemplo. Esta función está en un espacio de nombres del struct: la sintaxis :: se utiliza tanto para funciones asociadas como para los espacios de nombres creados por los módulos. Discutiremos los módulos en el Capítulo 7.

Varios bloques impl

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

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

Lista 5-16: Reescribiendo la Lista 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 múltiples bloques impl son útiles en el Capítulo 10, donde discutiremos tipos genéricos y rasgos.

Resumen

¡Felicitaciones! Has completado el laboratorio de Sintaxis de Métodos. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.