十、面向对象编程_上
10.1 结构体
10.1.1 结构体
看一个问题
10.1.2 一个程序就是一个世界,有很多对象(变量)
10.1.3 Golang语言面向对象编程说明
10.1.4 结构体与结构体变量(实例/对象)的关系示意图
对上图的说明
1、将一类事物的特性提取出来(比如:猫类),形成一个新的数据类型,就是一个结构体
2、通过这个结构体,我们可以创建多个变量(实例对象)
3、事物可以是猫类,也可以是person,也可以是鱼类
10.1.5 快速入门-面向对象的方式(struct)解决养猫的问题
10.1.6 结构体和结构体变量(实例)的区别联系
- 结构体是自定义的数据类型,代表一类事物
- 结构体变量(实例)是具体的,实际的,代表一个具体变量
10.1.7 结构体变量(实例)在内存中的布局—重要
结构体是值类型
10.1.8 如何声明结构体
type 结构体名称 struct {
field1 type
field2 type
.....
}
// 举例
type Cat struct {
Name string //Name 大写则表示可以被其他包引用
Age int
Color string
Hobby string
}
10.1.9 字段/属性
基本介绍
- 从概念或叫法上:结构体=属性=field
- 字段是结构体的一个组成部分,一般是基本数据类型、数组、也可是引用类型,比如我们前面定义猫的结构体的Name string就是属性
注意事项
- 字段声明语法同变量,示例: 字段名 字段类型
- 字段的类型可以为:基本类型、数组或引用类型
- 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面说的一样
- 布尔类型是 false 、整型是 0 、 字符串是 “”
- 数组类型的默认值和它的元素类型相关,比如:socore [3]int 则为[0,0,0]
- 指针,slice和map的零值都是nil,即还没有分配空间
- 不同结构体变量的字段是独立的,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型
上述代码的内存示意图
10.1.10 创建结构体变量和访问结构体字段
10.1.10.1 方式1
直接声明
var person Person
10.1.10.2 方式2
var person Person = Person {}
10.1.10.3 方式3
10.1.10.4 方式4
说明
-
第三种和第四种方式返回的是 结构体指针
-
结构体指针访问字段的标准方式应该是:(*结构体指针).字段名,
-
(*person).Name = "tom"
-
-
但go做了一个简化,也支持 结构体指针.字段名 比如:person.Name = "tom" 更加符合程序员使用的习惯,go编译器底层对 person.Name做了转化(*person).Name
10.1.11 struct类型的E内存分配机制
思考?
变量总是在内存中的,那么结构体变量在内存中究竟是怎样存在的?
结构体在内存中示意图
上述代码内存图
10.1.12 结构体的注意事项和使用细节
1、结构体的所有字段在内存中是连续的
2、结构体是用户单独定义的类型,和其他类型进行转换时需要有完全相同的字段(名字、个数和类型)
3、结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
10.1.13 结构体注意事项
- struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列号和反序列化
序列化的使用场景
代码
10.2 方法
10.2.1 基本介绍
在某些情况下,我们需要声明/定义(方法),比如:Person结构体,除了有一些字段外(年龄,姓名...)Person结构体还有一些行为,这时就要用方法才能完成
Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法。而不仅仅时struct
10.2.2 方法的声明和调用
type A struct {
Num int
}
func (a A) test() {
fmt.Println(a.Num)
}
对上面语法的说明
1、func (a A) test() {} 表示A结构体有一方法,方法名为 test
2、(a A) 体现方法是和A类型绑定的
举例
对上面代码说明
1、 test方法和Person类型绑定
2、test方法只能通过Person类型的变量来调用,也不能使用其他类型变量来调用
3、func (p Person) test(){....} p表示哪个Person变量调用,这个p就是它的副本,这点和函数的传参十分相似
4、func (p Person) test(){....} p这个名字,有程序员固定,不是固定不变的
10.2.3 方法快速入门
1、给Person结构体添加 speak方法,输出 xxx是一个好人
2、给Person结构体添加 jisuan方法,可以计算从1+....100的结果,说明方法体内可以函数一样,进行各种运算
3、给Person结构体添加 jisuan2方法,可以接收一个数n,计算从1+...+n的结果
4、给Person结构体添加 getSum方法,可以计算两个数的和,并返回结果
5、方法调用的几种形式
10.2.4 方法的调用和传参机制原理(重要!!)
说明:方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将方法的变量,当作实参也传递给方法
案例1
前面getSum方法的执行过程+说明
上述代码图示
说明
1、在通过一个变量去调用方法时,其调用机制和函数一样
2、不一样的地方是,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)
案例2
编写一个程序,要求如下
- 声明一个结构体Circle,字段为radius
- 声明一个方法area和Circle绑定,可以返回面积
上述代码图示
10.2.5 方法的声明/定义
func (recevier type) methodName (参数列表) (返回值列表) {
方法体
return 返回值
}
1、参数列表:表示方法输入
2、recevier type:表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型
3、recevier type:type可以是结构体,也可以是其他的自定义类型
4、receiver:就是type类型的一个变量(实例),比如:Person结构体的一个变量(实例)
5、返回值列表:表示返回的值,可以多个
6、方法主体:表示为了实现某一功能代码块
7、return :语句不是必须的
10.2.6 方法注意事项和细节讨论
1、结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
2、如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
3、Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定)。。因此自定义类型,都可以有方法,而不仅仅时struct,比如:int float32等都可以有方法
4、方法的访问范文控制的规则,和函数一样,方法名首字母小写,只能在本包访问,方法名大写,可以在本包和其他包访问
5、如果一个类型实现了 String() 这个方法,那么fmt.Println 默认会调用这个变量的String()进行输出
10.2.7 方法练习题
1、编写结构体,编程一个方法,方法不需要参数,在方法中打印一个10*8的矩形,在main方法中调用该方法
2、编写一个方法,提供m和n两个参数,方法中打印一个 m*n的矩形
3、编写一个方法,算该矩形的面积(可以接收长len,和宽width),将其作为方法返回值,在main方法中调用该方法,接收返回的面积并打印
4、编写一个方法,判断一个数是奇数还是偶数
5、根据行、列、字符打印对应行数和列数的字符,比如:行:3、列:2、字符:*
6、定义小小计算器结构体(Calcuator),实现加减乘除四个功能
- 实现形式1:分四个方法完成,分别计算 + - * /
package main
import (
"fmt"
)
type Calcuator struct {
Num1 float64
Num2 float64
}
func (calcuator *Calcuator) getSum() float64 {
return calcuator.Num1 + calcuator.Num2
}
func (calcuator *Calcuator) jianfa() float64 {
return calcuator.Num1 - calcuator.Num2
}
func (calcuator *Calcuator) chengfa() float64 {
return calcuator.Num1 * calcuator.Num2
}
func (calcuator *Calcuator) chufa() float64 {
return calcuator.Num1 / calcuator.Num2
}
func main() {
var calcuator Calcuator
calcuator.Num1 = 1.2
calcuator.Num2 = 2.2
fmt.Println("加法=", calcuator.getSum())
// 下一行的 fmt.Sprintf("%.2f") 是格式化处
fmt.Printf("减法= %v\n", fmt.Sprintf("%.2f",calcuator.jianfa()))
fmt.Println("乘法=", calcuator.chengfa())
fmt.Println("除法=", calcuator.chufa())
}
- 实现形式2:用一个方法完成 ,需要接收两个数,还有一个运算符
package main
import (
"fmt"
)
type Calcuator struct {
Num1 float64
Num2 float64
}
func (calcuator *Calcuator) getRes(operator byte) float64 {
res := 0.0
switch operator {
case '+':
res = calcuator.Num1 + calcuator.Num2
case '-':
res = calcuator.Num1 - calcuator.Num2
case '*':
res = calcuator.Num1 * calcuator.Num2
case '/':
res = calcuator.Num1 / calcuator.Num2
default:
fmt.Println("运算符输出有误.....")
}
return res
}
func main() {
var calcuator Calcuator
calcuator.Num1 = 1.2
calcuator.Num2 = 2.2
res := calcuator.getRes('+')
fmt.Println("res=", res)
}
10.3 方法和函数区别
1、调用方式不一样
函数的调用方式: 函数名(实参列表)
方法的调用方式: 变量.方法名(实参列表)
2、对于普通函数,接受者为值类型时,不能将指针类型的数据直接传递,反之亦然
3、对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
10.4 面向对象编程实例
10.4.1 步骤
- 声明(定义)结构体,确定结构体名
- 编写结构体的字段
- 编写结构体的方法
10.4.2 案例
学生个人信息案例
package main
import (
"fmt"
)
type Student struct {
name string
gender string
age int
id int
score float64
}
func (student *Student) say() string {
infoStr := fmt.Sprintf("Student的信息 name=[%v] gender=[%v] age=[%v] id=[%v] score=[%v]",
student.name, student.gender, student.age, student.id, student.score)
return infoStr
}
func main() {
// 测试
// 创建一个Student实例变量
var stu = Student{
name : "tom",
gender : "male",
age : 18,
id : 10000,
score : 99.99,
}
fmt.Println(stu.say())
}
10.4.3 盒子案例
- 编程创建一个Box结构体,在其中声明三个字段表示一个立方体的长、宽、高、这三者要从终端获取
- 声明一个方法获取立方体的体积
- 创建一个Box结构体变量,打印给定尺寸的立方体的体积
10.4.4 景区门票案例
- 一个景区根据游人的年龄收取不同价格的门票,比如年龄大于18,收费20元,其他情况门票免费
- 请编写Visitor结构体,根据年龄段决定能购买的门票价格并输出
package main
import (
"fmt"
)
// 创建一个 Visitor 结构体,定义了长宽高
type Visitor struct {
Name string
Age int
}
func (visitor *Visitor) showprice() {
if visitor.Age > 90 || visitor.Age <= 8 {
fmt.Println("考虑到安全,请不要去玩耍")
return
}
if visitor.Age > 18 {
fmt.Printf("游客的名字为%v 年龄为%v 收费20元 \n", visitor.Name, visitor.Age)
} else {
fmt.Printf("游客的名字为%v 年龄为%v 免费 \n", visitor.Name, visitor.Age)
}
}
func main() {
// 测试
var v Visitor
for {
fmt.Println("请输入你的名字:")
fmt.Scanln(&v.Name)
if v.Name == "n" {
fmt.Println("谢谢、退出程序")
break
}
fmt.Println("请输入你的年龄:")
fmt.Scanln(&v.Age)
v.showprice()
}
}
10.5 创建结构体变量时指定字段值
Golang在创建结构体实例(变量)时,可以直接指定字段的值。
创建结构体变量时指定字段值方式
方式1
package main
import (
"fmt"
)
type Student struct {
Name string
Age int
}
func main() {
//方式1
// 在创建结构体变量时,就直接指定字段的值
var stu1 = Student{"小明", 19} // stu1 --> 结构体数据空间
stu2 := Student{"小明~", 22}
// 方式2
// 在创建结构体变量时,把字段名和字段值写在一起
// 这种写法,就不依赖字段的定义顺序
var stu3 = Student{
Name : "Jack",
Age : 9,
}
stu4 := Student {
Age : 30,
Name : "Marry",
}
fmt.Println(stu1, stu2, stu3, stu4)
fmt.Println()
}
方式2
package main
import (
"fmt"
)
type Student struct {
Name string
Age int
}
func main() {
// 方式2 返回结构体的指针类型!!!!!!!!!!
var stu5 = &Student{"小王", 29} //stu5 --> 地址 --> 结构体数据
stu6 := &Student{"小王", 33}
// 在创建结构体指针变量时,把字段名和字段值写在一起
// 这种写法,就不依赖字段的定义顺序
var stu7 = &Student {
Name : "小李",
Age : 49,
}
stu8 := &Student {
Name : "小李~",
Age : 49,
}
fmt.Println(*stu5, *stu6, *stu7, *stu8)
}
10.6 工厂模式
10.6.1 说明
Golang的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
10.6.2 看一个需求
一个结构体的声明是这样的:
package model
type Student struct {
Name string....
}
因为这里的Student的首字母S是大写的,如果我们想在其它包创建Student的实例(比如:main包),引入model包后,就可以直接创建Student结构体的变量,但是问题来了,如果首字母是小写的,比如:type strdent struct {....} 就不行了,怎么办,------> 工厂模式解决
student.go
main.go
如果model包的student 的结构体的字段 Score改成score,我们还能正常访问吗?又该如何解决这个问题?
解决办法