Skip to content
Published at:

12. Operator Overloading — 运算符重载

Rust 的运算符重载通过 trait 实现——每个可重载的运算符都对应 std::ops(或 std::cmp)中的一个 trait。当你写 a + b 时,Rust 将其脱糖为 a.add(b)。这种设计统一了运算符和 trait 系统:运算符重载不过是实现了特定的 trait。

与 C++ 不同,Rust 不允许凭空创造新的运算符,也不能重载 &&||= 等逻辑和赋值运算符。可重载的运算符是精心选择的一组,涵盖算术、位运算、比较、索引等常见需求。

可重载运算符总览

表 12-1 汇总了所有可重载的运算符及其对应的 trait:

类别运算符Trait方法
一元-xstd::ops::Negneg(self) -> Output
一元!xstd::ops::Notnot(self) -> Output
算术a + bstd::ops::Addadd(self, rhs) -> Output
算术a - bstd::ops::Subsub(self, rhs) -> Output
算术a * bstd::ops::Mulmul(self, rhs) -> Output
算术a / bstd::ops::Divdiv(self, rhs) -> Output
算术a % bstd::ops::Remrem(self, rhs) -> Output
位运算a & bstd::ops::BitAndbitand(self, rhs) -> Output
位运算a | bstd::ops::BitOrbitor(self, rhs) -> Output
位运算a ^ bstd::ops::BitXorbitxor(self, rhs) -> Output
位运算a << bstd::ops::Shlshl(self, rhs) -> Output
位运算a >> bstd::ops::Shrshr(self, rhs) -> Output
复合赋值a += bstd::ops::AddAssignadd_assign(&mut self, rhs)
复合赋值a -= bstd::ops::SubAssignsub_assign(&mut self, rhs)
复合赋值a *= bstd::ops::MulAssignmul_assign(&mut self, rhs)
比较a == bstd::cmp::PartialEqeq(&self, &rhs) -> bool
比较a != bstd::cmp::PartialEqne(&self, &rhs) -> bool
比较a < bstd::cmp::PartialOrdlt(&self, &rhs) -> bool
比较a <= bstd::cmp::PartialOrdle(&self, &rhs) -> bool
比较a > bstd::cmp::PartialOrdgt(&self, &rhs) -> bool
比较a >= bstd::cmp::PartialOrdge(&self, &rhs) -> bool
索引a[i]std::ops::Indexindex(&self, idx) -> &Output
索引a[i] = vstd::ops::IndexMutindex_mut(&mut self, idx) -> &mut Output

算术和位运算符

Add 和二元运算符的模式

Add trait 的定义展示了 Rust 运算符重载的标准模式:

rust
pub trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

关键设计:

  • 泛型参数 Rhs:允许左操作数和右操作数是不同类型。默认 Rhs = Self 意味着 Complex + Complex 不需要额外指定。
  • 关联类型 Output:加法的结果类型可以不同于操作数类型。
  • self 而非 &self:接受所有权,支持移动语义和高效实现。

以复数类型为例,逐步展示从具体到泛型的实现:

rust
#[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,
        }
    }
}

混合类型运算

有时候需要支持左右操作数类型不同的情况:

rust
// 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 遵循相同的模式:

rust
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):

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

pub trait Not {
    type Output;
    fn not(self) -> Self::Output;
}

! 在整数上执行按位取反,在 bool 上执行逻辑取反:

rust
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)

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

注意这里接收 &mut self(而非 self),返回 ()(而非 Self::Output)。复合赋值在原地修改:

rust
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 的——都实现了 AddAddAssign 才能同时使用 ++=

相等性比较:PartialEq 和 Eq

PartialEq

rust
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]

rust
// 手动实现
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 与任何值(包括自身)都不相等:

rust
assert!(f64::NAN != f64::NAN);  // 永远为真!

这违反了等价关系的自反性x == x 应对所有 x 成立)。因此 f32f64 只实现 PartialEq,不实现 Eq。这是标准库中唯一实现 PartialEq 但不实现 Eq 的类型。

Eq

Eq 是一个标记 trait——没有方法,声明该类型的相等性是完整的等价关系:

rust
pub trait Eq: PartialEq<Self> {
    // 没有新方法——这只是一种标记
}

Eq 用于 HashMap 的键类型等需要数学意义上等价关系的场景。几乎所有类型都应该同时实现 PartialEqEq(除非像 f32/f64 那样本质上有不完整的相等性)。

顺序比较:PartialOrd 和 Ord

rust
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)——因为两个值可能不可比较

rust
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)——所有值都可相互比较:

rust
pub trait Ord: Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;  // 总是返回 Ordering
}

只有实现了 Ord 的类型才能用于 sort()

rust
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)

rust
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() 中“创建”新值返回。

切片同时实现了多种索引类型:

rust
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>>

实现二维索引

为图像类型实现二维索引:

rust
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 标记区分可能失败的索引):

rust
let img = Image { width: 10, pixels: vec![0; 100] };
// img[100][0];  // panic: index out of bounds

可以像 HashMap 那样通过实现 get() 方法提供不 panic 的替代方案。

Drop Trait:析构函数

Drop 是 Rust 的析构函数——当值离开作用域时自动调用:

rust
pub trait Drop {
    fn drop(&mut self);
}

典型用途是释放 Rust 不了解的 OS 资源:

rust
struct FileDesc {
    fd: i32,
}

impl Drop for FileDesc {
    fn drop(&mut self) {
        // 关闭操作系统文件描述符
        unsafe { libc::close(self.fd); }
    }
}

重要规则:

  • DropCopy 互斥——类型不能同时实现两者(编译器强制)
  • 字段的 drop 按声明顺序的逆序调用
  • 不能在 drop 中调用 self 的任何方法的 drop(但可以放字段所有权)
  • 可用 std::mem::drop(x) 提前释放值(标准 prelude 中的函数)
rust
let large_data = vec![0u8; 1_000_000];
// ... 用完 large_data 后还有很多代码 ...
drop(large_data);  // 提前释放内存
// large_data 在此后不可访问

Deref 和 DerefMut:控制解引用

* 运算符和 . 运算符的行为由 DerefDerefMut 控制:

rust
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 类型:

rust
// 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 的位置也会自动转换:

rust
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>。它不应该被滥用来模拟继承:

rust
// 好的用法:智能指针
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

不可重载的运算符

以下运算符不能被重载:

运算符原因
?仅用于 ResultOption
&&||仅用于 bool(短路求值)
....=语法创建范围结构体
&总是取引用
=总是移动或复制
f(x)(函数调用)不可重载(闭包用 Fn/FnMut/FnOnce trait)

函数调用运算符不能重载——闭包的 Fn/FnMut/FnOnce trait 是实现自定义可调用对象的唯一方式(见第 14 章)。

与 C++ 的对比

维度RustC++
重载机制实现标准 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 的指导原则:

  1. 语义明确时实现:复数加法、矩阵乘法、集合合并等数学/逻辑运算。
  2. 不滥用语义:不要给 User 实现 Add 来做“合并两个用户账号”——用有名字的方法。
  3. 保持一致性:如果实现了 Add,通常也应该实现 AddAssign。如果实现了 PartialOrd,也应该实现 PartialEq
  4. 考虑性能:运算符接受 self(所有权)而非 &self,让编译器可以优化临时值的移动。
  5. 文档清楚说明行为:特别是当运算符出于性能原因有特殊行为时(如 String + &str 消耗左侧的 String)。

小结

  • 运算符重载通过 trait 实现:每个运算符对应一个 std::opsstd::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(全序)。
  • 索引返回引用IndexIndexMut 要求返回引用——不能凭空创建值。越界访问 panic。
  • Deref 强制转换:Rust 自动插入 deref() 调用,让智能指针“看起来像”它们包裹的类型。Deref 应仅用于智能指针,不应滥用。
  • Drop 是析构函数:释放外部资源,与 Copy 互斥。字段按声明的逆序释放。