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

Vic*_*voy 9 generics rust

我有这个代码(游乐场):

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();
}
Run Code Online (Sandbox Code Playgroud)

我想制作一个多态对象(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
Run Code Online (Sandbox Code Playgroud)

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

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

然后它成功编译但是:

  1. 我们为什么需要Sized这里?如果我们不能从特征对象使用此方法,则这会违反多态性.
  2. Arc<Messenger>从那时起我们实际上不能使用这种方法:

    fn main() {
        let b = Bot::new();
        b.messenger.send_embed(0u64, "ABRACADABRA", |s| s);
    }
    
    Run Code Online (Sandbox Code Playgroud)

    得到:

    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
    
    Run Code Online (Sandbox Code Playgroud)

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

Mat*_* M. 16

特征和特征

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

  • 相关类型,
  • 相关常数,
  • 相关功能.

你可以使用以下特征:

  • 作为泛型参数的编译时绑定
  • 作为类型,在引用或指针后面.

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

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


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

当使用特征作为一种类型:&Trait,Box<Trait>,Rc<Trait>,...运行时实现使用的由脂肪指针:

  • 数据指针,
  • 虚拟指针.

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

对于如下特征:

trait A {
    fn one(&self) -> usize;
    fn two(&self, other: usize) -> usize;
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 选择正确的桌子成员,
  • 用数据指针和参数调用它.

这意味着<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)
}
Run Code Online (Sandbox Code Playgroud)

为什么我不能使用任何特质作为一种类型?什么是对象安全?

这是一个技术限制.

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

  • 相关类型,
  • 相关常数,
  • 相关的通用函数,
  • Self签名中的相关函数.
  • ......也许别人.......

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

  • 提前:trait如果有上述任何一种,拒绝使用a 作为类型,
  • 迟到:拒绝在trait一个类型上使用上述任何一个.

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

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


怎么办?

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

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

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

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

这使得Messenger特征对象的安全,因此,您可以使用&Messenger,Box<Messenger>...


Chr*_*son 15

动态调度(即通过特征对象调用方法)通过调用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;
}
Run Code Online (Sandbox Code Playgroud)

(游乐场)

它现在接受特征对象引用,而不是send_embed对于每种类型都可以是不同的Fn(String) -> String.(你也可以使用Box<Fn()>或类似).你必须使用FnFnMut不使用FnOnce,因为后者采用self值,即它也不是对象安全的(调用者不知道传递什么大小作为闭包的self参数).

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

self.messenger.send_embed(0, "abc", &|x| x);
Run Code Online (Sandbox Code Playgroud)

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


ken*_*ytm 5

通用方法不能成为对象安全的,因为您无法使用它实现vtable.@ ChrisEmerson的回答详细解释了为什么.

在您的情况下,您可以send_embed通过制作f特征对象而不是通用参数来制作对象特征.如果你的函数接受了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.
Run Code Online (Sandbox Code Playgroud)

但是,从Rust 1.17.0开始,你无法装入FnOnce并调用它,你必须使用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));
Run Code Online (Sandbox Code Playgroud)

如果您不想使用不稳定的功能,可以使用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));
Run Code Online (Sandbox Code Playgroud)