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

nbysray5  于 2022-12-13  发布在  其他
关注(0)|答案(3)|浏览(178)

我有这个代码(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;

然后,编译成功,但:
1.为什么这里需要Sized?如果我们不能从trait对象使用这个方法,这就违反了多态性。
1.我们实际上不能用这个方法从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

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

tjrkku2a

tjrkku2a1#

特质与特质

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

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

您可以使用以下两种特性:

  • 做为泛型参数的编译时期界限
  • 做为型别,在指涉或指标之后。

然而......只有 * 一些 * 特性可以直接用作类型。那些可以直接用作类型的特性被标记为 * 对象安全 *。
现在,一个单一的trait关键字同时定义全功能和对象安全特性被认为是不幸的。

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

将特征用作类型时:&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)
因此,运行时分派通过以下方式执行:

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

这意味着<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)
}

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

这是技术限制。
有许多traits功能无法在运行时调度中实现:

  • 相关类型,
  • 相关常数,
  • 相关的通用函数,
  • 与签名中的Self关联的函数。
  • *...也许其他人 *

有两种方式可以表示此问题:

  • early:如果trait具有上述任何一项,则拒绝将其用作类型,
  • late:拒绝在trait上使用上述任何一种作为类型。

现在,拉斯特选择在早期就发出信号:不使用上述任何特性的特性称为 Object Safe,可以用作类型。
不是 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是对象安全的,因此允许您使用&MessengerBox<Messenger>、...

zbwhf8kr

zbwhf8kr2#

动态调度(即通过trait对象调用方法)通过vtable调用(即使用函数指针),因为在编译时你不知道它是哪个函数。
但是如果你的函数是泛型的,那么对于每个实际使用的F示例,它需要进行不同的编译(单态化)。这意味着对于每个不同的闭包类型,你都有一个不同的send_embed副本。每个闭包都是不同的类型。
这两个模型不兼容:你不可能有一个能处理不同类型的函数指针。
但是,您也可以将方法更改为使用trait对象,而不是编译时泛型:

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

Playground)(英文)
它现在接受一个trait对象引用,而不是为每个类型(可以是Fn(String) -> String)提供一个不同的send_embed。(您也可以使用Box<Fn()>或类似的函数)。您必须使用FnFnMut,而不是FnOnce,因为后者通过值获取self,也就是说,它也不是对象安全的(调用者不知道作为闭包的self参数传入的大小)。
你仍然可以用一个闭包/lambda函数调用send_embed,但是它只需要通过引用,就像这样:

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

我已经更新了playground,包括一个使用引用闭包直接调用send_embed的示例,以及通过Bot上的通用 Package 器的间接路由。

brjng4g3

brjng4g33#

一个泛型方法不能成为object-safe,因为你不能用它实现一个vtable。@ChrisEmerson的回答详细解释了为什么。
在你的例子中,你可以通过让f接受一个trait-object而不是泛型参数来使send_embed成为object-trait。如果你的函数接受一个f: F where F: Fn(X) -> Y,你可以让它接受f: &Fn(X) -> Y,类似于FnMut f: &mut FnMut(X) -> 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));

相关问题