首页 文章

为什么特征中的泛型方法需要调整特征对象的大小?

提问于
浏览
8

我有这段代码(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();
}

我想制作一个多态对象(trait 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 ?如果我们不能从特征对象使用此方法,则这会违反多态性 .

  • 我们实际上无法使用 Arc<Messenger> 中的此方法:

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

我完全被困在这里 . 不知道如何在特征中使用泛型方法的多态性 . 有办法吗?

3 回答

  • 5

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

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

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

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

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

    Playground

    现在,它接受一个特征对象引用,而不是每个可以 Fn(String) -> String 的类型的不同 send_embed . (您也可以使用 Box<Fn()> 或类似的) . 你必须使用 FnFnMut 而不是 FnOnce ,因为后者按值获取 self ,即's also not object safe (the caller doesn'知道要传递的大小为闭包的 self 参数 .

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

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

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

  • 14

    Traits and Traits

    在Rust中,您可以使用 trait 来定义包含以下内容的接口:

    • 相关类型,

    • 相关常数,

    • 相关功能 .

    你可以使用以下特征:

    • 作为通用参数的编译时绑定

    • 作为类型,在引用或指针后面 .

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

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


    Interlude: How does run-time dispatch work?

    使用特征作为类型: &TraitBox<Trait>Rc<Trait> ,...运行时实现使用由以下组成的胖指针:

    • 数据指针,

    • 虚拟指针 .

    方法调用通过虚拟指针分派到虚拟表 .

    对于如下特征:

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

    为类型 X 实现,虚拟表看起来像 (<X as A>::one, <X as A>::two) .

    因此,运行时调度由以下方式执行:

    • 挑选 table 的正确成员,

    • 用数据指针和参数调用它 .

    这意味着 <X as A>::two 看起来像:

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

    Why cannot I use any trait as a type? What's Object Safe?

    这是一个技术限制 .

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

    • 相关类型,

    • 相关常数,

    • 相关的通用函数,

    • 与签名中的 Self 关联的函数 .

    • ......也许其他人......

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

    • 提前:拒绝使用 trait 作为类型,如果它有上述任何一种,

    • 迟:拒绝在 trait 上使用上述任何一种作为类型 .

    目前,Rust选择在早期发出问题信号:不使用上述任何功能的特性称为Object Safe,可用作类型 .

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


    Now what?

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

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

    有一点皱纹: FnOnce 需要移出 f 并且它只是在这里借用,所以你需要使用 FnMutFn . FnMut 是下一个更通用的方法,所以:

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

    这使得 Messenger 特征对象安全,因此允许您使用 &MessengerBox<Messenger> ,...

  • 13

    无法使用通用方法object-safe,因为您无法使用它实现vtable . @ChrisEmerson's answer详细解释了原因 .

    在你的情况下,你可以通过使 f 采用特征对象而不是泛型参数来制作 send_embed object-trait . 如果你的函数接受 f: F where F: Fn(X) -> Y ,你可以让它接受 f: &Fn(X) -> Y ,类似于FnMut f: &mut FnMut(X) -> Y . 由于Rust不支持移动未经过类型化的类型,因此FnOnce更加棘手,但您可以尝试将其装箱:

    //           ↓ 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));
    

相关问题