🐶

Type definitionによるメソッドの引き継ぎをシンプルに覚えられる(かも)

2020/10/01に公開

2021/02/14 Fri 訂正追記

https://play.golang.org/p/g48hm9MiIXb のような例を考えるとこの記事の「法則」は誤りでした。ややこしく思えても仕様書通りに考えるべきところだったようです。

仕様書にも、次のような例があげられていますね。普通に書かれていることを見落としてしまった。

// The method set of PtrMutex's underlying type *Mutex remains unchanged,
// but the method set of PtrMutex is empty.
type PtrMutex *Mutex

はじめに〜何がややこしいか

Go言語の仕様書に次のような記述があります。

A defined type may have methods associated with it. It does not inherit any methods bound to the given type, but the method set of an interface type or of elements of a composite type remains unchanged:

(意訳)defined typeはメソッドを持つことができます。(Type definitionで定義した) defined typeは右辺の型が持つメソッドを引き継ぐわけではありません。ただし、インターフェース型のメソッドセットや、合成型の要素が持つメソッドセットは変わりません(つまり、新しく定義した型に引き継がれます)。

つまり、

type NewType OldType

のようにtype definitionを書いた場合において、OldTypeの型が、

  • interface typeのときはそのメソッドセットはNewTypeに引き継がれる
  • struct typeのときはその構成要素のメソッドセットはNewTypeに引き継がれる
  • それ以外の時はメソッドセットが引き継がれない

これがややこしい、どうしてこうなっているのだろうというコメントがgospecreading勉強会で出ていました。確かに上記の場合わけは、あまり必然性がよくわからないです。(※composite typeはinterface typeとstruct typeの他にもあるのですが、ここでは考えなくて良いです)

type definitionにおいては、右辺の型のunderlying typeのmethod setが新しい型に引き継がれる

しかし視点を変えると、上記のような振る舞いをシンプルに捉えられるかもしれないと思いました。

その捉え方とは、「type definitionにおいては、右辺の型のunderlying typeのmethod setが新しい型に引き継がれる」というものです。これを以下で単に「法則」と読んで参照します。

以下、具体的に見ていきます。

interface type の場合

https://play.golang.org/p/Wi6RGzUMXfw

package main

type IA interface {
	Method()
}

type IB IA

// 実装を用意しておく
type T int

func (t T) Method() {}

func main() {
	var t IB = T(0)
	t.Method() // IAのMethod setはIBに引き継がれている
	// これはIAのunderlying typeであるinterface type literalのメソッドセットでもある
}

IBがtype definitionで作られた新しい型です。右辺の型とはこの場合IAです。IAのunderlying typeとは何かというと、IAを定義した型宣言の右辺にある型リテラルである

interface {
	Method()
}

です。そしてこの型リテラルのメソッドセットはMethod()からなります。(一応検証したい方はこちらを見てください)これで、interface型の場合に「法則」が成り立つことがわかりました。

struct typeの場合

https://play.golang.org/p/Tqs5iukBhbk

package main

func main() {
	// x's type is U's underlying type literal
	var x struct {
		S
		T
	}
	x.TMethod()
	x.SMethod() // Uのunderlying typeのメソッドセットを確認した

	var v V
	v.TMethod()
	v.SMethod() // VはUのunderlying typeのメソッドセットを引き継いでいる
}

type S int

func (s S) SMethod() {}

type T int

func (s T) TMethod() {}

type U struct {
	S
	T
}

type V U

この場合も、VはUのunderlying typeであるstruct型リテラルのメソッドセットを引き継いでいるので、「法則」が成り立ちます。

それ以外の場合

最後に一番普通の場合です。

https://play.golang.org/p/nJAneGnruTD

package main

type S string

func (s S) Method() {} 
// これはSのメソッドではあってもSのunderlying typeであるstringのメソッドではない

type T S // stringのメソッドセットはemptyであるので何も引き継がない

func main() {
	var t T
	t.Method() // 期待通りコンパイルエラーとなる
}

法則によるとTはSのメソッドセットではなく、Sのunderlying typeのメソッドセットを引き継ぎます。

ところがSのunderlying typeとはstringですから、そのメソッドセットは空です。ですからTは型定義によっては何のメソッドセットも引き継がないことになります。これは仕様書の記述と同じ結論ですから、やはりこの場合も法則が成り立っています。

まとめ

type definitionによるメソッドセットの引き継ぎについて、仕様書では右辺の型とそのメソッドセットをどう引き継ぐか、という規定ぶりになっていますが、この記事のようにunderlying typeのメソッドセットを引き継ぐ、と考えると場合わけを考える必要がなくなりそうです。

type definitionについての仕様書の最初の記述をみても、次のようにunderlying typeがポイントになっています。

A type definition creates a new, distinct type with the same underlying type and operations as the given type, and binds an identifier to it.

これも合わせると、「type definitionではunderlying typeが重要!」と考えておくとなんとなく楽になりそうな気がしています。

何か気づいた点や気になった点があればお気軽に教えていただけると嬉しいです。

Discussion