11  Métodos

En el capítulo anterior vimos cómo definir tipos compuestos mediante structs. Un struct permite agrupar datos relacionados bajo una misma entidad lógica, pero por sí solo no define qué operaciones pueden realizarse sobre esos datos.

En Rust es habitual asociar funciones a los tipos para encapsular el comportamiento relacionado con ellos. Estas funciones asociadas reciben el nombre de métodos. Permiten expresar de forma más natural operaciones que actúan sobre una instancia concreta de un tipo. Por ejemplo, un contador puede incrementarse, un rectángulo puede calcular su área o un nombre de dominio puede resolverse para obtener su dirección IP.

En este capítulo veremos cómo definir métodos, cómo invocarlos y cómo Rust utiliza su sistema de propiedad y préstamos para controlar el acceso a los datos de una instancia.

11.1 Definición de métodos

Los métodos se definen dentro de bloques impl (implementation), asociados a un tipo concreto. En este capítulo definiremos todos los métodos en un único bloque impl. En proyectos más complejos es frecuente repartir los métodos entre varios bloques distintos.

Podemos clasificar los métodos en dos grandes grupos:

  1. Los vinculados a un struct. Reciben, como primer parámetro, el struct sobre el que actúan, representado mediante la palabra reservada self. A su vez, se subdividen en:

    • Métodos con &self. Son métodos de solo lectura. Pueden consultar el contenido del struct, pero no modificarlo. Reciben una referencia compartida.

    • Métodos con &mut self. Son métodos de lectura y escritura. Pueden tanto consultar como modificar el struct. Reciben una referencia mutable a él.

    • Métodos con self. Reciben el struct por valor, toman posesión de él y lo consumen durante la llamada.

  2. Los no vinculados a un struct concreto, sino al propio tipo. Son los métodos asociados. Como no actúan sobre ningún struct concreto, no reciben el parámetro self. Por ello, se invocan mediante el nombre del tipo. En muchos lenguajes orientados a objetos encontramos un concepto muy parecido: los métodos de clase o métodos estáticos, que se invocan mediante el nombre de la clase en lugar de sobre un objeto concreto.

En total forman cuatro categorías, que trataremos a continuación.

11.2 Métodos &self (lectura)

El siguiente programa define un método area() para el tipo Rectangulo, crea una instancia de dicho tipo y utiliza el método para calcular su área:

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

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

fn main() {
    let r = Rectangulo {
        ancho: 30,
        alto: 20,
    };

    println!("Área: {}", r.area());
}

En este ejemplo, el método area() pertenece a la categoría de los métodos con &self. Como recibe una referencia compartida al struct, puede consultar su contenido, pero no modificarlo. Dentro del método, la variable especial self representa el struct sobre el que se está realizando la operación. La llamada

r.area()

invoca el método area() del struct r y devuelve el resultado. Conceptualmente es equivalente a otra llamada en la que la instancia se pasa como primer argumento:

Rectangulo::area(&r)

La primera forma no es más que una sintaxis simplificada de la segunda, lo que se denomina azúcar sintático.

Como self representa una referencia compartida al struct, podemos acceder a sus campos mediante la notación habitual:

self.ancho
self.alto

11.3 Métodos &mut self (lectura y escritura)

Un método que recibe &self solo puede consultar el contenido del struct. Si intenta modificar alguno de sus campos, el compilador producirá un error. Cuando un método necesita modificar el struct sobre la que se invoca, debe recibir una referencia mutable. Para ello, el primer parámetro se escribe como &mut self.

El siguiente ejemplo añade un método que modifica las dimensiones del rectángulo:

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

impl Rectangulo {
    fn redimensionar(&mut self, ancho: u32, alto: u32) {
        self.ancho = ancho;
        self.alto = alto;
    }
}

fn main() {
    let mut r = Rectangulo {
        ancho: 20,
        alto: 10,
    };

    r.redimensionar(40, 25);

    println!("{} × {}", r.ancho, r.alto);
}

Obsérvese que la variable r debe declararse como mutable (mut). De lo contrario, no podría modificarse el struct. O, siendo más precisos, no sería posible obtener una referencia mutable al struct y, por tanto, invocar un método con &mut self.

Igual que ocurría con &self, una llamada como

r.redimensionar(40, 25)

es conceptualmente equivalente a

Rectangulo::redimensionar(&mut r, 40, 25)

En ambos casos, el método recibe una referencia mutable al struct, lo que le permite modificar sus campos.

11.4 Métodos self (Consumo)

En algunos casos, aunque son poco frecuentes, un método necesita tomar posesión del struct sobre el que se invoca. Por ejemplo, cuando el método extrae sus datos para crear otro valor y el struct original deja de existir. Para ello, el primer parámetro se escribe simplemente como self.

Cuando un método recibe self, el método toma posesión del struct, no de una mera referencia a él. Como consecuencia, el struct deja de estar disponible tras la llamada.

El siguiente ejemplo convierte un rectángulo en una tupla con sus dimensiones:

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

impl Rectangulo {
    fn descomponer(self) -> (u32, u32) {
        (self.ancho, self.alto)
    }
}

fn main() {
    let r = Rectangulo {
        ancho: 20,
        alto: 10,
    };

    let t = r.descomponer();

    println!("{:?}", t);

    // println!("{}", r.ancho);  // Esto sería un error, r
                                 // ya no existe
}

En este ejemplo, el método toma posesión del struct y extrae sus datos para construir una tupla. Una vez finalizada la llamada, r ya no puede utilizarse. Obsérvese que, a diferencia de los ejemplos anteriores, aquí se pasa r, no &r ni &mut r, ya que el método recibe el struct por valor.

