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

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.

fn main() {
let add_one = |x: i32| x + 1;
println!("{}", add_one(5));
}

Kết quả:

6
▶ Tự chạy thử

Cặ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ường
let add_one_closure = |x: i32| x + 1; // cùng việc đó, viết bằng closure

Rust 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().

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ẻ
▶ Tự chạy thử

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ề.

Đâ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ả:

90
40
▶ Tự chạy thử

discount 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 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à 1
count bây giờ là 2
count bây giờ là 3
count cuối cùng: 3
▶ Tự chạy thử

Nế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.

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]
▶ Tự chạy thử

Ở đâ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.

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à 60
120 cộng thuế là 144
▶ Tự chạy thử

tax_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.

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 1
người dùng thứ hai nhận id 2
người dùng thứ ba nhận id 3
▶ Tự chạy thử

Cả 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.

  • 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 fn tươ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

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