As variables ​​polimórficas são permitidas?

Eu tenho várias estruturas que implementam o mesmo traço. Eu quero ramificar em alguma condição, decidindo em tempo de execução quais dessas estruturas instanciar. Então, independentemente de qual ramo eu segui, eu quero chamar methods dessa característica.

Isso é possível em Rust? Eu estou esperando para conseguir algo como o seguinte (que não compila):

trait Barks { fn bark(&self); } struct Dog; impl Barks for Dog { fn bark(&self) { println!("Yip."); } } struct Wolf; impl Barks for Wolf { fn bark(&self) { println!("WOOF!"); } } fn main() { let animal: Barks; if 1 == 2 { animal = Dog; } else { animal = Wolf; } animal.bark(); } 

Sim, mas não tão facilmente. O que você escreveu lá é que o animal deve ser uma variável do tipo Barks , mas o Barks é uma característica; uma descrição de uma interface. Os traços não possuem um tamanho definido estaticamente, já que um tipo de qualquer tamanho poderia aparecer e impl Barks . O compilador não tem ideia do tamanho do animal .

O que você precisa fazer é adicionar uma camada de indireção. Neste caso, você pode usar o Box , embora você também possa usar coisas como Rc ou referências simples:

 fn main() { let animal: Box; if 1 == 2 { animal = Box::new(Dog); } else { animal = Box::new(Wolf); } animal.bark(); } 

Aqui, eu estou alocando o Dog ou Wolf no heap, depois lançando isso para um Box . Isso é como converter um object para uma interface em algo como C # ou Java, ou converter um Dog* em um Barks* em C ++.

Uma abordagem totalmente diferente que você também poderia usar seria enums. Você poderia ter o enum Animal { Dog, Wolf } então definir um impl Animal { fn bark(&self) { ... } } . Depende se você precisa de um conjunto completamente aberto de animais e / ou múltiplas características.

Finalmente, note que “tipo de” acima. Existem várias coisas que não funcionam como em Java / C # / C ++. Por exemplo, o Rust não tem downcasting (você não pode ir do Box volta para o Box , ou de um traço para outro). Além disso, isso só funciona se o traço for “object seguro” (sem genéricos, sem usar self ou Self por valor).

DK tem uma boa explicação, vou apenas falar com um exemplo onde alocamos o Dog ou Wolf na pilha, evitando uma alocação de heap:

 fn main() { let dog; let wolf; let animal: &Barks; if 1 == 2 { dog = Dog; animal = &dog; } else { wolf = Wolf; animal = &wolf; } animal.bark(); } 

É um pouco feio, mas as referências realizam a mesma indireção que uma Box com um mínimo de sobrecarga.

Definir uma enumeração personalizada é a maneira mais eficiente de fazer isso. Isso permitirá que você aloque na pilha exatamente a quantidade de espaço necessária, ou seja, o tamanho da maior opção, mais 1 byte adicional para rastrear qual opção está armazenada. Também permite access direto sem um nível de indireção, ao contrário das soluções que usam uma Box ou uma referência de característica.

Infelizmente, requer mais placa de caldeira:

 enum WolfOrDog { IsDog(Dog), IsWolf(Wolf) } use WolfOrDog::*; impl Barks for WolfOrDog { fn bark(&self) { match *self { IsDog(ref d) => d.bark(), IsWolf(ref w) => w.bark() } } } fn main() { let animal: WolfOrDog; if 1 == 2 { animal = IsDog(Dog); } else { animal = IsWolf(Wolf); } animal.bark(); } 

Na main , usamos apenas uma única variável alocada na pilha, mantendo uma instância de nossa enumeração personalizada.