Ders 11 / 18
Sözlükler
Bir sözlükten okumak dört ayrı biçimde dört ayrı sınıfa düşüyor: var olan anahtar değerini veriyor, olmayan anahtar sıfır değerle birlikte bir yokluk yanıtı veriyor, nil bir sözlükten okumak tanımlı ama ona yazmak panikliyor, silinen bir anahtar yeniden yokmuş gibi davranıyor. Gezinme sırası bilerek rastgeledir ve hiçbir yerde doğrudan yazdırılmaz. Sınırlayıcı ölçüm iki yanıtlı erişimin neden gerekli olduğunu gösteriyor: değeri sıfır olan bir kayıt ile hiç var olmayan bir kayıt, tek yanıtlı erişimde ayırt edilemiyor.
İçindekiler
Önceki ders dilimlerde paylaşımın ne zaman sürüp ne zaman kesileceğinin, kaynak koddan değil kapasiteden okunduğunu ölçtü ve bunu ayırmayı ikinci bir soruya bıraktı: anahtar-değer eşleşmesinde olmayan bir anahtara erişildiğinde ne olur, ve bu yanıt tek bir değerle mi geliyor, iki değerle mi? Bu ders o soruyla başlıyor.
Go’da anahtar-değer eşleşmesinin adı sözlüktür (map). Dilim gibi sözlük de bir
başlıktır — kendi verisini değil, çalışma zamanının yönettiği bir karma tabloya giden bir
göstericiyi taşır. Ama sözlüğün yazılmayan bir anahtara verdiği yanıt, dilimin yazılmayan
bir sınıra verdiği yanıttan daha katmanlı: dört ayrı erişim biçimi, dört ayrı sınıfa
düşüyor.
Bu katmanlılığın kaynağı, dilimin yalnızca konum üzerinden erişilmesine karşın sözlüğün anahtar üzerinden erişilmesidir. Bir dilimde geçersiz bir konuma erişmek her zaman aynı şeyi yapar — sınırların dışına taşan bir indeks panikler. Bir sözlükte “anahtar yok” durumu panik değildir, çünkü sözlük olmayan bir anahtarı aramanın normal bir sonuç olduğunu, tükenmiş bir sınır değil sıradan bir sorgu olduğunu varsayıyor. Panik yalnız sözlüğün kendisi hiç var olmadığında (yazma denemesinde) geliyor.
Dört Erişim Biçimi
// sozluk.go — sozlukte dort erisim bicimi, dort ayri sinif
package main
import "fmt"
func panikliyorMu(f func()) (p bool) {
defer func() {
if recover() != nil {
p = true
}
}()
f()
return false
}
func main() {
sayac := map[string]int{"kalem": 3, "defter": 7, "silgi": 1}
deger, varMi := sayac["kalem"]
fmt.Println("var olan anahtar : deger=", deger, " varMi=", varMi)
deger, varMi = sayac["mudur"]
fmt.Println("olmayan anahtar : deger=", deger, " varMi=", varMi)
var bosSayac map[string]int
fmt.Println("nil sozlukten okuma : deger=", bosSayac["herhangi"])
yazmaPanikledi := panikliyorMu(func() { bosSayac["yeni"] = 1 })
fmt.Println("nil sozluge yazma : panik=", yazmaPanikledi)
delete(sayac, "silgi")
degerSil, varMiSil := sayac["silgi"]
fmt.Println("silinen anahtar : deger=", degerSil, " varMi=", varMiSil)
}
var olan anahtar : deger= 3 varMi= true olmayan anahtar : deger= 0 varMi= false nil sozlukten okuma : deger= 0 nil sozluge yazma : panik= true silinen anahtar : deger= 0 varMi= false
Beş satır dört sınıfı ayırıyor. Kaynaktaki deger, varMi := sayac["kalem"] yazımının adı
iki yanıtlı erişimdir: sözlükten okumak, istenirse tek bir değer yerine değer ve
varlık bilgisini birlikte döndürür. Var olan bir anahtardan okumak beklendiği gibi çalışıyor.
Olmayan bir anahtar (sayac içinde hiç bulunmayan "mudur") hata vermiyor, intin sıfır
değeriyle birlikte bir yokluk yanıtı veriyor — bu ikinci yanıt aşağıda ayrı bir bölümün
konusu. Hiç ilk değeri verilmemiş bir sözlükten — bosSayak, nil durumunda — okumak da
çalışıyor ve yine sıfır değer dönüyor; Sıfır Değerler dersinin bağlayıcı 8/4/3 dağılımında
bu satır tanımlı sınıfına giren sekiz durumdan biriydi. Ama aynı nil sözlüğe yazmak
farklı bir sınıfa düşüyor: dil burada bir sıfır değer koymuyor, çalışma zamanı panikliyor —
aynı dağılımın açıkta kalan üç durumundan biri. Son satır silinen bir anahtarın, hiç
var olmamış bir anahtardan davranışça ayırt edilemediğini gösteriyor: delete sonrası
sayac["silgi"] de tıpkı sayac["mudur"] gibi sıfır değer ve false veriyor.
Dört erişim biçiminin üçü aynı kalıba uyuyor — okumak her zaman güvenli, yazmak nil
sözlükte güvenli değil. Bu asimetri rastgele değil: bir sözlükten okumak yalnızca karma
tabloda arama yapar ve bulamazsa sıfır değer döndürür, ama yazmak karma tabloya bir girdi
eklemeyi gerektirir ve nil bir sözlüğün henüz hiçbir karma tablosu yoktur — eklenecek
yer, dilimin nil durumundaki gibi sıfır uzunluklu bir alt dizi değil, tümüyle yok. Bu
asimetri deletee kadar uzanıyor: nil bir sözlükten var olmayan bir anahtarı silmeye
çalışmak da panik vermiyor, çünkü delete de yazma gibi değil okuma gibi davranıyor —
silinecek bir şey bulamazsa sessizce hiçbir şey yapmıyor.
Anahtar Tipi Karşılaştırılabilir Olmak Zorundadır
Diziler ve Dilimler dersi bir dizinin == ile karşılaştırılabildiğini, bir dilimin
karşılaştırılamadığını ölçmüştü ve bunun bir sözlük anahtarı olarak kullanılabilirlikle
ilgili olduğunu bir cümleyle bırakmıştı. Sözlüğün kendi arama düzeneği bu bağlantıyı
zorunlu kılıyor: bir anahtarı bulmak için sözlük onu var olan anahtarlarla eşitlik
üzerinden karşılaştırıyor, bu yüzden anahtar tipi karşılaştırılabilir olmak zorunda.
BT9. — Bu kısıt bir derleyici çağrısıyla koşturuluyor; yalnızca “derlendi” ya da “derlenmedi” sınıfı basılıyor.
// anahtar.go — sozluk anahtari karsilastirilabilir olmak zorunda
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
func gecerMi(kaynak string) bool {
dizin, err := os.MkdirTemp("", "sinama")
if err != nil {
return false
}
defer os.RemoveAll(dizin)
os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module sinama\n\ngo 1.24\n"), 0o644)
os.WriteFile(filepath.Join(dizin, "kod.go"), []byte(kaynak), 0o644)
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dizin
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
return cmd.Run() == nil
}
func main() {
diziAnahtar := `package sinama
func F() {
m := map[[2]int]string{}
m[[2]int{0, 0}] = "sol-ust"
}`
dilimAnahtar := `package sinama
func F() {
m := map[[]int]string{}
_ = m
}`
fmt.Println("dizi anahtarli sozluk :", gecerMi(diziAnahtar))
fmt.Println("dilim anahtarli sozluk :", gecerMi(dilimAnahtar))
}
dizi anahtarli sozluk : true dilim anahtarli sozluk : false
Dizi anahtarlı sözlük derleniyor, çünkü dizinin karşılaştırılabilirliği önceki derste
kuruldu. Dilim anahtarlı sözlük derlenmiyor — dilim karşılaştırılamadığı için sözlüğün
kendisi bile tanımlanamıyor, tek bir girdi eklenmeden önce. Bu, iki bileşik tipin aynı
özelliğin (karşılaştırılabilirlik) iki ayrı yerde nasıl sonuç doğurduğunu gösteriyor: biri
== işlecinde, öteki bir sözlüğün anahtar tipi olarak kabul edilip edilmemesinde.
Fonksiyona Geçerken: Sözlük Dilimden Farklı Davranır
Önceki ders bir işlevin aldığı dilime append ile öğe eklemesinin, sonucu geri
döndürmediği sürece çağırana hiç yansımadığını ölçmüştü. Sözlükte aynı tuzak yok.
// gecis.go — sozluge yazmak, sonucu dondurmeden de cagirana yansir
package main
import "fmt"
func ekleGuvenli(s map[string]int, anahtar string, deger int) {
s[anahtar] = deger
}
func main() {
sayac := map[string]int{"kalem": 3}
ekleGuvenli(sayac, "silgi", 5)
fmt.Println("cagirandaki sozluk:", sayac["silgi"])
}
cagirandaki sozluk: 5
ekleGuvenli hiçbir şey döndürmüyor, ama sayac["silgi"] çağırandan bakıldığında 5.
Fark, dilimin appendiyle sözlüğün yazma işleminin farklı şeyler yapmasından geliyor:
append kapasiteyi aştığında yeni bir alt dizi ayırıp başlığı günceller — bu güncelleme
yalnızca yerel kopyada olur. Sözlüğe yazmak ise var olan karma tabloyu yerinde
değiştirir; başlığın kendisi hiç değişmez, yalnızca tablonun içeriği değişir. Parametre
olarak geçen sözlük değişkeni çağırandakiyle aynı tabloyu gösterdiği için, tabloya yapılan
her değişiklik çağırana görünür. Sözlüğün dilimden ayrıldığı yer tam olarak burası: dilim
büyürken bazen alt dizisini değiştirmek zorunda kalıyor, sözlük ise büyürken kendi
tablosunu asla değiştirmiyor — yalnızca genişletiyor.
Kapasite İpucu, Kapasite Sınırı Değildir
make ile bir sözlük kurulurken verilebilen ikinci argüman, dilimdeki kapasiteyle aynı
sözü taşımıyor. Dilimde kapasite bir sınırdı — aşıldığında append yeni bir alt dizi
ayırırdı. Sözlükte aynı sayı yalnızca bir ipucudur; karma tablonun başlangıç
büyüklüğünü belirler ama sözlüğün büyümesini hiçbir şekilde sınırlamaz.
// ipucu.go — sozluk kapasitesi bir ipuçudur, sinir degildir
package main
import "fmt"
func main() {
kucuk := make(map[string]int, 2) // yalnizca baslangic ipucu
for i, ad := range []string{"a", "b", "c", "d", "e", "f"} {
kucuk[ad] = i
}
fmt.Println("baslangic ipucu=2, eklenen anahtar sayisi:", len(kucuk))
}
baslangic ipucu=2, eklenen anahtar sayisi: 6
kucuk yalnızca iki anahtar için ayrılmış bir ipucuyla kuruldu ama altı anahtar sorunsuz
eklendi. make([]int, 0, 2) yazımıyla kurulan bir dilime üçüncü öğeyi eklemek appendi
yeni bir alt dizi ayırmaya zorlardı ve bu, Dilim İşlemleri dersinde ölçülen paylaşım
kesilmesine yol açardı; sözlükte böyle bir eşiği geçme anı yok, çünkü ikinci argüman
kapasiteyi taahhüt etmiyor, yalnızca çalışma zamanına bir tahmin veriyor. Sözlüğün
kendi iç tablosu gerektiğinde büyür, ama bu büyüme dilimin appendi gibi çağırana
görünen bir başlık değişikliği doğurmaz — çünkü sözlüğün başlığı zaten çağıranla paylaşılan
tek başlıktır, yeniden atanacak ikinci bir kopyası yoktur.
Gezinme Sırası: Tanımlı Bir Belirsizlik
Bir sözlük üzerinde range ile gezinmenin sırası bilerek rastgeledir; aynı sözlük aynı
program içinde bile iki ayrı range çağrısında farklı sırayla dolaşılabilir. Bu bir
gerçekleştirim kusuru değil, tanımlı bir belirsizliktir: dil, gezinme sırasının hiçbir
koda bağımlı hâle gelmemesi için sırayı bilinçli olarak karıştırıyor. Bu yüzden kursun
hiçbir dersinde bir sözlük doğrudan yazdırılmıyor; bunun yerine anahtarlar sıralanıyor.
// gezinme.go — sozluk dogrudan yazdirilmaz, anahtarlar siralanir
package main
import (
"fmt"
"sort"
)
func main() {
sayac := map[string]int{"kalem": 3, "defter": 7, "silgi": 1}
anahtarlar := make([]string, 0, len(sayac))
for a := range sayac {
anahtarlar = append(anahtarlar, a)
}
sort.Strings(anahtarlar)
for _, a := range anahtarlar {
fmt.Println(" ", a, "=", sayac[a])
}
}
defter = 7 kalem = 3 silgi = 1
Burada basılan sıra alfabetiktir, çünkü onu üreten sort.Strings çağrısıdır — sözlüğün
kendi gezinme sırası değil. for a := range sayac döngüsü yalnızca anahtarları bir dilime
topluyor; hangi sırayla topladığı bu ölçüm için önemsiz, çünkü sort.Strings sonucu her
koşumda aynı sıraya getiriyor.
Bilerek rastgele tutulan sıranın amacı, kodun sıraya bağımlı hâle gelmesini baştan engellemektir. Sıra tutarlı olsaydı — örneğin her zaman ekleme sırasını izleseydi — bu tutarlılığa güvenen kod zamanla birikirdi: bir döngü çıktısını ekleme sırasına göre biçimlendiren, ya da iki sözlüğü aynı sırada gezip karşılaştıran kod. Böyle bir kod, karma tablonun iç yapısı değiştiği anda (örneğin bir anahtar silinip yeniden eklendiğinde) sessizce bozulurdu. Sırayı baştan rastgele tutmak, bu bağımlılığın hiç oluşmamasını garantiliyor — programcı sıraya güvenemez, çünkü güvenilecek sabit bir sıra hiç yok.
Bir sözlüğü fmt.Println(sayac) ile doğrudan yazdırmak da teknik olarak çalışır ve hatta
biçimlendirme paketi çıktıyı anahtara göre sıralı üretebilir — ama bu, dilin garantisi
değil, biçimlendirme paketinin kendi seçimidir; range ile doğrudan gezinildiğinde aynı
garanti yoktur. Ölçüm bu farka güvenmiyor ve sıralamayı kendi elleriyle yapıyor; kursun
hiçbir dersi bir sözlüğü range çıktısına güvenerek yazdırmıyor.
BT10. — Sıralama üç anahtarlık küçük bir kümede yapılıyor; ölçümün amacı sıralamanın kendisini göstermek değil, sözlüğün kendi sırasına hiç dayanılmadığını göstermek. Aynı sonuç yüzlerce anahtarla da alınır, yalnızca çıktısı daha uzun olurdu.
Sınırlayıcı Ölçüm: Sıfır Değer ile Yokluk Tek Yanıtla Ayırt Edilemez
İki yanıtlı erişimin neden var olduğu, tek yanıtlı erişimin nerede yetersiz kaldığı görülmeden tam anlaşılmıyor. Bir anahtarın değeri, tipin sıfır değeriyle aynı olduğunda sözlük iki farklı durumu artık ayırt edemiyor: anahtar var ve değeri sıfır, ya da anahtar hiç yok.
// belirsizlik.go — sifir degerli kayit ile yokluk, tek yanitla ayirt edilemez
package main
import "fmt"
func main() {
sayac := map[string]int{"kalem": 3}
sayac["yeni-urun"] = 0 // kayit var, degeri kasten sifir
tekYanit := sayac["yeni-urun"]
tekYanitYok := sayac["hic-eklenmemis"]
fmt.Println("var, degeri sifir olan : tek yanit=", tekYanit)
fmt.Println("hic eklenmemis : tek yanit=", tekYanitYok)
_, varMi1 := sayac["yeni-urun"]
_, varMi2 := sayac["hic-eklenmemis"]
fmt.Println("iki yanitla ayrim: yeni-urun varMi=", varMi1, " hic-eklenmemis varMi=", varMi2)
}
var, degeri sifir olan : tek yanit= 0 hic eklenmemis : tek yanit= 0 iki yanitla ayrim: yeni-urun varMi= true hic-eklenmemis varMi= false
İlk iki satır birebir aynı: 0. Tek yanıtlı erişimden bakıldığında "yeni-urun" ile
"hic-eklenmemis" arasında hiçbir fark yok — ikisi de sıfır veriyor. Üçüncü satır bu
körlüğü gideriyor: varMi1 true, varMi2 false. Fark, değerin kendisinde değil,
sözlüğün o anahtarı hiç tutup tutmadığında.
Bu sınırlama pratikte gerçek sonuçlar doğuruyor. Bir sayaç sözlüğünde “hiç görülmedi” ile “görüldü ama sıfır kez arttı” arasında fark varsa, ya da bir yapılandırma haritasında “varsayılan alınmış” ile “açıkça sıfıra ayarlanmış” arasında fark varsa, tek yanıtlı erişim bu ayrımı hiç göremiyor. İki yanıtlı erişim burada zorunlu bir yazım değil, tek doğru yazım hâline geliyor — “tanımlı” olmak, tek başına, “yeterli” olmak anlamına gelmiyor.
Bu sınırlama, dersin baştaki dört sınıfını da yeniden okumayı gerektiriyor. “Olmayan
anahtar” ile “silinen anahtar” bölümündeki iki satır tek yanıtla bakıldığında aynı 0’ı
verdi; iki yanıtlı erişim eklenmeden önce bu ikisinin — ve değeri gerçekten sıfır olan
üçüncü bir anahtarın — birbirinden ayrılamayacağı görülmüyordu. Sözlüğün verdiği tek
yanıt, “bir değer var” ile “değer yok, yerine sıfır kondu” arasındaki farkı taşımıyor;
taşıyan, ikinci dönüş değeridir.
Özet
- Bir sözlükten dört erişim biçimi dört ayrı sınıfa düşüyor: var olan anahtar değerini
veriyor, olmayan anahtar sıfır değerle bir yokluk yanıtı veriyor,
nilsözlükten okumak tanımlı ama ona yazmak panikliyor, silinen bir anahtar yeniden yokmuş gibi davranıyor. - Sözlüğe yazmak var olan tabloyu yerinde değiştirir; bir işleve geçirilen sözlüğe yapılan
yazma, sonucu geri döndürmeden de çağırana yansır — dilimin
appenddavranışından farklı. - Sözlüğün gezinme sırası bilerek rastgeledir ve hiçbir derste doğrudan yazdırılmaz;
anahtarlar
sort.Stringsile sıralanıp öyle basılır. - İki yanıtlı erişim, sıfır değerli bir kaydı yokluktan ayırmanın tek yoludur; tek yanıtlı erişim ikisini de aynı sıfır değerle döndürür.
Sonraki Adım
Sözlüğün anahtarı, tek bir tam sayı ya da dizgi gibi bölünmez bir değerdi; sayaç örneğindeki gibi tek bir bilgiyi taşıyordu. Sıradaki tip birden çok alanı bir arada tutuyor ve kendi alanlarının her birine ayrı ayrı erişilebiliyor — sözlüğün tek bir anahtarla eriştiği yerde, bu yeni tip adlandırılmış birden çok alanla erişiyor. Yapı da yazılmayan bir şeyle başlıyor: alanlarına değer verilmezse ne olur? Ama sözlükten farklı olarak alanların sırası da bir anlam taşıyor, ve bu sıraya güvenmenin kendi kırılganlığı var — sözlüğün anahtar adıyla eriştiği yerde bu yeni tip, konumla erişmenin de bir seçenek olduğu bir alan taşıyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.