Métodos e blocos impl
Esta aula explora como definir métodos em Rust usando blocos `impl`, diferenciando `self`, `&self` e `&mut self`, além de métodos associados e construtores como `new`. Aprenda a organizar comportamento de tipos de forma idiomática e segura.
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
- The Rust Programming Language - Method Syntax
- Rust Reference - impl blocks
- Rust by Example - Methods
- Box documentation (exemplo de métodos)
- Vec documentation (exemplo de métodos associados)
Exercícios
Crie uma struct
Livrocom campostitulo(String) epaginas(u32). Implemente um métodonewque recebe título e número de páginas, e um métododescricaoque 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) } }Adicione um método
aumentar_paginasque recebe um valor e incrementa o número de páginas. Qual deve ser o tipo deself?✓ Resposta:impl Livro { fn aumentar_paginas(&mut self, incremento: u32) { self.paginas += incremento; } }Implemente um método associado
max_paginasque 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 } } }Crie um método
para_stringque 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.Modifique o construtor
newpara retornar umResultque 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 }) } }