Nyimpen Keys (Kunci) dengan Nilai Terkait di Hash Maps
Koleksi umum kita yang terakhir adalah hash map. Tipe HashMap<K, V> nyimpen
pemetaan dari keys (kunci) bertipe K ke nilai bertipe V pake sebuah fungsi
hashing, yang nentuin gimana cara naruh keys sama nilai-nilai ini ke dalem
memori. Banyak bahasa pemrograman support struktur data jenis ini, tapi mereka
sering pake nama yang beda-beda, kayak hash, map, object, hash table,
dictionary, atau associative array, buat nyebut beberapa di antaranya.
Hash maps sangat berguna pas kita mau nyari data bukan pake indeks, kayak yang kita lakuin pake vectors, tapi pake key yang tipenya bisa apa aja. Misalnya, di dalem game, kita bisa nyatet skor tiap tim di dalem hash map di mana tiap key-nya adalah nama timnya dan nilainya adalah skor tiap tim. Kalo kita punya nama timnya, kita bisa ngambil skornya.
Kita bakal ngebahas API dasar dari hash maps di bagian ini, tapi masih banyak
lagi fungsionalitas keren yang ngumpet di fungsi-fungsi yang didefinisikan pada
HashMap<K, V> sama standard library. Kayak biasa, cek dokumentasi standard
library buat info lebih lanjut.
Bikin Hash Map Baru
Salah satu cara buat bikin hash map kosong adalah pake new terus nambahin
elemennya pake insert. Di Listing 8-20, kita lagi nyatet skor dari dua tim
yang namanya Blue sama Yellow. Tim Blue mulai dengan 10 poin, dan tim Yellow
mulai dengan 50 poin.
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}
Perhatiin ya kalau kita pertama-tama harus bawa HashMap dari porsi collections
di standard library ke dalem scope pake use. Dari ketiga koleksi umum kita,
yang satu ini paling jarang dipake, jadi dia nggak dimasukin ke fitur-fitur yang
otomatis dibawa ke dalem scope lewat prelude. Hash maps juga dapet lebih
dikit support dari standard library; nggak ada macro bawaan buat
ngonstruksi mereka, misalnya.
Sama kayak vectors, hash maps nyimpen datanya di heap. HashMap ini punya
keys tipe String sama nilai tipe i32. Sama juga kayak vectors, hash maps
itu homogen: semua keys harus punya tipe yang sama, dan semua nilai harus
punya tipe yang sama.
Akses Nilai di dalem Hash Map
Kita bisa ngambil nilai dari hash map dengan masukin key-nya ke method get,
kayak yang ditunjukin di Listing 8-21.
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
}
Di sini, score bakal punya nilai yang terkait sama tim Blue, dan hasilnya
bakal 10. Method get balikin sebuah Option<&V>; kalau nggak ada nilai
buat key itu di hash map-nya, get bakal balikin None. Program ini nanganin
Option-nya pake manggil copied buat dapet Option<i32> bukannya
Option<&i32>, terus pake unwrap_or buat nge-set score jadi nol kalau
scores nggak punya entri buat key tersebut.
Kita bisa iterasi lewat tiap pasangan key-value (kunci-nilai) di hash map pake
cara yang mirip kayak di vectors, pake for loop:
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{key}: {value}");
}
}
Kode ini bakal nyetak tiap pasangan dengan urutan yang sembarangan (arbitrary):
Yellow: 50
Blue: 10
Hash Maps dan Ownership
Buat tipe-tipe yang mengimplementasikan trait Copy, kayak i32, nilainya
di-copy ke dalem hash map. Buat nilai yang dimiliki (owned values) kayak
String, nilainya bakal di-move dan hash map bakal jadi pemilik (owner)
dari nilai-nilai itu, kayak yang didemonstrasiin di Listing 8-22.
fn main() {
use std::collections::HashMap;
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name and field_value are invalid at this point, try using them and
// see what compiler error you get!
}
Kita nggak bisa pake variabel field_name sama field_value setelah mereka di-
move ke dalem hash map pake pemanggilan insert.
Kalau kita masukin referensi ke nilai ke dalem hash map, nilainya nggak bakal di-move ke dalem hash map-nya. Nilai yang ditunjuk sama referensi itu harus tetep valid setidaknya selama hash map-nya valid. Kita bakal bahas masalah ini lebih banyak di “Mervalidasi Referensi pake Lifetimes” di Bab 10.
Ngubah Hash Map
Walaupun jumlah pasangan key sama value bisa nambah, tiap unique key cuma bisa
punya satu nilai yang terkait dengannya dalam satu waktu (tapi nggak berlaku
sebaliknya: misalnya, baik tim Blue maupun tim Yellow bisa punya nilai 10
yang disimpan di hash map scores).
Pas kita mau ngubah data di dalem hash map, kita harus mutusin gimana cara nanganin kasus pas sebuah key udah punya nilai yang di-assign ke dia. Kita bisa nggantiin nilai lama pake nilai baru, sama sekali nyuekin nilai yang lama. Kita bisa pertahanin nilai lama dan nyuekin nilai baru, dan cuma nambahin nilai baru kalau key itu belum punya nilai. Atau kita bisa ngegabungin nilai lama sama nilai baru. Yuk kita liat gimana cara ngelakuin semua hal ini!
Nindih Nilai (Overwriting a Value)
Kalau kita nge-insert sebuah key sama nilai ke hash map terus nge-insert
key yang sama pake nilai yang beda, nilai yang terkait sama key itu bakal
diganti. Walaupun kode di Listing 8-23 manggil insert dua kali, hash map-nya
cuma bakal punya satu pasangan key-value karena kita nge-insert nilai buat
key tim Blue dua kali.
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{scores:?}");
}
Kode ini bakal nyetak {"Blue": 25}. Nilai asli 10 udah ditindih.
Nambahin Key dan Nilai Cuma Kalau Key-nya Belum Ada
Sering sekali kita mau nge-cek apakah sebuah key tertentu udah ada di hash map dengan suatu nilai terus ngambil tindakan berikut: kalau key itu emang udah ada di hash map, nilai yang udah ada biarin aja kayak gitu; kalau key-nya belum ada, insert key itu bareng nilainya.
Hash maps punya API khusus buat ini namanya entry yang nerima key yang mau
kita cek sebagai parameternya. Nilai return dari method entry ini adalah enum
namanya Entry yang ngewakilin nilai yang mungkin udah ada atau mungkin belum.
Katakanlah kita mau nge-cek apakah key buat tim Yellow punya nilai yang terkait
dengannya. Kalau belum ada, kita mau nge-insert nilai 50, dan lakuin hal yang
sama buat tim Blue. Pake API entry, kodenya keliatan kayak Listing 8-24.
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{scores:?}");
}
entry buat nge-insert cuma kalau key-nya belum punya nilaiMethod or_insert pada Entry didefinisikan buat balikin sebuah mutable
reference ke nilai buat key Entry yang terkait kalau key itu ada, dan kalau
nggak ada, dia nge-insert parameternya sebagai nilai baru buat key ini terus
balikin mutable reference ke nilai yang baru. Teknik ini jauh lebih bersih
daripada nulis logikanya sendiri dan, selain itu, main lebih akur sama borrow
checker.
Jalanin kode di Listing 8-24 bakal nyetak {"Yellow": 50, "Blue": 10}.
Pemanggilan entry yang pertama bakal nge-insert key buat tim Yellow dengan
nilai 50 karena tim Yellow belum punya nilai. Pemanggilan entry yang kedua
nggak bakal ngubah hash map-nya karena tim Blue udah punya nilai 10.
Ngubah Nilai Berdasarkan Nilai Lamanya
Skenario umum lainnya buat hash maps adalah nyari nilai dari sebuah key terus
ngubah nilainya berdasarkan nilai yang lama. Misalnya, Listing 8-25 nunjukin
kode yang ngitung berapa kali tiap kata muncul di sebuah teks. Kita pake hash
map dengan kata-katanya sebagai keys dan nambahin (increment) nilainya buat
nyatet berapa kali kita ngeliat kata itu. Kalau ini pertama kalinya kita liat
kata itu, kita bakal nge-insert nilai 0 dulu.
fn main() {
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{map:?}");
}
Kode ini bakal nyetak {"world": 2, "hello": 1, "wonderful": 1}. Kita mungkin
bakal ngeliat pasangan key-value yang sama dicetak pake urutan yang beda: inget
kan di “Akses Nilai di dalem Hash Map” kalau iterasi lewat hash map
itu terjadi dengan urutan yang sembarangan (arbitrary).
Method split_whitespace balikin iterator yang ngelewatin subslices,
dipisahin sama spasi (whitespace), dari nilai di text. Method or_insert
balikin sebuah mutable reference (&mut V) ke nilai buat key yang spesifik
itu. Di sini, kita nyimpen mutable reference itu di variabel count, jadi
buat nge-assign (ngasih nilai) ke nilai itu, kita pertama-tama harus pake dereference
pada count pake tanda bintang (*). Mutable reference-nya keluar dari
scope di akhir dari for loop, jadi semua perubahan ini aman dan dibolehin sama
aturan borrowing.
Fungsi Hashing
Secara default, HashMap pake fungsi hashing namanya SipHash yang bisa ngasih
ketahanan dari serangan denial-of-service (DoS) yang ngelibatin hash
tables1. Ini bukan algoritma hashing paling cepet yang ada, tapi
trade-off buat keamanan yang lebih baik dengan sedikit penurunan performa
itu sepadan sekali. Kalau kita nge-profile kode kita dan ngerasa kalau
fungsi hash default-nya terlalu lambat buat tujuan kita, kita bisa ganti ke
fungsi lain dengan nentuin hasher yang beda. Sebuah hasher adalah tipe yang
mengimplementasikan trait BuildHasher. Kita bakal bahas traits dan cara
mengimplementasikan mereka di Bab 10. Kita nggak perlu harus
mengimplementasikan hasher kita sendiri dari nol kok;
crates.io punya library-library yang di-share sama user
Rust lainnya yang nyediain hashers yang mengimplementasikan banyak algoritma
hashing umum.
Ringkasan
Vectors, strings, sama hash maps bakal nyediain banyak fungsionalitas yang dibutuhin di program kita pas kita perlu nyimpen, akses, dan ngubah data. Ini ada beberapa latihan yang sekarang harusnya udah bisa kita selesein:
- Dikasih sekumpulan integer, pake sebuah vector dan balikin median (pas diurutin, nilai di posisi tengah) sama modus (nilai yang paling sering muncul; hash map bakal ngebantu sekali di sini) dari sekumpulan nilai itu.
- Convert strings ke pig latin. Konsonan pertama dari tiap kata dipindah ke akhir kata terus ditambahin ay, jadi first jadi irst-fay. Kata-kata yang diawali sama huruf vokal ditambahin hay di akhirnya (apple jadi apple-hay). Inget detail soal encoding UTF-8 ya!
- Pake hash map sama vectors, bikin interface teks buat ngebolehin user nambahin nama pegawai ke sebuah departemen di sebuah perusahaan; misalnya, “Tambahin Sally ke Engineering” atau “Tambahin Amir ke Sales.” Terus bolehin user buat narik (retrieve) daftar semua orang di suatu departemen atau semua orang di perusahaan berdasarkan departemen, diurutin secara alfabet.
Dokumentasi API standard library ngejelasin method-method yang dipunyai sama vectors, strings, sama hash maps yang bakal ngebantu sekali buat latihan- latihan ini!
Kita lagi masuk ke program-program yang lebih kompleks di mana operasi bisa aja gagal, jadi ini waktu yang paling pas buat bahas penanganan error (error handling). Kita bakal ngelakuin itu berikutnya!