跳转到内容

Rust 错误 E0631:type mismatch in closure arguments(闭包参数类型不匹配)

E0631 的意思是:你的闭包参数个数对了,但至少有一个参数的类型不对。十有八九,差别只在一个 &:你写的是 |x: i32|,而迭代器借给你的是 &i32。最快的修法往往是删掉类型注解,让 Rust 自己推断。本页先给出这个修法,再解释 & 是从哪儿来的,下次你就能提前料到。这个系列的其他错误,请看 Rust 编译错误详解。

error[E0631]: type mismatch in closure arguments
--> src/main.rs:3:43
|
3 | let doubled: Vec<i32> = prices.iter().map(|p: i32| p * 2).collect(); // ❌ items are &i32
| ^^^ -------- found signature defined here
| |
| expected due to this
|
= note: expected closure signature `fn(&{integer}) -> _`
found closure signature `fn(i32) -> _`
note: required by a bound in `map`
help: consider adjusting the signature so it borrows its argument
|
3 | let doubled: Vec<i32> = prices.iter().map(|p: &i32| p * 2).collect(); // ❌ items are &i32
| +

第一行的意思是“闭包参数类型不匹配”。help 建议你“调整一下签名,让它借用参数”。

产生这个错误的程序是这样的:

fn main() {
let prices = vec![120, 45, 300];
let doubled: Vec<i32> = prices.iter().map(|p: i32| p * 2).collect(); // ❌ items are &i32
println!("{:?}", doubled);
}

rustc 紧接着还会打印第二个更长的错误:the method `collect` exists for struct `Map<...>`, but its trait bounds were not satisfied(E0599,方法 collect 存在,但不满足它的 trait 约束)。那只是连带出来的问题,修好 E0631 它就会消失。

两行 note 是这条信息的核心。把它们读成“方法想要的是这个,你写的却是那个”:

  • expected fn(&{integer}) -> _:map 调用你的闭包时,会传入一个整数的引用。{integer} 表示“某种 Rust 还没确定下来的整数类型”,_ 表示“任意返回类型”。
  • found fn(i32) -> _:你的闭包声明它按值接收一个 i32。

把两行并排一看,唯一的区别就是那个 &。help: 那一行给出了只改一个字符的修法。

可以想想衣帽间:你交出外套,拿回一张号牌。.iter() 也是这样:它交给你的不是元素本身,而是指向元素的号牌(引用)。一个非要外套本身的闭包,是不会收号牌的。

每个迭代器方法都明确决定了它传给闭包的类型,而这个类型取决于你是怎样得到迭代器的:

  • v.iter() 借出每个元素:&T。
  • v.into_iter()(在你拥有的 Vec 上)把每个元素交出去:T。
  • filter 和 find 会把元素再借一次,所以在 .iter() 之上你得到的是 &&T。
  • sort_by 借给你两个元素:&T 和 &T。

不写参数类型时,Rust 会根据方法把它们补上,所以总是对得上。你自己写了类型,Rust 就会当真,并检查它。它不会悄悄复制值,也不会把引用变成拥有所有权的值,因为那会改变代码的行为(而且对 String 来说,复制要花内存和时间)。迭代器那一课讲了获取迭代器的三种方式,借用讲了引用。

先试这个。闭包参数几乎从来不需要写类型:Rust 清楚地知道 map、filter 和 sort_by 会传什么。删掉 : i32,代码就能编译了:

fn main() {
let prices = vec![120, 45, 300];
let ages = vec![12, 34, 17, 50];
let mut scores = vec![42, 7, 99, 15];
// 不写类型注解:Rust 会用迭代器真正给出的类型
let doubled: Vec<i32> = prices.iter().map(|p| p * 2).collect();
let adults: Vec<&i32> = ages.iter().filter(|age| **age >= 18).collect();
scores.sort_by(|a, b| b.cmp(a));
println!("{:?}", doubled);
println!("{:?}", adults);
println!("{:?}", scores);
}

输出:

[240, 90, 600]
[34, 50]
[99, 42, 15, 7]
▶ 动手试一试

p * 2 能用在 &i32 上,因为 Rust 的数字运算符也接受引用。在 filter 里,age 是 &&i32,所以要用 **age 穿过两层引用拿到数字。b.cmp(a) 这样的比较可以直接用在引用上。

修复 2:写上方法真正传入的类型

Section titled “修复 2:写上方法真正传入的类型”

当你想让类型一目了然时选这个,比如让一条很长的调用链更好读。照着 rustc 的 help: 加上 & 就行。也可以把 & 写在模式里:|&p| 会拆开这个引用,给你一份数字的普通副本(这适用于整数这类 Copy 类型):

fn main() {
let prices = vec![120, 45, 300];
let ages = vec![12, 34, 17, 50];
let mut scores = vec![42, 7, 99, 15];
// 写出真正的类型:.iter() 是 &,filter 里是 &&
let doubled: Vec<i32> = prices.iter().map(|p: &i32| p * 2).collect();
let adults: Vec<&i32> = ages.iter().filter(|age: &&i32| **age >= 18).collect();
scores.sort_by(|a: &i32, b: &i32| b.cmp(a));
// 或者用 & 模式把数字复制出来
let tax: Vec<i32> = prices.iter().map(|&p| p / 10).collect();
println!("{:?}", doubled);
println!("{:?}", adults);
println!("{:?}", scores);
println!("{:?}", tax);
}

