Type definitionによるメソッドの引き継ぎをシンプルに覚えられる(かも)
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 の場合
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の場合
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型リテラルのメソッドセットを引き継いでいるので、「法則」が成り立ちます。
それ以外の場合
最後に一番普通の場合です。
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