Bỏ qua để đến nội dung

Lỗi Rust E0369: binary operation cannot be applied to type (toán tử không áp dụng được)

E0369 nghĩa là bạn dùng một toán tử như ==, < hay + trên một kiểu chưa biết cách làm việc đó. Trong Rust, struct và enum của bạn không tự có sẵn toán tử. Mỗi toán tử đến từ một trait, và kiểu của bạn chỉ có nó khi bạn derive hoặc cài đặt (implement) trait đó. Cách sửa thường chỉ là một dòng #[derive(...)], và trình biên dịch còn nói luôn cho bạn là dòng nào. Các lỗi khác trong loạt bài này nằm ở lỗi trình biên dịch 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 {
|

Trình biên dịch báo rằng toán tử == không dùng được cho kiểu Point, vì Point chưa cài đặt PartialEq, và gợi ý thêm #[derive(PartialEq)].

Đây là chương trình gây ra lỗi:

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!");
}
}

Chú thích ở dòng 9 giữ tiếng Anh vì nó xuất hiện trong thông báo lỗi; nó nói rằng Point không biết cách tự so sánh.

Dòng đầu tiên thay đổi theo toán tử, nhưng vẫn là cùng một lỗi:

  • binary operation `<` cannot be applied to type `Version`: thiếu PartialOrd (cũng áp dụng cho >, <=, >=).
  • cannot add `Point` to `Point`: thiếu Add. Bạn cũng sẽ thấy “cannot subtract”, “cannot multiply”, v.v.
  • cannot add `&str` to `&str`: bạn cố nối hai slice chuỗi bằng +.

Các nhãn dưới toán tử cho biết kiểu ở mỗi bên, ở đây là Point và Point. Sau đó dòng note: chỉ vào struct của bạn và nói nó phải cài đặt PartialEq. Nói cách khác: “bạn bảo tôi so sánh hai Point, nhưng chưa ai nói cho tôi biết bằng nhau nghĩa là gì với một Point.”

Hãy hình dung một trọng tài được đưa cho hai đồ vật và hỏi “cái nào lớn hơn?”. Với con số thì câu trả lời quá rõ. Với hai chiếc ba lô, trọng tài cần có quy tắc trước đã: theo cân nặng? theo thể tích? Rust chính là vị trọng tài cẩn thận đó.

Mỗi toán tử thật ra là một lời gọi phương thức được ngụy trang. a == b gọi PartialEq::eq, a < b gọi PartialOrd::partial_cmp, và a + b gọi Add::add. Số, String và nhiều kiểu chuẩn khác đã cài đặt sẵn các trait này. Struct mới của bạn thì bắt đầu với con số không.

Rust không đoán, vì đoán thì đôi khi sẽ sai. Hai User có nên bằng nhau khi ID trùng nhau, hay chỉ khi mọi trường đều trùng? Point + Point nên cộng tọa độ, hay việc cộng hai điểm vốn đã không có nghĩa? Bạn quyết định bằng cách derive trait (mặc định so từng trường) hoặc tự viết impl. Các trait này được giải thích ở trang Các trait thông dụng.

Chọn cách này cho == và != khi “bằng nhau” nghĩa là “mọi trường đều bằng nhau”. Đây là cách sửa đúng trong phần lớn trường hợp, và đúng là điều rustc gợi ý. Thêm cả Debug để bạn in được struct:

#[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!("Cùng một điểm!");
}
println!("{:?} != {:?}: {}", a, c, a != c);
}

Kết quả:

Cùng một điểm!
Point { x: 1, y: 2 } != Point { x: 5, y: 0 }: true
▶ Tự chạy thử

Mọi trường cũng phải so sánh được. Nếu kiểu của một trường thiếu PartialEq, hãy derive nó cho kiểu đó luôn. Enum cũng vậy: #[derive(PartialEq)] trên enum Status giúp status == Status::Banned biên dịch được.

Chọn cách này khi kiểu của bạn có thứ tự tự nhiên. PartialOrd cần PartialEq, nên rustc gợi ý #[derive(PartialEq, PartialOrd)]. Phiên bản được derive so sánh các trường từ trên xuống dưới: trước hết là major, và chỉ khi chúng bằng nhau mới tới minor. Vì vậy thứ tự các trường trong struct là quan trọng:

