A sobrecarga de operadores em Rust permite que tipos definidos pelo usuário utilizem operadores como +, *, [] entre outros, através da implementação de traits específicos da biblioteca padrão. Diferente de algumas linguagens, Rust não permite criar novos operadores ou sobrecarregar operadores arbitrários; apenas os operadores definidos na linguagem podem ser sobrecarregados, e isso é feito implementando os traits correspondentes do módulo std::ops.

Isso torna o código mais expressivo e intuitivo, permitindo que tipos personalizados sejam usados de maneira semelhante aos tipos primitivos. Por exemplo, um tipo Ponto pode ser somado com + se implementar o trait Add. Nesta aula, veremos os principais traits de operadores, como implementá-los e exemplos práticos.

Traits Add, Mul, etc.

Os principais traits para operadores aritméticos estão no módulo std::ops. Cada operador binário (como +, -, *, /) possui um trait correspondente: Add, Sub, Mul, Div. Para operadores unários, temos Neg e Not. O trait Add é definido como:

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

O tipo genérico Rhs (right-hand side) permite que o operando direito seja de um tipo diferente do Self. O tipo associado Output define o tipo do resultado. Por padrão, Rhs = Self, então se não especificarmos, assume-se que ambos operandos são do mesmo tipo.

Outros traits importantes incluem: Sub, Mul, Div, Rem (resto), Neg (negação unária), Not (negação lógica), BitAnd (&), BitOr (|), BitXor (^), Shl (<<), Shr (>>). Todos seguem a mesma estrutura: recebem o operando direito como argumento e retornam o tipo Output.

Index

O trait Index permite que um tipo seja indexado com colchetes [], como em obj[i]. Sua definição é:

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

O tipo Idx é o tipo do índice (geralmente usize ou um intervalo). O método index retorna uma referência ao elemento. Para permitir atribuição via índice (como obj[i] = valor), é necessário implementar o trait IndexMut, que adiciona o método index_mut retornando &mut Self::Output.

É possível implementar Index para múltiplos tipos de índice, permitindo indexação com inteiros, strings ou outros tipos. Por exemplo, uma struct Matriz pode ser indexada com dois inteiros usando uma tupla (usize, usize).

Implementando

Para implementar um trait de operador em um tipo personalizado, usamos a sintaxe impl Trait for Tipo. Por exemplo, para implementar Add para uma struct Ponto:

use std::ops::Add;

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

impl Add for Ponto {
    type Output = Ponto;

    fn add(self, other: Ponto) -> Ponto {
        Ponto {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

Depois disso, podemos usar + entre dois Ponto:

let a = Ponto { x: 1, y: 2 };
let b = Ponto { x: 3, y: 4 };
let c = a + b;
println!("{:?}", c); // Ponto { x: 4, y: 6 }

Para implementar Mul com um escalar, podemos fazer:

impl Mul<i32> for Ponto {
    type Output = Ponto;

    fn mul(self, rhs: i32) -> Ponto {
        Ponto { x: self.x * rhs, y: self.y * rhs }
    }
}

Para implementar Index, devemos especificar o tipo do índice. Por exemplo, para indexar um Ponto com usize retornando x para índice 0 e y para índice 1:

use std::ops::Index;

impl Index<usize> for Ponto {
    type Output = i32;

    fn index(&self, i: usize) -> &i32 {
        match i {
            0 => &self.x,
            1 => &self.y,
            _ => panic!("Índice inválido"),
        }
    }
}

Para permitir mutabilidade, implemente IndexMut:

use std::ops::IndexMut;

impl IndexMut<usize> for Ponto {
    fn index_mut(&mut self, i: usize) -> &mut i32 {
        match i {
            0 => &mut self.x,
            1 => &mut self.y,
            _ => panic!("Índice inválido"),
        }
    }
}

Exemplos

Vamos criar um tipo Vetor3D que suporta soma, multiplicação por escalar e indexação.

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

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

impl Add for Vetor3D {
    type Output = Vetor3D;

    fn add(self, other: Vetor3D) -> Vetor3D {
        Vetor3D {
            x: self.x + other.x,
            y: self.y + other.y,
            z: self.z + other.z,
        }
    }
}

impl Mul<f64> for Vetor3D {
    type Output = Vetor3D;

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

impl Index<usize> for Vetor3D {
    type Output = f64;

    fn index(&self, i: usize) -> &f64 {
        match i {
            0 => &self.x,
            1 => &self.y,
            2 => &self.z,
            _ => panic!("Índice fora dos limites"),
        }
    }
}

impl IndexMut<usize> for Vetor3D {
    fn index_mut(&mut self, i: usize) -> &mut f64 {
        match i {
            0 => &mut self.x,
            1 => &mut self.y,
            2 => &mut self.z,
            _ => panic!("Índice fora dos limites"),
        }
    }
}

fn main() {
    let v1 = Vetor3D { x: 1.0, y: 2.0, z: 3.0 };
    let v2 = Vetor3D { x: 4.0, y: 5.0, z: 6.0 };
    let v3 = v1 + v2;
    println!("Soma: {:?}", v3);

    let v4 = v3 * 2.0;
    println!("Multiplicado por 2: {:?}", v4);

    println!("v4[0] = {}", v4[0]);
    v4[0] = 10.0;
    println!("v4[0] modificado: {}", v4[0]);
}

Outro exemplo comum é implementar Add para um tipo que representa uma string ou um número complexo. Rust também permite implementar operadores para referências, mas isso é mais avançado. A implementação deve seguir as regras de ownership: geralmente os métodos consomem os operandos (tomam self por valor), mas você também pode implementar para &T se quiser operar sem consumir.

Boas Práticas

  • Implemente apenas operadores que fazem sentido semanticamente para o tipo. Evite sobrecarregar operadores de forma confusa.
  • Considere implementar tanto o trait para valor quanto para referência, se necessário, para evitar clones desnecessários.
  • Use o tipo associado Output para definir o tipo de retorno adequado.
  • Lembre-se de que a precedência dos operadores não pode ser alterada; apenas a implementação do operador.

Referências

Exercícios

  1. Crie uma struct NumeroComplexo com partes real e imaginária (f64). Implemente o trait Add para somar dois números complexos.

    ✓ Resposta:
    use std::ops::Add;
    
    #[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,
            }
        }
    }
    
