Значення приймача порівняно з приймачем покажчика


108

Для мене дуже незрозуміло, у якому випадку я б хотів використовувати приймач значення, а не завжди використовувати приймач вказівника.
Щоб скласти резюме з документів:

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

У документах говорять також «Для таких типів, як основні типів, скибочки і малі структури, значення приймач дуже дешево , тому , якщо семантиці методи не потрібно покажчик, значення приймач є ефективним і ясно.»

Перший момент говорить про те, що це "дуже дешево", але питання більше, чи дешевше він, ніж вказівник приймача. Тому я зробив невеликий орієнтир (код на суті), який показав мені, що приймач покажчика швидше навіть для структури, що має лише одне рядкове поле. Ось такі результати:

// 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

(Редагувати. Зверніть увагу, що другий пункт став недійсним у нових версіях перегляду, дивіться коментарі) .
Другий пункт говорить: "Ефективне і чітке" - це питання смаку, чи не так? Особисто я віддаю перевагу консистенції, використовуючи всюди однаковий спосіб. Ефективність у якому сенсі? ефективність, яка здається, покажчик майже завжди є більш ефективною. Кілька тестових прогонів з одним властивістю int показали мінімальну перевагу приймача значення (діапазон 0,01-0,1 нс / оп)

Чи може хто-небудь розповісти мені випадок, коли приймач значення має сенс більше, ніж приймач вказівника? Або я роблю щось не так у еталоні, чи не помічав інших факторів?


3
Я провів подібні орієнтири з одним рядковим полем, а також з двома полями: поля рядка та int. Я отримав швидші результати від приймача значення. BenchmarkChangePointerReceiver-4 10000000000 0,99 нс / оп BenchmarkChangeItValueReceiver-4 10000000000 0,33 нс / оп Для використання Go 1,8. Цікаво, чи були оптимізації компілятора, зроблені з моменту останнього запуску тестів. Докладніше див. У суті .
pbitty

2
Ти маєш рацію. Запускаючи свій оригінальний орієнтир за допомогою Go1.9, я отримую і інші результати. Приймач покажчика 0,60 нс / оп, значення приймача 0,38 нс / оп
Кріспорт

Відповіді:


118

Зауважте, що у FAQ часто згадується послідовність

Далі - консистенція. Якщо деякі з методів типу мають мати приймачі покажчиків, решта також повинні, тому набір методів є послідовним незалежно від того, яким типом використовується. Деталі див. У розділі набору методів .

Як згадується в цій темі :

Правило щодо покажчиків проти значень для приймачів полягає в тому, що методи значень можна викликати вказівники та значення, але методи покажчиків можна викликати лише на покажчики

Зараз:

Чи може хто-небудь розповісти мені випадок, коли приймач значення має сенс більше, ніж приймач вказівника?

The Коментар Код Огляд може допомогти:

  • Якщо приймачем є карта, функція чи чан, не використовуйте вказівник на нього.
  • Якщо одержувачем є фрагмент, а метод не перерозподіляє і не перерозподіляє фрагмент, не використовуйте вказівник на нього.
  • Якщо методу потрібно мутувати приймач, приймач повинен бути вказівником.
  • Якщо приймач - це структура, яка містить a sync.Mutex або подібне поле синхронізації, приймач повинен бути вказівником, щоб уникнути його копіювання.
  • Якщо приймач - це велика структура або масив, приймач покажчика є більш ефективним. Наскільки великий великий? Припустимо, це еквівалентно передавати всі його елементи як аргументи методу. Якщо це відчувається занадто великим, воно також занадто велике для приймача.
  • Чи можуть функціонування або методи, одночасно або коли викликані цим методом, мутувати приймач? Тип значення створює копію приймача, коли метод викликається, тому зовнішні оновлення не застосовуватимуться до цього приймача. Якщо зміни повинні бути помітні в оригінальному приймачі, приймач повинен бути вказівником.
  • Якщо приймач - це структура, масив або фрагмент і будь-який з його елементів є вказівником на щось, що може мутувати, то віддайте перевагу приймачу вказівника, оскільки це зробить намір більш зрозумілим для читача.
  • Якщо одержувач - це невеликий масив або структура, яка є, природно, типом значення (наприклад, щось на зразок time.Timeтипу), без змінних полів і покажчиків, або це просто простий базовий тип, такий як int або string, приймач значення робить сенс .
    Приймач значення може зменшити кількість сміття, яке можна створити; якщо значення передається значущому методу, замість виділення на купі може використовуватися копія на стеці. (Компілятор намагається бути розумним щодо уникнення цього розподілу, але це не завжди може досягти успіху.) Не вибирайте тип приймача значення з цієї причини без попереднього профілювання.
  • Нарешті, коли виникають сумніви, використовуйте приймач вказівника.

