当成功得到保证时,如何方便地访问 'Option<Box<Any>>'?

6

在编写通用接口的回调函数时,定义自己的本地数据并负责创建和访问这些数据可能会很有用。

在C语言中,我只会使用void指针,如下所示:

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 *替换为Option<Box<Any>>。但是我发现访问数据过于冗长,例如:

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()
虽然每个方法本身都很有意义,但实际上我不想在整个代码库中编写这样的代码,也不想经常输入或轻易记住它们。
按照惯例,这里使用unwrap并不危险,因为:
  • 虽然只有一些工具定义了自定义数据,但定义了自定义数据的工具总是定义了它。
  • 当设置数据时,按照惯例工具只会设置自己的数据。所以没有机会得到错误的数据。
  • 任何时候如果违反这些约定,那么就是一个bug,应该panic。
鉴于这些约定,并假设从工具访问自定义数据是经常发生的事情,有什么好的方法可以简化这个表达式?
一些可能的选项:
  • 删除Option,只使用Box<Any>,使用Box::new(())表示None,这样可以稍微简化访问。
  • 使用宏或函数隐藏冗长性-当然可以使用传递Option<Box<Any>>的方式,但最好不要这样做-将其作为最后的选择。
  • 向Option<Box<Any>>添加一个trait,该trait公开一个方法,例如tool.custom_data.unwrap_box::<StructOnlyForThisTool>(),并带有匹配的unwrap_box_mut。

更新1):自提出这个问题以来,我没有包括一个看似相关的点。可能会有多个回调函数(如execute),它们都必须能够访问custom_data。当时我认为这并不重要。
更新2):将其包装在一个取tool的函数中并不实用,因为借用检查器会阻止直到转换变量超出范围之前无法进一步访问tool的成员,我发现唯一可靠的方法是编写一个宏。

5
你为什么不直接使用闭包呢?闭包的作用是避免我们需要使用函数指针和数据指针对。就像切片的作用是避免我们需要使用数据指针和长度对一样。 - oli_obk
1
SomeTool 创建访问器方法怎么样?custom_data()custom_data_mut() - Jorge Israel Peña
@JorgeIsraelPeña:我也有同样的反应;我想知道是否有什么我们忽略了,因为很明显应该创建一个函数而不是复制/粘贴 :( - Matthieu M.
拥有一个函数当然是有意义的,但来自C语言,这是否意味着每次想要使用这种访问方式的类型时都必须定义一些新函数?还是将其编写为通用函数,以Option<Box<Any>>作为参数?例如:some_box_any_unwrap_as::<StructOnlyForThisTool>(&tool.custom_data)。虽然我可以编写一个可行的宏或函数,但它们感觉有点笨拙 - 特别是如果这是供他人使用的API。 - ideasman42
1个回答

1
如果实现只有一个名为execute的方法,那么这强烈表明要考虑使用闭包来捕获实现数据。 SomeTool可以使用装箱的FnMut以类型擦除的方式合并任意可调用项,如此答案所示。然后,execute()会将存储在结构体字段实现闭包中的闭包调用简化为(self.impl_)()。对于更通用的方法,当您有更多实现方法时也会起作用,请继续阅读。
一种惯用且类型安全的类型+数据指针C模式等效方法是将实现类型和数据指针一起存储为特质对象SomeTool结构体可以包含单个字段,即装箱的SomeToolImpl特质对象,其中特质指定了特定于工具的方法,例如execute。这具有以下特征:
  • 由于运行时类型信息已经包含在特质对象中,因此您不再需要显式的type字段。

  • 每个工具的特质方法实现可以以类型安全的方式访问自己的数据,无需转换或解包。这是因为特质对象的虚表会自动调用正确的特质实现函数,尝试调用不同的函数将导致编译时错误。

  • 特质对象的“胖指针”表示具有与类型+数据指针对相同的性能特征-例如,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是没有意义的,因为我们总是将工具类型与实现关联起来。虽然拥有没有数据的实现是完全有效的,但它必须始终具有与之关联的类型。

当有多个像execute这样的方法需要时,这是否有效? - ideasman42
@ideasman42,确实如此。事实上,这就是定义特质的整个意义所在;否则,一个简单的FnMut就足够了。试一下,示例可编译 - user4815162342

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接