Aprende Rust

Lección 10 de 26 Structs, enums y patrones

Métodos con impl

  • 4 min de lectura
  • 4 ejercicios
  1. Un bloque impl añade métodos a un tipo: funciones que usan sus datos, como area para Rectangulo.
  2. Se llaman con un punto: rect.area() da 6. Con &self, el método solo toma prestado el rectángulo.
  3. Con &mut self puede modificarlo: rect.escalar(2) dobla ancho y alto. Por eso rect es mut.
  4. Sin self es una función asociada y se llama con ::. Por convención, el constructor se llama new.
1/4

En la lección anterior agrupaste datos con structs. Ahora vas a darles comportamiento: funciones que pertenecen a un tipo y trabajan con sus datos. Se escriben dentro de un bloque impl (de implementation).

Si vienes de otros lenguajes, es lo más parecido a los métodos de una clase, pero con una diferencia importante: cada método declara qué hace con la instancia (leerla, modificarla o quedársela), con las mismas reglas de ownership que ya conoces.

Tu primer método

struct Rectangulo {
    ancho: u32,
    alto: u32,
}

impl Rectangulo {
    fn area(&self) -> u32 {
        self.ancho * self.alto
    }
}

fn main() {
    let rect = Rectangulo { ancho: 30, alto: 50 };
    println!("Área: {}", rect.area());
}
  • impl Rectangulo { ... } agrupa todo lo que pertenece al tipo Rectangulo.
  • Un método es una función dentro de un impl cuyo primer parámetro es self: la instancia sobre la que se llama.
  • &self es la forma corta de self: &Self, y Self es un alias del tipo del bloque (aquí, Rectangulo). Así que area solo toma prestado el rectángulo para leerlo.
  • Se llama con un punto: rect.area(). No hace falta escribir (&rect).area(): Rust añade el & por ti.

Podrías haber escrito una función suelta area(rect: &Rectangulo), pero el método deja claro que el área es algo propio de los rectángulos y reúne todo su comportamiento en un solo sitio.

&self, &mut self y self

El primer parámetro decide qué hace el método con la instancia:

Primer parámetroQué recibeÚsalo para
&selfUna referencia: solo lecturaConsultar datos (el caso más habitual)
&mut selfUna referencia mutableModificar campos
selfLa propiedad: consume la instanciaTransformarla o terminar con ella

Modificar con &mut self

struct Rectangulo {
    ancho: u32,
    alto: u32,
}

impl Rectangulo {
    fn area(&self) -> u32 {
        self.ancho * self.alto
    }

    fn escalar(&mut self, factor: u32) {
        self.ancho *= factor;
        self.alto *= factor;
    }
}

fn main() {
    let mut rect = Rectangulo { ancho: 3, alto: 2 };
    rect.escalar(10);
    println!("Área: {}", rect.area());
}

Para llamar a un método con &mut self, la variable tiene que ser mut, igual que para crear un &mut a mano.

Consumir con self

Un método que recibe self a secas se queda con la instancia: después de llamarlo, ya no puedes usarla.

struct Carta {
    destinatario: String,
    texto: String,
}

impl Carta {
    fn enviar(self) {
        println!("Para {}: {}", self.destinatario, self.texto);
    }
}

fn main() {
    let carta = Carta {
        destinatario: String::from("Ferris"),
        texto: String::from("¡Hola!"),
    };
    carta.enviar();
    carta.enviar(); // la carta ya se envió
}
error[E0382]: use of moved value: `carta`
  --> src/main.rs:18:5
   |
17 |     carta.enviar();
   |           -------- `carta` moved due to this method call
18 |     carta.enviar(); // la carta ya se envió
   |     ^^^^^ value used here after move
   |
note: `Carta::enviar` takes ownership of the receiver `self`, which moves `carta`

Es útil cuando, después de la operación, el valor original ya no tiene sentido: una carta enviada, un borrador que se convierte en publicación…

Funciones asociadas

Las funciones de un impl que no reciben self se llaman funciones asociadas. No trabajan sobre una instancia concreta, así que se llaman con el nombre del tipo y ::. Su uso más típico es crear instancias y, por convención, el constructor principal se llama new:

struct Rectangulo {
    ancho: u32,
    alto: u32,
}

impl Rectangulo {
    fn new(ancho: u32, alto: u32) -> Self {
        Self { ancho, alto }
    }

    fn cuadrado(lado: u32) -> Self {
        Self { ancho: lado, alto: lado }
    }

    fn area(&self) -> u32 {
        self.ancho * self.alto
    }
}

fn main() {
    let rect = Rectangulo::new(4, 5);
    let cuadrado = Rectangulo::cuadrado(3);
    println!("Áreas: {} y {}", rect.area(), cuadrado.area());
}

Dentro del impl, Self equivale a Rectangulo: si algún día renombras el tipo, no tendrás que cambiarlo en cada función. Y sí, ya habías usado funciones asociadas: String::from("hola") es una función asociada del tipo String.

Ojo: A diferencia de otros lenguajes, new no es una palabra reservada. Es una función normal con un nombre que todo el mundo usa por convención.

Punto o dos puntos

SintaxisPara quéEjemplo
valor.metodo()Métodos: reciben self, &self o &mut selfrect.area()
Tipo::funcion()Funciones asociadas: no reciben selfRectangulo::new(4, 5)

Métodos que reciben otra instancia

Un método puede tener más parámetros después de self, incluida otra instancia del mismo tipo. Como solo hay que leerla, lo normal es recibirla por referencia:

struct Rectangulo {
    ancho: u32,
    alto: u32,
}

impl Rectangulo {
    fn puede_contener(&self, otro: &Rectangulo) -> bool {
        self.ancho > otro.ancho && self.alto > otro.alto
    }
}

fn main() {
    let grande = Rectangulo { ancho: 30, alto: 50 };
    let estrecho = Rectangulo { ancho: 10, alto: 40 };
    let ancho = Rectangulo { ancho: 60, alto: 45 };

    println!("¿grande contiene a estrecho? {}", grande.puede_contener(&estrecho));
    println!("¿grande contiene a ancho? {}", grande.puede_contener(&ancho));
}

Varios bloques impl

Un tipo puede tener tantos bloques impl como quieras; el resultado es el mismo que si estuviera todo en uno:

impl Rectangulo {
    fn area(&self) -> u32 {
        self.ancho * self.alto
    }
}

impl Rectangulo {
    fn es_cuadrado(&self) -> bool {
        self.ancho == self.alto
    }
}

Aquí no aporta nada, pero te será útil cuando llegues a los traits y los genéricos.

Consejo: Empieza siempre por &self. Pasa a &mut self solo si el método modifica la instancia, y usa self solo cuando de verdad tenga que consumirla.

Hora de practicar

Ejercicios

0/4 resueltos

Reto 1 de 4

Añade dos métodos con &self al bloque impl Rectangulo: perimetro, que devuelve 2 * ancho + 2 * alto, y es_mayor_que(&self, otro: &Rectangulo) -> bool, que devuelve true si el área de self es mayor que la de otro.
Salida esperada
Perímetro de a: 14
¿a es mayor que b? true
¿b es mayor que a? false

Al ejecutar, unos tests ocultos comprueban tus funciones.

src/main.rs
$ cargo run Listo

Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.

¿Algo no cuadra en este reto? Avísanos