Mục lục
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ữfnthườ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;
fnpointer 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
fnpointer; closure có capture thì không. - Nắm hai use case kinh điển bắt buộc dùng
fnpointer: FFI callback với C quaextern "C" fnvà collection đồng nhất kiểuVec<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.
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.
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);
fnpointer 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) -> i32ngượ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à FFI —
fnpointer có ABI ổn định, có thể khai báoextern "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.
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.
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++.
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.
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:
fnpointer — 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 Fnbound) — 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.
Sizing — size_of Compare
Có thể đo trực tiếp bằng std::mem::size_of và size_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.
Tổng Kết
fn(...) -> ...là 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
fnpointer; closure có capture thì không. - Use case
fnpointer bắt buộc: FFI callback (extern "C" fnpass sang C) và collection đồng nhất kiểuVec<fn(...)>. - Trade-off:
fnpointer 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 traitFn/FnMut/FnOncevà hierarchyFn ⊂ FnMut ⊂ FnOnce(Bài 196), closure as parameter (Bài 197), từ khóamovecho thread/async (Bài 198), return closure quaBox<dyn Fn>(Bài 199) vàimpl Fnstatic dispatch (Bài 200), tới function pointer ở bài này.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Vì sao
let f: fn(i32) -> i32 = |x| x + 1;compile được, cònlet n = 5; let g: fn(i32) -> i32 = |x| x + n;không compile? - Khi nào phải dùng
fnpointer thay vì closure? Liệt kê hai trường hợp đã học. - So sánh
size_of_val(&|x: i32| x + 1)vớisize_of_val(&{let n=7i32; move |x: i32| x + n}). Vì sao khác? - 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
- Closure không capture coerce được sang
fnpointer; closure capturenkhông coerce vì cần mang state. - (a) FFI callback với C (
extern "C" fn); (b) collection đồng nhất kiểuVec<fn(...)>. - Closure không capture là ZST (0 byte); closure capture
i32~4 byte. Khác vì size closure = sum of captures. - 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ùngVec<fn(...)>; nếu có capture, dùngVec<Box<dyn Fn>>.
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.
