Data di dunia nyata sering berbentuk pasangan: setiap tim punya skor, setiap produk punya harga, dan setiap siswa punya nilai. Vector yang kita kuasai sejauh ini menyimpan deretan nilai dengan posisi berindex — angka 0, 1, 2 — tetapi tidak bisa menjawab pertanyaan “berapa skor tim Garuda?” tanpa kita mencari posisinya terlebih dahulu. Rust menyediakan HashMap<K, V> untuk menyimpan pasangan key dan value, di mana key menjadi alamat langsung menuju value-nya: berikan "Garuda", terima 3. Chapter ini memperkenalkan pembuatan HashMap, pengambilan value dengan get, dan mekanisme hash yang bekerja di balik layar — termasuk satu sifat penting yang membedakan HashMap dari koleksi lain: urutan isinya tidak pernah dijamin.

Membuat dan Mengisi HashMap #

Berbeda dari Vec dan String yang tersedia langsung, HashMap tinggal di modul std::collections sehingga perlu satu baris use di bagian atas file. Setelah pintu terbuka, kita isi papan skor pertandingan bola:

rust
use std::collections::HashMap;

fn main() {
    let mut match_scores = HashMap::new();

    match_scores.insert(String::from("Garuda"), 3);
    match_scores.insert(String::from("Rajawali"), 1);
    match_scores.insert(String::from("Elang"), 2);

    println!("Jumlah tim yang tercatat: {}", match_scores.len());
    println!("Skor Garuda : {:?}", match_scores.get("Garuda"));
    println!("Skor Merpati: {:?}", match_scores.get("Merpati"));
}

Output yang Dihasilkan:

text
Jumlah tim yang tercatat: 3
Skor Garuda : Some(3)
Skor Merpati: None

Mari kita bedah kode di atas baris per baris:

  1. Baris 1 (use std::collections::HashMap;): membawa nama HashMap ke dalam jangkauan file. Tanpa baris ini, kita harus menulis std::collections::HashMap::new() secara penuh setiap kali memakainya.
  2. Baris 4 (let mut match_scores = HashMap::new();): membuat map kosong bertipe HashMap<String, i32> — tipe key String dan tipe value i32 disimpulkan compiler dari pasangan pertama yang di-insert. Kata kunci mut wajib karena insert mengubah isi map.
  3. Baris 6-8 (match_scores.insert(...)): setiap insert memasukkan satu pasangan key-value. Perhatikan bahwa key dibuat langsung dengan String::from(...) di dalam argumen — kepemilikan key dan value berpindah ke map, hal yang kita bedah di bagian akhir chapter.
  4. Baris 10 (match_scores.len()): method len menghitung jumlah pasangan yang tersimpan, yaitu 3.
  5. Baris 11-12 (match_scores.get(...)): method get mencari value berdasarkan key dan mengembalikan Option<&i32> — Some(3) untuk key yang ada, None untuk key "Merpati" yang tidak pernah dimasukkan. Pencarian dengan "Garuda" bertipe &str tetap sah meskipun key tersimpan sebagai String, karena Rust mengizinkan pencarian memakai bentuk pinjaman dari tipe key.

Mekanisme Hash di Balik Layar #

Bagaimana mungkin "Garuda" bisa jadi alamat? Setiap insert menjalankan tiga langkah. Pertama, hash function mengolah isi key menjadi angka hash — SipHash, algoritma bawaan Rust, memeras teks "Garuda" menjadi angka dengan distribusi yang merata. Kedua, angka hash tersebut dipetakan ke salah satu bucket atau slot penyimpanan di dalam tabel internal map. Ketiga, pasangan key-value disimpan ke bucket tersebut; jika bucket sudah dihuni key lain (collision), map menyusun kedua pasangan dalam satu deret dan membandingkan kesetaraan key saat pencarian. Berkat mekanisme ini, get selesai dalam waktu rata-rata konstan tanpa perlu memeriksa seluruh isi map.

Dua konsekuensi mengikuti mekanisme tersebut. Pertama, tipe key harus mendukung perhitungan hash dan perbandingan kesetaraan — semua tipe bawaan seperti String, integer, dan tuple dari tipe-tipe tersebut sudah memenuhinya. Kedua, posisi key di dalam tabel ditentukan oleh angka hash, dan Rust sengaja memberi setiap proses program biji acak (hash seed) yang berbeda. Akibatnya urutan elemen saat iterasi berbeda-beda antar eksekusi program — bahkan antar komputer. Kita akan menangani urutan ini secara saksama di chapter Iterasi dan Update.

Mengambil Nilai dengan get #

Hasil get berupa Option<&V> mengikuti pola yang sama dengan Vec::get: nilai hadir dalam Some, ketidakhadiran dinyatakan None. Pola match dan if let yang kita kuasai di section Pattern Matching bekerja mulus di sini:

rust
use std::collections::HashMap;

