Щоб було зрозуміло, я надам загальні рекомендації, а не жорсткі правила. Використовуйте власне судження. Але якщо ви не впевнені, я рекомендую дотримуватися рекомендацій, обговорених тут.Щоб було зрозуміло, я надам загальні рекомендації, а не жорсткі правила. Використовуйте власне судження. Але якщо ви не впевнені, я рекомендую дотримуватися рекомендацій, обговорених тут.

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)
Відмова від відповідальності: статті, опубліковані на цьому сайті, взяті з відкритих джерел і надаються виключно для інформаційних цілей. Вони не обов'язково відображають погляди MEXC. Всі права залишаються за авторами оригінальних статей. Якщо ви вважаєте, що будь-який контент порушує права третіх осіб, будь ласка, зверніться за адресою [email protected] для його видалення. MEXC не дає жодних гарантій щодо точності, повноти або своєчасності вмісту і не несе відповідальності за будь-які дії, вчинені на основі наданої інформації. Вміст не є фінансовою, юридичною або іншою професійною порадою і не повинен розглядатися як рекомендація або схвалення з боку MEXC.

Вам також може сподобатися