Closure trong Rust
Closure là một hàm nhỏ, không có tên, mà bạn có thể lưu vào một biến và truyền đi khắp nơi. Điều làm nó khác hàm thông thường là nó có thể “bắt” (capture) các biến từ phạm vi nơi nó được tạo ra.
Cú pháp closure cơ bản
Phần tiêu đề “Cú pháp closure cơ bản”fn main() { let add_one = |x: i32| x + 1; println!("{}", add_one(5));}Kết quả:
6Cặp dấu gạch đứng |x: i32| thay cho cặp ngoặc tròn mà một hàm thông thường dùng. Hãy so sánh hai cách viết cạnh nhau:
fn add_one_fn(x: i32) -> i32 { x + 1 } // một hàm thông thườnglet add_one_closure = |x: i32| x + 1; // cùng việc đó, viết bằng closureRust thường cũng suy ra được kiểu của tham số và kiểu trả về, nên bạn sẽ hay thấy closure được viết còn ngắn hơn:
let add_one = |x| x + 1;Closure không có tham số dùng cặp gạch đứng rỗng: let say_hi = || println!("hi");, và bạn gọi nó bằng say_hi().
Closure nhiều dòng
Phần tiêu đề “Closure nhiều dòng”Khi có nhiều hơn một biểu thức, hãy bọc thân closure trong dấu ngoặc nhọn, giống như một hàm:
fn main() { let describe = |n: i32| { let kind = if n % 2 == 0 { "chẵn" } else { "lẻ" }; format!("{n} là số {kind}") };
println!("{}", describe(7));}Kết quả:
7 là số lẻformat! hoạt động như println!, nhưng thay vì in văn bản ra, nó trả văn bản đó về dưới dạng một String. Ở đây, String đó là biểu thức cuối cùng trong cặp ngoặc nhọn, nên nó chính là giá trị closure trả về.
Bắt biến từ phạm vi xung quanh
Phần tiêu đề “Bắt biến từ phạm vi xung quanh”Đây là tính năng mà hàm thông thường không có: closure có thể dùng các biến ở nơi nó được định nghĩa, mà bạn không cần truyền chúng vào một cách tường minh:
fn main() { let discount = 10; let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90 println!("{}", apply_discount(50)); // 40}Kết quả:
9040discount không phải là tham số của closure — nó chỉ nằm sẵn trong phạm vi của main, và closure với tay ra dùng trực tiếp. Một fn thông thường không làm được điều này; nó sẽ cần discount được truyền vào như một đối số.
Closure thay đổi biến đã bắt
Phần tiêu đề “Closure thay đổi biến đã bắt”Closure cũng có thể thay đổi một biến mà nó bắt, miễn là biến đó là mut. Có thêm một quy tắc: bản thân closure cũng phải được lưu trong một biến mut. Gọi closure sẽ thay đổi một thứ gì đó, và Rust muốn điều đó hiện rõ trong code của bạn.
fn main() { let mut count = 0;
let mut increment = || { count += 1; println!("count bây giờ là {count}"); };
increment(); increment(); increment();
println!("count cuối cùng: {count}");}Kết quả:
count bây giờ là 1count bây giờ là 2count bây giờ là 3count cuối cùng: 3Nếu bạn quên mut ở increment, trình biên dịch sẽ nói chính xác điều gì sai: calling increment requires mutable binding due to mutable borrow of count (gọi increment cần một binding khả biến vì nó mượn count theo kiểu khả biến).
Có một điểm đáng biết. Chừng nào closure còn có thể được gọi, nó giữ quyền thay đổi count, nên không ai khác được động vào count. Nếu bạn thêm println!("{count}"); vào giữa hai lần gọi increment(), bạn sẽ nhận error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable. Sau lần gọi increment() cuối cùng, closure đã xong việc, và count lại hoàn toàn thuộc về bạn. Vì thế dòng count cuối cùng chạy được. Bạn sẽ hiểu đầy đủ câu chuyện đằng sau quy tắc này trong bài Mượn.
Đôi khi closure cần lấy toàn quyền sở hữu những gì nó bắt, ví dụ khi nó được trao cho một luồng (thread) khác. Khi đó bạn viết move trước cặp gạch đứng: move || { ... }. Bạn sẽ thấy nó hoạt động trong bài Đồng thời.
Closure xuất hiện ở đâu trong thực tế
Phần tiêu đề “Closure xuất hiện ở đâu trong thực tế”Bạn sẽ ít khi dùng closure đứng một mình — chỗ chúng thật sự hữu ích là truyền một hành vi tùy chỉnh vào một hàm khác, đặc biệt là các phương thức iterator mà bạn sẽ gặp trong bài Iterator:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);}Kết quả:
[2, 4, 6, 8, 10]Ở đây, |n| n * 2 là một closure được truyền thẳng vào .map() — mẫu này (một closure nhỏ điều khiển một phương thức như .map() hay .filter()) là cách bạn thật sự dùng closure hằng ngày.
Luyện tập
Phần tiêu đề “Luyện tập”1. Giá có thuế
Phần tiêu đề “1. Giá có thuế”Tạo biến tax_percent có giá trị 20. Sau đó viết closure with_tax nhận một mức giá và trả về giá cộng thuế, dùng tax_percent đã bắt. In kết quả cho giá 50 và 120. Gợi ý: tiền thuế là price * tax_percent / 100.
Xem lời giải
fn main() { let tax_percent = 20; let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50 cộng thuế là {}", with_tax(50)); println!("120 cộng thuế là {}", with_tax(120));}Kết quả:
50 cộng thuế là 60120 cộng thuế là 144tax_percent không hề được truyền vào. Closure bắt nó từ main, đúng là điều mà một fn thông thường không làm được.
2. Bộ tạo ID
Phần tiêu đề “2. Bộ tạo ID”Viết closure next_id trả về 1 ở lần gọi đầu tiên, 2 ở lần thứ hai, 3 ở lần thứ ba, và cứ thế. Gọi nó ba lần và in ra từng ID. Gợi ý: giữ một bộ đếm mut bên ngoài closure, cộng thêm 1 vào nó bên trong, và để bộ đếm làm biểu thức cuối cùng của closure để nó được trả về.
Xem lời giải
fn main() { let mut last_id = 0;
let mut next_id = || { last_id += 1; last_id };
println!("người dùng thứ nhất nhận id {}", next_id()); println!("người dùng thứ hai nhận id {}", next_id()); println!("người dùng thứ ba nhận id {}", next_id());}Kết quả:
người dùng thứ nhất nhận id 1người dùng thứ hai nhận id 2người dùng thứ ba nhận id 3Cả last_id và next_id đều cần mut: cái đầu vì giá trị của nó thay đổi, cái sau vì gọi closure sẽ thay đổi thứ mà nó đã bắt.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Closure là một hàm ẩn danh nhỏ:
|params| expression. - Kiểu thường được suy luận, nên closure thường ngắn hơn một
fntương đương. - Khác với hàm thông thường, closure có thể bắt biến từ phạm vi nơi nó được định nghĩa.
- Closure thay đổi một biến đã bắt phải được lưu trong biến
mut, và nó mượn biến đó chừng nào còn được dùng. - Closure hữu ích nhất khi được truyền vào hàm khác — đặc biệt là các phương thức iterator như
.map().
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Closure dùng ký hiệu gì thay cho cặp ngoặc tròn của hàm để khai báo tham số?
2. Closure làm được điều gì mà một fn thông thường không làm được?
3. Theo bài này, trong thực tế closure thường được dùng ở đâu nhất?
4. Một closure làm count += 1 trên một biến đã bắt. Bạn cần thêm gì nữa?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- Closures — The Rust Book (tiếng Anh): chương chính thức về closure, suy luận kiểu và các trait
Fn. - Capturing References or Moving Ownership — The Rust Book (tiếng Anh): ba cách một closure có thể bắt biến, và khi nào dùng
move. - Closures — Rust by Example (tiếng Anh): các ví dụ ngắn chạy được, so sánh closure với hàm.
- Capturing — Rust by Example (tiếng Anh): thêm ví dụ về closure đọc, thay đổi và lấy quyền sở hữu biến.
- Closure expressions — The Rust Reference (tiếng Anh): các quy tắc chính xác về cú pháp closure và cách bắt biến.