Memasukkan dan membaca satu per satu hanya separuh pekerjaan. Laporan harian toko harus menelusuri seluruh produk, pembaruan diskon mengubah banyak stok sekaligus, dan proses pembersihan menghapus entry yang sudah tidak laku. Iterasi di HashMap memiliki satu kejutan yang harus kita hadapi dengan jujur: urutan penelusuran tidak pernah dijamin karena biji acak hash berbeda di setiap eksekusi. Chapter ini mengajarkan pola iterasi yang stabil, pembaruan value lewat get_mut lengkap dengan kasus key yang tidak ada, penghapusan entry dengan remove, dan satu jebakan klasik berupa insert di tengah iterasi yang ditolak compiler dengan E0502.

Iterasi Pasangan Key-Value #

Loop for di atas reference map membagikan setiap pasangan dalam bentuk tuple:

rust
use std::collections::HashMap;

fn main() {
    let mut product_stock = HashMap::new();
    product_stock.insert(String::from("Kopi"), 25);
    product_stock.insert(String::from("Teh"), 40);
    product_stock.insert(String::from("Gula"), 60);

    for (name, stock) in &product_stock {
        println!("{}: {} unit", name, stock);
    }
}

Contoh output pada salah satu eksekusi di komputer penulis:

text
Kopi: 25 unit
Gula: 60 unit
Teh: 40 unit

Perhatikan label di atas: kita sengaja tidak menyebutnya “Output yang Dihasilkan” seperti biasa, karena tiga baris tersebut hanya salah satu dari enam kemungkinan urutan. Angka hash menentukan bucket tempat tiap key disimpan, dan biji acak hash di-generate ulang setiap kali proses program berjalan — pada eksekusi berikutnya di mesin yang sama, urutan Kopi-Gula-Teh bisa berubah menjadi Teh-Kopi-Gula atau susunan lainnya. Menjalankan program ini sepuluh kali menghasilkan lebih dari satu macam urutan; itulah bukti eksperimental bahwa urutan iterasi HashMap tidak boleh dijadikan dasar logika program. Pola destructuring (name, stock) di baris 9 mengikat name sebagai &String dan stock sebagai &i32 — keduanya pinjaman immutable atas isi map, cukup untuk mencetak laporan.

Urutan Stabil: Kumpulkan lalu Sort #

Ketika laporan memang harus tampil terurut — pelanggan berhak melihat daftar produk berurutan abjad — solusinya bukan memaksa HashMap berurutan, melainkan mengumpulkan hasil iterasi ke dalam Vec lalu memanggil sort:

rust
use std::collections::HashMap;

fn main() {
    let mut product_stock = HashMap::new();
    product_stock.insert(String::from("Kopi"), 25);
    product_stock.insert(String::from("Teh"), 40);
    product_stock.insert(String::from("Gula"), 60);

    let mut product_names = Vec::new();
    for (name, _) in &product_stock {
        product_names.push(name);
    }
    product_names.sort();

    println!("Produk yang tercatat (terurut):");
    for name in product_names {
        println!("- {}", name);
    }
}

Output yang Dihasilkan:

text
Produk yang tercatat (terurut):
- Gula
- Kopi
- Teh

Output ini stabil di setiap eksekusi karena keterurutan datang dari sort, bukan dari internal map. Mari kita bedah baris-barisnya:

  1. Baris 9 (let mut product_names = Vec::new();): vector penampung dibuat kosong; tipe elemennya &String akan disimpulkan dari isi yang di-push.
  2. Baris 10-12 (for (name, _) in &product_stock): iterasi tetap berjalan di urutan acak, tetapi urutan masuk tidak penting lagi. Pattern (name, _) hanya mengikat key — underscore membuang value yang tidak dibutuhkan. Setiap push(name) menyimpan pinjaman &String ke dalam vector; tidak ada clone, tidak ada penyalinan data teks.
  3. Baris 13 (product_names.sort();): method sort mengurutkan elemen reference berdasarkan nilai yang ditunjuknya, sehingga nama tersusun abjad. Vec adalah struktur yang urutannya kita kendalikan penuh — di sinilah letak keterurutan.
  4. Baris 16-18 (for name in product_names): loop kedua mencetak hasil terurut. Memakai product_names tanpa & memindahkan vector ke dalam loop — sah, karena vector ini memang sekali pakai untuk pencetakan.

Pola “kumpulkan lalu sort” ini adalah jawaban standar setiap kali kita membutuhkan laporan terurut dari HashMap. Ketika keterurutan adalah kebutuhan utama — bukan sekadar kebutuhan saat cetak — Rust punya koleksi yang lahir untuk itu: BTreeMap, yang kita bahas dua chapter ke depan.

Mengubah Nilai dengan get_mut #

Pembaruan value mengikuti pola Vec dengan iter_mut, hanya saja kuncinya bukan index melainkan key. Method get_mut mengembalikan Option<&mut V>, dan kasus None adalah tempat kita menempatkan keputusan bisnis untuk key yang belum tercatat:

rust
use std::collections::HashMap;

fn main() {
    let mut product_stock = HashMap::new();
    product_stock.insert(String::from("Kopi"), 25);

    println!("Stok awal Kopi: {:?}", product_stock.get("Kopi"));

    match product_stock.get_mut("Kopi") {
        Some(stock) => *stock += 10,
        None => {
            product_stock.insert(String::from("Kopi"), 10);
            println!("Kopi belum tercatat, sekarang ditambahkan");
        }
    }

    match product_stock.get_mut("Susu") {
        Some(stock) => *stock += 10,
        None => {
            product_stock.insert(String::from("Susu"), 10);
            println!("Susu belum tercatat, sekarang ditambahkan");
        }
    }

    println!("Stok Kopi: {:?}", product_stock.get("Kopi"));
    println!("Stok Susu: {:?}", product_stock.get("Susu"));
}

