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

Lỗi Rust E0277: the trait bound is not satisfied (chưa triển khai trait)

Lỗi E0277 nghĩa là: bạn dùng một kiểu ở chỗ cần một khả năng mà kiểu đó không có. In nó ra, so sánh nó, cộng nó, chuyển đổi nó, gửi nó sang luồng khác: mỗi việc này đều cần một trait, và kiểu của bạn đang thiếu trait đó. Đây là một trong những lỗi Rust thường gặp nhất, thông báo thường cho bạn biết chính xác cách sửa, và phần lớn thời gian cách sửa chỉ là một dòng. (Trang này thuộc loạt bài Giải thích các lỗi trình biên dịch Rust.)

Dạng mà hầu hết mọi người gặp đầu tiên là in struct của chính mình bằng {}:

error[E0277]: `Player` doesn't implement `std::fmt::Display`
--> src/main.rs:8:20
|
8 | println!("{}", p);
| -- ^ `Player` cannot be formatted with the default formatter
| |
| required by this formatting parameter
|
help: the trait `std::fmt::Display` is not implemented for `Player`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

Thông báo của trình biên dịch luôn bằng tiếng Anh. Nó nói: Player chưa triển khai std::fmt::Display, nên không định dạng được bằng {}. Đây là chương trình gây ra lỗi đó:

struct Player {
name: String,
score: u32,
}
fn main() {
let p = Player { name: String::from("Ana"), score: 42 };
println!("{}", p);
}

Một trait là một lời hứa rằng kiểu đó làm được một việc gì đó. Display là lời hứa “tôi biết cách in mình ra cho đẹp”. PartialEq là “tôi so sánh được bằng ==”. Send là “tôi được phép chuyển sang luồng khác một cách an toàn”.

Những đoạn code như println!, .sort(), + và thread::spawn đòi hỏi một trong những lời hứa đó. Trình biên dịch chỉ vào giá trị bạn truyền vào (p ở dòng 8) và nói: kiểu này chưa bao giờ hứa điều đó, nên tôi không dùng nó ở đây được.

Hãy nghĩ đến một ổ cắm điện. Ổ cắm chỉ nhận phích cắm có một hình dạng nhất định. Thiết bị của bạn có thể chạy hoàn hảo, nhưng nếu phích cắm không vừa thì không cắm vào được. E0277 là trình biên dịch nói “sai phích cắm”. Bạn sửa bằng cách cho kiểu đó đúng phích cắm: thường là bằng #[derive(...)], đôi khi bằng một khối impl ngắn.

Rust không bao giờ đoán. Với kiểu Player của riêng bạn, không có một cách “đúng” duy nhất để in, so sánh hay sắp xếp nó. Hai người chơi có nên được coi là bằng nhau khi tên giống nhau, hay chỉ khi mọi trường đều giống nhau? Rust bắt bạn nói rõ, thay vì lặng lẽ làm một điều bạn không hề muốn.

Việc kiểm tra diễn ra lúc biên dịch. Ở nhiều ngôn ngữ, in một đối tượng mà bạn chưa từng dạy nó cách in sẽ cho ra thứ vô dụng kiểu <Player object at 0x7f...>, còn gửi một giá trị không an toàn với luồng sang luồng khác sẽ gây ra một vụ sập hiếm hoi trên môi trường production. Trong Rust, cả hai đều là lỗi biên dịch mà bạn chỉ cần sửa một lần.

Bạn có thể đọc thêm về cách trait hoạt động ở trang Trait, và về những trait dùng hằng ngày (Debug, Display, PartialEq, Ord, Hash) ở trang Các trait thông dụng.

E0277 có nhiều “gương mặt”. Hãy tìm thông báo khớp với lỗi của bạn ở bên dưới.

Cách sửa 1: In một struct bằng {} hoặc {:?}

Phần tiêu đề “Cách sửa 1: In một struct bằng {} hoặc {:?}”

{:?} cần Debug. Bạn gần như không bao giờ phải tự viết nó: hãy thêm #[derive(Debug)]. Dùng nó cho việc debug và ghi log. Lỗi với {:?} trông như thế này:

error[E0277]: `Player` doesn't implement `Debug`
= help: the trait `Debug` is not implemented for `Player`
= note: add `#[derive(Debug)]` to `Player` or manually `impl Debug for Player`

