Rust'ta HashMap<K, V>, her anahtarı (K) bir değerle (V) eşleştiren ve değeri anahtarla hızlıca bulmanızı sağlayan bir sözlük yapısıdır. use std::collections::HashMap; satırıyla içeri alınır, insert ile doldurulur, get ile okunur ve var olan bir değeri koşullu güncellemek için entry API'si kullanılır. Bu yazı bu dört işlemi, sahiplik kurallarını ve kendi tiplerinizi anahtar olarak kullanmayı çalışan örneklerle anlatıyor.

Vektörlerde değere sayı indeksiyle ulaşırsınız; hash map'te ise anahtar herhangi bir tipte olabilir: takım adı, kullanıcı kimliği, koordinat. Diğer dillerde aynı yapıya sözlük, nesne, ilişkisel dizi ya da hash tablosu dendiğini görebilirsiniz. Serinin tamamı için Rust öğrenme rehberi sayfasına bakın.

HashMap oluşturma ve değer okuma

HashMap oluşturma ve değer okuma

Üç temel koleksiyon arasında en az kullanılanı olduğu için HashMap otomatik olarak kapsama alınan prelude'da yer almaz; önce use ile içeri almanız gerekir. Kitap ayrıca vektörlerdeki vec! gibi yerleşik bir oluşturma makrosu olmadığını belirtir. Vektör gibi hash map de verisini yığında (heap) tutar ve homojendir: bütün anahtarlar aynı tipte, bütün değerler aynı tipte olmalıdır.

rust
use std::collections::HashMap;

fn main() {
    let mut puanlar = HashMap::new();
    puanlar.insert(String::from("Mavi"), 10);
    puanlar.insert(String::from("Sarı"), 50);

    let takim = String::from("Mavi");
    let puan = puanlar.get(&takim).copied().unwrap_or(0);
    println!("Mavi: {puan}");
    println!("Yeşil: {}", puanlar.get("Yeşil").copied().unwrap_or(0));
    println!("eleman sayısı: {}", puanlar.len());

    // Yineleme sırası belirsizdir; kararlı çıktı için sıralayın
    let mut ciftler: Vec<(&String, &i32)> = puanlar.iter().collect();
    ciftler.sort();
    for (ad, p) in ciftler {
        println!("{ad}: {p}");
    }
}
// çıktı: Mavi: 10
// çıktı: Yeşil: 0
// çıktı: eleman sayısı: 2
// çıktı: Mavi: 10
// çıktı: Sarı: 50

