Métodos e blocos impl
1. Introdução aos blocos impl
Em Rust, blocos impl são a estrutura fundamental para associar comportamentos a tipos. Eles permitem definir funções que operam diretamente sobre instâncias de structs, enums ou outros tipos. A principal finalidade dos blocos impl é agrupar lógica relacionada a um tipo específico, promovendo encapsulamento e organização.
A diferença essencial entre funções associadas e métodos está no primeiro parâmetro: métodos recebem self (ou uma de suas variantes), enquanto funções associadas não. A sintaxe básica segue este padrão:
struct NomeDaStruct {
campo: i32,
}
impl NomeDaStruct {
// Função associada (sem self)
fn novo(valor: i32) -> NomeDaStruct {
NomeDaStruct { campo: valor }
}
// Método (com self)
fn obter_campo(&self) -> i32 {
self.campo
}
}
2. Definindo métodos em um bloco impl
A assinatura de um método sempre inclui self como primeiro parâmetro. Rust oferece três variantes:
&self— referência imutável (apenas leitura)&mut self— referência mutável (permite alterar o estado)self— consume a instância (move ownership)
Vejamos um exemplo prático com uma struct Retangulo:
struct Retangulo {
largura: u32,
altura: u32,
}
impl Retangulo {
fn area(&self) -> u32 {
self.largura * self.altura
}
fn dobrar_tamanho(&mut self) {
self.largura *= 2;
self.altura *= 2;
}
fn destruir(self) -> (u32, u32) {
(self.largura, self.altura)
}
}
let mut ret = Retangulo { largura: 10, altura: 5 };
println!("Área: {}", ret.area()); // 50
ret.dobrar_tamanho();
println!("Nova área: {}", ret.area()); // 200
let (l, a) = ret.destruir(); // ret não pode mais ser usado
3. Métodos com parâmetros adicionais
Métodos podem aceitar argumentos além de self, permitindo operações mais complexas:
struct Ponto {
x: f64,
y: f64,
}
impl Ponto {
fn distancia(&self, outro: &Ponto) -> f64 {
let dx = self.x - outro.x;
let dy = self.y - outro.y;
(dx * dx + dy * dy).sqrt()
}
fn mover(&mut self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
}
let mut p1 = Ponto { x: 0.0, y: 0.0 };
let p2 = Ponto { x: 3.0, y: 4.0 };
println!("Distância: {}", p1.distancia(&p2)); // 5.0
p1.mover(1.0, 1.0);
println!("Nova posição: ({}, {})", p1.x, p1.y); // (1.0, 1.0)
4. Funções associadas (sem self)
Funções associadas não recebem self e são chamadas usando a sintaxe Tipo::funcao(). São comumente usadas como construtores:
struct Pessoa {
nome: String,
idade: u8,
}
impl Pessoa {
fn new(nome: &str, idade: u8) -> Self {
Pessoa {
nome: nome.to_string(),
idade,
}
}
fn aniversario(&mut self) {
self.idade += 1;
}
}
let mut pessoa = Pessoa::new("Alice", 30);
pessoa.aniversario();
println!("{} tem {} anos", pessoa.nome, pessoa.idade);
A diferença crucial: funções associadas são construtoras ou utilitárias que não precisam de uma instância existente, enquanto métodos operam sobre dados já instanciados.
5. Múltiplos blocos impl
Rust permite dividir a implementação de um tipo em vários blocos impl, melhorando a organização:
struct ContaBancaria {
saldo: f64,
titular: String,
}
// Bloco para construtores
impl ContaBancaria {
fn nova(titular: &str, saldo_inicial: f64) -> Self {
ContaBancaria {
saldo: saldo_inicial,
titular: titular.to_string(),
}
}
}
// Bloco para operações básicas
impl ContaBancaria {
fn depositar(&mut self, valor: f64) {
self.saldo += valor;
}
fn sacar(&mut self, valor: f64) -> Result<(), String> {
if valor > self.saldo {
Err("Saldo insuficiente".to_string())
} else {
self.saldo -= valor;
Ok(())
}
}
}
// Bloco para consultas
impl ContaBancaria {
fn saldo(&self) -> f64 {
self.saldo
}
fn titular(&self) -> &str {
&self.titular
}
}
Essa separação é especialmente útil em projetos grandes, onde diferentes módulos ou desenvolvedores podem trabalhar em aspectos distintos do mesmo tipo.
6. Métodos em Enums e tipos built-in
Enums também podem ter métodos, o que é extremamente poderoso:
enum Cor {
Vermelho,
Verde,
Azul,
RGB(u8, u8, u8),
}
impl Cor {
fn is_red(&self) -> bool {
matches!(self, Cor::Vermelho)
}
fn para_hex(&self) -> String {
match self {
Cor::Vermelho => "#FF0000".to_string(),
Cor::Verde => "#00FF00".to_string(),
Cor::Azul => "#0000FF".to_string(),
Cor::RGB(r, g, b) => format!("#{:02X}{:02X}{:02X}", r, g, b),
}
}
}
let cor = Cor::RGB(255, 128, 0);
println!("É vermelho? {}", cor.is_red());
println!("Hex: {}", cor.para_hex());
Tipos primitivos como i32 e String também possuem métodos definidos em seus blocos impl na biblioteca padrão:
let numero = 42_i32;
println!("{} ao quadrado: {}", numero, numero.pow(2));
let texto = String::from("rust");
println!("Tamanho: {}", texto.len());
println!("Maiúsculo: {}", texto.to_uppercase());
7. Herança de métodos e trait bounds
Com genéricos, podemos implementar métodos condicionalmente usando trait bounds:
use std::fmt::Display;
struct Par<T> {
primeiro: T,
segundo: T,
}
impl<T> Par<T> {
fn novo(primeiro: T, segundo: T) -> Self {
Par { primeiro, segundo }
}
}
// Métodos disponíveis apenas quando T implementa Display
impl<T: Display> Par<T> {
fn imprimir(&self) {
println!("({}, {})", self.primeiro, self.segundo);
}
}
// Método com where clause
impl<T> Par<T> {
fn trocar(&mut self)
where
T: Copy
{
let temp = self.primeiro;
self.primeiro = self.segundo;
self.segundo = temp;
}
}
let par = Par::novo(10, 20);
par.imprimir(); // Funciona porque i32 implementa Display
let mut par_str = Par::novo("hello", "world");
par_str.trocar(); // Funciona porque &str implementa Copy
8. Boas práticas e padrões comuns
Algumas convenções importantes ao trabalhar com métodos em Rust:
Nomenclatura: Use verbos para ações (calcular_area, enviar_email), prefixos como get_, set_, is_ para acesso a propriedades:
struct Temperatura {
celsius: f64,
}
impl Temperatura {
fn new(celsius: f64) -> Self {
Temperatura { celsius }
}
fn get_celsius(&self) -> f64 {
self.celsius
}
fn set_celsius(&mut self, valor: f64) {
self.celsius = valor;
}
fn is_congelando(&self) -> bool {
self.celsius <= 0.0
}
}
Encadeamento de métodos (builder pattern): Métodos que retornam Self permitem chamadas encadeadas:
struct Pedido {
item: String,
quantidade: u32,
desconto: f64,
}
impl Pedido {
fn novo(item: &str) -> Self {
Pedido {
item: item.to_string(),
quantidade: 1,
desconto: 0.0,
}
}
fn com_quantidade(mut self, qtd: u32) -> Self {
self.quantidade = qtd;
self
}
fn com_desconto(mut self, desc: f64) -> Self {
self.desconto = desc;
self
}
fn finalizar(self) {
println!("Pedido: {} x {} com {:.0}% desconto",
self.item, self.quantidade, self.desconto * 100.0);
}
}
Pedido::novo("Notebook")
.com_quantidade(2)
.com_desconto(0.15)
.finalizar();
Quando evitar métodos: Se uma função não precisa de acesso ao estado interno do tipo, prefira funções livres ou funções associadas. Métodos devem representar comportamentos intrinsicamente ligados ao tipo.
Referências
- The Rust Programming Language - Method Syntax — Capítulo oficial do livro sobre métodos e blocos impl, com exemplos detalhados
- Rust by Example - Methods — Exemplos práticos interativos de métodos em structs e enums
- Rust Reference - Items/Implementations — Documentação de referência oficial sobre a sintaxe e semântica de blocos impl
- Rust API Guidelines - Method Conventions — Diretrizes oficiais para nomenclatura de métodos e funções associadas
- Rust Design Patterns - Builder — Padrão builder em Rust utilizando encadeamento de métodos
- Effective Rust - Methods and Associated Functions — Guia avançado sobre quando usar métodos vs funções associadas vs funções livres