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

Lỗi Rust E0593: closure is expected to take a single 2-tuple as argument (sai số tham số)

E0593 nghĩa là bạn đưa một closure (hoặc một hàm) cho một thứ khác, như map, sort_by hay thread::spawn, và số tham số của nó không khớp với cách nó sẽ được gọi. Trường hợp phổ biến nhất: một iterator đưa cho closure của bạn một tuple, còn bạn lại viết hai tham số. Phần lớn thời gian chỉ cần sửa một ký tự, và rustc thường viết sẵn cách sửa cho bạn. Nếu chính bạn là người gọi closure với sai số giá trị, thì đó lại là E0057. Các lỗi khác trong loạt bài này có ở Giải thích các lỗi trình biên dịch 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
|

Trình biên dịch nói rằng closure lẽ ra phải nhận một tuple 2 phần tử duy nhất, nhưng nó lại nhận 2 đối số riêng lẻ, và gợi ý đổi sang dạng tuple. Đây là chương trình gây ra lỗi:

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 còn đi kèm vài dòng mở đầu khác. Tất cả đều có nghĩa là “sai số tham số”:

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

Hãy nhìn hai dấu đánh dấu dưới dòng 6:

  • ^^^ chỉ vào map. Đó là bên gọi closure của bạn, và nó quyết định truyền bao nhiêu giá trị.
  • --------- chỉ vào danh sách tham số |i, name| của bạn. Đó là thứ bạn hứa sẽ nhận.

Hai bên không khớp nhau. enumerate() biến mỗi phần tử thành một tuple, (index, item), và map truyền tuple đó cho closure của bạn như một đối số. Còn closure của bạn lại đòi hai đối số riêng.

Hãy tưởng tượng một khe nhận thư chỉ nhận từng phong bì một. Bạn có thể bỏ hai lá thư vào trong một phong bì (một tuple), nhưng khe sẽ không nhận hai lá thư rời. |(i, name)| có nghĩa là “đưa tôi cái phong bì, tôi sẽ mở nó ngay tại đây”.

Chữ “closure” hay “function” trong thông báo cho biết bạn đã truyền gì: một closure viết tại chỗ, hay một fn có tên như .map(add).

Mọi phương thức nhận closure đều nói chính xác nó sẽ gọi closure như thế nào. map muốn thứ gì đó kiểu FnMut(Self::Item) -> B: một đối số, là phần tử. Với enumerate() thì phần tử là (usize, &&str). Đó là một giá trị, chỉ tình cờ là một tuple. sort_by muốn FnMut(&T, &T) -> Ordering: hai đối số. thread::spawn muốn FnOnce() -> T: không đối số nào.

Rust không bao giờ tự tách một tuple thành nhiều đối số riêng, và cũng không bao giờ gói nhiều đối số riêng thành một tuple. Nhờ vậy quy tắc rất đơn giản: chữ ký (signature) của phương thức nói gì thì closure của bạn nhận đúng như vậy. Khi đã đọc được những chữ ký này, bạn có thể đoán trước mọi phương thức iterator sẽ đưa gì cho bạn.

Bạn có thể đọc thêm về các trait Fn đứng sau chuyện này trong Trait của closure, và về tuple trong Destructuring một tuple.

Cách sửa 1: Tách tuple ngay trong danh sách tham số

Phần tiêu đề “Cách sửa 1: Tách tuple ngay trong danh sách tham số”

Chọn cách này khi thông báo nói “a single 2-tuple”. Đây là cách sửa bạn sẽ cần nhiều nhất. Thêm dấu ngoặc: |(i, name)| nhận một tuple và đặt tên cho hai phần của nó. Cách này dùng được cho enumerate, zip, và khi duyệt qua một map:

use std::collections::BTreeMap;
fn main() {
let names = vec!["Ferris", "Corro", "Rusty"];
// enumerate cho ra một tuple (index, name) cho mỗi phần tử
let lines: Vec<String> = names
.iter()
.enumerate()
.map(|(i, name)| format!("{}. {name}", i + 1))
.collect();
println!("{:?}", lines);
// duyệt một map cũng cho ra một tuple (key, value) cho mỗi phần tử
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} quả trái cây");
}

Kết quả:

["1. Ferris", "2. Corro", "3. Rusty"]
8 quả trái cây
▶ Tự chạy thử

Dấu _ ở đầu _name nói với Rust rằng “tôi biết mình không dùng biến này”. Bạn cũng có thể chỉ viết _. Ý tưởng tương tự cho zip được trình bày ở trang E0057.

Cách sửa 2: Viết đúng số tham số mà phương thức truyền vào

Phần tiêu đề “Cách sửa 2: Viết đúng số tham số mà phương thức truyền vào”

Chọn cách này khi thông báo nói “expected to take N arguments”. Hãy xem phương thức truyền gì, rồi viết đúng chừng ấy tham số:

  • sort_by so sánh hai phần tử: |a, b|.
  • for_each, map, filter truyền một phần tử: |item|, hoặc |_| nếu bạn không cần nó.
  • thread::spawn không truyền gì: ||, thường là move ||.
