Danh sách bài viết

Bài 196: Fn, FnMut, FnOnce — 3 Closure Traits

Bài 196 của series Rust Cơ Bản — với mỗi capture mode (&T, &mut T, move), compiler tự động implement một trong ba trait closure tương ứng — Fn, FnMut, hay FnOnce. Đây là cách Rust gắn thông tin "closure này gọi được mấy lần, có modify state không" vào type system, để khi function nhận closure làm tham số có thể yêu cầu đúng năng lực mình cần. FnOnce là trait dành cho closure consume capture: body chuyển ownership của biến capture đi nơi khác, nên closure chỉ chạy được đúng một lần — lần thứ hai biến đã bị moved, compiler sẽ chặn. FnMut là trait dành cho closure mượn &mut capture để modify state nội bộ: state vẫn nằm trong closure giữa các lần gọi nên gọi nhiều lần OK. Fn là trait dành cho closure chỉ mượn & capture: đọc thuần, gọi parallel an toàn vì không có shared mutable state. Ba trait không đứng độc lập mà tạo thành hierarchy thu hẹp: Fn ⊂ FnMut ⊂ FnOnce. Hiểu hierarchy này giúp chọn bound chuẩn cho function — bound lỏng nhất đủ dùng để không khoá tay caller. Bài cũng giải mã các thông báo compile error quen thuộc và lý do iterator method như map, filter, for_each nhận FnMut chứ không phải Fn.

09/06/2026
11 phút đọc
2 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu Rust có ba trait closure: FnOnce (gọi tối đa 1 lần), FnMut (gọi nhiều lần, có modify), Fn (gọi nhiều lần, không modify).
  • Biết compiler tự suy luận closure impl trait nào dựa trên capture mode đã học ở bài 195.
  • Nắm hierarchy Fn ⊂ FnMut ⊂ FnOnce — closure impl trait "mạnh" cũng đồng thời impl trait "yếu" hơn.
  • Chọn được bound đúng khi viết function nhận closure: lỏng nhất đủ dùng để không gò bó caller.
  • Đọc và sửa được compile error "use of moved value" hoặc "expected a closure that implements Fn".
  • Hiểu vì sao Iterator::map, filter, for_each nhận FnMut chứ không phải Fn hay FnOnce.

Bài yêu cầu nắm chắc ba capture mode (Bài 195), ownership và borrow checker (Nhóm 5), và generic trait bound (Nhóm 12). Bài 197 sẽ áp dụng ngay ba trait này để viết function nhận closure làm tham số với impl Trait hoặc generic.

2

3 Closure Trait Tổng Quan

Khi viết một closure |x| ..., compiler Rust tự sinh ngầm một struct ẩn danh giữ các biến capture, rồi auto-implement một hoặc cả ba trait sau cho struct đó:

  • FnOnce(Args) -> Output — gọi closure tiêu thụ chính nó (self). Sau call, closure bị drop. Tối đa một lần.
  • FnMut(Args) -> Output — gọi closure mượn mut chính nó (&mut self). Có thể modify state capture qua các lần gọi.
  • Fn(Args) -> Output — gọi closure chỉ mượn chính nó (&self). Không modify state, gọi đồng thời cũng OK.

Quy tắc auto-impl dựa hoàn toàn vào capture mode đã học ở bài 195. Body chỉ đọc capture → closure impl cả ba (Fn + FnMut + FnOnce). Body modify capture qua &mut → closure impl FnMut + FnOnce nhưng không impl Fn. Body move capture đi nơi khác → closure chỉ impl FnOnce. Lập trình viên không bao giờ gõ impl Fn for ... bằng tay — compiler lo hết.

Điểm quan trọng: ba trait này nằm trong module std::ops, ba trait này nằm trong prelude nên dùng trực tiếp tên ngắn. Ký hiệu Fn(i32) -> bool trong code là cú pháp đường (syntactic sugar) cho trait có một method call nhận i32 trả về bool. Cú pháp này đặc biệt thân thiện — không cần khai báo associated type Args/Output rườm rà.

3

FnOnce — Gọi 1 Lần Duy Nhất

