go语言学习笔记(四)——继承
什么是继承
继承是面向对象思想中的一个概念。
如果一个类别A“继承自”另一个类别B,就把这个A称为“B的子类别”,而把B称为“A的父类别”也可以称“B是A的超类”。继承可以使得子类别具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类别追加新的属性和方法也是常见的做法。
在golang中如何实现继承呢?我们知道golang中结构体可以自由定义属性和方法,golang中便主要是针对结构体来实现类似继承的方法。
使用匿名组合达到继承
如果一个struct嵌套了另一个【有名】的结构体,那么这个模式称作组合。如下:
type Car struct {
weight int
name string
}
type Bike struct {
Car
lunzi int
}
如上,在Bike中引入了Car类。
如果一个struct嵌套了另一个匿名结构体,那么这个结构可以直接访问匿名结构体的方法,从而实现继承
type Car struct {
weight int
name string
}
type Train struct {
Car
}
根据以下代码可以清楚理解,匿名和组合均能实现我们想要的“继承”效果
type Car struct {
weight int
name string
}
func (p *Car) Run() {
fmt.Println("running")
}
type Bike struct {
Car
lunzi int
}
type Train struct {
Car
}
func (p *Train) String() string {
str := fmt.Sprintf("name=[%s] weight=[%d]", p.name, p.weight)
return str
}
func main() {
var a Bike
a.weight = 100
a.name = "bike"
a.lunzi = 2
fmt.Println(a)
a.Run()
var b Train
b.weight = 100
b.name = "train"
b.Run()
fmt.Printf("%s", &b)
}
输出:
{{100 bike} 2}
running
running
name=[train] weight=[100]
如果一个struct嵌套了多个结构体,那么这个结构可以直接访问多个结构体的方法,从而实现多重继承
继承规则
- 在派生类没有改写基类的成员方法时,相应的成员方法被继承。
- 派生类可以直接调用基类的成员方法,譬如基类有个成员方法为Base.Func(),那么Derived.Func()等同于Derived.Base.Func()
- 倘若派生类的成员方法名与基类的成员方法名相同,那么基类方法将被覆盖或叫隐藏,譬如基类和派生类都有成员方法Func(),那么Derived.Func()将只能调用派生类的Func()方法,如果要调用基类版本,可以通过Derived.Base.Func()来调用。
详细调用:
重点一:
上面说明了派生类成员方法名与基类成员方法名相同时基类方法将被覆盖的情况,这对于成员变量名来说,规则也是一致的。
type Base struct {
Name string
}
type Derived struct {
Base
Name string
}
func main() {
d := &Derived{}
d.Name = "Derived"
d.Base.Name = "Base"
fmt.Println(d.Name) // Derived
fmt.Println(d.Base.Name) // Base
}
重点二:
匿名组合多个类时,类名相同,会不会冲突?答案是,会。就算包名不同,类名相同,也会冲突。原因在于,匿名成员也有一个隐式的名字,以其类型名称(去掉包名部分)作为成员变量的名字。 这就是匿名冲突
type Logger struct {
Level int
}
type MyJob struct {
*Logger
Name string
*log.Logger // duplicate field Logger
}
该方法事实上并不是原本含义的“继承”
当我们嵌入一个类型,这个类型的方法就变成了外部类型的方法,但是当它被调用时,方法的接受者是内部类型(嵌入类型),而非外部类型。
type Job struct {
Command string
*log.Logger
}
func (job *Job)Start() {
job.Log("starting now...")
... // 做一些事情
job.Log("started.")
}
上面这个Job例子,即使组合后调用的方式变成了job.Log(...),但Log函数的接收者仍然是 log.Logger指针,因此在Log中也不可能访问到job的其他成员方法和变量。
类型别名(Type Aliases)
类型别名是Golang 1.9引入的新特性,顾名思义类型别名是给Golang的类型提供创建别名的方法。使用的语法如下:
type AliasType = SomeType //给SomeType 创建别名AliasType
类型别名的使用实例如下:
type T1 struct {
Name string
}
func (t *T1) Hello() {
fmt.Printf("hello %s\n", t.Name)
}
type T2 = T1
func main() {
t := &T2{}
t.Name = "world"
t.Hello() //out: hello world
fmt.Printf("%#v", t) //out: &main.T1{Name:"world"}
}
通过示例可以看出我们可以在T2直接使用T1的成员变量与方法。有趣的是最后t变量本身的输出,我们得到了&main.T1{Name:"world"} 。这表明看起来我们声明了T2,但是经过编译后T2实际上会被替换为T1,他只是T1的一个字面上的别名。(看起来像C语言当中的#define T2 T1)
我们尝试为T2定义一个新方法:
type T1 struct {
Name string
}
func (t *T1) Hello() {
fmt.Printf("hello %s\n", t.Name)
}
type T2 = T1
func (t *T2) Bye() {
fmt.Print("Bye!")
}
func main() {
t1 := &T1{}
t1.Bye() //out: Bye!
}
为T2定义新方法后我们可以看到T1也得到了同样的方法。这就进一步说明T2仅仅是T1的一个字面上的别名,两者表达的是相同的类型。
最后因为类型别名仅仅是字面上的另一个类型的别名,我们无法为包含在其他包当中的方法创建别名后为其声明新的方法,也就是下面的做法会报错:
import "bytes"
type Buf = bytes.Buffer
func (b Buf) Hello(){ //cannot define new methods on non-local type bytes.Buffer
fmt.Print("hello")
}
总结
- 可以继承原有类型的成员变量和方法
- 如果要拓展一个新方法,需要在原有类型的同一个包里定义,否则会报错
- 因为别名方式类似于c里面定义宏,所以不能重写原有函数
- 使用别名定义一个新函数后,原有类型也拥有了新的函数
- 别名定义的类型和原有数据类型是同一个数据类型
使用已有类型声明新类型
Golang的类型声明可以使用已有的类型来创建新的类型。
type T1 struct {
I int
}
type T2 T1
func main() {
t := &T2{
I: 12,
}
fmt.Printf("%d\n", t.I)
}
从T1创建的新的类型T2可以使用T1中声明的成员变量I,但是无法使用在T1上定义的方法。
func (*T1) Hello() {
fmt.Printf("hello\n")
}
func main() {
t := &T2{}
t.Hello()
}
我们在T1上定义了Hello方法。如果我们在T2中调用Hello将会得到错误:
t.Hello undefined (type *T2 has no field or method Hello)
可见此种方式定义的新类型只是与原类型拥有相同的数据定义,但是不共享方法。
总结
- 只继承了原有类型的成员变量
- 如果是同一个包内,可以访问原有类型的小写变量,如果不在同一个包,则不能访问小写变量
- 因为不是同一个类型,所以可以重写(相当于新写)原有类型的方法