Danh sách bài viết

Bài 129: Iterating HashMap

Bài 129 của series Rust Cơ Bản — sáu cách đi qua HashMap. Bộ method song hành với Vec — .iter(), .iter_mut(), .into_iter() — cộng thêm .keys(), .values(), .values_mut() để chọn duyệt một phía. Bài này phân tích từng cách, demo modify in-place qua iter_mut, giải thích vì sao thứ tự duyệt không deterministic giữa các lần chạy (SipHash seed ngẫu nhiên chống HashDoS), và đưa workaround collect rồi sort để có output ổn định khi cầ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ẽ:

  • Biết viết for (k, v) in &m để duyệt qua mọi entry trong HashMap mà không move map.
  • Phân biệt .keys(), .values(), .values_mut() — khi nào dùng từng method.
  • Dùng .iter_mut() để sửa giá trị in-place mà không cần tra cứu lại qua key.
  • Hiểu .into_iter() consume map, yield cặp (K, V) owned — cần khi muốn move giá trị ra ngoài.
  • Nhận ra rằng thứ tự duyệt là random per process theo thiết kế bảo mật, không thể dựa vào trong test/output.
  • Áp dụng workaround collect + sort_by_key hoặc chuyển sang BTreeMap khi cần ordering ổn định.
2

for (k, v) in &m — Iterate Borrow

Form thường gặp nhất: duyệt qua toàn bộ entry với borrow shared, không move m:

use std::collections::HashMap;

let mut m = HashMap::new();
m.insert("apple", 3);
m.insert("banana", 5);
m.insert("cherry", 2);

for (key, value) in &m {
    println!("{key}: {value}");
}

println!("len = {}", m.len());   // m còn nguyên, dùng được

Chi tiết quan trọng:

  • key kiểu &K (ở ví dụ là &&str), value kiểu &V (&i32). Đây là vì &HashMap implement IntoIterator với Item = (&K, &V).
  • m không bị move; sau loop bạn vẫn dùng m.len(), m.get(...)... bình thường — đúng như &Vec đã học ở Bài 123.
  • Form này tương đương method m.iter(): for (k, v) in m.iter() { ... }. Nếu cần chain adapter (.filter, .map), bắt buộc dùng .iter().
  • Khi println! in &i32, trait Display auto-deref nên không cần viết *value. Truyền vào hàm nhận i32 thuần thì phải dereference.

Đây là default — viết quen tay khi chỉ cần đọc cặp (key, value).

3

.keys() Chỉ Iterate Key

Khi chỉ cần danh sách key (ví dụ in tên tất cả mặt hàng, build set tên cột, hoặc kiểm tra membership), m.keys() trả về iterator chỉ yield reference đến key:

for k in m.keys() {
    println!("- {k}");
}

// Hoặc collect thành Vec để xử lý tiếp:
let names: Vec<&&str> = m.keys().collect();

Một số điểm cần nhớ:

  • Signature là fn keys(&self) -> Keys<'_, K, V>, trả về iterator yield &K. Đây là borrow shared — m không bị ảnh hưởng.
  • Tương đương m.iter().map(|(k, _)| k) nhưng ngắn và không phải viết tuple destructuring với underscore.
  • Hay dùng cùng .collect::<Vec<_>>() để có danh sách key dạng Vec rồi sort hoặc filter.

Đối ứng là .into_keys() — consume m, trả iterator yield K owned. Hữu ích khi cần lấy key ra để move sang struct khác và không còn cần m.

4

.values() / .values_mut() Iterate Value

Đối xứng với .keys(), hai method giúp duyệt phía value:

// Borrow shared, yield &V
for v in m.values() {
    println!("count = {v}");
}

// Borrow mut, yield &mut V — sửa từng value in-place
for v in m.values_mut() {
    *v += 100;
}
// Tất cả count đã + 100, không cần biết key

Vài lưu ý quan trọng:

  • .values() trả Values<'_, K, V> yield &V. Tốt cho phép tính tổng: let total: i32 = m.values().sum();.
  • .values_mut() trả ValuesMut<'_, K, V> yield &mut V. Đây là cách ngắn nhất để update tất cả value mà không cần đi qua từng key bằng m.get_mut(k).
  • m phải khai báo let mut m = ... để có quyền borrow mut.
  • Trong khi iterator values_mut còn sống, không được modify cấu trúc của map (insert/remove) — borrow checker chặn.

