Главная / Курсы / Golang / Дженерики
# Глава 15. Дженерики ## Дженерики-функции Начиная с Go 1.18, в языке появились дженерики — **generics**. Дженерики позволяют писать обобщенный код, способный обрабатывать разные типы данных. Благодаря ним достаточно реализовать алгоритм лишь однажды. После этого его применяют к разным типам. Дженерики-функции — **generic functions** — легче понять на примере. Допустим, мы реализовали функцию `unique`, которая возвращает срез, состоящий из уникальных значений: ``` go {.example_for_playground} package main import "fmt" func unique(slice []int) []int { seen := make(map[int]struct{}) result := []int{} for _, item := range slice { if _, ok := seen[item]; !ok { seen[item] = struct{}{} result = append(result, item) } } return result } func main() { input := []int{1, 2, 3, 3, 4, 5} output := unique(input) fmt.Println(output) } ``` ``` [1 2 3 4 5] ``` Что делать, если мы хотим работать не только с `int`, но и, например, с `float64`? Мы могли бы решить повторить эту функцию для `float64`: ``` go {.example_for_playground} package main import "fmt" func unique(slice []int) []int { seen := make(map[int]struct{}) result := []int{} for _, item := range slice { if _, ok := seen[item]; !ok { seen[item] = struct{}{} result = append(result, item) } } return result } func unique(slice []float64) []float64 { seen := make(map[float64]struct{}) result := []float64{} for _, item := range slice { if _, ok := seen[item]; !ok { seen[item] = struct{}{} result = append(result, item) } } return result } func main() { fmt.Println(unique([]int{1, 2, 3, 3, 4, 5})) fmt.Println(unique([]float64{1.8, 2.2, 3.2, 3.2})) } ``` ``` cmd/main.go:20:6: unique redeclared in this block ``` Получаем ошибку компиляции: двух функций с одинаковым именем быть не может. Выходит, что мы должны еще и придумывать им разные имена! ```go {.example_for_playground} package main import "fmt" func uniqueInt(slice []int) []int { seen := make(map[int]struct{}) result := []int{} for _, item := range slice { if _, ok := seen[item]; !ok { seen[item] = struct{}{} result = append(result, item) } } return result } func uniqueFloat64(slice []float64) []float64 { seen := make(map[float64]struct{}) result := []float64{} for _, item := range slice { if _, ok := seen[item]; !ok { seen[item] = struct{}{} result = append(result, item) } } return result } func main() { fmt.Println(uniqueInt([]int{1, 2, 3, 3, 4, 5})) fmt.Println(uniqueFloat64([]float64{1.8, 2.2, 3.2, 3.2})) } ``` ``` [1 2 3 4 5] [1.8 2.2 3.2] ``` А если нужны и `float32`? Придется каждый раз писать одинаковый код для разных типов. То есть копировать один и тот же алгоритм и заменять лишь типы данных, которые он обрабатывает. Более того, нужно именовать по смыслу одинаковые функции разными именами. Можно воспользоваться [кодогенерацией](https://go.dev/blog/generate) — переложить эту рутинную работу на компилятор. Но проще написать обобщенный код, используя дженерики: ``` go {.example_for_playground} package main import "fmt" func unique[T int | float64](slice []T) []T { seen := make(map[T]struct{}) result := []T{} for _, item := range slice { if _, ok := seen[item]; !ok { seen[item] = struct{}{} result = append(result, item) } } return result } func main() { fmt.Println(unique([]int{1, 2, 3, 3, 4, 5})) fmt.Println(unique([]float64{1.4, 1.3, 1.4, 1.5, 2.7})) } ``` ``` [1 2 3 4 5] [1.4 1.3 1.5 2.7] ``` В квадратных скобках после имени функции описывается новый тип, с которым мы будем работать. В нашем случае `T` — имя этого типа. Его вы даете сами, но на практике чаще всего используют букву `T`. Типы, которые может принимать `T`, перечисляются через вертикальную черту `|`. После того, как мы описали новый тип, он становится доступен всюду внутри функции. Типов может быть несколько. В общем случае, синтаксис дженерика-функции такой: ``` func имяФункции[тип1,тип2,...типN](параметры)(возвращемыеЗначения){ тело } ``` Конструкция `[тип1,тип2,...типN]` называется параметры-типы — **type parameters.** Что делать, если мы хотим работать не только с `int` и `float64`, но и с другими сравнимыми типами? Например, `float32` или даже `string`. Мы могли бы воспользоваться интерфейсом `any`, чтобы передать любой тип в функцию. Но как понять, что элементы этого типа можно сравнивать? Мы могли бы перечислить все типы, с которыми мы можем работать явно, но это утомительно и громоздко. За нас это уже сделали в интерфейсе `comparable`. Интерфейсы в Go бывают двух видов: * Базовые — **basic** — определяются через список методов. * Общие — **generic** — могут включать не только методы, но и типы. Интерфейс `comparable` определяется через все сравнимые типы. Он является общим. Общие интерфейсы не могут быть использованы как обычные типы. Их применяют совместно с дженериками. Для нашего примера как раз подойдет такой интерфейс: ``` go {.example_for_playground .example_for_playground_001} func unique[T comparable](slice []T) []T { // ... } ``` В качестве типа `T` мы использовали некоторый обобщенный тип `comparable`. Теперь мы можем работать со всеми сравнимыми типами: ``` go {.example_for_playground .example_for_playground_002} fmt.Println(unique([]int{1, 2, 3, 3, 4, 5})) fmt.Println(unique([]float64{1.4, 1.3, 1.4, 1.5, 2.7})) fmt.Println(unique([]string{"juice", "orange", "juice", "apple"})) ``` ``` [1 2 3 4 5] [1.4 1.3 1.5 2.7] [juice orange apple] ``` Что выведет следующий код? В случае ошибки напишите `error`. {.task_text} ```go {.example_for_playground} package main import "fmt" func unique[T comparable](slice []T) []T { seen := make(map[T]struct{}) result := []T{} for _, item := range slice { if _, ok := seen[item]; !ok { seen[item] = struct{}{} result = append(result, item) } } return result } func main() { input := [][]int{{1, 2, 3}, {1, 2, 3}, {1, 4, 6}} output := unique(input) fmt.Println(output) } ``` ```consoleoutput {.task_source #golang_chapter_0150_task_0010} ``` Типу `comparable` соответствуют только сравнимые типы. {.task_hint} ```{.task_answer} error ``` Функция `Filter` позволяет фильтровать срез по некоторому предикату. В данном случае предикат представляет собой функцию. При передаче некоторого аргумента в нее функция возвращает значение типа `bool`. Если это значение истинно, то мы добавляем очередной элемент в новый срез. В противном случае — нет. {.task_text} Сейчас функция `Filter` принимает значения типа `any`. Но как убедиться в том, что тип элементов среза совпадает с типом аргумента, который принимает предикат? Это должен быть один и тот же тип. {.task_text} С помощью дженерика отредактируйте функцию `Filter` так, чтобы выполнился код из `main`. {.task_text} ```go {.task_source #golang_chapter_0150_task_0020} package main import ( "fmt" "net/mail" ) func valid(email string) bool { _, err := mail.ParseAddress(email) return err == nil } // отредактируйте эту функцию func Filter(slice []any, predicate func(any) bool) []any { var result []any for _, v := range slice { if predicate(v) { result = append(result, v) } } return result } func main() { slice := []string{"anton@yandex.ru", "https://go.dev/", "go@best@hackers"} newSlice := Filter(slice, valid) fmt.Println(newSlice) consts := []float64{3.141592653589, 2.718281828} newConsts := Filter(consts, func(p float64) bool { return p < 3 }) fmt.Println(newConsts) } ``` Внутри дженерика объявите тип `T`. Тип, который может принимать `T`, — `any`. {.task_hint} ``` go {.task_answer} package main import ( "fmt" "net/mail" ) func valid(email string) bool { _, err := mail.ParseAddress(email) return err == nil } func Filter[T any](slice []T, predicate func(T) bool) []T { var result []T for _, v := range slice { if predicate(v) { result = append(result, v) } } return result } func main() { slice := []string{"anton@yandex.ru", "https://go.dev/", "go@best@hackers"} newSlice := Filter(slice, valid) fmt.Println(newSlice) consts := []float64{3.141592653589, 2.718281828} newConsts := Filter(consts, func(p float64) bool { return p < 3 }) fmt.Println(newConsts) } ``` ## Дженерики-типы Когда в структуре есть поля, в которых необходимо хранить данные различных типов, бывает удобно использовать дженерики-типы — **generic types**: ``` go {.example_for_playground} package main import "fmt" type recordT[T int | float64] struct { data []T tag string } func main() { sampleRecord := recordT[int]{[]int{1, 2, 3}, "sample data"} anotherRecord := recordT[float64]{[]float64{3.141592653589, 2.718281828}, "consts"} fmt.Println(sampleRecord) fmt.Println(anotherRecord) } ``` ``` {[1 2 3] sample data} {[3.141592653589 2.718281828] consts} ``` В этом случае при создании переменной типа `recordT` необходимо указывать тот конкретный тип, с которым мы работаем. Как уже было сказано, ничто не мешает использовать в дженериках более одного типа: ```go {.example_for_playground} package main import "fmt" type Pair[T any, U any] struct { First T Second U } func NewPair[T any, U any](first T, second U) Pair[T, U] { return Pair[T, U]{First: first, Second: second} } func main() { nameAge := NewPair("Alice", 30) fmt.Println("Name and age:", nameAge) coordinates := NewPair(10, 20.5) fmt.Println("Coordinates:", coordinates) } ``` ``` Name and age: {Alice 30} Coordinates: {10 20.5} ``` Кеш `Cache` позволяет хранить данные в хеш-таблице с ключами типа `string` и значениями типа `cacheItem`. Поле `ttl` структуры `Cache` расшифровывается как *time to live* — время жизни. Его тип — `time.Duration`, псевдоним для `int64`. Оно хранит количество наносекунд, в течение которых кеш актуален. {.task_text} Поле `value` из `cacheItem` хранит само значение типа `int`, а `expiresAt` — время, после которого значение перестает быть актуальным. Тип `expiresAt` — `time.Time` — структура, которая хранит конкретное время. {.task_text} Функция `NewCache` возвращает указатель на вновь созданный кеш. Функция `Set` позволяет установить значение кеша по его ключу. Функция `Get` возвращает значение кеша по его ключу и признак того, что актуальное значение присутствует в нем. Функция `Delete` удаляет значение кеша по его ключу. {.task_text} Модифицируйте код над функцией `main` таким образом, чтобы в качестве ключей кеша можно было использовать любой сравнимый тип. В качестве значений кеша также должны выступать данные любого типа, необязательного сравнимого. {.task_text} В результате ваших модификаций код из `main` должен выполнится без ошибок. {.task_text} ```go {.task_source #golang_chapter_0150_task_0030} package main import ( "fmt" "time" ) type Cache struct { data map[string]cacheItem ttl time.Duration } type cacheItem struct { value int expiresAt time.Time } func NewCache(ttl time.Duration) *Cache { return &Cache{ data: make(map[string]cacheItem), ttl: ttl, } } func (c *Cache) Set(key string, value int) { c.data[key] = cacheItem{ value: value, expiresAt: time.Now().Add(c.ttl), } } func (c *Cache) Get(key string) (int, bool) { item, exists := c.data[key] if !exists || time.Now().After(item.expiresAt) { return 0, false } return item.value, true } func (c *Cache) Delete(key string) { delete(c.data, key) } func main() { // Кеш для числовых значений intCache := NewCache[string, int](10 * time.Minute) intCache.Set("request_count", 42) intCache.Set("active_users", 150) if count, ok := intCache.Get("request_count"); ok { fmt.Printf("Requests: %d\n", count) } intCache.Delete("request_count") intCache.Delete("active_users") // Кеш для структур type User struct { Name string Email string } userCache := NewCache[int, User](time.Hour) userCache.Set(1, User{Name: "Bob", Email: "bob@example.com"}) userCache.Set(2, User{Name: "Charlie", Email: "charlie@test.com"}) if user, ok := userCache.Get(1); ok { fmt.Printf("User: %+v\n", user) } userCache.Delete(1) userCache.Delete(2) } ``` Используйте дженерики-типы и дженерики-функции. В качестве типа ключей будет выступать `comparable`, а в качестве типа значений — `any`. {.task_hint} ```go {.task_answer} package main import ( "fmt" "time" ) type Cache[K comparable, V any] struct { data map[K]cacheItem[V] ttl time.Duration } type cacheItem[V any] struct { value V expiresAt time.Time } func NewCache[K comparable, V any](ttl time.Duration) *Cache[K, V] { return &Cache[K, V]{ data: make(map[K]cacheItem[V]), ttl: ttl, } } func (c *Cache[K, V]) Set(key K, value V) { c.data[key] = cacheItem[V]{ value: value, expiresAt: time.Now().Add(c.ttl), } } func (c *Cache[K, V]) Get(key K) (V, bool) { item, exists := c.data[key] if !exists || time.Now().After(item.expiresAt) { var zero V return zero, false } return item.value, true } func (c *Cache[K, V]) Delete(key K) { delete(c.data, key) } func main() { // Кеш для числовых значений intCache := NewCache[string, int](10 * time.Minute) intCache.Set("request_count", 42) intCache.Set("active_users", 150) if count, ok := intCache.Get("request_count"); ok { fmt.Printf("Requests: %d\n", count) } intCache.Delete("request_count") intCache.Delete("active_users") // Кеш для структур type User struct { Name string Email string } userCache := NewCache[int, User](time.Hour) userCache.Set(1, User{Name: "Bob", Email: "bob@example.com"}) userCache.Set(2, User{Name: "Charlie", Email: "charlie@test.com"}) if user, ok := userCache.Get(1); ok { fmt.Printf("User: %+v\n", user) } userCache.Delete(1) userCache.Delete(2) } ``` ## Конструкция с тильдой Следующий пример подсчитывает количество заранее заданных лексем. Обратите внимание, что лексемы представлены строками. Функция `wordCounter` также работает со строками. ``` go {.example_for_playground} package main import ( "fmt" "strings" ) func wordCounter() func(word string) map[string]int { res := make(map[string]int) return func(word string) map[string]int { res[word]++ return res } } func main() { wc := wordCounter() lexemeToFind := "i" anotherLexemeToFind := "print" s := "for i := 0; i < 5; i++" + "{print(\"hello world!\n\"))}" var res map[string]int for _, word := range strings.Split(s, " ") { // проверка вхождения lexemeToFind в word if strings.Contains(word, lexemeToFind) { res = wc(lexemeToFind) } if strings.Contains(word, anotherLexemeToFind) { res = wc(anotherLexemeToFind) } } fmt.Println(res) } ``` ``` map[i:3 print:1] ``` Что, если мы используем псевдоним `lexeme` для типа `string`? Функция `wordCounter` перестанет работать, потому что мы умеем обрабатывать только строки. ``` go {.example_for_playground} package main type lexeme string func wordCounter() func(word string) map[string]int { res := make(map[string]int) return func(word string) map[string]int { res[word]++ return res } } func main() { wc := wordCounter() var lexemeToFind lexeme = "i" wc(lexemeToFind) } ``` ``` ./main.go:18:5: cannot use lexemeToFind (variable of string type lexeme) as string value in argument to wc (exit status 1) ``` Получаем ошибку компиляции. Но ведь по смыслу ничего не изменилось! Псевдоним `lexeme` — это все тот же `string`, только с другим названием. Чтобы функция принимала не только базовый тип, но и его псевдонимы, удобно применить дженерик с символом тильды `~`. Достаточно просто поставить символ тильды перед базовым типом при перечислении параметров-типов: ```go func wordCounter[T ~string]() func(word T) map[T]int { ... } ``` Для встроенных типов базовым типом является сам тип. Например, для `float64` — это `float64`. Для составных типов базовый тип определяется по их структуре. Например, для `type Point struct { x int }` базовый тип — `struct{ x int }`. Для псевдонимов базовый тип — это тот тип, для которого объявлен псевдоним. В нашем случае базовый тип `lexeme` — `string`. Вот как будет выглядеть полный код: ``` go {.example_for_playground} package main import "fmt" type lexeme string func wordCounter[T ~string]() func(word T) map[T]int { res := make(map[T]int) return func(word T) map[T]int { res[word]++ return res } } func main() { var lexemeToFind lexeme = "i" wc := wordCounter[lexeme]() fmt.Println(wc(lexemeToFind)) } ``` ``` map[i:1] ``` Когда вызываем функцию `wordCounter`, также необходимо указывать тип, с которым будем работать. В данном случае этот тип — `lexeme`. В противном случае возникнет ошибка компиляции: ```go func main() { var lexemeToFind lexeme = "i" wc := wordCounter() fmt.Println(wc(lexemeToFind)) } ``` ``` ./main.go:18:21: in call to wordCounter, cannot infer T (declared at ./main.go:7:20) (exit status 1) ``` Таким образом, правильный вариант: ``` go func main() { var lexemeToFind lexeme = "i" wc := wordCounter[lexeme]() fmt.Println(wc(lexemeToFind)) } ``` ``` map[i:1] ``` Реализуйте функцию-обертку `hasPrefix` над встроенной `strings.HasPrefix`. Первый ее аргумент — текст, второй — префикс. Она должна проверить, содержит ли текст соответствующий префикс. Функция возвращает `true`, если содержит, и `false` — в противном случае. {.task_text} В качестве типов первого и второго аргумента функция должна принимать все псевдонимы типа `string`. Эти псевдонимы необязательно должны быть одинаковыми. {.task_text} ```go {.task_source #golang_chapter_0150_task_0040} package main import ( "fmt" "strings" ) // ваш код здесь func main() { type Username string var user1 Username = "angel336" type Prefix string var p Prefix = "angel" fmt.Println(hasPrefix(user1, p)) } ``` Используйте конструкции с тильдой. {.task_hint} ```go {.task_answer} package main import ( "fmt" "strings" ) func hasPrefix[T ~string, T2 ~string](s T, prefix T2) bool { return strings.HasPrefix(string(s), string(prefix)) } func main() { type Username string var user1 Username = "angel336" type Prefix string var p Prefix = "angel" fmt.Println(hasPrefix(user1, p)) } ``` ## Резюме 1. Дженерики бывают удобны, когда необходимо выполнять одинаковые действия над различными типами. 2. В Go существуют базовые — **basic**, и общие — **generic** интерфейсы. Общие интерфейсы могут определяться не только через набор методов, но и через конкретные типы. Они используются только в дженериках. 3. Дженерики используют не только с функциями, но и со структурами. 4. Тильда рядом с базовым типом означает, что дженерик работает не только с этим типом. Он работает со всеми его псевдонимами.

Следующие главы находятся в разработке

Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!