пятница, 20 мая 2022 г.

Начало работы с дженериками в Golang

В этом руководстве представлены основы дженериков в Go. С помощью дженериков вы можете объявлять и использовать функции или типы, которые написаны для работы с любым из набора типов, предоставляемых вызывающим кодом.

В этом руководстве вы объявите две простые неуниверсальные функции, а затем зафиксируете ту же логику в одной универсальной функции.

Предпосылки

  • Установка Go 1.18 или более поздней версии.
  • Инструмент для редактирования вашего кода. Любой текстовый редактор, который у вас есть, будет работать нормально.
  • Командный терминал. Go хорошо работает с любым терминалом в Linux и Mac, а также с PowerShell или cmd в Windows.

Создайте папку для своего кода

Для начала создайте папку для кода, который вы будете писать.

1. Откройте командную строку и перейдите в свой домашний каталог.

В Linux или Mac:

$ cd

В Windows:

C:\> cd %HOMEPATH%

В остальной части руководства в качестве подсказки будет отображаться символ $. Используемые вами команды будут работать и в Windows.

2. В командной строке создайте каталог для своего кода с именем generics.

$ mkdir generics
$ cd generics

3. Создайте модуль для хранения вашего кода.

Запустите команду инициализации go mod, указав путь к модулю вашего нового кода.

$ go mod init example/generics
go: creating new go.mod: module example/generics

Примечание. Для производственного кода вы должны указать путь к модулю, который больше соответствует вашим потребностям.

Далее вы добавите простой код для работы с картами.

Добавьте неуниверсальные функции

На этом шаге вы добавите две функции, каждая из которых суммирует значения карты и возвращает итог.

Вы объявляете две функции вместо одной, потому что работаете с картами двух разных типов: в одной хранятся значения int64, а в другой хранятся значения float64.

Напишите код

1. Используя текстовый редактор, создайте файл с именем main.go в каталоге generics. Вы будете писать свой код Go в этом файле.

2. В файл main.go в верхней части файла вставьте следующее объявление пакета.

package main

Автономная программа (в отличие от библиотеки) всегда находится в пакете main.

3. Под объявлением пакета вставьте следующие два объявления функций.

// SumInts суммирует значения m.
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {
        s += v
    }
    return s
}

// SumFloats суммирует значения m.
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {
        s += v
    }
    return s
}

В этом коде вы объявляете две функции для сложения значений карты и возврата суммы.

SumFloats преобразует строку в значения float64.

SumInts преобразует строку в значения int64.

4. В верхней части main.go под объявлением пакета вставьте следующую main функцию, чтобы инициализировать две карты и использовать их в качестве аргументов при вызове функций, которые вы объявили на предыдущем шаге.

func main() {
    // Инициализировать карту для целочисленных значений
    ints := map[string]int64{
        "first":  34,
        "second": 12,
    }

    // Инициализируем карту для значений с плавающей запятой
    floats := map[string]float64{
        "first":  35.98,
        "second": 26.99,
    }

    fmt.Printf("Non-Generic Sums: %v and %v\n",
        SumInts(ints),
        SumFloats(floats))
}

В этом коде вы:

  • Инициализируете карту значений float64 и карту значений int64, каждая с двумя записями.
  • Вызывате две объявленные ранее функции, чтобы найти сумму значений каждой карты.
  • Распечатываете результат.

5. В верхней части main.go, прямо под объявлением пакета, импортируйте пакет, который вам понадобится для поддержки кода, который вы только что написали.

Первые строки кода должны выглядеть так:

package main

import "fmt"

6. Сохраните main.go.

Запустите код

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .
Non-Generic Sums: 46 and 62.97

С дженериками здесь можно написать одну функцию вместо двух. Далее вы добавите одну общую функцию для карт, содержащих либо целочисленные значения, либо значения с плавающей запятой.

Добавьте общую функцию для обработки нескольких типов

В этом разделе вы добавите одну универсальную функцию, которая может получать карту, содержащую либо целые числа, либо значения с плавающей запятой, эффективно заменяя две функции, которые вы только что написали, одной функцией.

Для поддержки значений любого типа этой единственной функции потребуется способ объявить, какие типы она поддерживает. С другой стороны, вызывающему коду потребуется способ указать, вызывается ли он с целочисленной или плавающей картой.

Для поддержки этого вы напишете функцию, которая объявляет параметры типа в дополнение к своим обычным параметрам функции. Эти параметры типа делают функцию универсальной, позволяя ей работать с аргументами разных типов. Вы будете вызывать функцию с аргументами типа и обычными аргументами функции.

Каждый параметр типа имеет ограничение типа, которое действует как своего рода метатип для параметра типа. Каждое ограничение типа указывает допустимые аргументы типа, которые вызывающий код может использовать для соответствующего параметра типа.

В то время как ограничение параметра типа обычно представляет набор типов, во время компиляции параметр типа обозначает один тип — тип, предоставляемый вызывающим кодом в качестве аргумента типа. Если тип аргумента типа не разрешен ограничением параметра типа, код не будет компилироваться.

Имейте в виду, что параметр типа должен поддерживать все операции, выполняемые над ним универсальным кодом. Например, если бы код вашей функции пытался выполнить строковые операции (например, индексацию) над параметром типа, ограничение которого включало числовые типы, код не скомпилировался бы.

В коде, который вы собираетесь написать, вы будете использовать ограничение, допускающее целочисленные типы или типы с плавающей запятой.

Напишите код

1. Под двумя функциями, которые вы добавили ранее, вставьте следующую общую функцию.

// SumIntsOrFloats суммирует значения карты m.
// Поддерживает как int64, так и float64 
// как типы для значений карты.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

В этом коде вы:

  • Объявите функцию SumIntsOrFloats с двумя параметрами типа (в квадратных скобках), K и V, и одним аргументом, который использует параметры типа, m типа map[K]V. Функция возвращает значение типа V.
  • Укажете для параметра типа K сопоставимое ограничение типа. Предназначенное специально для таких случаев, сопоставимое ограничение уже объявлено в Go. Он допускает любой тип, значения которого можно использовать в качестве операнда операторов сравнения == и !=. Go требует, чтобы ключи сопоставления были сопоставимы. Поэтому необходимо объявить K сопоставимым, чтобы вы могли использовать K в качестве ключа в переменной карты. Это также гарантирует, что вызывающий код использует допустимый тип для ключей сопоставления.
  • Укажете для параметра типа V ограничение, представляющее собой объединение двух типов: int64 и float64. Использование | задает объединение двух типов, что означает, что это ограничение допускает любой тип. Любой тип будет разрешен компилятором в качестве аргумента в вызывающем коде.
  • Укажете, что аргумент m имеет тип map[K]V, где K и V — это типы, уже указанные для параметров типа. Обратите внимание, что мы знаем, что map[K]V является допустимым типом карты, потому что K является сопоставимым типом. Если бы мы не объявили K сопоставимым, компилятор отклонил бы ссылку на map[K]V.

2. В main.go под уже имеющимся кодом вставьте следующий код.

fmt.Printf("Generic Sums: %v and %v\n",
    SumIntsOrFloats[string, int64](ints),
    SumIntsOrFloats[string, float64](floats))

В этом коде вы:

  • Вызовете общую функцию, которую вы только что объявили, передав каждую из созданных вами карт.
  • Укажете аргументы типа — имена типов в квадратных скобках — чтобы было ясно, какие типы должны заменить параметры типа в вызываемой вами функции.
    Как вы увидите в следующем разделе, вы часто можете опустить аргументы типа в вызове функции. Go часто может вывести их из вашего кода.
  • Выведете суммы, возвращаемые функцией.
Запустите код

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97

Чтобы выполнить ваш код, в каждом вызове компилятор заменял параметры типа конкретными типами, указанными в этом вызове.

При вызове написанной вами универсальной функции вы указали аргументы типа, которые сообщали компилятору, какие типы использовать вместо параметров типа функции. Как вы увидите в следующем разделе, во многих случаях вы можете опустить эти аргументы типа, потому что компилятор может их вывести.

Удалить аргументы типа при вызове универсальной функции

В этом разделе вы добавите модифицированную версию универсального вызова функции, внеся небольшие изменения для упрощения вызывающего кода. Вы удалите аргументы типа, которые в данном случае не нужны.

Вы можете опустить аргументы типа в вызывающем коде, когда компилятор Go может вывести типы, которые вы хотите использовать. Компилятор выводит аргументы типа из типов аргументов функции.

Обратите внимание, что это не всегда возможно. Например, если вам нужно вызвать универсальную функцию без аргументов, вам нужно будет включить аргументы типа в вызов функции.

Напишите код

В main.go под уже имеющимся кодом вставьте следующий код.

fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
    SumIntsOrFloats(ints),
    SumIntsOrFloats(floats))

В этом коде вы вызовете универсальную функцию, опуская аргументы типа.

Запустите код

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97

Далее вы еще больше упростите функцию, захватив объединение целых чисел и чисел с плавающей запятой в ограничение типа, которое вы можете повторно использовать, например, из другого кода.

Объявите ограничение типа

В этом последнем разделе вы переместите ограничение, которое вы определили ранее, в его собственный интерфейс, чтобы вы могли повторно использовать его в нескольких местах. Объявление ограничений таким образом помогает упростить код, например, когда ограничение более сложное.

Вы объявляете ограничение типа как интерфейс. Ограничение позволяет любому типу реализовать интерфейс. Например, если вы объявляете интерфейс ограничения типа с тремя методами, а затем используете его с параметром типа в универсальной функции, аргументы типа, используемые для вызова функции, должны иметь все эти методы.

Интерфейсы ограничений также могут ссылаться на определенные типы, как вы увидите в этом разделе.

Напишите код

1. Чуть выше main функции, сразу после операторов импорта, вставьте следующий код, чтобы объявить ограничение типа.

type Number interface {
    int64 | float64
}

В этом коде вы:

  • Объявите тип интерфейса Number для использования в качестве ограничения типа.
  • Объявите объединение int64 и float64 внутри интерфейса.
    По сути, вы перемещаете объединение из объявления функции в ограничение нового типа. Таким образом, если вы хотите ограничить параметр типа значением int64 или float64, вы можете использовать это ограничение числового типа вместо записи int64 | float64.

2. Под уже имеющимися функциями вставьте следующую общую функцию SumNumbers.

// SumNumbers суммирует значения карты m. 
// Поддерживает целые числа и 
// числа с плавающей точкой как значения карты.
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

В этом коде вы объявляете универсальную функцию с той же логикой, что и универсальная функция, которую вы объявили ранее, но с новым типом интерфейса вместо объединения в качестве ограничения типа. Как и прежде, вы используете параметры типа для типов аргументов и возвращаемых значений.

3. В main.go под уже имеющимся кодом вставьте следующий код.

fmt.Printf("Generic Sums with Constraint: %v and %v\n",
    SumNumbers(ints),
    SumNumbers(floats))

В этом коде вы вызовете SumNumbers с каждой картой, выводя сумму из значений каждой.

Как и в предыдущем разделе, вы опускаете аргументы типа (имена типов в квадратных скобках) в вызовах универсальной функции. Компилятор Go может вывести аргумент типа из других аргументов.

Запустите код

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97

Завершенный код

Вы можете запустить эту программу на игровой площадке Go. На игровой площадке просто нажмите кнопку Run.

package main

import "fmt"

type Number interface {
    int64 | float64
}

