12. Operator Overloading — 运算符重载
Rust 的运算符重载通过 trait 实现——每个可重载的运算符都对应 std::ops(或 std::cmp)中的一个 trait。当你写 a + b 时,Rust 将其脱糖为 a.add(b)。这种设计统一了运算符和 trait 系统:运算符重载不过是实现了特定的 trait。
与 C++ 不同,Rust 不允许凭空创造新的运算符,也不能重载 &&、||、= 等逻辑和赋值运算符。可重载的运算符是精心选择的一组,涵盖算术、位运算、比较、索引等常见需求。
可重载运算符总览
表 12-1 汇总了所有可重载的运算符及其对应的 trait:
| 类别 | 运算符 | Trait | 方法 |
|---|---|---|---|
| 一元 | -x | std::ops::Neg | neg(self) -> Output |
| 一元 | !x | std::ops::Not | not(self) -> Output |
| 算术 | a + b | std::ops::Add | add(self, rhs) -> Output |
| 算术 | a - b | std::ops::Sub | sub(self, rhs) -> Output |
| 算术 | a * b | std::ops::Mul | mul(self, rhs) -> Output |
| 算术 | a / b | std::ops::Div | div(self, rhs) -> Output |
| 算术 | a % b | std::ops::Rem | rem(self, rhs) -> Output |
| 位运算 | a & b | std::ops::BitAnd | bitand(self, rhs) -> Output |
| 位运算 | a | b | std::ops::BitOr | bitor(self, rhs) -> Output |
| 位运算 | a ^ b | std::ops::BitXor | bitxor(self, rhs) -> Output |
| 位运算 | a << b | std::ops::Shl | shl(self, rhs) -> Output |
| 位运算 | a >> b | std::ops::Shr | shr(self, rhs) -> Output |
| 复合赋值 | a += b | std::ops::AddAssign | add_assign(&mut self, rhs) |
| 复合赋值 | a -= b | std::ops::SubAssign | sub_assign(&mut self, rhs) |
| 复合赋值 | a *= b | std::ops::MulAssign | mul_assign(&mut self, rhs) |
| 比较 | a == b | std::cmp::PartialEq | eq(&self, &rhs) -> bool |
| 比较 | a != b | std::cmp::PartialEq | ne(&self, &rhs) -> bool |
| 比较 | a < b | std::cmp::PartialOrd | lt(&self, &rhs) -> bool |
| 比较 | a <= b | std::cmp::PartialOrd | le(&self, &rhs) -> bool |
| 比较 | a > b | std::cmp::PartialOrd | gt(&self, &rhs) -> bool |
| 比较 | a >= b | std::cmp::PartialOrd | ge(&self, &rhs) -> bool |
| 索引 | a[i] | std::ops::Index | index(&self, idx) -> &Output |
| 索引 | a[i] = v | std::ops::IndexMut | index_mut(&mut self, idx) -> &mut Output |
算术和位运算符
Add 和二元运算符的模式
Add trait 的定义展示了 Rust 运算符重载的标准模式:
pub trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}关键设计:
- 泛型参数
Rhs:允许左操作数和右操作数是不同类型。默认Rhs = Self意味着Complex + Complex不需要额外指定。 - 关联类型
Output:加法的结果类型可以不同于操作数类型。 self而非&self:接受所有权,支持移动语义和高效实现。
以复数类型为例,逐步展示从具体到泛型的实现:
#[derive(Copy, Clone, Debug)]
struct Complex<T> {
re: T,
im: T,
}
// 最具体的实现:Complex<f64> + Complex<f64>
impl std::ops::Add for Complex<f64> {
type Output = Complex<f64>;
fn add(self, rhs: Complex<f64>) -> Complex<f64> {
Complex {
re: self.re + rhs.re,
im: self.im + rhs.im,
}
}
}
// 更泛型的版本:Complex<T> + Complex<T>,T 可加
impl<T> std::ops::Add for Complex<T>
where
T: std::ops::Add<Output = T>,
{
type Output = Complex<T>;
fn add(self, rhs: Complex<T>) -> Complex<T> {
Complex {
re: self.re + rhs.re,
im: self.im + rhs.im,
}
}
}混合类型运算
有时候需要支持左右操作数类型不同的情况:
// Complex<T> + T(标量加法)
impl<T> std::ops::Add<T> for Complex<T>
where
T: std::ops::Add<Output = T> + Copy,
{
type Output = Complex<T>;
fn add(self, rhs: T) -> Complex<T> {
Complex {
re: self.re + rhs,
im: self.im + rhs,
}
}
}
// 使用
let c = Complex { re: 1.0, im: 2.0 };
let result = c + 5.0; // Complex { re: 6.0, im: 7.0 }注意:Rust 不会自动提供 5.0 + c 的逆向版本——你需要额外实现 impl Add<Complex<T>> for T(但这受到孤儿规则的限制,通常不可行)。String + &str 能工作是因为标准库提供了 impl Add<&str> for String,但 &str + String 不可用——这正是 Rust 在这个问题上的务实选择。
Sub、Mul 等同类 trait
所有二元运算符 trait 遵循相同的模式:
pub trait Sub<Rhs = Self> {
type Output;
fn sub(self, rhs: Rhs) -> Self::Output;
}
pub trait Mul<Rhs = Self> {
type Output;
fn mul(self, rhs: Rhs) -> Self::Output;
}
// Div, Rem, BitAnd, BitOr, BitXor, Shl, Shr 结构相同数值类型实现了所有算术运算符;整数和 bool 实现了所有位运算符。+ 可用于 String 连接(String + &str),以及 Vec 连接。
一元运算符
Neg(-x)和 Not(!x):
pub trait Neg {
type Output;
fn neg(self) -> Self::Output;
}
pub trait Not {
type Output;
fn not(self) -> Self::Output;
}! 在整数上执行按位取反,在 bool 上执行逻辑取反:
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,
}
}
}
let z = Complex { re: 3.0, im: 4.0 };
let neg_z = -z; // Complex { re: -3.0, im: -4.0 }复合赋值运算符
a += b 不是 a = a + b 的脱糖——它直接调用 a.add_assign(b):
pub trait AddAssign<Rhs = Self> {
fn add_assign(&mut self, rhs: Rhs);
}注意这里接收 &mut self(而非 self),返回 ()(而非 Self::Output)。复合赋值在原地修改:
impl<T> std::ops::AddAssign for Complex<T>
where
T: std::ops::AddAssign,
{
fn add_assign(&mut self, rhs: Complex<T>) {
self.re += rhs.re;
self.im += rhs.im;
}
}
let mut z = Complex { re: 1.0, im: 0.0 };
z += Complex { re: 2.0, im: 3.0 };
// z = Complex { re: 3.0, im: 3.0 }标准库中,复合赋值 trait 是独立于二元运算符 trait 的——都实现了 Add 和 AddAssign 才能同时使用 + 和 +=。
相等性比较:PartialEq 和 Eq
PartialEq
pub trait PartialEq<Rhs: ?Sized = Self> {
fn eq(&self, other: &Rhs) -> bool;
fn ne(&self, other: &Rhs) -> bool { !self.eq(other) }
}?Sized 允许在 trait 对象和切片等无大小类型上比较。ne() 有默认实现(取反 eq)。
手动实现和 #[derive]:
// 手动实现
impl<T: PartialEq> PartialEq for Complex<T> {
fn eq(&self, other: &Complex<T>) -> bool {
self.re == other.re && self.im == other.im
}
}
// 或者简单用 derive
#[derive(PartialEq)]
struct Complex<T> {
re: T,
im: T,
}为什么叫“Partial”Eq?
IEEE 浮点数标准定义了 NaN——NaN 与任何值(包括自身)都不相等:
assert!(f64::NAN != f64::NAN); // 永远为真!这违反了等价关系的自反性(x == x 应对所有 x 成立)。因此 f32 和 f64 只实现 PartialEq,不实现 Eq。这是标准库中唯一实现 PartialEq 但不实现 Eq 的类型。
Eq
Eq 是一个标记 trait——没有方法,声明该类型的相等性是完整的等价关系:
pub trait Eq: PartialEq<Self> {
// 没有新方法——这只是一种标记
}Eq 用于 HashMap 的键类型等需要数学意义上等价关系的场景。几乎所有类型都应该同时实现 PartialEq 和 Eq(除非像 f32/f64 那样本质上有不完整的相等性)。
顺序比较:PartialOrd 和 Ord
pub trait PartialOrd<Rhs: ?Sized = Self>: PartialEq<Rhs> {
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 { ... }
}partial_cmp 返回 Option<Ordering>(而非 Ordering)——因为两个值可能不可比较:
use std::cmp::Ordering;
#[derive(Debug, PartialEq)]
struct Interval<T> {
lower: T,
upper: T,
}
impl<T: PartialOrd> PartialOrd for Interval<T> {
fn partial_cmp(&self, other: &Interval<T>) -> Option<Ordering> {
if self.upper < other.lower {
Some(Ordering::Less) // self 完全在 other 左侧
} else if self.lower > other.upper {
Some(Ordering::Greater) // self 完全在 other 右侧
} else if self == other {
Some(Ordering::Equal) // 区间完全重合
} else {
None // 区间有重叠但不等——无法比较
}
}
}Ord
Ord 提供全序(total order)——所有值都可相互比较:
pub trait Ord: Eq + PartialOrd<Self> {
fn cmp(&self, other: &Self) -> Ordering; // 总是返回 Ordering
}只有实现了 Ord 的类型才能用于 sort():
let mut numbers = vec![3, 1, 4, 1, 5, 9];
numbers.sort(); // 要求 T: Ord
numbers.sort_by(|a, b| b.cmp(a)); // 降序
// 按自定义键排序
numbers.sort_by_key(|n| -n); // 也是降序
// std::cmp::Reverse 包装器
use std::cmp::Reverse;
numbers.sort_by_key(|n| Reverse(*n));Index 和 IndexMut:自定义索引行为
a[i] 语法脱糖为 *a.index(i) 或 *a.index_mut(i):
pub trait Index<Idx> {
type Output: ?Sized;
fn index(&self, index: Idx) -> &Self::Output;
}
pub trait IndexMut<Idx>: Index<Idx> {
fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}注意 Index 返回的是引用——你无法在 index() 中“创建”新值返回。
切片同时实现了多种索引类型:
let v = vec![10, 20, 30, 40, 50];
assert_eq!(v[0], 10); // Index<usize>
assert_eq!(v[1..4], [20, 30, 40]); // Index<Range<usize>>
assert_eq!(v[..3], [10, 20, 30]); // Index<RangeTo<usize>>
assert_eq!(v[3..], [40, 50]); // Index<RangeFrom<usize>>实现二维索引
为图像类型实现二维索引:
struct Image<P> {
width: usize,
pixels: Vec<P>,
}
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]
}
}
// 使用
let mut img = Image { width: 10, pixels: vec![0; 100] };
img[2][3] = 255; // 设置第 2 行第 3 列的像素
let third_row = &img[3]; // 获取整行索引越界会导致 panic(无法编译时捕获,因为没有 C++ 式的 noexcept 标记区分可能失败的索引):
let img = Image { width: 10, pixels: vec![0; 100] };
// img[100][0]; // panic: index out of bounds可以像 HashMap 那样通过实现 get() 方法提供不 panic 的替代方案。
Drop Trait:析构函数
Drop 是 Rust 的析构函数——当值离开作用域时自动调用:
pub trait Drop {
fn drop(&mut self);
}典型用途是释放 Rust 不了解的 OS 资源:
struct FileDesc {
fd: i32,
}
impl Drop for FileDesc {
fn drop(&mut self) {
// 关闭操作系统文件描述符
unsafe { libc::close(self.fd); }
}
}重要规则:
Drop和Copy互斥——类型不能同时实现两者(编译器强制)- 字段的
drop按声明顺序的逆序调用 - 不能在
drop中调用self的任何方法的drop(但可以放字段所有权) - 可用
std::mem::drop(x)提前释放值(标准 prelude 中的函数)
let large_data = vec![0u8; 1_000_000];
// ... 用完 large_data 后还有很多代码 ...
drop(large_data); // 提前释放内存
// large_data 在此后不可访问Deref 和 DerefMut:控制解引用
* 运算符和 . 运算符的行为由 Deref 和 DerefMut 控制:
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
pub trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}Deref 强制转换(Deref Coercion)
Rust 在函数调用、方法调用等场景自动插入 deref() 调用,让类型“看起来像是”它们的 Target 类型:
// String: Deref<Target = str>
// &String 自动变为 &str
fn greet(name: &str) {
println!("Hello, {name}!");
}
let name = String::from("World");
greet(&name); // Rust 自动插入 *:&String -> &str
// &Vec<T> 自动变为 &[T]
fn sum(values: &[i32]) -> i32 {
values.iter().sum()
}
let v = vec![1, 2, 3];
println!("{}", sum(&v)); // &Vec<i32> -> &[i32]实现 DerefMut 的类型在需要 &mut T 的位置也会自动转换:
let mut v = vec![1, 2, 3];
// push() 定义在 Vec<i32> 上,但我们在 &mut [i32] 上找不到它
// Rust 通过 DerefMut 找到 &mut Vec<i32>,然后调用 push
v.push(4);实现 Deref 的指南
Deref 应该仅用于智能指针类型——如 Box<T>、Rc<T>、Arc<T>、Cow<T>。它不应该被滥用来模拟继承:
// 好的用法:智能指针
struct Selector<T> {
elements: Vec<T>,
current: usize,
}
impl<T> Deref for Selector<T> {
type Target = T;
fn deref(&self) -> &T {
&self.elements[self.current]
}
}
impl<T> DerefMut for Selector<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.elements[self.current]
}
}
// 使用:Selector<Point> 上的方法调用自动转发到 Point
let mut s = Selector {
elements: vec![Point { x: 1, y: 2 }],
current: 0,
};
s.x = 10; // 等价于 s.deref_mut().x = 10不可重载的运算符
以下运算符不能被重载:
| 运算符 | 原因 |
|---|---|
? | 仅用于 Result 和 Option |
&& 和 || | 仅用于 bool(短路求值) |
.. 和 ..= | 语法创建范围结构体 |
& | 总是取引用 |
= | 总是移动或复制 |
f(x)(函数调用) | 不可重载(闭包用 Fn/FnMut/FnOnce trait) |
函数调用运算符不能重载——闭包的 Fn/FnMut/FnOnce trait 是实现自定义可调用对象的唯一方式(见第 14 章)。
与 C++ 的对比
| 维度 | Rust | C++ |
|---|---|---|
| 重载机制 | 实现标准 trait | 定义 operator+ 等成员/非成员函数 |
| 运算符集合 | 固定、精选的集合 | 几乎所有运算符,包括 operator,、operator new 等 |
| 自定义运算符 | 不允许 | 不允许(但可滥用现有符号) |
| 复合赋值 | 独立 trait:a += b 直接调用 add_assign | 通常自动推导为 a = a + b,效率可能更低 |
| 比较 | PartialEq/Eq/PartialOrd/Ord 分离设计 | 统一的 operator==/operator< |
| NaN 处理 | f32/f64 只实现 PartialEq,Honoring NaN 语义 | == 必须兼容,但 NaN 行为未定义良好 |
| 索引 | Index/IndexMut + 可选 get() | operator[] + const 重载 |
| 类型安全 | trait bound 约束 | Concepts(C++20)/ SFINAE |
C++ 的运算符重载更灵活但更容易误用——operator,、operator&& 和 operator|| 的重载可以颠覆程序员对逗号和短路求值的基本预期。Rust 选择缩小可重载集合是一个有意的设计决策,牺牲灵活性换取一致性和安全性。
何时实现运算符 Trait
实现运算符 trait 的指导原则:
- 语义明确时实现:复数加法、矩阵乘法、集合合并等数学/逻辑运算。
- 不滥用语义:不要给
User实现Add来做“合并两个用户账号”——用有名字的方法。 - 保持一致性:如果实现了
Add,通常也应该实现AddAssign。如果实现了PartialOrd,也应该实现PartialEq。 - 考虑性能:运算符接受
self(所有权)而非&self,让编译器可以优化临时值的移动。 - 文档清楚说明行为:特别是当运算符出于性能原因有特殊行为时(如
String + &str消耗左侧的String)。
小结
- 运算符重载通过 trait 实现:每个运算符对应一个
std::ops或std::cmp中的 trait。a + b脱糖为a.add(b)。 - 二元运算符遵循统一模式:泛型
Rhs参数 + 关联Output类型 +self所有权接收者,支持灵活的混合类型运算。 - 复合赋值是独立的:
a += b直接调用a.add_assign(b)(接收&mut self),而非自动推导为a = a + b。 - 相等比较分层设计:
PartialEq接受 NaN 的不自反性;Eq标记全等关系。比较总是通过引用(&self),不消耗值。 - 顺序比较支持不可比较值:
PartialOrd::partial_cmp返回Option<Ordering>;Ord::cmp返回Ordering(全序)。 - 索引返回引用:
Index和IndexMut要求返回引用——不能凭空创建值。越界访问 panic。 - Deref 强制转换:Rust 自动插入
deref()调用,让智能指针“看起来像”它们包裹的类型。Deref 应仅用于智能指针,不应滥用。 - Drop 是析构函数:释放外部资源,与
Copy互斥。字段按声明的逆序释放。