为啥 trait 中的泛型方法需要调整 trait 对象的大小?

Posted

技术标签:

【中文标题】为啥 trait 中的泛型方法需要调整 trait 对象的大小?【英文标题】:Why does a generic method inside a trait require trait object to be sized?为什么 trait 中的泛型方法需要调整 trait 对象的大小? 【发布时间】:2017-03-06 07:38:54 【问题描述】:

我有这个代码 (playground):

use std::sync::Arc;

pub trait Messenger : Sync + Send 
    fn send_embed<F: FnOnce(String) -> String>(&self, u64, &str, f: F)
        -> Option<u64> where Self: Sync + Send;


struct MyMessenger 
    prefix: String,

impl MyMessenger 
    fn new(s: &str) -> MyMessenger 
        MyMessenger  prefix: s.to_owned(), 
    

impl Messenger for MyMessenger 
    fn send_embed<F: FnOnce(String) -> String>(&self, channel_id: u64, text: &str, f: F) -> Option<u64> 
        println!("Trying to send embed: chid=, text=\"\"", channel_id, text);
        None
    



struct Bot 
    messenger: Arc<Messenger>,

impl Bot 
    fn new() -> Bot 
        Bot 
            messenger: Arc::new(MyMessenger::new("HELLO")),
        
    


fn main() 
    let b = Bot::new();

我想做一个多态对象(特征Messenger,多态实现之一是MyMessenger)。但是当我尝试编译它时出现错误:

error[E0038]: the trait `Messenger` cannot be made into an object
  --> <anon>:25:5
   |
25 |     messenger: Arc<Messenger>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Messenger` cannot be made into an object
   |
   = note: method `send_embed` has generic type parameters

我发现在这种情况下我必须要求Sized,但这并不能解决它。如果我将 send_embed 方法更改为以下内容:

fn send_embed<F: FnOnce(String) -> String>(&self, u64, &str, f: F)
    -> Option<u64> where Self: Sized + Sync + Send;

然后编译成功但是:

    这里为什么需要Sized?如果我们不能从 trait 对象中使用此方法,这将违反多态性。

    我们实际上不能从Arc&lt;Messenger&gt;使用这个方法然后:

    fn main() 
        let b = Bot::new();
        b.messenger.send_embed(0u64, "ABRACADABRA", |s| s);
    
    

    给予:

    error[E0277]: the trait bound `Messenger + 'static: std::marker::Sized` is not satisfied
      --> <anon>:37:17
       |
    37 |     b.messenger.send_embed(0u64, "ABRACADABRA", |s| s);
       |                 ^^^^^^^^^^ the trait `std::marker::Sized` is not implemented for `Messenger + 'static`
       |
       = note: `Messenger + 'static` does not have a constant size known at compile-time
    

我完全被困在这里。不知道如何在 trait 中使用带有泛型方法的多态性。有什么办法吗?

【问题讨论】:

这与trait object 和“对象安全”的概念有关,已经讨论了很多......但我找不到一个很好的副本来详细解释这个问题。 this question 的答案可能会有所帮助。 @AleksanderFular 您是否尝试在操场上更正代码?因为在你说之前我已经尝试过了,不幸的是这对我没有帮助。在您发表评论后,我再次尝试了,without any success. 【参考方案1】:

性状和性状

在 Rust 中,您可以使用 trait 定义一个由以下组成的接口:

关联类型, 相关常量, 相关函数。

你也可以使用特征:

作为泛型参数的编译时界限 作为类型,在引用或指针之后。

但是...只有 一些 特征可以直接用作类型。这些特征被标记为对象安全

现在认为不幸的是,存在单个 trait 关键字来定义全功能和对象安全的特征。


插曲:运行时调度如何工作?

当使用 trait 作为类型时:&amp;TraitBox&lt;Trait&gt;Rc&lt;Trait&gt;、...运行时实现使用由以下组成的胖指针:

数据指针, 虚拟指针。

方法调用通过指向虚拟表的虚拟指针分派。

对于这样的特征:

trait A 
    fn one(&self) -> usize;
    fn two(&self, other: usize) -> usize;

X 类型实现,虚拟表看起来像(&lt;X as A&gt;::one, &lt;X as A&gt;::two)

运行时调度因此由以下人员执行:

选择表中的正确成员, 使用数据指针和参数调用它。

这意味着&lt;X as A&gt;::two 看起来像:

fn x_as_a_two(this: *const (), other: usize) -> usize 
    let x = unsafe  this as *const X as &X ;
    x.two(other)


为什么我不能使用任何 trait 作为类型?什么是对象安全?

这是一个技术限制。

有许多特征功能无法在运行时调度中实现:

关联类型, 相关常量, 关联的泛型函数, 与签名中的Self 关联的函数。 ...也许其他人...

有两种方法可以表明此问题:

early:如果 trait 具有上述任何一种类型,则拒绝使用它, 迟到:拒绝在trait 上使用上述任何一种类型。

目前,Rust 选择提早指出问题:不使用上述任何特性的特征被称为 Object Safe,并且可以用作类型。

对象安全的特征不能用作类型,并立即触发错误。


现在呢?

在您的情况下,只需将该方法从编译时多态切换到运行时多态:

