Danh sách bài viết

Bài 203: iter() vs into_iter() vs iter_mut()

Bài 203 của series Rust Cơ Bản — phân biệt ba phương thức tạo iterator từ collection. Bài 202 đã giới thiệu trait Iterator với phương thức cốt lõi fn next(&mut self) -> Option<Self::Item>. Câu hỏi tiếp theo: lấy iterator từ một collection ra bằng cách nào? Rust không trả lời bằng một method duy nhất — thay vào đó, mọi collection chuẩn (Vec, HashMap, HashSet, slice, String...) đều cung cấp ba phương thức song song: iter(), iter_mut(), và into_iter(). Ba phương thức trả về ba iterator khác nhau về kiểu Item — và sự khác biệt đó dẫn đến ba hành vi ownership hoàn toàn riêng biệt. iter() sinh ra iterator có Item = &T, tức là mượn shared từng phần tử; collection nguyên vẹn sau khi loop chạy xong. iter_mut() sinh ra iterator có Item = &mut T, mượn exclusive từng phần tử để cho phép modify in-place; khung sườn collection vẫn còn nhưng nội dung từng phần tử đã thay đổi. into_iter() consume collection, sinh ra iterator có Item = T, move ownership của từng phần tử ra ngoài; sau loop, biến giữ collection cũ không dùng được nữa. Bài này tóm tắt ba lựa chọn, đính kèm bảng so sánh, và phân tích lỗi compile E0382 "value borrowed/used after move" — pitfall hay gặp khi lỡ tay into_iter nhưng vẫn nghĩ collection còn nguyên.

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

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ểu Item mỗ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ỡ tay into_iter rồ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.
2

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.

3

.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
}

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.

4

.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ử.

5

.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).

6

Ả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 collectioniter(). 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ả collectioninto_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.

7

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)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.

8

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.

9

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ểu Item trả về.
  • iter()Iterator<Item = &T>, borrow shared, collection còn nguyên, default cho for x in &v.
  • iter_mut()Iterator<Item = &mut T>, borrow exclusive, cho phép modify in-place, default cho for x in &mut v.
  • into_iter()Iterator<Item = T>, consume collection, default cho for 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 sang iter_mut() khi cần modify; chỉ into_iter() khi thật sự muốn lấy ownership ra.
  • Lỗi E0382 "value moved" sau for x in v là dấu hiệu lỡ tay consume — fix bằng cách thay v thành &v hoặc snapshot dữ liệu cần trước khi consume.
10

Bài Tập Củng Cố

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

  1. 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ùng iter() chứ không into_iter() — tại sao?
  2. Cho let mut numbers = vec![1, 2, 3, 4, 5];, viết loop dùng iter_mut() để nhân đôi tất cả phần tử lẻ, giữ nguyên phần tử chẵn. In numbers sau loop để kiểm tra.
  3. 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);
  4. Với HashMap<String, i32>, ba method iter(), iter_mut(), into_iter() trả về Item kiểu gì? Tại sao iter_mut() không cho modify key?
Đáp án
  1. Vì tham số đã là &[String] — không sở hữu data, gọi into_iter() trên slice &[T] thực ra trả về Item = &T chứ không phải T (vì không thể move ra khỏi shared reference). Dùng iter() 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.
  2. for n in numbers.iter_mut() {
        if *n % 2 == 1 { *n *= 2; }
    }
    println!("{:?}", numbers); // [2, 2, 6, 4, 10]
  3. Không compile. v.into_iter() đã move v. Dòng println!("{:?}", v) phía sau cố borrow lại biến đã bị moved → E0382. Sửa bằng v.iter().map(|s| s.to_uppercase())to_uppercase trả về String mới, không cần consume v.
  4. 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á invariant HashMap/BTreeMap; muốn đổi key phải remove rồi insert lại.
11

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.