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

Go: Когда следует использовать дженерики? Когда не следует?

2025/10/04 23:00

Введение

Это версия блога моих выступлений на Google Open Source Live:

https://youtu.be/nr8EpUO9jhw?si=jlWTapr6NM6isLgt&embedable=true

и GopherCon 2021:

https://youtu.be/Pae9EeCdy8?si=M-87Eisb2nU1qmJ&embedable=true

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

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

Пишите код

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

Когда параметры типа полезны?

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

При использовании определенных языком типов контейнеров

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

\ Например, вот функция, которая возвращает срез всех ключей в карте любого типа:

// MapKeys returns a slice of all the keys in m. // The keys are not returned in any particular order. func MapKeys[Key comparable, Val any](m map[Key]Val) []Key {     s := make([]Key, 0, len(m))     for k := range m {         s = append(s, k)     }     return s } 

\ Этот код не предполагает ничего о типе ключа карты и вообще не использует тип значения карты. Он работает для любого типа карты. Это делает его хорошим кандидатом для использования параметров типа.

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

Структуры данных общего назначения

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

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

\ Например, вот часть того, как может выглядеть структура данных бинарного дерева с использованием параметров типа:

// Tree is a binary tree. type Tree[T any] struct {     cmp  func(T, T) int     root *node[T] }  // A node in a Tree. type node[T any] struct {     left, right  *node[T]     val          T }  // find returns a pointer to the node containing val, // or, if val is not present, a pointer to where it // would be placed if added. func (bt *Tree[T]) find(val T) **node[T] {     pl := &bt.root     for *pl != nil {         switch cmp := bt.cmp(val, (*pl).val); {         case cmp < 0:             pl = &(*pl).left         case cmp > 0:             pl = &(*pl).right         default:             return pl         }     }     return pl }  // Insert inserts val into bt if not already there, // and reports whether it was inserted. func (bt *Tree[T]) Insert(val T) bool {     pl := bt.find(val)     if *pl != nil {         return false     }     *pl = &node[T]{val: val}     return true } 

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

\ Это разумное использование параметров типа, потому что структура данных Tree, включая код в методах, в значительной степени не зависит от типа элемента T.

\ Структуре данных Tree нужно знать, как сравнивать значения типа элемента T; для этого она использует переданную функцию сравнения. Вы можете увидеть это в четвертой строке метода find, в вызове bt.cmp. Кроме этого, параметр типа вообще не имеет значения.

Для параметров типа предпочитайте функции методам

Пример Tree иллюстрирует еще одно общее руководство: когда вам нужно что-то вроде функции сравнения, предпочитайте функцию методу.

\ Мы могли бы определить тип Tree так, чтобы тип элемента должен был иметь метод Compare или Less. Это было бы сделано путем написания ограничения, требующего метод, что означает, что любой аргумент типа, используемый для инстанцирования типа Tree, должен иметь этот метод.

\ Следствием было бы то, что любой, кто хочет использовать Tree с простым типом данных, таким как int, должен был бы определить свой собственный целочисленный тип и написать свой собственный метод сравнения. Если мы определим Tree для принятия функции сравнения, как в коде, показанном выше, то легко передать желаемую функцию. Написать эту функцию сравнения так же легко, как написать метод.

\ Если тип элемента Tree уже имеет метод Compare, то мы можем просто использовать выражение метода, такое как ElementType.Compare, в качестве функции сравнения.

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

Реализация общего метода

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

\ Например, рассмотрим sort.Interface из стандартной библиотеки. Он требует, чтобы тип реализовывал три метода: Len, Swap и Less.

\ Вот пример обобщенного типа SliceFn, который реализует sort.Interface для любого типа среза:

// SliceFn implements sort.Interface for a slice of T. type SliceFn[T any] struct {     s    []T     less func(T, T) bool }  func (s SliceFn[T]) Len() int {     return len(s.s) } func (s SliceFn[T]) Swap(i, j int) {     s.s[i], s.s[j] = s.s[j], s.s[i] } func (s SliceFn[T]) Less(i, j int) bool {     return s.less(s.s[i], s.s[j]) } 

\ Для любого типа среза методы Len и Swap абсолютно одинаковы. Метод Less требует сравнения, что является частью Fn в имени SliceFn. Как и в предыдущем примере Tree, мы будем передавать функцию при создании SliceFn.

\ Вот как использовать SliceFn для сортировки любого среза с использованием функции сравнения:

// SortFn sorts s in place using a comparison function. func SortFn[T any](s []T, less func(T, T) bool) {     sort.Sort(SliceFn[T]{s, less}) } 

\ Это похоже на стандартную библиотечную функцию sort.Slice, но функция сравнения написана с использованием значений, а не индексов среза.

\ Использование параметров типа для такого рода кода уместно, потому что методы выглядят абсолютно одинаково для всех типов срезов.

\ (Я должен упомянуть, что Go 1.19–не 1.18–скорее всего, будет включать обобщенную функцию для сортировки среза с использованием функции сравнения, и эта обобщенная функция, скорее всего, не будет использовать sort.Interface. См. предложение #47619. Но общий момент все еще верен, даже если этот конкретный пример, скорее всего, не будет полезен: разумно использовать параметры типа, когда вам нужно реализовать методы, которые выглядят одинаково для всех соответствующих типов.)

Когда параметры типа не полезны?

Теперь давайте поговорим о другой стороне вопроса: когда не использовать параметры типа.

Не заменяйте интерфейсные типы параметрами типа

Как мы все знаем, в Go есть интерфейсные типы. Интерфейсные типы позволяют использовать своего рода обобщенное программирование.

\ Например, широко используемый интерфейс io.Reader предоставляет обобщенный механизм для чтения данных из любого значения, которое содержит информацию (например, файл) или которое производит информацию (например, генератор случайных чисел). Если все, что вам нужно сделать со значением некоторого типа, - это вызвать метод для этого значения, используйте интерфейсный тип, а не параметр типа. io.Reader легко читается, эффективен и действенен. Нет необходимости использовать параметр типа для чтения данных из значения, вызывая метод Read.

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

func ReadSome(r io.Reader) ([]byte, error)  func ReadSome[T io.Reader](r T) ([]byte, error) 

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

\ Стоит подчеркнуть последний момент. Хотя возможно реализовать дженерики несколькими различными способами, и реализации будут меняться и улучшаться со временем, реализация, используемая в Go 1.18, во многих случаях будет обрабатывать значения, тип которых является параметром типа, так же, как значения, тип которых является интер

Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу [email protected] для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.

Вам также может быть интересно

Почему ОАЭ делают ставку на биткоин и готовятся к масштабному внедрению крипты

Почему ОАЭ делают ставку на биткоин и готовятся к масштабному внедрению крипты

Объединённые Арабские Эмираты не выбирают между биткоином и остальной криптоиндустрией. Страна развивает оба направления одновременно, но делает это поэтапно и
Поделиться
Coinspot2025/12/13 14:00