为啥 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<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
我完全被困在这里。不知道如何在 trait 中使用带有泛型方法的多态性。有什么办法吗?
【问题讨论】:
这与trait object 和“对象安全”的概念有关,已经讨论了很多......但我找不到一个很好的副本来详细解释这个问题。 this question 的答案可能会有所帮助。 @AleksanderFular 您是否尝试在操场上更正代码?因为在你说之前我已经尝试过了,不幸的是这对我没有帮助。在您发表评论后,我再次尝试了,without any success. 【参考方案1】:性状和性状
在 Rust 中,您可以使用 trait
定义一个由以下组成的接口:
你也可以使用特征:
作为泛型参数的编译时界限 作为类型,在引用或指针之后。但是...只有 一些 特征可以直接用作类型。这些特征被标记为对象安全。
现在认为不幸的是,存在单个 trait
关键字来定义全功能和对象安全的特征。
插曲:运行时调度如何工作?
当使用 trait 作为类型时:&Trait
、Box<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)
为什么我不能使用任何 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
并且只在这里借用,所以你需要改用FnMut
或Fn
。 FnMut
是下一个更通用的方法,所以:
pub trait Messenger : Sync + Send
fn send_embed(&self, u64, &str, f: &FnMut(String) -> String)
-> Option<u64>;
这使得Messenger
trait 对象安全,因此允许您使用&Messenger
、Box<Messenger>
、...
【讨论】:
我觉得&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) -> String
的类型使用不同的 send_embed
。 (您也可以使用Box<Fn()>
或类似名称)。您必须使用Fn
或FnMut
而不是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", &|s| s);
怎么办?这样可以吗?
@VictorPolevoy 你可以 - 我已经更新了答案以显示如何。
谢谢。最后一个问题 - 如果我想要一个多态对象,我应该总是这样做吗?这是一种正常的做法还是只是针对这个特定问题的一次性修复?
这里还有一个问题,如果我有send_file<R: std::io::Read>(..)
方法呢?如果我尝试做同样的事情,我会遇到另一个错误:the trait std::io::Read is not implemented for &std::io::Read
【参考方案3】:
不能创建泛型方法object-safe,因为你不能用它实现一个vtable。 @ChrisEmerson's answer详细解释了原因。
在您的情况下,您可以通过使 f
采用特征对象而不是通用参数来使 send_embed
对象特征。如果你的函数接受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));
【讨论】:
我忘了FnBox
!以上是关于为啥 trait 中的泛型方法需要调整 trait 对象的大小?的主要内容,如果未能解决你的问题,请参考以下文章
为啥在某些 trait 方法调用中会出现来自 &mut 的引用减弱?