Operator Overloading em Rust: std::ops

Guia completo sobre sobrecarga de operadores em Rust: traits Add, Sub, Mul, Div, Neg, Index, IndexMut e o newtype pattern.

O que são os Traits de Operadores?

Em Rust, operadores como +, -, *, /, [] e - (negação) não são mágicos — eles são chamadas a métodos definidos por traits no módulo std::ops. Quando você escreve a + b, o compilador traduz isso para a.add(b).

Isso significa que você pode definir o comportamento de operadores para seus próprios tipos implementando os traits correspondentes. Essa técnica é chamada de operator overloading (sobrecarga de operadores).

Os principais traits de std::ops:

OperadorTraitMétodo
a + bAddadd(self, rhs)
a - bSubsub(self, rhs)
a * bMulmul(self, rhs)
a / bDivdiv(self, rhs)
-aNegneg(self)
a[i]Indexindex(&self, idx)
a[i] = vIndexMutindex_mut(&mut self, idx)

Definição dos Traits Principais

// std::ops::Add
pub trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

// std::ops::Sub
pub trait Sub<Rhs = Self> {
    type Output;
    fn sub(self, rhs: Rhs) -> Self::Output;
}

// std::ops::Mul
pub trait Mul<Rhs = Self> {
    type Output;
    fn mul(self, rhs: Rhs) -> Self::Output;
}

// std::ops::Neg (operador unário -)
pub trait Neg {
    type Output;
    fn neg(self) -> Self::Output;
}

// std::ops::Index
pub trait Index<Idx: ?Sized> {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

// std::ops::IndexMut
pub trait IndexMut<Idx: ?Sized>: Index<Idx> {
    fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}

Note o tipo associado Output — ele permite que a operação retorne um tipo diferente dos operandos. Por exemplo, multiplicar uma Matriz por um Vetor pode retornar um Vetor.


Exemplos Práticos

Exemplo 1: Add e Sub para um tipo Vetor2D

use std::ops::{Add, Sub, Neg};
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq)]
struct Vec2 {
    x: f64,
    y: f64,
}

impl Vec2 {
    fn new(x: f64, y: f64) -> Self {
        Vec2 { x, y }
    }

