我有这段代码(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 回答
动态调度(即通过特征对象调用方法)通过调用vtable(即使用函数指针)来工作,因为在编译时你不知道它将是哪个函数 .
但是如果你的函数是通用的,那么它需要针对实际使用的
F
的每个实例进行不同的编译(单态) . 这意味着,对于它所调用的每种不同的闭包类型,您将拥有send_embed
的不同副本 . 每个闭合都是不同的类型 .这两个模型是不兼容的:你不能有一个适用于不同类型的函数指针 .
但是,您可以更改方法以使用特征对象,而不是编译时通用:
(Playground)
现在,它接受一个特征对象引用,而不是每个可以
Fn(String) -> String
的类型的不同send_embed
. (您也可以使用Box<Fn()>
或类似的) . 你必须使用Fn
或FnMut
而不是FnOnce
,因为后者按值获取self
,即's also not object safe (the caller doesn'知道要传递的大小为闭包的self
参数 .您仍然可以使用闭包/ lambda函数调用
send_embed
,但它只需要通过引用,如下所示:我已经更新了操场,以包含一个直接使用引用的闭包调用
send_embed
的示例,以及通过Bot
上的通用包装器的间接路由 .Traits and Traits
在Rust中,您可以使用
trait
来定义包含以下内容的接口:相关类型,
相关常数,
相关功能 .
你可以使用以下特征:
作为通用参数的编译时绑定
作为类型,在引用或指针后面 .
但是......只有一些特征可以直接用作类型 . 那些特征标记为对象安全 .
现在认为存在单个
trait
关键字来定义全功能和对象安全特征是不幸的 .Interlude: How does run-time dispatch work?
使用特征作为类型:
&Trait
,Box<Trait>
,Rc<Trait>
,...运行时实现使用由以下组成的胖指针:数据指针,
虚拟指针 .
方法调用通过虚拟指针分派到虚拟表 .
对于如下特征:
为类型
X
实现,虚拟表看起来像(<X as A>::one, <X as A>::two)
.因此,运行时调度由以下方式执行:
挑选 table 的正确成员,
用数据指针和参数调用它 .
这意味着
<X as A>::two
看起来像:Why cannot I use any trait as a type? What's Object Safe?
这是一个技术限制 .
有许多特征功能无法为运行时调度实现:
相关类型,
相关常数,
相关的通用函数,
与签名中的
Self
关联的函数 .......也许其他人......
有两种方法可以表明这个问题:
提前:拒绝使用
trait
作为类型,如果它有上述任何一种,迟:拒绝在
trait
上使用上述任何一种作为类型 .目前,Rust选择在早期发出问题信号:不使用上述任何功能的特性称为Object Safe,可用作类型 .
不是对象安全的特征不能用作类型,并且会立即触发错误 .
Now what?
在您的情况下,只需从编译时多态切换到该方法的运行时多态:
有一点皱纹:
FnOnce
需要移出f
并且它只是在这里借用,所以你需要使用FnMut
或Fn
.FnMut
是下一个更通用的方法,所以:这使得
Messenger
特征对象安全,因此允许您使用&Messenger
,Box<Messenger>
,...无法使用通用方法object-safe,因为您无法使用它实现vtable . @ChrisEmerson's answer详细解释了原因 .
在你的情况下,你可以通过使
f
采用特征对象而不是泛型参数来制作send_embed
object-trait . 如果你的函数接受f: F where F: Fn(X) -> Y
,你可以让它接受f: &Fn(X) -> Y
,类似于FnMutf: &mut FnMut(X) -> Y
. 由于Rust不支持移动未经过类型化的类型,因此FnOnce更加棘手,但您可以尝试将其装箱:但是,从Rust 1.17.0 you cannot box an FnOnce and call it开始,您必须使用FnBox:
如果您不想使用不稳定的功能,可以使用crate boxfnonce作为解决方法: