CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 第1章 系统程序员也能享受美好事物
  • 第2章 Rust概览
  • 第3章 基本类型
  • 第4章 所有权与移动
  • 第5章 引用
  • 第6章 表达式
  • 第7章 错误处理
  • 第8章 包和模块
  • 第9章 结构体
  • 第10章 枚举和模式
  • 第11章 特性与泛型
  • 第12章 运算符重载
    • 算术运算符和位运算符
      • 一元运算符
      • 二元运算符
      • 复合赋值运算符
    • 等价比较
    • 有序比较
    • Index 和 IndexMut
    • 其他运算符
  • 第13章 实用特性
  • 第14章 闭包
  • 第15章 迭代器
  • 第16章 集合
  • 第17章 字符串和文本
  • 第18章 输入与输出
  • 第19章 并发
  • 第20章 异步编程
  • Rust编程指南
zhangxf
2025-03-10
目录

第12章 运算符重载

# 第12章 运算符重载

在第2章展示的曼德布洛特集合绘图程序中,我们使用num库的Complex类型来表示复平面上的数字:

#[derive(Clone, Copy, Debug)]
struct Complex<T> {
    /// 复数的实部
    re: T,
    /// 复数的虚部
    im: T,
}
1
2
3
4
5
6
7

我们能够像使用任何内置数值类型一样,使用Rust的+和*运算符对Complex数字进行加法和乘法运算:

z = z * z + c;
1

通过实现一些内置特性,你也可以让自己定义的类型支持算术运算和其他运算符。这被称为运算符重载,其效果与C++、C#、Python和Ruby中的运算符重载非常相似。

根据所支持的语言功能,用于运算符重载的特性可分为几类,如表12-1所示。在本章中,我们将涵盖每一类特性。我们的目标不仅是帮助你将自己定义的类型很好地融入到Rust语言中,还希望让你更好地理解如何编写像 “反向工程约束” 中描述的点积函数那样的泛型函数,这些泛型函数能以最自然的方式对使用这些运算符的类型进行操作。本章还将让你深入了解Rust语言本身的一些特性是如何实现的。 表12-1 运算符重载特性总结

类别 特性 运算符
一元运算符 std::ops::Neg -x
std::ops::Not !x
算术运算符 std::ops::Add x + y
std::ops::Sub x - y
std::ops::Mul x * y
std::ops::Div x / y
std::ops::Rem x % y
位运算符 std::ops::BitAnd x & y
std::ops::BitOr x | y
std::ops::BitXor x ^ y
std::ops::Shl x << y
std::ops::Shr x >> y
复合赋值算术运算符 std::ops::AddAssign x += y
std::ops::SubAssign x -= y
std::ops::MulAssign x *= y
std::ops::DivAssign x /= y
std::ops::RemAssign x %= y
复合赋值位运算符 std::ops::BitAndAssign x &= y
std::ops::BitOrAssign x |= y
std::ops::BitXorAssign x ^= y
std::ops::ShlAssign x <<= y
std::ops::ShrAssign x >>= y
比较运算符 std::cmp::PartialEq x == y,x != y
std::cmp::PartialOrd x < y,x <= y,x > y,x >= y
索引运算符 std::ops::Index x[y],&x[y]
std::ops::IndexMut x[y] = z,&mut x[y]

# 算术运算符和位运算符

在Rust中,表达式a + b实际上是a.add(b)的简写形式,这是对标准库中std::ops::Add特性的add方法的调用。Rust的标准数值类型都实现了std::ops::Add。为了让a + b这样的表达式对Complex值也能起作用,num库也为Complex实现了这个特性。类似的特性涵盖了其他运算符:a * b是a.mul(b)的简写,a.mul(b)是std::ops::Mul特性的一个方法;std::ops::Neg涵盖了前缀-取反运算符,等等。

如果你想尝试写出z.add(c),就需要将Add特性引入作用域,这样它的方法才能可见。完成这一步后,你可以把所有算术运算当作函数调用:

use std::ops::Add;
assert_eq!(4.125f32.add(5.75), 9.875);
assert_eq!(10.add(20), 10 + 20);
1
2
3

下面是std::ops::Add的定义:

trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}
1
2
3
4

