以下代码使用具有泛型类型的结构。虽然它的实现仅对给定的特征边界有效,但可以使用或不使用相同的边界来定义结构。 struct的字段是私有的,因此无论如何其他代码都无法创建实例。
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原则,还是应该给出澄清依赖性?或者是否存在一种解决方案应优先于另一种解决方案?
答案 0 :(得分:16)
我认为现有的答案具有误导性。在大多数情况下,您不应在结构上设置边界,除非该结构没有它就无法编译。
我会解释一下,但首先,让我们先弄清楚一件事:这与减少击键无关。目前,在 Rust 中,您必须在接触它的每个 impl
上重复每个结构的边界,这是一个很好的理由,现在不对结构设置边界。然而,这不是我推荐从结构中省略特征边界的理由。 implied_bounds
RFC 最终会实施,但我仍然建议不要对结构设置界限。
结构上的界限对大多数人来说表达了错误的东西。它们具有传染性、冗余,有时近视,而且常常令人困惑。即使某个界限感觉正确,您通常也应该将其搁置,直到证明有必要为止。
(在这个答案中,我所说的关于结构的任何内容都同样适用于枚举。)
您的数据结构很特别。 “Object<T>
仅在 T
为 Trait
时才有意义,”您说。也许你是对的。但该决定不仅会影响 Object
,还会影响任何其他包含 Object<T>
的数据结构,即使它不总是包含 Object<T>
。考虑一个想将您的 Object
包装在 enum
中的程序员:
enum MyThing<T> { // error[E0277]: the trait bound `T: Trait` is not satisfied
Wrapped(your::Object<T>),
Plain(T),
}
在下游代码中,这是有意义的,因为 MyThing::Wrapped
仅用于实现 T
的 Thing
,而 Plain
可用于任何类型。但是如果 your::Object<T>
在 T
上有一个界限,这个 enum
不能在没有相同界限的情况下编译,即使 Plain(T)
有很多用途不不需要这样的界限。这不仅不起作用,而且即使添加边界并不会使其完全无用,它还会在任何碰巧使用 MyThing
的结构的公共 API 中公开边界。
结构上的界限限制了其他人可以用它们做什么。当然,对代码(impl
和函数)的限制也是如此,但这些限制(大概)是您自己的代码所必需的,而对结构的限制则是对下游可能使用您的结构的任何人的先发制人的打击创新方式。这可能很有用,但对于这些创新者来说,不必要的界限尤其令人讨厌,因为它们限制了可以编译的内容,而没有有效地限制实际运行的内容(稍后会详细介绍)。
所以你认为下游创新是不可能的?这并不意味着结构本身需要一个界限。为了使构造一个没有Object<T>
的{{1}}成为不可能,只要将该边界放在包含T: Trait
的<的impl
上就足够了强>构造函数(s);如果在没有 Object
的情况下无法在 a_method
上调用 Object<T>
,您可以在包含 T: Trait
的 impl
上调用,或者可能在 a_method
本身。 (在实现 a_method
之前,无论如何你都必须这样做,所以你甚至没有“保存击键”的弱理由。但这最终会改变。)
即使特别是当你想不出任何方法让下游使用无界implied_bounds
时,你也不应该先验地禁止它 ,因为...
绑定在 Object<T>
上的 T: Trait
意味着 比“所有 Object<T>
都必须有 Object<T>
”;它实际上意味着“除非T: Trait
,否则Object<T>
的概念本身没有意义”,这是一个更抽象的想法。想想自然语言:我从来没有见过紫色的大象,但我可以很容易地说出“紫色大象”的概念,尽管它并不对应于现实世界的动物。类型是一种语言,引用 T: Trait
的想法是有意义的,即使您不知道如何创建类型并且您肯定没有用。同样,在抽象中表达类型 Elephant<Purple>
也是有意义的,即使您现在没有也不能拥有它。尤其是当 Object<NotTrait>
是一个类型参数时,在此上下文中可能无法实现 NotTrait
,但在其他一些上下文中却可以实现。
Trait
对于最初具有特征边界但最终被删除的结构体的一个示例,请查看最初具有 Cell<T>
边界的 Cell<T>
。在 the RFC to remove the bound 中,许多人最初提出了您现在可能会想到的相同类型的论点,但最终的共识是“T: Copy
需要 Cell
”总是 错误地思考 Copy
。 RFC 被合并,为 Cell::as_slice_of_cells
等创新铺平了道路,它让您可以在安全代码中完成以前无法做到的事情,包括 temporarily opt-in to shared mutation。关键是 Cell
从来都不是 T: Copy
上有用的界限,从一开始就取消它不会有什么坏处(可能还有好处)。
这种抽象约束可能很难让人理解,这可能是它经常被误用的原因之一。这与我的最后一点有关:
这并不适用于结构上的所有边界情况,但这是一个常见的混淆点。例如,您可能有一个带有类型参数的结构体,该结构体必须实现泛型特征,但不知道特征应该采用什么参数。在这种情况下,很容易使用 Cell<T>
向主结构添加类型参数,但这通常是错误的,尤其是因为 PhantomData
很难正确使用。以下是由于不必要的边界而添加的不必要参数的一些示例:1 2 3 4 5 在大多数此类情况下,正确的解决方案很简单去除边界。
好的,什么时候需要绑定结构?我能想到两个原因。在 Shepmaster's answer 中,结构体不会在没有边界的情况下编译,因为 PhantomData
的 Iterator
实现实际上定义了结构体包含的内容;这不仅仅是一个任意的规则。此外,如果您正在编写 I
代码并且希望它依赖于一个边界(例如,unsafe
),您可能需要将该边界放在结构上。 T: Send
代码很特殊,因为它可以依赖于非unsafe
代码保证的不变量,因此不一定只将边界放在包含 unsafe
的 impl
上够了。但在所有其他情况下,除非您真的知道自己在做什么,否则您应该完全避免对结构的限制。
答案 1 :(得分:6)
应用于结构的每个实例的特征边界应该应用于结构:
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将删除冗余:
消除对函数的“冗余”边界的需要,并消除其中的位置 这些边界可以从输入类型和其他特征推断出来 界限。例如,在这个简单的程序中,impl将不再存在 需要一个绑定,因为它可以从
Foo<T>
类型推断:struct Foo<T: Debug> { .. } impl<T: Debug> Foo<T> { // ^^^^^ this bound is redundant ... }
答案 2 :(得分:5)
这实际上取决于类型的用途。如果它只是用于保存实现特征的值,那么是的,它应该具有特征限制,例如。
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
}
}
在这个例子中,并非所有客户都是黄金客户,并且在结构上绑定没有意义。