İçeriğe geç
academia.sh

Ders 12 / 13

Genel Veri Yapıları

Bir genel veri yapısının tip parametresi çağıranın bildiriminde açıkça yazılır: Yigin[int] ile Yigin[string] %T'de iki ayrı dinamik tip olarak görünüyor ve biri diğerinin yerine geçemiyor. Tip güvenliğinin ölçülebilir karşılığı: boş arayüzle yazılan aynı yapıda elemanı kullanmak için bir tip iddiası gerekirken genel yapıda sıfır. Sınırlayıcı ölçüm, karşılaştırılabilirliğin hem kapsayıcının kendi alan yapısına hem tip parametresinin kendisine bağlı olduğunu gösteriyor.

İçindekiler

Tip Kısıtları dersi bir bağın iki katmanda kurulduğunu gösterdi: kısıtın izin verdiği işlemler bildirimde durur, o işlemlerin hangi somut tip için çalışacağı kullanım yerinde kurulur. Bu ders aynı iki katmanı bir veri yapısı üzerinde ölçer, ama bir farkla: bir genel fonksiyonun tip parametresi çağrı yerinde çıkarılıyordu, kaynakta hiç yazılmıyordu. Bir genel veri yapısının tip parametresi ise çağıranın kendi bildiriminde açıkça yazılırYigin[int] diyen taraf, [int]i kendi eliyle koyar, derleyici bunu bir argümandan okumaz. Aynı soru, iki farklı yanıt: bağ yine kaynağın dışında (fonksiyonun kendi tanımının dışında) kuruluyor, ama bu kez çıkarımla değil, çağıranın yazdığı bir bildirimle.

Bu farkın kaynağı, bir fonksiyon çağrısıyla bir tip bildirimi arasındaki farkla aynı yerde duruyor. Bir fonksiyon çağrısında argümanlar zaten var — derleyici onların tipine bakıp Tyi çıkarabiliyor. Bir tip bildiriminde ise henüz hiçbir argüman yok; Yigin[int] bir değer değil, bir tip adı, ve bir tip adının hangi tip parametresiyle örnekleneceğini çıkaracak bir argüman ortada bulunmuyor. Derleyicinin çıkarım yapabilmesi için okuyacağı bir şeye ihtiyacı var; genel fonksiyonda bu, çağrının argümanlarıdır, genel tipte ise böyle bir argüman yok — çağıranın kendisi yazmak zorunda.

Örnekleme Çağıranın Bildiriminde Duruyor

Bir genel veri yapısı, adının hemen ardına bir tip parametresi listesi ekleyen bir tip bildirimidir: type Yigin[T any] struct{ ogeler []T }. Bu tek kaynak, Yigin[int] için de Yigin[string] için de kullanılabilir bir tip güvenli kapsayıcı kalıbı kurar — ama kullanılabilmesi için önce örneklenmesi gerekir, ve bu örnekleme her zaman kullanan tarafın kendi bildiriminde yazılıdır: bir değişken tanımında, bir alan tipinde ya da bir dönüş tipinde.

Yiginin yöntemleri kendi tip parametresini yeniden tanımlamıyor; alıcı yazımındaki (y *Yigin[T]) ifadesi, tipin kendi bildiriminde açılmış olan Tyi yönteme taşıyor. Ekle(o T) ve Tepe() (T, bool) bu yüzden yeni bir köşeli parantez açmıyor — kullandıkları T, Yiginin kendisiyle birlikte, Yigin[int] örneklendiği anda zaten inte bağlanmış oluyor. Örnekleme bir kez, tipin kendi bildiriminde oluyor; o tipe bağlı her yöntem bu tek örneklemeyi paylaşıyor.

// yigin.go — genel veri yapisinin ornek(leme)si cagiranin bildiriminde
package main

import "fmt"

type Yigin[T any] struct{ ogeler []T }

func (y *Yigin[T]) Ekle(o T) { y.ogeler = append(y.ogeler, o) }

func (y *Yigin[T]) Tepe() (T, bool) {
	var sifir T
	if len(y.ogeler) == 0 {
		return sifir, false
	}
	return y.ogeler[len(y.ogeler)-1], true
}

func main() {
	var tamsayiYigini Yigin[int]
	tamsayiYigini.Ekle(3)
	tamsayiYigini.Ekle(5)

	var dizgiYigini Yigin[string]
	dizgiYigini.Ekle("elma")

	t, _ := tamsayiYigini.Tepe()
	d, _ := dizgiYigini.Tepe()

	fmt.Printf("%-24s tip=%-16T deger=%v\n", "tamsayiYigini", tamsayiYigini, t)
	fmt.Printf("%-24s tip=%-16T deger=%v\n", "dizgiYigini", dizgiYigini, d)
	fmt.Println("sonuc uzerinde aritmetik:", t+1)
}
tamsayiYigini            tip=main.Yigin[int]  deger=5
dizgiYigini              tip=main.Yigin[string] deger=elma
sonuc uzerinde aritmetik: 6

GN9. var tamsayiYigini Yigin[int] satırının kendisi, Tnin int olacağını söyleyen bir bildirimdir — Genel Fonksiyonlar dersindeki ilkOge(tamsayilar) çağrısıyla karşılaştırıldığında fark açık: orada Tnin ne olduğu argümanın tipinden çıkarılıyordu ve kaynakta hiçbir yerde int yazmıyordu; burada int satırın içinde, gözle görülür biçimde duruyor. %T bunu doğruluyor: tamsayiYigini ile dizgiYigini aynı kaynaktan üretildi, ama çalışma zamanındaki dinamik tipleri main.Yigin[int] ve main.Yigin[string] — birbirinden tamamen ayrı iki tip, tıpkı [4]int ile [3]intin ayrı tip olması gibi (Diziler ve Dilimler, M09/K01). Ortak ölçümde bu bağ “genel tipin örneklenmesi” adıyla bildirim örneklerinden biri sayılıyor.

var tamsayiYigini Yigin[int] satırının bir çağrı içermediğine de dikkat etmek gerekir — bu bir sıfır değer bildirimi, New benzeri bir yapıcı çağrılmıyor. Sıfır Değerler dersinin (M09/K01) ölçtüğü gibi, bir yapının sıfır değeri kendi alanlarının sıfır değerlerinden kuruluyor: ogeler alanı burada nil bir dilim olarak başlıyor. Bu, örnekleme ile ilk değerin verilmesi arasındaki farkı gösteriyor: Yigin[int] yazmak Tyi bağlıyor, ama ogeleri doldurmuyor; ilk Ekle çağrısı appendi nil bir dilim üzerinde çalıştırıyor ve Değişken Sayıda Argüman dersinin (M09/K01) ölçtüğü gibi bu sorunsuz işliyor — nil dilime eklemek onu büyütüyor, panikletmiyor.

Tip Güvenliğinin Ölçülebilir Karşılığı

İki örneklemenin ayrı tip olması bir sonuç doğuruyor: biri diğerinin yerine geçemiyor. Aşağıdaki ölçüm bunu, ve aynı işi boş arayüzle yazmanın bedelini, derleyiciye sorarak sayıyor.

// guvenlik.go — tip guvenliginin olculebilir karsiligi: kac tip iddiasi gerekiyor
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

func derlenirMi(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 siniflaKaynak(kaynak string) string {
	if derlenirMi(kaynak) {
		return "derlendi"
	}
	return "derlenmedi"
}

func main() {
	yerDegistirme := `package sinama
type Yigin[T any] struct{ ogeler []T }
func F() {
	var a Yigin[int]
	var b Yigin[string]
	b = a
	_ = b
}`

	anySizIddia := `package sinama
type YiginAny struct{ ogeler []any }
func (y *YiginAny) Ekle(o any) { y.ogeler = append(y.ogeler, o) }
func (y *YiginAny) Tepe() any { return y.ogeler[len(y.ogeler)-1] }
func F() {
	var y YiginAny
	y.Ekle(3)
	t := y.Tepe()
	_ = t + 1
}`

	anyIleIddia := `package sinama
type YiginAny struct{ ogeler []any }
func (y *YiginAny) Ekle(o any) { y.ogeler = append(y.ogeler, o) }
func (y *YiginAny) Tepe() any { return y.ogeler[len(y.ogeler)-1] }
func F() {
	var y YiginAny
	y.Ekle(3)
	t, tamam := y.Tepe().(int)
	_ = tamam
	_ = t + 1
}`

	genelIleIddiasiz := `package sinama
type Yigin[T any] struct{ ogeler []T }
func (y *Yigin[T]) Ekle(o T) { y.ogeler = append(y.ogeler, o) }
func (y *Yigin[T]) Tepe() T { return y.ogeler[len(y.ogeler)-1] }
func F() {
	var y Yigin[int]
	y.Ekle(3)
	t := y.Tepe()
	_ = t + 1
}`

	fmt.Printf("%-42s %s\n", "durum", "sonuc")
	fmt.Printf("%-42s %s\n", "Yigin[string] = Yigin[int] (yer degistirme)", siniflaKaynak(yerDegistirme))
	fmt.Printf("%-42s %s\n", "any yigininda 0 tip iddiasiyla aritmetik", siniflaKaynak(anySizIddia))
	fmt.Printf("%-42s %s\n", "any yigininda 1 tip iddiasiyla aritmetik", siniflaKaynak(anyIleIddia))
	fmt.Printf("%-42s %s\n", "genel yiginda 0 tip iddiasiyla aritmetik", siniflaKaynak(genelIleIddiasiz))
}
durum                                      sonuc
Yigin[string] = Yigin[int] (yer degistirme) derlenmedi
any yigininda 0 tip iddiasiyla aritmetik   derlenmedi
any yigininda 1 tip iddiasiyla aritmetik   derlendi
genel yiginda 0 tip iddiasiyla aritmetik   derlendi

GN10. İlk satır, iki örneklemenin birbirinin yerine geçememesini doğrudan gösteriyor: b = a derlenmiyor, çünkü Yigin[string] ile Yigin[int] derleyici için [4]int ile [3]int kadar ayrı iki tip. Bu, örneklemenin çağıranın bildiriminde durmasının doğrudan sonucu — bildirimde yazılan tip, o değişkenin kalıcı kimliğinin bir parçası oluyor.

GN11. Son üç satır aynı işi iki yazımla karşılaştırıyor. Boş arayüzle yazılan YiginAny, Tepe()ten any döndürüyor; bu değeri doğrudan +1 ile toplamaya çalışmak derlenmiyor, çünkü anynin somut tipi kaynakta bilinmiyor. Tek çıkış yolu bir tip iddiası yazmak (y.Tepe().(int)) — Tip İddiası ve Tip Anahtarı dersinin ölçtüğü aynı mekanizma, burada bir kapsayıcının her okuma noktasında tekrar tekrar yazılması gereken bir yük olarak beliriyor. Genel Yigin[int] ise Tepe()ten doğrudan int döndürüyor ve +1 hiçbir tip iddiası yazılmadan derleniyor — tip güvenliğinin ölçülebilir karşılığı bu: aynı işi yapmak için gereken tip iddiası sayısı any yazımında en az bir, genel yazımda sıfır.

Bu fark kapsayıcı büyüdükçe katlanıyor. YiginAnynin her okuma noktası — Tepe, bir Alt yöntemi eklense onun dönüşü, bir Tumu yöntemi eklense onun döndürdüğü dilimin her elemanı — kendi tip iddiasını taşımak zorunda, çünkü kapsayıcı tip bilgisini anynin arkasında saklıyor ve her okuyan taraf bu bilgiyi kendi başına geri kazanmalı. Yigin[T]de ise tip bilgisi kapsayıcının kendi tipinin bir parçası; kaç yeni yöntem eklenirse eklensin, hiçbiri bir tip iddiası yazmak zorunda kalmıyor, çünkü dönüş tipleri zaten T — derleme zamanında bilinen, somut bir tip. Bir tip iddiasının kendisi de bedelsiz değil: yanlış tipte bir değer eklenmişse (bir any kapsayıcıya bu, hiçbir derleme hatası vermeden mümkün), iddia çalışma zamanında panikler ya da iki yanıtlı biçimde sessizce başarısız olur — genel yazımda bu hata sınıfının kendisi hiç var olmuyor, çünkü yanlış tipte bir eleman eklemek zaten derlemede reddediliyor.

Sınırlayıcı Ölçüm: Karşılaştırılabilirlik Hem Yapıya Hem T’ye Bağlı

Tip güvenliği bedelsiz değil. Bir genel tipin == ile karşılaştırılıp karşılaştırılamayacağı, yalnız tip parametresine değil, kapsayıcının kendi alan yapısına da bağlı — ve bu iki etken bağımsız çalışıyor.

// karsilastirma.go — genel tipin karsilastirilabilirligi hem kendi alan yapisina hem T'ye bagli
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

func derlenirMi(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 siniflaKaynak(kaynak string) string {
	if derlenirMi(kaynak) {
		return "derlendi"
	}
	return "derlenmedi"
}

func main() {
	yiginDilimli := `package sinama
type Yigin[T any] struct{ ogeler []T }
func F() bool { var a, b Yigin[int]; return a == b }`

	kutuKarsilastirilabilirT := `package sinama
type Kutu[T any] struct{ Deger T }
func F() bool { var a, b Kutu[int]; return a == b }`

	kutuKarsilastirilamazT := `package sinama
type Kutu[T any] struct{ Deger T }
func F() bool { var a, b Kutu[[]string]; return a == b }`

	fmt.Printf("%-46s %s\n", "durum", "sonuc")
	fmt.Printf("%-46s %s\n", "Yigin[int] == Yigin[int] (dilim alanli)", siniflaKaynak(yiginDilimli))
	fmt.Printf("%-46s %s\n", "Kutu[int] == Kutu[int] (T dogrudan, T=int)", siniflaKaynak(kutuKarsilastirilabilirT))
	fmt.Printf("%-46s %s\n", "Kutu[[]string] == Kutu[[]string] (T=dilim)", siniflaKaynak(kutuKarsilastirilamazT))
}
durum                                          sonuc
Yigin[int] == Yigin[int] (dilim alanli)        derlenmedi
Kutu[int] == Kutu[int] (T dogrudan, T=int)     derlendi
Kutu[[]string] == Kutu[[]string] (T=dilim)     derlenmedi

GN12. Üç satır iki bağımsız etkeni ayırıyor. Yigin[T] hiçbir zaman karşılaştırılamaz — T int olsa bile, çünkü ogeler bir dilim alanı ve Sözlükler ile Diziler ve Dilimler derslerinin (M09/K01) ölçtüğü gibi dilimler == ile karşılaştırılamaz; bu kısıtlama kapsayıcının kendi alan yapısından geliyor, tip parametresinden değil. Kutu[T] farklı bir yapı kurar — Deger alanı Tyi doğrudan tutuyor, dilim değil — ve bu durumda karşılaştırılabilirlik tamamen Tye devrediliyor: Kutu[int] karşılaştırılabiliyor çünkü int karşılaştırılabilir, Kutu[[]string] karşılaştırılamıyor çünkü bu kez Tnin kendisi ([]string) karşılaştırılamaz bir tip. Genel bir tipin karşılaştırılabilirliği bu yüzden tek bir soruya indirgenemez; hem kapsayıcının kendi tasarımına hem doldurulan Tye bakmak gerekiyor.

Bu, kısıtların neden yeterli olmadığını da gösteriyor: Yigin[T any] kısıtını Yigin[T comparable] yapmak Kutu için işe yarasa da Yigin için hiçbir şeyi değiştirmez — T ne kadar karşılaştırılabilir olursa olsun, ogeler bir dilim olduğu sürece Yigin karşılaştırılamaz kalır. Tip kısıtı yalnız Tyi sınırlar, Tyi taşıyan yapının kendi alan düzenini değil.

Bu ayrımın somut bir karşılığı var: Sözlükler dersi (M09/K01) bir sözlüğün anahtar tipinin karşılaştırılabilir olmak zorunda olduğunu ölçmüştü. Yigin[int] hiçbir zaman bir sözlük anahtarı olamaz — T ne olursa olsun, çünkü karşılaştırılamazlık ogeler alanından geliyor ve bu alan her örneklemede aynen duruyor. Kutu[int] ise anahtar olabilir, çünkü Deger alanı inti doğrudan tutuyor ve int karşılaştırılabilir; Kutu[[]string] yine olamaz, çünkü bu kez taşınan Tnin kendisi karşılaştırılamaz. Aynı iki kapsayıcı kalıbı, “bu tip bir sözlük anahtarı olabilir mi” sorusuna kaynaktaki alan yapısına bakılarak önceden yanıt veriyor — çalışma zamanında bir anahtar denemesi yapmaya gerek kalmadan.

Üç ders boyunca genellik hep kaynağın içinde kaldı: tip parametresi ya çıkarıldı ya açıkça yazıldı, ama her iki durumda da kaynağı okuyan biri, doğru yere baktığında, hangi somut tipin kullanıldığını görebildi. Bu, Arayüz Kavramı dersinin ölçtüğü örtük gerçekleştirimin tam tersi bir tavır: orada bir tipin bir arayüzü karşıladığı hiçbir yerde yazmıyordu, burada bir kapsayıcının hangi tiple çalıştığı ya çağrının argümanından ya da bildirimin köşeli parantezinden her zaman okunabiliyor. Yigin[int]in dinamik tipi %Tye sorulmadan da bilinebiliyor, çünkü kaynağın kendisinde yazılı; bir arayüz değerinin dinamik tipi ise yalnız değere sorularak öğrenilebiliyordu. Kursun kapanış dersi bu iki tavrı — hep okunabilir olan ile yalnız değere sorularak öğrenilebilen — aynı işi yaparak karşılaştırır.

Bu okunabilirlik bedelsiz değil: Yigin[int]i okuyan biri Tnin int olduğunu satırın kendisinden öğreniyor, ama bu bilgi başka bir yerde — Genel Fonksiyonlar dersindeki gibi bir argümandan — çıkarılamıyor; yazan tarafın onu yazmayı unutması mümkün değil, çünkü sözdizimi bunu zorunlu kılıyor. Genelliğin kazandırdığı öngörülebilirlik, çağıranın üstlendiği bu küçük bir yazım yüküyle geliyor.

Özet

  • Bir genel veri yapısının tip parametresi, genel fonksiyonun aksine, çağıranın kendi bildiriminde açıkça yazılır — çıkarılmaz.
  • Yigin[int] ile Yigin[string], aynı kaynaktan üretilmiş olsalar bile, çalışma zamanında %T ile okunan iki ayrı dinamik tiptir ve biri diğerinin yerine geçemez.
  • Tip güvenliğinin ölçülebilir karşılığı: aynı işi boş arayüzle yazmak en az bir tip iddiası gerektiriyor, genel yazımda bu sayı sıfır.
  • Bir genel tipin karşılaştırılabilirliği iki bağımsız etkene bağlıdır: kapsayıcının kendi alan yapısı (dilim alanı taşıyorsa hiçbir zaman karşılaştırılamaz) ve — kapsayıcı Tyi doğrudan tutuyorsa — Tnin kendi karşılaştırılabilirliği.
  • Bir tip kısıtı yalnız tip parametresini sınırlar; kapsayıcının kendi alan düzeninden gelen sınırları değiştiremez.

Sonraki Adım

Üç ders boyunca genellik hep tek bir tarafta ölçüldü: bağ ya bildirimde ya kullanım yerinde kuruldu, hiçbir zaman değere girmedi. Arayüz konusu ise tam tersini ölçmüştü — tip bilgisi değerin içinde taşınıyordu ve ancak değere sorularak okunabiliyordu. Kursun son dersi bu iki yazımı, ve aralarında duran arayüz sözleşmesini, aynı işi üç kez yaparak yan yana koyar: hangi yazım kaç tip iddiası gerektiriyor, hangisi derleme zamanında durup hangisi çalışma zamanına kalıyor, ve genellik her zaman kazanıyor mu.

İ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