Частина жирним шрифтом знаходиться, наприклад, у 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) {
...
}

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 Неправда, насправді. Обидва способи прийому значень і приймачів покажчиків можуть викликатися на правильно введеному покажчику або не вказівнику. Незалежно від того, що викликано методом, в тілі методу ідентифікатор приймача посилається на значення копіювання при використанні приймача значення та на вказівник, коли використовується приймач покажчика: Див. Play.golang.org/p / 3WHGaAbURM
Харт Сімха

3
Існує велика пояснення тут «Якщо х адресацією і & прісваіватель ікси містить т, хт () є скороченням (& х) .m ().»
тера

@tera Так: що обговорюється на stackoverflow.com/q/43953187/6309
VonC

4
Чудова відповідь, але я категорично не згоден з цим пунктом: "як це зробить наміри більш зрозумілими", NOPE, чистий API, X як аргумент і Y як повернене значення - це чіткий намір. Передача структури за вказівником та витрачання часу на уважне читання коду, щоб перевірити, що всі атрибути модифікуються, далеко не зрозуміло, бездоганно.
Лукаш Лукач

@HartSimha Я думаю, що публікація вище вказує на те, що методи приймачів покажчиків не знаходяться у "методі набору" для типів значень. У пов'язаному дитячому майданчику, додавши наступний рядок призведе до помилки компіляції: Int(5).increment_by_one_ptr(). Аналогічно, ознака, яка визначає метод increment_by_one_ptr, не буде задоволена значенням типу Int.
Гаурав Агарвал

16

Щоб додати до @VonC чудову, інформативну відповідь.

Я здивований, що ніхто не згадував вартість обслуговування, як тільки проект стає більшим, старі диски відходять і приходить новий. Іти неодмінно - це молода мова.

Взагалі кажучи, я намагаюся уникати покажчиків, коли можу, але вони мають своє місце і красу.

Я використовую покажчики, коли:

  • робота з великими наборами даних
  • мати стан підтримки структури, наприклад TokenCache,
    • Я впевнений, що ВСІ поля є ЧАСНИМИ, взаємодія можлива лише через визначені приймачі методу
    • Я не передаю цю функцію жодній програмі

Наприклад:

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
}

Причини, чому я уникаю покажчиків:

  • покажчики не є одночасно безпечними (вся суть GoLang)
  • один раз приймач вказівника, завжди приймач вказівника (для всіх методів консистенції Struct)
  • мутекси, безумовно, дорожчі, повільніші та складніші у підтримці порівняно зі "вартістю копіювання цінності"
  • Якщо говорити про "вартість копії цінності", це справді проблема? Передчасна оптимізація є коренем для всього зла, ви завжди можете додавати покажчики пізніше
  • це прямо, стисло примушує мене розробляти невеликі Структури
  • покажчиків можна в основному уникнути, створивши чисті функції з чітким наміром і очевидним введенням / виводом
  • Збір сміття важче за допомогою покажчиків, я вважаю
  • простіше сперечатися про інкапсуляцію, відповідальність
  • будьте простим, дурним (так, вказівники можуть бути хитрими, тому що ви ніколи не знаєте розробника наступного проекту)
  • Тестування одиниць - це як прогулянка по рожевому саду (словацький лише вираз?), означає легко
  • немає NIL, якщо умови (NIL може бути переданий там, де очікувався вказівник)

Моє правило, написати якомога більше інкапсульованих методів, таких як:

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) 

ОНОВЛЕННЯ:

Це питання надихнуло мене більше вивчити цю тему і написати про неї блог https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701


Мені подобається 99% того, що ви тут говорите, і сильно погоджуєтесь з цим. Це сказав, що мені цікаво, чи ваш приклад найкращий спосіб проілюструвати свою думку. Хіба TokenCache не є картою (від @VonC - "якщо одержувач - це карта, функція чи чан, не використовуйте вказівник на нього"). Оскільки карти є типовими типами, що ви отримуєте, зробивши "Add ()" приймачем покажчика? Будь-які копії TokenCache посилаються на ту саму карту. Дивіться цей майданчик Go - play.golang.com/p/Xda1rsGwvhq
Rich

Радий, що ми вирівняні. Чудова точка. Насправді, я думаю, я використав покажчик у цьому прикладі, тому що скопіював його з проекту, де TokenCache обробляє більше матеріалів, ніж просто цю карту. І якщо я використовую вказівник в одному методі, я використовую його у всіх. Чи пропонуєте ви видалити покажчик із цього конкретного прикладу SO?
Лукаш Лукач

LOL, скопіюйте / вставте повторно! 😉 ІМО ви можете залишити його таким, яким він є, оскільки він ілюструє пастку, в яку легко потрапити, або ви можете замінити карту чимось (ими), які демонструють стан та / або велику структуру даних.
Багатий

Ну, я впевнений, що вони прочитають коментарі ... PS: Багаті, ваші аргументи здаються розумними, додайте мене на LinkedIn (посилання в моєму профілі) із задоволенням.
Лукаш Лукач
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.