Обмен технологиями

Подробное объяснение дженериков Go

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Введение

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

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

func MinInt(x, y int) int {
	if x < y {
		return x
	}
	return y
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

func min[T int | float64](x, y T) T {
	if x < y {
		return x
	}
	return y
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Дженерики

Официальный документ сайта:https://go.dev/blog/intro-generics
Обобщения добавляют в язык три важные новые функции:

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

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

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

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

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

func main() {
	x := GMin[int](2, 3)
	fmt.Println(x) // 输出结果为2
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

Среди них ограничения.Ordered — это пользовательский тип (исходный код здесь не показан).
Если вы не понимаете, вы можете временно заменить ограничения. Заказывается у ·int | float64

Предоставление параметра типа (в данном случае int) GMin называется созданием экземпляра. Создание экземпляра происходит в два этапа.

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

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

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

Все коды

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

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

func main() {
	fmin := GMin[float64] // 相当于func GMin(x, y float64) float64{...}
	m := fmin(2.71, 3.14)
	fmt.Println(m) // 输出结果为2.71
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

Создание экземпляра 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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

Типовые наборы

Каждый параметр типа в списке параметров типа имеет тип. Поскольку параметр типа сам по себе является типом, тип параметра типа определяет набор типов.Этот метатип называетсяограничения типа
В универсальном методе GMin ограничения типа импортируются из пакета ограничений. Упорядоченные ограничения описывают все типы коллекций, которые имеют значения, которые можно упорядочить или, другими словами, сравнить с помощью оператора &lt; (или &lt;=, &gt; и т. д.). Это ограничение гарантирует, что в GMin можно будет передавать только типы с сортируемыми значениями. Это также означает, что в теле функции GMin значение этого параметра типа можно использовать для сравнения с оператором &lt;.
В Go ограничения типов должны быть интерфейсами. . То есть типы интерфейса могут использоваться как типы значений или метатипы. Интерфейсы определяют методы, поэтому очевидно, что мы можем выражать ограничения типов, требующие наличия определенных методов. Но ограничения.Ordered также является типом интерфейса, а оператор &lt; не является методом.
Двойное назначение типов интерфейса действительно является важной концепцией языка Go. Давайте разберемся глубже и проиллюстрируем примерами утверждение «типы интерфейса могут использоваться как типы значений, а также как метатипы» [1][2][3][4][5].

  1. Интерфейс как тип значения:

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

Например:

type Stringer interface {
    String() string
}

type Person struct {
    Name string
}

func (p Person) String() string {
    return p.Name
}

var s Stringer = Person{"Alice"} // Person 实现了 Stringer 接口
fmt.Println(s.String()) // 输出: Alice
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

В этом примереStringer Интерфейсы используются как типы значений,Person Тип реализуетString() метод, поэтому его можно назначитьStringer тип переменной.

  1. Интерфейс как метатип:

Когда интерфейс используется в качестве метатипа, он определяет набор ограничений типов для использования в универсальном программировании. Это новое использование после появления дженериков в Go 1.18.

Например:

type Ordered interface {
    int | float64 | string
}

func Min[T Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

fmt.Println(Min(3, 5))       // 输出: 3
fmt.Println(Min(3.14, 2.71)) // 输出: 2.71
fmt.Println(Min("a", "b"))   // 输出: a
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

В этом примереOrdered Интерфейсы используются как метатипы, определяющие набор типов (целые числа, числа с плавающей запятой и строки), которые можно сравнивать.Min Функции используют этот интерфейс как ограничение типа и могут принимать любыеOrdered Тип ограничения в качестве аргумента.

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

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

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

Для достижения этой цели мы расширили синтаксис типов интерфейсов. Например, интерфейс { int|string|bool } определяет набор типов, содержащий типы int, string и bool.
Вставьте сюда описание изображения
Другими словами, интерфейс удовлетворяет только int, string или bool.

Теперь давайте посмотрим на фактическое определение ограничений. По порядку:

type Ordered interface {
    Integer|Float|~string
}
  • 1
  • 2
  • 3

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

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

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

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

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

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

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

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

Поскольку пустые интерфейсы часто встречаются в списках параметров типа, а также в обычном коде Go, Go 1.18 вводит новый предварительно объявленный идентификатор Any в качестве псевдонима для пустых типов интерфейса. Таким образом, мы получаем такой идиоматический код:

[S ~[]E, E any]
  • 1

Вывод типа

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