跳转到内容

Rust 错误 E0369:binary operation cannot be applied to type(不支持该二元运算)

E0369 的意思是:你对一个类型用了 ==、< 或 + 这样的运算符,可这个类型还不知道怎么做这件事。在 Rust 里,你自己写的结构体和枚举并不自带运算符。每个运算符都来自一个 trait,只有 derive 或实现了那个 trait,你的类型才能用它。修复办法通常就是加一行 #[derive(...)],而且编译器会直接告诉你该加哪个。这个系列里的其他错误,见 Rust 编译错误详解。

error[E0369]: binary operation `==` cannot be applied to type `Point`
--> src/main.rs:9:10
|
9 | if a == b { // ❌ Point doesn't know how to compare itself
| - ^^ - Point
| |
| Point
|
note: an implementation of `PartialEq` might be missing for `Point`
--> src/main.rs:1:1
|
1 | struct Point {
| ^^^^^^^^^^^^ must implement `PartialEq`
help: consider annotating `Point` with `#[derive(PartialEq)]`
|
1 + #[derive(PartialEq)]
2 | struct Point {
|

下面这段程序会产生这个错误:

struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
if a == b { // ❌ Point doesn't know how to compare itself
println!("Same point!");
}
}

(代码里的注释保留英文,因为 rustc 会把这一行原样打印在错误信息里,意思是“Point 不知道怎么和自己比较”。)

第一行会随运算符变化,但都是同一个错误:

  • binary operation `<` cannot be applied to type `Version`:缺少 PartialOrd(>、<=、>= 也一样)。
  • cannot add `Point` to `Point`:缺少 Add。你还会看到 “cannot subtract”(不能相减)、“cannot multiply”(不能相乘)等等。
  • cannot add `&str` to `&str`:你想用 + 拼接两个字符串 slice。

运算符下面的标注显示两边的类型,这里是 Point 和 Point。接着 note: 指向你的结构体,说它 must implement PartialEq(必须实现 PartialEq)。换句话说:“你让我比较两个 Point,可没人告诉过我,对 Point 来说‘相等’是什么意思。”

想象一位裁判,别人递给他两样东西,问他“哪个更大?”。如果是数字,答案一目了然。如果是两个背包,裁判得先有个规则:按重量?按容量?Rust 就是这样一位谨慎的裁判。

每个运算符其实都是一次披着外衣的方法调用。a == b 调用的是 PartialEq::eq,a < b 调用的是 PartialOrd::partial_cmp,a + b 调用的是 Add::add。数字、String 和许多标准库类型早就实现了这些 trait,而你新写的结构体一个都没有。

Rust 不会去猜,因为猜总有猜错的时候。两个 User 是 ID 相同就算相等,还是每个字段都相同才算?Point + Point 是把坐标相加,还是说两个点相加根本没有意义?这些由你来决定:derive 这个 trait(默认逐个字段比较),或者自己写 impl。这些 trait 在 常用 trait 一页里有讲解。

修复 1:用 #[derive(PartialEq)] 支持 == 和 !=

Section titled “修复 1:用 #[derive(PartialEq)] 支持 == 和 !=”

如果“相等”就是“所有字段都相等”,用 == 和 != 时就选这个办法。大多数时候这都是正确的修复,也正是 rustc 给出的建议。再加上 Debug,就能把结构体打印出来:

#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
let c = Point { x: 5, y: 0 };
if a == b {
println!("是同一个点!");
}
println!("{:?} != {:?}: {}", a, c, a != c);
}

输出:

是同一个点!
Point { x: 1, y: 2 } != Point { x: 5, y: 0 }: true
▶ 动手试一试

每个字段本身也必须能比较。如果某个字段的类型没有 PartialEq,也给那个类型 derive 上。枚举也一样:给 enum Status 加上 #[derive(PartialEq)],status == Status::Banned 就能编译了。

修复 2:用 #[derive(PartialOrd)] 支持 < 和 >

Section titled “修复 2:用 #[derive(PartialOrd)] 支持 < 和 >”

如果你的类型有一个自然的顺序,就选这个办法。PartialOrd 依赖 PartialEq,所以 rustc 会建议 #[derive(PartialEq, PartialOrd)]。derive 出来的版本会从上到下比较字段:先比 major,只有 major 相等时才比 minor。所以结构体里字段的顺序很重要:

#[derive(Debug, PartialEq, PartialOrd)]
struct Version {
major: u32,
minor: u32,
}
fn main() {
let installed = Version { major: 1, minor: 4 };
let required = Version { major: 1, minor: 7 };
if installed < required {
println!("请从 {:?} 升级", installed);
}
let next = Version { major: 2, minor: 0 };
println!("2.0 比 1.7 新:{}", next > required);
}

输出:

请从 Version { major: 1, minor: 4 } 升级
2.0 比 1.7 新:true
▶ 动手试一试

如果想对你的类型组成的 Vec 调用 .sort(),还需要 Eq 和 Ord。见 排序。

修复 3:用 impl std::ops::Add 支持 +

Section titled “修复 3:用 impl std::ops::Add 支持 +”

如果把两个你的类型的值相加有明确的含义,比如坐标相加或金额相加,就选这个办法。数学运算符没有 derive,所以要写一个简短的 impl。Output 是 + 返回的类型:

use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point { x: self.x + other.x, y: self.y + other.y }
}
}
fn main() {
let start = Point { x: 1, y: 2 };
let step = Point { x: 3, y: 4 };
let end = start + step;
println!("{:?}", end);
println!("{}", end == Point { x: 4, y: 6 });
}

输出:

Point { x: 4, y: 6 }
true
▶ 动手试一试

Sub、Mul、Div 和 Neg 的用法相同,都在 std::ops 里。这里 derive 了 Copy,所以做完 + 之后 start 和 step 还能继续用。更多内容见 常用 trait 和 高级 trait。

修复 4:用 + 或 format! 拼接字符串

Section titled “修复 4:用 + 或 format! 拼接字符串”

如果错误信息是 cannot add `&str` to `&str`,就选这个办法。字符串上的 + 要求左边是拥有所有权的 String,右边是 &str。rustc 的 help 用它自己的话说了这一点:“create an owned String from a string reference”(从字符串引用创建一个拥有所有权的 String),也就是 first.to_owned() + second。如果两边都是 String,就借用右边那个:s1 + &s2。而 format! 永远管用,也不会移动任何值:

fn main() {
let first = String::from("Hello, ");
let second = String::from("Ferris");
let shout = format!("{first}{second}!"); // 两个都只是借用
let joined = first + &second; // first 被移动进了 joined
let short = "Hi, ".to_owned() + "crab"; // 左边是 &str:先把它变成 String
println!("{shout}");
println!("{joined}");
println!("{short}");
println!("{second} 还能用");
}

输出:

Hello, Ferris!
Hello, Ferris
Hi, crab
Ferris 还能用
▶ 动手试一试

+ 会拿走左边那个 String 的所有权,再把内容追加到它后面,所以那一行之后就不能再用 first 了。如果觉得绕,直接用 format! 就好。见 字符串。

在泛型代码里比较。 经典的 largest 函数会报 binary operation `>` cannot be applied to type `&T`,因为 T 可以是任何类型,包括没有顺序的类型:

fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { // ❌ T 可能根本不能比较
largest = item;
}
}
largest
}

修复:照 rustc 的建议加上 trait 约束:fn largest<T: PartialOrd>(list: &[T]) -> &T。见 泛型。

检查枚举的值。 在枚举加上 #[derive(PartialEq)] 之前,if status == Status::Banned 都会报错。如果你改不了这个枚举(它来自别的 crate),就改用 matches!(status, Status::Banned) 或者 match。

拼接两个 String。 两个 String 写 first + second 不是 E0369,而是 E0308(“expected &str, found String”,期望 &str,却得到 String),并附带 help: consider borrowing here,也就是 first + &second。同一类问题,修复方法和修复 4 一样。

比较两个不同的类型。 用 count == limit 比较一个 i32 和一个 f64 会得到 E0308;用 name == 'F' 比较一个 String 和一个 char 会得到 E0277(“can’t compare String with char”)。经验法则:左边的类型完全没有这个运算符时,报 E0369;有这个运算符、只是不支持右边这个类型时,报 E0308 或 E0277。修复:把一边转换一下(count as f64 == limit、name == "F")。

小测验

1. 你给结构体 Order 写了 if order_a == order_b,结果报 E0369。通常怎么修复?

2. 给 struct Version { major: u32, minor: u32 } 加上 #[derive(PartialOrd)] 后,两个版本是怎么比较的?

3. first 和 second 都是 &str。哪个表达式能拼接它们而不报错?

得分: 0 / 3