数ヶ月前、Qiitaにこんな投稿があったようだ。
[教えて]Go言語:なぜインターフェイスはポインタにできない? - Qiita
内容は元記事を読んでもらうとして、ちょっとスッキリしない点があった。具体的には:
ありがとうございます!
・インターフェイスはポインタにすることができるが、
・インターフェイスのポインタにはインターフェイスとしての機能がない
という理解になりました。
Cプログラマ的には、「ポインタと実体は別物」という意識があるのと同時に、「デリファレンス」の存在も心得ている。なので、「インターフェイスのポインタにはインターフェイスとしての機能がない、という理解になりました」と言われても困るというか、「おまえは何を言っているんだ」と返したくなる。
ポインタと実体は別物だ。つまり、インターフェイスのポインタとインターフェイスそのもの(実体)は別物だ。別物だから、ポインタに実体が持つはずの機能がないのは当たり前だ。
これは別にインターフェイスに限った話ではない。int型のポインタとint型の値(実体)は別物だ。string型のポインタとstring型の値(実体)は別物だ。ポインタは、実体が持つ機能を持たない。
ただし、ポインタには「デリファレンス」という抜け道がある。C言語の頃から、ポインタ使いはポインタに対してデリファレンスを行い、ポインタが指し示している実体を操作してきたのである。
ポインタは実体ではないので、実体が持つ機能を持たない。しかしポインタをデリファレンスすることで実体にアクセスすれば、実体そのものを操作できるので、結果として実体が持つ機能を使用することができる。
となれば、焦点は1つ。
Cプログラマ的には「デリファレンスすれば、それは実体(つまりインターフェイス)なのだから、当然インターフェイスとして振る舞うのではないか?」とごく普通に考える訳だが、実際のところはどうなのか?
実験すべく、先の記事中のサンプルコードに手を加えてみた。
package main import "log" type FooInterface interface { DoSomething() } type Foo struct { } func (this *Foo) DoSomething() { log.Println("Foo DoSomething was called") } func Call(foo *FooInterface) { (*foo).DoSomething() } func main() { var fi FooInterface = &Foo{} Call(&fi) }
元コードとの差分はこんな感じ。
--- main.go 2014-05-09 22:54:39.764621900 +0900 +++ main_new.go 2014-05-09 23:15:12.066445600 +0900 @@ -14,9 +14,10 @@ } func Call(foo *FooInterface) { - foo.DoSomething() + (*foo).DoSomething() } func main() { - Call(&Foo{}) + var fi FooInterface = &Foo{} + Call(&fi) }
元のサンプルコードでは、関数Call内でDoSomething()を呼び出す部分にて、「インターフェイスを指し示すポインタ」をインターフェイス(実体)として扱おうとしてエラーとなっていた。また関数mainにて*FooInterface型の引数に異なる型である*Foo型のリテラルを突っ込もうとしてエラーとなっていた。
このうち、関数Call内でDoSomething()を呼び出す部分については、修正方法は明らかだ。*FooInterface型をFooInterface型として――つまりポインタを実体として扱いたい訳だから、デリファレンスすればよい。
一方で関数main内で関数Callを呼び出す部分は一手間必要だ。まず*FooInterface型と*Foo型は異なる型だ。Go言語は強い型付けの言語なので、異なる型同士での代入はできない。
C言語やC++のように、安直に型変換で誤魔化すこともできない。例えば:
func main() {
Call((*FooInterface)(&Foo{}))
}
上記の書き方は、型変換に失敗してエラーとなる。
./main_new.go:21: cannot convert Foo literal (type *Foo) to type *FooInterface
これらの問題を回避するため、*Foo型のリテラルをFooInterface型の変数に代入した上で、その変数のアドレスを関数Callの引数に設定している(インターフェイスはGo言語の強い型付けに柔軟性をもたらす機能なので、異なる型(*Foo型とFooInterface型)なのに代入が可能となっている)。
というわけで、インターフェイスのポインタをデリファレンスしてインターフェイスそのものを取り出せば、しっかりとインターフェイスの役目を果たすのだった。
……うん、実に当たり前すぎてつまらないな。
Cプログラマ的に興味を覚えるのは、先ほど例に挙げた:
func main() {
Call((*FooInterface)(&Foo{}))
}
もしくはCプログラマ的には「こっちが正しいのでは?」と思うこれとか:
func main() {
Call((*FooInterface)(&(&Foo{})))
}
こんな風に型変換で誤魔化すことができない点だ(正確には、後者はダブルポインタNGという言語仕様の影響でビルドできないのだが)。
C言語やC++における「ポインタ型のキャスト」には、例えば「あるアドレスから開始するメモリ領域に型Aとして解釈されるbitの並びが存在するところを、型Bのbitの並びとみなして解釈する」的な雰囲気がある。
実際にはC++のダウンキャストやアップキャストでは多重継承の都合もあって事情が異なるし、何よりもこの辺の話題は言語規格ではなく処理系の実装に依存する話なのだが、それでも暗黙のうちに上記のような想定をしているCプログラマは多いように思う。
そのようなモノの見方からすると、Go言語のインターフェイスの実装には単なる型の誤魔化しでは対応できない何かがあるのではないか、という風にも感じる。あるいは、静的型付けのオブジェクト指向プログラミング言語におけるクラス階層のような「型と型の関係を示す何か」が介在しないシステムなので、関係が分からないのに安全な型変換なんて無理筋なのかもしれない。
まあしかし、これについては単純に「強い型付けの言語だから」で片付いてしまう気もするのだが。