Closure impl FnOnce (và chỉ FnOnce) khi body tiêu thụ biến capture — chuyển ownership đi: push vào collection khác, drop, hoặc return owned. Method call_once nhận self theo value, nghĩa là gọi xong closure cũng bị drop luôn.

fn main() {
    let msg = String::from("only-once");

    // closure consume `msg` vào print → cần ownership
    let consume = move || {
        let owned: String = msg; // move msg ra khỏi closure
        println!("Sent: {owned}");
    };

    consume();
    // consume(); // ERROR: use of moved value: `consume`
}

Ở đây msg bị bind sang owned bên trong body, ownership rời khỏi closure. Lần gọi đầu chạy bình thường; lần thứ hai compiler báo lỗi vì biến msg ngầm trong closure không còn. Đây là FnOnce điển hình. Nhiều API "fire-and-forget" sử dụng bound này: thread::spawn, Result::unwrap_or_else, Option::map_or_else — ý nghĩa là "tôi sẽ gọi closure đúng một lần".

4

FnMut — Modify Capture Mỗi Call

Closure impl FnMut khi body modify biến capture nhưng không consume — gọi push, gán lại, tăng đếm, ghi vào HashMap. State capture vẫn nằm trong closure giữa các lần gọi, nên có thể gọi nhiều lần để tích luỹ.

fn main() {
    let mut log: Vec<String> = Vec::new();

    let mut append = |s: &str| {
        log.push(s.to_string()); // modify capture, không move log đi
    };

    append("init");
    append("ready");
    append("done");

    drop(append); // kết thúc &mut log
    println!("{log:?}"); // ["init", "ready", "done"]
}

Closure append mượn &mut log. Mỗi call push thêm phần tử nhưng không lấy mất log — capture còn nguyên cho call sau. Vì gọi call_mut đòi &mut self, biến binding closure phải khai báo let mut append. Sau khi drop(append), borrow kết thúc và outer code lại đọc được log. Pattern này là xương sống của callback gom dữ liệu, builder, accumulator.

5

Fn — Read-Only Capture

Closure impl Fn khi body chỉ đọc biến capture — in ra, so sánh, dùng làm input mà không gán lại, không tiêu thụ. Vì method call nhận &self, closure có thể gọi nhiều lần và an toàn khi chạy song song (không có shared mutable state trong chính closure).

fn main() {
    let multiplier = 3;

    let scale = |x: i32| x * multiplier; // chỉ đọc multiplier

    println!("{}", scale(2));  // 6
    println!("{}", scale(10)); // 30
    println!("{}", scale(7));  // 21

    // multiplier vẫn dùng được vì closure chỉ borrow &i32
    println!("multiplier vẫn còn: {multiplier}");
}

Closure scale chỉ giữ &multiplier, không khoá quyền truy cập biến gốc. Gọi tuỳ thích, biến binding không cần mut. Đây là trait "lỏng nhất" về phía closure (yêu cầu ít quyền nhất) nhưng "chặt nhất" về phía bound function — function bound F: Fn chỉ nhận đúng nhóm closure này, từ chối closure modify hay consume.

6

Hierarchy: Fn ⊂ FnMut ⊂ FnOnce

Ba trait không độc lập mà tạo thành supertrait chain:

  • trait Fn<Args>: FnMut<Args> — closure impl Fn bắt buộc cũng impl FnMut.
  • trait FnMut<Args>: FnOnce<Args> — closure impl FnMut bắt buộc cũng impl FnOnce.
  • Hệ quả: mọi closure impl Fn đều impl cả ba.

Trực giác đơn giản: nếu closure chỉ cần &self để gọi (Fn), thì cũng gọi được khi cấp &mut self (FnMut) — chỉ là dùng nhiều quyền hơn cần. Và nếu gọi được nhiều lần qua &mut self, thì gọi được một lần qua self (FnOnce). Ngược lại không đúng: closure modify state thì không gọi được bằng &self chỉ đọc.

Closure bodyCapture modeFnFnMutFnOnce
Chỉ đọc capture&TOKOKOK
Modify capture&mut TOKOK
Consume capturemoveOK

Đảo chiều của hierarchy: function param càng "yếu" (FnOnce) thì nhận được nhiều loại closure nhất (cả ba cột); càng "mạnh" (Fn) thì gò bó nhất (chỉ cột đầu). Đây là điểm dễ nhầm — trait "mạnh hơn" về capability không có nghĩa là bound "thoáng hơn" cho caller.

