Tarkibga o'tish

Go’da generics bilan ishlash

Generics tur parametrlari (type parameters) orqali bir xil kodni bir nechta tur bilan xavfsiz ishlatish imkonini beradi. Masalan, int va float64 qiymatlarini jamlaydigan funksiyalar bir xil mantiqqa ega bo‘lsa, ularni bitta generic funksiya bilan ifodalash mumkin.

Go generics’ni 1.18 versiyasidan boshlab qo‘llab-quvvatlaydi. Har bir funksiyani generic qilish shart emas. Generics kod takrorini kamaytirsa va turlar orasidagi bog‘lanishni aniq saqlasa foydali bo‘ladi.

Generics nima uchun kerak?

Tasavvur qiling, buyurtmadagi miqdorlar int, narxlar esa float64 ko‘rinishida saqlanadi. Ikkala slice elementlarini jamlash uchun alohida funksiyalar yozish mumkin:

func intlarniJamlash(values []int) int {
    var total int
    for _, value := range values {
        total += value
    }
    return total
}

func floatlarniJamlash(values []float64) float64 {
    var total float64
    for _, value := range values {
        total += value
    }
    return total
}

Bu funksiyalarda faqat turlar farq qiladi. Algoritm esa bir xil. Yana bir son turi kerak bo‘lsa, shu kodning navbatdagi nusxasi paydo bo‘ladi.

Generic funksiya tur farqini parametrga aylantiradi:

package main

import "fmt"

type Number interface {
    ~int | ~int64 | ~float64
}

func jamlash[T Number](values []T) T {
    var total T
    for _, value := range values {
        total += value
    }
    return total
}

func main() {
    miqdorlar := []int{2, 3, 5}
    narxlar := []float64{12.5, 7.25, 10}

    fmt.Println(jamlash(miqdorlar))
    fmt.Println(jamlash(narxlar))
}

Natija:

10
29.75

jamlash() bitta algoritmni tur xavfsizligini yo‘qotmasdan turli sonlar uchun ishlatdi. Funksiya []int qabul qilsa int, []float64 qabul qilsa float64 qaytaradi.

Tur parametri va constraint

Generic funksiya nomidan keyingi [] ichida tur parametri yoziladi:

func jamlash[T Number](values []T) T

Bu yozuvda:

  • T — tur parametri;
  • NumberT qabul qilishi mumkin bo‘lgan turlarni belgilovchi constraint;
  • []T — elementlari T turida bo‘lgan slice;
  • oxirgi T — funksiya qaytaradigan tur.

Constraint tur parametrida qaysi amallarni bajarish mumkinligini ham belgilaydi. Number ichidagi barcha turlar + operatorini qo‘llab-quvvatlagani uchun total += value yozuvi to‘g‘ri.

type Number interface {
    ~int | ~int64 | ~float64
}

| belgisi ruxsat berilgan turlarni birlashtiradi. ~int yozuvi faqat intni emas, asosiy turi int bo‘lgan nomlangan turlarni ham qabul qiladi:

type Quantity int

Quantity turi ~int talabiga mos keladi. Agar constraintda faqat int yozilsa, bunday nomlangan tur unga mos kelmaydi.

Tur inferensiyasi

Generic funksiya chaqirilganda tur argumentini aniq yozish mumkin:

total := jamlash[int]([]int{1, 2, 3})

Ko‘p holatda Go argumentlardan tur argumentini o‘zi aniqlaydi:

total := jamlash([]int{1, 2, 3})

Har ikkala chaqiruvda T turi int. Ikkinchi ko‘rinish tur inferensiyasi (type inference) deyiladi.

Ba’zan kompilyator turni aniqlay olmaydi. Masalan, tur parametriga bog‘liq argument bo‘lmasa, tur argumenti aniq beriladi:

func zero[T any]() T {
    var value T
    return value
}

son := zero[int]()
matn := zero[string]()

zero() argument qabul qilmagani uchun Tni qiymatdan aniqlab bo‘lmaydi. Shu sababli [int] yoki [string] yozilishi kerak.

any constrainti

any har qanday turga ruxsat beradigan constraint. U interface{} uchun alias hisoblanadi.

Quyidagi funksiya slice nusxasini yaratadi. Algoritm element ustida arifmetik yoki taqqoslash amali bajarmagani uchun turga qo‘shimcha talab kerak emas:

package main

import "fmt"

func nusxalash[T any](values []T) []T {
    result := make([]T, len(values))
    copy(result, values)
    return result
}