use std::thread;
fn main() {
// sort_by so sánh hai phần tử mỗi lần: hai tham số
let mut scores = vec![42, 7, 99, 15];
scores.sort_by(|a, b| b.cmp(a));
println!("{:?}", scores);
// for_each đưa từng phần tử một: một tham số (dùng _ nếu không cần)
let tasks = vec!["viết", "kiểm thử", "phát hành"];
tasks.iter().for_each(|task| println!("việc cần làm: {task}"));
tasks.iter().for_each(|_| print!("*"));
println!();
// thread::spawn gọi closure mà không truyền gì: không tham số
let id = 1;
let handle = thread::spawn(move || format!("worker {id} đã xong"));
println!("{}", handle.join().unwrap());
}

Kết quả:

[99, 42, 15, 7]
việc cần làm: viết
việc cần làm: kiểm thử
việc cần làm: phát hành
***
worker 1 đã xong
▶ Tự chạy thử

Để đưa dữ liệu vào một luồng (thread), đừng thêm tham số. Hãy bắt (capture) nó bằng move, như id ở trên. Xem Đồng thời và Sắp xếp.

Cách sửa 3: Bọc hàm có tên, hoặc cho nó nhận tuple

Phần tiêu đề “Cách sửa 3: Bọc hàm có tên, hoặc cho nó nhận tuple”

Chọn cách này khi thông báo nói “function is expected to take…”. Bạn đã truyền một hàm theo tên, như .map(add), và tham số của nó không khớp. Ở đây các phần tử là các cặp, nhưng add lại nhận hai số:

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

Bạn có thể bọc hàm trong một closure nhỏ, tách tuple ra rồi gọi hàm, hoặc viết một phiên bản nhận thẳng tuple:

fn add(a: i32, b: i32) -> i32 {
a + b
}
// một phiên bản nhận cả cặp như một tuple
fn add_pair((a, b): (i32, i32)) -> i32 {
a + b
}
fn main() {
let pairs = vec![(1, 2), (3, 4), (10, 20)];
// bọc: một tuple vào, hai đối số ra
let sums: Vec<i32> = pairs.iter().map(|&(a, b)| add(a, b)).collect();
println!("{:?}", sums);
// hoặc truyền một hàm vốn đã nhận một tuple
let sums2: Vec<i32> = pairs.into_iter().map(add_pair).collect();
println!("{:?}", sums2);
}

Kết quả:

[3, 7, 30]
[3, 7, 30]
▶ Tự chạy thử

Dấu & trong |&(a, b)| có mặt vì .iter() cho mượn từng cặp dưới dạng &(i32, i32), và mẫu (pattern) sẽ sao chép hai con số ra. Cách bọc thường là lựa chọn tốt hơn: add vẫn là một hàm hai đối số bình thường mà phần còn lại của code có thể gọi.

Nếu hàm generic của riêng bạn muốn Fn(i32) -> i32 mà bạn lại truyền một add hai đối số, bạn sẽ nhận “function is expected to take 1 argument, but it takes 2 arguments”. Cách sửa là một closure bọc điền sẵn giá trị còn thiếu, được trình bày trong Cách sửa 3 của E0057.

HashMap::retain (ngược lại). Hầu hết các phương thức của map đưa cho bạn một tuple, nhưng retain truyền key và value thành hai đối số riêng. Vì vậy stock.retain(|(name, count)| *count > 0) cho ra “closure is expected to take 2 distinct arguments, but it takes a single 2-tuple as argument”. Cách sửa: bỏ dấu ngoặc, stock.retain(|_name, count| *count > 0). Vec::retain vẫn nhận một phần tử như thường lệ.

Quên tham số. tasks.iter().for_each(|| println!("task")) cho ra “closure is expected to take 1 argument, but it takes 0 arguments”, và rustc gợi ý |_|. Cách sửa: |_|, hoặc |task| nếu bạn muốn dùng nó.

Luồng. thread::spawn(|id| ...) cho ra “closure is expected to take 0 arguments, but it takes 1 argument”. Cách sửa: thread::spawn(move || ...) và bắt id từ bên ngoài.

Một tham số con trỏ hàm. Nếu hàm của bạn nhận f: fn(i32) -> i32 (một con trỏ hàm thường, không phải generic F: Fn), truyền vào một add hai đối số sẽ cho ra E0308, kèm nhãn “incorrect number of function parameters”. Cách sửa vẫn vậy: một closure bọc như |x| add(x, 10). Closure không bắt gì có thể được truyền như con trỏ fn.

Kiểm tra nhanh

1. names.iter().enumerate().map(|i, name| ...) cho ra E0593. Cách sửa là gì?

2. Closure cho scores.sort_by(...) cần bao nhiêu tham số?

3. thread::spawn(|id| println!("{id}")) cho ra E0593. Làm sao đưa id vào luồng?

Điểm: 0 / 3