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)Optiondö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.