运算符重载

既然我们对特质(traits)有了基本的认识,现在让我们回过头来探讨一下运算符重载(operator overloading)。

运算符重载是指为诸如+-*/==!=等运算符定义自定义行为的能力。

在Rust中,运算符是通过特质来实现的。对于每个运算符,都存在一个相应的特质来定义该运算符的行为。通过为你的类型实现这些特质,你就能解锁使用对应的运算符。

例如,PartialEq特质定义了==!=这两个运算符的行为:

#![allow(unused)]
fn main() {
// 从Rust标准库中简化得来的`PartialEq`特质定义
pub trait PartialEq {
    // 必须实现的方法
    // `Self`是一个Rust关键字,表示“实现该特质的类型”
    fn eq(&self, other: &Self) -> bool;

    // 默认提供的方法
    fn ne(&self, other: &Self) -> bool { ... }
}
}

当你编写x == y时,编译器会查找xy类型的PartialEq特质的实现,并将x == y替换为x.eq(y)。这是一种语法糖!

以下是主要运算符与其对应特质的对照表:

运算符特质
+Add
-Sub
*Mul
/Div
%Rem
==!=PartialEq
<><=>=PartialOrd

算术运算符位于std::ops模块中,而比较运算符则位于std::cmp模块。

默认实现

关于PartialEq::ne的注释说明它是“提供的方法”。这意味着PartialEq在其特质定义中为ne提供了一个默认实现——即定义片段中的省略号{ ... }所代表的部分。如果展开这部分,它看起来是这样的:

#![allow(unused)]
fn main() {
pub trait PartialEq {
    fn eq(&self, other: &Self) -> bool;

    fn ne(&self, other: &Self) -> bool {
        !self.eq(other)
    }
}
}

这正如你所料:neeq的否定。由于提供了默认实现,当你为自己的类型实现PartialEq时,可以省略实现ne。实现eq就足够了:

#![allow(unused)]
fn main() {
struct WrappingU8 {
    inner: u8,
}

impl PartialEq for WrappingU8 {
    fn eq(&self, other: &WrappingU8) -> bool {
        self.inner == other.inner
    }
    
    // 这里没有`ne`的实现
}
}

然而,你并不一定要使用默认实现。在实现特质时,你可以选择覆盖它:

#![allow(unused)]
fn main() {
struct MyType;

impl PartialEq for MyType {
    fn eq(&self, other: &MyType) -> bool {
        // 自定义实现
    }

    fn ne(&self, other: &MyType) -> bool {
        // 自定义实现
    }
}
}

参考练习

  • 本节的练习位于exercises/04_traits/03_operator_overloading目录下。