#[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!("Hãy cập nhật từ {:?}", installed);
}
let next = Version { major: 2, minor: 0 };
println!("2.0 mới hơn 1.7: {}", next > required);
}

Kết quả:

Hãy cập nhật từ Version { major: 1, minor: 4 }
2.0 mới hơn 1.7: true
▶ Tự chạy thử

Nếu muốn .sort() một Vec chứa kiểu của bạn, bạn sẽ cần thêm Eq và Ord. Xem Sắp xếp.

Chọn cách này khi cộng hai giá trị thuộc kiểu của bạn có ý nghĩa rõ ràng, như cộng tọa độ hay cộng tiền. Không có derive cho toán tử số học, nên bạn viết một impl ngắn. Output là kiểu mà + trả về:

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 });
}

Kết quả:

Point { x: 4, y: 6 }
true
▶ Tự chạy thử

Sub, Mul, Div và Neg hoạt động y như vậy, tất cả đều nằm trong std::ops. Derive Copy ở đây giúp start và step vẫn dùng được sau phép +. Đọc thêm ở Các trait thông dụng và Trait nâng cao.

Cách sửa 4: Nối chuỗi bằng + hoặc format!

Phần tiêu đề “Cách sửa 4: Nối chuỗi bằng + hoặc format!”

Chọn cách này khi thông báo lỗi nói cannot add `&str` to `&str`. + trên chuỗi cần một String sở hữu ở bên trái và một &str ở bên phải. Dòng help của rustc nói theo cách của nó: “create an owned String from a string reference” (tạo một String sở hữu từ tham chiếu chuỗi), tức là first.to_owned() + second. Với hai String, hãy mượn cái bên phải: s1 + &s2. Còn format! thì luôn chạy được và không move thứ gì:

fn main() {
let first = String::from("Xin chào, ");
let second = String::from("Ferris");
let shout = format!("{first}{second}!"); // mượn cả hai
let joined = first + &second; // first bị move vào joined
let short = "Chào, ".to_owned() + "cua"; // &str ở bên trái: tạo bản sở hữu trước
println!("{shout}");
println!("{joined}");
println!("{short}");
println!("{second} vẫn còn đây");
}

Kết quả:

Xin chào, Ferris!
Xin chào, Ferris
Chào, cua
Ferris vẫn còn đây
▶ Tự chạy thử

+ lấy quyền sở hữu String bên trái rồi nối thêm vào nó, đó là lý do first không dùng được sau dòng đó. Nếu thấy rối, cứ dùng format!. Xem Chuỗi.

So sánh trong code generic. Hàm largest kinh điển lỗi với binary operation `>` cannot be applied to type `&T`, vì T có thể là bất kỳ kiểu nào, kể cả kiểu không có thứ tự:

fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { // ❌ T might not be comparable
largest = item;
}
}
largest
}

Cách sửa: thêm ràng buộc trait (trait bound), đúng như rustc gợi ý: fn largest<T: PartialOrd>(list: &[T]) -> &T. Xem Generic.

Kiểm tra giá trị của enum. if status == Status::Banned sẽ lỗi cho tới khi enum có #[derive(PartialEq)]. Nếu bạn không sửa được enum (nó đến từ crate khác), hãy dùng matches!(status, Status::Banned) hoặc một match.

Nối hai String. first + second với hai String không phải E0369: nó là E0308 (“expected &str, found String”) kèm help: consider borrowing here, first + &second. Cùng họ, cùng cách sửa như Cách sửa 4.

So sánh hai kiểu khác nhau. count == limit với một i32 và một f64 cho ra E0308, còn name == 'F' với một String và một char cho ra E0277 (“can’t compare String with char”). Quy tắc dễ nhớ: bạn gặp E0369 khi kiểu bên trái hoàn toàn không có phiên bản nào của toán tử, và gặp E0308 hoặc E0277 khi nó có, nhưng không dành cho kiểu bên phải này. Cách sửa: chuyển đổi một bên (count as f64 == limit, name == "F").

Kiểm tra nhanh

1. Bạn viết if order_a == order_b cho một struct Order và gặp E0369. Cách sửa thường là gì?

2. Với #[derive(PartialOrd)] trên struct Version { major: u32, minor: u32 }, hai phiên bản được so sánh thế nào?

3. first và second đều là &str. Biểu thức nào nối chúng mà không bị lỗi?

Điểm: 0 / 3