Lập Trình Hàm Trong Rust: Closures và Iterators
Làm chủ closures và iterators trong Rust. Tìm hiểu cách zero-cost abstractions giúp bạn viết code biểu cảm mà không hy sinh hiệu suất.
Bài viết thứ 14 trong series Rust trung cấp. Sau khi đã thành thạo smart pointers, chúng ta sẽ chuyển sang những tính năng lập trình hàm của Rust. Rust rút mượn nhiều khái niệm từ các ngôn ngữ lập trình hàm: nó cung cấp closures (hàm ẩn danh) và iterators (chuỗi giá trị lười biếng, có thể kết hợp). Lời hứa chính là zero-cost abstractions — những cấu trúc cấp cao này biên dịch xuống cùng mã máy với vòng lặp viết tay.
graph TD
A["Lập Trình Hàm Rust"] --> B["Closures<br/>Fn, FnMut, FnOnce"]
A --> C["Iterators<br/>Đánh giá lười biếng"]
A --> D["Zero-Cost Abstractions"]
A --> E["map, filter, fold"]
Closures: Hàm Ẩn Danh#
Một closure là một hàm ẩn danh có thể bắt giữ môi trường xung quanh nó. Không giống như các hàm thông thường, closures có thể “đóng” (close over) các biến từ phạm vi bao quanh.
Cú Pháp Cơ Bản#
fn main() {
let add_one = |x: i32| x + 1;
println!("Kết quả: {}", add_one(5)); // Kết quả: 6
// Closures có thể bắt giữ môi trường
let multiplier = 3;
let multiply = |x: i32| x * multiplier;
println!("Kết quả: {}", multiply(5)); // Kết quả: 15
}rustCú pháp |x: i32| định nghĩa danh sách tham số, tương tự như closures ở các ngôn ngữ khác.
Bắt Giữ Môi Trường#
Closures bắt giữ các biến từ môi trường xung quanh theo ba cách:
Fn— bắt giữ bằng tham chiếu (&T)FnMut— bắt giữ bằng tham chiếu thay đổi (&mut T)FnOnce— bắt giữ bằng giá trị (chuyển quyền sở hữu)
fn main() {
let list = vec![1, 2, 3];
// Fn: bắt giữ bằng tham chiếu
let print_list = || println!("Danh sách: {:?}", list);
print_list();
// FnMut: bắt giữ bằng tham chiếu thay đổi
let mut counter = 0;
let mut increment = || {
counter += 1;
counter
};
println!("Bộ đếm: {}", increment()); // Bộ đếm: 1
println!("Bộ đếm: {}", increment()); // Bộ đếm: 2
// FnOnce: chuyển quyền sở hữu
let owned = String::from("hello");
let consume = || println!("Đã tiêu thụ: {owned}");
consume();
// println!("{owned}"); // ❌ Lỗi: giá trị đã bị di chuyển
}rust[!NOTE] Một closure cài đặt
FnOncecũng có thể được sử dụng nhưFnMuthoặcFn, nhưng ngược lại thì không. Compiler sẽ tự động suy luận trait ít hạn chế nhất cần thiết.
Closures Làm Đối Số Của Hàm#
Closures thực sự mạnh mẽ khi được truyền làm đối số cho các hàm bậc cao:
fn apply_to_list<T>(list: &[T], f: impl Fn(&T)) {
for item in list {
f(item);
}
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
apply_to_list(&numbers, |x| println!("Số: {x}"));
}rustClosures Là Giá Trị Trả Về#
Bạn cũng có thể trả về closures từ một hàm bằng cách sử dụng impl Fn:
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
|x| x * factor
}
fn main() {
let double = make_multiplier(2);
let triple = make_multiplier(3);
println!("Gấp đôi 5: {}", double(5)); // Gấp đôi 5: 10
println!("Gấp ba 5: {}", triple(5)); // Gấp ba 5: 15
}rustIterators: Chuỗi Giá Trị Lười Biếng#
Một iterator là một cấu trúc sản sinh ra một chuỗi giá trị. Trong Rust, iterators là lười biếng — chúng không làm gì cho đến khi bạn gọi một phương thức tiêu thụ như collect() hoặc for.
Tạo Iterators#
Mỗi collection trong Rust đều có phương thức .iter() trả về một iterator:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// .iter() trả về một iterator
let iter = numbers.iter();
// Iterators là lười biếng — chưa có gì xảy ra
for num in iter {
println!("{num}");
}
}rustCác Bộ Điều Hợp Iterator#
Các bộ điều hợp (adapter) nhận một iterator và trả về một iterator mới. Các bộ điều hợp phổ biến nhất là map, filter và fold:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers
.iter()
.filter(|&&x| x % 2 == 0) // giữ lại số chẵn
.map(|&x| x * 2) // nhân đôi
.collect(); // thu thập vào Vec
println!("{:?}", result); // [4, 8, 12, 16, 20]
}rustgraph LR
A["[1,2,3,4,5,6,7,8,9,10]"] --> B["filter: chẵn"]
B --> C["[2,4,6,8,10]"]
C --> D["map: x2"]
D --> E["[4,8,12,16,20]"]
Phương Thức fold#
fold là phiên bản hàm của vòng lặp for với bộ tích lũy:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Tính tổng
let sum: i32 = numbers.iter().fold(0, |acc, &x| acc + x);
println!("Tổng: {sum}"); // Tổng: 15
// Tìm giá trị lớn nhất
let max = numbers.iter().fold(0, |acc, &x| if x > acc { x } else { acc });
println!("Max: {max}"); // Max: 5
}rustTạo Iterator Tùy Chỉnh Với impl Iterator#
Bạn có thể tạo iterator riêng bằng cách cài đặt trait Iterator:
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Counter {
Counter { count: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
fn main() {
let counter = Counter::new(5);
let sum: u32 = counter.sum();
println!("Tổng: {sum}"); // Tổng: 15
}rustZero-Cost Abstractions#
Điểm nhấn của các tính năng lập trình hàm trong Rust là chúng biên dịch xuống cùng mã máy với vòng lặp viết tay. Hãy chứng minh:
// Phong cách hàm
fn functional_sum(numbers: &[i32]) -> i32 {
numbers.iter().filter(|&&x| x > &0).map(|&x| x * 2).sum()
}
// Phong cách thủ tục
fn imperative_sum(numbers: &[i32]) -> i32 {
let mut sum = 0;
for &x in numbers {
if x > 0 {
sum += x * 2;
}
}
sum
}rustCả hai hàm đều tạo ra mã lắp ráp (assembly) giống hệt nhau. Compiler tối ưu toàn bộ chuỗi iterator.
[!TIP] Iterators của Rust là ví dụ điển hình về zero-cost abstractions. Bạn được sở hữu sự biểu cảm của lập trình hàm với hiệu suất của C tối ưu thủ công.
Ví Dụ Trực Quan (ELI5): Dây Chuyền Sản Xuất#
Hãy tưởng tượng iterators như một dây chuyền sản xuất trong nhà máy:
- Nguyên liệu (collection gốc) sẽ đến ở đầu dây chuyền.
- Mỗi bộ điều hợp (
map,filter, v.v.) là một trạm làm việc biến đổi hoặc loại bỏ các mặt hàng. - Băng tải (iterator) là lười biếng — các mặt hàng chỉ di chuyển khi một công nhân ở cuối kéo chúng.
- Bộ tiêu thụ cuối cùng (
collect,sum,for) là công nhân lấy sản phẩm hoàn thiện từ băng tải.
Vì băng tải là lười biếng, nếu công nhân chỉ cần 3 mặt hàng, nhà máy chỉ xử lý 3 — nó không lãng phí năng lượng xử lý toàn bộ lô hàng!
Tổng Kết#
- Closures là hàm ẩn danh có thể bắt giữ môi trường, với ba chế độ bắt giữ:
Fn,FnMut, vàFnOnce. - Iterators là chuỗi giá trị lười biếng sản sinh giá trị khi cần, với các bộ điều hợp mạnh mẽ như
map,filter, vàfold. - Zero-cost abstractions đồng nghĩa với việc code phong cách hàm biên dịch thành cùng mã máy với vòng lặp thủ tục.
- Các iterator tùy chỉnh có thể được tạo bằng cách cài đặt trait
Iterator.
Trong bài viết tiếp theo, chúng ta sẽ đi sâu vào Fearless Concurrency — cách tiếp cận lập trình đồng thời an toàn của Rust với threads, channels và trạng thái chia sẻ.