Como en los casos anteriores, una llamada como

r.descomponer()

es conceptualmente equivalente a

Rectangulo::descomponer(r)

11.5 Métodos asociados (sin self)

No todos los métodos definidos dentro de un bloque impl necesitan trabajar con un struct concreto: hay métodos que pertencen al propio tipo, no a una instancia en particular. Se les llama métodos asociados y tienen dos características principales:

  • En la definición, no se incluye el parámetro self.

  • En la invocación, se escribe el nombre del tipo, el operador :: y el nombre del método.

Un uso muy habitual de los métodos asociados es construir nuevos structs. El siguiente ejemplo define un método asociado llamado nuevo:

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

impl Rectangulo {
    fn nuevo(ancho: u32, alto: u32) -> Self {  // sin parámetro 'self'
        Self { ancho, alto }
    }
}

fn main() {
    let r = Rectangulo::nuevo(20, 10);  // tipo::método

    println!("{} × {}", r.ancho, r.alto);
}

En este ejemplo, nuevo no recibe un parámetro self, ya que no existe todavía ningún struct sobre el que actuar. Su misión es construir y devolver un nuevo Rectangulo. Y, naturalmente, como no existe un parámetro self, la llamada se realiza directamente mediante el nombre del tipo.

let r = Rectangulo::nuevo(20, 10);

Por convenio, muchos tipos definen un método asociado llamado new, que actúa de manera similar a los constructores en otros lenguajes. La biblioteca estándar de Rust contiene numerosos ejemplos, como:

let v = Vec::new();
let s = String::new();

Aunque suelen utilizarse para construir srtucts, los métodos asociados pueden realizar cualquier otra operación relacionada con el tipo que no requiera acceder a a una instancia concreta.

11.6 Cadenas de llamadas a métodos

Es frecuente que una expresión esté formada por una cadena de llamadas a métodos. Cuando dicha cadena es larga, resulta más legible escribir cada llamada en una línea distinta.

Rust permite hacerlo sin necesidad de utilizar un carácter de continuación de línea, ya que el compilador reconoce que la expresión continúa mientras su sintaxis no haya finalizado.

Por ejemplo:

fn main() {
    let saludo = String::from("Hola")
        .to_uppercase()
        .replace("HOLA", "Buenos días");

    println!("{}", saludo);
}

Obsérvese que cada línea comienza con un punto (.), indicando que la llamada al método forma parte de la misma expresión. Este es un estilo muy habitual en el código Rust y mejora notablemente la legibilidad cuando se encadenan varias operaciones.

11.7 Ejemplo completo

El siguiente programa reúne la mayoría de los conceptos estudiados en este capítulo. Define un tipo Conexion con un método asociado para crear nuevas conexiones, varios métodos de solo lectura para consultar su estado y dos métodos de lectura y escritura para conectar y desconectar la instancia.


#[derive(Debug)]
struct Conexion {
    ip: String,
    puerto: u16,
    activa: bool,
}

impl Conexion {
    fn new(ip: String, puerto: u16) -> Conexion {
        Conexion {
            ip,
            puerto,
            activa: false,
        }
    }

    fn conectar(&mut self) {
        self.activa = true;
    }

    fn desconectar(&mut self) {
        self.activa = false;
    }

    fn esta_activa(&self) -> bool {
        self.activa
    }

    fn direccion(&self) -> String {
        self.ip.clone()
    }

    fn puerto(&self) -> u16 {
        self.puerto
    }
}

fn main() {
    let mut c = Conexion::new(String::from("193.147.71.64"), 443);
    println!("Conexión recién creada");
    println!("{:?}", c);
    println!("Conectamos");
    c.conectar();
    println!("{:?}", c);
    println!("Desconectamos");
    c.desconectar();
    println!("{:?}", c);
    println!("Activa: {}", c.esta_activa());
    println!("Endpoint: {}:{}", c.direccion(), c.puerto());
}

En este ejemplo pueden identificarse los distintos tipos de métodos estudiados:

  • new es un método asociado, ya que no recibe self y se invoca mediante el nombre del tipo.

  • esta_activa, direccion y puerto son métodos con &self, pues únicamente consultan el contenido del struct.

  • conectar y desconectar son métodos con &mut self, ya que modifican el estado del struct.

No aparece ningún método con self, ya que este tipo de métodos es menos frecuente y suele reservarse para situaciones en las que el método necesita tomar posesión del struct.

11.8 Resumen

En este capítulo hemos visto cómo asociar comportamiento a los struct mediante métodos.

Las ideas principales son las siguientes:

  • Los métodos se definen dentro de bloques impl asociados a un tipo.
  • El primer parámetro de un método puede ser &self, &mut self o self, según la forma en que el método necesite acceder al objeto.
  • Los métodos con &self permiten consultar el estado del objeto sin modificarlo.
  • Los métodos con &mut self pueden modificar el contenido del objeto.
  • Los métodos con self consumen el objeto y transfieren su propiedad al método.
  • Los métodos asociados no reciben self y se invocan mediante el nombre del tipo; el ejemplo más habitual es el constructor convencional new.
  • Un mismo tipo puede tener varios bloques impl, lo que permite organizar mejor el código.

La combinación de struct y métodos permite definir tipos que agrupan tanto los datos como las operaciones relacionadas con ellos. En el siguiente capítulo ampliaremos esta idea mediante los tipos enumerados (enum), que permiten representar valores que pueden adoptar distintas variantes.