在为通用接口编写回调时,对他们定义自己负责创建和访问的本地数据可能很有用。
在C中,我只会使用一个void指针, c like示例:
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 */ }
}
在Rust中写类似的内容时,用void *
代替CC_1,但是我发现访问数据是不合理的冗长,例如:
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 */ }
}
这里有一行,我想以更紧凑的方式写作:
-
tool.custom_data.as_ref().unwrap().downcast_ref::<StructOnlyForThisTool>().unwrap()
-
tool.custom_data.as_ref().unwrap().downcast_mut::<StructOnlyForThisTool>().unwrap()
虽然每种方法是自行有意义的,但实际上,这不是我想在整个代码库中写的东西,而不是我想经常输入或轻松记住的东西。
按照惯例,在这里解开包装的用途并不危险,因为:
- 虽然只有一些工具定义了自定义数据,但始终定义的数据。
- 通过惯例设置数据时,工具只设置了自己的数据。因此,没有错误的数据。
- 任何时候都没有遵循这些约定,这是一个错误,应该惊慌。
鉴于这些约定,并假设从工具访问自定义数据是经常完成的 - 什么是简化此表达式的好方法?
一些可能的选项:
- 删除
Option
,只需使用代表None
的Box<Any>
使用CC_6,因此可以简化访问。 - 使用宏或功能隐藏杂句 - 传递
Option<Box<Any>>
:当然会起作用,但不愿意使用 - 将用作最后的度假胜地。 - 向
Option<Box<Any>>
添加一个特质,该特征揭示了与匹配unwrap_box_mut
的tool.custom_data.unwrap_box::<StructOnlyForThisTool>()
这样的方法。
更新1):自从提出这个问题时,我没有包含的一点似乎是相关的。可能有多个回调函数,例如execute
,都必须能够访问custom_data
。当时我认为这并不重要。
更新2):将其包装在tool
的功能中,因为借用检查器然后防止进一步访问tool
成员,直到铸件变量不范围,我找到了这样做的唯一可靠方法是写一个宏。
如果实现实际上只有一种具有诸如execute
之类的名称的方法,那是考虑使用封闭来捕获实现数据的有力指示。SomeTool
可以使用盒装FnMut
以类型的方式合并任意可可的方式,如本答案所示。然后,Option<Box<Any>>
0沸腾以使用(self.impl_)()
调用在struct字段实现中存储的闭合。对于更通用的方法,当您对实现有更多方法时,这也将起作用,请阅读。
类型 dataptr c模式的惯用和类型安全等效的是将实现类型和指针一起存储为特征对象。SomeTool
结构可以包含一个字段,即盒装SomeToolImpl
特征对象,其中特征指定了特定于工具的方法,例如execute
。这具有以下特征:
-
您不再需要明确的
type
字段,因为运行时类型信息已包含在特征对象中。 -
每个工具的性状方法的实现都可以以类型安全的方式访问其自己的数据,而无需铸造或拆开。这是因为特征对象的VTable会自动调用正确的特征实现的正确函数,并且尝试调用其他一个是一个编译时错误。
-
特征对象的"胖指针"表示与类型 dataptr对具有相同的性能特征 - 例如,
SomeTool
的大小将是两个指针,并且访问实现数据仍将涉及一个单个指针指针解除。
这是一个示例实现:
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();
}
请注意,在此设计中,使实现成为Option
是没有意义的,因为我们始终将工具类型与实现相关联。虽然没有数据实施是完全有效的,但必须始终具有与之关联的类型。