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: 还提醒你,在格式字符串里也许可以改用 {:?}(或者用 {:#?} 美化输出)。
这个错误是什么意思
Section titled “这个错误是什么意思”trait 是一个承诺:某个类型能做某件事。Display 承诺“我知道怎么把自己漂亮地打印出来”。PartialEq 承诺“我可以用 == 比较”。Send 承诺“把我移动到另一个线程是安全的”。
像 println!、.sort()、+ 和 thread::spawn 这样的代码,都要求其中某个承诺。编译器指向你传进去的值(第 8 行的 p),然后说:这个类型从没做过这个承诺,所以我不能在这里用它。
想想墙上的插座。插座只接受一种形状的插头。你的电器也许完全没问题,可插头形状不对,就插不进去。E0277 就是编译器在说“插头不对”。修复方法是给这个类型配上合适的插头:通常用 #[derive(...)],有时写一个简短的 impl 块。
Rust 为什么要这样做
Section titled “Rust 为什么要这样做”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:trueLow 任务数:1derive 出来的 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.5f64::from(items) 也可以,而且永远不会丢失数据。反过来(f64 转 i32)会把小数部分直接切掉,所以先想好要不要四舍五入。细节见类型转换(as)和 From、Into 等转换 trait。
修复 4:? couldn’t convert the error
Section titled “修复 4:? couldn’t convert the error”error[E0277]: `?` couldn't convert the error to `ConfigError` |8 | fn parse_port(text: &str) -> Result<u16, ConfigError> { | ------------------------ expected `ConfigError` because of this9 | 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主线程还拥有:debugRefCell 也一样:跨线程时,用 Arc<Mutex<T>> 代替 Rc<RefCell<T>>。参见共享状态和并发。
- 泛型函数。 用你的结构体去调用
fn announce<T: Display>(item: T),会得到`Song` doesn't implement `std::fmt::Display`,并附带“required by a bound inannounce”(由announce的约束要求)。修复:实现约束要求的那个 trait。 - 测试。
assert_eq!(a, b)同时需要PartialEq和Debug(测试失败时要打印两个值)。修复:#[derive(Debug, PartialEq)]。 .collect()成了错误的类型。let nums: Vec<i32> = words.iter().collect();会报“a value of typeVec<i32>cannot be built from an iterator over elements of type&String”(不能用&String元素的迭代器构建Vec<i32>)。修复:先转换每个元素,比如.map(|w| w.parse().unwrap())。参见迭代器。
- E0599:no method found:在键不是
Hash的HashMap上调用.insert(),有时报的是 E0599(“trait bounds were not satisfied”)。修复方法一样:derive 这些 trait。 - E0308:mismatched types:在需要一种类型的地方给了另一种,比如在该放
f64的地方放了i32。 - E0382:borrow of moved value:常常在你为了 trait 加上
Clone或Copy之后紧接着出现。 - E0369:binary operation
==cannot be applied:缺少PartialEq时==运算符给出的版本。修复方法相同。 - 所有编译错误详解
小测验
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
- E0277 — Rust error codes index(英文):这个错误的官方解释。
- Trait:定义共同行为 — Rust 程序设计语言:trait 是什么,以及 trait 约束是怎么工作的。
- 附录 C:可派生的 trait — Rust 程序设计语言:derive
Debug、PartialEq、Eq、Hash、PartialOrd和Ord时分别能得到什么。 - 用 Result 处理可恢复的错误 — Rust 程序设计语言:
?是怎么工作的,包括它用From做的转换。 Send— 标准库文档(英文):哪些类型可以在线程之间移动,以及Rc为什么不行。