get, değere bir referans içeren Option<&V> döndürür; anahtar yoksa None verir. Örnekteki .copied() çağrısı Option<&i32>'yi Option<i32>'ye çevirir, unwrap_or(0) ise anahtar yoksa sıfır kullanır. Dikkat ederseniz anahtarlar String olduğu hâlde get("Yeşil") ile bir &str verebildik. Belge bunun nedenini açıklar: get, anahtar tipinin ödünç alınmış her biçimini (Borrow trait'i üzerinden) kabul eder; tek şart, o biçimin Hash ve Eq davranışının anahtar tipininkiyle aynı olmasıdır. Böylece arama yapmak için yeni bir String oluşturmanız gerekmez.

Önemli bir ayrıntı: hash map'i for ile gezdiğinizde çiftler belirsiz bir sırayla gelir. Kitaptaki örnek de aynı kodun farklı çalıştırmalarda farklı sırada yazdırabileceğini söyler. Çıktınızın sabit olması gerekiyorsa yukarıdaki gibi çiftleri bir vektöre toplayıp sıralayın ya da anahtar sırasını kendisi koruyan BTreeMap kullanın. Sıralamanın Türkçe alfabeye göre değil karakterlerin değerlerine göre yapıldığını da unutmayın; aşağıdaki bir örnekte "kahve" anahtarı "çay"dan önce geliyor.

Sahiplik: insert değerleri içeri taşır

i32 gibi Copy uygulayan tipler hash map'e kopyalanır. String gibi sahipli değerler ise içeri taşınır ve artık hash map'in malı olur. Taşındıktan sonra eski değişkeni kullanmaya çalışmak derleme hatasıdır:

rust
use std::collections::HashMap;

fn main() {
    let alan = String::from("Favori renk");
    let deger = String::from("Mavi");
    let mut harita = HashMap::new();
    harita.insert(alan, deger);
    println!("{alan}");
}
// error[E0382]: borrow of moved value: `alan`

Değişkeni sonra da kullanmak istiyorsanız harita.insert(alan.clone(), deger) ile bir kopya verebilir ya da kullanımı insert'ten önceye alabilirsiniz. Hash map'e değer yerine referans (HashMap<&str, &str> gibi) da koyabilirsiniz; bu durumda değerler taşınmaz, ama referansların gösterdiği verinin en az hash map kadar yaşaması gerekir. Bu kuralın ayrıntısı Rust lifetime yazısında, taşıma hatalarının çözümü ise E0382 hatası yazısında.

Güncelleme: üzerine yazmak, yoksa eklemek, eskisine göre değiştirmek

Her anahtarın aynı anda yalnızca bir değeri olabilir. Bu yüzden var olan bir anahtara değer yazarken ne olacağına karar vermeniz gerekir: eskisini tamamen değiştirmek, eskisini korumak ve yalnızca anahtar yoksa eklemek ya da eski değere göre yeni değer hesaplamak. Aşağıdaki örnek ilk ikisini ve birkaç yardımcı metodu gösteriyor:

rust
use std::collections::HashMap;

fn main() {
    let mut stok = HashMap::new();
    println!("{:?}", stok.insert("kalem", 10));
    println!("{:?}", stok.insert("kalem", 25));
    println!("{stok:?}");

    stok.entry("defter").or_insert(5);
    stok.entry("kalem").or_insert(99);
    println!("kalem: {}, defter: {}", stok["kalem"], stok["defter"]);

    if let Some(adet) = stok.get_mut("defter") {
        *adet -= 2;
    }
    println!("defter: {}", stok["defter"]);
    println!("silinen: {:?}", stok.remove("kalem"));
    println!("tekrar silme: {:?}", stok.remove("kalem"));
    println!("contains_key: {}", stok.contains_key("defter"));
}
// çıktı: None
// çıktı: Some(10)
// çıktı: {"kalem": 25}
// çıktı: kalem: 25, defter: 5
// çıktı: defter: 3
// çıktı: silinen: Some(25)
// çıktı: tekrar silme: None
// çıktı: contains_key: true

insert anahtar yoksa None, varsa eski değeri Some içinde döndürür ve değeri yenisiyle değiştirir. Belgenin vurguladığı ince bir nokta: değer güncellenir ama anahtarın kendisi güncellenmez. Eşit sayılan ama birebir aynı olmayan anahtarlar kullanıyorsanız (örneğin yalnızca bir alana göre karşılaştırılan bir struct) bu fark önem kazanır. entry(k).or_insert(v) ise yalnızca anahtar yoksa ekler; "kalem" zaten olduğu için 99 yazılmadı. get_mut değere değiştirilebilir bir referans verir, remove değeri çıkarıp Option içinde döndürür.

Entry API ile sayma ve gruplama

Entry API ile sayma ve gruplama

Hash map'lerin en yaygın kullanımlarından biri, bir şeyin kaç kez geçtiğini saymaktır. entry(anahtar), anahtarın dolu (Occupied) ya da boş (Vacant) olduğunu gösteren bir Entry enum'u döndürür. or_insert bu girişte değer yoksa varsayılanı ekler ve her iki durumda da değere &mut V döndürür. Yıldız operatörüyle bu referans üzerinden değeri artırırsınız:

rust
use std::collections::HashMap;

fn main() {
    let metin = "bir berber bir berbere gel beraber bir berber dükkanı açalım demiş";
    let mut sayac: HashMap<&str, u32> = HashMap::new();
    for kelime in metin.split_whitespace() {
        *sayac.entry(kelime).or_insert(0) += 1;
    }
    println!("bir: {}, berber: {}, demiş: {}", sayac["bir"], sayac["berber"], sayac["demiş"]);

    let mut harf: HashMap<char, usize> = HashMap::new();
    for c in "ağaç".chars() {
        harf.entry(c).and_modify(|n| *n += 1).or_insert(1);
    }
    println!("a: {}, ğ: {}, ç: {}", harf[&'a'], harf[&'ğ'], harf[&'ç']);

    let mut gruplar: HashMap<bool, Vec<i32>> = HashMap::new();
    for n in 1..=6 {
        gruplar.entry(n % 2 == 0).or_default().push(n);
    }
    println!("çift: {:?}, tek: {:?}", gruplar[&true], gruplar[&false]);

    let mut uzunluk: HashMap<&str, usize> = HashMap::new();
    let u = uzunluk.entry("çiçek").or_insert_with_key(|k| k.chars().count());
    println!("çiçek: {u}");
}
// çıktı: bir: 3, berber: 2, demiş: 1
// çıktı: a: 2, ğ: 1, ç: 1
// çıktı: çift: [2, 4, 6], tek: [1, 3, 5]
// çıktı: çiçek: 5

Entry API'nin birkaç biçimi vardır ve hangisini seçeceğiniz varsayılan değerin nasıl üretileceğine bağlıdır. or_insert(v) değeri her durumda önceden hesaplar; or_insert_with(f) fonksiyonu yalnızca anahtar yoksa çağırır, bu da varsayılanın pahalı olduğu durumlarda işe yarar. or_insert_with_key fonksiyona anahtarın referansını da verir, böylece anahtarı kopyalamadan ondan değer türetebilirsiniz. or_default değer tipinin Default değerini (sayılar için 0, vektör için boş vektör) kullanır; gruplama örneğinde her anahtar için boş bir vektör açıp ona eleman ekledik. and_modify ise yalnızca giriş doluysa çalışır ve ardından gelen or_insert ile birlikte "varsa artır, yoksa 1 yaz" mantığını tek satırda kurar.

Neden contains_key ile kontrol edip sonra insert yapmak yerine entry kullanmalısınız? std::collections belgesi bunu açıklar: önce arayıp sonra eklemek aynı aramayı iki kez yapmak demektir. Entry API anahtarı bir kez arar ve sonucu size verir. Kitap ayrıca bu yöntemin borrow checker ile daha uyumlu olduğunu belirtir, çünkü aynı anda hem okuyan hem yazan iki ayrı ödünç alma oluşturmazsınız.

rust
use std::collections::HashMap;

fn main() {
    let mut sayac: HashMap<String, u32> = HashMap::new();
    let kelime = "elma";
    // Kötü: iki arama yapar
    if sayac.contains_key(kelime) {
        *sayac.get_mut(kelime).unwrap() += 1;
    } else {
        sayac.insert(kelime.to_string(), 1);
    }
    // İyi: tek arama
    *sayac.entry(kelime.to_string()).or_insert(0) += 1;
    println!("{sayac:?}");
}
// çıktı: {"elma": 2}

Bir not: entry anahtarı sahipliğiyle alır (entry(key: K)). Anahtar tipi String ise anahtar zaten varken bile to_string() ile yeni bir String oluşturmanız gerekir. Çok sık güncellenen ve çoğunlukla var olan anahtarlarda bu ek ayırmayı önlemek için önce get_mut deneyip yalnızca bulunamazsa insert yapmak bir seçenek olabilir.

Kendi tipinizi anahtar yapmak ve diğer kullanışlı yollar

Anahtar tipinin Eq ve Hash trait'lerini uygulaması zorunludur. Belgeye göre en kolay yol #[derive(PartialEq, Eq, Hash)] eklemektir. Bu trait'leri elle yazarsanız uymanız gereken kural şudur: iki anahtar eşitse hash değerleri de eşit olmalıdır. Bu kuralın ihlal edilmesi bir mantık hatasıdır; belge sonucun belirsiz olduğunu ama tanımsız davranışa (undefined behavior) yol açmayacağını, yanlış sonuçlar, panik ya da sonsuz döngü gibi etkilerin o hash map ile sınırlı kalacağını söyler. Aynı şey, map içindeyken anahtarın hash'ini ya da eşitliğini değiştiren bir değişiklik için de geçerlidir.

rust
use std::collections::HashMap;

#[derive(Debug, PartialEq, Eq, Hash)]
struct Koordinat {
    x: i32,
    y: i32,
}

fn main() {
    let mut harita = HashMap::new();
    harita.insert(Koordinat { x: 0, y: 0 }, "başlangıç");
    harita.insert(Koordinat { x: 3, y: 4 }, "hazine");
    println!("{:?}", harita.get(&Koordinat { x: 3, y: 4 }));

    let baskentler = HashMap::from([("Türkiye", "Ankara"), ("Japonya", "Tokyo")]);
    println!("{}", baskentler["Türkiye"]);

    let isimler = vec!["ali", "veli", "ayşe"];
    let uzunluklar: HashMap<&str, usize> = isimler.iter().map(|s| (*s, s.len())).collect();
    println!("ayşe bayt: {}", uzunluklar["ayşe"]);

    let mut fiyat = HashMap::from([("çay", 15), ("kahve", 60), ("su", 10)]);
    fiyat.retain(|_, v| *v >= 15);
    let mut kalan: Vec<_> = fiyat.into_iter().collect();
    kalan.sort();
    println!("{kalan:?}");
}
// çıktı: Some("hazine")
// çıktı: Ankara
// çıktı: ayşe bayt: 5
// çıktı: [("kahve", 60), ("çay", 15)]

Başlangıç değerleri belli bir hash map'i HashMap::from([...]) ile bir diziden oluşturabilirsiniz. İkili (tuple) üreten bir iterator'ı collect() ile doğrudan hash map'e toplamak da yaygın bir yoldur. retain koşulu sağlamayan çiftleri yerinde siler. Derive edilebilir trait'lerin ne yaptığı Rust derive: Debug, Clone, PartialEq yazısında, iterator ve collect ise Rust iterator yazısında anlatılıyor.

harita[&anahtar] biçimindeki indeksleme kısa ve okunurdur ama anahtar yoksa program panikler. Panik mesajı hangi anahtarın eksik olduğunu bile söylemez:

rust
use std::collections::HashMap;

fn main() {
    let m = HashMap::from([("a", 1)]);
    println!("{}", m["b"]);
}
// panik: no entry found for key

Değerleri toplu güncellemek: values_mut ve iter_mut

Bir map'teki bütün değerleri aynı anda değiştirmek için anahtarları tek tek aramanız gerekmez. values_mut() her değere değiştirilebilir bir referans verir; iter_mut() ise anahtarı değiştirilemez, değeri değiştirilebilir referans olarak çift hâlinde verir. Anahtarların değiştirilebilir olarak verilmemesi tesadüf değildir: anahtar değişirse hash'i de değişir ve map tutarsız hâle gelir. Aşağıdaki örnek bütün fiyatlara 5 ekliyor, sonra yalnızca bir ürünün fiyatını sıfırlıyor:

rust
use std::collections::HashMap;

fn main() {
    let mut fiyat: HashMap<&str, u32> = HashMap::with_capacity(3);
    fiyat.insert("çay", 10);
    fiyat.insert("simit", 15);
    fiyat.insert("ayran", 20);

    for v in fiyat.values_mut() {
        *v += 5;
    }
    let toplam: u32 = fiyat.values().sum();
    println!("toplam: {toplam}");

    let mut adlar: Vec<&&str> = fiyat.keys().collect();
    adlar.sort();
    println!("{adlar:?}");

    for (ad, f) in fiyat.iter_mut() {
        if *ad == "çay" {
            *f = 0;
        }
    }
    println!("çay: {}", fiyat["çay"]);
}
// çıktı: toplam: 60
// çıktı: ["ayran", "simit", "çay"]
// çıktı: çay: 0

Örnekte map'i HashMap::with_capacity(3) ile oluşturduk: üç eleman geleceğini bildiğimiz için büyütme ihtiyacını baştan ortadan kaldırdık. Belgeye göre bu kurucu en az o kadar eleman için yer ayırır, sıfır verilirse hiç ayırmaz. Ayrıca anahtar tipi &str olduğu için keys() bize &&str değerleri verdi; iki katlı referans ilk bakışta garip görünse de iterator'ın her zaman map'teki değere referans vermesinin doğal sonucudur.

Mini örnek: departmana göre çalışan listesi

The Rust Book bu bölümün sonunda okura birkaç alıştırma önerir. Bunlardan biri, "Ayşe'yi Muhasebe'ye ekle" gibi komutlarla çalışanları departmanlara eklemek ve sonra her departmandaki kişileri alfabetik olarak listelemektir. Aşağıdaki çözüm bu yazıda gördüğünüz parçaları bir araya getiriyor: HashMap<String, Vec<String>>, departman yoksa boş liste açan entry(...).or_default(), belirsiz sırayı düzeltmek için anahtarları sıralamak ve değerler üzerinde gezmek:

rust
use std::collections::HashMap;

fn main() {
    let komutlar = [
        "Ekle Ayşe Muhasebe",
        "Ekle Can Yazılım",
        "Ekle Ali Yazılım",
        "Ekle Deniz Muhasebe",
        "Ekle Burak Yazılım",
    ];

    let mut sirket: HashMap<String, Vec<String>> = HashMap::new();
    for komut in komutlar {
        let parcalar: Vec<&str> = komut.split_whitespace().collect();
        if let ["Ekle", kisi, departman] = parcalar[..] {
            sirket
                .entry(departman.to_string())
                .or_default()
                .push(kisi.to_string());
        }
    }

    let mut departmanlar: Vec<&String> = sirket.keys().collect();
    departmanlar.sort();
    for d in departmanlar {
        let mut kisiler = sirket[d].clone();
        kisiler.sort();
        println!("{d}: {}", kisiler.join(", "));
    }
    let toplam: usize = sirket.values().map(|v| v.len()).sum();
    println!("toplam çalışan: {toplam}");
}
// çıktı: Muhasebe: Ayşe, Deniz
// çıktı: Yazılım: Ali, Burak, Can
// çıktı: toplam çalışan: 5

Komutu parçalara ayırdıktan sonra if let ["Ekle", kisi, departman] = parcalar[..] satırı bir dilim deseniyle hem komut adını kontrol ediyor hem de diğer iki parçayı değişkenlere bağlıyor; beklenen biçimde olmayan satırlar sessizce atlanıyor. Bu desen sözdizimi Rust pattern sözdizimi yazısında anlatılıyor. sirket[d] burada güvenle kullanılabilir, çünkü d zaten map'in kendi anahtarlarından geliyor. Kişileri sıralamak için listeyi kopyaladık; map'in içindeki sırayı kalıcı olarak değiştirmek isteseydik values_mut() ile her vektörü yerinde sıralayabilirdik. Anahtar sırasıyla tutmak her zaman gerekiyorsa HashMap yerine BTreeMap kullanıp sıralama adımını tamamen kaldırabilirsiniz.

Hash fonksiyonu, güvenlik ve performans

Hash fonksiyonu, güvenlik ve performans: Ekle ya da değiştir, get(&k), Değiştirmek için oku, Yoksa ekle

Varsayılan HashMap, HashDoS adı verilen ve kötü niyetli girdilerle hash tablosunu yavaşlatmayı hedefleyen saldırılara karşı dayanıklı olacak şekilde seçilmiş bir algoritma kullanır. Belgeye göre bu algoritma şu anda SipHash 1-3'tür ama ileride haber verilmeden değişebilir; ayrıca her map rastgele bir tohumla (seed) başlatılır. Belge, bu algoritmanın orta büyüklükteki anahtarlarda oldukça rekabetçi olduğunu, tam sayılar gibi küçük anahtarlarda ya da çok uzun metinlerde başka algoritmaların daha hızlı olabileceğini, ancak onların genellikle bu saldırılara karşı koruma sağlamadığını söyler.

Profil çıkarıp hash işleminin gerçekten darboğaz olduğunu görürseniz, BuildHasher trait'ini uygulayan başka bir hasher'ı with_hasher ya da with_capacity_and_hasher ile verebilirsiniz; crates.io'da bu tür hasher'lar bulunur. Rastgele tohum nedeniyle HashMap::new normalde const ya da static başlatıcıda kullanılamaz; belge bunun için LazyLock ile sarmalamayı önerir. Kaç eleman geleceğini biliyorsanız HashMap::with_capacity gereksiz büyütmeleri önler; HashMap::new ise ilk eklemeye kadar bellek ayırmaz.

İşlemMetotDönüş
Ekle ya da değiştirinsert(k, v)Option<V> (eski değer)
Okuget(&k)Option<&V>
Değiştirmek için okuget_mut(&k)Option<&mut V>
Yoksa ekleentry(k).or_insert(v)&mut V
Varsa değiştir, yoksa ekleentry(k).and_modify(f).or_insert(v)&mut V
Silremove(&k)Option<V>
Var mıcontains_key(&k)bool
Koşullu temizleretain(f)()
Paniklemeyen okuma yerine kısa yolmap[&k]V (anahtar yoksa panik)

Sık yapılan hatalar

  • Yineleme sırasına güvenmek: Test çıktılarında ya da kullanıcıya gösterilen listelerde sıra her çalıştırmada değişebilir. Sıralı çıktı için sıralayın ya da BTreeMap kullanın.
  • map[&k] ile olmayan anahtarı okumak: Panik verir. Anahtarın varlığından emin değilseniz get kullanın.
  • Önce contains_key, sonra insert: İki arama yapar ve kodu uzatır; entry API aynı işi tek aramada yapar.
  • or_insert'e pahalı bir değer vermek: Değer anahtar varken de hesaplanır. Pahalı varsayılanlar için or_insert_with kullanın.
  • Anahtarın Hash/Eq uygulamasını tutarsız yazmak: Eşit anahtarların hash'i farklıysa map yanlış sonuç verir. Mümkünse derive kullanın.

Sık Sorulan Sorular

HashMap'i sıralı nasıl yazdırırım?

let mut v: Vec<_> = map.iter().collect(); v.sort(); ile çiftleri sıralayabilir ya da baştan BTreeMap kullanabilirsiniz. BTreeMap anahtarları her zaman sıralı tutar.

Anahtar olarak f64 kullanabilir miyim?

Anahtar tipinin Eq ve Hash uygulaması gerekir. Kayan noktalı sayılar NaN değeri yüzünden Eq uygulamaz, bu yüzden doğrudan anahtar olamaz. Değeri tam sayıya ölçekleyip saklamak gibi bir dönüşüm gerekir.

Değerleri ya da anahtarları ayrı ayrı nasıl gezerim?

keys() yalnızca anahtarları, values() yalnızca değerleri, values_mut() değerlere değiştirilebilir referansları verir. Hash map'i tüketip sahipli değerler almak için into_keys() ve into_values() vardır.

Birden çok iş parçacığından aynı HashMap'e yazabilir miyim?

Doğrudan değil; paylaşılan değiştirilebilir durum için map'i Arc<Mutex<HashMap<K, V>>> gibi bir yapıya sarmanız gerekir. Bu konu Rust Arc ve Mutex yazısında anlatılıyor.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Rust öğrenme rehberi · Vec kullanımı · String ve &str · BTreeMap, HashSet, VecDeque · Derive · Iterator