CodeGym /Kurslar /Go SELF /Tip parametrləri: sadə generics funksiyaları

Tip parametrləri: sadə generics funksiyaları

Go SELF
Səviyyə, Dərs
Mövcuddur

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
T tipli elementlərdən ibarət slice (dəyərlər)
[N]T
N uzunluqlu massiv (dəyərlər)
F[T any](...)
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.

1
Tapşırıq
Go SELF, səviyyə, dərs
Bağlanıb
Dəyərlərin güzgüsü
Dəyərlərin güzgüsü
1
Tapşırıq
Go SELF, səviyyə, dərs
Bağlanıb
İlk siqnal
İlk siqnal
1
Tapşırıq
Go SELF, səviyyə, dərs
Bağlanıb
Jeton axtarışı
Jeton axtarışı
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION