
Neste pequeno artigo, falarei sobre um padrão no Rust que permite "salvar" para usar mais tarde um tipo passado por um método genérico. Esse padrão é encontrado nas bibliotecas de origem das bibliotecas Rust e às vezes também o uso em meus projetos. Como não encontrei publicações sobre ele na rede, dei o meu nome: “Fechamento de tipo generalizado” e, neste artigo, quero lhe dizer o que é, por que e como pode ser usado.
O problema
No Rust, um sistema de tipo estático desenvolvido e seus recursos estáticos são suficientes para provavelmente 80% dos casos. Mas acontece que a digitação dinâmica é necessária quando você deseja armazenar objetos de tipos diferentes no mesmo local. Os tipos-objetos de caracteres são úteis aqui: eles apagam os tipos reais de objetos, os reduzem a uma certa interface comum definida pelo tipo e, em seguida, você pode operar nesses objetos como os mesmos tipos-tipos de objetos.
Isso funciona bem na metade dos casos restantes. Mas e se ainda precisarmos restaurar os tipos de objetos apagados ao usá-los? Por exemplo, se o comportamento de nossos objetos é determinado por um tipo que não pode ser usado como um objeto de tipo . Essa é uma situação comum para características com tipos associados. O que fazer neste caso?
Solução
Para todos 'static
tipos 'static
(ou seja, tipos que não contêm links não estáticos), o Rust implementa o tipo Any
, que permite a conversão do objeto de tipo dyn Any
em uma referência ao tipo de objeto original:
let value = "test".to_string(); let value_any = &value as &dyn Any;
Executar
Box
também possui um método de downcast
para esse fim.
Essa solução é adequada para os casos em que o tipo de fonte é conhecido no local de trabalho. Mas e se não for assim? E se o código de chamada simplesmente não souber sobre o tipo de origem do objeto no lugar de seu uso? Então, precisamos lembrar de alguma forma o tipo original, levá-lo onde está definido e salvá-lo junto com o objeto dyn Any
type, para que mais tarde este último seja convertido no tipo original no lugar certo.
Tipos generalizados no Rust podem ser tratados como variáveis de tipo nas quais um ou outro valor de tipo pode ser passado quando chamado. Mas no Rust não há como lembrar desse tipo para uso posterior em outros lugares. No entanto, existe uma maneira de lembrar toda a funcionalidade usando esse tipo, junto com esse tipo. Essa é a idéia do padrão "Fechamento de um tipo generalizado": o código que usa um tipo é executado na forma de um fechamento, que é salvo como uma função regular, porque não usa nenhum objeto do ambiente, exceto os tipos generalizados.
Implementação
Vejamos um exemplo de implementação. Suponha que desejemos criar uma árvore recursiva que represente uma hierarquia de objetos gráficos, na qual cada nó possa ser um primitivo gráfico com nós filhos ou um componente - uma árvore separada de objetos gráficos:
enum Node { Prim(Primitive), Comp(Component), } struct Primitive { shape: Shape, children: Vec<Node>, } struct Component { node: Box<Node>, } enum Shape { Rectangle, Circle, }
Node
empacotamento do Node
na estrutura Component
é necessário porque a própria estrutura Component
é usada no Node
.
Agora, suponha que nossa árvore seja apenas uma representação de algum modelo ao qual ela deve ser associada. Além disso, cada componente terá seu próprio modelo:
struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, } struct Component<Model> { node: Box<Node<Model>>, model: Model,
Poderíamos escrever:
enum Node<Model> { Prim(Primitive<Model>), Comp(Component<Model>), }
Mas esse código não funcionará como precisamos. Porque o componente deve ter seu próprio modelo, e não o modelo do elemento pai, que contém o componente. Ou seja, precisamos:
enum Node<Model> { Prim(Primitive<Model>), Comp(Component), } struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, _model: PhantomData<Model>,
Executar
Movemos a indicação de um tipo específico de modelo para o new
método e, no próprio componente, armazenamos o modelo e a subárvore já com tipos apagados.
Agora adicione o método use_model
, que usará o modelo, mas não será parametrizado por seu tipo:
struct Component { node: Box<dyn Any>, model: Box<dyn Any>, use_model_closure: fn(&Component), } impl Component { fn new<Model: 'static>(node: Node<Model>, model: Model) -> Self { let use_model_closure = |comp: &Component| { comp.model.downcast_ref::<Model>().unwrap(); }; Self { node: Box::new(node), model: Box::new(model), use_model_closure, } } fn use_model(&self) { (self.use_model_closure)(self); } }
Executar
Observe que no componente armazenamos um ponteiro para uma função criada no new
método usando a sintaxe para definir um fechamento. Mas tudo o que deve capturar de fora é o tipo de Model
; portanto, somos forçados a passar um link para o próprio componente nessa função por meio de um argumento.
Parece que, em vez de fechamento, podemos usar uma função interna, mas esse código não é compilado. Como a função interna no Rust não pode capturar tipos generalizados da externa, porque difere da função de nível superior usual apenas na visibilidade.
O método use_model
pode ser usado em um contexto em que o tipo real de Model
desconhecido. Por exemplo, em uma travessia recursiva em árvore que consiste em muitos componentes diferentes com modelos diferentes.
Alternativa
Se for possível transferir a interface do componente para um tipo que permita a criação de um objeto de texto, é melhor fazê-lo e, em vez disso, use o próprio componente para operar em seu objeto de texto:
enum Node<Model> { Prim(Primitive<Model>), Comp(Box<dyn ComponentApi>), } struct Component<Model> { node: Node<Model>, model: Model, } impl<Model> Component<Model> { fn new(node: Node<Model>, model: Model) -> Self { Self { node, model, } } } trait ComponentApi { fn use_model(&self); } impl<Model> ComponentApi for Component<Model> { fn use_model(&self) { &self.model; } }
Executar
Conclusão
Acontece que os fechamentos no Rust podem capturar não apenas objetos do ambiente, mas também tipos. No entanto, eles podem ser interpretados como funções comuns. Essa propriedade se torna útil quando você precisa trabalhar de maneira uniforme com diferentes tipos sem perder informações sobre eles, se os tipos de caracteres não forem aplicáveis.
Espero que este artigo ajude você a usar o Rust. Compartilhe seus pensamentos nos comentários.