Đây cũng là cách thay đổi đồng loạt giá trị mà giữ nguyên key — ví dụ scale tất cả số đếm, đổi đơn vị, hoặc reset về 0 trước batch mới.

5

.iter_mut() Mutable Reference

Khi cần cả key (để quyết định công thức update) mutable access đến value, dùng .iter_mut():

let mut m: HashMap<&str, i32> = HashMap::new();
m.insert("apple", 3);
m.insert("banana", 5);

for (k, v) in m.iter_mut() {
    if *k == "apple" {
        *v *= 2;       // chỉ nhân đôi apple
    } else {
        *v += 1;
    }
}

Vài chi tiết cần nắm:

  • Iterator yield (&K, &mut V) — key vẫn là shared reference (không được đổi key giữa chừng vì hash đã placement), value là mutable reference.
  • Phải dereference *v để gán hoặc compound-assign. v = ... chỉ rebind biến local trong loop, không sửa map.
  • Đây là cách thay thế cho pattern dài dòng for k in keys { let v = m.get_mut(k).unwrap(); ... } — nhanh hơn (một lần lookup), gọn hơn, và borrow checker thấy rõ scope.
  • m phải là mut. Trong khi iterator còn sống, không thể đụng vào m theo cách khác.
6

.into_iter() Consume

Khi cần ownership của cặp key-value — ví dụ chuyển sang container khác, gửi vào channel, hoặc consume sang struct — dùng .into_iter():

let m: HashMap<String, Vec<i32>> = build_map();

let pairs: Vec<(String, Vec<i32>)> = m.into_iter().collect();
// m đã move, không dùng lại được

// Hoặc move sang BTreeMap để có thứ tự:
use std::collections::BTreeMap;
let sorted: BTreeMap<String, Vec<i32>> = m.into_iter().collect();

Một số điểm cần nhớ:

  • Iterator yield (K, V) owned. Bạn có thể move thẳng từng phần tử sang nơi khác, điều mà .iter() (yield (&K, &V)) không cho phép.
  • m bị move; sau loop không thể đụng vào nữa. Compiler sẽ báo "borrow of moved value" nếu bạn thử.
  • Form for (k, v) in m (không có &) là syntactic sugar gọi .into_iter() ngầm — kết quả tương đương.
  • Có thêm .into_keys().into_values() nếu chỉ cần một phía dạng owned.

Quy tắc tóm tắt giống Vec: cần đọc → .iter(); cần sửa value → .iter_mut() hoặc .values_mut(); cần move ra ngoài → .into_iter().

7

Order Không Deterministic

Điểm rất dễ "vấp" với newcomer: HashMap không bảo đảm thứ tự duyệt khớp với thứ tự insert, và càng không khớp giữa các lần chạy chương trình:

use std::collections::HashMap;

let mut m = HashMap::new();
m.insert("a", 1);
m.insert("b", 2);
m.insert("c", 3);

for (k, v) in &m {
    println!("{k}: {v}");
}
// Run 1:  c: 3 / a: 1 / b: 2
// Run 2:  b: 2 / c: 3 / a: 1
// Run 3:  a: 1 / b: 2 / c: 3

Lý do là HashMap mặc định dùng SipHash với một seed sinh ngẫu nhiên mỗi khi process khởi động — biện pháp chống HashDoS, tránh attacker chèn key cố ý gây collision. Kết quả: thứ tự bucket khác nhau mỗi run.

  • KHÔNG dựa vào thứ tự duyệt trong test (so sánh output bằng string). Test sẽ flaky.
  • KHÔNG in trực tiếp HashMap ra log nếu output cần diff được giữa các run.
  • Nếu cần thứ tự cố định theo key, dùng BTreeMap — sẽ học ở bài sau, tự sort theo K: Ord.
  • Nếu cần thứ tự insert (kiểu Python 3.7+), dùng crate indexmap::IndexMap ngoài stdlib.

Đây là quyết định thiết kế có chủ đích, không phải bug — phải nhớ rõ ngay từ đầu.

8

Workaround: Collect Rồi Sort

Khi thực sự cần output có thứ tự ổn định mà chưa muốn chuyển hẳn sang BTreeMap, kỹ thuật chuẩn là collect sang Vec rồi sort:

let mut v: Vec<(&&str, &i32)> = m.iter().collect();
v.sort_by_key(|(k, _)| *k);

for (k, val) in v {
    println!("{k}: {val}");
}
// Luôn in theo thứ tự alphabetic: a, b, c

