Odbiornik wartości a odbiornik wskaźnika

108

Jest dla mnie bardzo niejasne, w którym przypadku chciałbym użyć odbiornika wartości zamiast zawsze używać odbiornika wskaźnika.
Podsumowując z dokumentów:

type T struct {
    a int
}
func (tv  T) Mv(a int) int         { return 0 }  // value receiver
func (tp *T) Mp(f float32) float32 { return 1 }  // pointer receiver

Plik docs mówi także „Dla typów, takich jak podstawowe typy, plasterki i małych strukturach, odbiornik wartość jest bardzo tanie, więc chyba semantyka metody wymaga wskaźnik, odbiornik wartość jest efektywny i przejrzysty”.

Po pierwsze , mówi, że jest „bardzo tani”, ale pytanie brzmi raczej, czy jest tańszy niż odbiornik wskaźnika. Więc zrobiłem mały test porównawczy (kod w istocie) który pokazał mi, że odbiornik wskaźnika jest szybszy nawet dla struktury, która ma tylko jedno pole tekstowe. Oto wyniki:

// Struct one empty string property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  500000000                3.62 ns/op


// Struct one zero int property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  2000000000               0.36 ns/op

(Edycja: należy pamiętać, że drugi punkt stał się nieważny w nowszych wersjach go, patrz komentarze) .
Drugi punkt mówi, że jest „wydajny i przejrzysty”, co jest bardziej kwestią gustu, prawda? Osobiście wolę spójność, używając wszędzie w ten sam sposób. W jakim sensie efektywność? pod względem wydajności wydaje się, że wskaźniki są prawie zawsze bardziej wydajne. Kilka testów z jedną właściwością int wykazało minimalną przewagę odbiornika wartości (zakres 0,01-0,1 ns / op)

Czy ktoś może mi powiedzieć o przypadku, w którym odbiornik wartości ma wyraźnie większy sens niż odbiornik wskaźnika? A może robię coś źle w teście porównawczym, czy przeoczyłem inne czynniki?

Chrisport
źródło
3
Przeprowadziłem podobne testy porównawcze z jednym polem tekstowym, a także z dwoma polami: ciągami i polami int. Otrzymałem szybsze wyniki z odbiornika wartości. BenchmarkChangePointerReceiver-4 10000000000 0,99 ns / op BenchmarkChangeItValueReceiver-4 10000000000 0,33 ns / op Używam Go 1.8. Zastanawiam się, czy od czasu ostatniego uruchomienia testów porównawczych dokonano optymalizacji kompilatora. Więcej informacji można znaleźć w streszczeniu .
pbitty
2
Masz rację. Korzystając z mojego oryginalnego testu porównawczego przy użyciu Go1.9, również uzyskuję inne wyniki. Odbiornik wskaźnika 0,60 ns / op, odbiornik wartości 0,38 ns / op
Chrisport

Odpowiedzi:

118

Zwróć uwagę, że FAQ wspomina o spójności

Następna to konsekwencja. Jeśli niektóre metody tego typu muszą mieć odbiorniki wskaźników, pozostałe również powinny, więc zestaw metod jest spójny niezależnie od tego, w jaki sposób jest używany typ. Szczegółowe informacje można znaleźć w sekcji dotyczącej zestawów metod .

Jak wspomniano w tym wątku :

Reguła dotycząca wskaźników i wartości dla odbiorników polega na tym, że metody wartości mogą być wywoływane na wskaźnikach i wartościach, ale metody wskaźnikowe mogą być wywoływane tylko na wskaźnikach

Teraz:

Czy ktoś może mi powiedzieć o przypadku, w którym odbiornik wartości ma wyraźnie większy sens niż odbiornik wskaźnika?

Komentarz Code Review może pomóc:

  • Jeśli odbiornikiem jest mapa, funkcja lub kanał, nie używaj do niego wskaźnika.
  • Jeśli odbiornikiem jest wycinek, a metoda nie zmienia podziału ani nie zmienia przydzielenia wycinka, nie używaj do niego wskaźnika.
  • Jeśli metoda wymaga mutacji odbiornika, odbiornik musi być wskaźnikiem.
  • Jeśli odbiornikiem jest struktura zawierająca rozszerzenie sync.Mutex lub podobne pole synchronizujące, odbiornikiem musi być wskaźnik, aby uniknąć kopiowania.
  • Jeśli odbiornik jest dużą strukturą lub tablicą, odbiornik wskaźnika jest bardziej wydajny. Jak duża jest duża? Załóżmy, że jest to równoważne przekazaniu wszystkich jego elementów jako argumentów do metody. Jeśli wydaje się, że jest za duże, jest również za duże dla odbiornika.
  • Czy funkcja lub metody, współbieżne lub wywoływane z tej metody, mogą mutować odbiornik? Typ wartości tworzy kopię odbiornika, gdy metoda jest wywoływana, więc aktualizacje zewnętrzne nie zostaną zastosowane do tego odbiornika. Jeśli zmiany muszą być widoczne w oryginalnym odbiorniku, odbiornik musi być wskaźnikiem.
  • Jeśli odbiornik jest strukturą, tablicą lub wycinkiem i którykolwiek z jego elementów jest wskaźnikiem do czegoś, co może podlegać mutacji, preferuj odbiornik wskaźnika, ponieważ sprawi, że intencja będzie bardziej jasna dla czytelnika.
  • Jeśli odbiornik jest małą tablicą lub strukturą, która jest naturalnie typem wartości (na przykład czymś takim jak time.Timetyp), bez zmiennych zmiennych i bez wskaźników lub jest po prostu prostym typem podstawowym, takim jak int lub string, odbiornik wartości sens .
    Odbiornik wartości może zmniejszyć ilość śmieci, które mogą zostać wygenerowane; jeśli wartość jest przekazywana do metody wartości, zamiast alokowania na stercie można użyć kopii na stosie.(Kompilator stara się być sprytny, jeśli chodzi o unikanie tej alokacji, ale nie zawsze się to udaje). Nie wybieraj z tego powodu typu odbiorcy wartości bez uprzedniego profilowania.
  • Wreszcie, jeśli masz wątpliwości, użyj odbiornika wskaźnika.

Pogrubiona część znajduje się na przykład w net/http/server.go#Write():

// Write writes the headers described in h to w.
//
// This method has a value receiver, despite the somewhat large size
// of h, because it prevents an allocation. The escape analysis isn't
// smart enough to realize this function doesn't mutate h.
func (h extraHeader) Write(w *bufio.Writer) {
...
}
VonC
źródło
16
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers Właściwie to nieprawda. Zarówno metody odbiornika wartości, jak i metody odbiornika wskaźnika mogą być wywoływane na wskaźniku o poprawnym typie lub wskaźniku niebędącym wskaźnikiem. Niezależnie od tego, na jakiej metodzie jest wywoływana, w treści metody identyfikator odbiorcy odnosi się do wartości by-copy, gdy używany jest odbiornik wartości, oraz wskaźnika, gdy używany jest odbiornik wskaźnika: Zobacz play.golang.org/p / 3WHGaAbURM
Hart Simha
3
Istnieje wielka wyjaśnienie tutaj „Jeśli x jest adresowalne i & X. Sposób zestaw zawiera m (XM) jest skrótem (& x) .m ().”
tera
@tera Tak: omówiono to na stackoverflow.com/q/43953187/6309
VonC,
4
Świetna odpowiedź, ale zdecydowanie nie zgadzam się z tym punktem: „ponieważ dzięki temu intencja będzie jaśniejsza”, NIE, czyste API, X jako argument i Y jako wartość zwracana to wyraźny zamiar. Przekazywanie Struct przez wskaźnik i spędzanie czasu na uważnym czytaniu kodu w celu sprawdzenia, jakie wszystkie atrybuty są modyfikowane, jest dalekie od jasności i utrzymania.
Lukas Lukac
@HartSimha Myślę, że powyższy post wskazuje na fakt, że metody odbiornika wskaźnika nie znajdują się w „zestawie metod” dla typów wartości. W połączonego zabaw, dodając następujący wiersz spowoduje błąd kompilacji: Int(5).increment_by_one_ptr(). Podobnie cecha definiująca metodę increment_by_one_ptrnie będzie zadowalająca z wartością typu Int.
Gaurav Agarwal
16

Aby dodać do @VonC świetną, pouczającą odpowiedź.

Dziwię się, że nikt tak naprawdę nie wspomniał o kosztach utrzymania, gdy projekt się rozrasta, starzy deweloperzy odchodzą i przychodzi nowy. Go z pewnością jest młodym językiem.

