Introdução
Traits e generics são dois dos recursos mais poderosos do Rust. Juntos, eles permitem escrever código altamente reutilizável, seguro em tempo de compilação e com abstrações de custo zero. Neste tutorial, vamos explorar como definir e implementar traits, usar generics para criar código flexível e combinar ambos para construir sistemas robustos.
O Que São Traits?
Uma trait em Rust define um conjunto de comportamentos (métodos) que um tipo pode implementar. É semelhante a interfaces em outras linguagens, mas com recursos adicionais como implementações padrão e tipos associados.
Definindo uma Trait
Vamos começar definindo uma trait simples:
trait Resumo {
fn resumir(&self) -> String;
// Método com implementação padrão
fn preview(&self) -> String {
format!("{}...", &self.resumir()[..50.min(self.resumir().len())])
}
}
A trait Resumo exige que qualquer tipo que a implemente forneça o método resumir. O método preview possui uma implementação padrão que pode ser sobrescrita.
Implementando Traits
Agora vamos implementar essa trait para diferentes tipos:
struct Artigo {
titulo: String,
autor: String,
conteudo: String,
}
struct Tweet {
usuario: String,
texto: String,
curtidas: u32,
}
impl Resumo for Artigo {
fn resumir(&self) -> String {
format!("{} por {} - {}", self.titulo, self.autor, &self.conteudo[..100.min(self.conteudo.len())])
}
}
impl Resumo for Tweet {
fn resumir(&self) -> String {
format!("@{}: {} ({} curtidas)", self.usuario, self.texto, self.curtidas)
}
// Sobrescrevendo a implementação padrão
fn preview(&self) -> String {
format!("@{}: {}...", self.usuario, &self.texto[..30.min(self.texto.len())])
}
}
Usando a Trait
fn main() {
let artigo = Artigo {
titulo: String::from("Rust no Brasil"),
autor: String::from("Maria"),
conteudo: String::from("Rust é uma linguagem de programação que tem crescido muito no Brasil nos últimos anos, com comunidades ativas em várias cidades."),
};
let tweet = Tweet {
usuario: String::from("rustacean_br"),
texto: String::from("Acabei de descobrir traits em Rust!"),
curtidas: 42,
};
println!("{}", artigo.resumir());
println!("{}", tweet.resumir());
}
Generics: Código Flexível e Reutilizável
Generics permitem que você escreva código que funciona com diferentes tipos sem duplicação. O compilador do Rust realiza monomorfização, gerando código especializado para cada tipo concreto usado.
Funções Genéricas
fn maior<T: PartialOrd>(lista: &[T]) -> &T {
let mut maior = &lista[0];
for item in &lista[1..] {
if item > maior {
maior = item;
}
}
maior
}
fn main() {
let numeros = vec![34, 50, 25, 100, 65];
println!("O maior número é {}", maior(&numeros));
let caracteres = vec!['a', 'z', 'm', 'b'];
println!("O maior caractere é {}", maior(&caracteres));
}
Structs Genéricas
struct Ponto<T> {
x: T,
y: T,
}
impl<T: std::fmt::Display> Ponto<T> {
fn exibir(&self) {
println!("Ponto({}, {})", self.x, self.y);
}
}
// Implementação específica para f64
impl Ponto<f64> {
fn distancia_origem(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let ponto_inteiro = Ponto { x: 5, y: 10 };
ponto_inteiro.exibir();
let ponto_float = Ponto { x: 3.0_f64, y: 4.0_f64 };
ponto_float.exibir();
println!("Distância da origem: {}", ponto_float.distancia_origem());
}
Structs com Múltiplos Tipos Genéricos
struct Par<T, U> {
primeiro: T,
segundo: U,
}
impl<T: std::fmt::Debug, U: std::fmt::Debug> Par<T, U> {
fn new(primeiro: T, segundo: U) -> Self {
Par { primeiro, segundo }
}
fn mostrar(&self) {
println!("Par: ({:?}, {:?})", self.primeiro, self.segundo);
}
}
fn main() {
let par = Par::new("nome", 42);
par.mostrar(); // Par: ("nome", 42)
}
Trait Bounds: Restringindo Generics
Trait bounds permitem especificar quais comportamentos um tipo genérico deve ter.
Sintaxe com where
Para funções com muitos trait bounds, a cláusula where torna o código mais legível:
use std::fmt::{Display, Debug};
fn imprimir_comparacao<T>(a: &T, b: &T)
where
T: Display + PartialOrd + Debug,
{
if a > b {
println!("{} é maior que {}", a, b);
} else if a < b {
println!("{} é menor que {}", a, b);
} else {
println!("{} é igual a {}", a, b);
}
println!("Debug: {:?} vs {:?}", a, b);
}
fn main() {
imprimir_comparacao(&10, &20);
imprimir_comparacao(&"abacaxi", &"banana");
}
Retornando Tipos que Implementam Traits
fn criar_resumivel() -> impl Resumo {
Artigo {
titulo: String::from("Tutorial Rust"),
autor: String::from("Comunidade"),
conteudo: String::from("Este é um tutorial completo sobre Rust para desenvolvedores brasileiros que querem aprender a linguagem."),
}
}
Tipos Associados
Tipos associados são uma forma de conectar um tipo placeholder a uma trait, permitindo que a implementação defina o tipo concreto:
trait Iterador {
type Item;
fn proximo(&mut self) -> Option<Self::Item>;
}
struct Contador {
valor: u32,
maximo: u32,
}
impl Contador {
fn new(maximo: u32) -> Self {
Contador { valor: 0, maximo }
}
}
impl Iterador for Contador {
type Item = u32;
fn proximo(&mut self) -> Option<Self::Item> {
if self.valor < self.maximo {
self.valor += 1;
Some(self.valor)
} else {
None
}
}
}
fn main() {
let mut contador = Contador::new(5);
while let Some(valor) = contador.proximo() {
println!("Contagem: {}", valor);
}
}
A diferença entre tipos associados e generics é que, com tipos associados, só pode haver uma implementação da trait para cada tipo. Com generics, um tipo poderia implementar a trait múltiplas vezes com diferentes parâmetros de tipo.
Sobrecarga de Operadores
Em Rust, a sobrecarga de operadores é feita implementando traits do módulo std::ops:
use std::ops::{Add, Mul};
use std::fmt;
#[derive(Debug, Clone, Copy)]
struct Vetor2D {
x: f64,
y: f64,
}
impl Vetor2D {
fn new(x: f64, y: f64) -> Self {
Vetor2D { x, y }
}
fn magnitude(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
impl Add for Vetor2D {
type Output = Self;
fn add(self, outro: Self) -> Self::Output {
Vetor2D {
x: self.x + outro.x,
y: self.y + outro.y,
}
}
}
impl Mul<f64> for Vetor2D {
type Output = Self;
fn mul(self, escalar: f64) -> Self::Output {
Vetor2D {
x: self.x * escalar,
y: self.y * escalar,
}
}
}
impl fmt::Display for Vetor2D {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({:.2}, {:.2})", self.x, self.y)
}
}
fn main() {
let v1 = Vetor2D::new(3.0, 4.0);
let v2 = Vetor2D::new(1.0, 2.0);
let soma = v1 + v2;
println!("v1 + v2 = {}", soma);
println!("Magnitude de v1: {:.2}", v1.magnitude());
let escalado = v1 * 2.5;
println!("v1 * 2.5 = {}", escalado);
}
Exemplo Prático Completo: Sistema de Notificações
Vamos combinar tudo em um exemplo real:
use std::fmt;
trait Notificavel: fmt::Display {
type Destino;
fn enviar(&self, destino: &Self::Destino) -> Result<(), String>;
fn prioridade(&self) -> u8;
fn eh_urgente(&self) -> bool {
self.prioridade() > 7
}
}
#[derive(Debug)]
struct Email {
assunto: String,
corpo: String,
prioridade: u8,
}
struct EnderecoEmail(String);
impl fmt::Display for Email {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[Email] {}: {}", self.assunto, self.corpo)
}
}
impl Notificavel for Email {
type Destino = EnderecoEmail;
fn enviar(&self, destino: &Self::Destino) -> Result<(), String> {
println!("Enviando email para {}: {}", destino.0, self.assunto);
Ok(())
}
fn prioridade(&self) -> u8 {
self.prioridade
}
}
fn processar_notificacoes<N>(notificacoes: &[N])
where
N: Notificavel + fmt::Display,
{
for notif in notificacoes {
if notif.eh_urgente() {
println!("URGENTE: {}", notif);
} else {
println!("Normal: {}", notif);
}
}
}
fn main() {
let emails = vec![
Email {
assunto: String::from("Reunião amanhã"),
corpo: String::from("Lembrete da reunião de equipe"),
prioridade: 5,
},
Email {
assunto: String::from("Servidor fora do ar!"),
corpo: String::from("O servidor de produção caiu"),
prioridade: 10,
},
];
processar_notificacoes(&emails);
let destino = EnderecoEmail(String::from("dev@rustbrasil.org"));
for email in &emails {
let _ = email.enviar(&destino);
}
}
Conclusão
Traits e generics são pilares fundamentais do sistema de tipos do Rust. Com traits, definimos comportamentos compartilhados de forma abstrata. Com generics, escrevemos código que funciona com múltiplos tipos. Combinados, eles oferecem:
- Reutilização de código sem sacrificar performance
- Segurança em tempo de compilação com verificação de tipos
- Abstrações de custo zero graças à monomorfização
- Flexibilidade para modelar domínios complexos
Pratique criando suas próprias traits e estruturas genéricas. À medida que você se familiarizar com esses conceitos, perceberá que eles são a base de praticamente toda a biblioteca padrão do Rust e do ecossistema de crates.