Mở rộng — sort theo value (ví dụ ranking) cũng dùng cùng pattern:

let mut v: Vec<(&&str, &i32)> = m.iter().collect();
v.sort_by(|a, b| b.1.cmp(a.1));   // sort giảm dần theo value

for (k, val) in v.iter().take(3) {
    println!("Top: {k} = {val}");
}

Ưu nhược điểm:

  • Tốn thêm một Vec và một lần sort — O(n log n). Với map nhỏ-trung bình không vấn đề.
  • Linh hoạt: có thể sort theo key, theo value, theo tuple (k, v) compound.
  • Nếu cần thứ tự key cố định luôn luôn, BTreeMap trực tiếp tốt hơn — không phải collect/sort mỗi lần.

Trong test, idiom let mut keys: Vec<_> = m.keys().collect(); keys.sort(); assert_eq!(keys, vec![...]); là cách an toàn để assert nội dung map mà không phụ thuộc hash seed.

9

Tổng Kết

  • Sáu cách duyệt: for (k, v) in &m (≡ .iter()), .keys(), .values(), .values_mut(), .iter_mut(), .into_iter().
  • Borrow: .iter() yield (&K, &V); .iter_mut() yield (&K, &mut V); .into_iter() yield (K, V) consume map.
  • Một phía: .keys() / .values() / .values_mut() ngắn hơn .iter().map(|(k, _)| k).
  • Order random: thứ tự duyệt khác nhau mỗi process do SipHash seed; không dựa vào.
  • Workaround: let mut v: Vec<_> = m.iter().collect(); v.sort_by_key(|(k, _)| *k); để có ordered iteration.
  • Cần thứ tự cố định lâu dài: dùng BTreeMap (sort theo key) hoặc crate indexmap (theo insert order).
10

Bài Tập Củng Cố

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

  1. Cho let mut m: HashMap<&str, i32> = HashMap::new(); với ba entry "a"=1, "b"=2, "c"=3. Viết loop in tổng tất cả value bằng .values().sum::<i32>().
  2. let mut scores: HashMap<&str, i32> = ...;. Nhân đôi tất cả score bằng .values_mut() mà không lookup từng key.
  3. Cho HashMap<String, i32>. Viết loop in từng entry theo dạng "key=value", có thứ tự alphabetic theo key (dùng collect + sort).
  4. Vì sao for (k, v) in m { ... } rồi println!("{:?}", m) báo lỗi? Sửa thế nào nếu chỉ muốn đọc?
  5. Cho hàm fn to_pairs(m: HashMap<String, i32>) -> Vec<(String, i32)>. Viết body dùng .into_iter().collect(). Tại sao không dùng được .iter()?
  6. Viết test assert map m chứa đúng ba entry "a"=1, "b"=2, "c"=3 mà không phụ thuộc thứ tự duyệt. Gợi ý: collect key vào Vec, sort, rồi assert.
Đáp án
  1. let total: i32 = m.values().sum(); println!("total = {total}");
  2. for v in scores.values_mut() { *v *= 2; } — không cần tra cứu lại bằng get_mut.
  3. let mut v: Vec<_> = m.iter().collect(); v.sort_by_key(|(k, _)| (*k).clone()); for (k, val) in v { println!("{k}={val}"); }
  4. for (k, v) in m ngầm gọi m.into_iter(), move m vào loop; sau loop m đã mất ownership. Sửa: viết for (k, v) in &m.
  5. m.into_iter().collect(). Không dùng .iter()iter() trả (&String, &i32), không phải owned (String, i32) như signature yêu cầu — collect ra Vec<(String, i32)> sẽ không match.
  6. let mut keys: Vec<&String> = m.keys().collect(); keys.sort(); assert_eq!(keys, vec![&"a".to_string(), &"b".to_string(), &"c".to_string()]); assert_eq!(m.get("a"), Some(&1)); assert_eq!(m.get("b"), Some(&2)); assert_eq!(m.get("c"), Some(&3));
11

Bài Tiếp Theo

Bài 130: HashMap Với Custom Struct Làm Key — đến giờ bạn đã dùng key là String, &str, i32 — toàn những type stdlib đã implement sẵn Hash + Eq. Bài sau giải thích cần derive những trait gì khi muốn struct của mình làm key, axiom hash phải consistent với Eq, và demo một Point struct làm key cho cache 2D.