Ogólnie rzecz biorąc, staram się unikać wskazówek, kiedy mogę, ale mają one swoje miejsce i piękno.

Używam wskaźników, gdy:

  • praca z dużymi zbiorami danych
  • mają strukturę utrzymującą strukturę, np. TokenCache,
    • Upewniam się, że WSZYSTKIE pola są PRYWATNE, interakcja jest możliwa tylko poprzez zdefiniowane odbiorniki metody
    • Nie przekazuję tej funkcji żadnemu gorutynowi

Na przykład:

type TokenCache struct {
    cache map[string]map[string]bool
}

func (c *TokenCache) Add(contract string, token string, authorized bool) {
    tokens := c.cache[contract]
    if tokens == nil {
        tokens = make(map[string]bool)
    }

    tokens[token] = authorized
    c.cache[contract] = tokens
}

Powody, dla których unikam wskazówek:

  • wskaźniki nie są jednocześnie bezpieczne (cały punkt GoLang)
  • niegdyś odbiornik wskaźnika, zawsze odbiornik wskaźnika (dla wszystkich metod Structa dla spójności)
  • muteksy są z pewnością droższe, wolniejsze i trudniejsze w utrzymaniu w porównaniu z „kosztem kopiowania wartości”
  • mówiąc o „kosztach kopiowania wartości”, czy to naprawdę problem? Przedwczesna optymalizacja jest źródłem wszelkiego zła, zawsze możesz później dodać wskaźniki
  • to bezpośrednio, świadomie zmusza mnie do projektowania małych Structs
  • wskaźników można w większości uniknąć, projektując czyste funkcje z jasnym zamiarem i oczywistymi wejściami / wyjściami
  • Wierzę, że zbieranie śmieci jest trudniejsze dzięki wskazówkom
  • łatwiej spierać się o hermetyzację, obowiązki
  • niech to będzie proste, głupie (tak, wskazówki mogą być trudne, ponieważ nigdy nie wiesz, kto jest deweloperem następnego projektu)
  • Testowanie jednostkowe jest jak chodzenie po różowym ogrodzie (tylko słowackie wyrażenie?), czyli łatwe
  • brak NIL, jeśli warunki (NIL można przekazać tam, gdzie oczekiwano wskaźnika)

Moja praktyczna zasada: napisz jak najwięcej hermetyzowanych metod, takich jak:

package rsa

// EncryptPKCS1v15 encrypts the given message with RSA and the padding scheme from PKCS#1 v1.5.
func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) ([]byte, error) {
    return []byte("secret text"), nil
}

cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock) 

AKTUALIZACJA:

To pytanie zainspirowało mnie do dokładniejszego zbadania tematu i napisania na ten temat wpisu na blogu https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701

Lukas Lukac
źródło
99% tego, co tu mówisz, podoba mi się i zdecydowanie się z tym zgadzam. To powiedziawszy, zastanawiam się, czy twój przykład jest najlepszym sposobem zilustrowania twojego punktu widzenia. Czy TokenCache nie jest w zasadzie mapą (z @VonC - „jeśli odbiornikiem jest mapa, func lub chan, nie używaj do tego wskaźnika”). Ponieważ mapy są typami referencyjnymi, co zyskujesz, ustawiając „Add ()” jako odbiornik wskaźnika? Wszelkie kopie TokenCache będą odnosić się do tej samej mapy. Zobacz ten plac zabaw Go - play.golang.com/p/Xda1rsGwvhq
Rich
Cieszę się, że jesteśmy wyrównani. Świetna uwaga. Właściwie myślę, że użyłem wskaźnika w tym przykładzie, ponieważ skopiowałem go z projektu, w którym TokenCache obsługuje więcej rzeczy niż tylko ta mapa. A jeśli używam wskaźnika w jednej metodzie, używam go we wszystkich. Czy sugerujesz usunięcie wskaźnika z tego konkretnego przykładu SO?
Lukas Lukac
LOL, kopiuj / wklej ponownie uderza! 😉 IMO możesz zostawić to tak, jak jest, ponieważ przedstawia pułapkę, w którą łatwo wpaść, lub możesz zastąpić mapę czymś, co demonstruje stan i / lub dużą strukturę danych.
Rich
Cóż, jestem pewien, że przeczytają komentarze ... PS: Bogato, twoje argumenty wydają się rozsądne, dodaj mnie na LinkedIn (link w moim profilu) chętnie się połączę.
Lukas Lukac