Cierre de tipo genérico en óxido


En este breve artículo, hablaré sobre un patrón en Rust que le permite "guardar" para usar más tarde un tipo pasado a través de un método genérico. Este patrón se encuentra en el código fuente de las bibliotecas Rust y a veces también lo uso en mis proyectos. No pude encontrar publicaciones sobre él en la red, así que le di mi nombre: "Cierre de tipo generalizado", y en este artículo quiero decirle qué es, por qué y cómo se puede usar.


El problema


En Rust, un sistema de tipo estático desarrollado y sus capacidades estáticas son suficientes para probablemente el 80% de los casos. Pero sucede que la escritura dinámica es necesaria cuando desea almacenar objetos de diferentes tipos en el mismo lugar. Los tipos de objetos de carácter vienen al rescate aquí: borran los tipos de objetos reales, los reducen a una determinada interfaz común definida por el tipo, y luego puede operar en estos objetos como los mismos objetos de tipo-tipos.


Esto funciona bien en la mitad de los casos restantes. Pero, ¿qué sucede si aún necesitamos restaurar los tipos de objetos borrados cuando los usamos? Por ejemplo, si el comportamiento de nuestros objetos está determinado por un tipo que no se puede usar como un objeto tipo . Esta es una situación común para rasgos con tipos asociados. ¿Qué hacer en este caso?


Solución


Para todos 'static tipos 'static (es decir, tipos que no contienen enlaces no estáticos), Rust implementa el tipo Any , que permite la conversión del objeto tipo dyn Any a una referencia al tipo de objeto original:


 let value = "test".to_string(); let value_any = &value as &dyn Any; //       String.  //   -      . if let Some(as_string) = value_any.downcast_ref::<String>() { println!("String: {}", as_string); } else { println!("Unknown type"); } 

Correr


Box también tiene un método downcast para este propósito.


Esta solución es adecuada para aquellos casos en que se conoce el tipo de fuente en el lugar de trabajo. Pero, ¿y si no es así? ¿Qué sucede si el código de llamada simplemente no conoce el tipo de fuente del objeto en el lugar de su uso? Luego, debemos recordar de alguna manera el tipo original, llevarlo a donde está definido y guardarlo junto con el objeto dyn Any type, para que luego este último se convierta al tipo original en el lugar correcto.


Los tipos generalizados en Rust pueden tratarse como variables de tipo a las que se puede pasar uno u otro valor de tipo cuando se llama. Pero en Rust no hay forma de recordar este tipo para usarlo en otro lugar. Sin embargo, hay una manera de recordar toda la funcionalidad que usa este tipo, junto con este tipo. Esta es la idea del patrón "Cierre de un tipo generalizado": el código que utiliza un tipo se ejecuta en forma de cierre, que se guarda como una función normal, porque no utiliza ningún objeto del entorno excepto los tipos generalizados.


Implementación


Veamos un ejemplo de implementación. Supongamos que queremos hacer un árbol recursivo que represente una jerarquía de objetos gráficos, en el que cada nodo puede ser una primitiva gráfica con nodos secundarios o un componente, un árbol separado 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 empaquetado de Node en la estructura Component es necesario porque la estructura Component sí se usa en el Node .


Ahora suponga que nuestro árbol es solo una representación de algún modelo con el que debería estar asociado. Además, cada componente tendrá su propio modelo:


 struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, } struct Component<Model> { node: Box<Node<Model>>, model: Model, //   Model } 

Podríamos escribir:


 enum Node<Model> { Prim(Primitive<Model>), Comp(Component<Model>), } 

Pero este código no funcionará como lo necesitamos. Porque el componente debe tener su propio modelo, y no el modelo del elemento padre, que contiene el componente. Es decir, necesitamos:


 enum Node<Model> { Prim(Primitive<Model>), Comp(Component), } struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, _model: PhantomData<Model>, //   Model } struct Component { node: Box<dyn Any>, model: Box<dyn Any>, } impl Component { fn new<Model: 'static>(node: Node<Model>, model: Model) -> Self { Self { node: Box::new(node), model: Box::new(model), } } } 

Correr


Hemos movido la indicación de un tipo específico de modelo al new método, y en el componente mismo almacenamos el modelo y el subárbol ya con tipos borrados.


Ahora agregue el método use_model , que usará el modelo, pero no será parametrizado por su 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); } } 

Correr


Tenga en cuenta que en el componente almacenamos un puntero a una función que se crea en el new método utilizando la sintaxis para definir un cierre. Pero todo lo que debe capturar desde el exterior es el tipo Model , por lo tanto, nos vemos obligados a pasar un enlace al componente en sí mismo a esta función a través de un argumento.


Parece que en lugar del cierre, podemos usar una función interna, pero dicho código no se compila. Debido a que la función interna en Rust no puede capturar tipos generalizados del externo debido al hecho de que difiere de la función habitual de nivel superior solo en visibilidad.

El método use_model puede usar en un contexto donde Model desconoce el tipo real de Model . Por ejemplo, en un árbol recursivo transversal que consta de muchos componentes diferentes con diferentes modelos.


Alternativa


Si es posible transferir la interfaz del componente a un tipo que permita la creación de un objeto tipo, entonces es mejor hacerlo y, en su lugar, usar el componente para operar en su objeto tipo:


 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; } } 

Correr


Conclusión


Resulta que los cierres en Rust pueden capturar no solo objetos del entorno, sino también tipos. Sin embargo, pueden interpretarse como funciones ordinarias. Esta propiedad se vuelve útil cuando necesita trabajar de manera uniforme con diferentes tipos sin perder información sobre ellos, si los tipos de caracteres no son aplicables.


Espero que este artículo te ayude a usar Rust. Comparte tus pensamientos en los comentarios.

Source: https://habr.com/ru/post/454310/


All Articles