跳转到内容

Rust 错误 E0277:the trait bound is not satisfied(不满足 trait 约束)

E0277 的意思是:你把一个类型用在了需要某种能力的地方,而这个类型没有这种能力。 打印它、比较它、相加、转换它、把它发送到另一个线程:这些事都需要一个 trait,而你的类型缺了这个 trait。这是 Rust 最常见的错误之一,报错信息通常会直接告诉你怎么修,而且大多数时候只要改一行。(本页属于 Rust 编译错误详解系列。)

大多数人最先遇到的版本,是用 {} 打印自己写的结构体:

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

产生这个错误的程序:

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

编译器说:Player 没有实现 std::fmt::Display,所以不能用默认的格式化方式打印(“cannot be formatted with the default formatter”)。note: 还提醒你,在格式字符串里也许可以改用 {:?}(或者用 {:#?} 美化输出)。

trait 是一个承诺:某个类型能做某件事。Display 承诺“我知道怎么把自己漂亮地打印出来”。PartialEq 承诺“我可以用 == 比较”。Send 承诺“把我移动到另一个线程是安全的”。

像 println!、.sort()、+ 和 thread::spawn 这样的代码,都要求其中某个承诺。编译器指向你传进去的值(第 8 行的 p),然后说:这个类型从没做过这个承诺,所以我不能在这里用它。

想想墙上的插座。插座只接受一种形状的插头。你的电器也许完全没问题,可插头形状不对,就插不进去。E0277 就是编译器在说“插头不对”。修复方法是给这个类型配上合适的插头:通常用 #[derive(...)],有时写一个简短的 impl 块。

Rust 从不瞎猜。对你自己的 Player 类型来说,打印、比较或排序它,并没有唯一“正确”的方式。两个玩家名字相同就算相等,还是每个字段都相同才算?Rust 要你明确说出来,而不是悄悄做一件你没打算做的事。

这项检查发生在编译时。在很多语言里,打印一个你从没教过它怎么打印的对象,只会得到 <Player object at 0x7f...> 这种没用的东西;把一个线程不安全的值发送给线程,会在生产环境里偶尔崩溃一次。在 Rust 里,这两种情况都是编译错误,修一次就好。

trait 是怎么工作的,可以在 trait 这一页了解;日常用到的那些(Debug、Display、PartialEq、Ord、Hash)见常用 trait。

E0277 有很多副面孔。在下面找到和你的报错对应的那一条。

修复 1:用 {} 或 {:?} 打印结构体

Section titled “修复 1:用 {} 或 {:?} 打印结构体”

{:?} 需要 Debug。你几乎从来不用手写它:加上 #[derive(Debug)] 就行。它适合调试和日志。{:?} 版本的报错是这样的:

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`

{} 需要 Display。Rust 没法 derive 它,因为只有你知道“漂亮”的版本应该长什么样。用 write! 写一个小小的 impl。它适合给用户看的文字。

use std::fmt;
#[derive(Debug)] // 免费得到 {:?}
struct Player {
name: String,
score: u32,
}
// Display 要你自己写:它决定 {} 打印出什么。
impl fmt::Display for Player {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}({} 分)", self.name, self.score)
}
}
fn main() {
let p = Player { name: String::from("Ana"), score: 42 };
println!("{:?}", p);
println!("{}", p);
}

输出:

Player { name: "Ana", score: 42 }
Ana(42 分)
▶ 动手试一试

修复 2:比较、排序,或者用作 HashMap 的键

Section titled “修复 2:比较、排序,或者用作 HashMap 的键”

.contains() 和 == 需要 PartialEq。.sort() 需要 Ord。HashSet 的元素和 HashMap 的键需要 Eq 和 Hash。报错是这样的:

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`

修复方法是 derive 它们。它们一个建立在另一个之上,所以常用的全套是 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(); // 需要 Ord
println!("{:?}", tasks);
println!("包含 High:{}", tasks.contains(&Priority::High)); // 需要 PartialEq
let mut counts: HashMap<Priority, u32> = HashMap::new(); // 键需要 Eq + Hash
for task in &tasks {
*counts.entry(*task).or_insert(0) += 1;
}
println!("Low 任务数:{}", counts[&Priority::Low]);
}

输出:

[Low, Medium, High]
包含 High:true
Low 任务数:1
▶ 动手试一试

derive 出来的 Ord 会按你书写的顺序给枚举变体排序;结构体则从上到下逐个字段比较。有一个坑要老实告诉你:带有 f64 字段的类型不能 derive Eq、Hash 或 Ord,因为浮点数有 NaN,而 NaN 不等于它自己。浮点数怎么排序,见排序。

修复 3:混用数字类型(cannot add f64 to i32)

Section titled “修复 3:混用数字类型(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`

编译器说:不能把 f64 加到 i32 上,因为 i32 没有实现 Add<f64>。Rust 从不替你转换数字,连 i32 转 f64 都不会。+、-、*、/ 两边必须是同一种类型。用 as 自己转换其中一边:

fn main() {
let items: i32 = 3;
let price: f64 = 2.5;
let total = items as f64 * price; // 先转换,再相乘
println!("总计:{total}");
}

输出:

总计:7.5
▶ 动手试一试

f64::from(items) 也可以,而且永远不会丢失数据。反过来(f64 转 i32)会把小数部分直接切掉,所以先想好要不要四舍五入。细节见类型转换(as)和 From、Into 等转换 trait。

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

编译器说:? 没法把这个错误转换成 ConfigError,因为 ConfigError 没有实现 From<ParseIntError>。? 遇到错误时,会把它从你的函数里返回出去。如果这个错误的类型和函数承诺的不一样,? 会尝试用 From 来转换。你要做的就是写出这个 From:

use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
BadPort(ParseIntError),
}
// 教会 `?` 怎么把 ParseIntError 变成 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("eighty"));
}

输出:

Ok(8080)
Err(BadPort(ParseIntError { kind: InvalidDigit }))
▶ 动手试一试

其他不错的办法:只转换某一处调用,写成 .map_err(ConfigError::BadPort)?;用 thiserror crate 的 #[from];在小程序里返回 Result<T, Box<dyn std::error::Error>>。参见自定义错误类型和 anyhow 与 thiserror。

修复 5:Rc cannot be sent between threads safely

Section titled “修复 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<String> 不能安全地在线程之间发送,因为它没有实现 Send。Rc 用一个普通的数字来记录所有者的个数,两个线程可能同时修改这个数字,把它弄乱。所以 Rc 不是 Send 的。改用 Arc(“原子”版本),它的用法完全一样,而且在线程之间是安全的:

use std::sync::Arc;
use std::thread;
fn main() {
let config = Arc::new(String::from("debug"));
let for_thread = Arc::clone(&config); // 给线程的第二个所有者
let handle = thread::spawn(move || {
println!("线程看到的是:{for_thread}");
});
handle.join().unwrap();
println!("主线程还拥有:{config}");
}

输出:

线程看到的是:debug
主线程还拥有:debug
▶ 动手试一试

RefCell 也一样:跨线程时,用 Arc<Mutex<T>> 代替 Rc<RefCell<T>>。参见共享状态和并发。

  • 泛型函数。 用你的结构体去调用 fn announce<T: Display>(item: T),会得到 `Song` doesn't implement `std::fmt::Display`,并附带“required by a bound in announce”(由 announce 的约束要求)。修复:实现约束要求的那个 trait。
  • 测试。 assert_eq!(a, b) 同时需要 PartialEq 和 Debug(测试失败时要打印两个值)。修复:#[derive(Debug, PartialEq)]。
  • .collect() 成了错误的类型。 let nums: Vec<i32> = words.iter().collect(); 会报“a value of type Vec<i32> cannot be built from an iterator over elements of type &String”(不能用 &String 元素的迭代器构建 Vec<i32>)。修复:先转换每个元素,比如 .map(|w| w.parse().unwrap())。参见迭代器。

小测验

1. 使用 {:?} 时得到“Order doesn't implement Debug”。最快的修复是什么?

2. 为什么 let total = 3_i32 + 2.5_f64; 会失败?

3. 你的线程代码报错“Rc<String> cannot be sent between threads safely”。应该用什么?

得分: 0 / 3