




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第3章Go语言面向对象编程面向对象的概念01类与对象1.类2.对象类与对象1.类现实世界中的各种事物都可以分类,例如,星球、动物、房子、学生、汽车等。类包含属性、方法和事件,通过属性表示它的特征(数据),通过方法实现它的行为(功能),通过事件做出响应。类可以派生出子类(派生类),派生子类的类称为父类。对于一个系统来说,其最基本的类称为基类,由基类派生出多个类,这些类还可以继续派生出更多的子类,形成类的层次结构。例如:基类——汽车子类:卡车、轿车、客车等汽车类属性:车轮、方向盘、发动机、车门等汽车类方法:前进、倒退、刹车、转弯、听音乐、导航等汽车类事件:车胎漏气、油用到临界、遇到碰撞等类与对象2.对象对象是类的具体化,是具有属性和方法的实体(实例)。对象通过唯一的标识名区别于其他对象。对象通常还有固定的对外接口,它是对象与外界通信的通道。例如,汽车类派生出的轿车子类的对象有:比亚迪F6、奥迪A6L等。02面向对象编程面向对象编程面向对象编程(Object-OrientedProgramming,简称OOP)是一种基于类和对象的程序设计方法。它将需要解决的问题抽象成一个个能以计算机逻辑形式表现的封装实体,即对象,类是在对象之上的抽象,通过定义属性和方法来描述其特征和职责。类为属于它的全部对象提供了统一的抽象描述,可看作是一种抽象的数据类型,是对象的模板;对象则是类的具体化,是类的实例,用接口来描述对象的地位以及对象实例之间的关系。由此构成面向对象的概念模型如图,它能更好地反映现实世界,对事物的描述更加自然,使问题的解决更容易。面向对象编程在面向对象程序设计中,数据结构与作用于其上的算法被视为了一个整体,即为对象,而现实世界中任何类的对象都具有一定的属性和方法,可以用数据结构与算法二者分别加以描述,所以可用下面的等式来定义面向对象程序:对象=数据结构+算法程序=对象+对象+…03面向对象语言的特征1.封装2.继承3.多态面向对象语言的特征1.封装所谓“封装”,也就是用一个框架把数据和代码组合在一起,形成对象。封装的对象之间通过一种称为“消息传递”的机制进行通信。消息是向对象发出的服务请求,它包含要求接收对象(接收者)去执行某些活动的信息,以及完成要求所需的其他信息(参数)。发送消息的对象(发送者)不需要知道接收者如何对请求予以响应,接收者收到消息,它就承担了执行指定动作的责任,通过执行自身内部封装的某个方法来满足发送者的请求,并作出答复。面向对象语言的特征2.继承世界本质是复杂的,但在大千世界中事物之间又有很多相似之处,而这种相似性正是人们理解纷繁事物的一个基础。相似的事物之间往往具有某种“继承”关系,比如,儿子长得像父亲,是因为儿子的基因继承了父亲的许多遗传特性;卡车、轿车、客车存在相似性,是因为它们都属于汽车,继承了汽车的一般特征。“继承”是面向对象方法的一块基石,通过它可以建立起具有等级层次的类的体系。例如,先创建一个通用的汽车类,定义汽车的一般属性(车轮、方向盘、发动机、车门等)和操作方法(前进、倒退、刹车、转弯等),再从这个已有的类出发,用继承的方式派生出新的子类,如卡车、轿车、客车等,见图。它们都是比汽车类更具体的类,每个具体的类都可以增加自己特有的一些属性和方法。在面向对象系统设计中,继承关系更一般的表示如图。面向对象语言的特征另外,继承也是父类与子类之间共享数据和方法的一个重要途径。如果一个类有两个或以上的直接父类,这样的继承结构称为多重继承或多继承。现实中这种模型也屡见不鲜,如一些类似于沙发床的组合功能产品,既有沙发的功能,又有床的功能,应当允许它同时继承自沙发和床这两个类,如图。多继承更一般的表示见图。面向对象语言的特征3.多态多态是指同一个消息或操作在作用于不同对象时,可以有不同的反应,产生不同的行为和结果。举个例子:春秋时期,孔子率众弟子周游列国因其“仁”的主张得不到采纳而被各国驱逐出境,有一次在荒野里饿得走不动了,于是问他的弟子为何会落到这步田地“匪兕匪虎,率彼旷野。吾道非耶,吾何为于此?”子路以为“是不是由于我们没有仁德和智谋,所以人们不采纳我们的主张?”子贡认为“先生的道(理想)大到了极点以致天下人所不容,倒不如降低自己的理想以求安身。”颜回则坚持“即使天下人都不容我们,但只要理想是正确的,不被世人接纳也没关系,走自己的路方显君子本色!”
——见《史记·孔子世家》陈蔡之厄的典故第3章Go语言面向对象编程面向对象在Go中的实现01封装的实现1.属性2.方法3.属性访问4.对象创建封装的实现1.属性Go语言并没有用于表示类的class关键字,但可以使用结构体(struct)实现对属性的封装,形式为:type类名struct{
属性1类型1
属性2类型2 ...
属性n类型n}例如,定义一个人类Human,包括姓名、身高、体重、年龄4个属性,如下:typeHumanstruct{ namestring heightfloat32 weightfloat32 ageint}封装的实现2.方法Go类的方法不像其他面向对象语言那样写在类体内部,而是一个以指针作用于类上的外部函数,形如:func(指针名*类名)方法名([形参列表])[(返回值列表)]{
方法体 [return[值列表]]}例如,给人类Human定义一个计算BMI(BodyMassIndex,身体质量指数)的方法bmiCal,如下:func(h*Human)bmiCal()float32{ returnh.weight/(h.height*h.height)}封装的实现3.属性访问在面向对象的编程方式下,外部程序通常不建议直接访问类内部的属性,而是通过类提供的一对get/set方法来获取和设置属性值,即所谓的通过“模型(值对象)”操作数据,很多持久化框架也是基于这样的规范来编写程序的。Go类的get/set方法的定义方式与普通方法一样,但建议将它们分别命名为getXxx、setXxx(其中Xxx为属性名,首字母大写)。例如,定义一对获取与设置人类体重的get/set方法,如下://get方法func(h*Human)getWeight()float32{ returnh.weight //获取体重}//set方法func(h*Human)setWeight(weightfloat32){ h.weight=weight //设置体重}这样在程序中就可以通过这对方法来设置和获取一个人的体重值,如下:man:=Human{} //创建人类的对象(一个人)man.setWeight(60) //设置他(她)的体重fmt.Println(man.getWeight()) //60封装的实现4.对象创建创建类的对象,通常有如下几种写法:对象名:=类名{}对象名:=new(类名)var对象名类名=类名{}在创建对象后,再通过定义好的一系列set方法给对象的各属性赋初值,形如:对象名.setXxx1(属性1值)对象名.setXxx2(属性2值)...对象名.setXxxn(属性n值)当然,也可以在创建对象的同时就初始化其各个属性的值,比如只写一句:对象名:=类名{属性1值,属性2值,...,属性n值}封装的实现【实例3.1】用面向对象方法封装一个人的身高、体重等属性,并计算其BMI值。程序代码如下(human.go):运行结果如图。02继承的实现继承的实现Go语言没有用于声明继承关系的extends关键字,而是采用在结构体中内嵌类型的方式来实现继承,即让子类包含其父类名称的属性,形式如下:type父类名struct{
属性1类型1
属性2类型2 ...
属性n类型n}
type子类名struct{
父类名
属性n+1类型n+1
属性n+2类型n+2 ...
属性n+n类型n+n}这样继承父类后,子类不仅自动“拥有”父类原来的所有属性(属性1~属性n),还可以增加定义一些自己特有的属性(如上面的属性n+1~属性n+n),同时,原来定义在父类上的方法也会自动属于子类,当然也可以另外增加定义一些子类所特有的方法。继承的实现【实例3.2】定义一个动物类Animal作为父类,人类Human作为子类继承动物类的属性,并增加一个物种(species)属性,然后输出一个具体的人的信息。程序代码如下(animal.go):运行结果如图。03多态的实现1.定义接口2.实现方法3.动态绑定多态的实现1.定义接口先定义一个接口,里面声明(罗列出)需要实现多态的一系列方法:type接口名interface{
方法名1()类型1
方法名2()类型2 ...
方法名n()类型n}多态的实现//第1个类对接口中方法的实现func(指针名*类名1)方法名1()类型1{ ...//方法体}func(指针名*类名1)方法名2()类型2{ ...//方法体}...func(指针名*类名1)方法名n()类型n{ ...//方法体}
//第2个类对接口中方法的实现func(指针名*类名2)方法名1()类型1{ ...//方法体}func(指针名*类名2)方法名2()类型2{ ...//方法体}...func(指针名*类名2)方法名n()类型n{ ...//方法体}2.实现方法接口仅提供方法声明,并未有方法实现,具体的实现则留给各个类自己去完成,这里假设有两个类,分别独立实现上面接口中的方法如下:多态的实现3.动态绑定编程时,可声明一个接口类型的变量,将不同对象实例的引用(地址)赋给它,运行时就会动态绑定到相应的类上,执行其所实现的方法,如下:var变量名接口名变量名=&类名1{...} //给变量赋第1个类的对象实例变量名.方法名i() //执行第1个类所实现的方法i变量名=&类名2{...} //给变量赋第2个类的对象实例变量名.方法名i() //执行第2个类所实现的方法i多态的实现【实例3.3】运用多态分别判断一个人和一只大熊猫是否成年。背景知识:对于一个人而言,满18周岁才算成年,熊猫则不然,由于熊猫的生理发育进程比人类快得多(大约在3倍以上),四五岁就已性成熟,而一般超过20岁的大熊猫就被认为是老年熊猫。大熊猫的年龄和人类的年龄可以用以下公式来大致换算:
大熊猫年龄≈3.5×人类年龄+1.5实现思路:本例先定义一个父类Animal(动物类),再定义两个子类Human(人类)和Panda(熊猫类)继承自Animal类。(1)定义一个Adult(成年)接口,其中有获取姓名和年龄的getName和getAge方法,获取物种名的getSpecies方法,还有判断对象是否成年的isAdult方法。(2)由父类Animal实现接口的getName和getAge方法,这两个方法是公共的,仅有唯一的实现,Human和Panda类都可以继承使用。多态的实现(3)Human和Panda类分别实现各自的getSpecies和isAdult方法,获取自身所属的物种名,并以不同的算法来判断自己的类对象是否成年,实现多态。程序代码如下(adult.go):运行结果如图。第3章Go语言面向对象编程类与方法01用结构体定义类1.命名与未命名类型2.自定义命名类型3.基于结构体定义的类4.基于类定义的类用结构体定义类1.命名与未命名类型在Go语言中,数据类型分为命名与未命名两种。(1)命名类型所有基本数据类型,包括整型、浮点型、复数型、布尔型、字符串型等都是命名类型,之所以叫“命名类型”是因为,基本数据类型的保留字就唯一确定了这个类型本身,比如,两个整型变量“varaint”与“varbint”,它们的类型完全相同并无任何差异,其保留字名int也就是类型名。除了Go内置的基本数据类型之外,用户自定义的类型也是命名类型,例如,自定义的类:typeHumanstruct{ ...}(2)未命名类型反之,那些没有固定名称来唯一确定其类型的则是未命名类型。用结构体定义类2.自定义命名类型Go的对象系统是基于用户自定义类型构建起来的,自定义类型使用关键字type,基本语法格式为:type类型名已有类型其中,“类型名”是自定义类型的名称,由用户任取,只要符合Go的标识符命名规范就行;“已有类型”可以是Go语言的基本数据类型、任何型态的复合数据类型,当然也可以是另一个自定义类型。显然,自定义类型有其确定的类型名,是“命名类型”。例如:typeMyFloatfloat32 //基于基本数据类型float32定义的类型typeINTint //基于基本数据类型int定义的类型typep_INT*int //基于整型指针定义的类型types_INT[]int //基于整型切片定义的类型typecircleAreafunc(float32)float32 //基于函数(函数签名)定义的类型用结构体定义类3.基于结构体定义的类Go语言的结构体(struct)是一种由一系列相同或不同类型的数据组合而成的集合,其中每个数据项称为该结构体的“成员”,各成员可以是基本类型也可以是复合类型的数据,甚至还可以是另一个结构体。显然,结构体是一种“未命名”的复合数据类型,可以基于它用以上方法定义出一个个命名的数据类型来,形式如下:type类型名struct{
字段1类型1
字段2类型2 ...
字段n类型n}用结构体定义类其中,结构体的每个成员对应于所定义类型中的一个字段,每个字段都拥有自己的类型,且同一个结构体类型定义中不能有相同的字段名,字段的类型可以相同也可以不同,如果几个字段的类型相同,也可以将它们写在同一行,形如:type类型名struct{
字段1类型1 ...
字段i,字段i+1,...,字段i+k类型i ...}将以上定义中的“类型名”作为“类名”、“字段”作为“属性”,就得到了之前所讲的面向对象封装中类的定义:type类名struct{
属性1类型1
属性2类型2 ...
属性n类型n}用结构体定义类4.基于类定义的类既然类本质上是一种类型(自定义的命名类型),当然也可以基于已有类定义出新类,使用语句:type新类名已有类名但要注意:用这种方式定义的类是一个新的命名类型,它与原有类之间并不存在继承关系,也不会继承原有类的方法。【实例3.4】证明“白马非马”。背景知识:“白马非马”是战国时思想家、名家代表人物公孙龙(前320~前250年)所提出的一个著名的命题。相传有一次公孙龙牵一匹白马出关,被守关士兵拦下告知“国君有令严禁马匹出关!”公孙龙说“国君只说不让马出关,而我牵的这匹是白马,白马与马是不同的……”一番长篇大论,有理有据,驳得士兵哑口无言,只得放行。
——详见《公孙龙子·白马论》用结构体定义类读者可能会奇怪:白马难道不是马么?公孙龙究竟是用什么样的理由说服守关的士兵放行的,看了下面的程序大家就会明白其中道理了。程序代码如下(horse.go):说明:(a)语句fmt.Println(whitehorse.isHorse())执行输出错误信息:whitehorse.isHorseundefined(typeWhiteHorsehasnofieldormethodisHorse)。这说明白马确实“非马”。(b)语句fmt.Println(yellowhorse.isHorse())执行输出错误信息:yellowhorse.isHorseundefined(typeYellowHorsehasnofieldormethodisHorse)。黄马也“非马”。在注释掉以上两句代码后,运行结果如图。02类的初始化1.按属性顺序2.指定属性名3.使用new函数类的初始化1.按属性顺序此方式将类的各个属性值按照定义时所声明的顺序依次罗列在一对大括号“{}”内,有3种不同写法,如下://写法1h1:=Human{"王林",1.75,65,19}
//写法2h2:=Human{ "Tom", 1.83, 81.5, 18, //加上英文逗号}
//写法3h3:=Human{ "王燕", 1.66, 49, 20}类的初始化2.指定属性名此方式显式地指定需要初始化的属性名及其值(中间以冒号“:”分隔),也有3种写法://写法1h1:=Human{name:"王林",height:1.75,weight:65,age:19}
//写法2h2:=Human{ name:"Tom", height:1.83, weight:81.5, age:18, //加上英文逗号}
//写法3h3:=Human{ name:"王燕", height:1.66, weight:49, age:20}同样地,如果结尾的“}”独占一行,最后的属性值后面也要加逗号。类的初始化这种方式的好处是写法比较灵活,可以不必严格按照类定义的属性顺序赋值,也可以省略一些属性(由系统自动初始化为其类型的零值),另外,当修改了类的定义(如增加属性)时,只须对增加的属性单独赋值,而原来的初始化代码段不用做任何修改,例如:3.使用new函数new是Go系统的内置函数,它一次性将类的所有属性都初始化为各自类型的零值,并返回一个指向类体(结构体)的指针,如下:h3:=new(Human)fmt.Println(h3) //&{000}03类的方法1.方法的本质2.方法调用类的方法1.方法的本质方法是一种对类行为的封装,将3.2.1节方法定义与2.7.1节函数定义的语法格式放在一起加以比较,如下。方法定义:func(指针名*类名)方法名([形参列表])[(返回值列表)]{
方法体 [return[值列表]]}函数定义:func函数名([参数列表])[(返回值列表)]{
函数体 [return[值列表]]}类的方法如果将方法定义语法中的“方法名”看作是一个特殊的参数(其类型为指向类名的指针)并与后面的“形参列表”合并为一个完整的“参数列表”,那么方法本质上其实就是一个函数,只不过它显式地指定将类对象的指针作为自己的第一个参数而已,这个参数在Go语言中又被称为方法的“接收者”,这样一来,方法定义的语法也就可以改写为函数定义的形式,如下:func函数名(接收者名接收类型[,其他参数列表])[(返回值列表)]{
函数(方法)体 [return[值列表]]}类的方法【实例3.5】将前面【实例3.1】人类Human计算BMI的方法改写为一个函数,实现同样的计算功能。程序代码如下(method01.go):运行结果如图。方法的“接收者”除了是指针类型外,还可以是值类型,如将上例中计算BMI的函数写成如下:funcbmiCal(hHuman)float32{ //接收者为值类型 returnh.weight/(h.height*h.height)}然后在调用函数时将传入的实参由引用改为类对象的变量值:fmt.Println(man.getName(),"BMI指数是",bmiCal(man))类的方法2.方法调用可通过两种方式调用类的方法,如下。(1)方法值调用这是最普通的方式,调用格式为:对象名.方法名([实参列表])或者(对象名).方法名([实参列表])值调用返回的是一个值类型的变量,可以直接在程序中使用,如打印输出或赋值给其他变量。类的方法(2)方法表达式这种方式实际是将类的方法转换为函数,再通过调用函数来执行方法。刚刚已揭示了方法的本质其实就是函数,既然如此,可以用“类名”与“方法名”所构成的表达式来唯一地确定一个等价函数,这个表达式又称为“方法表达式”。根据方法“接收者”类型的不同,方法表达式有两种书写格式,如下。①当“接收者”为值类型时,方法表达式写为:类名.方法名②当“接收者”为指针类型时,方法表达式写为:(*类名).方法名 //注意这里的括号不能省略类的方法【实例3.6】分别用上述两种方式调用方法,计算圆面积和周长。程序代码如下(method02.go):运行结果如图。04类的嵌套和方法覆盖1.内嵌属性的访问2.内嵌方法的覆盖类的嵌套和方法覆盖1.内嵌属性的访问用点操作符“.”访问内嵌属性,当有n层类的嵌套时,可使用全路径进行访问,形如“对象名.类名1.类名2....类名n.属性”,但是,如果属性在其整个路径的嵌套结构中是唯一的,就不需要写出全路径,简写为“对象名.属性”即可。例如,定义A、B、C三个类:typeAstruct{ astring bstring}
typeBstruct{ A bstring}
typeCstruct{ B bstring cstring}类的嵌套和方法覆盖分别创建它们的对象实例并初始化如下:objA:=A{ a:"I'mA.", b:"It'sA'sb."}objB:=B{ A:objA, b:"I'mB."}objC:=C{ B:objB, b:"It'sC'sb.", c:"I'mC.",}类的嵌套和方法覆盖由于只有基类A具有a属性,故objC.a、objC.B.a、objC.B.A.a、objB.A.a、objB.a都指的是A类的a,输出测试如下:fmt.Println(objC.a) //I'mA.fmt.Println(objC.B.a) //I'mA.fmt.Println(objC.B.A.a) //I'mA.fmt.Println(objB.A.a) //I'mA.fmt.Println(objB.a) //I'mA.但是,因为这三个类皆有b属性,所以objC.b、objC.B.b、objC.B.A.b是不同类的b,访问时必须写出全路径而不能简写,输出测试如下:fmt.Println(objC.a) //I'mA.fmt.Println(objC.B.a) //I'mA.fmt.Println(objC.B.A.a) //I'mA.fmt.Println(objB.A.a) //I'mA.fmt.Println(objB.a) //I'mA.fmt.Println(objC.b) //It'sC'sb.fmt.Println(objC.B.b) //I'mB.fmt.Println(objC.B.A.b) //It'sA'sb.类的嵌套和方法覆盖2.内嵌方法的覆盖内嵌方法调用也使用点操作符“.”,外层对象调用内嵌类的方法时也可以像访问内嵌属性一样使用全路径或简写,当采用简写形式时,Go编译器会从外向内逐层查找,如果外层类与内嵌类有相同的方法,优先调用最外层的方法,从而实现子类方法对父类方法的覆盖。比如,对上面的A、B、C三个类,分别定义如下同名的方法:func(tA)say(){ //A类的say方法 fmt.Println("Hi!",t.a)}
func(tB)say(){ //B类的say方法 fmt.Println("Hi!",t.b)}
func(tC)say(){ //C类的say方法 fmt.Println("Hi!",t.c)}类的嵌套和方法覆盖在程序中用不同方式调用它们,测试如下://从外向内查找,首先找到的是C类的say方法objC.say() //Hi!I'mC.//自B类对象开始向内查找,优先执行B类的say方法objB.say() //Hi!I'mB.//用全路径调用最内层A类的say方法objC.B.A.say() //Hi!I'mA.类的嵌套和方法覆盖使得Go语言的类具备了强大的表达力,几乎可以表示客观世界中任何复杂的对象实体,并给对象扩充出丰富的行为能力,下面通过一个实例形象地演示这一点。类的嵌套和方法覆盖【实例3.7】演示从“鱼”到“人”的进化。背景知识:根据古生物学的研究,生物的进化经历了从水生到陆生的演变过程。最初地球上的生物都生活在海洋中,是鱼类的时代。大约3亿7500万年前(泥盆纪晚期)的某个时候,有一种提塔利克鱼(学名:Tiktaalik,见图)的鳍发生了变异,长出原始的腕骨及趾头,于是这些鱼纷纷尝试着用鳍支撑身体爬上岸来,具有了初步的陆上爬行能力,演化出包括恐龙、猿猴在内种类繁多的陆地动物。到了距今550万年前(中新世末期),生活在非洲大陆东南部的一群猿猴(南方古猿)由于气候环境变化被迫下到地面,逐步学会了直立行走,最终进化成人类。类的嵌套和方法覆盖实现思路:本例先定义一个基类Fish(鱼类),及两个表示能力的类SwimAbility(游泳)和WalkAbility(行走)。Fish类嵌套SwimAbility表示鱼会游泳,并实现一个基础的游泳方法swimming;再定义一个Tiktaalik(提塔利克鱼类)继承自Fish类,增加嵌套一个WalkAbility类表示它比一般的鱼多了行走能力,并实现基础的行走方法walking;然后定义Tiktaalik的子类Monkey(猿猴类),覆盖其父类的walking方法,以直立行走取代爬行;最后定义的Human(人类)继承Monkey类,就同时拥有了鱼和猿的能力。另外,还定义了一个Sailfish(旗鱼类),它直接派生自Fish,并没有嵌套新的能力类,但重写(覆盖)了Fish类的swimming方法,游泳能力极大地增强了。程序代码如下(fishtohuman.go):运行结果如图。第3章Go语言面向对象编程接口01接口声明与初始化1.接口声明2.接口初始化3.空接口接口声明与初始化1.接口声明接口在本质上是一种类型,故也像其他自定义类型一样用type关键字声明。接口是一组方法的集合(也可以只有一个方法),但不包含这些方法的具体实现,其声明的语法格式如下:type接口名interface{
方法1([形参列表])[(返回值列表)]
方法2([形参列表])[(返回值列表)] ...
方法n([形参列表])[(返回值列表)]}例如:typeAquaticAnimalinterface{ //水生动物接口 swimming()string //游泳方法}
typeLandAnimalinterface{ //陆地动物接口 swimming()string //游泳方法 walking()string //行走方法}
typeManKindinterface{ //人类接口 swimming()string //游泳方法 walking()string //行走方法 manufacturing(toolstring)string //制造工具方法}接口声明与初始化接口声明中除了单纯的方法,还可以嵌入其他接口,这一点与类的嵌套(继承)有相似之处,如上面人类接口的定义还可以写成:typeManKindinterface{ AquaticAnimal //嵌入水生动物接口 walking()string manufacturing(toolstring)string}或者:typeManKindinterface{ LandAnimal //嵌入陆地动物接口 manufacturing(toolstring)string}接口声明与初始化2.接口初始化接口初始化的方式有两种,如下。1)用对象实例赋值如果一个类实现了接口中的所有方法(实现了该接口),就可以把这个类的对象实例赋值给接口。例如,定义一个人类Human,并实现人类接口中的所有(3个)方法,如下:typeHumanstruct{ //人类 namestring}
func(h*Human)swimming()string{ //实现游泳方法 return"会游泳"}
func(h*Human)walking()string{ //实现行走方法 return"会行走"}
func(h*Human)manufacturing(toolstring)string{ //实现制造工具方法 return"会制造"+tool}接口声明与初始化人类实现了ManKind接口,就可以将人类的对象实例赋值给ManKind接口,如下:funcmain(){ man:=Human{"北京猿人"} //创建一个人类的对象实例 varmanKindManKind=&man //赋值给接口 fmt.Println("我是",,",",manKind.manufacturing("石器"),"。")} //我是北京猿人,会制造石器。说明:(1)在赋值给接口时使用的是对象实例man的引用(&),因为Go语言的接口本质上是一个指针类型。(2)虽然对象实例赋给了接口,但只能通过这个接口调用对象实例的方法而不能访问其属性,要获取对象属性,只能用“对象.getXxx()”(若有get方法)或“对象.属性名”,而不能用“接口.属性名”,例如,如果将最后的输出语句改为:fmt.Println("我是",manK,",",manKind.manufacturing("石器"),"。")运行会报错:manKundefined(typeManKindhasnofieldormethodname)(3)只要接口的方法集是对象实例方法集的子集就都可以实现这种赋值,例如,人类对象也同样可以赋给陆地动物和水生动物接口,如下:varlandAnimalLandAnimal=&man //赋给陆地动物接口fmt.Println("我是",,",",landAnimal.walking(),"。") //我是北京猿人,会行走。varaquaticAnimalAquaticAnimal=&man //赋给水生动物接口fmt.Println("我是",,",",aquaticAnimal.swimming(),"。") //我是北京猿人,会游泳。接口声明与初始化2)用接口变量赋值用一个已经初始化的接口类型变量赋给另一个接口,这种方式在以下几种情况下可以。(1)两个接口等价两个拥有完全相同的方法集的接口称为等价接口,这个很好理解,如果接口A和接口B的方法是相同的,那么一个类只要实现了接口A自然也就实现了接口B,反之亦然,A和B实际上是等同的,它们的变量可以相互赋值。由于接口声明对方法的先后顺序并无要求,所以只要两个接口包含一样的方法,它们就满足等价条件,如下面这两个接口:typeOldManinterface{ //古人接口 swimming()string walking()string manufacturing(toolstring)string}
typeNewManinterface{ //新人接口 manufacturing(toolstring)string walking()string swimming()string}接口声明与初始化它们包含的都是swimming(游泳)、walking(行走)和manufacturing(制造工具)这3个方法,虽然方法声明的顺序不一致,接口的名称也不一样,但这两个接口实质是相同的,即它们完全等价,于是可以编写语句相互赋值,如下:man1:=Human{"北京猿人"}varoldManOldMan=&man1varnewManNewMan=oldMan //古人接口的变量赋给新人接口fmt.Println("我是",,",",newMan.manufacturing("旧石器"),"。") //我是北京猿人,会制造旧石器。man2:=Human{"山顶洞人"}newMan=&man2oldMan=newMan //新人接口的变量赋给古人接口fmt.Println("我是",,",",oldMan.manufacturing("新石器"),"。") //我是山顶洞人,会制造新石器。接口声明与初始化(2)被赋值接口的方法集是对方的子集如果接口A的方法集中包含了接口B的所有方法,即B的方法集是A的方法集的子集,就可以将接口A的变量赋值给B,但反之不行。例如:typeLandAnimalinterface{ //陆地动物(对应上面所说接口A) swimming()string walking()string}
typeAquaticAnimalinterface{ //水生动物(对应上面所说接口B) swimming()string}接口声明与初始化陆地动物接口的方法集有swimming(游泳)和walking(行走)两个方法,它包含了水生动物接口仅有的一个swimming方法,故陆地动物接口的变量可以赋给水生动物接口,例如:typeFishstruct{ //鱼类 categorystring}func(f*Fish)swimming()string{ //实现水生动物接口 return"会游泳"}
typeTiktaalikstruct{ //提塔利克鱼 Fish}func(t*Tiktaalik)walking()string{ //实现陆地动物接口 return"会爬行"}
funcmain(){ fish1:=Fish{"鱼"} fish2:=Tiktaalik{Fish{"提塔利克鱼"}} varnewAnimalLandAnimal=&fish2 varoldAnimalAquaticAnimal=newAnimal //陆地动物接口变量赋给水生动物 fmt.Println(fish2.category,"是",fish1.category,",",oldAnimal.swimming(),"。") //提塔利克鱼是鱼,会游泳。}接口声明与初始化但是,反过来却不行,如下语句:oldAnimal=&fish1newAnimal=oldAnimal //水生动物接口变量赋给陆地动物fmt.Println(fish1.category,"是",fish2.category,",",newAnimal.walking(),"。")运行会报错:cannotuseoldAnimal(variableoftypeAquaticAnimal)asLandAnimalvalueinassignment:AquaticAnimaldoesnotimplementLandAnimal(missingmethodwalking)接口声明与初始化3.空接口1)空接口的赋值显然,空接口的方法集为空,所以任何类(包括数据类型)都被认为实现了空接口,任何类(类型)的实例都可以赋值给空接口。例如:2)空接口的比较在Go中用一个内部常量nil代表空值,它也是空接口的初始值,但是,一个空接口类型的变量并非在任何情况下都是空的,两个空接口也不总是相等。接口声明与初始化下面声明两个空接口nulFace1和nulFace2,用程序测试并比较它们的实际值。varnulFace1,nulFace2interface{} //声明两个空接口(1)所有空接口变量初始值都为nil,都相等。测试如下:fmt.Println(nulFace1,nulFace2) //<nil><nil>fmt.Println(nulFace1==nil) //truefmt.Println(nulFace1==nulFace2) //true(2)任何非空接口变量的默认值也为nil,与空接口相等。例如,对于尚未初始化的水生动物接口:typeAquaticAnimalinterface{ swimming()string}执行语句:varoldAnimalAquaticAnimal //声明接口变量(但不初始化)fmt.Println(oldAnimal) //<nil>fmt.Println(nulFace1==oldAnimal) //true接口声明与初始化(3)当一个空接口接受了赋值之后,其值会出现多种情形,如下。①如果给空接口赋值的是一个有值的变量(或常量),则赋值后空接口值就等于该变量(或常量)的值,不再为nil,也与其他空接口不再相等。例如:constcint=299792458 //整型常量nulFace1=c //常量赋给空接口fmt.Println(nulFace1) //299792458fmt.Println(nulFace1==c) //truefmt.Println(nulFace1==nil) //falsefmt.Println(nulFace1==nulFace2) //falsenulFace1=nil //将接口nulFace1重置为空②如果给空接口赋值的是一个已经初始化了的非空接口,则赋值后空接口就与该非空接口相等,不再为nil,也与其他空接口不再相等。例如,将水生动物接口初始化后赋给空接口:fish1:=Fish{"鱼"}oldAnimal=&fish1 //初始化水生动物接口nulFace1=oldAnimal //赋给空接口fmt.Println(nulFace1) //&{鱼}fmt.Println(nulFace1==oldAnimal) //truefmt.Println(nulFace1==nil) //falsefmt.Println(nulFace1==nulFace2) //false接口声明与初始化③如果给空接口赋值的是一个尚未初始化的非空接口,则赋值后空接口值为(回归)空。例如,对于尚未初始化的陆地动物接口:typeLandAnimalinterface{ swimming()string walking()string}执行语句:varlandAnimalLandAnimal //声明接口变量(但未初始化)nulFace1=landAnimal //赋给空接口fmt.Println(nulFace1==nil) //truefmt.Println(nulFace1==nulFace2) //true接口声明与初始化④如果给空接口赋值的是未初始化的变量,则赋值后空接口值为该变量类型的零值或空值,不再为nil,也与其他空接口不再相等。例如:varaintnulFace1=a //未初始化的整型变量赋给空接口fmt.Println(nulFace1) //0fmt.Println(nulFace1==nil) //falsefmt.Println(nulFace1==nulFace2) //falsevarbcomplex64nulFace1=b //未初始化复数型变量赋给空接口fmt.Println(nulFace1) //(0+0i)接口声明与初始化(4)两个分别被赋了变量值的空接口的相等性取决于赋给它们的变量的值是否相等。例如:vard1float32=3.14vard2float32=math.Pi //圆周率精确值nulFace1=d1nulFace2=d2fmt.Println(nulFace1==nulFace2) //falsenulFace2=d1fmt.Println(nulFace1==nulFace2) //true02接口类型推断1.类型判断2.类型查询接口类型推断1.类型判断类型判断又称“类型断言”,写为一个表达式:接口变量名.(类型名)它用于判断接口变量绑定的实例是否实现了(或本身就是)括号中所指类型的接口(或数据),故这里的“类型名”可以是一个接口类型名,也可以是其他(基本或复合)数据类型名。程序中的类型判断表达式通常写在如下形式的代码块中:if_,ok:=类型判断表达式;ok{ ...//代码段1}else{ ...//代码段2}接口类型推断【实例3.8】判断一种类人动物是否是“人”。背景知识:在人类从“猿”到“人”的进化过程中,产生了很多中间过渡类型的动物,古人类学家判断一种动物是否是真正意义上的“人”,主要依据两条标准——直立行走和制造工具。如果一种类人动物已经会直立行走了,但还不能制造工具,那么它只能被划归为类人猿亚目下的“人科”成员,而不是“人属”物种,即还不能算作人,例如,史上曾经出现过的乍得沙赫人、始祖地猿、南方古猿、鲍氏傍人……虽然它们有的名称中也带有“人”字,但本质上并不是人;只有会制造工具的物种,如元谋人、蓝田人、北京猿人、山顶洞人、尼安德特人等,才是“人属”动物,才算真正的“人”。实现思路:本例先定义一个Monkey(猿猴)接口,里面有walking(直立行走)方法;定义ManKind(人类)接口嵌套(继承)Monkey接口,再增加一个manufacturing(制造工具)方法。Ape(类人猿)类实现Monkey接口,Human(人)类继承Ape类,并进一步实现ManKind接口。程序根据运行时接口的实际类型来判断一个对象究竟是猿还是人。程序代码如下(ishuman.go):运行结果如图。接口类型推断2.类型查询如果一个接口变量的类型尚不确定(或有多种可能),可使用“接口变量名.(type)”表达式结合switch语句进行类型查询以确定变量所属的准确类型,语法格式如下:switch接口变量名.(type){case类型名1: [<语句1>]case类型名2: [<语句2>]...case类型名n: [<语句n>][default:<语句n+1>]}接口类型推断【实例3.9】查询确定动物的类型。程序代码如下(iswhatanimal.go):运行结果如图。接口类型推断需要特别注意以下两点:(1)fallthrough语句不能用在类型查询的switch语句中。(2)当查询的多个可能类型之间存在嵌套(继承)关系,如本例的三个接口之间是逐层嵌套定义的,要将子接口的判断分支写在父接口之前,因为程序只要匹配上一个分支,就不会再去比对其余的分支,若将上面的查询函数改成如下:funcisWhatAnimal(animalAquaticAnimal){ switchanimal.(type){ caseAquaticAnimal: fmt.Println(animal.getName(),animal.swimming(),",是水生动物。") caseLandAnimal: fmt.Println(animal.getName(),animal.swimming(),",是陆地动物。") caseManKind: fmt.Println(animal.getName(),animal.swimming(),",是人。") default: fmt.Println(animal.getName(),"所属动物类型不确定!") }}接口类型推断再执行语句:varanimalAquaticAnimalanimal=&Human{Ape{Fish{"周何骏"}}}isWhatAnimal(animal)就会得出一个人是水生动物的结论如图,这显然是荒谬的!第3章Go语言面向对象编程反
射反
射Go语言提供一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不“知道”这些变量的具体类型,这称为“反射机制”。在Go内置的reflect包里定义了一个接口reflect.Type和一个结构体reflect.Value,它们提供很多函数来获取存储在接口里的类型信息。(1)reflect.Type接口:主要提供关于类型相关的信息。(2)reflect.Value结构体:主要提供关于值相关的信息,可以获取甚至改变类型的值。reflect包中提供了两个基础的函数来获取上述的接口和结构体,原型如下:funcTypeof(iinterface{})TypefuncValueOf(iinterface{})ValueGo语言中反射的使用遵循三大法则(又称“反射三定律”,详见《TheLawsofReflection》),如下:法则一:反射可以将“接口变量”转换为“反射对象”。法则二:反射可以由“反射对象”创建出“接口变量”。法则三:如果要修改“反射对象”,则其值必须是“可写的”。01接口变量转换为反射对象接口变量转换为反射对象反射是一种检查存储在接口变量中的(类型,值)对的机制。
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 宿舍线路火灾应急预案(3篇)
- 火灾山体滑坡地震应急预案(3篇)
- 软件设计师考试自我激励与提升策略试题及答案
- 逆袭成功的软件设计师考试试题及答案
- 企业网络服务模型试题及答案
- 高考数学解析能力提升指南试题及答案
- 2025年网络攻防技能试题及答案
- 法学概论的影响力试题与答案分析
- 面对失败的成长经历2023年高考作文试题及答案
- 网络测量工具试题及答案
- 保研经验分享会课件
- 2024年重庆市高考物理试卷(含答案解析)
- 2024-2030年中国军用个人防护装备行业市场发展趋势与前景展望战略分析报告
- 2022年6月英语四级真题 第一套
- DB33∕T 2154-2018 公路桥梁后张法预应力施工技术规范
- 新编应用文写作全套教学课件
- 四川省凉山州2022-2023学年七年级下学期期末历史试题
- JBT 1306-2024 电动单梁起重机(正式版)
- QBT 2262-1996 皮革工业术语
- 《工程建设标准强制性条文电力工程部分2023年版》
- 心理干预各论家庭治疗
评论
0/150
提交评论