换句话说,Add<T>特性表示能够将一个T类型的值与自身相加。例如,如果你希望能够将i32和u32类型的值与你的类型相加,那么你的类型必须同时实现Add<i32>和Add<u32>。该特性的类型参数Rhs默认为Self,所以如果你要实现相同类型的两个值之间的加法,这种情况下简单地写Add即可。关联类型Output描述了加法运算的结果。

例如,为了能够将Complex<i32>类型的值相加,Complex<i32>必须实现Add<Complex<i32>>。因为我们是将一个类型与其自身相加,所以这里只写Add:

use std::ops::Add;

impl Add for Complex<i32> {
    type Output = Complex<i32>;
    fn add(self, rhs: Self) -> Self {
        Complex {
            re: self.re + rhs.re,
            im: self.im + rhs.im,
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11

当然,我们不应该分别为Complex<i32>、Complex<f32>、Complex<f64>等实现Add。除了涉及的类型不同,所有这些定义看起来都完全一样,所以只要复数组件本身的类型支持加法,我们就应该能够编写一个通用的实现来涵盖所有情况:

use std::ops::Add;

impl<T> Add for Complex<T>
where
    T: Add<Output = T>,
{
    type Output = Self;
    fn add(self, rhs: Self) -> Self {
        Complex {
            re: self.re + rhs.re,
            im: self.im + rhs.im,
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

通过编写where T: Add<Output=T>,我们将T限制为可以与自身相加并产生另一个T类型值的类型。这是一个合理的限制,但我们还可以进一步放宽条件:Add特性并不要求+运算符的两个操作数具有相同的类型,也不限制结果类型。所以,一个最大限度通用的实现可以让左右操作数独立变化,并产生由加法运算结果类型作为组件类型的Complex值:

use std::ops::Add;

impl<L, R> Add<Complex<R>> for Complex<L>
where
    L: Add<R>,
{
    type Output = Complex<L::Output>;
    fn add(self, rhs: Complex<R>) -> Self::Output {
        Complex {
            re: self.re + rhs.re,
            im: self.im + rhs.im,
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

然而在实践中,Rust倾向于避免支持混合类型运算。由于我们的类型参数L必须实现Add<R>,通常情况下L和R会是相同的类型:实际上,能让L实现其他类型加法的情况并不多。所以最终,这个最大限度通用的版本可能并不比之前那个更简单的通用定义更有用。

Rust中用于算术和位运算符的内置特性分为三组:一元运算符、二元运算符和复合赋值运算符。在每组中,特性及其方法都具有相同的形式,所以我们将从每组中选取一个示例进行介绍。

# 一元运算符

除了解引用运算符*(我们将在 “Deref和DerefMut” 中单独介绍),Rust有两个可以自定义的一元运算符,如表12-2所示。 表12-2 一元运算符的内置特性

特性名称 表达式 等效表达式
std::ops::Neg -x x.neg()
std::ops::Not !x x.not()

Rust的所有有符号数值类型都实现了std::ops::Neg,用于一元取反运算符-;整数类型和bool类型实现了std::ops::Not,用于一元补码运算符!。对于这些类型的引用也有相应的实现。

注意,!对bool值进行取反,对整数执行按位取反(即翻转所有位)操作;它兼具C和C++中!和~运算符的功能。

这些特性的定义很简单:

trait Neg {
    type Output;
    fn neg(self) -> Self::Output;
}

trait Not {
    type Output;
    fn not(self) -> Self::Output;
}
1
2
3
4
5
6
7
8
9

对一个复数取反,只需对其每个分量取反即可。下面是我们可能为Complex值编写的通用取反实现:

use std::ops::Neg;

impl<T> Neg for Complex<T>
where
    T: Neg<Output = T>,
{
    type Output = Complex<T>;
    fn neg(self) -> Complex<T> {
        Complex {
            re: -self.re,
            im: -self.im,
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 二元运算符

Rust的二元算术运算符和位运算符及其对应的内置特性如表12-3所示。 表12-3 二元运算符的内置特性

类别 特性名称 表达式 等效表达式
算术运算符 std::ops::Add x + y x.add(y)
std::ops::Sub x - y x.sub(y)
std::ops::Mul x * y x.mul(y)
std::ops::Div x / y x.div(y)
std::ops::Rem x % y x.rem(y)
位运算符 std::ops::BitAnd x & y x.bitand(y)
std::ops::BitOr x | y x.bitor(y)
std::ops::BitXor x ^ y x.bitxor(y)
std::ops::Shl x << y x.shl(y)
std::ops::Shr x >> y x.shr(y)

Rust的所有数值类型都实现了算术运算符。Rust的整数类型和bool类型实现了位运算符。对于这些类型的引用作为一个或两个操作数的情况,也有相应的实现。

这里所有的特性都具有相同的一般形式。用于^运算符的std::ops::BitXor的定义如下:

trait BitXor<Rhs = Self> {
    type Output;
    fn bitxor(self, rhs: Rhs) -> Self::Output;
}
1
2
3
4

在本章开头,我们还展示了std::ops::Add,它是这一类特性中的另一个,以及几个示例实现。

你可以使用+运算符将String与&str切片或另一个String连接起来。然而,Rust不允许+的左操作数是&str,这是为了避免通过不断在左边连接小片段来构建长字符串(这种方式性能较差,所需时间与最终字符串长度的平方成正比)。一般来说,write!宏更适合逐段构建字符串;我们将在 “追加和插入文本” 中展示如何操作。

# 复合赋值运算符

复合赋值表达式,如x += y或x &= y,这类表达式有两个操作数,对它们执行加法或按位与等运算,并将结果存储回左操作数中。在Rust中,复合赋值表达式的值始终是(),而不是存储的结果值。

许多语言都有类似的运算符,通常将它们定义为x = x + y或x = x & y等表达式的简写形式。然而,Rust并不采用这种方式。

相反,x += y是方法调用x.add_assign(y)的简写形式,其中add_assign是std::ops::AddAssign特性的唯一方法:

trait AddAssign<Rhs = Self> {
    fn add_assign(&mut self, rhs: Rhs);
}
1
2
3

表12-4展示了Rust的所有复合赋值运算符以及实现它们的内置特性。 表12-4 复合赋值运算符的内置特性

类别 特性名称 表达式 等效表达式
算术运算符 std::ops::AddAssign x += y x.add_assign(y)
std::ops::SubAssign x -= y x.sub_assign(y)
std::ops::MulAssign x *= y x.mul_assign(y)
std::ops::DivAssign x /= y x.div_assign(y)
std::ops::RemAssign x %= y x.rem_assign(y)
位运算符 std::ops::BitAndAssign x &= y x.bitand_assign(y)
std::ops::BitOrAssign x |= y x.bitor_assign(y)
std::ops::BitXorAssign x ^= y x.bitxor_assign(y)
std::ops::ShlAssign x <<= y x.shl_assign(y)
std::ops::ShrAssign x >>= y x.shr_assign(y)

Rust的所有数值类型都实现了算术复合赋值运算符。Rust的整数类型和bool类型实现了位复合赋值运算符。

为我们的Complex类型编写一个通用的AddAssign实现很简单:

use std::ops::AddAssign;

impl<T> AddAssign for Complex<T>
where
    T: AddAssign<T>,
{
    fn add_assign(&mut self, rhs: Complex<T>) {
        self.re += rhs.re;
        self.im += rhs.im;
    }
}
1
2
3
4
5
6
7
8
9
10
11

复合赋值运算符的内置特性与相应二元运算符的内置特性是完全独立的。实现std::ops::Add并不会自动实现std::ops::AddAssign;如果你希望Rust允许你的类型作为+=运算符的左操作数,就必须自己实现AddAssign。

# 等价比较

Rust的相等运算符==和!=是对std::cmp::PartialEq特性的eq和ne方法调用的简写形式:

assert_eq!(x == y, x.eq(&y));
assert_eq!(x != y, x.ne(&y));
1
2

下面是std::cmp::PartialEq的定义:

trait PartialEq<Rhs = Self>
where
    Rhs: ?Sized,
{
    fn eq(&self, other: &Rhs) -> bool;
    fn ne(&self, other: &Rhs) -> bool {
        !self.eq(other)
    }
}
1
2
3
4
5
6
7
8
9

由于ne方法有默认定义,所以要实现PartialEq特性,你只需要定义eq方法即可。下面是Complex类型的完整实现:

impl<T: PartialEq> PartialEq for Complex<T> {
    fn eq(&self, other: &Complex<T>) -> bool {
        self.re == other.re && self.im == other.im
    }
}
1
2
3
4
5

换句话说,对于任何本身可以进行相等比较的组件类型T,这个实现为Complex<T>提供了相等比较功能。假设我们在某个地方也为Complex实现了std::ops::Mul,现在就可以这样写:

let x = Complex { re: 5, im: 2 };
let y = Complex { re: 2, im: 5 };
assert_eq!(x * y, Complex { re: 0, im: 29 });
1
2
3

PartialEq的实现通常都是像这样的形式:将左操作数的每个字段与右操作数的相应字段进行比较。编写这些实现会很繁琐,而相等比较是一个常用的操作,所以如果你有需求,Rust会自动为你生成PartialEq的实现。只需在类型定义的derive属性中添加PartialEq,如下所示:

#[derive(Clone, Copy, Debug, PartialEq)]
struct Complex<T> {
   ...
}
1
2
3
4

Rust自动生成的实现与我们手写的代码基本相同,依次比较类型的每个字段或元素。Rust也可以为枚举类型派生PartialEq实现。

当然,类型所包含的值(对于枚举类型,是可能包含的值)本身都必须实现PartialEq。

与算术和位运算特性不同,PartialEq通过引用获取操作数。这意味着比较像String、Vec或HashMap这样的不可复制(non-Copy)值时,不会导致它们被移动,否则会带来麻烦:

let s = "d\x6fv\x65t\x61i\x6c ".to_string();
let t = "\x64o\x76e\x74a\x69l".to_string();
assert!(s == t);  // s和t只是被借用了...
// ... 所以它们在这里仍然有值。
assert_eq!(format!("{} {}", s, t), "dovetail dovetail");
1
2
3
4
5

这就引出了该特性对Rhs类型参数的约束,这是我们之前没有见过的一种约束:

where
    Rhs: ?Sized,
1
2

这放宽了Rust通常要求类型参数必须是 Sized 类型的限制,使我们能够编写像PartialEq<str>或PartialEq<[T]>这样的特性。eq和ne方法接受&Rhs类型的参数,将某个值与&str或&[T]进行比较是完全合理的。由于str实现了PartialEq<str>,以下断言是等效的:

assert!("ungula" != "ungulate");
assert!("ungula".ne("ungulate"));
1
2

这里,Self和Rhs都是未确定大小的(unsized)str类型,使得ne的self和rhs参数都是&str值。

我们将在 “Sized” 中详细讨论 Sized 类型、未确定大小的类型和Sized特性。

为什么这个特性被称为PartialEq呢?传统数学中对等价关系(相等是其中一种情况)的定义有三个要求。对于任何值x和y:

  • 如果x == y为真,那么y == x也必须为真。换句话说,交换相等比较的两边不会影响结果。
  • 如果x == y且y == z,那么必然有x == z。给定任何一个值链,其中每个值都等于下一个值,那么这个链中的每个值都直接等于其他任何值。相等关系具有传递性。
  • x == x必须始终为真。

最后一个要求可能看起来太明显,都不值得一提,但问题恰恰出在这里。Rust的f32和f64是IEEE标准的浮点值。根据该标准,像0.0/0.0这样没有合适值的表达式必须产生特殊的非数字值,通常称为NaN值。该标准进一步要求,NaN值必须被视为与其他所有值都不相等,包括它自身。例如,该标准要求以下所有行为:

assert!(f64::is_nan(0.0 / 0.0));
assert_eq!(0.0 / 0.0 == 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 != 0.0 / 0.0, true);
1
2
3

此外,任何与NaN值的有序比较都必须返回false:

assert_eq!(0.0 / 0.0 < 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 > 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 <= 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 >= 0.0 / 0.0, false);
1
2
3
4

因此,虽然Rust的==运算符满足等价关系的前两个要求,但在用于IEEE浮点值时,显然不满足第三个要求。

这被称为部分等价关系(partial equivalence relation),所以Rust将==运算符的内置特性命名为PartialEq。如果你编写的泛型代码中,类型参数只已知实现了PartialEq,那么你可以假设前两个要求成立,但不应假设值总是等于其自身。

这可能有点违反直觉,如果不注意的话可能会导致错误。如果你希望你的泛型代码要求完全等价关系,可以使用std::cmp::Eq特性作为约束,它代表完全等价关系:如果一个类型实现了Eq,那么对于该类型的每个值x,x == x都必须为真。在实践中,几乎每个实现了PartialEq的类型也应该实现Eq;f32和f64是标准库中仅有的实现了PartialEq但未实现Eq的类型。

标准库将Eq定义为PartialEq的扩展,没有添加新方法:

trait Eq : PartialEq<Self> {}
1

如果你的类型实现了PartialEq,并且也希望它实现Eq,那么必须显式实现Eq,即使实际上不需要定义任何新函数或类型。所以为我们的Complex类型实现Eq很简单:

impl<T: Eq> Eq for Complex<T> {}
1

我们还可以通过在Complex类型定义的derive属性中包含Eq,更简洁地实现:

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Complex<T> {
   ...
}
1
2
3
4

对泛型类型派生的实现可能依赖于类型参数。使用derive属性时,Complex<i32>会实现Eq,因为i32实现了Eq;但Complex<f32>只会实现PartialEq,因为f32没有实现Eq。

当你自己实现std::cmp::PartialEq时,Rust无法检查你对eq和ne方法的定义是否真正符合部分或完全等价的要求。它们可以做任何你想做的事情。Rust只是相信你以符合该特性用户期望的方式实现了相等比较。

虽然PartialEq的定义为ne提供了默认实现,但如果你愿意,也可以提供自己的实现。不过,你必须确保ne和eq是完全互补的。PartialEq特性的用户会这样假设。

# 有序比较

Rust通过单一特性std::cmp::PartialOrd来定义有序比较运算符<、>、<=和>=的行为:

trait PartialOrd<Rhs = Self> : PartialEq<Rhs>
where
    Rhs: ?Sized,
{
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
    fn lt(&self, other: &Rhs) -> bool { ... }
    fn le(&self, other: &Rhs) -> bool { ... }
    fn gt(&self, other: &Rhs) -> bool { ... }
    fn ge(&self, other: &Rhs) -> bool { ... }
}
1
2
3
4
5
6
7
8
9
10

注意,PartialOrd<Rhs>扩展了PartialEq<Rhs>:只有能进行相等比较的类型,才能进行有序比较。

你必须自己实现PartialOrd中唯一的方法是partial_cmp。当partial_cmp返回Some(o)时,o表示self与other的关系:

enum Ordering {
    Less,       // self < other
    Equal,      // self == other
    Greater,    // self > other
}
1
2
3
4
5

但如果partial_cmp返回None,这意味着self和other之间没有顺序关系:既不是大于关系,也不相等。在Rust的所有基本类型中,只有浮点值之间的比较可能会返回None:具体来说,将NaN(非数字)值与其他任何值进行比较都会返回None。

我们在“等价比较”中对NaN值做了更多介绍。

与其他二元运算符一样,要比较Left和Right两种类型的值,Left必须实现PartialOrd<Right>。像x < y或x >= y这样的表达式是对PartialOrd方法调用的简写形式,如表12-5所示。 表12-5 有序比较运算符和PartialOrd方法

表达式 等效方法调用 默认定义
x < y x.lt(y) x.partial_cmp(&y) == Some(Less)
x > y x.gt(y) x.partial_cmp(&y) == Some(Greater)
x <= y x.le(y) matches!(x.partial_cmp(&y), Some(Less) | Some(Equal))
x >= y x.ge(y) matches!(x.partial_cmp(&y), Some(Greater) | Some(Equal))

与前面的示例一样,这里展示的等效方法调用代码假设std::cmp::PartialOrd和std::cmp::Ordering在作用域内。

如果你知道两种类型的值总是存在顺序关系,那么可以实现更严格的std::cmp::Ord特性:

trait Ord : Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;
}
1
2
3

这里的cmp方法只返回一个Ordering,而不像partial_cmp那样返回Option<Ordering>:cmp总是明确表示其参数相等,或者指出它们的相对顺序。几乎所有实现了PartialOrd的类型也都应该实现Ord。在标准库中,f32和f64是这条规则仅有的例外。

由于复数之间没有自然的顺序关系,所以我们不能用前面章节中的Complex类型来展示PartialOrd的示例实现。假设你正在处理以下类型,它表示落在给定半开区间内的一组数字:

#[derive(Debug, PartialEq)]
struct Interval<T> {
    lower: T, // 包含下限
    upper: T, // 不包含上限
}
1
2
3
4
5

你希望让这种类型的值具有部分顺序关系:如果一个区间完全在另一个区间之前,且没有重叠,那么这个区间就小于另一个区间。如果两个不相等的区间有重叠,那么它们是无序的:双方都存在一些元素小于对方的一些元素。而两个相等的区间就是相等的。下面对PartialOrd的实现遵循了这些规则:

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

impl<T: PartialOrd> PartialOrd<Interval<T>> for Interval<T> {
    fn partial_cmp(&self, other: &Interval<T>) -> Option<Ordering> {
        if self == other {
            Some(Ordering::Equal)
        } else if self.lower >= other.upper {
            Some(Ordering::Greater)
        } else if self.upper <= other.lower {
            Some(Ordering::Less)
        } else {
            None
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

有了这个实现,你就可以编写以下代码:

assert!(Interval { lower: 10, upper: 20 } < Interval { lower: 20, upper: 40 });
assert!(Interval { lower: 7, upper: 8 } >= Interval { lower: 0, upper: 8 });
assert!(Interval { lower: 7, upper: 8 } <= Interval { lower: 7, upper: 8 });

// 重叠的区间之间没有顺序关系。
let left = Interval { lower: 10, upper: 30 };
let right = Interval { lower: 20, upper: 40 };
assert!( !(left < right));
assert!( !(left >= right));
1
2
3
4
5
6
7
8
9

虽然PartialOrd是你通常会见到的,但在某些情况下,比如标准库中实现的排序方法,使用Ord定义的全序关系是必要的。例如,仅靠PartialOrd的实现无法对区间进行排序。如果你确实想对它们进行排序,就必须处理无序的情况。比如,你可能想按上限进行排序,使用sort_by_key很容易做到这一点:

intervals.sort_by_key(|i| i.upper);
1

Reverse包装类型利用了这一点,它通过一个方法实现了Ord,该方法只是反转任何顺序。对于任何实现了Ord的类型T,std::cmp::Reverse<T>也实现了Ord,但顺序是相反的。例如,按下限从高到低对我们的区间进行排序很简单:

use std::cmp::Reverse;
intervals.sort_by_key(|i| Reverse(i.lower));
1
2

# Index 和 IndexMut

你可以通过实现std::ops::Index和std::ops::IndexMut特性,来指定像a[i]这样的索引表达式在你的类型上的行为。数组直接支持[]运算符,但在其他任何类型上,表达式a[i]通常是*a.index(i)的简写形式,其中index是std::ops::Index特性的一个方法。不过,如果该表达式用于赋值或可变借用,那么它是*a.index_mut(i)的简写形式,这是对std::ops::IndexMut特性方法的调用。

以下是这些特性的定义:

trait Index<Idx> {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

trait IndexMut<Idx> : Index<Idx> {
    fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}
1
2
3
4
5
6
7
8

注意,这些特性将索引表达式的类型作为参数。你可以用单个usize对切片进行索引,以引用单个元素,因为切片实现了Index<usize>。但你也可以用a[i..j]这样的表达式引用一个子切片,因为它们还实现了Index<Range<usize>>。这个表达式是下面代码的简写形式:

*a.index(std::ops::Range { start: i, end: j })
1

Rust的HashMap和BTreeMap集合允许你使用任何可哈希或有序类型作为索引。以下代码能够正常工作,是因为HashMap<&str, i32>实现了Index<&str>:

use std::collections::HashMap;

let mut m = HashMap::new();
m.insert("十", 10);
m.insert("百", 100);
m.insert("千", 1000);
m.insert("万", 10000);
m.insert("億", 100000000);

assert_eq!(m["十"], 10);
assert_eq!(m["千"], 1000);
1
2
3
4
5
6
7
8
9
10
11

这些索引表达式等效于:

use std::ops::Index;

assert_eq!(*m.index("十"), 10);
assert_eq!(*m.index("千"), 1000);
1
2
3
4

Index特性的关联类型Output指定了索引表达式产生的类型:对于我们的HashMap,Index实现的Output类型是i32。

IndexMut特性通过index_mut方法扩展了Index,该方法接受对self的可变引用,并返回对Output值的可变引用。当索引表达式出现在需要可变引用的上下文中时,Rust会自动选择index_mut。例如,假设我们编写以下代码:

let mut desserts =
    vec!["Howalon".to_string(), "Soan papdi".to_string()];
desserts[0].push_str(" (fictional)");
desserts[1].push_str(" (real)");
1
2
3
4

因为push_str方法作用于&mut self,所以最后两行代码等效于:

use std::ops::IndexMut;

(*desserts.index_mut(0)).push_str(" (fictional)");
(*desserts.index_mut(1)).push_str(" (real)");
1
2
3
4

IndexMut的一个设计限制是,它必须返回对某个值的可变引用。这就是为什么你不能使用像m["十"] = 10;这样的表达式向HashMap m中插入值:哈希表需要先为"十"创建一个条目,并赋予某个默认值,然后返回对该值的可变引用。但并非所有类型都有廉价的默认值,有些类型的默认值丢弃时可能代价很高;仅仅为了在赋值时立即丢弃而创建这样一个值是一种浪费。(在语言的后续版本中,有计划对此进行改进。)

索引最常见的用途是用于集合。例如,假设我们正在处理位图图像,就像第2章中绘制曼德布洛特集合时创建的图像那样。回想一下,我们的程序中有这样的代码:

pixels[row * bounds.0 + column] =...;
1

如果有一个Image<u8>类型,它的行为像二维数组,让我们在访问像素时无需写出所有的算术运算,那就更好了:

image[row][column] =...;
1

要实现这一点,我们需要声明一个结构体:

struct Image<P> {
    width: usize,
    pixels: Vec<P>,
}

impl<P: Default + Copy> Image<P> {
    /// 创建一个给定大小的新图像。
    fn new(width: usize, height: usize) -> Image<P> {
        Image {
            width,
            pixels: vec![P::default(); width * height],
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

以下是符合要求的Index和IndexMut实现:

impl<P> std::ops::Index<usize> for Image<P> {
    type Output = [P];
    fn index(&self, row: usize) -> &[P] {
        let start = row * self.width;
        &self.pixels[start..start + self.width]
    }
}

impl<P> std::ops::IndexMut<usize> for Image<P> {
    fn index_mut(&mut self, row: usize) -> &mut [P] {
        let start = row * self.width;
        &mut self.pixels[start..start + self.width]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

当你对Image进行索引时,会得到一个像素切片;对切片再进行索引就能得到单个像素。

注意,当我们编写image[row][column]时,如果row超出范围,我们的.index()方法会尝试对self.pixels进行越界索引,从而触发panic。这是Index和IndexMut实现应有的行为:越界访问会被检测到并导致panic,这与对数组、切片或向量进行越界索引的情况相同。

# 其他运算符

并非所有运算符在Rust中都能被重载。截至Rust 1.50,错误检查运算符?仅适用于Result和Option值,不过目前也在努力将其扩展到用户自定义类型。同样,逻辑运算符&&和||仅适用于布尔值。..和..=运算符总是创建一个表示范围边界的结构体,&运算符总是借用引用,=运算符总是移动或复制值。它们都不能被重载。

解引用运算符*val,以及用于访问字段和调用方法的点运算符(如val.field和val.method()),可以使用Deref和DerefMut特性进行重载,我们将在下一章介绍这两个特性。(我们在这里没有介绍它们,是因为这些特性的作用不止是重载几个运算符。)

Rust不支持重载函数调用运算符f(x)。相反,当你需要一个可调用的值时,通常只需编写一个闭包。我们将在第14章解释这是如何工作的,并介绍Fn、FnMut和FnOnce这些特殊特性。

Lisp程序员会很高兴!表达式<i32 as Add>::add是i32类型上的+运算符,被捕获为一个函数值。

编辑 (opens new window)
上次更新: 2025/03/20, 19:44:38
第11章 特性与泛型
第13章 实用特性

← 第11章 特性与泛型 第13章 实用特性→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式