{} cần Display. Rust không derive được nó, vì chỉ bạn mới biết phiên bản “đẹp” nên trông thế nào. Hãy viết một khối impl nhỏ với write!. Dùng nó cho văn bản mà người dùng sẽ thấy.

use std::fmt;
#[derive(Debug)] // có ngay {:?} miễn phí
struct Player {
name: String,
score: u32,
}
// Display thì bạn tự viết: nó quyết định {} in ra gì.
impl fmt::Display for Player {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({} điểm)", self.name, self.score)
}
}
fn main() {
let p = Player { name: String::from("Ana"), score: 42 };
println!("{:?}", p);
println!("{}", p);
}

Kết quả:

Player { name: "Ana", score: 42 }
Ana (42 điểm)
▶ Tự chạy thử

Cách sửa 2: So sánh, sắp xếp, hoặc dùng làm khóa HashMap

Phần tiêu đề “Cách sửa 2: So sánh, sắp xếp, hoặc dùng làm khóa HashMap”

.contains() và == cần PartialEq. .sort() cần Ord. Khóa của HashSet và HashMap cần Eq và Hash. Các thông báo trông như thế này:

error[E0277]: can't compare `Point` with `Point`
= help: the trait `PartialEq` is not implemented for `Point`
error[E0277]: the trait bound `Priority: Ord` is not satisfied
= help: the trait `Ord` is not implemented for `Priority`
error[E0277]: the trait bound `Point: Hash` is not satisfied
= help: the trait `Hash` is not implemented for `Point`

Cách sửa là derive chúng. Các trait này xây dựng trên nhau, nên bộ đầy đủ thường dùng là PartialEq, Eq, Hash, PartialOrd, Ord:

use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum Priority {
Low,
Medium,
High,
}
fn main() {
let mut tasks = vec![Priority::High, Priority::Low, Priority::Medium];
tasks.sort(); // cần Ord
println!("{:?}", tasks);
println!("Có việc gấp: {}", tasks.contains(&Priority::High)); // cần PartialEq
let mut counts: HashMap<Priority, u32> = HashMap::new(); // khóa cần Eq + Hash
for task in &tasks {
*counts.entry(*task).or_insert(0) += 1;
}
println!("Số việc ưu tiên thấp: {}", counts[&Priority::Low]);
}

Kết quả:

[Low, Medium, High]
Có việc gấp: true
Số việc ưu tiên thấp: 1
▶ Tự chạy thử

Ord được derive sẽ sắp xếp các biến thể enum theo thứ tự bạn viết chúng, còn struct thì theo từng trường từ trên xuống dưới. Có một điểm cần nói thật: một kiểu có trường f64 không thể derive Eq, Hash hay Ord, vì số thực có NaN, và NaN không bằng chính nó. Để sắp xếp số thực, xem Sắp xếp.

Cách sửa 3: Trộn các kiểu số (cannot add f64 to i32)

Phần tiêu đề “Cách sửa 3: Trộn các kiểu số (cannot add f64 to i32)”
error[E0277]: cannot add `f64` to `i32`
--> src/main.rs:4:23
|
4 | let total = items + price;
| ^ no implementation for `i32 + f64`
|
= help: the trait `Add<f64>` is not implemented for `i32`

Rust không bao giờ tự chuyển đổi số giúp bạn, kể cả từ i32 sang f64. Hai vế của +, -, * và / phải cùng kiểu. Hãy tự chuyển đổi một vế bằng as:

fn main() {
let items: i32 = 3;
let price: f64 = 2.5;
let total = items as f64 * price; // chuyển đổi trước, rồi mới nhân
println!("Tổng: {total}");
}

Kết quả:

Tổng: 7.5
▶ Tự chạy thử

f64::from(items) cũng được, và không bao giờ làm mất dữ liệu. Đi theo chiều ngược lại (f64 sang i32) sẽ cắt mất phần thập phân, nên hãy nghĩ tới chuyện làm tròn trước. Các trang Ép kiểu và Chuyển đổi kiểu nói chi tiết hơn.

