Sobrecarga de operadores
Nesta aula, exploramos a sobrecarga de operadores em Rust, que permite definir comportamentos personalizados para operadores como +, *, [] em tipos definidos pelo usuário. Aprendemos a implementar traits como Add, Mul e Index, seguindo as regras de consistência e imutabilidade da linguagem, com exemplos práticos de um tipo NúmeroComplexo e uma Matriz.
A sobrecarga de operadores em Rust é uma funcionalidade que permite que operadores como +, *, [] e outros sejam redefinidos para tipos definidos pelo usuário. Diferente de linguagens como C++, Rust não permite a criação de novos operadores nem a sobrecarga arbitrária de qualquer operador; em vez disso, ela fornece um conjunto de traits (como Add, Mul, Index) que, quando implementados para um tipo, habilitam o uso dos operadores correspondentes. Essa abordagem garante consistência e previsibilidade, pois o significado dos operadores permanece ligado a traits bem definidos.
Nesta aula, vamos aprender a implementar os traits mais comuns de operadores aritméticos e de indexação. Veremos também como lidar com ownership e borrowing, já que os métodos dos traits recebem self por referência ou por valor, dependendo do operador. Por fim, exemplos práticos mostrarão como criar tipos como números complexos e matrizes que suportam operações matemáticas e acesso indexado.
Traits Add, Mul, etc.
Os operadores aritméticos em Rust são definidos por traits no módulo std::ops. Cada operador tem seu próprio trait: Add para +, Sub para -, Mul para *, Div para /, Rem para %, entre outros. Esses traits geralmente têm um tipo associado Output que define o tipo do resultado da operação. Por exemplo, o trait Add é definido como:
pub trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
O parâmetro de tipo RHS (right-hand side) permite que o operando direito seja de um tipo diferente do esquerdo, com valor padrão igual a Self. Isso significa que podemos somar tipos diferentes, desde que implementemos o trait adequadamente. Por exemplo, podemos querer somar um Complex com um f64, ou um Vector com um escalar.
Além dos operadores binários, existem também os operadores unários como Neg (para o sinal negativo) e Not (para !). A implementação segue o mesmo padrão: implementar o trait correspondente com o método apropriado.
Index
O operador de indexação [] é representado pelos traits Index e IndexMut. O trait Index permite acesso somente leitura, enquanto IndexMut permite acesso mutável. Suas definições são:
pub trait Index<Idx> {
type Output: ?Sized;
fn index(&self, index: Idx) -> &Self::Output;
}
pub trait IndexMut<Idx>: Index<Idx> {
fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}
O tipo Idx é o tipo do índice, que pode ser qualquer coisa: um usize, um intervalo (Range), uma tupla, etc. O tipo associado Output é o tipo do elemento retornado. Note que Index retorna uma referência (&Self::Output), e IndexMut retorna uma referência mutável. Isso permite que o operador [] seja usado tanto para leitura quanto para escrita, dependendo do contexto.
Um exemplo comum é implementar Index para uma matriz bidimensional, onde o índice pode ser uma tupla (usize, usize).
Implementando
Para implementar um trait de operador, devemos usar a sintaxe impl Trait for Tipo. Vamos considerar um tipo NumeroComplexo que representa números complexos (parte real e imaginária como f64). Queremos que ele suporte soma (+) e multiplicação (*) com outro NumeroComplexo.
use std::ops::{Add, Mul};
#[derive(Debug, Clone, Copy)]
struct NumeroComplexo {
real: f64,
imag: f64,
}
impl Add for NumeroComplexo {
type Output = NumeroComplexo;
fn add(self, other: NumeroComplexo) -> NumeroComplexo {
NumeroComplexo {
real: self.real + other.real,
imag: self.imag + other.imag,
}
}
}
impl Mul for NumeroComplexo {
type Output = NumeroComplexo;
fn mul(self, other: NumeroComplexo) -> NumeroComplexo {
NumeroComplexo {
real: self.real * other.real - self.imag * other.imag,
imag: self.real * other.imag + self.imag * other.real,
}
}
}
Agora, podemos usar os operadores + e * com instâncias de NumeroComplexo:
fn main() {
let a = NumeroComplexo { real: 1.0, imag: 2.0 };
let b = NumeroComplexo { real: 3.0, imag: 4.0 };
let c = a + b;
let d = a * b;
println!("Soma: {:?}", c);
println!("Produto: {:?}", d);
}
Para implementar Index, considere um tipo Matriz que armazena dados em um vetor unidimensional, mas queremos acessar elementos com índices bidimensionais (linha, coluna):
use std::ops::Index;
struct Matriz {
linhas: usize,
colunas: usize,
dados: Vec<f64>,
}
impl Index<(usize, usize)> for Matriz {
type Output = f64;
fn index(&self, (linha, coluna): (usize, usize)) -> &f64 {
&self.dados[linha * self.colunas + coluna]
}
}
// Para permitir escrita, implementamos IndexMut
impl IndexMut<(usize, usize)> for Matriz {
fn index_mut(&mut self, (linha, coluna): (usize, usize)) -> &mut f64 {
&mut self.dados[linha * self.colunas + coluna]
}
}
Com isso, podemos usar matriz[(0,0)] para acessar o elemento.
Exemplos
Vamos a um exemplo completo que utiliza sobrecarga de operadores para criar um tipo Ponto que suporte soma e multiplicação por escalar, e também indexação para acessar coordenadas x e y.
use std::ops::{Add, Mul, Index};
#[derive(Debug, Clone, Copy)]
struct Ponto {
x: f64,
y: f64,
}
// Soma de dois pontos
impl Add for Ponto {
type Output = Ponto;
fn add(self, other: Ponto) -> Ponto {
Ponto {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
// Multiplicação por escalar (f64)
impl Mul<f64> for Ponto {
type Output = Ponto;
fn mul(self, escalar: f64) -> Ponto {
Ponto {
x: self.x * escalar,
y: self.y * escalar,
}
}
}
// Indexação: 0 para x, 1 para y
impl Index<usize> for Ponto {
type Output = f64;
fn index(&self, idx: usize) -> &f64 {
match idx {
0 => &self.x,
1 => &self.y,
_ => panic!("Índice fora dos limites: {}", idx),
}
}
}
fn main() {
let p1 = Ponto { x: 1.0, y: 2.0 };
let p2 = Ponto { x: 3.0, y: 4.0 };
let soma = p1 + p2;
let escalado = p1 * 2.5;
println!("Soma: {:?}", soma);
println!("Escalado: {:?}", escalado);
println!("Coordenada x de p1: {}", p1[0]);
println!("Coordenada y de p2: {}", p2[1]);
}
Neste exemplo, note que a multiplicação é definida apenas para Ponto * f64. Se quisermos também f64 * Ponto, precisaríamos implementar Mul<Ponto> for f64 (ou usar uma abordagem com trait genérico). Além disso, a indexação com usize permite acesso às coordenadas como se fosse um array.
Boas práticas
Ao sobrecarregar operadores, mantenha a semântica intuitiva: o operador + deve representar adição ou concatenação, * multiplicação ou produto, etc. Evite surpresas. Sempre implemente o trait Add junto com AddAssign (para o operador +=) se fizer sentido, e o mesmo para outros operadores. Lembre-se de que os métodos consomem self por valor; se quiser evitar cópias, implemente para referências também (ex.: impl Add for &Ponto).
Referências
- Documentação oficial std::ops
- The Rust Book: Advanced Traits - Newtype Pattern
- Rust by Example: Operator Overloading
- Rust Reference: Operator Expressions
- Crate num: coleção de tipos e traits numéricos
Exercícios
-
Implemente o trait
Subpara o tipoNumeroComplexo(subtração). Use a mesma estrutura da aula.✓ Resposta:impl Sub for NumeroComplexo { type Output = NumeroComplexo; fn sub(self, other: NumeroComplexo) -> NumeroComplexo { NumeroComplexo { real: self.real - other.real, imag: self.imag - other.imag, } } } -
Implemente o trait
Indexpara um tipoVetorque armazenaf64em umVec. O índice deve serusize.✓ Resposta:struct Vetor { dados: Vec<f64>, } impl Index<usize> for Vetor { type Output = f64; fn index(&self, idx: usize) -> &f64 { &self.dados[idx] } } -
Implemente o trait
Addpara o tipoPontode forma que ele também aceite um escalarf64(some o escalar a ambas as coordenadas).✓ Resposta:impl Add<f64> for Ponto { type Output = Ponto; fn add(self, escalar: f64) -> Ponto { Ponto { x: self.x + escalar, y: self.y + escalar, } } } -
Crie um tipo
CorRGBcom camposr,g,b(tipou8) e implemente o traitMul<f64>para escalonar as cores (multiplicar cada componente pelo escalar, truncando parau8).✓ Resposta:struct CorRGB { r: u8, g: u8, b: u8, } impl Mul<f64> for CorRGB { type Output = CorRGB; fn mul(self, escalar: f64) -> CorRGB { CorRGB { r: (self.r as f64 * escalar).round().min(255.0) as u8, g: (self.g as f64 * escalar).round().min(255.0) as u8, b: (self.b as f64 * escalar).round().min(255.0) as u8, } } } -
Implemente o trait
Negpara o tipoNumeroComplexo(operador unário-).✓ Resposta:use std::ops::Neg; impl Neg for NumeroComplexo { type Output = NumeroComplexo; fn neg(self) -> NumeroComplexo { NumeroComplexo { real: -self.real, imag: -self.imag, } } }