输出:

[240, 90, 600]
[34, 50]
[99, 42, 15, 7]
[12, 4, 30]
▶ 动手试一试

注意两者的区别:|p: &i32| 的意思是“p 就是一个引用”,而 |&p| 的意思是“我收到一个引用,把它指向的东西给我”。

修复 3:改变迭代器交出来的东西

Section titled “修复 3:改变迭代器交出来的东西”

当你确实想要值本身、而不是引用时,选这个。有两种办法:

  • .iter().copied() 把每个 &i32 变成 i32。适用于数字、char、bool 这类小的 Copy 类型。(.cloned() 通过克隆,对 String 这类类型做同样的事。)
  • 在 Vec 上调用 .into_iter(),交出来的是元素本身。之后这个 Vec 就被用掉了,所以在你不再需要它时才选这个。
fn main() {
let prices = vec![120, 45, 300];
// .copied() 把 &i32 元素变成 i32 元素
let doubled: Vec<i32> = prices.iter().copied().map(|p: i32| p * 2).collect();
println!("{:?}", doubled);
// 在 Vec 上调用 .into_iter(),交出的是元素本身(Vec 也随之被用掉)
let names = vec![String::from("ferris"), String::from("corro")];
let loud: Vec<String> = names.into_iter().map(|name: String| name.to_uppercase()).collect();
println!("{:?}", loud);
}

输出:

[240, 90, 600]
["FERRIS", "CORRO"]
▶ 动手试一试

如果循环之后你还需要 names,就继续用 .iter(),改为接收 &String。从一个只是借来的 Vec 里把元素移出来,是另一个错误:E0507。

修复 4:让有名字的函数接收迭代器给出的类型

Section titled “修复 4:让有名字的函数接收迭代器给出的类型”

当你按名字传函数时选这个,比如 .map(shout)。这时信息里说的是“function”(函数),而不是“closure”(闭包):

error[E0631]: type mismatch in function arguments
--> src/main.rs:7:51
|
1 | fn shout(word: &String) -> String {
| --------------------------------- found signature defined here
...
7 | let loud: Vec<String> = words.into_iter().map(shout).collect();
| --- ^^^^^ expected due to this
| |
| required by a bound introduced by this call
|
= note: expected function signature `fn(&str) -> _`
found function signature `fn(&String) -> _`

元素是 &str,shout 要的却是 &String。一个好习惯:只读取文本的函数就接收 &str,这样字符串字面量、&String 等等它都能接受。当迭代器多加了一层 &(比如 filter)时,就用一个小闭包把调用包起来。在普通的函数调用里,Rust 会替你去掉多余的 &(这叫解引用强制转换,deref coercion),但按名字传函数时它做不到这一点:

// 接收 &str:字符串字面量、&String 等等都能用
fn shout(word: &str) -> String {
word.to_uppercase()
}
fn is_long(word: &str) -> bool {
word.len() > 4
}
fn main() {
let words = vec!["crab", "rustacean", "ferris"];
// 元素是 &str,所以 shout 可以按名字传入
let loud: Vec<String> = words.iter().copied().map(shout).collect();
println!("{:?}", loud);
// filter 给的是 &&str:用一个小闭包,让 Rust 替你转换
let long: Vec<&str> = words.iter().copied().filter(|w| is_long(w)).collect();
println!("{:?}", long);
}

输出:

["CRAB", "RUSTACEAN", "FERRIS"]
["rustacean", "ferris"]
▶ 动手试一试

这里的单词保留英文,因为 is_long 比较的是 len(),也就是字节数,换成中文结果就变了。&str 和 String 的区别,详见字符串。

排序时写了参数类型。 scores.sort_by(|a: i32, b: i32| b.cmp(&a)) 会得到 E0631,并提示 expected closure signature `for<'a, 'b> fn(&'a {integer}, &'b {integer}) -> _`。for<'a, 'b> 这部分只是说“对任意借用都成立”。修复:|a, b| b.cmp(a)。参见排序。

给 map 的条目写了拥有所有权的类型。 在 HashMap<String, i32> 上写 stock.iter().map(|(name, count): (String, i32)| ...),会得到 expected closure signature `fn((&String, &_)) -> _`。map 上的 .iter() 会把键和值都借出来。修复:去掉注解,或者写成 (&String, &i32)。

你自己写的泛型函数。 如果 fn apply<F: Fn(i32) -> i32>(f: F, x: i32) 收到的是闭包 |x: f64| x / 2.0,rustc 会报告 expected closure signature `fn(i32) -> _` 和 found closure signature `fn(f64) -> _`,并指向那个约束。修复:让闭包接收 i32(在里面再转换,x as f64 / 2.0);如果你本来就想要 f64,就改约束。参见闭包 trait。

for_each 里写了 String。 在 Vec<&str> 上写 words.iter().for_each(|w: String| ...),期望的是 fn(&&str)。&str 不是 String。修复:写 |w|,或者 |w: &&str|。

小测验

1. let v = vec![1, 2, 3]; 那么 v.iter().map(|x| ...) 里的闭包收到的是什么类型?

2. note 写着 expected closure signature fn(&{integer}) -> _ 和 found closure signature fn(i32) -> _。最快的修法是什么?

3. 在 prices.iter().map(...) 里,|&p| p * 2 做了什么?

得分: 0 / 3