func main() {
    // Инициализируем карту для целочисленных значений
    ints := map[string]int64{
        "first": 34,
        "second": 12,
    }

    // Инициализируем карту для значений с плавающей точкой
    floats := map[string]float64{
        "first": 35.98,
        "second": 26.99,
    }

    fmt.Printf("Non-Generic Sums: %v and %v\n",
        SumInts(ints),
        SumFloats(floats))

    fmt.Printf("Generic Sums: %v and %v\n",
        SumIntsOrFloats[string, int64](ints),
        SumIntsOrFloats[string, float64](floats))

    fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
        SumIntsOrFloats(ints),
        SumIntsOrFloats(floats))

    fmt.Printf("Generic Sums with Constraint: %v and %v\n",
        SumNumbers(ints),
        SumNumbers(floats))
}

// SumInts суммирует значения m.
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {
        s += v
    }
    return s
}

// SumFloats суммирует значения m.
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {
        s += v
    }
    return s
}

// SumIntsOrFloats суммирует значения карты m. 
// Поддерживает как числа с плавающей запятой, так и целые числа
// как значения карты.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

// SumNumbers суммирует значения карты m. 
// Поддерживает как числа с плавающей запятой, так и целые числа
// как значения карты.
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

Вывод:

Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97


Читайте также:


понедельник, 2 мая 2022 г.

Введение в использование дженериков в Golang

В релизе Go 1.18 добавлена поддержка дженериков. Дженерики — это самое большое изменение, которое было внесено в Go с момента первого релиза с открытым исходным кодом. В этом посте вы познакомитесь с новыми функциями языка. Мы не будем пытаться охватить все детали, но затронем все важные моменты.

Дженерики — это способ написания кода, который не зависит от используемых конкретных типов. Функции и типы теперь могут быть написаны для использования любого набора типов.

Дженерики добавляют в язык три новых важных вещи:

  • Параметры типа для функции и типов.
  • Определение типов интерфейсов как наборов типов, включая типы, не имеющие методов.
  • Вывод типа, позволяющий во многих случаях опускать аргументы типа при вызове функции.

Параметры типа

Функции и типы теперь могут иметь параметры типа. Список параметров типа выглядит как обычный список параметров, за исключением того, что в нем используются квадратные скобки вместо скобок.

Чтобы показать, как это работает, давайте начнем с базовой неуниверсальной функции Min для значений с плавающей запятой:

func Min(x, y float64) float64 {
    if x < y {
        return x
    }
    return y
}

Мы можем сделать эту функцию универсальной, чтобы она работала для разных типов, добавив список параметров типа. В этом примере мы добавляем список параметров типа с одним параметром типа T и заменяем использование float64 на T.

import "golang.org/x/exp/constraints"

func GMin[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}

Теперь можно вызвать эту функцию с аргументом типа, написав вызов вида

x := GMin[int](2, 3)

Предоставление GMin аргумента типа, в данном случае int, называется созданием экземпляра. Создание экземпляра происходит в два этапа. Во-первых, компилятор заменяет все аргументы типа соответствующими параметрами типа во всей универсальной функции или типе. Во-вторых, компилятор проверяет, что каждый аргумент типа удовлетворяет соответствующему ограничению. Вскоре мы доберемся до того, что это значит, но если второй шаг не удается, создание экземпляра завершается неудачно, и программа становится недействительной.

После успешного создания экземпляра у нас есть необобщенная функция, которую можно вызывать так же, как и любую другую функцию. Например, в таком коде

fmin := GMin[float64]
m := fmin(2.71, 3.14)

экземпляр GMin[float64] создает то, что фактически является нашей исходной функцией Min с плавающей запятой, и мы можем использовать это в вызове функции.

Параметры типа также могут использоваться с типами.

type Tree[T interface{}] struct {
    left, right *Tree[T]
    value       T
}

func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

var stringTree Tree[string]

Здесь дерево универсального типа хранит значения параметра типа T. Универсальные типы могут иметь методы, такие как Lookup в этом примере. Чтобы использовать универсальный тип, необходимо создать его экземпляр; Tree[string] — это пример создания экземпляра Tree с типом аргумента string.

