1. ホーム
  2. function

[解決済み] バリューレシーバー vs. ポインターレシーバー

2022-04-28 23:55:50

質問

どのような場合にポインタレシーバーではなく、バリューレシーバーを使用したいのかが非常に不明です。

docsからおさらいすると。

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

ドキュメント 基本型、スライス、小さな構造体などの型では、バリュー レシーバは非常に安価なので、メソッドのセマンティクスでポインタが必要な場合を除き、バリュー レシーバは効率的で明確です」とも書かれています。

第一点 ドキュメントにはバリューレシーバーは非常に安価であると書かれていますが、問題はポインターレシーバーよりも安価であるかどうかです。そこで、小さなベンチマークを作ってみました。 (コードはgistにあります) その結果、文字列フィールドを1つしか持たない構造体でも、ポインタレシーバの方が高速であることがわかりました。以下はその結果です。

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

(編集: 2番目のポイントは、新しいgoのバージョンでは無効になっていることに注意してください。)

2点目 docsによると、バリューレシーバーは "efficient and clear" とありますが、これは好みの問題ではないでしょうか? 個人的には、どこでも同じものを使うことで一貫性を持たせることを好みます。どのような意味で効率的なのでしょうか?パフォーマンス的には、ポインタの方が常に効率的だと思います。1つのintプロパティで数回テストしたところ、Value receiverの利点はほとんどありませんでした(0.01-0.1ns/opの範囲)。

どなたか、ポインタレシーバーよりもバリューレシーバーの方が明らかに理にかなっているケースを教えてください。あるいは、私がベンチマークで何か間違ったことをしているのでしょうか?他の要因を見落としたのでしょうか?

どのように解決するのですか?

注意点 は一貫性について言及しています。

次に一貫性です。もしその型のメソッドのいくつかがポインタのレシーバを持たなければならないなら、残りのメソッドもそうすべきです。そうすれば、その型がどのように使われるかに関係なく、メソッドセットに一貫性が生まれます。を参照してください。 メソッドセット をご覧ください。

前述したように このスレッドでは :

受信機のポインタと値に関するルールは、値のメソッドが可能です。 ポインタのメソッドは、ポインタと値に対して呼び出すことができますが、ポインタのメソッドは ポインタ

というのは事実ではありません。 コメント によって サルト・シムハ

Value Receiver と Pointer Receiver の両方のメソッドは、正しく型付けされたポインタまたは非ポインタに対して呼び出すことができます。

メソッドの呼び出し対象にかかわらず、メソッド本体内では、バリューレシーバーが使用された場合はレシーバーの識別子がバイコピーの値を参照し、ポインターレシーバーが使用された場合はポインターを参照します。 .

今すぐ

誰か、バリューレシーバーがポインターレシーバーよりも明らかに理にかなっているケースを教えてください。

は、その コードレビューのコメント が役に立ちます。

  • レシーバーが map、func、chan の場合、それへのポインターは使わないでください。
  • 受信機がスライスで、そのメソッドがスライスを再スライスまたは再割り当てしない場合、そのポインタを使用しないでください。
  • メソッドがレシーバーを変更する必要がある場合、レシーバーはポインターである必要があります。
  • 受信機が構造体で sync.Mutex または類似の同期化フィールドを使用する場合は、 コピーを避けるため、受信者はポインタでなければなりません。
  • 受信機が大きな構造体や配列の場合、ポインタ受信機の方が効率的です。大きいとはどの程度の大きさですか?メソッドの引数として全要素を渡すのと同等だと仮定してください。それが大きすぎると感じる場合は、レシーバも大きすぎるということです。
  • 関数やメソッドは、同時に、またはこのメソッドから呼び出されたときに、レシーバを変異させることができますか?値型は、メソッド呼び出し時に受信機のコピーを作成します。したがって、外部からの更新がこの受信機に適用されることはありません。元のレシーバで変更を確認する必要がある場合、レシーバはポインタである必要があります。
  • レシーバが構造体、配列、スライスで、その要素のいずれかが変異する可能性のあるものへのポインタである場合、ポインタレシーバの方が読み手にその意図がより明確に伝わるため、望ましい。
  • 受信機が小さな配列や構造体であり、当然ながら値型である場合 (のようなもの(例えば time.Time 型)で、mutableフィールドやポインタを持たないか、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) {
...
}


イルブル で指摘している。 コメント について警告しています。 インターフェースメソッド :

受信機の種類は統一した方が良いというアドバイスに従い、ポインタ受信機の場合は (p *type) String() string メソッドは また はポインタレシーバを使用する。

しかし、これは ない を実装しています。 Stringer インターフェイスの呼び出し元があなたの型へのポインタを使用しない限り、あなたのAPIの使い勝手の問題かもしれません。

一貫性がユーザビリティに勝るかどうかは分かりませんが。


に指摘する。

<ブロッククオート

を使えば、値のレシーバーを持つメソッドとポインターのレシーバーを持つメソッドを混在させ、値とポインターを含む変数で、どちらがどちらかを気にすることなく使用することができます。

どちらも動作しますし、構文も同じです。

しかし、インターフェイスを満たすためにポインターを受け取るメソッドが必要な場合、インターフェイスに割り当て可能なのはポインターだけで、値は有効ではありません。

<ブロッククオート

インターフェースを介して値を受け取るメソッドを呼び出すと、常に値の余分なコピーが作成される .

インターフェイスの値は基本的にポインターですが、値を受け取るメソッドは値を必要とします。つまり、呼び出すたびにGoは値の新しいコピーを作成し、それを使ってメソッドを呼び出し、そして値を捨てなければなりません。

バリューレシーバーメソッドを使用し、インターフェイス値を通して呼び出す限り、これを避ける方法はありません。

<ブロッククオート

のコンセプト アドレス指定不可の値 アドレス指定可能な値の反対である。丁寧な技術的なバージョンは、Go仕様の アドレス演算子 しかし、手を振って要約すると、ほとんどの匿名値はアドレス指定可能ではないということです(大きな例外は 複合リテラル )