Output yang Dihasilkan:

text
Stok awal Kopi: Some(25)
Susu belum tercatat, sekarang ditambahkan
Stok Kopi: Some(35)
Stok Susu: Some(10)

Mari kita bedah kode di atas baris per baris:

  1. Baris 7 (product_stock.get("Kopi")): pembacaan awal menegaskan stok Kopi saat ini — Some(25) — sebelum pengiriman barang diproses.
  2. Baris 9-15 (match product_stock.get_mut("Kopi")): key "Kopi" ada, cabang Some berjalan, dan *stock += 10 mengubah 25 menjadi 35 langsung di dalam map. Cabang None pada blok ini tidak pernah tersentuh.
  3. Baris 17-23 (match product_stock.get_mut("Susu")): key "Susu" tidak ada, cabang None berjalan — program memasukkan pasangan baru dengan stok awal 10 sekaligus mencatat kejadian tersebut. Kedua cabang match menutup kedua kemungkinan, sehingga compiler memastikan tidak ada jalur terlupakan.
  4. Baris 25-26: hasil akhir membuktikan kedua jalur bekerja: Kopi dinaikkan lewat Some, Susu diciptakan lewat None.

Jika pola “tambah kalau ada, buat kalau tidak” ini muncul di banyak tempat, satu baris entry(...).or_insert(...) dari chapter sebelumnya menggantikan seluruh blok match — pilihannya murni soal keterbacaan di titik pemanggilan.

Menghapus Entry dengan remove #

Method remove menghapus pasangan berdasarkan key dan mengembalikan value-nya dalam Option — None berarti tidak ada yang dihapus:

rust
use std::collections::HashMap;

fn main() {
    let mut product_stock = HashMap::new();
    product_stock.insert(String::from("Kopi"), 25);
    product_stock.insert(String::from("Gula"), 60);

    let removed_stock = product_stock.remove("Gula");
    println!("Stok Gula yang dihapus: {:?}", removed_stock);

    let removed_again = product_stock.remove("Gula");
    println!("Mencoba hapus Gula lagi: {:?}", removed_again);

    println!("Produk tersisa: {}", product_stock.len());
}

Output yang Dihasilkan:

text
Stok Gula yang dihapus: Some(60)
Mencoba hapus Gula lagi: None
Produk tersisa: 1

Alur kepemilikannya layak dicermati: remove("Gula") yang pertama memindahkan ownership value 60 keluar dari map ke dalam Option — map kehilangan satu entry, variabel removed_stock menjadi pemilik nilainya. Penghapusan kedua menemukan key yang sudah tidak ada, sehingga jawaban None dan isi map tidak berubah. Nilai kembalian berbentuk Option<V> membuat remove aman dipanggil tanpa pemeriksaan keberadaan terlebih dahulu.

Jebakan: insert di Dalam Iterasi #

Sekarang jebakan yang dijanjikan. Kita ingin menaikkan semua stok di bawah 50 menjadi 50 — logikanya sederhana, tetapi menaruh insert di dalam loop yang sedang meminjam map mengundang penolakan:

rust
use std::collections::HashMap;

fn main() {
    let mut product_stock = HashMap::new();
    product_stock.insert(String::from("Kopi"), 25);
    product_stock.insert(String::from("Teh"), 40);

    for (name, stock) in &product_stock {
        if *stock < 50 {
            product_stock.insert(name.clone(), 50);
        }
    }
}

Pesan error kompilator:

text
error[E0502]: cannot borrow `product_stock` as mutable because it is also borrowed as immutable
  --> main.rs:10:13
   |
 8 |       for (name, stock) in &product_stock {
   |       -                    --------------
   |       |                    |
   |       |                    immutable borrow occurs here
   |  _____|                    immutable borrow later used here
   | |
 9 | |         if *stock < 50 {
10 | |             product_stock.insert(name.clone(), 50);
   | |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
11 | |         }
12 | |     }
   | |_____- this for loop borrows `product_stock` immutably, preventing mutation within its body
   |
   = help: consider using an index-based loop instead, or collecting modifications into a separate collection

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0502`.

Iterator loop memegang pinjaman immutable atas product_stock selama seluruh iterasi berlangsung, sementara insert menuntut pinjaman mutable — dua permintaan yang menabrak aturan borrowing. Penolakan ini melindungi dari bahaya nyata: insert bisa memicu pertumbuhan tabel internal map yang memindahkan seluruh entry, sehingga iterator yang sedang berjalan akan tersesat menunjuk memori lama. Pesan compiler bahkan menyarankan dua arah perbaikan yang memang tepat: kumpulkan dulu daftar key yang perlu diubah ke dalam Vec, lalu jalankan insert pada loop kedua setelah pinjaman iterasi selesai — atau gunakan pembaruan lewat pinjaman mutable saja, sebagaimana get_mut di atas, yang mengubah value tanpa menyentuh struktur tabel.

Ringkasan #

Iterasi HashMap dengan for (key, value) in &map berjalan dalam urutan acak yang berubah antar eksekusi — urutan stabil diperoleh dengan mengumpulkan hasil ke Vec lalu sort. Pembaruan value memakai get_mut yang mengembalikan Option<&mut V>, dengan cabang None sebagai tempat menangani key yang belum ada, dan penghapusan entry memakai remove yang mengembalikan Option<V>. Upaya memanggil insert di dalam iterasi ditolak dengan E0502 karena iterator memegang pinjaman yang tidak boleh hidup bersama mutasi. Koleksi berikutnya menuntut kebutuhan yang berbeda sama sekali: menolak duplikasi dan menghitung keunikan — HashSet.