7

Bound Chọn Theo Nhu Cầu

Khi viết function nhận closure, quy tắc vàng là: chọn bound lỏng nhất đủ dùng. "Đủ dùng" nghĩa là khớp với cách function gọi closure bên trong body.

// Gọi closure đúng 1 lần → bound FnOnce
fn run_once<F: FnOnce()>(f: F) {
    f();
}

// Gọi nhiều lần, có thể giữa các lần closure cập nhật state
fn run_many<F: FnMut()>(mut f: F) {
    for _ in 0..3 { f(); }
}

// Gọi nhiều lần, không quan tâm modify
fn run_pure<F: Fn()>(f: F) {
    for _ in 0..3 { f(); }
}

fn main() {
    let read = || println!("read");
    let mut counter = 0;
    let increment = || { let _ = counter; }; // chỉ đọc → Fn

    run_once(read);     // OK: Fn cũng impl FnOnce
    run_many(read);     // OK: Fn cũng impl FnMut
    run_pure(increment);// OK
}

F: FnOnce là bound flexible nhất phía caller — nhận closure thuộc bất kỳ trong ba nhóm — nhưng function chỉ được gọi một lần. F: FnMut trung dung — gọi nhiều lần được, nhận closure modify hoặc read-only. F: Fn chặt chẽ nhất phía caller — closure phải pure read, đổi lại function gọi parallel an toàn (ví dụ rayon::par_iter().for_each(...) đòi Fn + Sync).

Quy tắc nhỏ: nếu function chỉ gọi closure một lần → dùng FnOnce. Nếu gọi vòng lặp tuần tự nhiều lần → FnMut. Nếu gọi đồng thời nhiều thread → Fn (kèm Sync). Không có lý do thì đừng mặc định Fn — sẽ chặn caller truyền closure modify.

8

Compile Error Khi Vi Phạm

Có hai họ lỗi điển hình mà người mới gặp.

Họ 1 — Gọi FnOnce hai lần:

fn call_twice<F: FnOnce()>(f: F) {
    f();
    f(); // ERROR: use of moved value: `f`
}

FnOnce::call_once nhận self (by value), call đầu đã consume f. Compiler báo "value used here after move". Fix: hoặc nâng bound lên FnMut/Fn, hoặc gọi đúng một lần.

Họ 2 — Closure modify nhưng bound yêu cầu Fn:

fn run_pure<F: Fn()>(f: F) { f(); }

fn main() {
    let mut counter = 0;
    let bump = || { counter += 1; }; // closure này impl FnMut, không impl Fn

    // run_pure(bump);
    // ERROR: expected a closure that implements the `Fn` trait,
    //        but this closure only implements `FnMut`
}

Closure bump modify counter nên capture &mut counter, dẫn tới chỉ impl FnMut + FnOnce. Bound Fn đòi &self call, không thoả mãn. Fix: đổi signature thành F: FnMut và biến param mut f: F; hoặc viết lại closure để không modify (ví dụ tính giá trị mới rồi return thay vì gán). Đọc kỹ message "expected ... but only implements ..." cho biết chính xác phải đổi bound nào.

9

Idiom Iterator Method

Mở doc của std::iter::Iterator, ta thấy hầu hết method nhận closure đều dùng bound FnMut chứ không phải Fn:

// Signature đơn giản hoá từ std
fn map<B, F: FnMut(Self::Item) -> B>(self, f: F) -> Map<Self, F>;
fn for_each<F: FnMut(Self::Item)>(self, f: F);
fn filter<P: FnMut(&Self::Item) -> bool>(self, predicate: P) -> Filter<Self, P>;

Lý do bound FnMut thay vì Fn: iterator chạy tuần tự trong một thread, không gọi parallel, nên không cần ràng buộc closure pure. Cho phép FnMut để caller có thể truyền closure tích luỹ state — pattern rất hay gặp:

