Convenient 'option <Box <Any>>', when success is guaranteed?

When writing callbacks for common interfaces, it may be useful for them to define their own local data, for which they are responsible for creating and accessing.

In C, I would just use the void pointer, a C-like example:

struct SomeTool { int type; void *custom_data; }; void invoke(SomeTool *tool) { StructOnlyForThisTool *data = malloc(sizeof(*data)); /* ... fill in the data ... */ tool.custom_data = custom_data; } void execute(SomeTool *tool) { StructOnlyForThisTool *data = tool.custom_data; if (data.foo_bar) { /* do something */ } } 

When writing something similar in Rust, replacing void * with Option<Box<Any>> , however, I found that data access was unreasonably detailed , for example:

 struct SomeTool { type: i32, custom_data: Option<Box<Any>>, }; fn invoke(tool: &mut SomeTool) { let data = StructOnlyForThisTool { /* my custom data */ } /* ... fill in the data ... */ tool.custom_data = Some(Box::new(custom_data)); } fn execute(tool: &mut SomeTool) { let data = tool.custom_data.as_ref().unwrap().downcast_ref::<StructOnlyForThisTool>().unwrap(); if data.foo_bar { /* do something */ } } 

There is one line here that I would like to write more compactly:

  • tool.custom_data.as_ref().unwrap().downcast_ref::<StructOnlyForThisTool>().unwrap()
  • tool.custom_data.as_ref().unwrap().downcast_mut::<StructOnlyForThisTool>().unwrap()

Although each method makes sense on its own, in practice this is not something that I would like to write based on code, not something that I would like to print often or easily remember.

By convention, using a U-turn here is not dangerous, because:

  • So far, only some tools define user data, those that always define it.
  • When data is set, by agreement, the tool only ever sets its own data. Thus, there is no chance of receiving incorrect data.
  • At any time when these agreements are not respected, his mistake should panic.

Given these conventions and assuming that accessing user data from the tool is something that is often done - what would be a good way to simplify this expression?


Some possible options:

  • Remove Option , just use Box<Any> with Box::new(()) representing None so access can be simplified a bit.
  • Use a macro or function to hide verbosity - passing to Option<Box<Any>> : will work, of course, but does not prefer - will use it as a last resort.
  • Add a dash to Option<Box<Any>> , which provides a method such as tool.custom_data.unwrap_box::<StructOnlyForThisTool>() with an unwrap_box_mut match.

Update 1): since asking this question, a question that I did not include seems relevant. There may be several callback functions, such as execute , that must have access to custom_data . At that time, I did not think it was important to indicate this.

Update 2): Wrapping this in the function that the tool accepts is not practical, since the loan checker prevents further access to the tool members until the transfer variable goes out of scope, I found the only reliable way to do this was to write a macro .

+6
source share
1 answer

If there really is only one method in the implementation with a name of type execute , this is a strong sign to consider using closures to capture implementation data. SomeTool may include an arbitrary type-erased call, using in this answer as shown. execute() then boils down to calling the closure stored in the closure of the implementation of the structure field using (self.impl_)() . For a more general approach that will also work when you have more methods to implement, read on.

The idiomatic and type-safe equivalent of type + dataptr C is to keep the implementation type and data pointer together as a trait object. The SomeTool structure can contain one field, an object with SomeToolImpl captions, in which this flag indicates special methods, such as execute . It has the following characteristics:

  • You no longer need an explicit type field, because information about the type of startup time is included in the object's object.

  • Each implementation of instrumental methods can access their own data in a safe manner without dropping or turning around. This is because the vtable tag object automatically calls the correct function to correctly implement the tag, and it is a compile-time error to try to call another.

  • The bold pointer representation of an object object has the same performance characteristics as a pair of type + dataptr - for example, the size of SomeTool will be two pointers, and access to implementation data will still include a single pointer spread.

Here is an example implementation:

 struct SomeTool { impl_: Box<SomeToolImpl>, } impl SomeTool { fn execute(&mut self) { self.impl_.execute(); } } trait SomeToolImpl { fn execute(&mut self); } struct SpecificTool1 { foo_bar: bool } impl SpecificTool1 { pub fn new(foo_bar: bool) -> SomeTool { let my_data = SpecificTool1 { foo_bar: foo_bar }; SomeTool { impl_: Box::new(my_data) } } } impl SomeToolImpl for SpecificTool1 { fn execute(&mut self) { println!("I am {}", self.foo_bar); } } struct SpecificTool2 { num: u64 } impl SpecificTool2 { pub fn new(num: u64) -> SomeTool { let my_data = SpecificTool2 { num: num }; SomeTool { impl_: Box::new(my_data) } } } impl SomeToolImpl for SpecificTool2 { fn execute(&mut self) { println!("I am {}", self.num); } } pub fn main() { let mut tool1: SomeTool = SpecificTool1::new(true); let mut tool2: SomeTool = SpecificTool2::new(42); tool1.execute(); tool2.execute(); } 

Please note that in this project it makes no sense to implement the Option implementation, because we always associate the type of instrument with the implementation. Although it is true to have an implementation without data, it should always have a type associated with it.

+1
source

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


All Articles