Наборы типов

Давайте немного подробнее рассмотрим аргументы типа, которые можно использовать для создания экземпляра параметра типа.

Обычная функция имеет тип для каждого параметра значения; этот тип определяет набор значений. Например, если у нас есть тип float64, как в неуниверсальной функции Min выше, допустимый набор значений аргументов — это набор значений с плавающей запятой, которые могут быть представлены типом float64.

Точно так же наборы параметров типа имеют тип для каждого параметра типа. Поскольку параметр типа сам по себе является типом, типы параметров типа определяют наборы типов. Этот метатип называется ограничением типа (type constraint).

В универсальном GMin ограничение типа импортируется из пакета constraints. Ограничение Ordered описывает набор всех типов со значениями, которые можно упорядочить или, другими словами, сравнить с оператором < (или <= , > и т. д.). Ограничение гарантирует, что в GMin могут быть переданы только типы с упорядочиваемыми значениями. Это также означает, что в теле функции GMin значения параметра этого типа могут использоваться для сравнения с оператором <.

В Go ограничения типов должны быть интерфейсами. То есть тип интерфейса может использоваться как тип значения, а также может использоваться как метатип. Интерфейсы определяют методы, поэтому очевидно, что мы можем выразить ограничения типов, которые требуют наличия определенных методов. Но constraints.Ordered — это тоже интерфейсный тип, а оператор < не является методом.

Чтобы это работало, мы смотрим на интерфейсы по-новому.

До недавнего времени в спецификации Go говорилось, что интерфейс определяет набор методов, который примерно представляет собой набор методов, перечисленных в интерфейсе. Любой тип, который реализует все эти методы, реализует этот интерфейс.

Два представления приводят к одному и тому же результату: для каждого набора методов мы можем представить соответствующий набор типов, реализующих эти методы, и это набор типов, определенных интерфейсом.

Однако для наших целей представление набора типов имеет преимущество перед представлением набора методов: мы можем явно добавлять типы в набор и, таким образом, управлять набором типов новыми способами.

Синтаксис типов интерфейса был расширен, чтобы это работало. Например, interface{ int|string|bool } определяет набор типов, содержащий типы int, string и bool.

Другими словами, этому интерфейсу удовлетворяют только int, string или bool.

Теперь давайте посмотрим на фактическое определение contraints.Ordered:

type Ordered interface {
    Integer|Float|~string
}

В этом объявлении говорится, что интерфейс Ordered представляет собой набор всех типов целых чисел, чисел с плавающей запятой и строк. Вертикальная черта обозначает объединение типов (или наборов типов в данном случае). Integer и Float — это типы интерфейса, которые аналогичным образом определены в пакете contraints. Обратите внимание, что нет методов, определенных интерфейсом Ordered.

Для ограничений типа мы обычно не заботимся о конкретном типе, таком как string; нас интересуют все типы строк. Именно для этого используется токен ~. Выражение ~string означает набор всех типов, базовым типом которых является string. Это включает в себя сам тип string, а также все типы, объявленные с такими определениями, как type MyString string.

Конечно, мы по-прежнему хотим указывать методы в интерфейсах, и мы хотим быть обратно совместимыми. В Go 1.18 интерфейс может содержать методы и встроенные интерфейсы, как и раньше, но он также может включать неинтерфейсные типы, объединения (union) и наборы базовых типов.

При использовании в качестве ограничения типа набор типов, определенный интерфейсом, указывает именно те типы, которые разрешены в качестве аргументов типа для соответствующего параметра типа. В теле универсальной функции, если тип операнда является параметром типа P с ограничением C, операции разрешены, если они разрешены всеми типами в наборе типов C (в настоящее время здесь есть некоторые ограничения реализации, но обычный код вряд ли столкнетесь с ними).

Интерфейсам, используемым в качестве ограничений, могут быть присвоены имена (например, Ordered), или они могут быть литеральными интерфейсами, встроенными в список параметров типа. Например:

