这段代码可以在没有泛型的情况下编写吗?

时间:2014-07-08 14:58:14

标签: 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);

所以,我想知道:那些代码真的必须使用泛型吗?不是,有一个特殊的原因,我不希望它不使用泛型。更多的是我想要理解为什么它必须使用泛型,因为来自诸如C#的Java之类的语言它可能只是一个非泛型方法,它使用接口作为参数,应该粗略地转换为Rust中的特征。

跟进弗拉基米尔的优秀答案

  

你试图在这个函数中传递一个特征对象。但是特征对象没有实现相应的特征,即它们的类型不满足它们各自的特征界限,除非这些特征明确地在它们上实现

我想,这对我来说很奇怪。如果self.middleware_stack.add(handler)看起来如此,我希望能够使用方框Middleware致电add

pub fn add<T: Middleware> (&mut self, handler: T) {
    self.handlers.push(box handler);
}

但确定:Middleware界限不满意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版本作为简单参数,如果这或多或少是编译器在幕后做的事情呢?

2 个答案:

答案 0 :(得分:5)

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对象允许&#34;擦除&#34;给定数据的实际类型,只留下这段数据实现的特征列表。因为编译器不知道所使用的实际类型,所以它不知道生成使用该类型对象的代码所需的大小,因此应始终通过某个指针访问特征对象。当您需要某些异构集合时,通常会使用特征对象,例如:一个向量,可以包含不同类型的项目,前提是它们都具有相同的特征集:

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特征。

然而,与仿制药不同,特质对象会对性能产生影响。因为编译器不知道它们的具体类型,所以应该使用vtable来查找要对它们执行的方法。因为trait对象应始终通过指针访问,所以您只能将它们保留在堆栈上或将它们装箱以将它们存储在结构中。无法保存&#34;裸露&#34;例如,将对象转换为结构的字段。

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

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

  

我更想明白为什么它必须使用泛型,因为   来自像C#Java这样的语言,它可能只是一个非   使用接口作为参数的泛型方法   粗略地转化为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字段是私有的。

回答后续行动

我不确定为什么你决定编译器翻译

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);
}

这是如何工作的应该非常简单。

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

  

最后跟进。在上面的例子中,你会赞成通用   实现非泛型实现,对吧?基本上   因为你不想&#34;泄漏&#34;拳击一路到达   来电,对吧?至少对我来说这将是最烦人的事情。   我不想使用Box作为参数并强制使用   调用者调用利用(box some_middleware)。那更美丽   与通用实施没有强制拳击所有   一路攀升。这会是核心动机吗?

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

你也可以通过@dbaupp阅读this great explanation(虽然这是一个有些不同的问题的答案)。只需将Go替换为Java/C#即可获得大致相同的内容。

答案 1 :(得分:1)

编译器不会翻译它:

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);
}

它将在编译时创建一个专用版本,并传递一个具体类型:

struct Foo;

impl Middleware for Foo { ... }

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