Trait objects em Rust (dyn)
Na página de Traits você escreveu funções que aceitam “qualquer tipo que implemente Speak”. Na página de Genéricos você viu que o Rust cria uma cópia separada de uma função genérica para cada tipo. As duas coisas funcionam muito bem quando você já sabe o tipo na hora de compilar. Mas e se você quiser uma única lista com vários tipos diferentes?
É para isso que existe o trait object (objeto de trait). Pense em um professor de música com uma sala cheia de alunos. Um toca piano, outro toca violino, outro toca bateria. O professor não precisa saber qual instrumento cada aluno tem: ele só diz “toquem!” e cada um toca do seu jeito. Um trait object deixa o seu código fazer a mesma coisa: “não sei o tipo exato, mas sei que ele consegue fazer isto”.
O problema: um Vec, tipos diferentes
Seção intitulada “O problema: um Vec, tipos diferentes”Digamos que você tem duas formas e quer guardar as duas no mesmo Vec:
struct Circle { radius: f64 }struct Square { side: f64 }
fn main() { let shapes = vec![Circle { radius: 1.0 }, Square { side: 2.0 }];}Isso não compila:
error[E0308]: mismatched types --> src/main.rs:5:47 |5 | let shapes = vec![Circle { radius: 1.0 }, Square { side: 2.0 }]; | ^^^^^^^^^^^^^^^^^^^^ expected `Circle`, found `Square`O compilador diz que os tipos não batem: ele esperava um Circle e encontrou um Square. Um Vec só guarda um tipo. Todos os itens precisam ter o mesmo tamanho na memória, para que o Rust consiga enfileirá-los um atrás do outro. Circle e Square são tipos diferentes, então não podem dividir um Vec comum.
Box<dyn Trait>: uma lista de formas diferentes
Seção intitulada “Box<dyn Trait>: uma lista de formas diferentes”A solução tem duas partes. Primeiro, dê às duas formas uma trait em comum. Depois, coloque cada forma dentro de um Box e diga que o tipo é Box<dyn Shape>:
trait Shape { fn name(&self) -> String; fn area(&self) -> f64;}
struct Circle { radius: f64 }struct Square { side: f64 }
impl Shape for Circle { fn name(&self) -> String { String::from("círculo") } fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }}
impl Shape for Square { fn name(&self) -> String { String::from("quadrado") } fn area(&self) -> f64 { self.side * self.side }}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 2.0 }), Box::new(Square { side: 3.0 }), Box::new(Circle { radius: 1.0 }), ];
for shape in &shapes { println!("{} tem área {:.2}", shape.name(), shape.area()); }}Saída:
círculo tem área 12.57quadrado tem área 9.00círculo tem área 3.14Vamos ler Box<dyn Shape> parte por parte:
dyn Shapequer dizer “algum tipo que implementaShape; vamos descobrir qual quando o programa estiver rodando”.dyné a abreviação de dynamic (dinâmico).Boxcoloca o valor na heap e guarda um ponteiro para ele (você conheceu oBoxna página de Ponteiros inteligentes). TodoBoxtem o mesmo tamanho, não importa o que está dentro. É por isso que agora oVecfica feliz.
O valor dentro da caixa (mais um ponteiro escondido que diz “este aqui é um Circle”) é o que chamamos de trait object.
&dyn Trait: pegando emprestado em vez de usar Box
Seção intitulada “&dyn Trait: pegando emprestado em vez de usar Box”Nem sempre você precisa de um Box. Se os valores já vivem em outro lugar, uma referência também serve. &dyn Shape quer dizer “uma referência emprestada para algum tipo que implementa Shape”:
trait Shape { fn area(&self) -> f64;}
struct Circle { radius: f64 }struct Square { side: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }}
impl Shape for Square { fn area(&self) -> f64 { self.side * self.side }}
fn print_area(shape: &dyn Shape) { println!("Área: {:.1}", shape.area());}
fn main() { let circle = Circle { radius: 1.0 }; let square = Square { side: 4.0 };
print_area(&circle); print_area(&square);
let both: Vec<&dyn Shape> = vec![&circle, &square]; println!("Temos {} formas", both.len());}Saída:
Área: 3.1Área: 16.0Temos 2 formasUma regra simples: use Box<dyn Trait> quando a lista deve ser dona dos valores e &dyn Trait quando você só precisa pegá-los emprestado por um tempo.
Dispatch estático vs. dinâmico
Seção intitulada “Dispatch estático vs. dinâmico”“Dispatch” quer dizer apenas “decidir qual método chamar”. O Rust pode fazer isso de dois jeitos.
O dispatch estático acontece com genéricos e impl Trait. O compilador sabe o tipo exato, então cria uma cópia especial da função para cada tipo (como você viu na página de Genéricos). A chamada vai direto para o método certo. É muito rápido.
O dispatch dinâmico acontece com dyn Trait. O compilador não sabe o tipo exato. Por isso, cada trait object carrega uma pequena tabela escondida (chamada vtable) que diz “para este valor, o area fica aqui”. Em tempo de execução, o Rust procura o método nessa tabela e depois o chama. Essa consulta tem um custo extra bem pequeno.
// Dispatch estático: o Rust cria uma cópia desta função para cada tipofn static_area(shape: &impl Shape) -> f64 { shape.area()}
// Dispatch dinâmico: uma só função; o método é procurado em tempo de execuçãofn dynamic_area(shape: &dyn Shape) -> f64 { shape.area()}As duas funções dão a mesma resposta para a mesma forma. A única diferença é como o Rust encontra o método area certo. Então, qual escolher?
Use genéricos / impl Trait quando… |
Use dyn Trait quando… |
|---|---|
| Cada chamada trabalha com um tipo conhecido | Você precisa misturar tipos diferentes na mesma coleção |
| Você quer o código mais rápido possível | Você decide o tipo em tempo de execução (por exemplo, pelo que o usuário digitou) |
| Um programa um pouco maior não incomoda | Você quer código menor e compilação mais rápida |
Um bom ponto de partida para quem está começando: comece com genéricos ou impl Trait. Use dyn quando bater no problema de “tipos diferentes na mesma lista”. Não se preocupe muito com a diferença de velocidade: na maioria dos programas você nunca vai notar.
Retornando Box<dyn Trait> de uma função
Seção intitulada “Retornando Box<dyn Trait> de uma função”Às vezes uma função precisa decidir em tempo de execução qual tipo retornar. Você pode tentar -> impl Shape, mas isso só funciona quando a função sempre retorna o mesmo tipo. Se cada ramo retorna um tipo diferente, retorne um Box<dyn Shape>:
trait Shape { fn describe(&self) -> String;}
struct Circle { radius: f64 }struct Square { side: f64 }
impl Shape for Circle { fn describe(&self) -> String { format!("um círculo de raio {}", self.radius) }}
impl Shape for Square { fn describe(&self) -> String { format!("um quadrado de lado {}", self.side) }}
fn make_shape(kind: &str, size: f64) -> Box<dyn Shape> { if kind == "circle" { Box::new(Circle { radius: size }) } else { Box::new(Square { side: size }) }}
fn main() { let a = make_shape("circle", 1.5); let b = make_shape("square", 2.0); println!("Eu criei {}", a.describe()); println!("Eu criei {}", b.describe());}Saída:
Eu criei um círculo de raio 1.5Eu criei um quadrado de lado 2Esse é um padrão comum em funções “fábrica” (factory), sistemas de plugins e código guiado por configuração: qualquer lugar onde o tipo exato depende de dados que você só vê com o programa rodando.
Um exemplo prático: enviando notificações
Seção intitulada “Um exemplo prático: enviando notificações”Aqui vai algo mais próximo de um app de verdade. Seu programa precisa avisar um usuário, mas cada usuário escolheu canais diferentes: e-mail, SMS ou os dois. Cada canal é um tipo próprio, e todos compartilham a trait Notifier:
trait Notifier { fn send(&self, message: &str) -> String;}
struct Email { address: String }struct Sms { phone: String }
impl Notifier for Email { fn send(&self, message: &str) -> String { format!("E-mail para {}: {}", self.address, message) }}
impl Notifier for Sms { fn send(&self, message: &str) -> String { format!("SMS para {}: {}", self.phone, message) }}
fn notify_all(channels: &[Box<dyn Notifier>], message: &str) { for channel in channels { println!("{}", channel.send(message)); }}
fn main() { let channels: Vec<Box<dyn Notifier>> = vec![ Box::new(Sms { phone: String::from("+55-11-5555-0100") }), ];
notify_all(&channels, "Seu pedido foi enviado!");}Saída:
E-mail para [email protected]: Seu pedido foi enviado!SMS para +55-11-5555-0100: Seu pedido foi enviado!A parte boa: se mais tarde você quiser um notificador Push, basta escrever uma struct nova e um impl Notifier for Push. A função notify_all não muda nada.
Os limites: compatibilidade com dyn
Seção intitulada “Os limites: compatibilidade com dyn”Nem toda trait pode virar um trait object. A trait precisa ser dyn compatible (compatível com dyn; livros e posts mais antigos chamam isso de object safety). As regras completas são longas, mas duas cobrem a maioria dos casos reais:
- Um método não pode retornar
Self. (Atrás de umdyn, o Rust não sabe o que éSelf, então não tem como saber o tamanho do valor de retorno.) - Um método não pode ter seus próprios parâmetros de tipo genéricos, como
fn process<T>(&self, item: T).
Veja o que acontece se você quebrar a primeira regra:
trait Shape { fn area(&self) -> f64; fn duplicate(&self) -> Self;}// ... mais adiante:let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 })];error[E0038]: the trait `Shape` is not dyn compatible |14 | let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 })]; | ^^^^^ `Shape` is not dyn compatible | 1 | trait Shape { | ----- this trait is not dyn compatible... 2 | fn area(&self) -> f64; 3 | fn duplicate(&self) -> Self; | ^^^^ ...because method `duplicate` references the `Self` type in its return type = help: consider moving `duplicate` to another traitO erro é honesto e ajuda de verdade: ele diz que a trait não é compatível com dyn porque o método duplicate usa Self no tipo de retorno, e sugere mover esse método para outra trait. Suas opções são justamente essa, ou adicionar where Self: Sized ao método. Isso diz ao Rust “este método só existe em tipos reais e conhecidos, não em trait objects”:
trait Shape { fn area(&self) -> f64; fn duplicate(&self) -> Self where Self: Sized;}É também por isso que algumas traits da biblioteca padrão, como Clone, não podem ser usadas como dyn Clone: clone() retorna Self.
Erros comuns
Seção intitulada “Erros comuns”Esquecer o Box. Dá vontade de escrever Vec<dyn Shape>:
let shapes: Vec<dyn Shape> = Vec::new();error[E0277]: the size for values of type `dyn Shape` cannot be known at compilation time --> src/main.rs:3:17 |3 | let shapes: Vec<dyn Shape> = Vec::new(); | ^^^^^^^^^^^^^^ doesn't have a size known at compile-time | = help: the trait `Sized` is not implemented for `dyn Shape`O compilador reclama que não dá para saber o tamanho de um dyn Shape em tempo de compilação. Faz sentido: um dyn Shape pode ter qualquer tamanho, desde um Square pequeno até uma struct grande com vários campos. O Rust precisa de um tamanho fixo, então um trait object sempre tem que ficar atrás de um ponteiro: Box<dyn Shape>, &dyn Shape ou Rc<dyn Shape>.
Usar dyn em todo lugar. Se uma função só recebe um tipo de cada vez, impl Trait ou genéricos são mais simples e mais rápidos (a página de Traits avançados fala mais sobre impl Trait). Guarde o dyn para quando você realmente precisar misturar tipos.
Prática
Seção intitulada “Prática”1. Um zoológico em um Vec
Seção intitulada “1. Um zoológico em um Vec”Defina uma trait Animal com fn name(&self) -> String e fn sound(&self) -> String. Implemente-a para Dog, Cow e Duck, coloque um de cada em um Vec<Box<dyn Animal>> e percorra o vetor imprimindo o som que cada animal faz. Dica: as structs podem ser vazias (struct Dog;), e cada uma entra no vetor com Box::new(...).
Ver solução
trait Animal { fn name(&self) -> String; fn sound(&self) -> String;}
struct Dog;struct Cow;struct Duck;
impl Animal for Dog { fn name(&self) -> String { String::from("O cachorro") } fn sound(&self) -> String { String::from("au au") }}
impl Animal for Cow { fn name(&self) -> String { String::from("A vaca") } fn sound(&self) -> String { String::from("muu") }}
impl Animal for Duck { fn name(&self) -> String { String::from("O pato") } fn sound(&self) -> String { String::from("quack") }}
fn main() { let zoo: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cow), Box::new(Duck)];
for animal in &zoo { println!("{} faz {}", animal.name(), animal.sound()); }}Saída:
O cachorro faz au auA vaca faz muuO pato faz quack2. Escolha uma saudação em tempo de execução
Seção intitulada “2. Escolha uma saudação em tempo de execução”Defina uma trait Greeter com fn hello(&self, name: &str) -> String e dois tipos: English e Spanish. Escreva fn make_greeter(lang: &str) -> Box<dyn Greeter>, que devolve Spanish para "es" e English para qualquer outro código. Cumprimente "Mia" para cada código em ["en", "es", "fr"]. Dica: os dois ramos do if precisam devolver o mesmo tipo, então embrulhe cada um em Box::new(...).
Ver solução
trait Greeter { fn hello(&self, name: &str) -> String;}
struct English;struct Spanish;
impl Greeter for English { fn hello(&self, name: &str) -> String { format!("Hello, {name}!") }}
impl Greeter for Spanish { fn hello(&self, name: &str) -> String { format!("¡Hola, {name}!") }}
fn make_greeter(lang: &str) -> Box<dyn Greeter> { if lang == "es" { Box::new(Spanish) } else { Box::new(English) }}
fn main() { for lang in ["en", "es", "fr"] { let greeter = make_greeter(lang); println!("{lang}: {}", greeter.hello("Mia")); }}Saída:
en: Hello, Mia!es: ¡Hola, Mia!fr: Hello, Mia!O "fr" não tem um tipo próprio, então cai no else e recebe a saudação em inglês.
- Um
Vecsó guarda um tipo. Para misturar tipos, use um trait object comoBox<dyn Trait>. Box<dyn Trait>é dono do valor;&dyn Traitpega emprestado. Um trait object sempre fica atrás de um ponteiro.- Genéricos e
impl Traitusam dispatch estático (rápido, decidido na compilação).dyn Traitusa dispatch dinâmico (uma consulta na vtable em tempo de execução). - Retorne
Box<dyn Trait>quando uma função escolhe entre tipos diferentes em tempo de execução. - Só traits dyn compatible podem virar trait objects: nada de métodos que retornam
Selfnem de métodos genéricos (a não ser que tenhamwhere Self: Sized).
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Por que você não pode colocar um Circle e um Square no mesmo Vec comum?
2. O que o dyn em Box<dyn Shape> diz ao Rust?
3. Qual destes usa dispatch estático?
4. Por que Vec<dyn Shape> (sem Box) dá erro?
5. Qual método impede uma trait de ser dyn compatible?
Pontuação: 0 / 5
Para saber mais
Seção intitulada “Para saber mais”- Using Trait Objects to Abstract over Shared Behavior — The Rust Book (em inglês): o capítulo oficial, que monta uma lista de componentes de interface gráfica com
Box<dyn Draw>. - Returning Traits with dyn — Rust by Example (em inglês): um exemplo curto e executável de uma função que devolve
Box<dyn Trait>. - Trait objects — The Rust Reference (em inglês): as regras exatas dos tipos
dyn Trait, incluindo lifetimes e marker traits extras, comoSend. - Dyn compatibility — The Rust Reference (em inglês): a lista completa de regras que uma trait precisa seguir para ser usada com
dyn. - A palavra-chave
dyn— documentação da std (em inglês): uma explicação rápida dodyn, das vtables e das vantagens e desvantagens em relação aos genéricos.