go
Go dersleri
Go/Tipler ve Soyutlama

Jenerikler (Generics)

Tip parametreleri, kısıtlar, ~ operatörü ve jenerik veri yapıları.

Ders 12 / 2125 dkOrta
Bu derste öğreneceklerin
  • Tip parametreli fonksiyonlar
  • Kısıtlar: any, comparable, cmp.Ordered
  • Kendi kısıtını tanımlama ve ~ operatörü
  • Jenerik tipler: Stack[T]
  • Tip çıkarımı (type inference)
  • Map, Filter ve Reduce örnekleri
  • Jenerikleri ne zaman kullanmamalı

Go uzun yıllar jenerikler olmadan yaşadı. Bu süre boyunca aynı işi yapan onlarca fonksiyon yazıldı: tamsayı dilimlerinde en büyüğü bulan bir fonksiyon, ondalık dilimlerde aynı işi yapan bir başkası, metinlerde bir üçüncüsü. Alternatif, her şeyi boş arayüz olarak alıp çalışma zamanında tip dönüşümü yapmaktı — ki bu da derleyicinin sağladığı tüm güvenceyi kaybetmek demekti.

Go 1.18, bu ikilemi tip parametreleriyle çözdü. Artık bir fonksiyonu ya da tipi, üzerinde çalışacağı tipten bağımsız yazabilir, derleme zamanı güvenliğinden de vazgeçmezsin. Ancak jenerikler Go'ya eklenirken dilin sadelik ilkesi korundu: Tip parametreleri, C++ şablonlarındaki gibi sınırsız bir hesaplama aracı değildir. Bir tip parametresinin hangi işlemleri destekleyeceğini kısıtlarla (constraint) açıkça bildirmen gerekir.

Bu derste tip parametreli fonksiyon yazmayı, hazır kısıtları, kendi kısıtını tanımlamayı ve yaklaşık eleman operatörünü, jenerik tipleri, tip çıkarımını ve klasik dönüşüm fonksiyonlarını öğreneceksin. Aynı derecede önemli bir başlık daha var: jenerikleri ne zaman kullanmaman gerektiği. Go topluluğunda jeneriklerin aşırı kullanımı, kodun okunmasını zorlaştıran en yaygın yeni alışkanlıklardan biri hâline geldi.

İlk jenerik fonksiyon

Tip parametreleri, fonksiyon adından sonra köşeli parantez içinde yazılır. Her tip parametresinin bir adı ve bir kısıtı vardır:

main.go
package main

import "fmt"

// T bir tip parametresidir; kısıtı "any" olduğu için her tip olabilir.
func First[T any](items []T) (T, bool) {
	var zero T // T'nin sıfır değeri
	if len(items) == 0 {
		return zero, false
	}
	return items[0], true
}

// Birden çok tip parametresi
func Pair[K comparable, V any](key K, value V) string {
	return fmt.Sprintf("%v=%v", key, value)
}

func main() {
	nums := []int{10, 20, 30}
	words := []string{"go", "dilim"}

	n, _ := First(nums)  // T = int, çıkarım yapılır
	w, _ := First(words) // T = string
	fmt.Println(n, w)

	_, ok := First([]float64{})
	fmt.Println("boş dilimde bulundu mu:", ok)

	fmt.Println(Pair("yaş", 30))
	fmt.Println(Pair(7, []string{"a", "b"}))
}
Çıktı
10 go
boş dilimde bulundu mu: false
yaş=30
7=[a b]

Dikkat edilecek iki nokta var. Birincisi, var zero T yazımı tip parametresinin sıfır değerini üretir; bir tip parametresi için nil veya 0 yazamazsın çünkü hangisinin geçerli olduğunu bilemezsin. Go 1.21'den beri bunun yerine *new(T) gibi numaralara da gerek yoktur.

İkincisi, çağrı yaparken tip parametresini yazmadık. Derleyici argümanlardan çıkarır. Gerekirse açıkça da yazabilirsin: First[int](nums).

Kısıtlar

Bir tip parametresinin kısıtı, o tiple ne yapabileceğini belirler. Kısıt aslında bir arayüzdür; ama jeneriklerle birlikte arayüzler yeni bir yetenek kazandı: metot listesine ek olarak tip kümesi de tanımlayabilirler.

