
Dans ce court article, je vais parler d'un modèle dans Rust qui vous permet de "sauvegarder" pour une utilisation ultérieure d'un type passé par une méthode générique. Ce modèle se trouve dans les bibliothèques source des bibliothèques Rust, et je l'utilise aussi parfois dans mes projets. Je n'ai pas pu trouver de publications à son sujet dans le réseau, alors je lui ai donné mon nom: «Fermeture de type généralisé», et dans cet article, je veux vous dire ce que c'est, pourquoi et comment il peut être utilisé.
Le problème
À Rust, un système de type statique développé et ses capacités statiques sont suffisants pour probablement 80% des cas. Mais il arrive que la saisie dynamique soit nécessaire lorsque vous souhaitez stocker des objets de différents types au même endroit. Les objets-types de personnages viennent à la rescousse ici: ils effacent les vrais types d'objets, les réduisent à une certaine interface commune définie par le type, puis vous pouvez opérer sur ces objets comme les mêmes objets-types-types.
Cela fonctionne bien dans la moitié des cas restants. Mais que se passe-t-il si nous avons encore besoin de restaurer les types d'objets effacés lors de leur utilisation? Par exemple, si le comportement de nos objets est déterminé par un type qui ne peut pas être utilisé comme objet-type . Il s'agit d'une situation courante pour les traits avec des types associés. Que faire dans ce cas?
Solution
Pour tous 'static
types 'static
(c'est-à-dire les types qui ne contiennent pas de liens non statiques), Rust implémente le type Any
, qui permet la conversion de l'objet type dyn Any
en une référence au type d'objet d'origine:
let value = "test".to_string(); let value_any = &value as &dyn Any;
Exécuter
Box
dispose également d'une méthode de downcast
à cet effet.
Cette solution convient aux cas où le type de source est connu sur le lieu de travail avec lui. Et si ce n'est pas le cas? Que se passe-t-il si le code appelant ne connaît tout simplement pas le type source de l'objet au lieu de son utilisation? Ensuite, nous devons en quelque sorte nous souvenir du type d'origine, le prendre là où il est défini et enregistrer avec l'objet dyn Any
type, afin que ce dernier puisse ensuite être converti au type d'origine au bon endroit.
Les types généralisés dans Rust peuvent être traités comme des variables de type dans lesquelles l'une ou l'autre valeur de type peut être transmise lors de l'appel. Mais à Rust, il n'y a aucun moyen de se souvenir de ce type pour une utilisation ultérieure ailleurs. Cependant, il existe un moyen de se souvenir de toutes les fonctionnalités utilisant ce type, avec ce type. C'est l'idée du modèle "Fermeture d'un type généralisé": le code utilisant un type est exécuté sous la forme d'une fermeture, qui est stockée comme une fonction normale, car il n'utilise aucun objet de l'environnement, sauf pour les types généralisés.
Implémentation
Regardons un exemple d'implémentation. Supposons que nous voulons créer un arbre récursif qui représente une hiérarchie d'objets graphiques, dans laquelle chaque nœud peut être soit une primitive graphique avec des nœuds enfants, soit un composant - un arbre distinct d'objets graphiques:
enum Node { Prim(Primitive), Comp(Component), } struct Primitive { shape: Shape, children: Vec<Node>, } struct Component { node: Box<Node>, } enum Shape { Rectangle, Circle, }
Node
empaquetage des Node
dans la structure des Component
est nécessaire car la structure des Component
elle-même est utilisée dans le Node
.
Supposons maintenant que notre arbre soit juste une représentation d'un modèle auquel il devrait être associé. De plus, chaque composant aura son propre modèle:
struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, } struct Component<Model> { node: Box<Node<Model>>, model: Model,
On pourrait écrire:
enum Node<Model> { Prim(Primitive<Model>), Comp(Component<Model>), }
Mais ce code ne fonctionnera pas comme nous en avons besoin. Parce que le composant doit avoir son propre modèle, et non le modèle de l'élément parent, qui contient le composant. Autrement dit, nous avons besoin de:
enum Node<Model> { Prim(Primitive<Model>), Comp(Component), } struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, _model: PhantomData<Model>,
Exécuter
Nous avons déplacé l'indication d'un type spécifique de modèle vers la new
méthode, et dans le composant lui-même, nous stockons le modèle et le sous-arbre déjà avec des types effacés.
Ajoutez maintenant la méthode use_model
, qui utilisera le modèle, mais ne sera pas paramétrée par son type:
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); } }
Exécuter
Notez que dans le composant, nous stockons un pointeur sur une fonction qui est créée dans la new
méthode en utilisant la syntaxe pour définir une fermeture. Mais tout ce qu'il doit capturer de l'extérieur est le type de Model
, nous sommes donc obligés de passer un lien vers le composant lui-même dans cette fonction via un argument.
Il semble qu'au lieu de fermer, nous pouvons utiliser une fonction interne, mais un tel code ne compile pas. Parce que la fonction interne dans Rust ne peut pas capturer les types généralisés à partir de la externe en raison du fait qu'elle ne diffère de la fonction de haut niveau habituelle que par la visibilité.
La méthode use_model
peut use_model
être utilisée dans un contexte où le type réel de Model
inconnu. Par exemple, dans une arborescence récursive composée de nombreux composants différents avec différents modèles.
Alternative
S'il est possible de transférer l'interface du composant vers un type qui permet la création d'un objet-type, il est préférable de le faire, et d'utiliser à la place le composant lui-même pour opérer sur son objet-type:
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; } }
Exécuter
Conclusion
Il s'avère que les fermetures dans Rust peuvent capturer non seulement des objets d'environnement, mais aussi des types. Cependant, ils peuvent être interprétés comme des fonctions ordinaires. Cette propriété devient utile lorsque vous devez travailler uniformément avec différents types sans perdre d'informations à leur sujet, si les types de caractères ne sont pas applicables.
J'espère que cet article vous aidera à utiliser Rust. Partagez vos pensées dans les commentaires.