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.
Thông báo lỗi
Phần tiêu đề “Thông báo lỗi”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 parameters6 + .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 argumenterror[E0593]: closure is expected to take 0 arguments, but it takes 1 argumenterror[E0593]: function is expected to take 1 argument, but it takes 2 argumentserror[E0593]: closure is expected to take 2 distinct arguments, but it takes a single 2-tuple as argumentÝ nghĩa
Phần tiêu đề “Ý nghĩa”Hãy nhìn hai dấu đánh dấu dưới dòng 6:
^^^chỉ vàomap. Đó 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).
Vì sao Rust làm vậy
Phần tiêu đề “Vì sao Rust làm vậy”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
Phần tiêu đề “Cách sửa”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âyDấ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_byso sánh hai phần tử:|a, b|.for_each,map,filtertruyền một phần tử:|item|, hoặc|_|nếu bạn không cần nó.thread::spawnkhô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ếtviệc cần làm: kiểm thửviệc cần làm: phát hành***worker 1 đã xongĐể đư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 callBạ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 tuplefn 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]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.
Thường gặp ở đâu
Phần tiêu đề “Thường gặp ở đâu”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.
Lỗi liên quan
Phần tiêu đề “Lỗi liên quan”- E0057: closure takes N arguments but M were supplied: chính bạn gọi closure với sai số giá trị.
- E0631: type mismatch in closure arguments: số lượng đúng, nhưng một tham số sai kiểu, thường là
i32trong khi iterator đưa&i32. - E0061: function takes N arguments but M were supplied: gọi một hàm bình thường với sai số đối số.
- E0308: mismatched types: lỗi bạn nhận khi dùng tham số con trỏ
fn(...), hoặc khi dùng|(a, b)|mà phần tử hoàn toàn không phải tuple. - E0599: no method found: đôi khi xuất hiện ngay sau E0593, ví dụ “the method
collectexists … but its trait bounds were not satisfied”. Sửa E0593 trước là nó biến mất. - Tất cả các lỗi trình biên dịch: danh sách đầy đủ các trang lỗi.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”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
Đọc thêm
Phần tiêu đề “Đọc thêm”- E0593 — Rust error codes index (tiếng Anh): giải thích chính thức về lỗi này.
- Closures — The Rust Book (tiếng Anh): cú pháp closure, cách suy luận kiểu tham số, và các trait
Fn. - As input parameters — Rust by Example (tiếng Anh): ràng buộc
Fncủa một hàm quyết định nó nhận closure nào. Iterator::enumerate— tài liệu thư viện chuẩn (tiếng Anh): cho thấy mỗi phần tử trở thành một tuple(index, value).HashMap::retain— tài liệu thư viện chuẩn (tiếng Anh): phương thức của map có closure nhận key và value thành hai đối số.