面向对象
在讲解具体面向对象编程之前,先说一下面向过程编程。我们前面学习都是面向过程的一种编程思想,接下来可以从生活中理解面向过程:
如果我们自己来修汽车,应该有哪些步骤呢?
第一步:找工具
第二步:判断问题的原因
第三步:暴力拆卸
这个修理的步骤就是面向过程,所谓的面向过程就是:强调的是步骤、过程、每一步都是自己亲自去实现的。
如果采用面向对象的思想,那么应该怎样修车呢?
找4s店的工作人员来帮我们修车,但是到底怎么修,我们是不用考虑的,也就是说我们不关心步骤与过程。
所谓的面向对象其实就是找一个专门做这个事的人来做,不用关心具体怎么实现的。
所以说,面向过程强调的是过程,步骤。而面向对象强调的是对象,也就是干事的人。
大家可以想一下,在生活中还有哪些事情是面向过程,面向对象的。
比如说,玩游戏,如果是面向过程,就是自己玩,自己打怪升级。如果是面向对象,可以找代练,一切打怪升级都有代练完成,不需要我们自己关心了。
比如说,做饭,面向过程就是自己做,自己买菜,自己洗,自己炒,整个过程都有自己来完成,但是如果是面向对象,可以叫外卖,不用关心饭是怎么做的。
所以通过以上案例,大家能够体会出,面向过程就是强调的步骤,过程,而面向对象强调的是对象,找个人来做。
在面向对象中,还有两个概念是比较重要的,一是对象,二是类。
什么是对象呢?
万物皆对象,例如张三同学是一个对象,李四同学也是一个对象
那么我们在生活中怎样描述一个对象呢?
比如,描述一下张三同学:
姓名:张三
性别:男
身高:180cm
体重:70kg
年龄:22岁
吃喝拉撒睡一切正常
通过以上的描述,可以总结出在生活中描述对象,可以通过特征(身高,体重,年龄等)和行为(爱好等)来进行描述。
那么在程序中,可以通过属性和方法(函数)来描述对象。属性就是特征,方法(函数)就是行为。所以说,对象必须具有属性和方法。虽然说,万物皆对象,但是在描述一个对象的时候,一定要具体不能泛指,例如,不能说“电灯”是一个对象,而是说具体的哪一台“电灯”。
下面我们在思考一下,下面这道题:
张三(一个学生)\杨老师\邻居售货员张阿姨\李四的爸爸\李四的妈妈
找出这道题中所有对象的共性(所谓共性,指的是相同的属性和方法)。
所以说,我们可以将这些具有相同属性和相同方法的对象进行进一步的封装,抽象出来类这个概念。
类就是个模子,确定了对象应该具有的属性和方法。
对象是根据类创建出来的
例如:上面的案例中,我们可以抽出一个“人”类(都有年龄,性别,姓名等属性,都有吃饭,走路等行为),“张三”这个对象就是根据“人”类创建出来的,也就是说先有类后有对象。
Go语言中的面向对象
前面我们了解了一下,什么是面向对象,以及类和对象的概念。但是,GO语言中的面向对象在某些概念上和其它的编程语言还是有差别的。
严格意义上说,GO语言中没有类(class)的概念,但是我们可以将结构体比作为类,因为在结构体中可以添加属性(成员),方法(函数)。
面向对象编程的好处比较多,我们先来说一下“继承”,
所谓继承指的是,我们可能会在一些类(结构体)中,写一些重复的成员,我们可以将这些重复的成员,
单独的封装到一个类(结构体)中,作为这些类的父类(结构体)。当然严格意义上,GO语言中是没有继承的,但是我们可以通过”匿名组合”来实现继承的效果。
匿名字段
创建与初始化
我们可以使用匿名字段,来做到属性的继承。那什么是匿名字段呢?
package main
import "fmt"
type Person struct {
id int
name string
age int
}
type Student struct {
Person
score int
}
func main() {
person := Person{name: "mark", id: 17, age: 25}
student := Student{person, 990}
fmt.Println(student)
person.age = 20
fmt.Println(student)
// 操作成员
fmt.Printf("age:%d\n", student.Person.age)
fmt.Printf("age:%d\n", student.age)
fmt.Printf("score:%d\n", student.score)
}
输出如下:
{{17 mark 25} 990}
{{17 mark 25} 990}
age:25
age:25
score:990
以上代码通过匿名字段实现了继承,将公共的属性封装在Person中,在Student中直接包含Person,那么Student中就有了Person中所有的成员,Person就是匿名字段。注意:Person匿名字段,只有类型,没有名字。
指针类型匿名对象
结构体(类)中的匿名字段的类型,也可以是指针。
package main
import "fmt"
type Person struct {
id int
name string
age int
}
type Student struct {
*Person
score int
}
func main() {
person := Person{name: "mark", id: 17, age: 25}
student := Student{&person, 990}
fmt.Println(student.Person)
// 操作成员
fmt.Printf("age:%d\n", student.age)
fmt.Printf("score:%d\n", student.score)
}
输出如下:
&{17 mark 25}
age:25
score:990
方法
在Go语言中,函数和方法不太一样,有明确的概念区分。其他语言中,比如Java,一般来说,函数就是方法,方法就是函数,但是在Go语言中,函数是指不属于任何结构体、类型的方法,也就是说,函数是没有接收者的;而方法是有接收者的,我们说的方法要么是属于一个结构体的,要么属于一个新定义的类型的。
我们先定义一个传统的函数:
func test(a, b int) int {
return a + b
}
func main() {
result := test(1, 2)
fmt.Println(result)
}
方法
方法的声明和函数类似,他们的区别是:方法在定义的时候,会在func
和方法名之间增加一个参数,这个参数就是接收者,这样我们定义的这个方法就和接收者绑定在了一起,称之为这个接收者的方法。在Go中,只有这样才能为一个结构体增加一个方法。
type person struct {
name string
}
func (p person) String() string{
return "the person name is "+p.name
}
留意例子中,func
和方法名之间增加的参数(p person)
,这个就是接收者。现在我们说,类型person
有了一个String
方法,现在我们看下如何使用它。
func main() {
p:=person{name:"张三"}
fmt.Println(p.String())
}
调用的方法非常简单,使用类型的变量进行调用即可,类型变量和方法之前是一个.
操作符,表示要调用这个类型变量的某个方法的意思。
Go语言里有两种类型的接收者:值接收者和指针接收者。我们上面的例子中,就是使用值类型接收者的示例。
使用值类型接收者定义的方法,在调用的时候,使用的其实是值接收者的一个副本,所以对该值的任何操作,不会影响原来的类型变量。
package main
import "fmt"
type Student struct {
id int
name string
age int
}
func (stu Student ) PrintShow() {
fmt.Println(stu)
}
func main() {
//{101 zhangsan 19}
s:= Student{101,"zhangsan",19}
s.PrintShow()
}
用指针当接收者
package main
import "fmt"
type Student struct {
id int
name string
age int
}
func (stu *Student ) PrintShow() {
stu.age = 25
}
func main() {
s:= Student{101,"zhangSan",19}
fmt.Println("调用前:",s)
s.PrintShow()
fmt.Println("调用后:",s)
}
输出如下:
调用前: {101 zhangSan 19}
调用后: {101 zhangSan 25}
Go没有方法重载
在使用方法是,要注意如下几个问题:
第一:只要接收者类型不一样,这个方法就算同名,也是不同方法,不会出现重复定义函数的错误
type Person struct {
id int
name string
level string
}
type Student struct {
id int
name string
age int
}
func (stu Student ) PrintShow() {
}
func (teacher Person) PrintShow() {
}
但是,如果接收类型一样,但是方法的参数不一样,是会出现错误的,也就说,在GO中没有方法重载(所谓重载,指的是方法名称一致,参数类型,个数不一致)。
package main
import "fmt"
type Person struct {
id int
name string
level string
}
type Student struct {
id int
name string
age int
}
func (stu Student ) PrintShow() {
}
func (stu Student ) PrintShow(i int) {
}
方法继承
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
//在human上面定义了一个method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
输出如下:
Hi, I am Mark you can call me on 222-222-YYYY
Hi, I am Sam you can call me on 111-888-XXXX
上面由于 Student和Employee中都包含了Human,所以当使用mark.SayHi()或sam.SayHi()的时候,他会先查找本结构体中有没有包含SayHi()方法。然后它发现并没有包含,于是就在属性中去寻找,属性Human中有SayHi()方法,于是就调用了human的SayHi()方法。上面等价于:
mark.Human.SayHi()
sam.Human.SayHi()
方法重写
在前面的案例中,子类(结构体)可以继承父类中的方法,但是,如果父类中的方法与子类的方法是重名方法会怎样呢?
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
//Human定义method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Employee的method重写Human的method
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
输出如下:
Hi, I am Mark you can call me on 222-222-YYYY
Hi, I am Sam, I work at Golang Inc. Call me on 111-888-XXXX
如果子类(结构体)中的方法名与父类(结构体)中的方法名同名,在调用的时候是先调用子类(结构体)中的方法,这就方法的重写。所谓的重写:就是子类(结构体)中的方法,将父类中的相同名称的方法的功能重新给改写了。
其实这也是就近原则,如果看懂了方法继承中的解释,这里也很容易理解。
为什么要重写父类(结构体)的方法呢?
通常,子类(结构体)继承父类(结构体)的方法,在调用对象继承方法的时候,调用和执行的是父类的实现。但是,有时候需要
对子类中的继承方法有不同的实现方式。
Hi, I am Mark you can call me on 222-222-YYYY
Hi, I am Sam, I work at Golang Inc. Call me on 111-888-XXXX
如果子类(结构体)中的方法名与父类(结构体)中的方法名同名,在调用的时候是先调用子类(结构体)中的方法,这就方法的重写。所谓的重写:就是子类(结构体)中的方法,将父类中的相同名称的方法的功能重新给改写了。
其实这也是就近原则,如果看懂了方法继承中的解释,这里也很容易理解。
为什么要重写父类(结构体)的方法呢?
通常,子类(结构体)继承父类(结构体)的方法,在调用对象继承方法的时候,调用和执行的是父类的实现。但是,有时候需要
对子类中的继承方法有不同的实现方式。