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:
| Operador | Trait | Método |
|---|---|---|
a + b | Add | add(self, rhs) |
a - b | Sub | sub(self, rhs) |
a * b | Mul | mul(self, rhs) |
a / b | Div | div(self, rhs) |
-a | Neg | neg(self) |
a[i] | Index | index(&self, idx) |
a[i] = v | IndexMut | index_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:
- Comutatividade (quando aplicável):
a + b == b + a - Associatividade:
(a + b) + c == a + (b + c) - Elemento neutro:
a + zero == a,a * um == a - Consistência com AddAssign: Se você implementa
AddeAddAssign, 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
Implemente para referências também: Para tipos que não são
Copy, implemente os traits para&Talém deT, 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 } } }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.
Output pode ser diferente de Self: Multiplicar
MetrosporMetrospode retornarMetrosQuadrados. Usetype Outputpara expressar isso.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.Combine com Deref para smart pointers: Para tipos wrapper, combine
Derefcom traits de operadores para criar abstrações transparentes. Veja Deref e DerefMut.AddAssign separado de Add: Implemente
AddAssignquando+=faz sentido. Ele pode ser mais eficiente queAddpor modificar in-place.
Veja Também
- Deref e DerefMut — Deref coercion e smart pointers
- Display e Debug — frequentemente implementados junto com ops traits
- From e Into — conversão entre tipos, útil com newtype pattern
- Eq e Ord — traits de comparação para tipos com operadores
- Erro E0369: Operador Não Aplicável — quando falta a implementação do trait