为什么Ord特质不能使用cmp函数为继承特质提供所需方法的默认实现?

14

我有一个新类型,我想要实现Ord

use std::cmp::{Ord, Ordering};

struct MyType(isize);

impl Ord for MyType {
    fn cmp(&self, &other: Self) -> Ordering {
        let MyType(ref lhs) = *self;
        let MyType(ref rhs) = *other;
        lhs.cmp(rhs)
    }
}

当我尝试比较两个变量的类型时,会出现错误:

error[E0277]: the trait bound `MyType: std::cmp::PartialOrd` is not satisfied
 --> src/main.rs:5:6
  |
5 | impl Ord for MyType {
  |      ^^^ can't compare `MyType` with `MyType`
  |
  = help: the trait `std::cmp::PartialOrd` is not implemented for `MyType`

error[E0277]: the trait bound `MyType: std::cmp::Eq` is not satisfied
 --> src/main.rs:5:6
  |
5 | impl Ord for MyType {
  |      ^^^ the trait `std::cmp::Eq` is not implemented for `MyType`

当我实现PartialEqEqPartialOrdgt()lt()eq()ge()le()等)时,一切都很顺利,但是如果我提供cmp,我们可以推断出像lt()eq()这样的函数!这是多余的!我不喜欢这个!在查看文档时,我发现在Ord的定义中有这样的内容:
pub trait Ord: Eq + PartialOrd<Self> 

这看起来像是特质从EqPartialOrd继承。

为什么特质不能使用cmp函数为继承的特质提供所需方法的默认实现?

我不知道特质继承是如何工作的,搜索也没有有用的结果,但我认为这应该是可能的。

在Rust中如何实现这个功能?我希望不是这个方法...

3个回答

16

对于您的特定情况,我会使用#[derive]

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct MyType(isize);

fn main() {
    let a = MyType(5);
    let b = MyType(6);

    println!("{:?}", a.cmp(&b))
}

If you need to特殊处理你的Ord实现,你仍然需要编写那段代码,没有任何东西可以帮助我们!如果有意义,您仍然可以派生其他trait的实现。
您问题的另一部分含糊不清,因此我将按照我读到的两种方式回答它:
为什么不能由任何具有PartialOrd的内容自动提供Ord
让我们看一下PartialOrdOrd的文档。 PartialOrd表示:"比较必须满足反对称性传递性",而Ord表示:"形成全序的类型"。这些都是数学术语,我不如维基百科在描述它们方面做得好。
然而,我们可以以浮点数为例。浮点数有一个特殊的值叫做NaN。与此值进行比较是棘手的。例如,所有的1.0 < NaN1.0 == NaN1.0 > NaN都是false!它们不构成全序,但我们仍然可以将一个值与另一个值进行比较。这就是为什么PartialOrd存在 - 允许我们比较这样的类型。顺便说一下,PartialEq也是出于类似的原因而存在的。

我们无法用PartialOrd来定义Ord,因为我们没有关于底层类型的适当保证。这就是Rust的类型系统防止我们犯错的方式!

为什么任何具有Ord的东西都不能自动提供PartialOrd

问题在于,更多的类型是PartialOrd而不是Ord。如果我们要求所有东西都是Ord,那么我们将无法进行任何浮点数比较,因为它们没有一个完全的次序,或者我们必须放弃拥有完全的次序并失去它提供的安全性。

然而,已经有了一些想法来自动完成这个操作,例如RFC 2385建议上述代码可以简化为:

#[derive(Debug, Copy, Eq, Ord)]
struct MyType(isize);

当我实现 PartialOrd (gt(), lt(), eq(), ge(), le() ...) 时,请注意 PartialOrd 已经有默认实现,你只需要实现 partial_cmp。其他方法存在是为了方便使用或可能是为了提高性能。

5
关于“将Ord应用于所有具有PartialOrd的类型”,OP似乎不想要求PartialOrd类型实现Ord,只是希望对已经实现Ord的类型提供一个默认实现以减少冗余。这完全合理和可行,但在目前的特质系统中不可能实现(但可能没有足够的好处以被包含在未来版本中)。 - user395760
@delnan 另一种解析那段文字的方式!英语很有趣! - Shepmaster
2
我认为你没有很好地回答“为什么任何具有Ord的东西都不能自动提供PartialOrd?”这个问题。该问题仅要求在已经假定了Ord的情况下自动提供PartialOrd,但并非总是如此,如果不总是这样,那么“如果我们要求所有东西都是Ord”从何而来?听起来相当混乱。 - oblitum
1
有没有情况是合法的,即使存在 cmp,也不实现 partial_cmp?如果没有,那么手动编写两者具有相同样板代码似乎是语言上的限制。 - oblitum
@Shepmaster 只需取消 Option 的包装,因为当您指定 Ord 时,您意味着每个 Option<Ordering> 都是 Some - TSK
显示剩余3条评论

10
请将此视为原回答的补充内容,该回答适用于您特定情况。本回答涉及您问题的第二部分。
考虑以下结构:
struct Person {
    id: u32,
    name: String,
    height: u32,
}

平等性: PartialEqEq 特质

PartialEq 特质,摘自文档

Trait for equality comparisons which are partial equivalence 
relations. This trait allows for partial equality, for types that do not
have a full equivalence relation. For example, in floating point numbers
NaN != NaN, so floating point types implement PartialEq but not Eq.

Formally, the equality must be (for all a, b and c):

symmetric: a == b implies b == a; and
transitive: a == b and b == c implies a == c.

如果你想表达你的类型的值相等的含义,你必须实现 PartialEq 特质。实现它允许我们为我们的类型编写 x == yx != y

impl PartialEq for Person {
    fn eq(&self, other: &Person) -> bool {
        self.height == other.height
    }
}

请注意,我们仅根据身高来决定Person结构的相等性。如果您想比较每个结构字段,也可以实现此eq方法。
fn eq(&self, other: &Person) -> bool {
     self.id == other.id && self.name == other.name && self.height == other.height
}

如果您想要这种行为,只需简单地添加 #[derive(PartialEq)] 即可更容易实现。

Eq Trait,来自文档

Trait for equality comparisons which are equivalence relations.

This means, that in addition to a == b and a != b being strict inverses, 
the equality must be (for all a, b and c):

reflexive: a == a;
symmetric: a == b implies b == a; and
transitive: a == b and b == c implies a == c.

This property cannot be checked by the compiler, and therefore Eq implies
PartialEq, and has no extra methods.

Derivable
This trait can be used with #[derive]. When derived, because Eq has no extra methods, 
it is only informing the compiler that this is an equivalence relation rather than a 
partial equivalence relation. Note that the derive strategy requires all 
fields are Eq, which isn't always desired.

PartialEq 是用于不一定自反的关系(即可能存在 x != x 的情况),而 Eq 是一个标记 trait,它表示该关系也是自反的(现在它是一个适当的等价关系)。

您还可以通过空 impl 块手动实现 Eq trait。

impl Eq for Person {}

但是,将Eq添加到您的#[derive(Eq)]列表中会更容易。

顺序: PartialOrdOrd traits

使用操作符<<=>=>计算值之间的相对顺序。要为自己的类型实现这些操作,必须实现PartialOrd特性。

在实现PartialOrd之前,必须实现PartialEq

impl PartialOrd for Person {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))    
    }
}

Ordering 是一个枚举类型,包含以下数值:

pub enum Ordering {
    Less,
    Equal,
    Greater,
}

partial_cmp 返回一个 Option 而不是一个 Ordering,因为有些类型的值不能总是进行比较,例如浮点数。 NaN 不是可表示的数字;表达式如 3.0 < NaN 没有任何意义。在这些情况下,partial_cmp 返回 None。浮点数是标准库中唯一发生这种情况的情况。更多细节可以在这里找到。

partial_cmp 返回 Option<Ordering> 的事实意味着:可能无法确定地将两个值 x 和 y 放入一个确定的顺序中。在实践中,这意味着仅实现 PartialOrd 不足以使您的值可排序。您还需要实现 Ord trait。

在实现 Ord 之前,必须先实现 PartialOrdEqPartialEq

对于我们的 Person 结构体,同样可以委托给我们的一个成员变量:

impl Ord for Person {
    fn cmp(&self, other: &Person) -> Ordering {
        self.height.cmp(&other.height)
    }
}

PartialOrd的实现中,Person从哪里获取了cmp函数? (Some(self.cmp(other))) - KindFrog

8
首先,你可以仅实现PartialOrd::partial_cmp,因为ltlegtge都有默认实现。此外,如果你实现了Ord,那么你只需要像往常一样实现cmppartial_cmp就变成了Some(self.cmp(other))
然而,如果你只想委托给某个字段的等式和顺序概念,则更好且更容易使用派生方式:
#[derive(PartialOrd, Ord, PartialEq, Eq)]
struct MyType(isize);

不错,我没有想过用Ord来定义PartialOrd!我总是反其道而行之并解包值。 - Shepmaster
这很接近,但是在使用PartialOrd时我还必须实现PartialEq。对于Ord也是如此,还需要实现Eq。所以:要实现Ord,先实现cmp,然后添加冗余的实现PartialEqPartialOrdEq,它们什么都不做,只是委托。这很遗憾,特别是Rust实际上非常优雅...希望仍然有一种干净的方法来解决它... - Kapichu
1
@Kapichu 嗯,Eq 不需要任何方法,而 PartialEq 只需要一个。是的,这是三个样板代码位,但老实说我从来没有手动实现过这些 trait,derive(..) 到目前为止总是对我有用的。所以这似乎不是一个巨大的问题,特别是因为任何修复都可能需要编译器或标准库中一些复杂的 do-what-I-mean 魔法,或者 trait 系统的非平凡扩展。 - user395760
@Kapichu 是的,它意味着任何实现了 Ord 的东西也实现了其他那些 trait。正如我在 Shepmaster 的回答评论中所说的,为 PartialEqEqPartialOrd 提供默认实现是可行的,但目前没有办法在 trait 系统中表达这一点。trait 系统必须被改变以支持它,以缓解迄今为止似乎是相对较小的不便之处。此外,它应该是向后兼容的,因此如果它被证明有用,可以随时在未来添加它。 - user395760
1
@Kapichu 因为 Ord "包含" PartialOrd,虽然在方法级别上是 true,但它们具有不同的语义含义。这就是我提到“全序”和所有其他花哨的数学词汇的参考所在。 - Shepmaster
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接