Danh sách bài viết

Bài 201: Closure vs Function Pointer — fn Type

Bài 201 của series Rust Cơ Bản — bài cuối cùng của Nhóm 25 - Closures. Sau khi đã đi qua syntax, capture modes, trait Fn/FnMut/FnOnce, move, return closure qua Box<dyn Fn> và impl Fn ở các Bài 194-200, bài này khép lại nhóm bằng một đối tượng "anh em" nhưng khác hẳn closure: function pointer — kiểu fn(...) -> ... viết chữ thường. Function pointer là một raw pointer trỏ tới function code, kích thước cố định (8 byte trên 64-bit), không capture biến outer scope; còn closure có thể capture nhưng mỗi closure mang một concrete type ẩn riêng. Bài phân tích định nghĩa, so sánh trực tiếp với closure, coercion closure-không-capture sang fn, hai use case kinh điển (FFI callback và collection of function), trade-off và sizing.

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

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

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

  • Hiểu function pointer trong Rust là kiểu fn(...) -> ... (chữ fn thường), một raw pointer trỏ tới function code, size cố định 8 byte trên 64-bit.
  • Phân biệt rõ với closure: closure capture được environment và mỗi closure mang một concrete type ẩn riêng; fn pointer không capture và là common type ai cũng dùng được.
  • Biết quy tắc coercion: closure không capture có thể tự động ép thành fn pointer; closure có capture thì không.
  • Nắm hai use case kinh điển bắt buộc dùng fn pointer: FFI callback với C qua extern "C" fn và collection đồng nhất kiểu Vec<fn(i32) -> i32>.
  • So sánh trade-off và đo đạc kích thước thực tế với std::mem::size_of.

Bài này khép lại Nhóm 25 - Closures. Hiểu được fn pointer cũng là tiền đề cho Nhóm 26 - Iterators, nơi nhiều adapter chấp nhận cả closure lẫn function name.

2

Function Pointer fn(...)

Function pointer trong Rust được viết bằng cú pháp fn(T1, T2, ...) -> R — đúng từ khóa fn nhưng chữ thường và dùng như một type, không phải khai báo function. Đây là một raw pointer trỏ tới function code trong text segment của binary; size cố định 8 byte trên target 64-bit (4 byte trên 32-bit), không có state đi kèm, không capture biến outer scope.

fn square(x: i32) -> i32 { x * x }
fn double(x: i32) -> i32 { x * 2 }

fn main() {
    // Gán tên function vào biến có type fn(i32) -> i32
    let f: fn(i32) -> i32 = square;
    println!("{}", f(5));   // 25

    // Hoán đổi cùng signature thoải mái
    let g: fn(i32) -> i32 = double;
    println!("{}", g(5));   // 10
}

Mọi function (kể cả method static) có signature khớp đều coerce được sang cùng một fn pointer. Khác biệt cốt lõi với closure: fn pointer là common type nhiều function chia sẻ; closure mỗi cái sinh ra một concrete type ẩn không trùng nhau.

3

Closure Vs Function Pointer

Ba mặt cần phân biệt:

  • Capture environment — closure capture được biến outer scope (theo các mode đã học Bài 195); fn pointer không bao giờ capture, chỉ thấy tham số.
  • Type identity — mỗi closure là một concrete type ẩn duy nhất do compiler sinh ra. Hai closure cùng signature nhưng khác biểu thức vẫn khác type. fn(i32) -> i32 ngược lại là một common type: mọi function (và mọi closure không capture) phù hợp signature đều coerce sang nó.
  • ABI và FFIfn pointer có ABI ổn định, có thể khai báo extern "C" fn để truyền qua biên giới ngôn ngữ. Closure không có ABI cố định, không pass sang C được.

Hệ quả thực tế: bạn không bỏ ba closure khác nhau vào cùng một Vec được (mỗi closure khác type) mà phải Box<dyn Fn> (Bài 199) hoặc gom về Vec<fn(...) -> ...> nếu chúng không capture. Đây là lý do quan trọng để giữ fn pointer trong toolbox.

4

Closure Không Capture Coerce To fn Pointer

Quy tắc của Rust: closure không capture biến nào có thể tự động coerce sang kiểu fn pointer cùng signature. Closure có capture không coerce được vì cần mang theo state.

fn main() {
    // OK: Closure không capture - coerce được
    let f: fn(i32) -> i32 = |x| x + 1;
    println!("{}", f(10));   // 11

    // LỖI: Closure capture biến outer - KHÔNG coerce
    let n = 5;
    // let g: fn(i32) -> i32 = |x| x + n;
    //     ^ expected fn pointer, found closure
    //       closures can only be coerced to `fn` types if they do not
    //       capture any variables
    let _ = n;
}

Compiler báo rất rõ: "closures can only be coerced to fn types if they do not capture any variables". Khi cần truyền closure capture qua nơi yêu cầu fn pointer, không có cách "lừa" — phải đổi sang API nhận bound impl Fn hoặc Box<dyn Fn>, hoặc tái cấu trúc để state truyền qua tham số rõ ràng.

Coercion này hữu ích khi viết code song tồn cả hai dạng — ví dụ một test fixture nhận fn pointer nhưng test viết closure ngắn không capture vẫn pass được vào.

5

Use Case fn Pointer: FFI Callback

Use case bắt buộc đầu tiên: FFI callback. Khi gọi một C library nhận callback (ví dụ qsort, signal, GUI event handler), bên C chỉ hiểu function pointer theo C ABI. Closure Rust có type ẩn, không có C ABI nên không pass được.

use std::os::raw::c_int;

// Khai báo C function nhận callback
extern "C" {
    fn register_handler(cb: extern "C" fn(c_int));
}

// Callback - phải extern "C" fn và là named function
extern "C" fn on_signal(sig: c_int) {
    println!("Got signal: {sig}");
}

fn main() {
    unsafe {
        register_handler(on_signal);  // pass fn pointer sang C
    }
}

extern "C" fn là biến thể của function pointer dùng C calling convention thay vì Rust ABI. Bạn không thể dùng closure ở chỗ này — closure không có ABI ổn định, không có địa chỉ duy nhất để pass cho C. Đây là lý do quan trọng nhất giữ fn pointer trong ngôn ngữ: cây cầu giữa Rust và thế giới C/C++.

6

Use Case fn Pointer: Function As Value

Use case thứ hai: chứa tập function trong cùng một collection. Vì mỗi closure có concrete type ẩn riêng, không bỏ vào cùng Vec được; nhưng nhiều named function (và closure không capture) đều coerce về cùng một fn pointer type — đẩy vào Vec<fn(...) -> ...> thoải mái.

fn square(x: i32) -> i32 { x * x }
fn double(x: i32) -> i32 { x * 2 }
fn negate(x: i32) -> i32 { -x }

fn main() {
    let ops: Vec<fn(i32) -> i32> = vec![square, double, negate];

    for op in &ops {
        println!("{}", op(5));   // 25, 10, -5
    }
}

Đây là pattern dispatch table kinh điển: lookup theo index hoặc key rồi gọi function tương ứng. Pattern thường thấy trong virtual machine, parser combinator dạng table-driven, hoặc strategy pattern không cần state. Nếu cần state hoặc capture biến, đổi sang Vec<Box<dyn Fn(i32) -> i32>> như đã học Bài 199 — đánh đổi indirection và allocation.

7

Trade-off Closure Vs fn Pointer

Cùng một bài toán "truyền hành vi", chọn đường nào phụ thuộc nhu cầu:

  • fn pointer — chọn khi cần đi qua FFI boundary, hoặc gom nhiều hành vi vào collection đồng nhất, hoặc API chỉ chấp nhận type ổn định không có state. Bù lại: không capture được.
  • Closure (impl Fn bound) — chọn khi hành vi cần capture biến outer scope, hoặc viết ngắn inline tại chỗ dùng. Bù lại: mỗi closure type khác nhau, khó dùng trong collection đồng nhất.
  • Box<dyn Fn> — chọn khi cần cả capture lẫn collection đồng nhất, chấp nhận overhead heap allocation và vtable indirection (Bài 199).

Một nguyên tắc thực dụng: ưu tiên impl Fn/Fn bound cho generic API public, dùng fn pointer khi rõ ràng cần "function thuần" không state, và chỉ dùng Box<dyn Fn> khi hai dạng trên không đủ. Đừng đẩy mọi thứ về Box<dyn Fn> chỉ vì "linh hoạt nhất" — bạn trả giá allocation và dispatch cho thứ thường không cần.

8

Sizing — size_of Compare

Có thể đo trực tiếp bằng std::mem::size_ofsize_of_val:

use std::mem::{size_of, size_of_val};

fn add_one(x: i32) -> i32 { x + 1 }

fn main() {
    // fn pointer: 8 byte trên 64-bit target
    println!("fn pointer: {}", size_of::<fn(i32) -> i32>());          // 8

    // Closure không capture: 0 byte (zero-sized type)
    let c0 = |x: i32| x + 1;
    println!("no-capture closure: {}", size_of_val(&c0));               // 0

    // Closure capture i32: ~4 byte (alignment có thể bump)
    let n: i32 = 7;
    let c1 = move |x: i32| x + n;
    println!("capture i32: {}", size_of_val(&c1));                       // 4

    // Coerce closure không capture sang fn pointer - lên 8 byte
    let f: fn(i32) -> i32 = c0;
    println!("coerced fn: {}", size_of_val(&f));                         // 8

    let _ = add_one(0);
}

Tóm: fn pointer luôn 8 byte (raw address). Closure size = tổng các capture sau alignment — không capture gì thì zero-sized type (ZST, 0 byte), capture một i32 thì 4 byte, capture String thì 24 byte (con trỏ + len + cap). Đó là sự khác biệt vật lý ở level memory layout.

9

Tổng Kết

  • fn(...) -> ...function pointer — raw pointer 8 byte, không capture, là common type nhiều function chia sẻ.
  • Closure khác: capture environment, mỗi closure một concrete type ẩn riêng, size = sum of captures.
  • Closure không capture coerce tự động sang fn pointer; closure có capture thì không.
  • Use case fn pointer bắt buộc: FFI callback (extern "C" fn pass sang C) và collection đồng nhất kiểu Vec<fn(...)>.
  • Trade-off: fn pointer cho FFI/collection không state; closure cho capture state; Box<dyn Fn> cho cả hai nhưng có chi phí.
  • Tổng kết Nhóm 25 - Closures: Tám bài 194-201 đã đi từ syntax |x| x + 1 (Bài 194), ba mode capture by-ref/by-mut/by-move (Bài 195), ba trait Fn/FnMut/FnOnce và hierarchy Fn ⊂ FnMut ⊂ FnOnce (Bài 196), closure as parameter (Bài 197), từ khóa move cho thread/async (Bài 198), return closure qua Box<dyn Fn> (Bài 199) và impl Fn static dispatch (Bài 200), tới function pointer ở bài này.
10

Bài Tập Củng Cố

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

  1. Vì sao let f: fn(i32) -> i32 = |x| x + 1; compile được, còn let n = 5; let g: fn(i32) -> i32 = |x| x + n; không compile?
  2. Khi nào phải dùng fn pointer thay vì closure? Liệt kê hai trường hợp đã học.
  3. So sánh size_of_val(&|x: i32| x + 1) với size_of_val(&{let n=7i32; move |x: i32| x + n}). Vì sao khác?
  4. Vì sao không bỏ ba closure khác nhau vào cùng Vec được? Hai cách giải quyết phổ biến?
Đáp án
  1. Closure không capture coerce được sang fn pointer; closure capture n không coerce vì cần mang state.
  2. (a) FFI callback với C (extern "C" fn); (b) collection đồng nhất kiểu Vec<fn(...)>.
  3. Closure không capture là ZST (0 byte); closure capture i32 ~4 byte. Khác vì size closure = sum of captures.
  4. Mỗi closure có concrete type ẩn riêng — type khác nhau không vào cùng Vec. Giải: nếu không capture, dùng Vec<fn(...)>; nếu có capture, dùng Vec<Box<dyn Fn>>.
11

Bài Tiếp Theo

Bài 202: Iterator Trait — fn next(&mut self) -> Option<Item> — mở Nhóm 26 - Iterators. Iterator là một trong những trait quan trọng nhất stdlib, chỉ cần định nghĩa fn next(&mut self) -> Option<Item> là có ngay hơn 70 method miễn phí (map, filter, fold, collect...). Bài tới phân tích trait core, cách pull element, và viết custom iterator đầu tiên.