【问题标题】:Create an Object-safe Trait in Rust with a method that accepts a closure使用接受闭包的方法在 Rust 中创建对象安全特征
【发布时间】:2017-09-15 09:14:06
【问题描述】:

我想为 Map 创建一个具有以下定义的特征:

pub trait Map<K: Sync, V> {
    fn put(&mut self, k: K, v: V) -> Option<V>;
    fn upsert<U: Fn(&mut V)>(&self, key: K, value: V, updater: &U);
    fn get<Q: ?Sized>(&self, k: &Q) -> Option<V> where K: Borrow<Q>, Q: Eq + Hash + Sync;
    // other methods ommited for brevity
}

现在,问题是如果我实现这个特性,例如 MyHashMap,那么我不能有这样的表达式:

let map: Box<Map<i32, i32>> = Box::new(MyHashMap::<i32, i32>::new());

错误是:

特征map::Map 不能成为对象

如何解决这个问题?因为直接开始使用 Map 实现并不是一个好主意,因为这不是一个好的软件工程实践。

主要问题是 trait 中的 getupsert 方法接受泛型类型参数。我的第一次尝试是摆脱这些泛型类型参数。

对于 get 方法,它是可能的,尽管它偏离了 get 在 rust 集合中的常见签名,并使其使用场景更加有限。结果如下:

pub trait Map<K: Sync, V> {
    fn put(&mut self, k: K, v: V) -> Option<V>;
    fn upsert<U: Fn(&mut V)>(&self, key: K, value: V, updater: &U);
    fn get(&self, k: &K) -> Option<V>;
    // other methods ommited for brevity
}

但是,我不知道如何删除 upsert 中的泛型类型参数。

你知道如何处理这个问题吗?

【问题讨论】:

  • 因为直接开始使用 Map 实现不是一个好主意,因为它不是一个好的软件工程实践。 => 你需要更新你的工程实践。在 Rust 或 C++ 等原生语言中,使用具体实例(和泛型)而不是接口可以让编译器挤出最后一点性能。

标签: generics rust


【解决方案1】:

如何解决这个问题?因为直接开始使用 Map 实现并不是一个好主意,因为这不是一个好的软件工程实践。

这在 Java 中是很好的做法,但在其他语言中则不一定。例如,在动态类型语言中,如果所有Map 实现对方法使用相同的命名约定,则无需大量代码更改即可替换它们。

在像 Rust 这样具有良好类型推断的语言中,您通常不需要使用过多的类型注释来污染代码。因此,如果您需要更改具体类型,则需要更新的地方更少,而且与 Java 等语言相比,问题更少。

“好”Java 的隐含目标是您可能希望在运行时 交换抽象类型的任何实现。 Java 使这很容易做到,所以这样做是合理的,尽管在实践中很少需要这样做。更有可能的是,您将使用一些需要抽象类型的代码,并为它提供一个在编译时已知的具体实例。

这正是 Rust 处理参数的方式。当您指定 M: Map 参数时,您可以使用 any M,它也实现了 Map。但是编译器会在编译时找出你实际使用的具体实现(这称为单态化)。如果您需要更改具体实现,只需更改一行代码即可。这对性能也有很大的好处。

所以,回到你的第一个问题:

如何解决这个问题?

如果你真的想这样做,你可以为 mapper 函数引入 另一个 trait 对象。一个特征对象不能有一个带有自己的泛型参数的方法的原因是编译器在编译时无法知道任何东西的大小。所以只要把你的函数参数也变成一个特征对象,这样这个问题就消失了:

fn upsert(&self, key: K, value: V, updater: &Fn(&mut V));

但正如我上面所描述的,我真正的答案是保持简单。如果你真的需要这个 Map 抽象,它应该与在编译时已知实例化的类型参数一起工作得很好。在编译时无法知道具体类型的情况下使用 trait 对象,例如在运行时实现可以更改的情况。

【讨论】:

    【解决方案2】:

    免责声明:我发现前提(良好实践)有问题,但仍然认为这个问题值得回答。运行时多态性有其作用,尤其是减少编译时间。

    创建一个对象安全的 trait 版本是完全可能的,它只需要两个组件:

    • 您希望通过运行时多态性使用的方法不应具有泛型类型参数,
    • 应通过where Self: Sized 子句保护具有类型参数(并且不能通过运行时多态性使用)的方法。

    虽然在 Rust 中需要不同的名称,但可以提供这些方法的两种替代方案:

    pub trait Map<K: Sync, V> {
        fn put(&mut self, k: K, v: V) -> Option<V>;
    
        fn upsert_erased(&self, key: K, value: V, updater: &Fn(&mut V));
    
        fn upsert<U: Fn(&mut V)>(&self, key: K, value: V, updater: &U)
            where Self: Sized
        {
            self.upsert_erased(key, value, updater);
        }
    }
    

    不是我在这里选择通过upsert_erased 提供upsert 的默认实现的方式,它减少了具体类型必须实现的方法的数量,同时仍然提供了在性能保证的情况下实际实现它的可能性。

    【讨论】:

      猜你喜欢
      • 2022-10-05
      • 1970-01-01
      • 1970-01-01
      • 2021-05-11
      • 1970-01-01
      • 2016-10-05
      • 1970-01-01
      • 1970-01-01
      • 2017-10-21
      相关资源
      最近更新 更多