trait Trait {
fn foo(&self);
}
struct Object<T: Trait> {
value: T,
}
impl<T: Trait> Object<T> {
fn bar(object: Object<T>) {
object.value.foo();
}
}
为了符合DRY原则,结构体的特质限定是否应该省略?还是应该给予以明确依赖关系?或者在什么情况下应该优先选择其中一种解决方案?
trait Trait {
fn foo(&self);
}
struct Object<T: Trait> {
value: T,
}
impl<T: Trait> Object<T> {
fn bar(object: Object<T>) {
object.value.foo();
}
}
为了符合DRY原则,结构体的特质限定是否应该省略?还是应该给予以明确依赖关系?或者在什么情况下应该优先选择其中一种解决方案?
这是最明显但(对我来说)最不令人信服的避免编写结构体边界的原因。截至本文撰写时(Rust 1.65),您必须在每个接触到它的impl
上重复每个结构体的边界,这足以成为现在不在结构体上放置边界的一个好理由。但是,有一个已被接受的RFC(implied_bounds
),当其实施和稳定后,将通过推断冗余边界来改变此情况。但即使如此,在结构体上的边界通常仍然是错误的:
您的数据结构很特殊。 "如果T是Trait,则“Object<T>
”才有意义",您说。也许你是对的。但这个决定不仅影响Object
,还影响包含Object<T>
的任何其他数据结构,即使它并不总是包含Object<T>
。考虑一个想要在enum
中包装您的Object
的程序员:
enum MyThing<T> { // error[E0277]: the trait bound `T: Trait` is not satisfied
Wrapped(your::Object<T>),
Plain(T),
}
MyThing :: Wrapped
仅与实现 Thing
的 T
一起使用,而 Plain
可以与任何类型一起使用。但是,如果 your :: Object
对 T
进行了绑定,则即使有许多使用 Plain(T)
的用途不需要此限制,也无法编译此 enum
。这不仅不起作用,而且即使添加约束并不完全无用,它也会将该约束公开在任何使用 MyThing
的结构的公共API中。 impl
和函数)的限制也是如此,但这些约束(可能)是您自己的代码所需的,而结构上的限制则是对那些可能以创新方式使用您的结构的下游方的预防性打击。这可能很有用,但是对于创新者来说,不必要的限制特别令人恼火,因为它们限制了可以编译的内容,而没有有用地限制实际上可以运行的内容(稍后将详细介绍)。 T:Trait
的 Object
,只需将该限制放在包含 Object
的构造函数(们)的 impl
中即可;如果无法在 Object
上调用 a_method
而没有 T:Trait
,则可以在包含 a_method
的 impl
上或者也许在 a_method
本身上这样说。(直到实现 implied_bounds
,您必须这样做,因此您甚至没有“节省击键”的弱理由。) Object
时,您也不应该 a priori 禁止它,因为...Object<T>
上的T: Trait
约束意味着比“所有Object<T>
都必须具有T: Trait
”更多;它实际上意味着“Object<T>
本身的概念没有意义,除非T: Trait
”,这是一个更抽象的想法。想想自然语言:我从未见过紫色大象,但我可以轻松地命名“紫色大象”的概念,尽管它对应于没有现实世界动物的事实。类型是一种语言,引用Elephant<Purple>
的概念可能是有意义的,即使您不知道如何创建它,当然也没有用处。同样,即使您现在没有并且无法拥有Object<NotTrait>
,在抽象中表达该类型也是有意义的。特别是当NotTrait
是类型参数时,在此上下文中可能不知道其是否实现了Trait
,但在其他上下文中可能会实现。
Cell<T>
关于最初具有最终被删除的trait约束的结构体的一个例子,请看看Cell<T>
,它最初具有T: Copy
约束。在删除该约束的RFC中,许多人最初提出了你现在可能正在考虑的相同类型的论点,但最终的共识是,“Cell
需要Copy
”始终是错误的思考方式。 RFC已合并,为创新铺平了道路,例如Cell::as_slice_of_cells
,它使您可以在安全代码中执行以前无法执行的操作,包括暂时选择共享突变。重点是,T: Copy
从来不是Cell<T>
上有用的约束,并且将其从一开始就省略掉不会造成任何伤害(可能会有一些好处)。
这种抽象约束可能很难理解,这可能是它经常被误用的原因之一。这与我的最后一点有关:
PhantomData
将一个类型参数添加到主结构体中,但这通常是一个错误,至少因为PhantomData
很难正确使用。以下是一些由于不必要的边界而添加的不必要参数的示例:1 2 3 4 5 在大多数这样的情况下,正确的解决方案是简单地删除边界。Iterator
实现实际上定义了结构包含的内容。另一个导致结构无法编译的方法是,当其Drop
的实现必须以某种方式使用特质时。出于健全性原因,Drop
不能具有不在结构上的边界,因此您也必须在结构上写它们。unsafe
代码并希望它依赖于一个边界(例如T: Send
)时,您可能需要将该边界放在结构上。unsafe
代码是特殊的,因为它可以依赖于非unsafe
代码保证的不变量,因此仅在包含unsafe
的impl
上放置边界并不一定足够。适用于 每个 结构体实例的特质约束应该应用于结构体本身:
struct IteratorThing<I>
where
I: Iterator,
{
a: I,
b: Option<I::Item>,
}
只适用于某些实例的特性界限应该仅适用于它们所属的 impl
块:
struct Pair<T> {
a: T,
b: T,
}
impl<T> Pair<T>
where
T: std::ops::Add<T, Output = T>,
{
fn sum(self) -> T {
self.a + self.b
}
}
impl<T> Pair<T>
where
T: std::ops::Mul<T, Output = T>,
{
fn product(self) -> T {
self.a * self.b
}
}
为符合DRY原则
通过RFC 2089,冗余将被消除:
Eliminate the need for “redundant” bounds on functions and impls where those bounds can be inferred from the input types and other trait bounds. For example, in this simple program, the impl would no longer require a bound, because it can be inferred from the
Foo<T>
type:struct Foo<T: Debug> { .. } impl<T: Debug> Foo<T> { // ^^^^^ this bound is redundant ... }
impl<T> Foo<T> { ... }
可能会有很多限制,除非你查看结构体,否则你不会知道这些限制。 - Shepmaster这取决于类型的用途。如果它只是用于保存实现该特征的值,则应该有该特征的限制,例如:
trait Child {
fn name(&self);
}
struct School<T: Child> {
pupil: T,
}
impl<T: Child> School<T> {
fn role_call(&self) -> bool {
// check everyone is here
}
}
trait GoldCustomer {
fn get_store_points(&self) -> i32;
}
struct Store<T> {
customer: T,
}
impl<T: GoldCustomer> Store {
fn choose_reward(customer: T) {
// Do something with the store points
}
}
Drop
,似乎我们必须在结构体上绑定泛型?还有一个例外吗? - SpriteDrop
实现,字段也会被隐式丢弃。std::mem::drop()
也适用于任何值,无论它是否是Drop
。 - chbaker0drop
更加简洁,并且有一个显式的清理方法,该方法通过值传递self
。如果绝对需要执行此清理操作,则可以要求调用此方法并使drop
无条件地发生恐慌。 - chbaker0