是否应该在struct和impl中复制特征边界?

use*_*659 13 rust

以下代码使用具有泛型类型的结构.虽然它的实现仅对给定的特征边界有效,但可以使用或不使用相同的边界来定义结构.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();
    }
}
Run Code Online (Sandbox Code Playgroud)

是否应该忽略对结构有约束力的特性以符合DRY原则,还是应该给予澄清依赖性?或者是否存在一种解决方案应优先于另一种解决方案?

tre*_*tcl 21

我认为现有的答案具有误导性。在大多数情况下,您不应在结构上设置边界,除非该结构在没有它的情况下无法编译

我会解释一下,但首先,让我们先弄清楚一件事:这与减少击键无关。目前,在 Rust 中,您必须在impl接触它的每个结构上重复每个结构的边界,这是一个很好的理由,现在不要在结构上设置边界。然而,这不是我建议从结构中省略特征边界的理由。该implied_boundsRFC最终会实现,但我还是会建议对结构没有把界限。


tl;博士

对于大多数人来说,结构上的界限表达了错误的东西。它们具有传染性、冗余,有时近视,而且常常令人困惑。即使感觉合适,您通常也应该放弃它,直到证明有必要为止。

(在这个答案中,我所说的关于结构的任何内容都同样适用于枚举。)


1. 结构上的边界从抽象中泄漏出来。

你的数据结构很特别。“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),
}
Run Code Online (Sandbox Code Playgroud)

在下游代码中,这是有道理的,因为MyThing::Wrapped仅用于T执行 implement 的 s Thing,而Plain可以用于任何类型。但是,如果在 上your::Object<T>有一个界限T,那么它enum不能在没有相同界限的情况下编译,即使 a 有很多用途Plain(T)不需要这样的界限。这不仅不起作用,而且即使添加边界并不会使其完全无用,它还会在任何碰巧使用MyThing.

结构的界限限制了其他人可以用它们做什么。impl当然,代码(s 和函数)的边界也是如此,但这些约束(大概)是您自己的代码所必需的,而结构的边界是对下游可能以创新方式使用您的结构的任何人的先发制人的打击。这可能很有用,但对于此类创新者来说,不必要的界限尤其令人讨厌,因为它们限制了可以编译的内容,而没有有效地限制实际运行的内容(稍后会详细介绍)。

2. 结构上的边界与代码上的边界是多余的。

所以你认为下游创新是不可能的?这并不意味着结构本身需要一个界限。为了不可能构造一个Object<T>without T: Trait,将那个绑定放在impl包含Object构造函数上就足够了;如果这是不可能叫a_methodObject<T>没有T: Trait,你可以说,在impl含有a_method,或者对a_method本身。(在implied_bounds实施之前,无论如何你都必须这样做,所以你甚至没有“保存击键”的弱理由。但这最终会改变。)

即使特别是当您想不出任何方式让下游使用 un-bounded 时Object<T>,您也不应该先验地禁止它,因为......

3. 结构上的边界实际上意味着与代码上的边界不同的东西。

一个T: Trait必然的Object<T>手段不是“所有Object<T>■找有T: Trait”; 它实际上意味着类似于“Object<T>除非这个概念本身没有意义T: Trait”,这是一个更抽象的想法。想想自然语言:我从来没有见过紫色的大象,但我可以很容易地说出“紫色大象”的概念,尽管它并不对应于现实世界的动物。类型是一种语言,引用 的想法是有意义的Elephant<Purple>,即使您不知道如何创建一个并且您肯定没有用它。类似地,Object<NotTrait>即使您现在没有也不能拥有一个类型,在抽象中表达类型也是有意义的。特别是当NotTrait是类型参数时,在这种情况下实施Trait但在其他一些情况下确实如此。

案例分析: Cell<T>

对于最初具有最终被删除的特征边界的结构的一个示例,请查看Cell<T>最初具有T: Copy边界的 。在RFC 中,许多人最初提出了与您现在可能正在考虑的相同类型的争论,但最终的共识是“Cell需要Copy始终是错误的思考方式Cell。RFC 被合并,为诸如 之类的创新铺平了道路Cell::as_slice_of_cells,它使您可以在安全代码中做以前无法做到的事情,包括暂时选择加入共享突变。关键是这T: Copy从来都不是一个有用的限制Cell<T>,从一开始就放弃它不会有什么坏处(可能还有一些好处)。

这种抽象约束可能很难让人理解,这可能是它经常被误用的原因之一。这与我的最后一点有关:

4. 不必要的边界会引入不必要的参数(更糟)。

这并不适用于结构上的所有边界情况,但这是一个常见的混淆点。例如,您可能有一个带有类型参数的结构体,该结构体必须实现泛型特征,但不知道特征应该采用什么参数。在这种情况下,很容易使用PhantomData向主结构添加类型参数,但这通常是一个错误,尤其是因为PhantomData很难正确使用。以下是一些由于不必要的边界而添加不必要参数的示例:1 2 3 4 5在大多数此类情况下,正确的解决方案是简单地删除边界。

规则的例外

好吧,当需要一个束缚于一个结构?我能想到两个原因。在Shepmaster 的回答中,该结构将不会在没有边界的Iterator情况下进行编译,因为for的实现I实际上定义了该结构包含的内容;这不仅仅是一个任意的规则。此外,如果您正在编写unsafe代码并且希望它依赖于一个绑定(T: Send例如),您可能需要将该绑定放在结构上。unsafe代码是特殊的,因为它可以依赖于非unsafe代码保证的不变量,所以仅仅将边界放在impl包含 的 上unsafe是不够的。但是在所有其他情况下,除非您真的知道自己在做什么,否则您应该完全避免对结构的限制。

  • 一个合理且令人信服的答案使我确信永远不要在结构或枚举中使用边界。 (2认同)

小智 7

这实际上取决于类型。如果仅打算保留实现特征的值,则可以,它应该具有特征绑定,例如

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
    }
}
Run Code Online (Sandbox Code Playgroud)

在此示例中,仅允许孩子进入学校,因此我们对结构具有约束。

如果该结构打算保留任何值,但您希望在实现特征时提供额外的行为,则否,该边界不应位于该结构上,例如

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
    }
}
Run Code Online (Sandbox Code Playgroud)

在此示例中,并非所有客户都是金牌客户,因此在结构上绑定没有意义。


She*_*ter 6

应用于结构的每个实例的特征边界应该应用于结构:

struct IteratorThing<I>
where
    I: Iterator,
{
    a: I,
    b: Option<I::Item>,
}
Run Code Online (Sandbox Code Playgroud)

仅适用于某些实例的特征边界应仅应用于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
    }
}
Run Code Online (Sandbox Code Playgroud)

符合DRY原则

RFC 2089将删除冗余:

消除了对函数和impl的"冗余"边界的需要,可以从输入类型和其他特征边界推断出这些边界.例如,在这个简单的程序中,impl将不再需要绑定,因为它可以从Foo<T>类型推断:

struct Foo<T: Debug> { .. }
impl<T: Debug> Foo<T> {
  //    ^^^^^ this bound is redundant
  ...
}
Run Code Online (Sandbox Code Playgroud)

  • RFC 是对首先让我感到震惊的冗余的答案。 (4认同)