struct-面向对象(1)-go语言

struct面向对象特点

  1. go语言只支持封装,不支持继承和多态
  2. go只有struct,class (就是因为只用封装)
    说明:继承和多态在C++、java里面都有,是比较麻烦不容易搞懂的事情,使用的时候也有诸多的限制条件。所有go语言面向对象里面去掉继承、多态,面向对象只要有封装就足够了,而继承和多态的任务使用接口来实现,面向接口编程。

struct结构体的定义

  1. 结构体都是有字段(feild)组成,每个字段(field)都有所属的数据类型,在struct体内字段名必须唯一。
  2. struct结构体就是用来存储数据的,可自定义的程度很高,使用灵活,所有go很多功能的实现依赖于结构体struct。
  3. go语言实际上是不支持面向对象的,面向对象中描述事物的类就是struct角色。面向对象中的继承,可以使用结构(struct)的组合(composite)实现:struct嵌套一个或多个类型。面向对象中父类与子类、对象和类是is a 的关系,例如 apple is a fruit 。struct组合是外部struct和内部struct的关系,对象和类关系是struct实例和struct结构体的关系。go通过struct的composite可以模仿很多面向对象的行为,他们很像。
//定义结构体的例子
type People struct {
	age int  //字段名 字段类型
	name string 
}
//某几个字段类型相同,可以缩写在同一行
type MyStruct struct {
   x,y,z int
}
//每个字段都有类型,可以是任意类型,包括内置简单数据类型、其它自定义的struct类型、当前struct类型本身、接口、函数、channel等等。
type Student struct {
	People //匿名字段 其他自定义struct类型 也可视为是类的继承
	class string
}

构造struct实例

定义了struct就表示定义了一个数据结构,或者说是一个数据类型,也或者说是一个类。定义了struct,就具备了成员属性,可以作为一个抽象模板。依据抽象模板生产具体的实例,也就是所谓的对象。

type Point struct {
   x,y,z int
}
//声明变量类型后置式
var p1 Point  //实例化一个struct

//可以直接赋值定义struct的属性来生成struct的实例,go语言根据值推断出类型。
var p2 = Point{7,4,1}
//简短声明 := 
p3 := Point{1,2,3} 
//如果struct的属性分行赋值,则必须不能省略每个字段后面的逗号",",否则就会报错。这为未来移除、添加属性都带来方便:
p4 := Point{
    x:17,
    y:100,  // 这个逗号不能省略
}
//除此之外,还可以使用new()函数或&TYPE{}的方式来构造struct实例,它会为struct分配内存,为各个字段做好默认的赋0初始化。它们是等价的,都返回数据对象的指针给变量,实际上&TYPE{}的底层会调用new()。
p5 := new(Point)
//赋值
p5.x = 100
p6 := &Point{}
//使用&TYPE{}的方式也可以初始化赋值,但new()不行。
//选择new()还是选择&TYPE{}的方式构造实例?完全随意,它们是等价的。但如果想要初始化时就赋值,可以考虑使用&TYPE{}的方式
p7 := &Point{4,3,9}

p1是具体的Point实例,它是依据抽象的模板Point构造出来的。具有具体的属性x,y,z 的值,初始化的时各个字段都是零值(zero)。
struct初始化时,会做默认的赋零值初始化,会给它的每个字段根据它们的数据类型赋予对应的零值。例如int类型是数值0,string类型是"",引用类型是nil等。
访问方式: p1.field

//调用实例p1字段 赋值
p1.x = 2
p1.y = 4
//获取某个属性的值
fmt.Println(p2.z)

struct的值和指针

下面三种方式都可以构造Tree struct的实例,t1和t2、t3是不一样的,输出一下就知道了。

package main
import "fmt"
//定义一个结构体
type Tree struct {
	value int
	left,right *Tree
}
func main(){
	//三种方式实例化结构体
	t1 := Tree{}
	t2 := new(Tree)
	t3 := &Tree{}
	fmt.Println(t1)
	fmt.Println(t2)
	fmt.Println(t3)
}

输出结果:
输出结果
t1、t2、t3都是Tree struct的实例,但t2和t3是完全等价的,它们都指向实例的指针,指针中保存的是实例的地址,所以指针再指向实例,p1则是直接指向实例。这三个变量与Tree struct实例的指向关系如下:

 变量名      指针     数据对象(实例)
t1(addr) -------------> { 0}
t2 -----> ptr(addr) --> { 0}
t3 -----> ptr(addr) --> { 0}

t1和ptr(addr)保存的都是数据对象的地址,t2、t3则保存ptr(addr)的地址。通常将指向指针的变量(t1、t2)直接称为指针,将直接指向数据对象的变量(t1)称为对象本身,因为指向数据对象的内容就是数据对象的地址,其中ptr(addr)和t1保存的都是实例对象的地址。尽管一个是数据对象值,一个是指针,但是它们都是数据对象的实例。也就是说,t1.value和t2.value都能访问对应实例的属性。
var t4 *Tree 是什么?该语句表示t4是一个指针,它的指向对象是Tree类型的,但因为它是一个指针,它将初始化为nil,即表示没有指向目标。但已经明确表示了,t4所指向的是一个保存数据对象地址的指针。t4的指向关系如下:

t4 -> ptr(nil)
//定义t4 Tree的指针类型
var t4 *Tree
//赋值
t4 = &Tree{
    value:7,
}
fmt.Println(t4)

传值 or 传指针

go函数给参数传递值的时候是以复制的方式进行的。复制传值时,如果函数的参数是一个struct对象,将直接复制整个数据结构的副本传递给函数,有以下两个问题:

  1. 函数内部无法修改传递给函数的原始数据结构,它修改的只是原始数据结构拷贝后的副本。
  2. 若传递的原始数据结构很大,完整地复制出一个副本开销大。
    所以如果条件允许,应当给函数传struct的指针。例如:
func play(t *Tree){...}

struct的指针何来?自然是通过&符号来获取。分两种情况:

  1. 创建成功的实例,已经创建成功的struct实例t1,如果这个实例是一个值而非指针,那么可以&p来获取这个已存在的实例的指针传递给函数。
play(&t1)
  1. 尚未创建的实例,可以使用&person{}或者new(person)的方式直接生成实例的指针t,虽然是指针,但go能自动解析成实例对象
t2 := new(Tree)
play(t2)

struct field的tag属性

结构体(struct)字段(field)除了名称和数据类型,还可以有一个tag属性。tag属性用于"注释"各个字段。除了reflect包,正常的程序中都无法使用这个tag属性。go语言中可以通过反射机制在运行时动态获取到struct的tag来实现一些特定功能。例如 json序列化的时候指定字段名,字段名必须大写首字母才能被外界访问和json格式化。

package main

import (
	"encoding/json"
	"fmt"
	"reflect"
)

//定义一个结构体
type Student struct {
	Name string `json:"stu_name" bson:"b_name"`
	Age int `json:"stu_id" bson:"b_id"`
	Class string `json:"stu_class" bson:"b_class"`
}

func main(){
	stu1 := Student{"张三",10,"5年级六班"}
	//转化成json  按照tag指定的字段名转换
	stu1Json,_ := json.Marshal(&stu1)
	fmt.Println("-----json格式化数据")
	fmt.Println(string(stu1Json))
	//通过reflect获得tag属性
	t:=reflect.TypeOf(&stu1)
	field := t.Elem().Field(0)
	fmt.Println("-----tag属性值")
	fmt.Println(field.Tag.Get("bson"))
	fmt.Println(field.Tag.Get("json"))
}

输出结果:
输出结果2

匿名字段和Struct嵌套

Struct中的字段可以不用给名称,这时称为匿名字段,匿名字段的名称和类型相同。例如:

type People struct{
	Name string `json:"stu_name" bson:"b_name"`
	Age int `json:"stu_id" bson:"b_id"`
}

type Student struct {
	People
	int
	Class string `json:"stu_class" bson:"b_class"`
}

上面的Student中有两个匿名字段int和People,它的名称和类型都是int和People。

type Student struct {
	People People
	int int
	Class string `json:"stu_class" bson:"b_class"`
}

上面Student结构体中嵌套了People结构体。其中People称为内部struct,Student称为外部struct。内部struct的属性也能被外部struct调用,也就是说,外部struct has a内部struct,或者称为struct has afield。下面演示嵌套字段的调用

package main

import (
	"fmt"
)
type People struct{
	Name string `json:"stu_name" bson:"b_name"`
	Age int `json:"stu_id" bson:"b_id"`
}
type Student struct {
	People //内部结构体
	int
	Class string `json:"stu_class" bson:"b_class"`
}
func main(){
	stu := Student{People{"张三",10},45,"5年级六班"}
	//调用外边结构体的字段
	fmt.Println(stu.int)
	fmt.Println(stu.Class)
	//调用内部结构体的字段
	fmt.Println(stu.People.Name)
	fmt.Println(stu.Name)
}

在赋值Student中的Name和Age时不能少了People{},否则会认为Name、Age是直接属于Student,而非嵌套属于Student。显然,Struct的嵌套类似于面向对象的继承,继承的关系模式是"子类 is a 父类",例如"轿车是一种汽车",嵌套Struct的关系模式是外部struct has a 内部struct,正如上面示例中Student拥有People。而且,从上面的示例中可以看出,go是支持"多重继承"的。

嵌套struct的名称冲突问题

外部Struct中的字段名和内部Struct的字段名相同时,遵循以下规则:

  1. 外部Struct覆盖内部Struct的同名字段、同名方法
  2. 同级别的struct出现同名字段、方法将报错

第一个规则使得Struct能够实现面向对象中的重写(override),可以重写字段、重写方法。
第二个规则使得同名属性不会出现歧义。
以下外部Struct覆盖内部Struct的同名字段的例子:

package main
import (
	"fmt"
)
type People struct{
	Name string `json:"stu_name" bson:"b_name"`
	Age int `json:"stu_id" bson:"b_id"`
}
type Student struct {
	People
	int
	Class string `json:"stu_class" bson:"b_class"`
	Name string
}
func main(){
	stu := Student{People{"张三",10},45,"5年级六班","李四"}
	//调用外边结构体的字段
	fmt.Println(stu.int)
	fmt.Println(stu.Class)
	//就近原则
	fmt.Println(stu.People.Name) //调用内部的同名Name
	fmt.Println(stu.Name) //调用外部的同名Name
}

输出结果
输出结果3

递归Struct:嵌套自身

如果Struct中嵌套的Struct类型是自己的指针类型,可以用来生成特殊的数据结构:链表或二叉树(双端链表)。例如定义一个单链表数据结构,每个Node都指向下一个Node,最后一个Node指向空。

type Node struct {
    data string
    ri   *Node
}

以下是链表结构示意图:

 ------|----         ------|----         ------|-----
| data | ri |  -->  | data | ri |  -->  | data | nil |
 ------|----         ------|----         ------|----- 
  • 如果嵌套两个自己的指针,每个结构都有一个左指针和一个右指针,分别指向它的左边节点和右边节点,就形成了二叉树或双端链表数据结构。二叉树的左右节点可以留空,可随时向其中加入某一边加入新节点(像节点加入到树中)。添加节点时,节点与节点之间的关系是父子关系。添加完成后,节点与节点之间的关系是父子关系或兄弟关系。
  • 双端链表有所不同,添加新节点时必须让某节点的左节点和另一个节点的右节点关联。例如目前已有的链表节点A <-> C,现在要将B节点加入到A和C的中间,即A<->B<->C,那么A的右节点必须设置为B,B的左节点必须设置为A,B的右节点必须设置为C,C的左节点必须设置为B。也就是涉及了4次原子性操作,它们要么全设置成功,失败一个则链表被破坏。

关于双端链表的解释:https://www.cnblogs.com/freebrid/p/4621843.html
双端链表

关于二叉树的解释 https://www.jianshu.com/p/bf73c8d50dc2
二叉树的示例图
二叉树有以下特点:
1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
2)左子树和右子树是有顺序的,次序不能任意颠倒。
3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

定义一个二叉树:

type Tree struct {
	data int
    le   *Tree
    ri   *Tree
}

最初生成二叉树时,root节点没有任何指向。root节点的初始左右两端为空

root := new(Tree)
root.data = 1

随着节点增加,root节点开始指向其它左节点、右节点,这些节点还可以继续指向其它节点。向二叉树中添加节点的时候,只需将新生成的节点赋值给它前一个节点的le或ri字段即可。例如:

package main
import (
	"fmt"
)
type Tree struct {
	le *Tree
	ri *Tree
	data int
}
func main(){
	root := Tree{data:1}
	newLeft := new(Tree)
	newLeft.data = 3
	newRight := &Tree{nil, nil, 5}
	// 添加到树中
	root.le = newLeft
	root.ri = newRight
	//再添加一个新节点到newLeft节点的右节点
	newRight1 := &Tree{nil, nil, 9}
	newLeft.ri = newRight1
	//简单输出这个树中的节点:
	fmt.Println(root)
	fmt.Println(newLeft)
	fmt.Println(newRight)
}

输出结果
二叉树输出结果
使用二叉树的时候,必须为二叉树结构设置相关的方法,例如添加节点、设置数据、删除节点等。另外需要注意的是,一定不要将某个新节点的左、右同时设置为树中已存在的节点,因为这样会让树结构封闭起来,这会破坏了二叉树的结构。
附件是一个二叉树的例子 TODO 待续

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值