func main() {
    sonlar := nusxalash([]int{10, 20, 30})
    sozlar := nusxalash([]string{"Go", "Generics"})

    fmt.Println(sonlar)
    fmt.Println(sozlar)
}

Natija:

[10 20 30]
[Go Generics]

any barcha amallarga ruxsat beradi degani emas. T any bo‘lsa, a + b, a > b yoki a == b yozib bo‘lmaydi. Chunki barcha Go turlari bu operatorlarni qo‘llab-quvvatlamaydi.

Oddiy any qiymati va T any tur parametri ham bir xil vazifani bajarmaydi:

func birinchi(values []any) any
func birinchi[T any](values []T) T

Birinchi funksiya aniq tur haqidagi ma’lumotni yo‘qotadi va any qaytaradi. Ikkinchi funksiya kiruvchi slice element turini saqlab, aynan shu turni qaytaradi.

comparable constrainti

comparable == va != bilan solishtirish mumkin bo‘lgan turlar uchun tayyor constraint:

package main

import "fmt"

func mavjud[T comparable](values []T, target T) bool {
    for _, value := range values {
        if value == target {
            return true
        }
    }
    return false
}

func main() {
    fmt.Println(mavjud([]string{"go", "rust"}, "go"))
    fmt.Println(mavjud([]int{10, 20}, 30))
}

Natija:

true
false

comparable tartib bo‘yicha solishtirishni anglatmaydi. U < yoki > operatorlariga ruxsat bermaydi. Slice, map va funksiyalarni == bilan solishtirib bo‘lmagani uchun ular comparable constraintiga mos kelmaydi.

Bir nechta tur parametri

Funksiya bir nechta tur parametri qabul qilishi mumkin. Masalan, map kalitlarini slice ko‘rinishida olishda kalit va qiymat turlari boshqa-boshqa bo‘ladi:

package main

import "fmt"

func kalitlar[K comparable, V any](items map[K]V) []K {
    result := make([]K, 0, len(items))
    for key := range items {
        result = append(result, key)
    }
    return result
}

func main() {
    ages := map[string]int{
        "Ali":  24,
        "Vali": 31,
    }

    fmt.Println(kalitlar(ages))
}

K comparable bo‘lishi shart, chunki Go map kaliti faqat solishtiriladigan turda bo‘ladi. V ustida hech qanday amal bajarilmagani uchun unga any yetarli.

Map elementlari tartiblanmagan. Shu sababli natijadagi kalitlar tartibi har ishga tushirishda farq qilishi mumkin.

Generic type

Tur parametrini faqat funksiyada emas, type e’lonida ham ishlatish mumkin. Generic type bir xil ma’lumot tuzilmasini turli element turlari bilan yaratishga yordam beradi.

Quyidagi Stack qiymatlarni oxirgi qo‘shilganidan boshlab qaytaradi:

package main

import "fmt"

type Stack[T any] struct {
    items []T
}

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

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

    last := len(s.items) - 1
    value := s.items[last]
    s.items = s.items[:last]
    return value, true
}

func main() {
    var stack Stack[string]
    stack.Push("birinchi")
    stack.Push("ikkinchi")

    value, ok := stack.Pop()
    if ok {
        fmt.Println(value)
    }
}

Natija:

ikkinchi

Stack[T] ichidagi items faqat T qiymatlarini saqlaydi. Stack[string] yaratilganda Push() faqat string qabul qiladi va Pop() string qaytaradi. Stack[int] esa shu tuzilma va metodlarni int bilan ishlatadi.

Pop() bo‘sh stack uchun Tning nol qiymati va false qaytaradi. Generic kodda Tning nol qiymatini olish uchun var zero T yozuvi ishlatiladi.

Generic type metodining receiverida ham tur parametri ko‘rsatiladi:

func (s *Stack[T]) Push(value T)

Bu yerdagi T Stack e’lonidagi constraintdan foydalanadi. Receiverda constraintni qayta yozish shart emas.

Generics qachon kerak emas?

Generics quyidagi holatlarda ortiqcha bo‘lishi mumkin:

  • kod faqat bitta aniq tur bilan ishlasa;
  • turlar bir xil algoritm emas, turli xatti-harakat talab qilsa;
  • oddiy interface kerakli xatti-harakatni aniqroq ifodalasa;
  • tur parametri kodni kamaytirmay, aksincha o‘qishni qiyinlashtirsa.

Constraintni imkon qadar tor tanlang. Funksiyaga faqat tenglik tekshiruvi kerak bo‘lsa comparable, hech qanday turga xos amal kerak bo‘lmasa any yetarli. Keraksiz turlarni katta constraintga qo‘shish funksiyaning vazifasini noaniq qiladi.

