İçeriğe geç
academia.sh

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, nil sö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 append davranışından farklı.
  • Sözlüğün gezinme sırası bilerek rastgeledir ve hiçbir derste doğrudan yazdırılmaz; anahtarlar sort.Strings ile 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat