LoginSignup
2
0

More than 1 year has passed since last update.

Go言語で値からポインタレシーバーのメソッドを呼べるのとポインタから値レシーバーのメソッドを呼べるのは理由が違うという話

Posted at

仕様を眺めていたら「へー」となったので。

メソッドとかレシーバーとか

Goではレシーバーを持つ関数によってメソッドを定義する。

例えば、C++で

struct S {
    void f(){}
};

と書くところが、Go言語だと

type S struct{}

func (S) f() {}

こうなる。

レシーバーは値でもポインタでも良い。関数のその他の引数と同じように、値ならコピーのコストが掛かるし、ポインタなら中身を書き換えられる(書き換えることができてしまう)。

package main

import "fmt"

type S struct {
	x int
}

func (s S) incValue()    { s.x++ }
func (s *S) incPointer() { s.x++ }

func main() {
	s := S{x: 0}

	s.incValue()
	fmt.Printf("%d\n", s.x) // 0

	(&s).incPointer()
	fmt.Printf("%d\n", s.x) // 1
}

C++のメソッドは、Goのポインタレシーバーにあたるだろうか。

incPointer はポインタを受け取るので (&s).incPointer() と書いたが、これは s.incPonter() でも良い。逆に、変数がポインタでレシーバーが値のときも (*s).f() と書く必要は無い。

package main

type S struct{}

func (S) value()    {}
func (*S) pointer() {}

func main() {
	value := S{}
	pointer := &S{}

	// 全てコンパイルが通る。
	value.value()
	value.pointer()
	pointer.value()
	pointer.pointer()

	// 値とポインタを合わせて、こう書いても通る。
	(&value).pointer()
	(*pointer).value()
}

ちなみに、C++では (*pointer).value()pointer.value() と書くことはできず、このためにアロー演算子がある。 pointer->value()

コードを書く側としては、値かポインタかということを気にせずに常にドット . を使っておけば良いので便利。

てっきり、まとめて「値でもポインタレシーバーのメソッドを呼べるよ。逆もOK」的なことが仕様に書かれていると思ったけど、そうではなかった……というのがこの記事の話である。

値からポインタレシーバーのメソッドが呼べる理由

上記のコードの value.pointer() が通る理由。

Callsに書かれている。

A method call x.m() is valid if the method set of (the type of) x contains m and the argument list can be assigned to the parameter list of m. If x is addressable and &x's method set contains m, x.m() is shorthand for (&x).m():

&value と書けて、 &value がメソッド pointer() を持っているならば、 value.pointer()(&value).pointer() の省略記法となる。

ポインタから値レシーバーのメソッドが呼べる理由

上記のコードの pointer.value() が通る理由。

Method setsに書かれている。

The method set of a pointer to a defined type T (where T is neither a pointer nor an interface) is the set of all methods declared with receiver *T or T.

ポインタは、ポインタレシーバーの関数と値レシーバーの関数の両方をメソッドとして持つから。そもそもメソッドとして持っているので、 「pointer.value()(*pointer).value() の省略記法」というような仕様は不要である。

なんで?

pointer.value()(*pointer).value() の省略記法」か、「値は値レシーバーの関数とポインタレシーバーの関数をメソッドとして持つ」という仕様を追加するほうが、仕様が分かりやすくなって良いのではないか?

インターフェースが理由らしい。

package main

type Interface interface {
	f()
}

type Value struct{}

func (Value) f() {}

type Pointer struct{}

func (*Pointer) f() {}

func main() {
	var i Interface
	_ = i

	// (1)
	i = Value{}

	// (2)
	i = &Value{}

	// (3)
	// ./go.go:26:4: cannot use Pointer literal (type Pointer) as type Interface in assignment:
	//         Pointer does not implement Interface (f method has pointer receiver)
	i = Pointer{}

	// (4)
	i = &Pointer{}
}

(4) は、メソッド f を持っている変数をメソッド f を要求しているインターフェースに代入しているので、当然通る。

(1) も同様に当然通る。いや、「当然」と書いたけど、ポインタ以外をインターフェース型の変数に代入できるのを知らなかったな。インターフェースは、C++で基底クラスのポインタに派生クラスのポインタを代入できるのの柔軟な版だと思っていた。

(2) は、 &Value は値レシーバーの関数もメソッドに持つというルールによって、 f をメソッドに持つので、代入できる。 v := &Value{}; v.f() が有効なのだから代入できるほうが自然だし、代入できて便利なこともあるだろう。

(3) だけが通らない。関数呼び出し時のルールによって、 p := Pointer{}; p.f() は有効なものの、これは関数呼び出し時のルールなので、インターフェース型の変数への代入時には無関係。

(2) を通しつつ (3) を弾きたくて、こういう仕様になっているっぽい。インターフェースは型と値を持っている。インターフェースの持っている値を書き換えられたくないらしい。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0