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:
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:
Jumlah tim yang tercatat: 3
Skor Garuda : Some(3)
Skor Merpati: NoneMari kita bedah kode di atas baris per baris:
- Baris 1 (
use std::collections::HashMap;): membawa namaHashMapke dalam jangkauan file. Tanpa baris ini, kita harus menulisstd::collections::HashMap::new()secara penuh setiap kali memakainya. - Baris 4 (
let mut match_scores = HashMap::new();): membuat map kosong bertipeHashMap<String, i32>— tipe keyStringdan tipe valuei32disimpulkan compiler dari pasangan pertama yang di-insert. Kata kuncimutwajib karenainsertmengubah isi map. - Baris 6-8 (
match_scores.insert(...)): setiapinsertmemasukkan satu pasangan key-value. Perhatikan bahwa key dibuat langsung denganString::from(...)di dalam argumen — kepemilikan key dan value berpindah ke map, hal yang kita bedah di bagian akhir chapter. - Baris 10 (
match_scores.len()): methodlenmenghitung jumlah pasangan yang tersimpan, yaitu3. - Baris 11-12 (
match_scores.get(...)): methodgetmencari value berdasarkan key dan mengembalikanOption<&i32>—Some(3)untuk key yang ada,Noneuntuk key"Merpati"yang tidak pernah dimasukkan. Pencarian dengan"Garuda"bertipe&strtetap sah meskipun key tersimpan sebagaiString, 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:
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:
Rajawali mencetak 1 gol
Merpati tidak ikut pertandingan iniMari kita telaah dua bentuk penanganannya:
- Baris 7-10 (
match match_scores.get("Rajawali")):matchmemaksa kita menutup dua cabang sekaligus. PatternSome(score)mengikat reference&i32untuk dicetak, dan cabangNonemenjawab skenario key yang belum tercatat. - Baris 12-16 (
if let Some(score) = ...):if letadalah bentuk ringkas ketika hanya satu cabang yang berarti. Karena"Merpati"memang tidak ada di map, blokelseyang 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:
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:
Skor Garuda: Some(3)
Skor Garuda setelah update: Some(5)
Jumlah tim: 1Dua 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:
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:
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.