Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Phân biệt được ba phương thức
iter(),iter_mut(),into_iter()theo đúng kiểuItemmỗi method sinh ra. - Biết hệ quả ownership của mỗi lựa chọn: collection còn nguyên, bị modify in-place, hay bị consume hoàn toàn.
- Hiểu mối tương ứng giữa ba method và ba dạng cú pháp
for x in &v,for x in &mut v,for x in v. - Đọc và sửa được lỗi compile
E0382 "value borrowed/used after move"khi lỡ tayinto_iterrồi đụng vào biến cũ. - Có một bảng so sánh gọn để tra cứu khi viết adapter chain ở các bài tiếp theo về
map,filter,collect.
3 Method Tạo Iterator
Mọi collection chuẩn trong stdlib đều có cùng bộ ba phương thức. Lấy Vec<String> làm ví dụ điển hình:
fn main() {
let names = vec![
String::from("Anh"),
String::from("Bình"),
String::from("Cường"),
];
// 1) iter() → Iterator<Item = &String>
for name in names.iter() {
println!("đọc: {}", name);
}
// 2) iter_mut() cần biến mutable
let mut names = names;
for name in names.iter_mut() {
name.push('!');
}
// 3) into_iter() consume names — sau loop names không còn dùng được
for name in names.into_iter() {
println!("move ra: {}", name);
}
}
Khác biệt cốt lõi không nằm ở tên gọi mà nằm ở kiểu của Item: &T, &mut T, hay T. Từ kiểu này, compiler suy ra hành vi ownership tương ứng — không phải developer phải nhớ thuộc lòng quy tắc riêng cho từng method.
Cả ba phương thức đều được trait IntoIterator chuẩn hoá: stdlib impl IntoIterator cho Vec<T>, &Vec<T>, &mut Vec<T> với ba kiểu Item tương ứng. Vì vậy cú pháp for ... in chỉ là syntactic sugar gọi đúng method phù hợp.
.iter() — Iterator<Item=&T>
iter() là phương thức "an toàn" nhất, gọi được kể cả khi collection không khai báo mut. Nó borrow shared toàn bộ collection trong suốt thời gian iterator còn sống, và sinh ra từng phần tử dạng &T:
fn main() {
let nums = vec![10, 20, 30];
let total: i32 = nums.iter().sum();
println!("tổng = {}", total); // 60
// nums vẫn dùng được sau loop
println!("len = {}", nums.len()); // 3
println!("phần tử đầu = {}", nums[0]); // 10
}
Vì Item = &i32, các adapter như sum::<i32>() cần được dereference (stdlib làm sẵn cho primitive). Với type tự định nghĩa, thường viết .iter().map(|x| x.do_something()) và tự dereference khi cần.
Cú pháp đường tương đương: for x in &v. Đây cũng là lý do trong nhiều codebase Rust, người ta thấy for x in &v nhiều hơn for x in v.iter() — cùng nghĩa nhưng gọn hơn và idiomatic hơn.
.iter_mut() — Iterator<Item=&mut T>
iter_mut() borrow exclusive collection và sinh ra từng phần tử dạng &mut T. Biến giữ collection bắt buộc là mut, nếu không compile lỗi ngay. Trong khối loop ta được phép modify in-place:
fn main() {
let mut nums = vec![1, 2, 3];
for n in nums.iter_mut() {
*n *= 10;
}
println!("{:?}", nums); // [10, 20, 30] — vẫn cùng vector, chỉ thay nội dung
}
Hai chi tiết hay nhầm. Thứ nhất, n có kiểu &mut i32 nên phải dereference *n khi gán giá trị mới. Thứ hai, borrow exclusive nghĩa là trong khi iterator còn sống, không ai khác (kể cả chính tên biến gốc) được phép borrow lại collection — nếu đọc nums.len() bên trong loop, borrow checker sẽ chặn.
Cú pháp đường tương đương: for n in &mut v. Sau loop, nums vẫn còn nguyên về mặt cấu trúc (cùng địa chỉ heap, cùng len, cùng capacity), chỉ khác giá trị từng phần tử.
.into_iter() — Iterator<Item=T>
into_iter() consume collection — gọi xong thì biến giữ collection cũ không còn dùng được, vì ownership của cả vector lẫn từng phần tử bên trong đã chuyển vào iterator. Mỗi lần next(), iterator move một phần tử ra dạng T:
fn main() {
let names = vec![
String::from("Anh"),
String::from("Bình"),
String::from("Cường"),
];
for name in names.into_iter() {
// name có kiểu String — sở hữu thực sự
println!("xử lý: {}", name);
// hết scope của name → String bị drop, heap được giải phóng
}
// println!("{:?}", names); // LỖI compile: names đã bị moved
}
Đây là lựa chọn đúng khi cần lấy ownership của từng phần tử ra để cho vào collection khác, gửi đi channel, hay return từ function. Với Copy type như i32, into_iter() không khác mấy về thực thể vì giá trị được copy chứ không move — nhưng về mặt type, Item = i32 chứ không phải &i32, nên các phép tính như .sum::<i32>() gọn hơn vài chỗ.
Cú pháp đường tương đương: for x in v — không có &, không có &mut. Chính vì cú pháp này quá gọn nên đây cũng là chỗ pitfall hay xảy ra (sẽ phân tích ở bước 8).
Ảnh Hưởng Ownership
Cách dễ nhớ nhất là gắn từng method vào một câu hỏi nghiệp vụ:
- Chỉ đọc, không thay đổi gì, sau loop vẫn dùng tiếp collection →
iter(). Borrow shared, an toàn nhất, không khoá tay nhiều người đọc. - Modify in-place từng phần tử, không xáo trộn cấu trúc (không insert/remove) →
iter_mut(). Borrow exclusive, ngăn người khác đọc cùng lúc. - Cần lấy ownership từng phần tử ra để chuyển nơi khác, hoặc destruct cả collection →
into_iter(). Move toàn bộ.
Quy tắc thực dụng: chọn cái nhẹ tay nhất đủ dùng. Cùng một bài toán, viết bằng iter() giữ collection lại để gọi tiếp các hàm khác sẽ linh hoạt hơn so với lỡ tay into_iter() rồi mất luôn vector. Khi đã biết chắc chắn vector chỉ dùng đúng một lần (ví dụ trong return statement cuối function), into_iter() mới phù hợp vì tránh được copy/clone không cần thiết.
Bảng So Sánh
| Method | Return Type | Ownership | Collection Sau Loop | Default for Syntax |
|---|---|---|---|---|
v.iter() |
Iterator<Item = &T> |
Borrow shared (&v) |
Còn nguyên, dùng tiếp được | for x in &v |
v.iter_mut() |
Iterator<Item = &mut T> |
Borrow exclusive (&mut v) |
Cấu trúc giữ nguyên, nội dung có thể thay đổi | for x in &mut v |
v.into_iter() |
Iterator<Item = T> |
Move (consume v) |
Invalid — biến cũ không dùng được | for x in v |
Bảng đúng cho Vec<T>, slice [T], VecDeque<T>, HashSet<T>, BTreeSet<T>. Với map (HashMap, BTreeMap) thì T là tuple (K, V) và iter_mut() chỉ cho modify value (&K, &mut V) — không cho modify key vì sẽ phá invariant của bảng băm/cây.
Common Pitfall: E0382 Sau Loop
Pitfall kinh điển: cú pháp for x in v trông giống y hệt JavaScript / Python nên người mới hay viết theo phản xạ, không nhận ra đây là into_iter() consume:
fn main() {
let names = vec![
String::from("Anh"),
String::from("Bình"),
];
for name in names { // sugar cho names.into_iter()
println!("{}", name);
}
println!("{} tên", names.len()); // LỖI compile
}
Compiler phát ra E0382 rất rõ ràng:
error[E0382]: borrow of moved value: `names`
|
6 | let names = vec![...];
| ----- move occurs because `names` has type `Vec<String>`,
| which does not implement the `Copy` trait
9 | for name in names {
| ----- `names` moved due to this implicit call to `.into_iter()`
12 | println!("{} tên", names.len());
| ^^^^^^^^^^^ value borrowed here after move
Cách sửa cũng rất gọn: thay for name in names bằng for name in &names (dùng iter()). Nếu thật sự muốn consume, hãy đảm bảo không có dòng nào đụng tới names sau loop — hoặc đẩy names.len() lên trước để snapshot lại số lượng:
let count = names.len();
for name in names { // consume — chấp nhận mất names
println!("{}", name);
}
println!("{} tên", count); // OK, count là i32 đã copy ra trước
Đối xứng, nếu compile error là "cannot borrow as mutable" thì kiểm tra xem có quên khai báo let mut trước khi gọi iter_mut() không — đó là pitfall bên kia của cùng câu chuyện.
Tổng Kết
- Mọi collection chuẩn cung cấp ba phương thức song song
iter(),iter_mut(),into_iter()— khác nhau ở kiểuItemtrả về. iter()→Iterator<Item = &T>, borrow shared, collection còn nguyên, default chofor x in &v.iter_mut()→Iterator<Item = &mut T>, borrow exclusive, cho phép modify in-place, default chofor x in &mut v.into_iter()→Iterator<Item = T>, consume collection, default chofor x in v. Sau loop, biến cũ không dùng được.- Chọn method theo nhu cầu thực: nhẹ tay nhất đủ dùng. Mặc định ưu tiên
iter(); chuyển sangiter_mut()khi cần modify; chỉinto_iter()khi thật sự muốn lấy ownership ra. - Lỗi
E0382 "value moved"saufor x in vlà dấu hiệu lỡ tay consume — fix bằng cách thayvthành&vhoặc snapshot dữ liệu cần trước khi consume.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Viết function
fn longest(words: &[String]) -> Option<&String>trả về tham chiếu tới phần tử cólen()lớn nhất. Phải dùngiter()chứ khônginto_iter()— tại sao? - Cho
let mut numbers = vec![1, 2, 3, 4, 5];, viết loop dùngiter_mut()để nhân đôi tất cả phần tử lẻ, giữ nguyên phần tử chẵn. Innumberssau loop để kiểm tra. - Cho
let v = vec![String::from("a"), String::from("b")];. Đoạn code sau compile được không? Vì sao?let upper: Vec<String> = v.into_iter().map(|s| s.to_uppercase()).collect(); println!("{:?}", v); - Với
HashMap<String, i32>, ba methoditer(),iter_mut(),into_iter()trả vềItemkiểu gì? Tại saoiter_mut()không cho modify key?
Đáp án
- Vì tham số đã là
&[String]— không sở hữu data, gọiinto_iter()trên slice&[T]thực ra trả vềItem = &Tchứ không phảiT(vì không thể move ra khỏi shared reference). Dùngiter()rõ ràng và idiomatic hơn. Kết quảOption<&String>mượn lại slot trong slice gốc nên không tốn cấp phát mới. for n in numbers.iter_mut() { if *n % 2 == 1 { *n *= 2; } } println!("{:?}", numbers); // [2, 2, 6, 4, 10]- Không compile.
v.into_iter()đã movev. Dòngprintln!("{:?}", v)phía sau cố borrow lại biến đã bị moved →E0382. Sửa bằngv.iter().map(|s| s.to_uppercase())—to_uppercasetrả vềStringmới, không cần consumev. iter()→Item = (&K, &V).iter_mut()→Item = (&K, &mut V)— key là shared, value là exclusive.into_iter()→Item = (K, V). Lý do key không cho mutable: nếu thay key sẽ làm sai bucket / vị trí cây, phá invariantHashMap/BTreeMap; muốn đổi key phảiremoverồiinsertlại.
Bài Tiếp Theo
Bài 204: Lazy Evaluation — Consumer Kích Hoạt — sau khi đã biết lấy iterator ra bằng ba cách, câu hỏi tiếp theo là iterator chỉ "chạy" khi nào? Bài 204 phân tích bản chất lazy của iterator chain: các adapter như map, filter không thực sự làm gì, chỉ ghép thêm một tầng cấu trúc; phải có một consumer như collect, sum, for_each kéo qua next() thì computation mới chạy. Hiểu lazy này giúp giải thích các quirk thường gặp (ví dụ map không in gì cả) và tận dụng được short-circuit của find, take, any.