pub trait Messenger : Sync + Send 
    fn send_embed(&self, u64, &str, f: &FnOnce(String) -> String)
        -> Option<u64>;

有一点小问题:FnOnce 需要搬出f 并且只在这里借用,所以你需要改用FnMutFnFnMut 是下一个更通用的方法,所以:

pub trait Messenger : Sync + Send 
    fn send_embed(&self, u64, &str, f: &FnMut(String) -> String)
        -> Option<u64>;

这使得Messenger trait 对象安全,因此允许您使用&amp;MessengerBox&lt;Messenger&gt;、...

【讨论】:

我觉得&amp;FnOnce()不能调用,所以还是要改成Fn或者FnMut @ChrisEmerson:确实是好点。不可能从参考中移动。【参考方案2】:

动态调度(即通过 trait 对象调用方法)通过 vtable 调用(即使用函数指针)来工作,因为您在编译时不知道它将是哪个函数。

但是,如果您的函数是通用的,则需要对实际使用的 F 的每个实例进行不同的编译(单态化)。这意味着对于每个调用它的不同闭包类型,您将拥有一个不同的 send_embed 副本。每个闭包都是不同的类型。

这两个模型是不兼容的:你不能有一个函数指针适用于不同的类型。

但是,您也可以更改方法以使用 trait 对象,而不是编译时通用:

pub trait Messenger : Sync + Send 
    fn send_embed(&self, u64, &str, f: &Fn(String) -> String)
        -> Option<u64> where Self: Sync + Send;

(Playground)

它现在接受一个特征对象引用,而不是为每个可以是 Fn(String) -&gt; String 的类型使用不同的 send_embed。 (您也可以使用Box&lt;Fn()&gt; 或类似名称)。您必须使用FnFnMut 而不是FnOnce,因为后者按值取self,即它也不是对象安全的(调用者不知道作为闭包的@ 传递什么大小987654332@参数)。

您仍然可以使用闭包/lambda 函数调用 send_embed,但它只需要通过引用进行,如下所示:

self.messenger.send_embed(0, "abc", &|x| x);

我更新了 Playground 以包含一个使用引用闭包直接调用 send_embed 的示例,以及通过 Bot 上的通用包装器的间接路由。

【讨论】:

那没有办法使用 lambda 吗?如果我将它用作 b.messenger.send_embed(0u64, "ABRACADABRA", &amp;|s| s); 怎么办?这样可以吗? @VictorPolevoy 你可以 - 我已经更新了答案以显示如何。 谢谢。最后一个问题 - 如果我想要一个多态对象,我应该总是这样做吗?这是一种正常的做法还是只是针对这个特定问题的一次性修复? 这里还有一个问题,如果我有send_file&lt;R: std::io::Read&gt;(..) 方法呢?如果我尝试做同样的事情,我会遇到另一个错误:the trait std::io::Read is not implemented for &amp;std::io::Read【参考方案3】:

不能创建泛型方法object-safe,因为你不能用它实现一个vtable。 @ChrisEmerson's answer详细解释了原因。

在您的情况下,您可以通过使 f 采用特征对象而不是通用参数来使 send_embed 对象特征。如果你的函数接受f: F where F: Fn(X) -&gt; Y,你可以让它接受f: &amp;Fn(X) -&gt; Y,FnMut 接受f: &amp;mut FnMut(X) -&gt; Y。 FnOnce 比较棘手,因为 Rust 不支持移动未调整大小的类型,但您可以尝试将它装箱:

//           ↓ no generic          ↓~~~~~~~~~~~~~~~~~~~~~~~~~~~~ box the closure
fn send_embed(&self, u64, &str, f: Box<FnOnce(String) -> String>) -> Option<u64> 
    where Self: Sync + Send

    f("hello".to_string());
    None


b.messenger.send_embed(1, "234", Box::new(|a| a));
// note: does not work.

但是,从 Rust 1.17.0 you cannot box an FnOnce and call it 开始,您必须使用 FnBox:

#![feature(fnbox)]
use std::boxed::FnBox;

//                                     ↓~~~~
fn send_embed(&self, u64, &str, f: Box<FnBox(String) -> String>) -> Option<u64> 
    where Self: Sync + Send 

    f("hello".to_string());
    None


b.messenger.send_embed(1, "234", Box::new(|a| a));

如果您不想使用不稳定的功能,可以使用 crate boxfnonce 作为解决方法:

extern crate boxfnonce;
use boxfnonce::BoxFnOnce;

fn send_embed(&self, u64, &str, f: BoxFnOnce<(String,), String>) -> Option<u64> 
    where Self: Sync + Send 

    f.call("hello".to_string());
    None


b.messenger.send_embed(1, "234", BoxFnOnce::from(|a| a));

【讨论】:

我忘了FnBox

以上是关于为啥 trait 中的泛型方法需要调整 trait 对象的大小?的主要内容,如果未能解决你的问题,请参考以下文章

Rust编程语言入门之泛型Trait生命周期

为啥在某些 trait 方法调用中会出现来自 &mut 的引用减弱?

Scala语法详解:特质 (Traits)

Rust入坑指南:海纳百川

为啥没有为“T* const”定义pointer_traits?

STL 萃取(Traits)机制剖析