generics - 可以在没有泛型的情况下编写此代码吗?

标签 generics rust

我有这个结构:

#[deriving(Clone)]
pub struct MiddlewareStack {
    handlers: Vec<Box<Middleware + Send>>
}

然后我有代码将处理程序添加到handlers
pub fn utilize<T: Middleware>(&mut self, handler: T){
    self.middleware_stack.add(handler);
}

这可以正常工作,但我想知道,为什么必须使用泛型呢?

所以我尝试了这个:
pub fn utilize(&mut self, handler: Middleware){
    self.middleware_stack.add(handler);
}

但这给我留下了这个错误:
error: reference to trait `Middleware` where a type is expected; try `Box<Middleware>` or `&Middleware`
       pub fn utilize(&mut self, handler: Middleware){

那好吧。特性不能直接用作参数(因为它们被删除了吗?)。但是,为什么它们作为通用类型参数合法?

因此,我继续尝试:
pub fn utilize(&mut self, handler: Box<Middleware + Send>){
    self.middleware_stack.add(handler);
}

但这给我留下了以下错误:
error: failed to find an implementation of trait middleware::Middleware for Box<middleware::Middleware:Send>
       self.middleware_stack.add(handler);

因此,我想知道:该代码真的必须使用泛型吗?不,这是我不希望它不使用泛型的一个特殊原因。我想了解的更多是为什么它必须使用泛型,因为它来自Java之类的语言,例如C#,它可能只是使用接口(interface)作为参数的非泛型方法,应该大致将其转换为Rust的特征。

Vladimirs出色回答的后续行动

you're trying to pass a trait object inside this function. But trait objects do not implement corresponding traits, that is, their types do not satisfy their respective trait bounds unless these traits are explicitly implemented on them



我认为,这对我来说很奇怪。鉴于self.middleware_stack.add(handler)看起来像这样,我希望能够使用带框的Middleware来调用add:
pub fn add<T: Middleware> (&mut self, handler: T) {
    self.handlers.push(box handler);
}

但是好吧,:Middleware绑定(bind)对Box<Middleware>不满意,并且第二个想法实际上是有道理的。否则无论如何我最终都会在上面的代码中出现两次装箱的现象。

如果我更改为:
pub fn utilize(&mut self, handler: Box<Middleware + Send>){
    self.middleware_stack.add(handler);
}

add转换为
pub fn add (&mut self, handler: Box<Middleware+Send>) {
    self.handlers.push(handler);
}

它按预期工作。注意,它们在同一模块中是而不是,因此是封装。但这意味着在调用者站点上,我必须将代码更改为utilize(box some_middleware)。

通过通用实现,我能够在层的最底部调用box,即
pub fn add<T: Middleware> (&mut self, handler: T) {
    self.handlers.push(box handler);
}

但是对于非通用实现,我必须在调用者网站上打个框,否则我会遇到:
error: reference to trait `Middleware` where a type is expected; try `Box<Middleware>` or `&Middleware`

让我们面对现实:我永远不能把Middleware作为一个简单的参数。我将始终需要Box<Middleware>&Middleware,这意味着我必须在此过程中对早期的进行装箱,而对于泛型,我可以沿途进行装箱。

我想我还没有完全理解为什么会这样。因为如果编译器翻译
pub fn add<T: Middleware> (&mut self, handler: T) {
    self.handlers.push(box handler);
}

进入:
pub fn add (&mut self, handler: Middleware) {
    self.handlers.push(box handler);
}

无论如何在某个时候。

为什么不允许将无盒装版本的Middleware用作简单参数,如果无论如何或多或少这都是编译器将在后台执行的操作?

最佳答案

Rust目前提供两种编写多态代码的方式:泛型和特征对象。

泛型以类型参数的形式存在。也就是说,函数具有由调用者选择的其他参数。然后,编译器将生成函数的相应单态版本,其中所有类型参数都将替换为具体类型:

fn add<T: Add<T, Output=T>>(a: T, b: T) -> T {
    a + b
}

// when used like this:
let (a, b) = (1, 2);
let c = add(a, b);

// roughly the following code will be generated:
fn add(a: i32, b: i32) -> i32 {
    a + b
}

您会看到,这是非常有效的:异构化可导致最快的代码,而无需依赖任何间接形式。在任何时间,编译器都知道应调用哪些函数以及确切使用了哪种类型。

另一方面,特质对象允许“擦除”给定数据的实际类型,仅留下该数据实现的特征列表。因为编译器不知道使用的实际类型,所以不知道生成与该类型的对象一起使用的代码所需的大小,因此应始终通过某些指针访问trait对象。特质对象通常在需要一些异构集合时使用,例如一个可以包含不同类型项目的向量,只要它们都具有相同的特征集:
fn show_all(v: &[&Display]) {
    for (i, item) in v.iter().enumerate() {
        println!("v[{}] = {}", i, item);
    }
}

let a = 10;
let b = "abcd";
let c = 0.9f64;
show_all(&[&a, &b, &c]);

请注意,向量包含不同实际类型的元素,但它们都满足Display特性。

但是,特征对象与泛型不同,会对性能产生影响。由于编译器不知道其具体类型,因此应使用vtables查找要在其上执行的方法。由于特征对象应始终通过指针访问,因此只能将它们保留在堆栈上或装箱以将其存储在结构中。例如,不可能将“裸”特征对象保存到结构的字段中。

同样,并非所有特征都能产生特征对象,或者换句话说,并非所有特征都对特征对象有用。例如,签名中具有Self类型的特征方法不能用于特征对象。原因应该很明显:这些方法要求在调用站点上知道实现者的具体类型,而特征对象则不是这种情况。

备注:尽管有一些限制,但实际上可以将裸特征对象存储在结构中。例如,您只能将裸特征对象存储为结构的最后一个字段,并且由于无法调整大小,因此只能通过指针访问该结构。您可以阅读更多有关未调整大小的类型(或动态调整大小的类型,它们是同义词)here的信息。

It's more that I want to understand why it has to use generics because coming from languages such as C# of Java it could just be a non generic method that uses an interface as a parameter which should roughly translate into a trait in Rust.



从以上所有内容可以看出,在绝大多数情况下,泛型更为有用和高效。因此,Rust促进了泛型而不是特征对象的使用。因此,当您需要编写泛型函数时,需要从泛型开始,然后仅在真正需要时才转为特征对象。在这方面,Rust与Java或C#不同。

但是,您的特定问题似乎在于您正在调用middleware_stack.add()方法,根据错误消息,该方法似乎是通用的。它看起来应该像这样:
fn add<T: Middleware+Send>(&mut self, handler: T) { ... }

(就像您的通用版本一样)。这是发生错误的原因:您正在尝试在此函数内传递特征对象。但是特征对象不能实现相应的特征,也就是说,除非明确在其上实现这些特征,否则它们的类型不能满足其各自的特征范围:
impl Middleware for Box<Middleware> { ... }

似乎并非如此,并且Middleware并未在Box<Middleware>上实现。因此,您无法在其上调用add<T: Middleware+Send>()函数。

如果在与utilize()结构相同的模块中定义了MiddlewareStack方法,则可以直接访问其字段:
pub fn utilize(&mut self, handler: Box<Middleware+Send>){
    self.middleware_stack.handlers.push(handler);
}

这将起作用,但是仅当此方法与MiddlewareStack结构在同一模块中定义时才有效,因为handlers字段是私有(private)的。

对后续的回答

我不确定为什么您决定编译器翻译
pub fn add<T: Middleware> (&mut self, handler: T) {
    self.handlers.push(box handler);
}

进入:
pub fn add (&mut self, handler: Middleware) {
    self.handlers.push(box handler);
}

这不是它的工作方式。上面的通用版本在使用特定类型进行调用时是单态化的,这就是我帖子中的第一个示例。例如,如果您具有impl Middleware for SomeHandler并调用self.add(SomeHandler { ... }),则编译器将生成add()方法的专用版本:
pub fn add(&mut self, handler: SomeHandler) {
    self.handlers.push(box handler);
}

并且这如何工作应该非常简单。

在对其他答案的评论中,对最新跟进的答案

Last follow up. In the above example you would favor the generic implementation over the non-generic implementation,right? Basically because you wouldn't want to "leak" the boxing all the way up to the caller, right? At least for me that would be the most annoying thing. I don't want to use Box as a parameter and force the caller to call utilize(box some_middleware). That's more beautiful with the generic implementation which doesn't enforce boxing all the way up. Would that be the core motivation?



实际上,这只是一种动机。但是,我认为最重要的一点是泛型更为有效。我在上面说过:泛型函数的单态化允许静态分派(dispatch),即编译器确切知道函数的哪个变体,并且可以基于此知识应用优化,例如内联。特质对象根本不可能进行内联,因为对特质对象方法的所有调用都应通过该对象的虚拟表。

您也可以通过@dbaupp阅读this great explanation(尽管它是对某些不同问题的答案)。只需将Go替换为Java/C#,您将获得大致相同的结果。

关于generics - 可以在没有泛型的情况下编写此代码吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24635146/

相关文章:

java - 如何从泛型类中的私有(private)ArrayList中删除元素

c# - 关于通用方法的术语

java - 是否可以打包一个泛型类?

string - 如何将带有多个参数的单个字符串传递给 std::process::Command?

file - 如何从文件更改中热加载结构

c# - 无法实现具有约束的基于多个泛型参数的方法?

Java:泛型类型类型不匹配语法错误

multithreading - 我如何总结使用 Rust 从 1 到 1000000 的并发性?

macros - 无法使用 rsfuzzy crate 中的宏

rust - 如何查看我的项目所依赖的库的 rustdoc?