1. Niyə generics lazımdır və niyə bu sehr deyil
İlk dəfə func First[T any](xs []T) ... kimi kod gördükdə, beyin çox vaxt bunu sanki lövhəyə təbaşirlə yazılmış Wi-Fi parolu kimi qəbul edir: hərf-lər tanışdır, amma fotoya çəkmək və getmək istəyirsən. Bu normaldır. Go-da generics elə qurulub ki, mümkün qədər “yerə yaxın” olsun: bu, bir alqoritmi təsvir etməyin yoludur, hansı ki müxtəlif tiplər üçün eyni cür işləyir və buna baxmayaraq kompilyator tərəfindən yoxlanıla bilir.
Formal olaraq tip parametri — gələcək konkret tip üçün “boş yer”dir: kompilyasiya mərhələsində kompilyator onun yerinə tip arqumentini yerləşdirəcək və sonra hər şeyin düzgün olduğunu yoxlayacaq. Bu, dəyişənə bənzəyir, amma dəyərlər arasında yox, tiplər arasında yaşayır: “tip dəyişəni” kompilyasiya zamanı məlum olur.
Təsəvvür edin ki, siz artıq bir-birinə çox oxşayan iki funksiyanı yazmısınız:
- FirstInt([]int) (int, bool)
- FirstString([]string) (string, bool)
Hər ikisi eyni işi görür: birinci elementi götürür və ya “boşdur” deyir. Generics isə bir funksiyanı yazmağa imkan verir — First, hansı ki həm []int, həm []string, həm də []Task üçün işləyir.
Vacibdir: Go-da generics “tam gücü ilə refleksiya” deyil və “hər şeyi any-ə çeviririk” də deyil. Əksinə, generics adətən tip təhlükəsizliyini saxlayır: əgər funksiya T qaytarırsa, çağıran kod məhz işlədiyi tipin özünü alır, müəmmalı interface{} yox.
Generics-i imzaya görə necə tanımaq olar: [...]
Əgər generics funksiyanı tez “tanımaq” istəyirsinizsə, bir sadə əlamət axtarın: funksiyanın və ya tipin adından dərhal sonra kvadrat mötərizələr.
Adi funksiya belə görünür:
func FirstInt(xs []int) (int, bool) { ... }
Amma ümumiləşdirilmiş funksiya belədir:
func First[T any](xs []T) (T, bool) { ... }
Açar məqam: kvadrat mötərizələr slice-a, indeksə və ya massiva aid deyil. Bu, tip parametrlərinin siyahısıdır.
Burada kiçik bir cədvəli yadda saxlamaq faydalıdır (mötərizələr gözə ilişəndə xatırlamaq rahatdır):
| Kodda nə görürsünüz | Bu nə deməkdir |
|---|---|
|
T tipli elementlərdən ibarət slice (dəyərlər) |
|
N uzunluqlu massiv (dəyərlər) |
|
T tip parametri olan F funksiyası (tip səviyyəsi) |
Və bir vacib fikir də var: T — tipin adıdır, dəyişən deyil. Onu ədəd kimi “çıxarmaq” olmaz. O, kompilyatorun əlaqəni başa düşməsi üçün mövcuddur: “girişdə []T gəldi, deməli çıxışda T eyni tip olacaq”.
Tip parametri vs funksiya parametri: tiplər və dəyərlər
Generics-də iki oxşar sözü qarışdırmaq çox asandır: “tip parametri” və “funksiya parametri”. Fərq prinsipialdır: funksiya parametrləri dəyərlərdir, tip parametrləri isə tiplərdir.
Nümunəyə baxın və onu müqavilə kimi, sözbəsöz oxuyun:
package main
import "fmt"
func Echo[T any](v T) T {
return v
}
func main() {
fmt.Println(Echo(10)) // 10
fmt.Println(Echo("go")) // go
}
Burada:
- v — funksiya parametridir, o, icra zamanı “yaşayır”.
- T — tip parametridir, o, kompilyasiya zamanı “yaşayır”.
Echo[T any] ifadəsi belə deməkdir: “Echo istənilən T tipi ilə işləyə bilər, amma funksiyanın içində T ilə istədiyim hər şeyi edə biləcəyimi vəd etmirəm”. Formal olaraq məhz məhdudiyyət (constraint) T üzərində hansı əməliyyatlara icazə verildiyini müəyyən edir. Bu gün ən sadə variantı götürürük: any.
Tip arqumenti və tip çıxarılması: nə vaxt F[int](...) yazmağa ehtiyac olmur
Yeni başlayanların növbəti tələsi: First[T any] görüb hər yerdə “ehtiyat üçün” First[int](...) yazmağa başlamaqdır. Praktikada Go çox vaxt bunu özü həll edir.
Generics istifadəsinin iki rejimi var:
- siz tip arqumentini açıq şəkildə göstərirsiniz;
- kompilyator tip arqumentini funksiyanın adi arqumentlərindən çıxarır.
Kompilyatorun hər şeyi özü çıxardığı nümunə:
package main
import "fmt"
func First[T any](xs []T) (T, bool) {
if len(xs) == 0 {
var zero T
return zero, false
}
return xs[0], true
}
func main() {
n, ok := First([]int{10, 20})
fmt.Println(n, ok) // 10 true
s, ok := First([]string{"go"})
fmt.Println(s, ok) // go true
}
Biz heç yerdə First[int] yazmadıq. Kompilyator []int gördü və başa düşdü: “hə, deməli T = int”. Buna type inference (tip arqumentinin çıxarılması) deyilir.
Tipin açıq şəkildə göstərilməsi daha çox mübahisəli hallarda lazımdır, məsələn kompilyatora kifayət qədər informasiya çatmadıqda. Amma başlanğıc üçün belə qaydanı yadda saxlamaq yaxşıdır: əgər kompilyator tipi çıxara bilirsə — qoy özü çıxarsın. Beləliklə kod daha qısa və daha az “səs-küylü” olur.
T üçün null dəyər: “boş” necə qaytarılır
First[T any] yazarkən, içəridə T-nin nə olacağını bilmirsiniz: int, string, Task və ya başqa bir şey. Amma bəzən məlumat olmadıqda “standart dəyər” qaytarmaq lazım olur.
Go-da bu, şablon üzrə həll olunur:
var zero T
return zero, false
Bu işləyir, çünki hər tipin zero value-u var: int üçün bu 0-dır, string üçün "", bool üçün false, struktur üçün isə sahələrin zero value-larından ibarət struktur.
Gəlin bunu tədris layihəmizə diqqətlə daxil edək. Tutaq ki, bizim bir task modeli var (biz artıq struct ilə işləməyi bilirik):
package main
type Task struct {
ID int
Title string
Done bool
}
İndi First həm də []Task üçün işləyə bilər:
package main
import "fmt"
type Task struct {
ID int
Title string
Done bool
}
func First[T any](xs []T) (T, bool) {
if len(xs) == 0 {
var zero T
return zero, false
}
return xs[0], true
}
func main() {
t, ok := First([]Task{{ID: 1, Title: "learn generics"}})
fmt.Println(t.Title, ok) // learn generics true
}
Bizim “Go-vari” müqavilə ideyasına diqqət yetirin: nil əvəzinə (T, bool) qaytarırıq. Mənalı tiplər üçün (məsələn, Task) bu, çox vaxt “nullability” qurmağa çalışmaqdan daha rahatdır.
2. Generics üzərində utilitilər: Ptr və Must
İndi xoş bir məqam var: generics xüsusilə kiçik utilitilər üçün çox rahatdır, onları əvvəllər ya hər tip üçün kopyalayırdınız, ya da yöndəmsiz yazırdınız.
Ptr: istənilən tipdən olan dəyərə göstərici
Bəzən *T-ni T-dən tez yaratmaq istəyirsiniz (məsələn, data modeli göstərici sahələr saxlayırsa və ya “qeyd olunmayıb” ilə zero value arasında fərq qoymaq istəyirsinizsə). Hər tip üçün &x yazmaq olar, amma bəzən kiçik bir helper saxlamaq rahatdır:
package main
import "fmt"
func Ptr[T any](v T) *T {
return &v
}
func main() {
p := Ptr(7)
fmt.Println(*p) // 7
s := Ptr("go")
fmt.Println(*s) // go
}
Burada generics “supergüc” vermir, amma literal-lardan tez-tez göstərici yaratdığınız yerlərdə kodu daha yığcam edir.
Must: (T, error) içindən dəyəri götürmək
Kursumuzda biz (value, error) üslubuna öyrəşmişik. Bəzən (xüsusilə kiçik nümunələrdə, prototiplərdə və ya testlərdə) kodu qısaltmaq istəyirsiniz: “əgər xəta varsa — qoy partlasın”. Bəli, partlamaq hər zaman yaxşı deyil, amma nümayiş üçün və testlər üçün bəzən uyğundur.
Generic versiyası belə görünür:
package main
import (
"fmt"
"strconv"
)
func Must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
func main() {
n := Must(strconv.Atoi("42"))
fmt.Println(n) // 42
}
Burada vacibdir ki, Must tipi saxlayır: strconv.Atoi (int, error) qaytarır, deməli Must(...) int qaytarır. any deyil, interface{} deyil, konkret int.
Bu, generics-in interface{}-ə əsaslanan “universal funksiyalar”dan xoş fərqidir: orada çox vaxt sonra tip çevirmələri etmək məcburiyyətində qalırsınız.
3. Generic tiplər: Box və Stack
İndiyə qədər generic funksiyalardan danışdıq. Amma generics tiplər üçün də işləyir. Sintaksis oxşardır: tip adından sonra kvadrat mötərizələr görünür.
Budur sadə Box[T] “qabı”:
package main
import "fmt"
type Box[T any] struct {
Value T
}
func main() {
a := Box[int]{Value: 10}
b := Box[string]{Value: "hello"}
fmt.Println(a.Value) // 10
fmt.Println(b.Value) // hello
}
Bu, “həyatda mütləq olmalı olan” bir pattern deyil, amma oxumaq üçün çox yaxşı məşqdir: Box[int] gördükdə, siz sözbəsöz “T = int olan Box” oxuyursunuz. Box daxilində Value sahəsi int tipində olacaq.
İndi tədris tətbiqimiz üçün bir qədər daha “praktik” şey edək: əmrlər tarixçəsi üçün stack. Tutaq ki, istifadəçi hərəkətlərinin tarixçəsini saxlamaq istəyirik (məsələn, task əlavə etdi, tamamlandı kimi işarələdi). Hərəkətlərin özünü hələlik sətir kimi saxlamaq olar — bu, daha sadədir.
package main
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) {
if len(s.items) == 0 {
var zero T
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
Bu struktur artıq “davranışlı normal tip”ə bənzəyir: metodlar, inkapsulyasiya, (T, bool) vasitəsilə kontrakt.
Və kiçik istifadə nümunəsi:
package main
import "fmt"
func main() {
var h Stack[string]
h.Push("add task #1")
h.Push("done task #1")
x, ok := h.Pop()
fmt.Println(x, ok) // done task #1 true
}
Bəli, bu, cəmi 10 sətir “təmiz fayda”dır, amma siz artıq Stack[T] oxumağı və yazmağı bacarırsınız, və məqsədimiz də budur: generics kodunu gözlə inamla qavramaq.
4. Generics-i vərəqdən necə oxumaq olar: tip əvəzlənməsi sxemi
Generics kodu oxuyanda düşüncə ilə bir sadə əməliyyat etmək faydalıdır: “T yerinə konkret tipi qoy və nə alındığına bax”. Bu, “abstraktlıq” hissini azaldır.
Məsələn, bizdə belə bir funksiya var:
func First[T any](xs []T) (T, bool)
Əgər First([]int{1,2,3}) çağırsaq, düşüncədə əvəzləmə belə olacaq:
- T = int
- imza func First(xs []int) (int, bool) formasına düşür
Əgər First([]Task{...}) çağırsaq, onda:
- T = Task
- imza func First(xs []Task) (Task, bool) formasına düşür
Bu fikir Go-da tip parametrlərinin necə təsvir olunduğu ilə üst-üstə düşür: bu, gələcək tip arqumenti üçün “doldurucu”dur və o, kompilyasiya mərhələsində məlumdur.
Kiçik blok-sxemə (onsuz yaşamaq olmaz deyə yox, sadəcə beynin bəzən mətndən daha çox şəkli sevdiyi üçün):
flowchart TD
A["Kod: First[T any](xs []T) (T, bool)"] --> B["Çağırış: First([]int{1,2})"]
B --> C["Kompilyator çıxarır: T = int"]
C --> D["Tip yoxlanışı: xs is []int, return (int, bool)"]
A --> E["Çağırış: First([]Task{...})"]
E --> F["Kompilyator çıxarır: T = Task"]
F --> G["Tip yoxlanışı: xs is []Task, return (Task, bool)"]
Məhz buna görə Go-da generics adətən “dil daxilində ayrı bir dil” kimi yox, “şablon tipi olan adi Go” kimi qəbul edilir.
5. Type parameters ilə ilk tanışlıqda tipik səhvlər
Səhv №1: []T və [T any] arasını qarışdırmaq.
Bu, daim baş verir: çoxlu mötərizə görürsünüz və beyin onların hamısının eyni olduğunu düşünür. Əslində []T — dəyərlərdən ibarət slice, [T any] isə tip parametrlərinin siyahısıdır. Bunu müalicə etməyin yaxşı yolu — mötərizələrin harada durduğuna baxmaqdır: generics-də onlar funksiyanın/tipin adından dərhal sonra gəlir.
Səhv №2: T-ni tip yox, dəyər kimi qəbul etmək.
Yeni başlayanlar bəzən T-ni sanki dəyişənmiş kimi “icra etməyə” çalışırlar: “bəs T nəyə bərabərdir?”. Düzgün sual budur: “bu çağırışda T yerinə hansı konkret tip qoyulacaq?”. T icra zamanı yaşamır, o, kompilyator üçün lazımdır.
Səhv №3: hər yerdə “etibarlılıq üçün” F[int](...) yazmaq.
Çox vaxt kompilyator tip arqumentlərini funksiyanın adi parametrlərinə görə özü çıxarır. Əgər hər yerdə tip arqumentlərini açıq göstərməyə başlasanız, kod səs-küylü olacaq və generics sizə vaxt qənaət etdirmək əvəzinə onu sərf etdirəcək.
Səhv №4: T üzərində zəmanət verilməyən əməliyyatlar etməyə çalışmaq.
Bu gün biz T any istifadə etdik və bu, qəsdən idi: any “istənilən tip” deməkdir, amma istənilən dəyər üçün icazəli olanlardan başqa heç bir əməliyyatı vəd etmir (ötürmək, saxlamaq, qaytarmaq). Əgər T-ni müqayisə etmək və ya []T-ni çeşidləmək istəyirsinizsə, məhdudiyyətlər daxil etməli olacaqsınız. Amma bu artıq ayrıca mövzudur və ora tələsmədən, sakit getmək daha yaxşıdır.
Səhv №5: tip təhlükəsizliyini itirərək T əvəzinə any qaytarmaq.
Bəzən “universal” funksiya yazmaq istəyirsiniz, amma nədənsə ən asan variant func First(xs []any) any olur. Bu, demək olar ki, həmişə geriləmədir: nəticənin konkret tipini itirirsiniz və sonra runtime-da tip çevirmələri və yoxlamalar etməyə məcbur olursunuz. Generics-in üstünlüyü məhz budur: T qaytarmağa və nəticənin tipini kompilyasiya səviyyəsində saxlamağa imkan verir.
GO TO FULL VERSION