[S interface{~[]E}, E interface{}]

Здесь S должен быть типом среза, тип элемента которого может быть любым.

Поскольку это распространенный случай, охватывающий interface{} может быть опущен для интерфейсов в позиции ограничения, и мы можем просто написать:

[S ~[]E, E interface{}]

Поскольку пустой интерфейс распространен в списках параметров типа и в обычном коде Go, в этом отношении Go 1.18 вводит новый предварительно объявленный идентификатор any в качестве псевдонима для пустого типа интерфейса. При этом мы приходим к этому идиоматическому коду:

[S ~[]E, E any]

Интерфейсы как наборы типов — это новый мощный механизм, который является ключом к тому, чтобы ограничения типов работали в Go. На данный момент интерфейсы, использующие новые синтаксические формы, могут использоваться только в качестве ограничений. Но нетрудно представить, насколько полезными в целом могут быть интерфейсы с явным ограничением типов.

Вывод типа

Последней новой крупной функцией языка является вывод типов. В некотором смысле это самое сложное изменение языка, но оно важно, поскольку позволяет людям использовать естественный стиль при написании кода, вызывающего универсальные функции.

Вывод типа аргумента функции

С параметрами типа возникает необходимость передавать аргументы типа, что может привести к многословному коду. Вернемся к нашей общей функции GMin:

func GMin[T constraints.Ordered](x, y T) T { ... }

параметр типа T используется для указания типов обычных нетиповых аргументов x и y. Как мы видели ранее, это можно вызвать с явным аргументом типа

var a, b, m float64

m = GMin[float64](a, b) // явный аргумент типа

Во многих случаях компилятор может вывести аргумент типа для T из обычных аргументов. Это делает код короче, оставаясь при этом понятным.

var a, b, m float64

m = GMin(a, b) // нет аргумента типа

Это работает путем сопоставления типов аргументов a и b с типами параметров x и y.

Этот тип вывода, который выводит аргументы типа из типов аргументов функции, называется выводом типа аргумента функции.

Вывод типа аргумента функции работает только для параметров типа, которые используются в параметрах функции, а не для параметров типа, используемых только в результатах функции или только в теле функции. Например, это не относится к таким функциям, как MakeT[T any]() T, которые используют только T для результата.

Вывод типа ограничения

Язык поддерживает другой вид вывода типа, вывод типа ограничения. Чтобы описать его, давайте начнем с этого примера масштабирования среза целых чисел:

