Go Method Sets (Japanese)

"Method Sets" from golang wiki

Go公式WikiのMethod Setsの日本語意訳になります。
(元記事の最終更新日: 2017/04/17 rev.5)

メソッドセットとレシーバの型に関する記述がされています。


Method Sets

はじめに

特定の型や値のメソッドセットは、Go言語では値が実装しているメソッドセットによってその値がどのインターフェースになるかを決定するため、特に重要です。

仕様

メソッドセットに関するGo言語仕様には、重要な節が2つあります。

メソッドセット: 型にはメソッドセットが関連付けられている場合があります。
インターフェース型のメソッドセットはそのインターフェース定義です。
その他の型、例えばT型と名付けられた型のメソッドセットは、レシーバTを持つ全てのメソッドによって構成されます。ポインタ型*Tのメソッドセットはレシーバ*TまたはTを持つ全てのメソッドになります(つまり、T型のメソッドセットを含んでいます)。
他の型のメソッドセットは空になります。メソッドセットでは、全てのメソッドが一意の名前にならなくてはなりません。

呼び出し: x.m()というメソッド呼び出しは、x型のメソッドセットにmが含まれており、引数リストがmの仮引数リストに代入可能な場合に有効になります。もしxがアドレス指定可能で&xのメソッドセットがmを含んでいる場合は、x.m()(&x).m()の省略形になります。

使用方法

日々のプログラミングで現れるメソッドセットが異なるケースが多々あります。主なものとしては、変数、スライスの要素、マップの要素、そしてインターフェースに格納された値のメソッドを呼び出す時です。

変数

一般的に、ある型の変数がある場合には、その型のメソッドセットは何でも呼び出すことができます。上記の2つのルールを組み合わせると、以下のコードは正しいものになります。

type List []int

func (l List) Len() int        { return len(l) }
func (l *List) Append(val int) { *l = append(*l, val) }

func main() {
	// 構造体の値
	var lst List
	lst.Append(1)
	fmt.Printf("%v (len: %d)\n", lst, lst.Len())

	// ポインタの値
	plst := new(List)
	plst.Append(2)
	fmt.Printf("%v (len: %d)\n", plst, plst.Len())
}

注: ポインタと値、両方のメソッドはポインタ値と非ポインタ値の両方から呼び出すことができます。理由を理解するために、両方の型のメソッドセットをその仕様から直接調べてみましょう。

List
- Len() int

*List
- Len() int
- Append(int)

たとえ上記コードで問題なくメソッドの呼び出しができたとしても、ListのメソッドセットにAppend(int)を実際には含んでいないことに注意してください。これは上記に掲載した仕様の2番目による結果になります。下記の1行目を2行目のように暗黙的に変換しています。

lst.Append(1)
(&lst).Append(1)

ドットの前の値は*Listで、そのメソッドセットにはAppendを含んでいるため、呼び出しは正当なものです。
これらのルールを簡単に覚えるために、ポインタレシーバと値レシーバのメソッドをメソッドセットから切り離して考えるのが良いでしょう。既にポインタであるか(上記の例のように)値のアドレスが取得可能であれば、ポインタ値のメソッドを呼び出すことは正当です。
値、または(仕様として明示的に規定されているポインタのケースのように)デリファレンス可能であれば、値のメソッドを呼び出すことは正当です。

スライスの要素

スライスの要素は変数とほとんど同じになります。アドレス取得可能なので、ポインタレシーバや値レシーバのメソッドは、ポインタ要素・値要素のスライスのどちらからでも呼び出すことができます。

マップの要素

マップの要素はアドレス取得可能ではありません。
したがって、以下は不正な操作となります。

lists := map[string]List{}
lists["primes"].Append(7) // (&lists["primes"]).Append(7) のように書き換えることはできません.

しかしながら、以下のコードは正しく動作します。(そしてもっと一般的なケースでしょう。)

lists := map[string]*List{}
lists["primes"] = new(List)
lists["primes"].Append(7)
count := lists["primes"].Len() // (*lists["primes"]).Len() として書き換えることができます.

つまり、ポインタ要素のマップからポインタレシーバ・値レシーバのメソッドの両方を呼び出すことができます。
しかし値要素のマップからは値レシーバのメソッドだけが呼び出し可能です。
構造体を要素に持つマップがほとんどポインタ要素として作られるのは、このことが理由になっています。

インターフェース

インターフェース内に格納される具体的な値は、マップの要素と同じ理由で、アドレス取得可能ではありません。
したがって、インターフェース上のメソッドを呼び出すときは、レシーバの型が同じであるか、具象型から直接識別できる必要があります。

ポインタレシーバと値レシーバのメソッドは、ポインタと値の両方から期待通り呼び出すことができます。
値レシーバのメソッドは、まず最初にデリファレンスされるため、ポインタ値から呼び出すことができます。しかし、ポインタレシーバのメソッドは値からは呼び出すことができません。なぜならインターフェースに格納されている値にはアドレスが無いからです。
インターフェースに値を割り当てる際に、コンパイラは可能な全てのインタフェースメソッドがその値から実際に呼び出されることを保証します。したがって、不適切な割り当てをしようとするとコンパイルに失敗するでしょう。

以下のコードでは前記のコード例を拡張し、有効なインターフェースとそうでないものについて記述しています。

type List []int

func (l List) Len() int        { return len(l) }
func (l *List) Append(val int) { *l = append(*l, val) }

type Appender interface {
	Append(int)
}

func CountInto(a Appender, start, end int) {
	for i := start; i <= end; i++ {
		a.Append(i)
	}
}

type Lener interface {
	Len() int
}

func LongEnough(l Lener) bool {
	return l.Len()*10 > 42
}

func main() {
	// 構造体の値
	var lst List
	CountInto(lst, 1, 10) // NG: Append はポインタレシーバ
	if LongEnough(lst) {  // OK: 同一のレシーバの型
		fmt.Printf(" - lst は十分長い")
	}

	// ポインタの値
	plst := new(List)
	CountInto(plst, 1, 10) // OK: 同一のレシーバの型
	if LongEnough(plst) {  // OK: *List はレシーバとしてデリファレンス可能
		fmt.Printf(" - plst は十分長い")
	}
}
 
comments powered by Disqus