Xulosa

  • Generics bir xil algoritmni bir nechta tur bilan xavfsiz qayta ishlatadi.
  • Tur parametri [] ichida, uning constrainti esa parametr nomidan keyin yoziladi.
  • Go ko‘p hollarda tur argumentini funksiya argumentlaridan aniqlaydi.
  • any barcha turlarni, comparable esa == va != bilan solishtiriladigan turlarni qabul qiladi.
  • Generic type turli elementlar uchun qayta ishlatiladigan ma’lumot tuzilmasini yaratadi.
  • Generics kodni aniq soddalashtirgandagina ishlatilishi kerak.

Misollar

1. Istalgan turning nol qiymatini olish

Bu misolda argumentsiz generic funksiya berilgan turning nol qiymatini qaytaradi.

package main

import "fmt"

func zero[T any]() T {
    var value T
    return value
}

func main() {
    fmt.Println(zero[int]())
    fmt.Printf("%q\n", zero[string]())
    fmt.Println(zero[bool]())
}

var value T qiymatni T turning nol qiymati bilan yaratadi. Bu int uchun 0, string uchun bo‘sh matn, bool uchun false. Funksiyada argument yo‘q, shuning uchun kompilyator Tni aniqlay olmaydi va [int] kabi tur argumentini aniq yozish kerak.

2. Slice ichidan birinchi elementni xavfsiz olish

Bu misolda generic funksiya elementni va u topilganini bildiruvchi bool qiymatni qaytaradi.

package main

import "fmt"

func first[T any](values []T) (T, bool) {
    if len(values) == 0 {
        var zero T
        return zero, false
    }

    return values[0], true
}

func main() {
    number, numberOK := first([]int{10, 20, 30})
    word, wordOK := first([]string{})

    fmt.Println(number, numberOK)
    fmt.Printf("%q %t\n", word, wordOK)
}

Slice indekslari 0 dan boshlangani uchun birinchi element values[0] orqali olinadi. Bo‘sh slice uchun 0-indeks mavjud emas. Shu sabab len(values) == 0 avval tekshiriladi va Tning nol qiymati bilan false qaytariladi.

3. Slice tartibini joyida teskarilash

Bu misolda bitta generic funksiya turli elementli slicelar tartibini o‘zgartiradi.

package main

import "fmt"

func reverse[T any](values []T) {
    for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 {
        values[left], values[right] = values[right], values[left]
    }
}

func main() {
    numbers := []int{1, 2, 3, 4}
    words := []string{"bir", "ikki", "uch"}

    reverse(numbers)
    reverse(words)

    fmt.Println(numbers)
    fmt.Println(words)
}

left birinchi indeks bo‘lgani uchun 0 dan boshlanadi. right esa oxirgi indeks bo‘lib, len(values)-1 ga teng. left < right sharti indekslar uchrashganda siklni to‘xtatadi. Funksiya elementlarni faqat almashtirgani uchun any yetarli.

4. Takrorlangan qiymatlarni olib tashlash

Bu misolda comparable constrainti qiymatlarni map kaliti sifatida ishlatishga imkon beradi.

package main

import "fmt"

func unique[T comparable](values []T) []T {
    seen := make(map[T]bool)
    result := make([]T, 0, len(values))

    for _, value := range values {
        if !seen[value] {
            seen[value] = true
            result = append(result, value)
        }
    }

    return result
}

func main() {
    fmt.Println(unique([]int{2, 2, 3, 2, 4, 3}))
    fmt.Println(unique([]string{"go", "api", "go"}))
}

Map kalitlari solishtiriladigan turda bo‘lishi kerak, shu sabab T comparable tanlangan. result uzunligi 0 dan boshlanadi, chunki hali hech bir qiymat qo‘shilmagan. Uning sig‘imi kiruvchi slice uzunligicha olinadi: eng ko‘p holatda barcha qiymatlar noyob bo‘ladi.

5. Ikki qiymatdan kichigini tanlash

Bu misolda constraint < operatorini qo‘llab-quvvatlaydigan son turlarini cheklaydi.

package main

import "fmt"

type OrderedNumber interface {
    ~int | ~int64 | ~float64
}

func min[T OrderedNumber](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(min(8, 3))
    fmt.Println(min(4.5, 7.2))
}

Constraintdagi barcha turlar < bilan solishtiriladi. Shu sabab bu operator generic funksiya ichida ruxsat etiladi. Ikkala argument bir xil T turida bo‘lishi va natija ham aynan shu turda qaytishi saqlanadi.

6. Nomlangan son turini jamlash

Bu misolda ~ belgisi asosiy turi int bo‘lgan yangi turni constraintga moslashtiradi.

package main

import "fmt"

type Addable interface {
    ~int | ~float64
}

type Distance int

func sum[T Addable](values []T) T {
    var total T
    for _, value := range values {
        total += value
    }
    return total
}

func main() {
    distances := []Distance{12, 8, 15}
    prices := []float64{10.5, 4.25}

    fmt.Println(sum(distances))
    fmt.Println(sum(prices))
}

Distance alohida nomlangan tur, lekin uning asosiy turi int. ~int uni ham qabul qiladi. totalning nol qiymati 0; yig‘indiga 0 qo‘shish natijani o‘zgartirmagani uchun jamlash aynan shu qiymatdan boshlanadi.

7. Slice elementlarini boshqa turga aylantirish

Bu misolda ikkita tur parametri kiruvchi va chiquvchi slice turlarini alohida belgilaydi.

package main

import "fmt"

func transform[T any, R any](values []T, convert func(T) R) []R {
    result := make([]R, 0, len(values))
    for _, value := range values {
        result = append(result, convert(value))
    }
    return result
}

func main() {
    numbers := []int{3, 5, 8}
    labels := transform(numbers, func(value int) string {
        return fmt.Sprintf("son=%d", value)
    })

    fmt.Println(labels)
}

T kiruvchi element turini, R esa natija turini bildiradi. result avval bo‘sh, ya’ni uzunligi 0. Sig‘im len(values) qilib olinadi, chunki har bir kiruvchi elementdan bittadan natija yaratiladi.

8. Shartga mos elementlarni saralab olish

Bu misolda generic funksiya tekshiruvchi funksiyani qabul qilib, mos elementlardan yangi slice tuzadi.

package main

import "fmt"

func filter[T any](values []T, keep func(T) bool) []T {
    result := make([]T, 0, len(values))
    for _, value := range values {
        if keep(value) {
            result = append(result, value)
        }
    }
    return result
}

func main() {
    even := filter([]int{1, 2, 3, 4, 5, 6}, func(value int) bool {
        return value%2 == 0
    })
    long := filter([]string{"Go", "kod", "generics"}, func(value string) bool {
        return len(value) >= 3
    })

    fmt.Println(even)
    fmt.Println(long)
}

filter() elementning o‘zida turga xos amal bajarmaydi, shuning uchun T any ishlatiladi. Kerakli shart keep funksiyasida beriladi. len(value) >= 3 uzunligi 3 ga teng matnlarni ham natijaga qo‘shadi; > 3 bo‘lsa ular olinmas edi.

9. Ikki xil turdagi qiymatni bitta structda saqlash

Bu misolda generic struct kalit va qiymat turlarini mustaqil saqlaydi.

package main

import "fmt"

type Pair[K any, V any] struct {
    Key   K
    Value V
}

func main() {
    stock := Pair[string, int]{Key: "daftar", Value: 25}
    point := Pair[int, float64]{Key: 7, Value: 18.5}

    fmt.Println(stock.Key, stock.Value)
    fmt.Println(point.Key, point.Value)
}

K va V bir-biridan mustaqil tur parametrlari. Birinchi qiymatda ular string va int, ikkinchisida int va float64. Struct maydonlarida solishtirish yoki arifmetik amal yo‘q, shu sabab ikkala constraint uchun ham any yetarli.

10. Generic navbat yaratish

Bu misolda generic type elementlarni qo‘shilgan tartibda qaytaradigan navbatni ifodalaydi.

package main

import "fmt"

type Queue[T any] struct {
    items []T
}

func (q *Queue[T]) Add(value T) {
    q.items = append(q.items, value)
}

func (q *Queue[T]) Remove() (T, bool) {
    if len(q.items) == 0 {
        var zero T
        return zero, false
    }

    value := q.items[0]
    q.items = q.items[1:]
    return value, true
}

func main() {
    var queue Queue[string]
    queue.Add("birinchi")
    queue.Add("ikkinchi")

    first, firstOK := queue.Remove()
    second, secondOK := queue.Remove()
    emptyValue, emptyOK := queue.Remove()

    fmt.Println(first, firstOK)
    fmt.Println(second, secondOK)
    fmt.Printf("%q %t\n", emptyValue, emptyOK)
}

Navbatning birinchi elementi 0-indeksda turadi, shu sabab Remove() aynan q.items[0]ni oladi. q.items[1:] olingan elementni slicedan chiqaradi. Navbat bo‘sh bo‘lsa, stringning nol qiymati bo‘lgan bo‘sh matn va false qaytadi.