为什么Golang没有継承,以及如何在Golang中通过强行实现継承
首先
Golang是一种面向对象的编程语言吗?答案是“是和否”(官方FAQ)。但是,您是否对哪些方面是”是”,哪些方面是”否”感到困惑和不明朗?
虽然非常感谢 @shibukawa 对我在尝试将 Golang 作为面向对象语言时所帮助的非常清楚易懂的指导,但是在进行到中间的时候,我开始有些觉得可能有点不同了。而且,在 Golang 里实现继承的「黑暗技巧」mattn 先生的内容里,并没有提及具体细节,这让我更加困惑了。(所以 Golang 可以实现继承啊,为什么说最好不要用?)
我已尝试将其整理成我自己的方式。
-
- そもそも継承は良くない
- Goに入ってはGoに従え
只要在那个方面我都会视而不见,请多关照。
为什么说Golang没有继承,是因为它有哪些限制?
我将立即给出关于这个问题的结论。在可以继承的语言中,
从维基百科的“继承(编程)”中引用
可以将继承了该超类的子类实例存储到超类中进行类型标注的变量中。
然而,Golang无法实现这一点。只能将struct的实例存储在struct的指针中。
所以,即使在子类中重写了父类的部分方法,也不会从父类的方法中调用它。
让我们确认一下。在父类中实现Hop()、Step()和连续调用这两个方法的Go(),在嵌入了父类的子类中,只重写了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原本的身份已经丧失了。
两种指针类型
在Golang中实现强制继承的方法是使用两种类型的指针:普通的结构体指针和能够接收结构体实例的接口类型变量。
type Jumper interface {
Hop(t string)
Step(t string)
Go(t string)
}
如果定义一个接口,那么这个变量可以存储实现了这些方法的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
*/
只要接口的变量实现了所有的方法,它就可以存储任何结构的实例。这表明它具有继承所需的功能,即“可以将子类的实例存储在父类的指针中”。
但是,只能通过接口类型的变量来调用方法,无法访问字段,因此不能直接将其作为self/this使用。
让接口拥有字段功能
即,
-
- フィールドにアクセスするには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能够引用父级对象。这样一来,嵌入的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的方式来做”是当然的。