Üç hazır kısıtla sık karşılaşırsın:

KısıtAnlamıİzin verilen işlemler
anyHer tip (interface{} için takma ad)Atama, kopyalama
comparable== ve != desteklenen tiplerKarşılaştırma, map anahtarı olma
cmp.OrderedSıralanabilir tipler (sayılar, string)<, >, <=, >=
main.go
package main

import (
	"cmp"
	"fmt"
)

// comparable: eşitlik kontrolü yapabiliriz
func Contains[T comparable](items []T, target T) bool {
	for _, v := range items {
		if v == target {
			return true
		}
	}
	return false
}

// cmp.Ordered: büyüklük karşılaştırması yapabiliriz
func Max[T cmp.Ordered](items []T) (T, bool) {
	var best T
	if len(items) == 0 {
		return best, false
	}
	best = items[0]
	for _, v := range items[1:] {
		if v > best {
			best = v
		}
	}
	return best, true
}

// Frekans sayımı: anahtar comparable olmalı ki map'te kullanılabilsin
func Counts[T comparable](items []T) map[T]int {
	result := make(map[T]int, len(items))
	for _, v := range items {
		result[v]++
	}
	return result
}

func main() {
	fmt.Println(Contains([]string{"elma", "armut"}, "armut"))
	fmt.Println(Contains([]int{1, 2, 3}, 9))

	m1, _ := Max([]int{3, 17, 8})
	m2, _ := Max([]string{"zeytin", "armut", "kiraz"})
	fmt.Println(m1, m2)

	counts := Counts([]string{"a", "b", "a", "c", "a"})
	fmt.Println(counts["a"], counts["b"], counts["z"])

	fmt.Println(cmp.Compare(3, 7), cmp.Compare("b", "a"), cmp.Compare(2.5, 2.5))
}
Çıktı
true
false
17 zeytin
3 1 0
-1 1 0

cmp paketi Go 1.21 ile geldi ve cmp.Ordered, cmp.Compare, cmp.Or gibi küçük ama çok kullanışlı araçlar sunar. Ondan önce bu kısıt golang.org/x/exp/constraints paketinden alınıyordu.

Kendi kısıtını tanımlama ve ~ operatörü

Bir kısıt, izin verdiğin tipleri | ile listeleyen bir arayüzdür:

main.go
package main

import "fmt"

// Yalnızca bu dört tipe izin veren bir kısıt
type Number interface {
	~int | ~int64 | ~float64 | ~float32
}

func Sum[T Number](items []T) T {
	var total T
	for _, v := range items {
		total += v
	}
	return total
}

func Average[T Number](items []T) float64 {
	if len(items) == 0 {
		return 0
	}
	return float64(Sum(items)) / float64(len(items))
}

// ~ operatörü sayesinde temel tipi int olan kendi tiplerin de kabul edilir
type Score int
type Temperature float64

func main() {
	fmt.Println(Sum([]int{1, 2, 3, 4}))
	fmt.Printf("%.2f\n", Sum([]float64{1.5, 2.25}))

	scores := []Score{80, 95, 70}
	fmt.Println(Sum(scores), "ortalama:", Average(scores))

	temps := []Temperature{21.5, 23.0, 19.5}
	fmt.Printf("%.2f\n", Average(temps))
}
Çıktı
10
3.75
245 ortalama: 81.66666666666667
21.33

Buradaki ~ işareti kritiktir. int yazarsan yalnızca int tipinin kendisi kabul edilir; type Score int ile tanımlanmış bir tip kabul edilmez, çünkü Score ile int farklı tiplerdir. ~int yazdığında ise "temel tipi int olan bütün tipler" anlamına gelir ve Score de kapsama girer. Kendi kısıtlarını yazarken neredeyse her zaman ~ kullanmak istersin.

Jenerik tipler

Tip parametreleri yalnızca fonksiyonlara değil, tiplere de eklenebilir. Bu, aynı veri yapısını her eleman tipi için yeniden yazma zorunluluğunu ortadan kaldırır:

main.go
package main

import "fmt"

// Stack[T]: her tipten eleman tutabilen bir yığın
type Stack[T any] struct {
	items []T
}

func (s *Stack[T]) Push(v T) {
	s.items = append(s.items, v)
}

func (s *Stack[T]) Pop() (T, bool) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	last := len(s.items) - 1
	v := s.items[last]
	s.items = s.items[:last]
	return v, true
}

func (s *Stack[T]) Len() int { return len(s.items) }

// Jenerik bir eşleme tipi: iki tip parametresi
type Cache[K comparable, V any] struct {
	data map[K]V
}

func NewCache[K comparable, V any]() *Cache[K, V] {
	return &Cache[K, V]{data: make(map[K]V)}
}

func (c *Cache[K, V]) Set(key K, value V) { c.data[key] = value }

func (c *Cache[K, V]) Get(key K) (V, bool) {
	v, ok := c.data[key]
	return v, ok
}

func main() {
	var ints Stack[int]
	ints.Push(1)
	ints.Push(2)
	top, _ := ints.Pop()
	fmt.Println("int yığını:", top, "kalan:", ints.Len())

	var words Stack[string]
	words.Push("merhaba")
	w, _ := words.Pop()
	fmt.Println("string yığını:", w)

	cache := NewCache[string, []int]()
	cache.Set("asal", []int{2, 3, 5, 7})
	if v, ok := cache.Get("asal"); ok {
		fmt.Println("önbellek:", v)
	}
	if _, ok := cache.Get("yok"); !ok {
		fmt.Println("bulunamadı")
	}
}
Çıktı
int yığını: 2 kalan: 1
string yığını: merhaba
önbellek: [2 3 5 7]
bulunamadı

Metot tanımlarken alıcıda tip parametrelerini tekrar yazman gerekir: func (s *Stack[T]) Push(v T). Ayrıca metotlar kendi tip parametrelerini ekleyemez; yalnızca tipin parametrelerini kullanabilirler. Bu, Go'nun bilinçli bir kısıtlamasıdır.

Dikkat: Jenerik tiplerde tip çıkarımı yapılmaz. NewCache[string, []int]() çağrısında tipleri açıkça yazdık, çünkü argüman yoktu. Argümandan çıkarılabiliyorsa yazmana gerek kalmaz.

Tip çıkarımı

Derleyici, çağrı argümanlarına bakarak tip parametrelerini çıkarır. Çıkarım güçlüdür ama sınırları vardır:

main.go
package main

import "fmt"

func Map[T, U any](items []T, fn func(T) U) []U {
	out := make([]U, 0, len(items))
	for _, v := range items {
		out = append(out, fn(v))
	}
	return out
}

func Filter[T any](items []T, keep func(T) bool) []T {
	out := make([]T, 0, len(items))
	for _, v := range items {
		if keep(v) {
			out = append(out, v)
		}
	}
	return out
}

func Reduce[T, A any](items []T, initial A, fn func(A, T) A) A {
	acc := initial
	for _, v := range items {
		acc = fn(acc, v)
	}
	return acc
}

func main() {
	nums := []int{1, 2, 3, 4, 5, 6}

	// T = int çıkarılır, U = string dönüş tipinden çıkarılır
	labels := Map(nums, func(n int) string { return fmt.Sprintf("#%d", n) })
	fmt.Println(labels)

	evens := Filter(nums, func(n int) bool { return n%2 == 0 })
	fmt.Println(evens)

	total := Reduce(nums, 0, func(acc, n int) int { return acc + n })
	fmt.Println("toplam:", total)

	// A = string: başlangıç değerinden çıkarılır
	joined := Reduce(labels, "", func(acc, s string) string {
		if acc == "" {
			return s
		}
		return acc + " " + s
	})
	fmt.Println(joined)
}
Çıktı
[#1 #2 #3 #4 #5 #6]
[2 4 6]
toplam: 21
#1 #2 #3 #4 #5 #6

Çıkarımın çalışmadığı en yaygın durum, tip parametresinin yalnızca dönüş tipinde geçmesidir. func Zero[T any]() T fonksiyonunu çağırırken Zero[int]() yazmak zorundasın; derleyicinin bakabileceği bir argüman yoktur.

Jenerikleri ne zaman kullanmamalı

Jenerikler güçlü bir araçtır, ama Go'da varsayılan çözüm değildir. Topluluk içinde yerleşmiş birkaç ölçüt vardır ve bunları bilmek, gereksiz karmaşıklıktan kaçınmanı sağlar.

Tek bir tiple çalışıyorsan kullanma. Yalnızca tamsayılarla çalışan bir fonksiyonu jenerik yazmanın hiçbir faydası yoktur; okuması zorlaşır, hata mesajları uzar.

Davranış tipe göre değişiyorsa arayüz kullan. Jenerikler, farklı tipler üzerinde aynı kodu çalıştırmak içindir. Her tipin kendine özgü bir davranışı olacaksa doğru araç arayüzlerdir. "Tipe göre dallanma" ihtiyacı duyuyorsan, muhtemelen yanlış aracı seçmişsindir.

Yalnızca iki tip için tekrar varsa acele etme. Aynı fonksiyonu iki kez yazmak, çoğu zaman bir tip parametresi katmanı eklemekten daha ucuzdur. Üçüncü tekrarda jenerikleri düşünmeye başla.

Veri yapıları jeneriklerin doğal alanıdır. Yığın, kuyruk, ağaç, küme gibi kap (container) tipleri jeneriklerden gerçekten kazanır: Tip güvenliği korunur, kod tek yerde toplanır. Standart kütüphanedeki slices ve maps paketleri de bu düşünceyle yazılmıştır.

Okunabilirlik her zaman önceliklidir. İmzada üç tip parametresi ve iki özel kısıt varsa, kodu okuyan kişinin zihinsel yükü katlanır. Böyle bir imza yazdığında bir adım geri çekilip daha sade bir tasarım aramak neredeyse her zaman işe yarar.

Standart kütüphane bu dengeyi iyi örnekler: slices.Sort, slices.Contains, maps.Keys gibi fonksiyonlar jeneriktir çünkü her eleman tipi için aynı iş yapılır; buna karşılık dosya, ağ ve zaman API'lerinde tip parametresi neredeyse hiç görmezsin.

Perde arkası: derleyici ne yapıyor?

Jenerik kod yazarken doğal olarak şu soru akla gelir: Derleyici her tip için ayrı bir kopya mı üretiyor, yoksa tek bir ortak kod mu çalışıyor? Cevap ikisinin arasında bir yerdedir ve Go'nun seçtiği yol, derleme süresi ile çalışma hızı arasında bilinçli bir dengedir.

Bazı diller, her tip için kodun ayrı bir kopyasını üretir. Bu, en hızlı sonucu verir ama üretilen ikili dosya şişer ve derleme süresi uzar. Bazı diller ise her şeyi tek bir ortak kodla çalıştırır ve değerleri kutulayarak dolaştırır; bu da bellek ayırmayı ve dolaylı erişimi artırır. Go, ikisinin ortasında duran bir yaklaşım kullanır: Bellekte aynı şekle sahip tipler aynı makine kodunu paylaşır. Örneğin tüm işaretçi tipleri tek bir kopyayı kullanırken, farklı boyutlardaki sayısal tipler kendi kopyalarını alır.

Bunun pratikte iki sonucu vardır. Birincisi, jenerik bir fonksiyon elle yazılmış özel bir fonksiyondan daha hızlı çalışmaz. Ortak kod paylaşıldığında, derleyicinin tipe özel bazı iyileştirmeleri yapması zorlaşır; bu yüzden çok sıcak döngülerde jenerik sürüm bazen bir miktar yavaş bile olabilir. Yani jenerikleri performans için değil, kod tekrarını azaltmak ve tip güvenliğini korumak için kullanırsın.

İkinci sonuç, hata mesajlarıyla ilgilidir. Kısıtlar sağlanmadığında alacağın mesajlar, sıradan tip hatalarından daha uzun ve daha soyut olur. Bir tip parametresinin neden kabul edilmediğini anlamak, iç içe geçmiş kısıtlarda gerçekten zaman alabilir. Bu da imzaları sade tutmak için ayrı bir gerekçedir.

Son olarak, derleme süresine etkisini aklında tut. Jenerik kod, derleyicinin her çağrı yerinde daha fazla iş yapmasını gerektirir. Küçük projelerde bu fark ölçülemez; ama yüz binlerce satırlık bir kod tabanında, her yere serpiştirilmiş derin jenerik katmanlar derlemeyi fark edilir biçimde yavaşlatabilir. Go'nun en büyük vaatlerinden birinin hızlı derleme olduğunu düşünürsek, bu da tasarım kararlarında bir ölçüt olmayı hak eder.

Sık yapılan hatalar

  • Kısıtta ~ kullanmayı unutmak. int yazarsan kendi tanımladığın type Score int kabul edilmez.
  • Tip parametresi için nil veya 0 yazmak. Sıfır değer için var zero T kullan.
  • Metotlara tip parametresi eklemeye çalışmak. Go bunu desteklemez; parametreyi tipe taşı.
  • Tip kümeli arayüzü değişken tipi olarak kullanmak. Bunlar yalnızca kısıt olabilir.
  • Her yerde jenerik kullanmak. Tek tiple çalışan koda tip parametresi eklemek yalnızca gürültü üretir.
  • Çıkarımın her zaman çalışacağını varsaymak. Tip yalnızca dönüşte geçiyorsa açıkça yazman gerekir.
  • Jeneriklerin performans sihri sandığını sanmak. Derleyici bazı durumlarda tek bir ortak kod üretir; jenerik kod elle yazılmış koddan hızlı olmaz.

Standart kütüphanedeki jenerikler

Jenerikler geldikten sonra standart kütüphaneye iki yeni paket eklendi ve bunlar günlük kodda en çok kullanacağın jenerik araçlardır.

Dilimlerle çalışan paket, arama, sıralama, karşılaştırma, kopyalama, ters çevirme, tekrar edenleri ayıklama gibi işleri hazır sunar. Bunların hepsini elle yazabilirsin, ama hazır olanları kullanmanın üç avantajı vardır: niyetin okuyana anında belli olur, sınır durumları (boş dilim, tek elemanlı dilim) zaten düşünülmüştür ve uygulamalar özenle iyileştirilmiştir. Sıralama fonksiyonları, örneğin, küçük dilimlerde farklı bir algoritmaya geçen melez bir yaklaşım kullanır.

Haritalarla çalışan paket ise anahtarları veya değerleri gezmek, bir haritayı klonlamak, iki haritayı birleştirmek gibi işleri kapsar. Burada bir ayrıntıya dikkat etmelisin: Anahtarları döndüren fonksiyon bir dilim değil, bir iteratör üretir. Sıralı bir dilim istiyorsan bunu sıralama yardımcılarıyla birleştirirsin. İteratörlerin ne olduğunu ve nasıl çalıştığını İteratörler dersinde ayrıntısıyla göreceksin.

Üçüncü bir paket, karşılaştırma işlemleri için küçük yardımcılar sunar. Sıralanabilir tipler için bir kısıt, iki değeri karşılaştırıp negatif, sıfır ya da pozitif döndüren bir fonksiyon ve sıfır değer olmayan ilk argümanı seçen bir yardımcı içerir. Bu sonuncusu, varsayılan değer zincirleri kurarken şaşırtıcı derecede işe yarar.

Bu paketleri incelemek, iyi jenerik tasarımın nasıl göründüğünü öğrenmenin en hızlı yoludur. Dikkat edeceğin şey şudur: İmzalar neredeyse her zaman tek bir tip parametresi içerir, kısıtlar mümkün olduğunca gevşek tutulur ve fonksiyonlar tek bir iş yapar. Kendi jenerik kodunu yazarken bu üç özelliği hedef alırsan, sonuç büyük olasılıkla okunabilir olur.

Alıştırmalar

Alıştırma·Küme tipi
Kolay

Set[T comparable] adında bir jenerik küme tipi yaz. Add, Has, Remove ve Len metotları olsun. Altında bir map[T]struct{} kullan ve neden bool yerine struct{} seçtiğini açıkla.

İpucu

map[T]struct{} değer için hiç bellek ayırmaz; yalnızca anahtarın varlığı önemlidir.

Çözümü göster
main.go
package main

import "fmt"

type Set[T comparable] struct {
	items map[T]struct{}
}

func NewSet[T comparable](values ...T) *Set[T] {
	s := &Set[T]{items: make(map[T]struct{}, len(values))}
	for _, v := range values {
		s.Add(v)
	}
	return s
}

func (s *Set[T]) Add(v T)      { s.items[v] = struct{}{} }
func (s *Set[T]) Remove(v T)   { delete(s.items, v) }
func (s *Set[T]) Has(v T) bool { _, ok := s.items[v]; return ok }
func (s *Set[T]) Len() int     { return len(s.items) }

func main() {
	fruits := NewSet("elma", "armut", "elma") // tekrar eden eleman bir kez tutulur
	fmt.Println("uzunluk:", fruits.Len())
	fmt.Println("elma var mı:", fruits.Has("elma"))

	fruits.Remove("elma")
	fmt.Println("silindikten sonra:", fruits.Len(), fruits.Has("elma"))

	ids := NewSet(3, 7, 3, 9)
	fmt.Println("sayı kümesi:", ids.Len(), ids.Has(7), ids.Has(4))
}
Çıktı
uzunluk: 2
elma var mı: true
silindikten sonra: 1 false
sayı kümesi: 3 true false

struct{} boyutu sıfır bayt olan bir tiptir; milyonlarca elemanlı kümelerde bool yerine onu kullanmak gözle görülür bellek kazandırır. comparable kısıtı zorunludur, çünkü map anahtarı olabilmesi için eleman tipinin == ile karşılaştırılabilmesi gerekir.

Alıştırma·GroupBy
Orta

Bir dilimi, her elemandan hesaplanan bir anahtara göre gruplayan GroupBy fonksiyonu yaz. İmzası func GroupBy[T any, K comparable](items []T, key func(T) K) map[K][]T olsun. Kelimeleri uzunluklarına, sayıları tek/çift olmalarına göre grupla ve deterministik çıktı için anahtarları sırala.

İpucu

Map gezinme sırası rastgeledir; slices.Sorted(maps.Keys(m)) ile sıralı yazdır.

Çözümü göster
main.go
package main

import (
	"fmt"
	"maps"
	"slices"
)

func GroupBy[T any, K comparable](items []T, key func(T) K) map[K][]T {
	groups := make(map[K][]T)
	for _, item := range items {
		k := key(item)
		groups[k] = append(groups[k], item)
	}
	return groups
}

func main() {
	words := []string{"go", "dilim", "map", "harita", "tip", "jenerik"}

	byLength := GroupBy(words, func(s string) int { return len([]rune(s)) })
	for _, k := range slices.Sorted(maps.Keys(byLength)) {
		fmt.Printf("%d harf: %v\n", k, byLength[k])
	}

	nums := []int{1, 2, 3, 4, 5, 6, 7}
	byParity := GroupBy(nums, func(n int) string {
		if n%2 == 0 {
			return "çift"
		}
		return "tek"
	})
	for _, k := range slices.Sorted(maps.Keys(byParity)) {
		fmt.Printf("%s: %v\n", k, byParity[k])
	}
}
Çıktı
2 harf: [go]
3 harf: [map tip]
5 harf: [dilim]
6 harf: [harita]
7 harf: [jenerik]
tek: [1 3 5 7]
çift: [2 4 6]

T için any yeterlidir çünkü elemanlara yalnızca dokunmadan kopyalıyoruz; K ise map anahtarı olacağı için comparable olmalıdır. append, nil bir dilimle de çalıştığı için map'e önce boş dilim koymaya gerek yoktur — Go'da sıfır değerin kullanışlı olmasının bir örneği daha.

Alıştırma·Jenerik ikili arama ağacı
Zor

Tree[T cmp.Ordered] adında bir ikili arama ağacı yaz: Insert, Contains ve sıralı gezinme yapan InOrder metotları olsun. InOrder bir dilim döndürsün. Sayılarla ve metinlerle test et.

İpucu

Düğüm için type node[T cmp.Ordered] struct \{ value T; left, right *node[T] \} kullan. Ekleme işlemini özyinelemeli bir yardımcı fonksiyonla yap.

Çözümü göster
main.go
package main

import (
	"cmp"
	"fmt"
)

type node[T cmp.Ordered] struct {
	value       T
	left, right *node[T]
}

type Tree[T cmp.Ordered] struct {
	root *node[T]
	size int
}

func (t *Tree[T]) Insert(v T) {
	inserted := false
	t.root, inserted = insert(t.root, v)
	if inserted {
		t.size++
	}
}

func insert[T cmp.Ordered](n *node[T], v T) (*node[T], bool) {
	if n == nil {
		return &node[T]{value: v}, true
	}
	switch {
	case v < n.value:
		var ok bool
		n.left, ok = insert(n.left, v)
		return n, ok
	case v > n.value:
		var ok bool
		n.right, ok = insert(n.right, v)
		return n, ok
	default:
		return n, false // zaten var
	}
}

func (t *Tree[T]) Contains(v T) bool {
	n := t.root
	for n != nil {
		switch {
		case v < n.value:
			n = n.left
		case v > n.value:
			n = n.right
		default:
			return true
		}
	}
	return false
}

func (t *Tree[T]) InOrder() []T {
	out := make([]T, 0, t.size)
	var walk func(*node[T])
	walk = func(n *node[T]) {
		if n == nil {
			return
		}
		walk(n.left)
		out = append(out, n.value)
		walk(n.right)
	}
	walk(t.root)
	return out
}

func main() {
	var nums Tree[int]
	for _, v := range []int{50, 30, 70, 20, 40, 60, 80, 30} {
		nums.Insert(v)
	}
	fmt.Println("sıralı:", nums.InOrder())
	fmt.Println("boyut:", nums.size)
	fmt.Println("40 var mı:", nums.Contains(40), "| 45 var mı:", nums.Contains(45))

	var words Tree[string]
	for _, w := range []string{"kiraz", "armut", "zeytin", "elma"} {
		words.Insert(w)
	}
	fmt.Println("kelimeler:", words.InOrder())
}
Çıktı
sıralı: [20 30 40 50 60 70 80]
boyut: 7
40 var mı: true | 45 var mı: false
kelimeler: [armut elma kiraz zeytin]
ZamanO(log n) ortalama, O(n) en kötüAlanO(n)

Ağaç dengeli kaldığı sürece ekleme ve arama O(log n) sürer; sıralı veri eklenirse ağaç bağlı listeye dönüşür ve O(n)'e düşer. Bu sorunun çözümü olan dengelemeyi AVL Ağaçları dersinde göreceksin. Jenerikler burada gerçek değerini gösteriyor: Aynı ağaç hem sayılarla hem metinlerle, tek satır kod tekrarı olmadan çalışıyor.

Kısa sınav

Kısa sınav

~int kısıtı ile int kısıtı arasındaki fark nedir?

Bir tip parametresi T için sıfır değer nasıl elde edilir?

Hangi kısıt bir tipin map anahtarı olarak kullanılabilmesini garanti eder?

Jenerik bir tipin metotları kendi tip parametrelerini tanımlayabilir mi?

func Zero[T any]() T fonksiyonu neden Zero() diye çağrılamaz?

Aşağıdaki durumlardan hangisinde jenerik yazmak doğru tercihtir?

Özet

  • Tip parametreleri köşeli parantez içinde yazılır: func F[T any](...). Her parametrenin bir kısıtı vardır.
  • Kısıt, tip parametresiyle hangi işlemleri yapabileceğini belirler: any, comparable, cmp.Ordered en sık kullanılanlardır.
  • Kendi kısıtını tip kümesiyle tanımlarsın; ~ operatörü temel tipi eşleşen kendi tiplerini de kapsar.
  • var zero T bir tip parametresinin sıfır değerini verir.
  • Jenerik tipler kap veri yapıları için idealdir; metot alıcılarında tip parametreleri tekrarlanır ve metotlar yeni parametre ekleyemez.
  • Tip çıkarımı argümanlardan yapılır; tip yalnızca dönüşte geçiyorsa açıkça yazılmalıdır.
  • Map, Filter, Reduce gibi dönüşümler jeneriklerle tek kez yazılabilir; slices ve maps paketleri bunların çoğunu hazır sunar.
  • Jenerikleri tekrar gerçekten ortaya çıktığında kullan; tipe göre değişen davranış için arayüzler doğru araçtır.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.