Jenerik
Jenerik veya daha akademik adı ile Parametreli Polimorfizm (Parameterized Polymorphism), kodun yeniden kullanımı ve esnekliği için tür parametreleme yoluyla gerçekleştirilir. Birçok programlama dilinde, parametreli polimorfizm önemli bir kavramdır. Fonksiyon veya veri yapılarının her tür için ayrı kod yazmadan farklı türlerde veri işleyebilmesini sağlar. İlk Go'da jenerik diye bir şey yoktu. Ancak doğuşundan beri, topluluğun Go için en yüksek sesli talebi jenerik eklenmesiydi. Sonunda Go dili 2022'de 1.18 sürümünde jenerik desteği ekledi.
Tasarım
Go dili başlangıçta jenerik tasarlarken, aşağıdaki planları düşündü
- stenciling: Monomorfizasyon, tipik olarak C++, Rust gibi dillerde görülür. Kullanılan her tür için bir şablon kodu oluşturur. Bu en iyi performansa sahiptir, çalışma zamanı maliyeti tamamen yoktur. Performans doğrudan çağırmaya eşittir. Dezavantajı derleme hızını büyük ölçüde yavaşlatmasıdır (Go'nun kendisine kıyasla). Aynı zamanda her tür için kod oluşturulduğundan, derlenen ikili dosya boyutunun şişmesine neden olur.
- dictionaries: Sadece bir kod kümesi oluşturur. Aynı zamanda derleme zamanında tüm kullanılacak tür bilgilerini depolayan bir tür sözlüğü salt okunur veri bölümünde oluşturur. Fonksiyon çağrılırken, sözlüğe göre tür bilgilerini sorgular. Bu yöntem derleme hızını yavaşlatmaz, boyut şişmesine neden olmaz. Ancak büyük çalışma zamanı maliyetine neden olur, jenerik performansı çok düşüktür.
Yukarıdaki iki yöntem iki aşırı uç temsil eder. Go dilinin nihayetinde seçtiği uygulama planı Gcshape stenciling'dir. Bu bir uzlaşma seçimidir. Aynı bellek şekline sahip türler için (şekil bellek ayırıcı tarafından belirlenir) monomorfizasyon kullanılır. Onlar için aynı kodu oluşturur. Örneğin type Int int ile int aslında aynı türdür, bu nedenle bir kod kümesini paylaşır. Ancak işaretçiler için, tüm işaretçi türleri aynı bellek şekline sahip olsa da, örneğin *int, *Person aynı bellek şeklidir, ancak bir kod kümesini paylaşamazlar. Çünkü referans kaldırma sırasında hedef tür bellek düzeni tamamen farklıdır. Bu nedenle, Go aynı zamanda çalışma zamanında tür bilgilerini almak için sözlük kullanır. Bu nedenle Go'nun jeneriği de çalışma zamanı maliyetine sahiptir.
Giriş
Önce basit bir örneğe bakalım.
func Sum(a, b int) int {
return a + b
}Bu çok basit bir fonksiyondur. İki int türü tamsayıyı toplar ve sonucu döndürür. Eğer iki float64 türü kayan nokta sayısı toplamak istenirse, açıkçası bu mümkün değildir. Çünkü tür eşleşmez. Bir çözüm başka bir fonksiyon tanımlamaktır. Aşağıdaki gibi
func SumFloat64(a, b float64) float64 {
return a + b
}Sorun şu ki, bir matematik araç paketi geliştirilirse, tüm sayı türleri için iki sayı toplamı hesaplanırsa, her tür için bir fonksiyon mu yazılmalı? Açıkçası bu mümkün değil. Veya any türü + yansıma kullanılarak判断 edilebilir. Aşağıdaki gibi
func SumAny(a, b any) (any, error) {
tA, tB := reflect.ValueOf(a), reflect.ValueOf(b)
if tA.Kind() != tB.Kind() {
return nil, errors.New("disMatch type")
}
switch tA.Kind() {
case reflect.Int:
case reflect.Int32:
...
}
}Ancak bu şekilde yazmak zahmetli ve performans düşüktür. Ancak Sum fonksiyonunun mantığı tamamen aynıdır, sadece iki sayıyı toplamak而已. İşte bu sırada jenerik kullanılması gerekir. Bu nedenle neden jeneriğe ihtiyaç vardır, jenerik tür ile ilgisiz yürütme mantığını çözmek içindir. Bu tür sorunlar verilen türün ne olduğuyla ilgilenmez, sadece karşılık gelen işlemi tamamlaması yeterlidir.
Sözdizimi
Jenerik yazımı şu şekildedir
func Sum[T int | float64](a, b T) T {
return a + b
}Tür Parametresi: T bir tür parametresidir. Somut parametre ne türde olursa olsun, iletilen türe bağlıdır
Tür Kısıtlaması: int | float64 bir tür kısıtlaması oluşturur. Bu tür kısıtlaması hangi türlerin izin verildiğini belirtir. Tür parametresinin tür aralığını kısıtlar
Tür Argümanı: Sum[int](1,2), manuel olarak int türü belirtilir, int tür argümanıdır.
İlk kullanım, açıkça hangi türün kullanılacağını belirtir. Aşağıdaki gibi
Sum[int](2012, 2022)İkinci kullanım, tür belirtilmez, derleyicinin kendisinin çıkarmasına izin verilir. Aşağıdaki gibi
Sum(3.1415926, 1.114514)Bu bir jenerik slice'dır. Tür kısıtlaması int | int32 | int64'dır
type GenericSlice[T int | int32 | int64] []TBurada kullanım sırasında tür argümanı atlanamaz
GenericSlice[int]{1, 2, 3}Bu bir jenerik hash tablosudur. Anahtar türü karşılaştırılabilir olmalıdır. Bu nedenle comparable arayüzü kullanılır. Değer tür kısıtlaması V int | string | byte'dır
type GenericMap[K comparable, V int | string | byte] map[K]VKullanım
gmap1 := GenericMap[int, string]{1: "hello world"}
gmap2 := make(GenericMap[string, byte], 0)Bu bir jenerik yapıdır. Tür kısıtlaması T int | string'dir
type GenericStruct[T int | string] struct {
Name string
Id T
}Kullanım
GenericStruct[int]{
Name: "jack",
Id: 1024,
}
GenericStruct[string]{
Name: "Mike",
Id: "1024",
}Bu bir jenerik slice parametresi örneğidir
type Company[T int | string, S []T] struct {
Name string
Id T
Stuff S
}
//Aşağıdaki gibi de olabilir
type Company[T int | string, S []int | []string] struct {
Name string
Id T
Stuff S
}Kullanım
Company[int, []int]{
Name: "lili",
Id: 1,
Stuff: []int{1},
}::: ipucu
Jenerik yapıda, bu yazım daha çok önerilir
type Company[T int | string, S int | string] struct {
Name string
Id T
Stuff []S
}:::
SayAble bir jenerik arayüzdür. Person bu arayüzü uygular.
type SayAble[T int | string] interface {
Say() T
}
type Person[T int | string] struct {
msg T
}
func (p Person[T]) Say() T {
return p.msg
}
func main() {
var s SayAble[string]
s = Person[string]{"hello world"}
fmt.Println(s.Say())
}Jenerik Arayüz
Jenerik arayüz daha iyi soyut kısıtlama yeteneği sağlayabilir. Aşağıda bir örnek var
func PrintObj[T fmt.Stringer](s T) {
fmt.Println(s.String())
}
type Person struct {
Name string
}
func (p Person) String() string {
return fmt.Sprintf("Person: %s", p.Name)
}
func main() {
PrintObj(Person{Name: "Alice"})
}Jenerik olmayan arayüzü jeneriğin tür parametresi olarak da kullanabilirsiniz
func Write[W io.Writer](w W, bs []byte) (int, error) {
return w.Write(bs)
}Jenerik Tür Assertion
any türü üzerinde tür assertion yapmak için jenerik kullanabiliriz. Aşağıdaki fonksiyon tüm türleri assertion yapabilir.
func Assert[T any](v any) (bool, T) {
var av T
if v == nil {
return false, av
}
av, ok := v.(T)
return ok, av
}Tür Kümesi
1.18'den sonra, arayüz tanımı tür kümesi (type set) olarak değiştirildi. Tür kümesi içeren arayüzlere General interfaces yani genel arayüzler denir.
An interface type defines a type set
Tür kümesi sadece jenerikteki tür kısıtlaması için kullanılabilir. Tür bildirimi, tür dönüşümü, tür assertion için kullanılamaz. Tür kümesi bir küme olarak, boş küme, birleşim, kesişim vardır. Sonraki bölümlerde bu üç durum anlatılacaktır.
Birleşim
Arayüz türü SignedInt bir tür kümesidir. İşaretli tamsayı türlerinin birleşimi SignedInt'dir. Tersine SignedInt onların üst kümesidir.
type SignedInt interface {
int8 | int16 | int | int32 | int64
}Temel veri türleri böyle, diğer genel arayüzler için de aynıdır
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type UnSignedInt interface {
uint8 | uint16 | uint32 | uint64
}
type Integer interface {
SignedInt | UnSignedInt
}Kesişim
Boş olmayan arayüzün tür kümesi tüm elemanlarının tür kümelerinin kesişimidir. İnsan diline çevirirsek: Eğer bir arayüz birden fazla boş olmayan tür kümesi içeriyorsa, o zaman bu arayüz bu tür kümelerinin kesişimidir. Örnek aşağıdaki gibidir
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type Integer interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}
type Number interface {
SignedInt
Integer
}Örnekteki kesişim kesinlikle SignedInt'tir.
func Do[T Number](n T) T {
return n
}
Do[int](2)
DO[uint](2) // Derleme başarısızBoş Küme
Boş küme kesişim olmamasıdır. Örnek aşağıdaki gibidir. Aşağıdaki örnekteki Integer bir tür boş kümesidir.
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type UnsignedInt interface {
uint8 | uint16 | uint | uint32 | uint64
}
type Integer interface {
SignedInt
UnsignedInt
}Çünkü işaretsiz tamsayı ve işaretli tamsayı iki kümenin kesinlikle kesişimi yoktur. Bu nedenle kesişim boş kümedir. Aşağıdaki örnekte hangi tür iletilirse iletilsin derleme başarısız olur.
Do[Integer](1)
Do[Integer](-100)Boş Arayüz
Boş arayüz boş küme ile aynı değildir. Boş arayüz tüm tür kümelerinin kümesidir. Yani tüm türleri içerir.
func Do[T interface{}](n T) T {
return n
}
func main() {
Do[struct{}](struct{}{})
Do[any]("abc")
}Ancak genellikle any jenerik parametresi olarak kullanırız. Çünkü interface{} güzel görünmüyor.
Alt Tür
type anahtar kelimesi kullanılarak yeni bir tür bildirildiğinde, alt türü tür kümesinde olsa bile, iletilirken hala derleme başarısız olur.
type Int interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}
type TinyInt int8
func Do[T Int](n T) T {
return n
}
func main() {
Do[TinyInt](1) // Derleme başarısız, alt türü Int tür kümesi aralığında olsa bile
}İki çözüm vardır. Birincisi tür kümesine bu türü birleştirmektir. Ancak bu anlamsızdır. Çünkü TinyInt ile int8 alt türü tutarlıdır. Bu nedenle ikinci çözüm vardır.
type Int interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64 | TinyInt
}~ sembolü kullanılarak alt tür belirtilir. Eğer bir türün alt türü bu tür kümesine aitse, o zaman bu tür bu tür kümesine aittir. Aşağıda gösterildiği gibi
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64
}Değiştirildikten sonra derleme başarılı olur.
func main() {
Do[TinyInt](1) // Derleme başarılı, çünkü TinyInt Int tür kümesinde
}Dikkat Noktaları
Jenerik bir türün temel türü olarak kullanılamaz
Aşağıdaki yazım yanlıştır. Jenerik parametresi T temel tür olarak kullanılamaz
type GenericType[T int | int32 | int64] TAşağıdaki yazım izin verilse de, anlamsızdır ve sayı taşması sorununa neden olabilir. Bu nedenle önerilmez
type GenericType[T int | int32 | int64] intJenerik tür tür assertion kullanamaz
Jenerik tür üzerinde tür assertion kullanmak derleme başarısız olur. Jeneriğin çözmek istediği sorun tür ile ilgisiz'dir. Eğer bir sorun farklı türlere göre farklı mantık yapmayı gerektiriyorsa, o zaman kesinlikle jenerik kullanılmamalıdır. interface{} veya any kullanılmalıdır.
func Sum[T int | float64](a, b T) T {
ints,ok := a.(int) // İzin verilmez
switch a.(type) { // İzin verilmez
case int:
case bool:
...
}
return a + b
}Anonim yapı jeneriği desteklemez
Anonim yapılar jeneriği desteklemez. Aşağıdaki kod derleme başarısız olur
testStruct := struct[T int | string] {
Name string
Id T
}[int]{
Name: "jack",
Id: 1
}Anonim fonksiyon özel jeneriği desteklemez
Aşağıdaki iki yazım da derleme başarısız olur
var sum[T int | string] func (a, b T) T
sum := func[T int | string](a,b T) T{
...
}Ancak mevcut jenerik türü kullanabilir. Örneğin kapamada
func Sum[T int | float64](a, b T) T {
sub := func(c, d T) T {
return c - d
}
return sub(a,b) + a + b
}Jenerik yöntem desteklemez
Yöntemler jenerik parametreye sahip olamaz. Ancak receiver jenerik parametreye sahip olabilir. Aşağıdaki kod derleme başarısız olur
type GenericStruct[T int | string] struct {
Name string
Id T
}
func (g GenericStruct[T]) name[S int | float64](a S) S {
return a
}Tür kümesi tür argümanı olarak kullanılamaz
Tür kümesi içeren herhangi bir arayüz, tür argümanı olarak kullanılamaz.
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
func Do[T SignedInt](n T) T {
return n
}
func main() {
Do[SignedInt](1) // Derleme başarısız
}Tür kümesinin kesişim sorunu
Arayüz olmayan türler için, tür birleşiminde kesişim olamaz. Aşağıdaki örnekte TinyInt ile ~int8 kesişimi vardır.
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // Derleme başarısız
}
type TinyInt int8Ancak arayüz türü için, kesişime izin verilir. Aşağıdaki örnek gibi
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // Derleme başarılı
}
type TinyInt interface {
int8
}Tür kümesi doğrudan veya dolaylı olarak kendini birleştiremez
Aşağıdaki örnekte, Floats doğrudan kendini birleştirir. Double da Floats'ı birleştirir. Bu nedenle dolaylı olarak kendini birleştirir.
type Floats interface { // Kod derleme başarısız
Floats | Double
}
type Double interface {
Floats
}comparable arayüzü tür kümesine birleştirilemez
Aynı şekilde, tür kısıtlamasına da birleştirilemez. Bu nedenle temelde ayrı kullanılır.
func Do[T comparable | Integer](n T) T { // Derleme başarısız
return n
}
type Number interface { // Derleme başarısız
Integer | comparable
}
type Comparable interface { // Derleme başarılı ancak anlamsız
comparable
}Yöntem kümesi tür kümesine birleştirilemez
Yöntem içeren herhangi bir arayüz, tür kümesine birleştirilemez
type I interface {
int | fmt.Stringer // cannot use fmt.Stringer in union (fmt.Stringer contains methods)
}Ancak kesişim yapabilirler. Ancak bunu yapmak anlamsızdır
type I interface {
int
fmt.Stringer
}Kullanım
Veri yapısı jeneriğin en yaygın kullanım senaryosudur. Aşağıda iki veri yapısı kullanarak jeneriğin nasıl kullanılacağını göstereceğiz.
Kuyruk
Aşağıda jenerik kullanarak basit bir kuyruk uygulaması. Önce kuyruk türünü bildiriyoruz. Kuyruk eleman türü herhangi bir tür olabilir. Bu nedenle tür kısıtlaması any'dir
type Queue[T any] []TToplamda dört yöntem var: Pop, Peek, Push, Size. Kod aşağıdaki gibidir.
type Queue[T any] []T
func (q *Queue[T]) Push(e T) {
*q = append(*q, e)
}
func (q *Queue[T]) Pop(e T) (_ T) {
if q.Size() > 0 {
res := q.Peek()
*q = (*q)[1:]
return res
}
return
}
func (q *Queue[T]) Peek() (_ T) {
if q.Size() > 0 {
return (*q)[0]
}
return
}
func (q *Queue[T]) Size() int {
return len(*q)
}Pop ve Peek yöntemlerinde, dönüş değerinin _ T olduğunu görebilirsiniz. Bu isimli dönüş değerinin kullanımıdır. Ancak alt çizgi _ kullanılarak anonim olduğu belirtilir. Bu gereksiz değildir, jenerik sıfır değerini belirtmek içindir. Jenerik kullanıldığından, kuyruk boş olduğunda, sıfır değer döndürmek gerekir. Ancak tür bilinmediğinden, somut tür döndürülemez. Yukarıdaki yöntemle jenerik sıfır değeri döndürülebilir. Jenerik değişken bildirerek sıfır değer sorununu çözebilirsiniz. Bir jenerik değişken için, varsayılan değeri bu türün sıfır değeridir. Aşağıdaki gibi
func (q *Queue[T]) Pop(e T) T {
var res T
if q.Size() > 0 {
res = q.Peek()
*q = (*q)[1:]
return res
}
return res
}Yığın (Heap)
Yukarıdaki kuyruk örneğinde, elemanlar üzerinde hiçbir gereksinim olmadığı için tür kısıtlaması any'dir. Ancak yığın farklıdır. Yığın özel bir veri yapısıdır. O(1)时间内 maksimum veya minimum değeri判断 edebilir. Bu nedenle elemanlar üzerinde bir gereksinim vardır: Sıralanabilir tür olmalıdır. Ancak yerleşik sıralanabilir türler sadece sayılar ve string'lerdir. Bu nedenle yığın başlatılırken, özel bir karşılaştırıcı iletilmelidir. Karşılaştırıcı çağrılan tarafından sağlanır ve karşılaştırıcı da jenerik kullanmalıdır. Aşağıdaki gibi
type Comparator[T any] func(a, b T) intAşağıda basit bir ikili yığın uygulaması var. Önce jenerik yapıyı bildiriyoruz. Hala any kullanarak kısıtlıyoruz. Bu şekilde herhangi bir türü depolayabiliriz
type Comparator[T any] func(a, b T) int
type BinaryHeap[T any] struct {
s []T
c Comparator[T]
}Birkaç yöntem uygulaması
func (heap *BinaryHeap[T]) Peek() (_ T) {
if heap.Size() > 0 {
return heap.s[0]
}
return
}
func (heap *BinaryHeap[T]) Pop() (_ T) {
size := heap.Size()
if size > 0 {
res := heap.s[0]
heap.s[0], heap.s[size-1] = heap.s[size-1], heap.s[0]
heap.s = heap.s[:size-1]
heap.down(0)
return res
}
return
}
func (heap *BinaryHeap[T]) Push(e T) {
heap.s = append(heap.s, e)
heap.up(heap.Size() - 1)
}
func (heap *BinaryHeap[T]) up(i int) {
if heap.Size() == 0 || i < 0 || i >= heap.Size() {
return
}
for parentIndex := i>>1 - 1; parentIndex >= 0; parentIndex = i>>1 - 1 {
// Büyük veya eşit
if heap.compare(heap.s[i], heap.s[parentIndex]) >= 0 {
break
}
heap.s[i], heap.s[parentIndex] = heap.s[parentIndex], heap.s[i]
i = parentIndex
}
}
func (heap *BinaryHeap[T]) down(i int) {
if heap.Size() == 0 || i < 0 || i >= heap.Size() {
return
}
size := heap.Size()
for lsonIndex := i<<1 + 1; lsonIndex < size; lsonIndex = i<<1 + 1 {
rsonIndex := lsonIndex + 1
if rsonIndex < size && heap.compare(heap.s[rsonIndex], heap.s[lsonIndex]) < 0 {
lsonIndex = rsonIndex
}
// Küçük veya eşit
if heap.compare(heap.s[i], heap.s[lsonIndex]) <= 0 {
break
}
heap.s[i], heap.s[lsonIndex] = heap.s[lsonIndex], heap.s[i]
i = lsonIndex
}
}
func (heap *BinaryHeap[T]) Size() int {
return len(heap.s)
}
func NewHeap[T any](n int, c Comparator[T]) BinaryHeap[T] {
var heap BinaryHeap[T]
heap.s = make([]T, 0, n)
heap.Comparator = c
return heap
}Kullanım aşağıdaki gibidir
type Person struct {
Age int
Name string
}
func main() {
heap := NewHeap[Person](10, func(a, b Person) int {
return cmp.Compare(a.Age, b.Age)
})
heap.Push(Person{Age: 10, Name: "John"})
heap.Push(Person{Age: 18, Name: "mike"})
heap.Push(Person{Age: 9, Name: "lili"})
heap.Push(Person{Age: 32, Name: "miki"})
fmt.Println(heap.Peek())
fmt.Println(heap.Pop())
fmt.Println(heap.Peek())
}Çıktı
{9 lili}
{9 lili}
{10 John}Jenerik desteği ile, orijinal olarak sıralanamayan türler karşılaştırıcı iletildikten sonra yığın kullanabilir. Bu kesinlikle önce interface{} kullanarak tür dönüşümü ve assertion yapmaktan daha zarif ve rahattır.
Nesne Havuzu
Orijinal nesne havuzu sadece any türü kullanabilir. Her seferinde alındığında tür assertion yapılmalıdır. Jenerik ile basitçe dönüştürüldükten sonra, bu işten kurtulunabilir.
package main
import (
"bytes"
"fmt"
"sync"
)
func NewPool[T any](newFn func() T) *Pool[T] {
return &Pool[T]{
pool: &sync.Pool{
New: func() interface{} {
return newFn()
},
},
}
}
type Pool[T any] struct {
pool *sync.Pool
}
func (p *Pool[T]) Put(v T) {
p.pool.Put(v)
}
func (p *Pool[T]) Get() T {
var v T
get := p.pool.Get()
if get != nil {
v, _ = get.(T)
}
return v
}
func main() {
bufferPool := NewPool(func() *bytes.Buffer {
return bytes.NewBuffer(nil)
})
for range 100 {
buffer := bufferPool.Get()
buffer.WriteString("Hello, World!")
fmt.Println(buffer.String())
buffer.Reset()
bufferPool.Put(buffer)
}
}Özet
Go'nun büyük özelliklerinden biri derleme hızının çok hızlı olmasıdır. Derleme hızlıdır çünkü derleme zamanında yapılan optimizasyonlar azdır. Jenerik eklenmesi derleyicinin iş yükünü artırır, iş daha karmaşık hale gelir. Bu kaçınılmaz olarak derleme hızının yavaşlamasına neden olur. Aslında go1.18 jenerik çıkardığında gerçekten derlemeyi daha yavaş yaptı. Go ekibi hem jenerik eklemek istiyor hem de derleme hızını çok yavaşlatmak istemiyor. Geliştirici rahat kullanırsa, derleyici zorlanır. Tersine derleyici rahatlar (en rahatsız edici tabii ki jenerik olmaması), geliştirici zorlanır. Şimdiki jenerik bu iki taraf arasında uzlaşma ürünüdür.