    fn magnitude(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

impl Add for Vec2 {
    type Output = Vec2;

    fn add(self, rhs: Vec2) -> Vec2 {
        Vec2 {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

impl Sub for Vec2 {
    type Output = Vec2;

    fn sub(self, rhs: Vec2) -> Vec2 {
        Vec2 {
            x: self.x - rhs.x,
            y: self.y - rhs.y,
        }
    }
}

impl Neg for Vec2 {
    type Output = Vec2;

    fn neg(self) -> Vec2 {
        Vec2 {
            x: -self.x,
            y: -self.y,
        }
    }
}

impl fmt::Display for Vec2 {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({:.1}, {:.1})", self.x, self.y)
    }
}

fn main() {
    let a = Vec2::new(3.0, 4.0);
    let b = Vec2::new(1.0, 2.0);

    println!("a + b = {}", a + b);     // (4.0, 6.0)
    println!("a - b = {}", a - b);     // (2.0, 2.0)
    println!("-a = {}", -a);           // (-3.0, -4.0)
    println!("|a| = {:.2}", a.magnitude()); // 5.00
}

Exemplo 2: Mul com tipo diferente (escalar * vetor)

use std::ops::Mul;

#[derive(Debug, Clone, Copy)]
struct Vec2 {
    x: f64,
    y: f64,
}

// Vec2 * f64 (vetor vezes escalar)
impl Mul<f64> for Vec2 {
    type Output = Vec2;

    fn mul(self, escalar: f64) -> Vec2 {
        Vec2 {
            x: self.x * escalar,
            y: self.y * escalar,
        }
    }
}

// f64 * Vec2 (escalar vezes vetor)
impl Mul<Vec2> for f64 {
    type Output = Vec2;

    fn mul(self, vec: Vec2) -> Vec2 {
        Vec2 {
            x: self * vec.x,
            y: self * vec.y,
        }
    }
}

fn main() {
    let v = Vec2 { x: 3.0, y: 4.0 };

    let dobro = v * 2.0;
    println!("{:?}", dobro);  // Vec2 { x: 6.0, y: 8.0 }

    let triplo = 3.0 * v;
    println!("{:?}", triplo); // Vec2 { x: 9.0, y: 12.0 }
}

Exemplo 3: Index e IndexMut para container customizado

use std::ops::{Index, IndexMut};

#[derive(Debug)]
struct Matriz {
    dados: Vec<Vec<f64>>,
    linhas: usize,
    colunas: usize,
}

impl Matriz {
    fn nova(linhas: usize, colunas: usize) -> Self {
        Matriz {
            dados: vec![vec![0.0; colunas]; linhas],
            linhas,
            colunas,
        }
    }
}

// Acesso com matrix[(linha, coluna)]
impl Index<(usize, usize)> for Matriz {
    type Output = f64;

    fn index(&self, (linha, coluna): (usize, usize)) -> &f64 {
        &self.dados[linha][coluna]
    }
}

impl IndexMut<(usize, usize)> for Matriz {
    fn index_mut(&mut self, (linha, coluna): (usize, usize)) -> &mut f64 {
        &mut self.dados[linha][coluna]
    }
}

// Acesso a uma linha inteira com matrix[linha]
impl Index<usize> for Matriz {
    type Output = Vec<f64>;

    fn index(&self, linha: usize) -> &Vec<f64> {
        &self.dados[linha]
    }
}

fn main() {
    let mut m = Matriz::nova(3, 3);

    // IndexMut: atribuir valor
    m[(0, 0)] = 1.0;
    m[(1, 1)] = 1.0;
    m[(2, 2)] = 1.0;

    // Index: ler valor
    println!("m[1][1] = {}", m[(1, 1)]); // 1.0
    println!("m[0][1] = {}", m[(0, 1)]); // 0.0

    // Acessar uma linha inteira
    println!("Linha 0: {:?}", m[0]); // [1.0, 0.0, 0.0]
}

Exemplo 4: Newtype pattern com sobrecarga de operadores

O newtype pattern encapsula um tipo existente para dar-lhe nova semântica e operadores personalizados:

use std::ops::{Add, Sub};
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Reais(f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Dolares(f64);

impl Add for Reais {
    type Output = Reais;
    fn add(self, rhs: Reais) -> Reais {
        Reais(self.0 + rhs.0)
    }
}

impl Sub for Reais {
    type Output = Reais;
    fn sub(self, rhs: Reais) -> Reais {
        Reais(self.0 - rhs.0)
    }
}

impl Add for Dolares {
    type Output = Dolares;
    fn add(self, rhs: Dolares) -> Dolares {
        Dolares(self.0 + rhs.0)
    }
}

impl fmt::Display for Reais {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "R$ {:.2}", self.0)
    }
}

impl fmt::Display for Dolares {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "$ {:.2}", self.0)
    }
}

fn main() {
    let preco = Reais(100.0);
    let desconto = Reais(15.50);
    let total = preco - desconto;
    println!("Total: {}", total); // Total: R$ 84.50

    let usd1 = Dolares(50.0);
    let usd2 = Dolares(25.0);
    println!("Soma: {}", usd1 + usd2); // Soma: $ 75.00

    // ERRO DE COMPILAÇÃO: não pode somar Reais com Dolares!
    // let mistura = preco + usd1;
    // Isso é segurança de tipos em ação!
}

Exemplo 5: Traits de atribuição composta (AddAssign, MulAssign)

use std::ops::{Add, AddAssign, MulAssign};

#[derive(Debug, Clone, Copy)]
struct Contador {
    valor: i64,
}

impl Contador {
    fn novo(valor: i64) -> Self {
        Contador { valor }
    }
}

impl Add for Contador {
    type Output = Contador;
    fn add(self, rhs: Contador) -> Contador {
        Contador { valor: self.valor + rhs.valor }
    }
}

// += operador
impl AddAssign for Contador {
    fn add_assign(&mut self, rhs: Contador) {
        self.valor += rhs.valor;
    }
}

// += com i64
impl AddAssign<i64> for Contador {
    fn add_assign(&mut self, rhs: i64) {
        self.valor += rhs;
    }
}

// *= com i64
impl MulAssign<i64> for Contador {
    fn mul_assign(&mut self, rhs: i64) {
        self.valor *= rhs;
    }
}

fn main() {
    let mut c = Contador::novo(10);

    c += Contador::novo(5);
    println!("{:?}", c);  // Contador { valor: 15 }

    c += 3;
    println!("{:?}", c);  // Contador { valor: 18 }

    c *= 2;
    println!("{:?}", c);  // Contador { valor: 36 }
}

O Contrato dos Traits de Operadores

Ao implementar traits de operadores, respeite as expectativas matemáticas:

  1. Comutatividade (quando aplicável): a + b == b + a
  2. Associatividade: (a + b) + c == a + (b + c)
  3. Elemento neutro: a + zero == a, a * um == a
  4. Consistência com AddAssign: Se você implementa Add e AddAssign, eles devem produzir o mesmo resultado.

Essas propriedades não são verificadas pelo compilador, mas violá-las causará surpresas para os usuários do seu tipo.


Padrões e Boas Práticas

  1. Implemente para referências também: Para tipos que não são Copy, implemente os traits para &T além de T, evitando moves desnecessários:

    impl Add for &Vec2 {
        type Output = Vec2;
        fn add(self, rhs: &Vec2) -> Vec2 {
            Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
        }
    }
    
  2. Use o newtype pattern para segurança de tipos: Encapsule tipos primitivos para evitar erros como somar metros com segundos ou reais com dólares.

  3. Output pode ser diferente de Self: Multiplicar Metros por Metros pode retornar MetrosQuadrados. Use type Output para expressar isso.

  4. Não abuse da sobrecarga: Operadores devem ter significado intuitivo. Não use + para concatenar listas se isso não for óbvio. Se a operação não é clara, use um método nomeado.

  5. Combine com Deref para smart pointers: Para tipos wrapper, combine Deref com traits de operadores para criar abstrações transparentes. Veja Deref e DerefMut.

  6. AddAssign separado de Add: Implemente AddAssign quando += faz sentido. Ele pode ser mais eficiente que Add por modificar in-place.


Veja Também