Funkcja generyczna
Parametry typów zapisuje się w nawiasach kwadratowych przed zwykłymi parametrami. Każdy z nich ma ograniczenie, które mówi, jakie typy są dozwolone.
T i U są wnioskowane z argumentów, więc rzadko piszesz Map[int, string](...). Inferencja działa na podstawie argumentów funkcji; nie potrafi wywnioskować typu, który występuje tylko w wyniku. Dla func Zero[T any]() T musisz napisać Zero[int]().
Przed Go 1.18 tę funkcję trzeba było pisać osobno dla każdego typu albo przyjmować []interface{} i tracić bezpieczeństwo typów.
Ograniczenia
Ograniczenie (constraint) to interfejs. Określa, jakich typów można użyć, a w zamian mówi kompilatorowi, jakie operacje są dozwolone na wartościach tego typu.
| Ograniczenie | Dopuszcza | Pozwala użyć |
|---|---|---|
any | każdy typ | przypisania, przekazywania, przechowywania |
comparable | typy obsługujące == | ==, !=, kluczy mapy |
cmp.Ordered (Go 1.21) | liczby całkowite, zmiennoprzecinkowe, stringi | <, <=, >, >=, a także == |
| interfejs z metodami | typy z tymi metodami | wywoływania tych metod |
unia typów, jak ~int | ~float64 | dokładnie te typy | operatorów obsługiwanych przez wszystkie z nich |
Z any zamiast comparable wyrażenie v == target się nie kompiluje: invalid operation: v == target (incomparable types in type set). To ograniczenie sprawia, że operator jest dozwolony.
Te dwie funkcje pomocnicze już istnieją: slices.Index i slices.Max. Standardowe pakiety slices, maps i cmp pokrywają większość codziennych potrzeb generycznych, więc sprawdź je, zanim napiszesz własne.
Własne ograniczenia z uniami typów i ~
Ograniczenie może wymieniać typy przez |. Dozwolone są te operatory, które obsługuje każdy z wymienionych typów.
Tylda ma znaczenie. ~int64 oznacza "dowolny typ, którego typem bazowym jest int64", więc nazwany typ Cents zostaje zaakceptowany. Zapisane jako zwykłe int64, Sum([]Cents{...}) kończy się błędem Cents does not satisfy Number (possibly missing ~ for int64 in Number).
Interfejsów zawierających unie typów można używać tylko jako ograniczeń, nigdy jako zwykłych typów zmiennych. var n Number się nie kompiluje.
Pakiet golang.org/x/exp/constraints ma gotowe ograniczenia Integer, Float i Signed. Jest poza biblioteką standardową, więc tutejsze środowisko uruchomieniowe nie może go zaimportować; standardowym ograniczeniem jest cmp.Ordered.
Typy generyczne
Struktury, slice'y, mapy i inne typy też mogą mieć parametry typów. Metody typu generycznego używają parametrów odbiorcy.
Dwa szczegóły z tego przykładu pojawiają się bez przerwy:
- Wartość zerowa T.
var zero Tto sposób na zwrócenie "niczego" z kodu generycznego. Nie maT{}aninil, które działałyby dla każdego typu. - Instancjonowanie. Typ generyczny trzeba zinstancjonować przed użyciem:
Stack[string], a nieStack. Wewnątrz jego własnych metod odbiorcę zapisuje się jakoStack[T].
Go 1.24 dodało generyczne aliasy typów: type Set[T comparable] = map[T]struct{} teraz się kompiluje.
Ograniczenia, na które trafisz
- Brak parametrów typów w metodach.
func (s *Stack[T]) Map[U any](...)jest niedozwolone. Użyj funkcji na najwyższym poziomie. - Brak specjalizacji. Nie da się napisać osobnej implementacji dla
T = string. Obejściem jest switch typów naany(v)wewnątrz funkcji, a zwykle jest to znak, że generyki to złe narzędzie. - Brak ograniczeń operatorów po nazwie. Nie da się powiedzieć "dowolny typ z metodą
+"; wymieniasz typy w unii. - Dostęp do pól przez ograniczenie nie działa. Nawet jeśli każdy typ w unii ma pole
ID,v.IDjest niedozwolone. Zamiast tego użyj metody w ograniczeniu.
Kiedy nie używać generyków
Generyki pasują do typów kontenerowych (stosy, zbiory, cache, drzewa) i do algorytmów na slice'ach i mapach, które robią to samo dla każdego typu elementu. Nie pasują wszędzie.
- Jeden konkretny typ. Jeśli funkcja jest zawsze wywoływana tylko z
[]User, wersja generyczna dodaje koszt czytania i nic poza tym. - Zachowanie zależne od typu. Do tego służą interfejsy. Funkcja przyjmująca
io.Writerjest prostsza niż taka, która przyjmuje[T io.Writer], i działa tak samo. - Zastępowanie parametrów interfejsowych.
func Print[T fmt.Stringer](v T)nie jest lepsze odfunc Print(v fmt.Stringer).
Wskazówka samego zespołu Go: najpierw napisz kod dla konkretnego typu, a po parametry typów sięgnij, gdy zauważysz, że drugi raz piszesz ten sam kod, w którym zmieniają się tylko typy.
Typowe błędy
- Używanie
anytam, gdzie potrzebne jestcomparablelubcmp.Ordered. Kompilator odrzuca==lub<na parametrze typuany. - Zapomniana
~. Typy zdefiniowane przez użytkownika, jaktype ID int, nie spełniają zwykłego ograniczeniaint. - Zwracanie
nildla parametru typu. Niedozwolone, chyba że ograniczenie zawęża T do typów podobnych do wskaźników. Zwracaj wartość zerową. - Pisanie generycznej funkcji pomocniczej, która już istnieje. Najpierw sprawdź
slices,mapsicmp.
Najczęściej zadawane pytania
Czy Go ma generyki?
Tak, od Go 1.18 (marzec 2022). Funkcje i typy mogą deklarować parametry typów w nawiasach kwadratowych: func Map[T, U any](s []T, f func(T) U) []U. Każdy parametr typu ma ograniczenie, czyli interfejs opisujący, co typ musi obsługiwać.
Czym różni się any od comparable w generykach Go?
any dopuszcza każdy typ, ale z wartościami pozwala zrobić niewiele poza przypisywaniem, przekazywaniem i przechowywaniem. comparable dopuszcza tylko typy obsługujące == i !=, czyli to, czego potrzebujesz do kluczy mapy lub sprawdzania równości. Do < i > użyj cmp.Ordered.
Co oznacza tylda (~) w ograniczeniu w Go?
~T oznacza "dowolny typ, którego typem bazowym jest T". ~int pasuje do int, a także do type Celsius int. Bez tyldy int w ograniczeniu pasuje tylko do samego int, więc typy zdefiniowane przez użytkownika byłyby odrzucane.
Czy metody w Go mogą mieć parametry typów?
Nie. Metoda może używać parametrów typów swojego typu odbiorcy (func (s *Stack[T]) Push(v T)), ale nie może deklarować nowych. Zamiast tego napisz generyczną funkcję na najwyższym poziomie: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].