跳转到内容

Rust 错误 E0593:closure is expected to take a single 2-tuple as argument(闭包参数形式不对)

E0593 的意思是:你把一个闭包(或函数)交给了别的东西,比如 map、sort_by 或 thread::spawn,可它的参数个数和对方调用它的方式对不上。目前最常见的情况是:迭代器给你的闭包一个元组,你却写了两个参数。大多数时候只要改一个字符,而且 rustc 通常会替你写好修法。如果是你自己调用闭包时传的值个数不对,那就是 E0057。这个系列的其他错误,请看 Rust 编译错误详解。

error[E0593]: closure is expected to take a single 2-tuple as argument, but it takes 2 distinct arguments
--> src/main.rs:6:10
|
6 | .map(|i, name| format!("{}. {name}", i + 1)) // ❌ two parameters
| ^^^ --------- takes 2 distinct arguments
| |
| expected closure that takes a single 2-tuple as argument
|
help: change the closure to accept a tuple instead of individual arguments
|
6 - .map(|i, name| format!("{}. {name}", i + 1)) // ❌ two parameters
6 + .map(|(i, name)| format!("{}. {name}", i + 1)) // ❌ two parameters
|

第一行的意思是“闭包应该接收一个二元组作为参数,它却接收了 2 个独立的参数”。help 建议你“把闭包改成接收一个元组,而不是几个单独的参数”。

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

fn main() {
let names = vec!["Ferris", "Corro", "Rusty"];
let lines: Vec<String> = names
.iter()
.enumerate()
.map(|i, name| format!("{}. {name}", i + 1)) // ❌ two parameters
.collect();
println!("{:?}", lines);
}

E0593 还有另外几种第一行。它们说的都是“参数个数不对”:

error[E0593]: closure is expected to take 2 arguments, but it takes 1 argument
error[E0593]: closure is expected to take 0 arguments, but it takes 1 argument
error[E0593]: function is expected to take 1 argument, but it takes 2 arguments
error[E0593]: closure is expected to take 2 distinct arguments, but it takes a single 2-tuple as argument

依次是:闭包应该接收 2 个参数却只接收 1 个;应该接收 0 个却接收了 1 个;函数应该接收 1 个却接收了 2 个;应该接收 2 个独立的参数,却接收了一个二元组。

看第 6 行下面的两个标记:

  • ^^^ 指向 map。它是调用你的闭包的那一方,由它决定传几个值。
  • --------- 指向你的参数列表 |i, name|。这是你承诺要接收的东西。

两边对不上。enumerate() 把每个元素变成一个元组 (index, item),map 再把这个元组作为一个参数传给你的闭包。而你的闭包要的是两个单独的参数。

可以想象一个一次只能塞进一个信封的投信口。信封里可以装两封信(元组),但投信口不收两封散着的信。|(i, name)| 的意思是:“把信封给我,我当场拆开”。

信息里的“closure”或“function”告诉你传进去的是什么:是就地写的闭包,还是像 .map(add) 这样有名字的 fn。

每个接收闭包的方法都写清楚了它会怎样调用这个闭包。map 要的是 FnMut(Self::Item) -> B 这样的东西:一个参数,就是元素本身。对于 enumerate(),元素是 (usize, &&str),它是一个值,只不过恰好是个元组。sort_by 要的是 FnMut(&T, &T) -> Ordering:两个参数。thread::spawn 要的是 FnOnce() -> T:一个参数都没有。

Rust 从不替你把元组拆成几个单独的参数,也从不把几个单独的参数打包成元组。这样规则就很简单:方法签名写的是什么,你的闭包收到的就是什么。等你能看懂这些签名,就能预料到任何迭代器方法会交给你什么。

这背后的 Fn 系列 trait,详见闭包 trait;元组的相关内容见解构元组。

当信息里写着“a single 2-tuple”(一个二元组)时,选这个。这是你最常用到的修法。加上括号:|(i, name)| 接收一个元组,并给它的两个部分起名字。它适用于 enumerate、zip 和遍历 map:

use std::collections::BTreeMap;
fn main() {
let names = vec!["Ferris", "Corro", "Rusty"];
// enumerate 为每个元素给出一个 (index, name) 元组
let lines: Vec<String> = names
.iter()
.enumerate()
.map(|(i, name)| format!("{}. {name}", i + 1))
.collect();
println!("{:?}", lines);
// 遍历 map 时,每个元素也是一个 (key, value) 元组
let mut stock = BTreeMap::new();
stock.insert("apples", 3);
stock.insert("pears", 0);
stock.insert("plums", 5);
let total: i32 = stock.iter().map(|(_name, count)| *count).sum();
println!("一共 {total} 个水果");
}

输出:

["1. Ferris", "2. Corro", "3. Rusty"]
一共 8 个水果
▶ 动手试一试

_name 开头的 _ 是在告诉 Rust:“我知道我没用它”。你也可以只写一个 _。zip 的同类写法见 E0057 页面。

修复 2:按方法调用时传的个数来写参数

Section titled “修复 2:按方法调用时传的个数来写参数”

