soranoba
soranoba Author of soranoba.net
programming

Golangのinterfaceとレシーバの自動解決

Golangのinterfaceは多態性 (ポリモーフィズム) を実現する為の機能であるが、Javaのinterface、C++のInterface Classとは名称が同じであるものの全く別物であり、その実態はダック・タイピングです。
静的型付け言語でダック・タイピングを実現することで、多くの言語のそれとは異なる特徴を持ちます。

その為、理解するまで「Golangなにもワカラン!」となる嵌りポイントなので、interfaceとレシーバの自動解決について、私なりに纏めてみました。

違いはデータ構造から

まずは、以下の記事の文を引用します。

Languages with methods typically fall into one of two camps: prepare tables for all the method calls statically (as in C++ and Java), or do a method lookup at each call (as in Smalltalk and its many imitators, JavaScript and Python included) and add fancy caching to make that call efficient. Go sits halfway between the two: it has method tables but computes them at run time. I don’t know whether Go is the first language to use this technique, but it’s certainly not a common one. (I’d be interested to hear about earlier examples; leave a comment below.)

Go Data Structures: Interfacesより

様々な言語が、静的にメソッドテーブルを持つか、動的にメソッドを探すかの2つに大きく分かれる中、Golangはこれら2つの中間の方法で実現していることが書かれています。
Interface自体にメソッドテーブルを持たず、データと実体の型情報が保持さることで間接的にメソッドテーブルを参照することができるということです。

これについてはA Tour of Go: Interface valuesでも紹介されており、実際にコードで確認することができます。

interfaceの実体は変数ポインタにすることもできる

データ構造の違いの他に、大きな違いがもう一つあります。
それは、Interfaceの実体として変数ポインタを取ることができるという点です。
C++やJavaだとClass自体にメソッドが存在する必要があるが、GolangのInterfaceはダッグ・タイピングとしてメソッドが呼び出せれば良い為、メソッドの呼び出し可否さえ満たされれば、実体をポインタ型にすることができます。

先の、A Tour of Go: Interface valuesでもそれを確認することができます。
また、メソッドが呼び出せることを指定している関係か、interface定義ではレシーバ指定はありません。

type I interface {
  Get()
}

上記のように定義すると、「Iはその値がGet()の呼び出しができればよい」ことになります。

Golangのレシーバ自動解決

さらにややこしくしている要因として、Golangのレシーバ自動解決の仕組みがあります。

v.Scale(5) のステートメントでは、 v は変数であり、ポインタではありません。 メソッドでポインタレシーバが自動的に呼びだされます。 Scale メソッドはポインタレシーバを持つ場合、Goは利便性のため、 v.Scale(5) のステートメントを (&v).Scale(5) として解釈します。

A Tour of Go : Methods and pointer indirectionより

メソッドが変数レシーバである場合、呼び出し時に、変数、または、ポインタのいずれかのレシーバとして取ることができます: この場合、 p.Abs() は (*p).Abs() として解釈されます。

A Tour of Go : Methods and pointer indirection (2)

上記の説明の通り、Golangでは変数からValue Receiver、変数ポインタからPointer Receiverを呼び出す他に、変数からPointer Receiver、変数ポインタからValue Receiverを自動解決によって呼び出すことができます。

まとめると、以下になります。

  Value Receiver Pointer Receiver
変数 ◯(自動解決)
変数ポインタ ◯(自動解決)


この機能によって、メソッドがValue ReceiverなのかPointer Receiverなのかを(あまり)意識せずに実装をすることができるようになっている訳です。

Interfaceでレシーバ解決ができない場合

しかし、Interfaceの場合は変数によるPointer Receiverの解決ができません。
この理由となる確かな記述を見つけることはできませんでしたが、immustableとmutableの事情が絡んでいるのではないかと推察されます。

var a S = S{V: 10}
var b S = a // copy
b.V = 20
fmt.Println("a.V =", a.Get()) // 10
fmt.Println("b.V =", b.Get()) // 20

fmt.Println("----")

var c S = S{V: 10}
var d I = c // copy
// d.(S).V = 20 // cannot assign to d.(S).V
c = d.(S)
c.V = 20
var e I = d.(S) // copy
fmt.Println("c.V =", c.Get()) // 20
fmt.Println("d.V =", d.Get()) // 10
fmt.Println("e.V =", e.Get()) // 10

fmt.Println("----")

var f J = &S{}
var g J = f

f.Update(10)
g.Update(20)
fmt.Println("f.V =", f.Get()) // 20
fmt.Println("g.V =", g.Get()) // 20

Go Playground

Interfaceの実体が値型の場合は、値渡しによって実体がコピーされます。(a、b)
Interfaceの実体がポインタ型の場合は、ポインタ渡しになるので実体は同じ値を指します。(f、g)

また、(c、d、e)のケースを見て分かるように、実体の型に戻して値を操作しようとする場合、直接代入を行おうとするとcannot assign to ...になる為、一度代入をする必要があります。
型としてはIである為、Sに戻さないといけないのは比較的自然と言えます。

このようにInterfaceの実体が値型である場合、実体のメンバ変数を変更することができません。
であるなら、Interfaceの実体が値型の場合に、Pointer Receiverの自動解決ができないというのは自然と言えるのではないでしょうか。


特に最後は根拠となる公式の文が見つからず、真偽が怪しいところではありますが、このような理解を私はしました。
公式の説明をご存知の方は教えてください!!

(Updated: )

comments powered by Disqus