Em Rust, métodos são funções associadas a um tipo (struct, enum ou trait) definidas dentro de um bloco impl. Eles permitem encapsular comportamento que opera nos dados do tipo, seguindo o paradigma de orientação a objetos, mas com foco em segurança e propriedade. Nesta aula, você aprenderá a sintaxe dos blocos impl, os diferentes tipos de receptores (self, &self, &mut self), métodos associados (que não recebem self) e o padrão de construtor new.

Entender esses conceitos é fundamental para escrever código idiomático em Rust, pois eles são usados em toda a biblioteca padrão e em bibliotecas externas. Métodos tornam o código mais legível e organizado, agrupando funcionalidades relacionadas a um tipo.

impl

O bloco impl é usado para implementar métodos para um tipo. A sintaxe básica é:

struct Retangulo {
    largura: u32,
    altura: u32,
}

impl Retangulo {
    // métodos aqui
}

Dentro do bloco impl, podemos definir funções que recebem self (ou suas variações) ou funções associadas (sem self). O bloco impl pode ser repetido para o mesmo tipo, permitindo separar métodos por funcionalidade. Exemplo:

impl Retangulo {
    fn area(&self) -> u32 {
        self.largura * self.altura
    }
}

impl Retangulo {
    fn perimetro(&self) -> u32 {
        2 * (self.largura + self.altura)
    }
}

self, &self, &mut self

O primeiro parâmetro de um método é sempre self, que representa a instância do tipo. Existem três variantes:

  • self: toma posse do valor (move). Usado quando o método consome a instância.
  • &self: empresta imutavelmente. Usado para métodos que apenas leem dados.
  • &mut self: empresta mutavelmente. Usado para métodos que modificam a instância.

Exemplo com cada variante:

struct Contador {
    valor: i32,
}

impl Contador {
    fn novo(valor_inicial: i32) -> Contador {
        Contador { valor: valor_inicial }
    }

    fn ler(&self) -> i32 {
        self.valor
    }

    fn incrementar(&mut self) {
        self.valor += 1;
    }

    fn destruir(self) {
        println!("Contador com valor {} foi destruído", self.valor);
    }
}

No exemplo, ler usa &self porque só lê, incrementar usa &mut self porque modifica, e destruir usa self porque consome a instância. A escolha correta é crucial para a segurança de memória.

Métodos associados

Métodos associados são funções definidas dentro de um bloco impl que não recebem self. Eles são chamados usando a sintaxe Tipo::metodo(). Servem para funções utilitárias ou construtores. Exemplo:

struct Ponto {
    x: f64,
    y: f64,
}

impl Ponto {
    fn distancia_origem(x: f64, y: f64) -> f64 {
        (x.powi(2) + y.powi(2)).sqrt()
    }
}

fn main() {
    let d = Ponto::distancia_origem(3.0, 4.0);
    println!("{}", d); // 5.0
}

Métodos associados são comuns para criar instâncias (como new) ou funções auxiliares que não precisam de uma instância.

Construtores (new)

Em Rust, não há construtores especiais como em outras linguagens. Por convenção, usa-se um método associado chamado new para criar e inicializar uma instância. Exemplo:

struct Usuario {
    nome: String,
    email: String,
    ativo: bool,
}

impl Usuario {
    fn new(nome: String, email: String) -> Usuario {
        Usuario {
            nome,
            email,
            ativo: true,
        }
    }
}

fn main() {
    let user = Usuario::new("Alice".to_string(), "alice@example.com".to_string());
    println!("{} está ativo: {}", user.nome, user.ativo);
}

O padrão new permite encapsular a lógica de inicialização e garantir invariantes. Pode retornar Result se a criação puder falhar. Exemplo com validação:

impl Usuario {
    fn new(nome: String, email: String) -> Result<Usuario, String> {
        if nome.is_empty() || email.is_empty() {
            return Err("Nome e email devem ser preenchidos".to_string());
        }
        Ok(Usuario {
            nome,
            email,
            ativo: true,
        })
    }
}

Boas práticas

Prefira &self sempre que o método não precisar modificar ou consumir a instância. Use &mut self apenas quando necessário. Métodos que consomem self são raros e devem ser bem justificados. Métodos associados (sem self) são úteis para construtores e funções auxiliares. Siga a convenção de nomear construtores como new para facilitar o entendimento.

Referências

Exercícios

  1. Crie uma struct Livro com campos titulo (String) e paginas (u32). Implemente um método new que recebe título e número de páginas, e um método descricao que retorna uma string formatada.

    ✓ Resposta:
    struct Livro {
        titulo: String,
        paginas: u32,
    }
    
    impl Livro {
        fn new(titulo: String, paginas: u32) -> Livro {
            Livro { titulo, paginas }
        }
    
        fn descricao(&self) -> String {
            format!("{} - {} páginas", self.titulo, self.paginas)
        }
    }
  2. Adicione um método aumentar_paginas que recebe um valor e incrementa o número de páginas. Qual deve ser o tipo de self?

    ✓ Resposta:
    impl Livro {
        fn aumentar_paginas(&mut self, incremento: u32) {
            self.paginas += incremento;
        }
    }
  3. Implemente um método associado max_paginas que recebe dois livros por referência e retorna o título do livro com mais páginas.

    ✓ Resposta:
    impl Livro {
        fn max_paginas(livro1: &Livro, livro2: &Livro) -> &String {
            if livro1.paginas >= livro2.paginas {
                &livro1.titulo
            } else {
                &livro2.titulo
            }
        }
    }
  4. Crie um método para_string que consuma a instância (tome posse) e retorne uma string com os dados do livro. O que acontece com o livro após a chamada?

    ✓ Resposta:
    impl Livro {
        fn para_string(self) -> String {
            format!("Livro: {} ({} páginas)", self.titulo, self.paginas)
        }
    }
    // O livro é movido para dentro do método e não pode mais ser usado após a chamada.
  5. Modifique o construtor new para retornar um Result que valide se o título não está vazio e o número de páginas é maior que zero.

    ✓ Resposta:
    impl Livro {
        fn new(titulo: String, paginas: u32) -> Result<Livro, String> {
            if titulo.is_empty() {
                return Err("Título não pode ser vazio".to_string());
            }
            if paginas == 0 {
                return Err("Número de páginas deve ser maior que zero".to_string());
            }
            Ok(Livro { titulo, paginas })
        }
    }