【问题标题】:Can this code be written without generics?可以在没有泛型的情况下编写此代码吗?
【发布时间】:2016-02-26 16:01:36
【问题描述】:

我有这个结构:

#[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# 之类的语言,它可能只是一个使用接口作为参数的非泛型方法,应该粗略地转换为 Rust 中的特征。

对弗拉基米尔出色回答的跟进

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

我认为,这对我来说很奇怪。鉴于add 看起来像这样,我希望能够使用盒装的Middleware 呼叫self.middleware_stack.add(handler)

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

但是好的,:Middleware 绑定对Box&lt;Middleware&gt; 不满意,再想一想这实际上是有道理的。否则无论如何我都会在上面的代码中使用双重装箱。

如果我将使用更改为:

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&lt;Middleware&gt;&amp;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 作为简单参数,如果编译器在幕后或多或少会这样做?

【问题讨论】:

    标签: generics rust


    【解决方案1】:

    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 trait。

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

    也不是所有的特征都可以产生特征对象,或者换句话说,不是所有的特征都对特征对象有用。例如,签名中有Self 类型的特征方法不能用于特征对象。原因应该很明显:这些方法需要在调用站点知道实现者的具体类型,而 trait 对象则不然。

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

    我更想了解为什么它必须使用泛型,因为 来自 Java 的 C# 等语言,它可能只是一个非 使用接口作为参数的泛型方法 粗略地转化为 Rust 中的一个特征。

    您可以从以上所有内容中看到,泛型在绝大多数情况下都更加有用和高效。因此,Rust促进使用泛型而不是 trait 对象。所以,当你需要编写一个泛型函数时,你需要从泛型开始,只有在你真正需要的时候才使用 trait 对象。在这方面,Rust 与 Java 或 C# 不同。

    但是,您的具体问题似乎在于您正在调用 middleware_stack.add() 方法,根据错误消息,该方法似乎是通用的。它应该是这样的:

    fn add<T: Middleware+Send>(&mut self, handler: T) { ... }
    

    (与您的通用版本完全一样)。这是您的错误的原因:您试图在此函数中传递特征对象。但是 trait 对象并没有实现相应的 trait,也就是说,它们的类型不满足各自的 trait 界限,除非这些 trait 明确地在它们身上实现:

    impl Middleware for Box<Middleware> { ... }
    

    好像不是这样,Middleware 并没有在Box&lt;Middleware&gt; 上实现。因此,您不能在其上调用 add&lt;T: Middleware+Send&gt;() 函数。

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

    这不是它的工作方式。当使用特定类型调用时,上面的通用版本是 monomorphized 的,这就是我帖子中的第一个示例所显示的。例如,如果您有impl Middleware for SomeHandler 并调用self.add(SomeHandler { ... }),编译器将生成add() 方法的专用版本:

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

    它的工作原理应该非常简单。

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

    最后一次跟进。在上面的例子中,你会喜欢泛型 实现优于非通用实现,对吗?基本上 因为您不想将拳击一直“泄漏”到 来电者,对吧?至少对我来说,这将是最烦人的事情。 我不想使用 Box 作为参数并强制 调用者调用利用(框 some_middleware)。这样更美 使用不强制装箱的通用实现 一路攀升。这会是核心动机吗?

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

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

    【讨论】:

    • 感谢非常详细的回答!我在我的问题中留下了跟进。如果您能就此发表一些反馈意见,那就太好了。
    • @Christoph,我已经为您的后续行动添加了答案。不过,它们与 Arjan 的答案基本相同:)
    • 太棒了!这些是最后丢失的部分。我倾向于问许多特殊的(跟进)问题,就好像我五岁时一样。那是因为我想吸收每一点,以便真正深入了解。您的回答对我帮助很大,值得一百万个赞:) 感谢您抽出这么多时间!
    【解决方案2】:

    编译器不会翻译这个:

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

    【讨论】:

    • 最后一次跟进。在上面的例子中,你会喜欢泛型实现而不是非泛型实现,对吧?基本上是因为您不想将拳击一直“泄漏”给呼叫者,对吗?至少对我来说,那将是最烦人的事情。我不想使用Box&lt;Middleware&gt; 作为参数并强制调用者调用utilize(box some_middleware)。使用不强制装箱的通用实现更加美观。这会是核心动机吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-16
    • 2021-02-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多