当信息里写着“expected to take N arguments”(应该接收 N 个参数)时,选这个。先查清方法会传什么,再写上正好那么多的参数:

  • sort_by 比较两个元素:|a, b|。
  • for_each、map、filter 传一个元素:|item|,不需要它时写 |_|。
  • thread::spawn 什么都不传:||,通常写成 move ||。
use std::thread;
fn main() {
// sort_by 每次比较两个元素:两个参数
let mut scores = vec![42, 7, 99, 15];
scores.sort_by(|a, b| b.cmp(a));
println!("{:?}", scores);
// for_each 每次交出一个元素:一个参数(不需要时用 _)
let tasks = vec!["write", "test", "ship"];
tasks.iter().for_each(|task| println!("待办:{task}"));
tasks.iter().for_each(|_| print!("*"));
println!();
// thread::spawn 调用闭包时什么都不传:零个参数
let id = 1;
let handle = thread::spawn(move || format!("{id} 号工作线程完成了"));
println!("{}", handle.join().unwrap());
}

输出:

[99, 42, 15, 7]
待办:write
待办:test
待办:ship
***
1 号工作线程完成了
▶ 动手试一试

想把数据传进线程,不要加参数,而是像上面的 id 那样用 move 捕获它。参见并发和排序。

修复 3:包装有名字的函数,或者让它接收元组

Section titled “修复 3:包装有名字的函数,或者让它接收元组”

当信息里写着“function is expected to take…”(函数应该接收……)时,选这个。你按名字传了一个函数,比如 .map(add),而它的参数对不上。下面的元素是一对一对的值,add 却接收两个数字:

error[E0593]: function is expected to take a single 2-tuple as argument, but it takes 2 distinct arguments
--> src/main.rs:7:48
|
1 | fn add(a: i32, b: i32) -> i32 {
| ----------------------------- takes 2 distinct arguments
...
7 | let sums: Vec<i32> = pairs.into_iter().map(add).collect();
| --- ^^^ expected function that takes a single 2-tuple as argument
| |
| required by a bound introduced by this call

你可以用一个小闭包把函数包起来,由它解开元组再调用函数;也可以写一个直接接收元组的版本:

fn add(a: i32, b: i32) -> i32 {
a + b
}
// 把一对值当作一个元组来接收的版本
fn add_pair((a, b): (i32, i32)) -> i32 {
a + b
}
fn main() {
let pairs = vec![(1, 2), (3, 4), (10, 20)];
// 包一层:进来一个元组,出去两个参数
let sums: Vec<i32> = pairs.iter().map(|&(a, b)| add(a, b)).collect();
println!("{:?}", sums);
// 或者传一个本来就接收一个元组的函数
let sums2: Vec<i32> = pairs.into_iter().map(add_pair).collect();
println!("{:?}", sums2);
}

输出:

[3, 7, 30]
[3, 7, 30]
▶ 动手试一试

|&(a, b)| 里的 & 是因为 .iter() 以 &(i32, i32) 的形式借出每一对值,这个模式把两个数字复制出来。通常包一层是更好的选择:add 仍然是个普通的双参数函数,代码的其他地方照样能调用它。

如果你自己写的泛型函数要求 Fn(i32) -> i32,而你传进去的是双参数的 add,就会得到“function is expected to take 1 argument, but it takes 2 arguments”(函数应该接收 1 个参数,它却接收 2 个)。修法是包一层闭包,把多出来的那个值补上,见 E0057 的修复 3。

HashMap::retain(正好反过来)。 大多数 map 方法给你的是元组,但 retain 会把键和值作为两个单独的参数传入。所以 stock.retain(|(name, count)| *count > 0) 会得到“closure is expected to take 2 distinct arguments, but it takes a single 2-tuple as argument”(闭包应该接收 2 个独立的参数,它却接收一个二元组)。修复:去掉括号,stock.retain(|_name, count| *count > 0)。Vec::retain 则和平常一样,接收一个元素。

忘了写参数。 tasks.iter().for_each(|| println!("task")) 会得到“closure is expected to take 1 argument, but it takes 0 arguments”(闭包应该接收 1 个参数,它却接收 0 个),rustc 会建议写成 |_|。修复:写 |_|,如果要用这个元素就写 |task|。

线程。 thread::spawn(|id| ...) 会得到“closure is expected to take 0 arguments, but it takes 1 argument”(闭包应该接收 0 个参数,它却接收 1 个)。修复:写成 thread::spawn(move || ...),从外面捕获 id。

函数指针类型的参数。 如果你的函数接收的是 f: fn(i32) -> i32(普通的函数指针,而不是泛型 F: Fn),传入双参数的 add 得到的会是 E0308,标注为“incorrect number of function parameters”(函数参数个数不对)。修法一样:包一层闭包,比如 |x| add(x, 10)。不捕获任何变量的闭包可以当作 fn 指针传入。

小测验

1. names.iter().enumerate().map(|i, name| ...) 报了 E0593。该怎么修?

2. scores.sort_by(...) 的闭包需要几个参数?

3. thread::spawn(|id| println!("{id}")) 报了 E0593。怎样把 id 传进线程?

得分: 0 / 3