fn main() {
    let nums = vec![1, 2, 3, 4, 5];

    // closure FnMut: capture &mut total
    let mut total = 0;
    nums.iter().for_each(|x| total += x);
    println!("Sum = {total}"); // 15

    // closure FnMut: dùng &mut index để gán nhãn
    let mut idx = 0;
    let labeled: Vec<String> = nums.iter()
        .map(|x| { idx += 1; format!("#{idx}: {x}") })
        .collect();
    println!("{labeled:?}");
}

Vì bound là FnMut, nếu thay bằng closure read-only thuần (chỉ impl Fn) thì vẫn pass — nhờ hierarchy Fn ⊂ FnMut. Nhưng đảo lại, nếu API là Fn (như rayon::iter::IndexedParallelIterator::for_each) thì closure tích luỹ total ở trên sẽ không compile — phải đổi sang Mutex/AtomicI32 để share an toàn qua thread. Đây là lý do thực dụng vì sao đọc kỹ trait bound trước khi viết closure giúp tránh nhiều thất vọng.

10

Tổng Kết

  • Compiler Rust tự auto-impl một trong ba trait Fn, FnMut, FnOnce cho mọi closure dựa trên cách body dùng capture.
  • FnOnce: consume capture, gọi tối đa 1 lần (call_once nhận self).
  • FnMut: modify capture qua &mut, gọi nhiều lần OK (call_mut nhận &mut self).
  • Fn: chỉ đọc capture, gọi nhiều lần và parallel an toàn (call nhận &self).
  • Hierarchy Fn ⊂ FnMut ⊂ FnOnce: closure impl trait mạnh hơn cũng impl các trait yếu hơn.
  • Function bound: lỏng nhất đủ dùng — gọi 1 lần thì FnOnce, loop tuần tự thì FnMut, parallel thì Fn.
  • Iterator method (map, filter, for_each) bound FnMut để caller tích luỹ state thoải mái.
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Closure let f = |x: i32| x + n; (với n: i32 local, body chỉ cộng) impl trait nào? Có thể truyền cho function bound F: Fn(i32) -> i32 không?
  2. Closure let mut g = |x: i32| { v.push(x); }; (với v: Vec<i32>) impl trait nào? Có thể truyền cho function bound F: Fn không? Cho F: FnMut thì sao?
  3. Closure let h = move || { drop(s); }; (với s: String) impl trait nào? Vì sao chỉ impl một trait?
  4. Function fn apply<F: FnOnce()>(f: F) { f(); f(); } báo lỗi gì? Hai cách fix là gì?
  5. Vì sao Iterator::map bound FnMut chứ không phải Fn? Hệ quả thực dụng cho caller là gì?
Đáp án
  1. Closure capture &n nên impl cả ba: Fn, FnMut, FnOnce. Truyền cho F: Fn(i32) -> i32 hoàn toàn được.
  2. Capture &mut v → impl FnMut + FnOnce, không impl Fn. Truyền cho F: Fn báo lỗi "expected closure that implements Fn but only implements FnMut". Truyền cho F: FnMut thì OK, nhớ param phải khai báo mut f: F.
  3. Body drop(s) consume ownership s → closure chỉ impl FnOnce. Lý do: sau lần gọi đầu s đã bị drop, không còn để chạy lần hai, nên Rust không cấp FnMut hay Fn.
  4. Lỗi "use of moved value: f" ở call thứ hai vì FnOnce::call_once consume f. Fix 1: đổi bound thành F: FnMut hoặc F: Fn và param mut f: F. Fix 2: chỉ gọi f() một lần đúng theo ý nghĩa FnOnce.
  5. Iterator chạy tuần tự một thread, không cần ràng pure. Bound FnMut cho phép caller truyền closure tích luỹ state (sum, counter, builder) — vẫn pass closure read-only thuần nhờ hierarchy Fn ⊂ FnMut. Nếu API là Fn (parallel iter), caller phải dùng Mutex/atomic để share state qua thread.
12

Bài Tiếp Theo

Bài 197: Closure As Function Parameter — áp dụng ngay ba trait vừa học để viết function nhận closure làm tham số. Hai cú pháp phổ biến: generic fn apply<F: Fn(i32) -> i32>(f: F)impl Trait ngắn gọn fn apply(f: impl Fn(i32) -> i32). So sánh với Box<dyn Fn> khi cần trait object, và phân tích signature của map/filter trong std::iter.