    fn main() {
        let a = NumeroComplexo { real: 1.0, imag: 2.0 };
        let b = NumeroComplexo { real: 3.0, imag: 4.0 };
        let c = a + b;
        println!("{:?}", c);
    }
  2. Para a struct NumeroComplexo do exercício anterior, implemente o trait Mul para multiplicar dois números complexos. Lembre-se: (a+bi)*(c+di) = (ac - bd) + (ad + bc)i.

    ✓ Resposta:
    use std::ops::Mul;
    
    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,
            }
        }
    }
  3. Crie uma struct Matriz2x2 com quatro campos f64: a, b, c, d. Implemente Index e IndexMut para indexar com tupla (usize, usize), onde (0,0) retorna a, (0,1) retorna b, (1,0) retorna c, (1,1) retorna d. Panic para índices inválidos.

    ✓ Resposta:
    use std::ops::{Index, IndexMut};
    
    struct Matriz2x2 {
        a: f64,
        b: f64,
        c: f64,
        d: f64,
    }
    
    impl Index<(usize, usize)> for Matriz2x2 {
        type Output = f64;
    
        fn index(&self, (i, j): (usize, usize)) -> &f64 {
            match (i, j) {
                (0, 0) => &self.a,
                (0, 1) => &self.b,
                (1, 0) => &self.c,
                (1, 1) => &self.d,
                _ => panic!("Índice inválido"),
            }
        }
    }
    
    impl IndexMut<(usize, usize)> for Matriz2x2 {
        fn index_mut(&mut self, (i, j): (usize, usize)) -> &mut f64 {
            match (i, j) {
                (0, 0) => &mut self.a,
                (0, 1) => &mut self.b,
                (1, 0) => &mut self.c,
                (1, 1) => &mut self.d,
                _ => panic!("Índice inválido"),
            }
        }
    }
  4. Para a struct Matriz2x2, implemente Add para somar duas matrizes elemento a elemento.

    ✓ Resposta:
    use std::ops::Add;
    
    impl Add for Matriz2x2 {
        type Output = Matriz2x2;
    
        fn add(self, other: Matriz2x2) -> Matriz2x2 {
            Matriz2x2 {
                a: self.a + other.a,
                b: self.b + other.b,
                c: self.c + other.c,
                d: self.d + other.d,
            }
        }
    }
  5. Crie uma struct Polar que representa coordenadas polares (r, theta). Implemente o trait Neg (negação unária) que retorna um novo Polar com o ângulo deslocado por π (ou seja, r permanece o mesmo, theta += π).

    ✓ Resposta:
    use std::ops::Neg;
    
    #[derive(Debug, Clone, Copy)]
    struct Polar {
        r: f64,
        theta: f64,
    }
    
    impl Neg for Polar {
        type Output = Polar;
    
        fn neg(self) -> Polar {
            Polar {
                r: self.r,
                theta: self.theta + std::f64::consts::PI,
            }
        }
    }
    
    fn main() {
        let p = Polar { r: 2.0, theta: 1.0 };
        let q = -p;
        println!("{:?}", q);
    }