简体   繁体   English

在Rust中使用接受闭包的方法创建对象安全的特性

[英]Create an Object-safe Trait in Rust with a method that accepts a closure

I want to create a trait for Map with the following definition: 我想使用以下定义为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
}

Now, the problem is that if I implement this trait, for example as MyHashMap , then I cannot have an expression like this: 现在的问题是,如果我实现了这个特征,例如作为MyHashMap ,那么我将不能有这样的表达式:

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

The error would be: 错误将是:

the trait map::Map cannot be made into an object trait map::Map无法制作为对象

How can one solve this issue? 如何解决这个问题? Because it's not a good idea to start using a Map implementation directly, as it's not a good software engineering practice. 因为直接开始使用Map实现不是一个好主意,因为这不是一个好的软件工程实践。

The main issue is that get and upsert methods in the trait accept generic type parameters. 主要问题是特征中的getupsert方法接受通用类型参数。 My first attempt was to get rid of these generic type parameters. 我的第一次尝试是摆脱这些通用类型参数。

For get method, it's possible, even though it deviates from the common signature of get in rust collections and makes its usage scenarios more limited. 对于get方法,即使它偏离了rust集合中get的通用签名,并使其使用场景更加受限制,也是可能的。 Here is the result: 结果如下:

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
}

However, I do not have any idea about the way to remove the generic type parameter in upsert . 但是,我对在upsert中删除通用类型参数的方法一无所知

Any idea on how to deal with this issue? 关于如何处理此问题的任何想法?

How can one solve this issue? 如何解决这个问题? Because it's not a good idea to start using a Map implementation directly, as it's not a good software engineering practice. 因为直接开始使用Map实现不是一个好主意,因为这不是一个好的软件工程实践。

This is good practice in Java, but not necessarily in other languages. 这是Java的良好做法,但不一定是其他语言。 For example, in dynamically typed languages, provided all Map implementations use the same naming convention for the methods, they can be substituted without a lot of code changes. 例如,在动态类型的语言中,如果所有Map实现均对方法使用相同的命名约定,则无需大量代码更改即可替换它们。

In languages like Rust, which have good type inference, you don't typically need to pollute the code with excessive type annotations. 在具有良好类型推断功能的Rust之类的语言中,通常不需要使用过多的类型标注来污染代码。 As a result, if you need to change a concrete type, there are fewer places that need to be updated, and it is less of a problem than you'd find in languages like Java. 结果,如果您需要更改具体类型,则需要更新的地方就更少了,这比在Java之类的语言中遇到的问题少。

"Good" Java has the implicit goal that you might want to swap any implementation of your abstract type at run-time . “良好” Java的隐含目标是,您可能希望在运行时交换抽象类型的任何实现。 Java makes this easy to do, so it's reasonable to do it, even though, in practice, this is needed very rarely. Java使此操作易于实现,因此这样做是合理的,尽管实际上很少需要这样做。 More likely, you will use some code that expects an abstract type, and you provide it with a concrete instance which is known at compile-time . 更可能的是,您将使用一些期望抽象类型的代码,并为它提供在编译时已知的具体实例。

This is exactly how Rust works with parameters. 这正是Rust使用参数的方式。 When you specify a M: Map parameter, you can work to any M which also implements Map . 指定M: Map参数时,您可以使用任何也实现Map M But the compiler will figure out at compile-time which concrete implementation you are actually using (this is called monomorphization). 但是编译器会在编译时确定您实际使用的是哪种具体实现(这称为“同构化”)。 If you need to change the concrete implementation, you can do so by changing just one line of code. 如果您需要更改具体的实现,则只需更改一行代码即可。 This also has huge benefits for performance. 这对于性能也有巨大的好处。

So, coming back to your first question: 因此,回到您的第一个问题:

How can one solve this issue? 如何解决这个问题?

If you really want to do this, you can introduce another trait object for the mapper function. 如果确实要执行此操作,则可以为mapper函数引入另一个 trait对象。 The reason why a trait object can't have a method with its own generic arguments is because the compiler can't know at compile-time the size of whatever will go there. 特质对象不能具有带有其自己的通用参数的方法的原因是,因为编译器在编译时无法知道将要存放的内容的大小。 So just make your function argument into a trait object too, so that this problem goes away: 因此,也只需将您的函数参数设置为特征对象,这样就可以解决此问题:

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

But my real answer is, as I've described above, to keep things simple. 但是,如上所述,我的真实答案是保持简单。 If you really need this Map abstraction, it should work perfectly well with type parameters whose instantiation is known at compile-time. 如果您确实需要此Map抽象,则它应与在编译时知道实例化的类型参数一起很好地工作。 Use trait objects for when a concrete type cannot be known at compile-time, for example where the implementation can change at run-time. 在无法在编译时知道具体类型的情况下(例如,在运行时可以更改实现的情况下),请使用trait对象。

Disclaimer: I find the premise (good practices) faulty, but still think the question worth answering. 免责声明:我认为前提(良好实践)有误,但仍然认为该问题值得回答。 Run-time polymorphism has its place, notably to reduce compilation-times. 运行时多态性占有一席之地,特别是可以减少编译时间。

It is perfectly possible to create an object-safe version of your trait, it just requires two components: 创建特征的对象安全版本是完全可能的,它只需要两个组件:

  • the methods that you wish to use via run-time polymorphism should not have generic type parameters, 您希望通过运行时多态使用的方法不应具有通用类型参数,
  • the methods that have type parameters (and which you cannot use via run-time polymorphism) should be guarded via a where Self: Sized clause. 具有类型参数(并且您无法通过运行时多态使用的方法)的方法应通过where Self: Sized子句进行保护。

It is possible to offer both alternatives of such methods, though in Rust it requires different names: 可以提供这两种方法的两种选择,尽管在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);
    }
}

Not how I chose here to provide a default implementation of upsert via upsert_erased , it reduces the number of methods the concrete type will have to implement while still offering the possibility to actually implemented it if performance warrants it. 不是我在这里选择通过upsert_erased提供upsert的默认实现的upsert_erased ,它减少了具体类型必须实现的方法的数量,同时仍然提供了在性能允许的情况下实际实现它的可能性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM