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