fn main() {
    let mut match_scores = HashMap::new();
    match_scores.insert(String::from("Rajawali"), 1);

    match match_scores.get("Rajawali") {
        Some(score) => println!("Rajawali mencetak {} gol", score),
        None => println!("Rajawali belum tercatat"),
    }

    if let Some(score) = match_scores.get("Merpati") {
        println!("Merpati mencetak {} gol", score);
    } else {
        println!("Merpati tidak ikut pertandingan ini");
    }
}

Output yang Dihasilkan:

text
Rajawali mencetak 1 gol
Merpati tidak ikut pertandingan ini

Mari kita telaah dua bentuk penanganannya:

  1. Baris 7-10 (match match_scores.get("Rajawali")): match memaksa kita menutup dua cabang sekaligus. Pattern Some(score) mengikat reference &i32 untuk dicetak, dan cabang None menjawab skenario key yang belum tercatat.
  2. Baris 12-16 (if let Some(score) = ...): if let adalah bentuk ringkas ketika hanya satu cabang yang berarti. Karena "Merpati" memang tidak ada di map, blok else yang berjalan dan program tetap terkendali tanpa panic.

Method pendamping yang sejenis adalah contains_key, yang mengembalikan bool tanpa mengambil value — berguna saat kita hanya perlu tahu kehadiran key. Namun untuk mengambil lalu memakai value, get dalam satu langkah lebih efisien daripada contains_key diikuti get, karena pencarian cukup dilakukan sekali.

Memperbarui Nilai dengan insert Ulang #

Pertandingan berlanjut dan Garuda mencetak gol tambahan. Memasukkan key yang sudah ada di map tidak menambah baris baru — operasi insert mengganti value lama dengan value baru:

rust
use std::collections::HashMap;

fn main() {
    let mut match_scores = HashMap::new();

    match_scores.insert(String::from("Garuda"), 3);
    println!("Skor Garuda: {:?}", match_scores.get("Garuda"));

    match_scores.insert(String::from("Garuda"), 5);
    println!("Skor Garuda setelah update: {:?}", match_scores.get("Garuda"));
    println!("Jumlah tim: {}", match_scores.len());
}

Output yang Dihasilkan:

text
Skor Garuda: Some(3)
Skor Garuda setelah update: Some(5)
Jumlah tim: 1

Dua bukti tercetak jelas: skor Garuda berubah dari 3 menjadi 5, sementara jumlah tim tetap 1 — key "Garuda" tidak terduplikasi, hanya value-nya yang ditimpa. Sebenarnya insert juga mengembalikan nilai lama dalam Option<V> (Some berisi value yang diganti, None jika key baru), detail yang kadang berguna ketika kita ingin tahu nilai sebelumnya. Ada satu kebutuhan yang tidak dijawab pola ini: bagaimana jika kita ingin menambah skor yang ada tanpa tahu nilainya saat ini? Pola “kalau ada tambah, kalau tidak buat baru” memiliki jawaban khusus bernama Entry API — pokok bahasan chapter berikutnya setelah satu persoalan ownership diselesaikan.

Key dan Value Berpindah ke HashMap #

insert bekerja seperti push: kepemilikan key dan value berpindah ke map. Key yang dikirim dari variabel akan hangus setelah masuk:

rust
use std::collections::HashMap;

fn main() {
    let mut match_scores = HashMap::new();

    let team_name = String::from("Garuda");
    match_scores.insert(team_name, 3);

    println!("Nama tim: {}", team_name);
}

Pesan error kompilator:

text
error[E0382]: borrow of moved value: `team_name`
 --> main.rs:9:30
  |
6 |     let team_name = String::from("Garuda");
  |         --------- move occurs because `team_name` has type `String`, which does not implement the `Copy` trait
7 |     match_scores.insert(team_name, 3);
  |                         --------- value moved here
8 |
9 |     println!("Nama tim: {}", team_name);
  |                              ^^^^^^^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
7 |     match_scores.insert(team_name.clone(), 3);
  |                                  ++++++++

error: aborting due to 1 previous error

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

Pesan E0382 memetakan alurnya dengan teliti: team_name kehilangan kepemilikan di baris 7 ketika insert menerima key tersebut, lalu peminjaman di baris 9 gagal karena pemiliknya sudah berganti. Solusinya sekeluarga dengan kasus vector: gunakan team_name.clone() jika kedua salinan dibutuhkan, atau cukup biarkan variabel lama pensiun dan akses key lewat map. Sebaliknya, get("Garuda") aman dari masalah ini karena pencarian hanya meminjam key pustaka "Garuda" tanpa memindahkannya.

Ringkasan #

HashMap menyimpan pasangan key-value dengan pencarian waktu konstan: insert menaruh pasangan (key yang sudah ada diganti nilainya), get mengembalikan Option<&V>, dan len menghitung jumlah pasangan. Di balik layar, hash function memetakan key ke bucket, sementara biji acak per proses membuat urutan iterasi tidak bisa diandalkan — sifat yang harus selalu kita ingat saat menelusuri isi map. Kepemilikan key dan value berpindah ke map lewat insert, sehingga variabel sumber tidak bisa dipakai lagi. Kini kita siap menghadapi pola pembaruan yang paling sering muncul di dunia data: tambah nilai yang mungkin belum ada, dengan Entry API.