// Scale возвращает копию s с каждым элементом, умноженным на c.
// У этой реализации есть проблема, как мы увидим.
func Scale[E constraints.Integer](s []E, c E) []E {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

Это общая функция, которая работает для среза любого целочисленного типа.

Теперь предположим, что у нас есть многомерный тип Point, где каждый Point — это просто список целых чисел, задающих координаты точки. Естественно, этот тип будет иметь несколько методов.

type Point []int32

func (p Point) String() string {
    // Детали не важны.
}

Иногда мы хотим масштабировать Point. Поскольку Point — это просто срез целых чисел, мы можем использовать функцию Scale, которую мы написали ранее:

// ScaleAndPrint удваивает Point и печатает ее.
func ScaleAndPrint(p Point) {
    r := Scale(p, 2)
    fmt.Println(r.String()) // НЕ КОМПИЛИРУЕТСЯ
}

К сожалению, это не компилируется, выдавая ошибку типа r.String undefined (тип []int32 не имеет поля или метода String).

Проблема в том, что функция Scale возвращает значение типа []E, где E — тип элемента среза аргумента. Когда мы вызываем Scale со значением типа Point, базовым типом которого является []int32, мы возвращаем значение типа []int32, а не типа Point. Это следует из того, как написан универсальный код, но это не то, что нам нужно.

Чтобы исправить это, мы должны изменить функцию Scale, чтобы использовать параметр типа для типа среза.

// Scale возвращает копию s с каждым элементом, умноженным на c.
func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
    r := make(S, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

Мы ввели новый параметр типа S, который является типом аргумента среза. Мы наложили на него ограничения таким образом, что базовым типом является S, а не []E, а тип результата теперь равен S. Поскольку E ограничено целым числом, эффект тот же, что и раньше: первый аргумент должен быть срез некоторого целочисленного типа. Единственное изменение в теле функции заключается в том, что теперь мы передаем S, а не []E, когда вызываем make.

Новая функция действует так же, как и раньше, если мы вызываем ее с помощью простого среза, но если мы вызываем ее с типом Point, мы теперь возвращаем значение типа Point. Это то, чего мы хотим. В этой версии Scale предыдущая функция ScaleAndPrint будет компилироваться и работать, как мы и ожидали.

Но справедливо спросить: почему можно написать вызов Scale без передачи явных аргументов типа? То есть, почему мы можем писать Scale(p, 2) без аргументов типа вместо того, чтобы писать Scale[Point, int32](p, 2)? Наша новая функция Scale имеет два параметра типа, S и E. При вызове Scale без передачи каких-либо аргументов типа вывод типа аргумента функции, описанный выше, позволяет компилятору сделать вывод, что аргумент типа для S — это Point. Но функция также имеет параметр типа E, который является типом коэффициента умножения c. Соответствующий аргумент функции равен 2, и поскольку 2 является нетипизированной константой, вывод типа аргумента функции не может вывести правильный тип для E (в лучшем случае он может вывести тип по умолчанию для 2, который является int и который будет неправильным). Вместо этого процесс, посредством которого компилятор делает вывод о том, что аргумент типа для E является типом элемента среза, называется выводом типа ограничения.

Вывод типа ограничения выводит аргументы типа из ограничений параметров типа. Он используется, когда один параметр типа имеет ограничение, определенное в терминах другого параметра типа. Когда аргумент типа одного из этих параметров типа известен, ограничение используется для вывода аргумента типа другого.

Обычно это применяется, когда одно ограничение использует форму ~type для некоторого типа, где этот тип записывается с использованием других параметров типа. Мы видим это в примере Scale. S — это ~[]E, за которым следует тип []E, записанный в терминах другого параметра типа. Если мы знаем аргумент типа для S, мы можем вывести аргумент типа для E. S — это тип среза, а E — тип элемента этого среза.

Вывод типа на практике

Точные детали того, как работает вывод типов, сложны, но их использование — нет: вывод типов либо завершается успешно, либо терпит неудачу. В случае успеха аргументы типа могут быть опущены, а вызов универсальных функций ничем не отличается от вызова обычных функций. Если вывод типа не удался, компилятор выдаст сообщение об ошибке, и в таких случаях мы можем просто предоставить необходимые аргументы типа.

Добавляя в язык вывод типов, разработчики пытались найти баланс между мощностью вывода и сложностью. Разработчики хотели гарантировать, что когда компилятор выводит типы, эти типы никогда не будут неожиданными. Разработчики старались быть осторожными, чтобы ошибиться в сторону неспособности вывести тип, а не в сторону вывода неправильного типа. Вероятно вывод типа продолжат улучшать в будущих релизах. В результате можно будет написать больше программ без явных аргументов типа. Программам, которым сегодня не нужны аргументы типов, они не понадобятся и завтра.

Заключение

Дженерики — это большая новая функция языка в версии 1.18. Эти новые языковые изменения потребовали большого количества нового кода, который не подвергался серьезному тестированию в производственных условиях. Это произойдет только тогда, когда все больше людей будут писать и использовать универсальный код. Считается, что эта функция реализована хорошо и качественно. Однако, в отличие от большинства аспектов Go, не возможно подкрепить это убеждение реальным опытом. Поэтому, хотя поощряется использование дженериков там, где это имеет смысл, соблюдайте осторожность при развертывании универсального кода в рабочей среде.


Читайте также: