如何重载 + 运算符?

解读

在国内 Rust 岗位面试中,运算符重载属于“必须掌握的语言基础”,但面试官往往不满足于“写得出”,而是追问“为什么能重载、重载后还能不能 Copy、会不会破坏借用规则”。

  • 运算符在 Rust 中只是 std::ops::Add trait 的语法糖,编译器遇到 a + b 会脱糖为 Add::add(a, b)。因此“重载”本质是为自定义类型实现 Add trait。
    面试时,手写一个完整、可编译、带泛型与生命周期标注的实现是最低要求;进一步需要解释 Output 关联类型的作用、所有权转移与借用冲突的规避、以及 CopyClone 对重载的影响。

知识点

  1. std::ops::Add trait 定义:trait Add<RHS = Self> { type Output; fn add(self, rhs: RHS) -> Self::Output; }
  2. 运算符脱糖规则:a + bAdd::add(a, b)左侧消耗、右侧默认移动;若需只读,可 impl<'a, T> Add<&'a T> for &T
  3. 关联类型 Output 决定返回值类型,可与 Self 不同,常用于“大数相加返回新分配类型”或“向量相加返回切片视图”。
  4. 自动派生陷阱:#[derive(Copy, Clone)]Add 同时存在时,Copy 语义要求实现不能消耗自身,否则编译器会强制退化为 Clone,导致性能暗示失效。
  5. 孤儿规则:必须为本地类型(struct/enum)实现外部 trait,跨 crate 重载需 newtype 模式
  6. 泛型约束:常结合 where Self: SizedRHS: Borrow<T> 做零成本抽象,面试加分项
  7. 异步场景:重载 Add 不能是 async fn,但返回的 Output 可以是 impl Future,需解释“运算符不是异步入口”的设计哲学。

答案

use std::ops::Add;

/// 二维平面向量,面试时优先写“最常用、无宏”的版本
#[derive(Debug, Clone, Copy)]
struct Vec2 { x: f64, y: f64 }

/// 1. 默认 RHS=Self,Output=Self,符合直觉
impl Add for Vec2 {
    type Output = Self;
    fn add(self, rhs: Self) -> Self::Output {
        Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
    }
}

/// 2. 支持 &Vec2 + &Vec2,避免移动,展示生命周期
impl<'a, 'b> Add<&'b Vec2> for &'a Vec2 {
    type Output = Vec2;
    fn add(self, rhs: &'b Vec2) -> Self::Output {
        Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
    }
}

/// 3. 泛型版:支持任意可转换为 f64 的类型,展示面试深度
impl<T> Add<T> for Vec2
where
    T: Into<f64>,
{
    type Output = Vec2;
    fn add(self, rhs: T) -> Self::Output {
        let scalar = rhs.into();
        Vec2 { x: self.x + scalar, y: self.y + scalar }
    }
}

fn main() {
    let a = Vec2 { x: 1.0, y: 2.0 };
    let b = Vec2 { x: 3.0, y: 4.0 };
    let c = &a + &b;          // 不消耗 a、b
    let d = a + 5i32;         // 调用泛型实现
    println!("{:?}", c);      // Vec2 { x: 4.0, y: 6.0 }
    println!("{:?}", d);      // Vec2 { x: 6.0, y: 7.0 }
}

面试口述要点
“我首先为本地类型实现 std::ops::Add,通过关联类型 Output 指定返回值;随后用生命周期重载 &Self + &Self 避免拷贝;最后用泛型加 Into<f64> 支持标量加法,全程零堆分配、编译期展开,符合 Rust 零成本抽象原则。”

拓展思考

  1. 反向加法:若需要 5 + vec,可为 i32 newtype 包装后实现 Add<Vec2>,展示对孤儿规则的灵活运用。
  2. 复合赋值+= 对应 AddAssign,实现时需考虑 &mut selfCopy 的冲突,面试常问“为什么 AddAssign 没有 Output”
  3. 泛型代数:利用 num-traits crate 的 Num 约束,实现 fn dot<T: Num>(a: &[T], b: &[T]) -> T,展示“运算符重载与生态协同”。
  4. const 场景:Rust 1.82 起 Add 可在 const fn 内调用,可追问“编译期求值对重载实现有何限制”
  5. FFI 安全:若 Output 类型包含 #[repr(C)],需保证内存布局与 C 端一致,体现系统级语言的安全边界思维