İçeriğe geç
academia.sh

Ders 10 / 12

Koleksiyonlar

Aynı 'değiştirme' işi üç ayrı koleksiyonda ölçülüyor ama iki sınıftan birine düşüyor: büyüyen dizinin eklemesi () veriyor (deyim), karma sözlüğün eklemesi eski değeri Option olarak veriyor (ifade). Sınırlayıcı ölçüm bu dönüş değeri yakalanmazsa eski verinin sessizce kaybolduğunu gösteriyor.

Yayın Güncelleme

Bu derste

  • Büyüyen dizinin ekleme yönteminin deyim, çıkarma yönteminin ifade olduğunu gösterir
  • Bir dizginin bayt sayısı ile karakter sayısının neden ayrıştığını ölçer
  • Karma sözlüğün ekleme yönteminin bir ifade olduğunu ve eski değeri döndürdüğünü açıklar
  • Yakalanmayan bir dönüş değerinin ne zaman sessiz veri kaybına yol açtığını örnekle gösterir
İçindekiler

Desen Eşleme dersi tek bir Sekil değerinin nasıl ayrıştırıldığını ölçtü; şimdiye kadarki her ders tek bir değişkene bağlanan tek bir değeri konu etti. Ama bir programın verisi neredeyse hiçbir zaman tek bir değerden ibaret değildir: birden çok alan aynı anda tutulur, büyür, küçülür. Bu ders o sınırı kaldırıyor ve aynı soruyu — bir yazımın ifade mi deyim mi olduğu — çok sayıda değeri bir arada tutan üç yapıya taşıyor: büyüyen dizi, dizgi, karma sözlük.

Bu üç yapının ortak noktası, kendilerini değiştiren yöntemler taşımasıdır: bir öğe eklemek, bir öğe çıkarmak. Bu dersin ölçtüğü soru şudur: aynı iş — “bir şey ekle” — üç ayrı yapıda üç ayrı sınıfa mı düşüyor, yoksa hepsi aynı mı davranıyor? Ortak tanımın çekirdeğinde bu soruya kısa bir yanıt zaten verilmişti: HashSet::insert hem kümeyi değiştirip hem bool döndürüyordu, Vec::push ise yalnızca () veriyordu. Bu ders o tek karşılaştırmayı üç yapıya, dört yönteme genişletiyor.

Büyüyen Dizi: Ekleme Bir Deyim, Çıkarma Bir İfadedir

Vec, Rust’ın büyüyebilen dizi tipidir; sabit uzunluklu bir dizinin aksine, eleman sayısı çalışma zamanında artıp azalabilir. İki temel değiştirici yöntemi vardır: sona bir öğe eklemek ve sondan bir öğe çıkarmak. Bu iki yöntemin dönüş tipleri birbirinden tamamen farklıdır — ve bu fark, bu dersin geri kalanında tekrar tekrar görülecek bir düzenin ilk örneğidir: bir değiştirici yöntemin sınıfı, o yöntemin ne işe yaradığından değil, hangi tipi döndürdüğünden okunur.

// buyuyen_dizi.rs — push () verir (deyim), pop Option verir (ifade)
fn main() {
    let mut alanlar: Vec<f64> = Vec::new();

    let itme_sonucu = alanlar.push(12.57); // Cember(2.0) alani
    println!("{:<24}: {:?}", "push donus degeri", itme_sonucu);

    alanlar.push(12.0); // Dikdortgen(3.0, 4.0) alani
    alanlar.push(6.0); // Ucgen(3.0, 4.0, 5.0) alani
    println!("{:<24}: {:?}", "alanlar (push sonrasi)", alanlar);

    let cikan = alanlar.pop();
    println!("{:<24}: {:?}", "pop donus degeri", cikan);
    println!("{:<24}: {:?}", "alanlar (pop sonrasi)", alanlar);
}
push donus degeri       : ()
alanlar (push sonrasi)  : [12.57, 12.0, 6.0]
pop donus degeri        : Some(6.0)
alanlar (pop sonrasi)   : [12.57, 12.0]

Sona ekleme yönteminin dönüş değeri ()dir — çıktıda push donus degeri: () bunu gösteriyor. Bu bir deyim: alanlar değişir, ama bu değişimin sonucunu temsil eden bir değer geri gelmez. Sondan çıkarma yöntemi ise Option<f64> döndürür — Some(6.0), çünkü listenin son öğesi 6.0dı. Bu bir ifade: dönen değer, çıkarılan öğenin kendisidir ve bir sonraki hesapta kullanılabilir.

RV12. — “Aynı iş sınıfındaki iki yöntem ayrı sınıflara düşebilir” iddiası burada push ile pop çiftiyle ölçülüyor: ikisi de alanlar listesini değiştiriyor, ikisi de tek bir öğeyle ilgileniyor, ama biri () biri Option<f64> veriyor. Bu ölçüm, ortak tanımın çekirdeğinde (ifade.rs) HashSet::insert ile Vec::push arasında kurulan aynı karşıtlığın bir yansımasıdır: sınıf, işin “ekleme” ya da “çıkarma” olmasından değil, o işin dönüş tipinden okunur.

Neden bu fark var? Sona eklemek her zaman başarılıdır — listenin boş ya da dolu olması sonucu değiştirmez, eklenecek yer her zaman vardır. Bu yüzden döndürülecek bilgilendirici bir sonuç yoktur. Sondan çıkarmak ise her zaman başarılı olmayabilir: liste boşsa çıkarılacak bir öğe yoktur. Option, bu iki olasılığı — bir öğe vardı (Some) ya da yoktu (None) — tek bir dönüş tipinde taşır. pushin “her zaman başarılı” olması onu bir sonuç döndürmekten muaf kılarken, popun “başarısız olabilmesi” onu bir sonuç döndürmeye zorluyor.

Çıktıdaki alanlar (push sonrasi): [12.57, 12.0, 6.0] satırı listenin tamamını gösteriyor; köşeli parantezler ve virgüllerle ayrılan sayılar, Vecin kendi hata ayıklama (debug) biçimidir. Üç push çağrısından sonra liste üç elemanlıdır; pop çağrısından sonra son eleman (6.0) çıkarılır ve geriye iki eleman ([12.57, 12.0]) kalır. Listenin kaç eleman tutabileceği ile ilgili bir üst sınır kaynakta hiçbir yerde yazılmıyor — Vec gerektiğinde büyür, bu büyümenin gerçekleştirim ayrıntıları bu dersin konusu değil.

Dizgi Bir Bayt Dizisidir

Rust’ta bir dizgi, karakterlerin değil baytların bir dizisidir; metin UTF-8 kodlamasıyla tutulur ve her karakter aynı sayıda bayt kaplamaz. ASCII aralığındaki bir harf tek bir bayt kaplarken, Türkçeye özgü ç, ş, ğ, ı, ö, ü gibi harfler iki bayt kaplar.

// dizgi_bayt_karakter.rs — dizgi bir bayt dizisidir, karakter sayisi ayri olculur
fn main() {
    let sekil_adi = "üçgen";
    println!("{:<24}: {}", "bayt sayisi (len)", sekil_adi.len());
    println!("{:<24}: {}", "karakter sayisi (chars)", sekil_adi.chars().count());
}
bayt sayisi (len)       : 7
karakter sayisi (chars) : 5

"üçgen" beş karakterden oluşur: ü, ç, g, e, n. Ama len() yöntemi (bayt uzunluğunu veren) 7 sonucunu veriyor, chars().count() (karakter sayısını veren) ise 5. Aradaki iki baytlık fark, ü ve ç harflerinin her birinin UTF-8’de iki bayt kaplamasından geliyor; g, e, n birer bayttır. Bu, önceki derslerde görülen “yalnızca kaynağı okuyarak bulunabilen sayı” ile “yalnızca çalıştırarak bulunabilen sayı” ayrımının bir başka örneğidir: "üçgen" yazımının kaç karakter olduğu kaynağı okuyarak sayılabilir, ama kaç bayt tuttuğu, hangi harflerin çok baytlı olduğu bilinmeden kaynaktan çıkarılamaz.

RV13. — “Bayt sayısı ile karakter sayısının ayrıştığı” iddiası burada, iki farklı yöntemin aynı dizgi üzerinde farklı sayı vermesiyle ölçülüyor: len() 7, chars().count() 5 veriyor. Fark tam olarak 2dir ve bu, dizgideki iki çok baytlı karakterin (ü, ç) her birinin bayt uzunluğuna bir fazladan katkısıyla açıklanır — beş karakterden ikisi ikişer bayt, üçü birer bayt kaplıyor: 2×2 + 3×1 = 7.

Bu ayrım, dizginin de bir koleksiyon olduğunu hatırlatıyor: len() yöntemi, Vecin len()iyle aynı adı taşır ve aynı şeyi yapar — dizginin taşıdığı öğe sayısını verir. Fark, bir dizginin öğelerinin bayt olmasıdır, karakter değil. chars() yöntemi bu baytları gerçek karakterlere ayrıştırıp saymayı üstleniyor; bu ikinci bir geçiş gerektirir, çünkü kaç baytın bir karakter oluşturduğu her karakterde değişebilir.

Bu yüzden bir dizgiye, bir büyüyen diziye erişir gibi konumla (sekil_adi[0] gibi) doğrudan erişilemez. Bir büyüyen dizide konum 0, her zaman bir tam öğedir; bir dizgide bayt konumu 0, çok baytlı bir karakterin ortasına denk gelebilir — "üçgen"in ilk baytı ü harfinin tamamı değil, onun iki baytından yalnızca ilkidir. Konuma dayalı erişimin dizgiler için tanımlanmamış olması, bu yarım karakter sorununu kaynaktan kaldırıyor: bir dizgiden tek tek karakter almak isteyen kod, chars() üzerinden geçmek zorunda kalıyor ve bu geçiş bayt sınırlarını değil karakter sınırlarını izliyor.

Karma Sözlük: Ekleme Bir İfadedir

Karma sözlük (HashMap), anahtar-değer çiftlerini tutan bir koleksiyondur. Büyüyen dizi bir öğeye konumla (0, 1, 2, …) erişirken, karma sözlük bir öğeye seçilen bir anahtarla erişir — bir sayı, bir dizgi, karşılaştırılabilir herhangi bir tip. Bu ders &str anahtarlar kullanıyor: her şekil adının kendi alan değerine karşılık geldiği küçük bir tablo. Ekleme yöntemi, büyüyen dizininkinden farklı bir dönüş tipine sahiptir: () değil, Option<V>.

// karma_sozluk_insert.rs — insert Option doner (ifade), dogrudan yazdirilmaz
use std::collections::HashMap;

fn main() {
    let mut alan_sozlugu: HashMap<&str, f64> = HashMap::new();

    let ilk_donus = alan_sozlugu.insert("cember", 12.57);
    println!("{:<24}: {:?}", "insert donus (ilk)", ilk_donus);

    alan_sozlugu.insert("dikdortgen", 12.0);
    alan_sozlugu.insert("ucgen", 6.0);

    let mut anahtarlar: Vec<&&str> = alan_sozlugu.keys().collect();
    anahtarlar.sort();
    for anahtar in anahtarlar {
        println!("{:<24}: {}", format!("alan_sozlugu[{}]", anahtar), alan_sozlugu[anahtar]);
    }
}
insert donus (ilk)      : None
alan_sozlugu[cember]    : 12.57
alan_sozlugu[dikdortgen]: 12
alan_sozlugu[ucgen]     : 6

"cember" anahtarı ilk kez eklendiğinde dönen değer Nonedur — bu anahtarın önceden bir değeri yoktu. Bu, Vec::pushin () vermesinden farklıdır: insert, eklemenin öncesi hakkında bir bilgi taşıyan bir ifadedir. Eğer aynı anahtara ikinci kez ekleme yapılsaydı, dönen değer None değil, önceki değeri taşıyan bir Some olurdu — bu, sınırlayıcı ölçümün konusu.

Karma sözlüğün kendisi doğrudan yazdırılmıyor; anahtarlar önce bir listeye toplanıp sıralanıyor, sonra tek tek erişiliyor. Bir karma yapının gezinme sırası koşumdan koşuma değişebilir; bu derste basılan her sayının beş yıl sonra da aynı çıktıyı vermesi gerektiği için, karma sözlüğün ham gezinme sırasına hiçbir yerde güvenilmiyor. Bu kuralın tam gerekçesi ve karşıtı — sıralı sözlüğün gezinmesinin neden her koşumda aynı olduğu — sıradaki dersin konusu.

Bu adım, alan_sozlugu[anahtar] erişiminin kendisini engellemiyor: bir anahtar zaten biliniyorsa, o anahtarla değere ulaşmak doğrudan ve deterministiktir — değişen yalnızca sözlüğün tamamının hangi sırayla dolaşılacağıdır. Bu yüzden yukarıdaki döngü önce keys() ile bütün anahtarları alıyor, onları sıralıyor ve ancak sıralı listeyi dolaşırken sözlüğe geri dönüp her anahtarın değerini okuyor — sözlüğün kendi iç sırasına hiçbir noktada bakmadan.

RV14. — “insert’in bir ifade olduğu ve önceki değeri taşıdığı” iddiası burada ilk ekleme çağrısının dönüş değeriyle ölçülüyor: ilk_donus adlı bir değişkene bağlanabiliyor ve {:?} ile basılabiliyor — bir () için bu bağlama anlamsız olurdu, çünkü ()in taşıdığı hiçbir bilgi yoktur. None çıktısı, “cember” anahtarının bu sözlükte daha önce hiç var olmadığını doğrudan söylüyor.

Anahtarları sıralamak için kullanılan anahtarlar: Vec<&&str> satırındaki çift & işareti gelişigüzel değildir: alan_sozlugu.keys() yöntemi zaten sözlükteki anahtarlara birer referans verir (&str) ve bu referansları bir listede toplamak listeyi referansların referansı yapar (&&str). Bu ayrıntı, sözlüğün kendisini hiç kopyalamadan yalnızca anahtarları okumaya izin verir; kopyalamanın ve referansların tam anlamı Sahiplik ve Ödünç Alma dersinin konusudur, burada yalnızca sıralama adımının hangi tipte çalıştığı görülüyor.

Sınırlayıcı Ölçüm: Yakalanmayan Dönüş Değeri Sessizce Kaybolur

insertin döndürdüğü eski değer, bir değişkene bağlanmazsa program çalışmaya devam eder — Rust bunu bir hata saymaz. Ama bu durumda eski değer, hiçbir yerde görünmeden kaybolur.

// insert_veri_kaybi.rs — insert'in eski degeri yakalanmazsa sessizce duser
use std::collections::HashMap;

fn main() {
    let mut stok: HashMap<&str, i32> = HashMap::new();
    stok.insert("cember", 5);
    stok.insert("cember", 9); // donus degeri yakalanmadi -- eski deger 5 burada kayboldu
    println!("{:<22}: {}", "yakalanmadan (guncel)", stok["cember"]);

    let eski_deger = stok.insert("cember", 3);
    println!("{:<22}: {:?}", "yakalanarak (eski)", eski_deger);
    println!("{:<22}: {}", "yakalanarak (guncel)", stok["cember"]);
}
yakalanmadan (guncel) : 9
yakalanarak (eski)    : Some(9)
yakalanarak (guncel)  : 3

İkinci satırda "cember" anahtarına 9 yazılıyor; bu satırın dönüş değeri (5i taşıyan Some(5)) hiçbir değişkene bağlanmıyor, sonuç olarak hesaplanıp hemen atılıyor. Programın geri kalanı bu kaybı fark etmez: stok["cember"] sorgusu doğrudan 9u verir, çünkü sözlük zaten güncellenmiştir. Kaybolan yalnızca eski değerdir — 5 sayısı, bu satırdan sonra programın hiçbir yerinde yeniden elde edilemez.

Üçüncü çağrı aynı işlemi tekrarlıyor, ama bu kez dönüş değeri eski_deger adına bağlanıyor. Sonuç Some(9)dur — bu, stok["cember"]in bu çağrıdan önce 9 olduğunu söylüyor. Aynı satır hem sözlüğü günceller hem de güncellemeden önceki değeri geri verir; tek fark, çağıranın bu değeri yakalayıp yakalamamasıdır.

RV15. — “Yakalanmayan dönüş değerinin veri kaybına yol açtığı” iddiası burada iki farklı çağrı biçiminin yan yana koşturulmasıyla ölçülüyor: ikinci insert çağrısı (9 yazan) dönüş değerini atıyor ve 5in programda bir daha görünmediği doğrudan gözlemleniyor; üçüncü çağrı (3 yazan) aynı yapıyı kullanıyor ama dönüş değerini eski_degere bağlıyor ve Some(9) olarak geri getiriyor. İki çağrı da aynı yöntemi, aynı imzayla çağırıyor; fark yalnızca çağıranın sonucu bir ada bağlayıp bağlamadığında.

Bu, insertin bir ifade olmasının doğrudan bir sonucudur: bir ifadenin değeri her zaman hesaplanır, ama bu değerin kullanılması zorunlu değildir. Bir deyimde böyle bir soru hiç doğmaz — pushin zaten döndürecek bir değeri yoktur, kaybedilecek bir şey de yoktur. Kayıp riski, tam olarak dönüş değeri taşıyan yöntemlerde ortaya çıkar; bu risk, o dönüş değerinin ne anlama geldiğini bilmeden kodu okuyan biri için görünmez kalabilir.

stok örneğindeki "cember" anahtarı bir sayım anlamı taşısaydı — örneğin depodaki parça adedi — ikinci çağrının 5i sessizce 9a değiştirmesi (üzerine eklemesi değil) gerçek bir veri kaybı olurdu: önceki adet hiçbir yerde toplanmadan kaybolurdu. Bu örnekte insert bilerek “üzerine yaz” anlamında kullanıldı; bir önceki değeri toplama amacıyla korumak isteyen bir kod, dönüş değerini mutlaka yakalamak zorundadır. Kaynakta bu iki niyet — üzerine yazmak ve toplamak — aynı insert çağrısıyla ifade edilir; aradaki farkı belirleyen, çağıranın dönüş değerini ne yaptığıdır.

Bu ders, üç ayrı koleksiyonun değiştirici yöntemlerini ölçtü ve hepsinin aynı iki kalıptan birine düştüğünü gösterdi: bir yöntem ya () verir (deyim, push gibi) ya da işlemin sonucuyla ilgili bir bilgi verir (ifade, pop ve insert gibi). Bu iki koleksiyonun — büyüyen dizi ve karma sözlük — ortak noktası, ikisinin de “kaçıncı sırada” ya da “hangi anahtarla” sorularına yanıt verse de, bu yanıtın hangi sırayla geleceği konusunda aynı garantiyi vermemesidir. Büyüyen dizide sıra kaynaktaki ekleme sırasıdır ve hiç değişmez. Karma sözlükte ise bu dersin ölçtüğü gibi, ham gezinme sırasına hiç güvenilmedi — anahtarlar her zaman önce sıralandı.

Özet

  • Büyüyen dizinin sona ekleme yöntemi (push) () döner ve bir deyimdir; sondan çıkarma yöntemi (pop) Option döner ve bir ifadedir.
  • Bir dizginin bayt uzunluğu ile karakter sayısı, çok baytlı karakterler içeren metinlerde birbirinden ayrışır.
  • Karma sözlüğün ekleme yöntemi (insert) bir ifadedir; dönüş değeri, anahtarın önceki değerini (Some) ya da anahtarın yeni olduğunu (None) taşır.
  • Karma sözlük doğrudan yazdırılmaz; anahtarlar önce toplanıp sıralanır.
  • insertin dönüş değeri bir değişkene bağlanmazsa, üzerine yazılan eski değer sessizce kaybolur; bu risk yalnızca dönüş değeri taşıyan yöntemlerde vardır.

Bir koleksiyonun kendisi de aynı ifade/deyim/öğe ekseninden geçirilebilir: Vec::new() ve HashMap::new() çağrıları birer ifadedir, çünkü boş bir koleksiyon değeri üretirler; bu değeri let ile bir ada bağlamak da yine bir bağlama deyimidir. Bu dersin asıl katkısı, koleksiyonun kendisinden değil, onu değiştiren yöntemlerden geliyor — çünkü değiştirme, tek bir çağrının hem bir yan etki (koleksiyonun içeriği değişir) hem de isteğe bağlı bir sonuç (dönüş değeri) taşıyabildiği tek an.

Sonraki Adım

Bu ders, karma sözlüğün doğrudan yazdırılmadığını, anahtarların önce sıralandığını ölçtü — ama bu neden gerekli ve karşıtı olan sıralı yapı bu sıralama adımına neden ihtiyaç duymuyor? Sıradaki ders bu soruyu açıyor: sıralı sözlük ve sıralı küme ile karma sözlük ve karma küme arasındaki güvence farkı nedir ve bu güvence hangi koşulu karşılamayı ister?

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat