更新: 将对象安全规则修正为它们的1.0版本。换句话说,按值传递的self
不再使方法不安全。
这个错误是由于对象安全引起的。
为了能够从一个trait创建一个trait对象,该trait必须是对象安全的。如果同时满足以下两个条件,则trait是对象安全的:
- 它没有
Sized
要求,例如trait Whatever: Sized {}
;
- 它的所有方法都是对象安全的。
如果同时满足以下两个条件,则方法是对象安全的:
- 它有
where Self: Sized
要求,例如fn method() where Self: Sized
;
以下任何一个条件都不成立:
- 此方法在其签名中以任何形式提到
Self
,即使在引用下,除了关联类型;
- 此方法是静态的;
- 此方法是泛型的。
如果你更深入地思考,这些限制实际上是相当自然的。
请记住,当值被转换为特质对象时,它们的类型信息(包括大小)会被抹去。因此,特质对象只能通过引用使用。当应用于特质对象时,引用(或其他智能指针,如Box
或Rc
)变成了“胖指针”——除了指向值的指针外,它们还包含指向该值的虚表的指针。
由于特质对象只能通过指针使用,在它们身上不能调用按值传递的self
方法——为了调用这样的方法,你需要实际的值。这曾经是一种违反对象安全性的行为,这意味着具有这样的方法的特质无法成为特质对象,但即使在1.0之前,规则已经被调整以允许特质对象上的按值传递的self
方法。尽管如此,由于上述原因,仍然无法调用这些方法。有理由期望未来将取消这个限制,因为它目前导致语言中存在一些怪癖,例如无法调用Box<FnOnce()>
闭包。
Self
不能在应该在特质对象上调用的方法中使用,因为特质对象具有其实际类型被擦除的特点,但是为了调用这些方法,编译器需要知道这个已擦除的类型。
我猜为什么不能在特质对象上调用静态方法,这是显而易见的 - 静态方法从定义上来说“属于”特质本身,而不是值,因此您需要知道实现特质的具体类型才能调用它们。更具体地说,常规方法通过存储在特质对象内部的虚拟表进行分派,但静态方法没有接收器,因此它们没有任何可分派的内容,因此它们无法存储在虚拟表中。因此,在不知道具体类型的情况下,它们是无法调用的。
泛型特质方法由于比较技术性的原因而无法调用,我认为这比逻辑更加复杂。在Rust中,通用函数和方法是通过单态化实现的 - 也就是说,对于每个具有一组具体类型参数的通用函数实例化,编译器都会生成一个单独的函数。对于语言用户来说,它看起来像是在调用通用函数; 但在最低层次上,对于每组类型参数,都有一个函数的副本,专门为实例化的类型工作。
考虑到这种方法,为了在一个特质对象上调用通用方法,您需要其虚拟表包含指向每个可能类型的通用方法的虚拟指针,这是不可能的,因为它将需要无限数量的实例化。因此,在特质对象上调用通用方法是被禁止的。
如果Drawable是外部特质,则无法做到您想要的,即在异构集合中调用每个项的draw()。如果您的可绘制集合是静态已知的,可以为每个可绘制类型创建单独的集合,或者创建自己的枚举,其中包含您拥有的每个可绘制类型的变体。然后,您可以为枚举本身实现Drawable,这将是相当简单的。
i32 是一种类型
,对于所有的T,Option<T> 都是一种类型
(这些命题显然是正确的),那么可以轻松地推导出i32
、Option<i32>
、Option<Option<i32>>
等所有类型。因此,类型集合是无限的,因为我们已经找到了它的无限子集。当然,程序中实际使用的类型集合是有限的,但是要查找并针对每个泛型特征生成方法,然后删除未使用的方法将非常困难。 - Vladimir MatveevOption<Option<Option<i32>>>
直到你单态化它才算是一种类型。因此,找到所有类型就像在 Rust 用于缓存事物的任何表中查找一样简单。 - Veedrac