error[E0277]: `?` couldn't convert the error to `ConfigError`
|
8 | fn parse_port(text: &str) -> Result<u16, ConfigError> {
| ------------------------ expected `ConfigError` because of this
9 | let port = text.parse::<u16>()?;
| --------------^ the trait `From<ParseIntError>` is not implemented for `ConfigError`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait

Khi ? gặp một lỗi, nó trả lỗi đó về từ hàm của bạn. Nếu kiểu lỗi khác với kiểu mà hàm của bạn đã hứa, ? sẽ cố chuyển đổi nó bằng From. Việc của bạn là viết From đó:

use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
BadPort(ParseIntError),
}
// Dạy `?` cách biến một ParseIntError thành ConfigError.
impl From<ParseIntError> for ConfigError {
fn from(err: ParseIntError) -> Self {
ConfigError::BadPort(err)
}
}
fn parse_port(text: &str) -> Result<u16, ConfigError> {
let port = text.parse::<u16>()?;
Ok(port)
}
fn main() {
println!("{:?}", parse_port("8080"));
println!("{:?}", parse_port("tám mươi"));
}

Kết quả:

Ok(8080)
Err(BadPort(ParseIntError { kind: InvalidDigit }))
▶ Tự chạy thử

Những lựa chọn tốt khác: chỉ chuyển đổi cho một lời gọi bằng .map_err(ConfigError::BadPort)?, dùng #[from] của crate thiserror, hoặc trả về Result<T, Box<dyn std::error::Error>> trong các chương trình nhỏ. Xem Lỗi tự định nghĩa và anyhow và thiserror.

Cách sửa 5: Rc cannot be sent between threads safely

Phần tiêu đề “Cách sửa 5: Rc cannot be sent between threads safely”
error[E0277]: `Rc<String>` cannot be sent between threads safely
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(move || {
| ------------- required by a bound introduced by this call
...
= help: within `{closure@src/main.rs:6:32: 6:39}`, the trait `Send` is not implemented for `Rc<String>`

Rc đếm số chủ sở hữu bằng một con số bình thường, mà hai luồng có thể cùng lúc thay đổi và làm hỏng nó. Vì vậy Rc không phải Send (thông báo nói: Rc<String> không thể được gửi giữa các luồng một cách an toàn). Hãy dùng Arc (phiên bản “atomic”) thay vào đó. Nó hoạt động y như vậy và an toàn giữa các luồng:

use std::sync::Arc;
use std::thread;
fn main() {
let config = Arc::new(String::from("debug"));
let for_thread = Arc::clone(&config); // một chủ sở hữu thứ hai cho luồng
let handle = thread::spawn(move || {
println!("Luồng thấy: {for_thread}");
});
handle.join().unwrap();
println!("main vẫn giữ: {config}");
}

Kết quả:

Luồng thấy: debug
main vẫn giữ: debug
▶ Tự chạy thử

RefCell cũng vậy: giữa các luồng, hãy dùng Arc<Mutex<T>> thay cho Rc<RefCell<T>>. Xem Trạng thái chia sẻ và Đồng thời.

  • Hàm generic. Gọi fn announce<T: Display>(item: T) với struct của bạn sẽ báo `Song` doesn't implement `std::fmt::Display` kèm “required by a bound in announce” (bị ràng buộc trong announce yêu cầu). Cách sửa: triển khai trait mà ràng buộc (trait bound) đòi hỏi.
  • Test. assert_eq!(a, b) cần cả PartialEq lẫn Debug (để in các giá trị ra khi test thất bại). Cách sửa: #[derive(Debug, PartialEq)].
  • .collect() vào sai kiểu. let nums: Vec<i32> = words.iter().collect(); báo “a value of type Vec<i32> cannot be built from an iterator over elements of type &String” (không thể tạo Vec<i32> từ một iterator gồm các phần tử &String). Cách sửa: chuyển đổi từng phần tử trước, ví dụ .map(|w| w.parse().unwrap()). Xem Iterator.

Kiểm tra nhanh

1. Bạn gặp "Order doesn't implement Debug" khi dùng {:?}. Cách sửa nhanh nhất là gì?

2. Vì sao let total = 3_i32 + 2.5_f64; bị lỗi?

3. Code dùng luồng của bạn báo lỗi "Rc<String> cannot be sent between threads safely". Bạn nên dùng gì?

Điểm: 0 / 3