はじめに
Golangはオブジェクト指向言語なのかというと「Yes and no」(公式FAQ)ってことですが、一体どこがyesでどこがnoなのか、いまいちわからなくてモヤモヤしませんか?
@shibukawaさんのオブジェクト指向言語としてGolangをやろうとするとハマること
はとてもわかりやすくて大変お世話になりましたが、途中から自分としてはちょっと違うんじゃないかなと思ってきたのと、Golangのすごい人mattnさんの継承を実現する「闇のテクニック」の中身に言及が無くて、ますますモヤモヤしました。
(なんだよGolangで継承できてるじゃん、やめたほうが良いってどゆこと?)
その辺を自分なりにまとめてみました。
- そもそも継承は良くない
- Goに入ってはGoに従え
というあたりには目をつむってますのでよろしくお願いします。
Golangは何ができないから「継承が無い」のか
こっちについてはいきなり結論を書いてしまいます。継承ができる言語では、
(Wikipedia「継承 (プログラミング)」から引用)
スーパークラスで型付けされた変数に、そのスーパークラスを継承したサブクラスのインスタンスを格納することができる。
けど、Golangではこれができません。structのポインタに格納できるのはそのstructのインスタンスだけ。
だから親クラスの一部のメソッドを子クラスでオーバーライドしても、親クラスのメソッドからはそれが呼ばれることは無いんです。
確認してみます。ParentクラスでHop()、Step()とその二つを続けてよぶGo()を実装し、Parentを埋め込んだChildクラスでHop()だけオーバーライドしました。全部のメソッドで最初にレシーバ=self/this相当を表示してみます。
package main
import (
"fmt"
)
type Parent struct {
}
func (s *Parent) Hop(t string) {
fmt.Printf("%p %s Parent.Hop\n", s, t)
}
func (s *Parent) Step(t string) {
fmt.Printf("%p %s Parent.Step\n", s, t)
}
func (s *Parent) Go(t string) {
fmt.Printf("%p %s Parent.Go\n", s, t)
s.Hop(t)
s.Step(t)
}
type Child struct {
dummy int
Parent
}
func (s *Child) Hop(t string) {
fmt.Printf("%p %s Child.Hop\n", s, t)
}
func main() {
child := &Child{}
// 親クラスのポインタに子クラスのオブジェクトは格納できない
/*
var p *Parent;
p = child // エラー: cannot use child (type *Child) as type *Parent in assignment
*/
// Parent.Go()からはChild.Hop()は呼ばれない
child.Go("child.Go")
// レシーバのアドレスが変わる
child.Hop("child.Hop")
child.Step("child.Step")
}
/* 実行結果
0x416024 child.Go Parent.Go
0x416024 child.Go Parent.Hop
0x416024 child.Go Parent.Step
0x416020 child.Hop Child.Hop
0x416024 child.Step Parent.Step
*/
Parent.Go()からChild.Hop()は呼ばれないし、親クラスにしか実装がないStep()が呼ばれた時点でレシーバのアドレスまで変わっています。
これは明らかに埋め込んだParentのアドレスが渡されているわけで、こうなるともう本来Childであったことは失われてしまうとはっきりわかります。
2種類のポインタ
どうやって無理やり継承を実現するか、ですが、Golangには普通のstructのポインタの他に、structのインスタンスを受けられるポインタがもう1種類あります。
それがinterface型の変数です。
type Jumper interface {
Hop(t string)
Step(t string)
Go(t string)
}
とinterfaceを定義すると、その変数には、これらのメソッドをすべて実装しているParent、Childどちらのインスタンスも格納できて、メソッドを呼び出すことができます。
var p Jumper
p = &Parent{}
p.Hop("p(parent).Hop()")
p = &Child{}
p.Hop("p(child).Hop()")
/* 実行結果
0x1b5498 p(parent).Hop() Parent.Hop
0x416040 p(child).Hop() Child.Hop
*/
interfaceの変数にはメソッドをすべて実装していればどんなstructのインスタンスでも格納できるのですが、このことは継承に必要な、「親クラスのポインタに子クラスのインスタンスを格納できる」ことを含んでいると言えます。
ただしinterface型の変数でできるのはメソッドを呼び出すことだけでフィールドにはアクセスできないので、このままself/thisとして利用することができません。
interfaceをフィールドに持たせる
つまり、
- フィールドにアクセスするにはstructのポインタが必要
- 継承のためにはinterfaceの変数が必要
これを解決すればよいわけで、それには
(1) interface型の変数をフィールドに持たせて自分のポインタを格納する
(2) メソッド呼び出しはinterface型の変数を介して行う
という様にすればよいのではないでしょうか。
type Parent struct {
i Jumper
}
func NewParent() *Parent {
s := &Parent{}
s.i = s
return s
}
func (s *Parent) Go(t string) {
fmt.Printf("%p %s Parent.Go\n", s, t)
s.i.Hop(t)
s.i.Step(t)
}
func NewChild() *Child {
s := &Child{}
s.i = s
return s
}
func main() {
child := NewChild()
// Parent.Go()からChild.Hop()が呼ばれる
child.Go("child.Go")
}
/* 実行結果
0x40c0e4 child.Go Parent.Go
0x40c0e0 child.Go Child.Hop
0x40c0e4 child.Go Parent.Step
*/
親のメソッドからオーバーライドした子のメソッドが呼び出されるようになりました。
superと初期化
ついでにsuperで親を参照できるようにしましょう。そうなってくると埋め込んだstructの初期化のルールも必要になってきます。
(3) structには初期化メソッドInit()を定義し、superをセットし、super.Init()を呼ぶ。
孫まで定義した例です。
func main() {
parent := NewParent()
parent.Go("parent.Go")
child := NewChild()
child.Go("child.Go")
grandchild := NewGrandChild()
grandchild.Go("grandchild.Go")
grandchild.super.Hop("grandchild.super.Hop()")
grandchild.super.super.Hop("grandchild.super.super.Hop()")
}
/*
parent.Go Parent.Go
parent.Go Parent.Hop
parent.Go Parent.Step
parent.Go Parent.Jump
child.Go Parent.Go
child.Go Child.Hop
child.Go Child.Step
child.Go Parent.Jump
grandchild.Go Parent.Go
grandchild.Go GrandChild.Hop
grandchild.Go Child.Step
grandchild.Go Parent.Jump
grandchild.super.Hop() Child.Hop
grandchild.super.super.Hop() Parent.Hop
*/
https://play.golang.org/p/IN8OAe7Ai8I
まとめ
3つほどのルールを適用することで、Golangで無理やり継承を実現できました。特別なテクニックという気はしませんが、やっぱり闇になるんでしょうか。
少なくとも一から作るものに適用するのは良くないのでしょうが、既存資産をGolangに移植する場合の保険としては有用ではないかと思います。
(2018/11/11追記) アイディアとしては前例がありました。
go - golang-and-inheritance - Stack Overflow
提示コードが端折り過ぎで意図が伝わりにくいこともあって肯定的な回答は付いていませんが、どっちにしてもこれからの話であれば「継承はやめて素直にGoのやり方でやれ」となるのは当然かなと思います。