Go 学习笔记(36)— 基于Go方法的面向对象(封装、继承、多态)

Go 面向对象编程的三大特性:封装、继承和多态。

  • 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式
  • 继承:使得子类具有父类的属性和方法或者重新定义、追加属性和方法等
  • 多态:不同对象中同种行为的不同实现方式

Go 语言的结构体(struct)和其他语言的类(class)有同等的地位,但 Go 语言放弃了包括继
承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。

例如,我们要定义一个矩形类型

type Rect struct {
	x, y          float64
	width, height float64
}

然后我们定义成员方法 Area() 来计算矩形的面积:

func (r *Rect) Area() float64 {
	return r.width * r.height
}

在定义了 Rect 类型后,该如何创建并初始化 Rect 类型的对象实例呢?这可以通过如下几种方法实现:

rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{0, 0, 100, 200}
rect4 := &Rect{width: 100, height: 200}

Go 语言中,未进行显式初始化的变量都会被初始化为该类型的零值,例如 bool 类型的零值为 falseint 类型的零值为 0,string 类型的零值为空字符串。

Go 语言中没有构造函数的概念,对象的创建通常交由一个全局的创建函数来完成,以 NewXXX来命名,表示“构造函数”:

func NewRect(x, y, width, height float64) *Rect {
	return &Rect{x, y, width, height}
}

详细参见:https://blog.csdn.net/wohu1104/article/details/106202892 中的结构体初始化章节。

1. 封装

package main

import "fmt"

type data struct {
	val int
}

func (p_data *data) set(num int) {
	p_data.val = num
}

func (p_data *data) show() {
	fmt.Println(p_data.val)
}

func main() {
	p := &data{4}
	p.set(10)
	p.show()
}

或者

package main

import (
	"fmt"
)

// 矩形结构体
type Rectangle struct {
	Length int
	Width  int
}

// 计算矩形面积
func (r *Rectangle) Area() int {
	return r.Length * r.Width
}

func main() {
	r := Rectangle{4, 2}
	// 调用 Area() 方法,计算面积
	fmt.Println(r.Area())
}

2. 继承

确切地说,Go 语言也提供了继承,但是采用了组合的文法,所以我们将其称为匿名组合,Go 语言的继承方式采用的是匿名组合的方式。

package main

type Base struct {
	Name string
}

func (base *Base) Foo() { ... }
func (base *Base) Bar() { ... }

type Foo struct {
	Base
	...
}

func (foo *Foo) Bar() {
	foo.Base.Bar()
	...
}

以上代码定义了一个 Base 类(实现了 Foo()Bar() 两个成员方法),然后定义了一个 Foo 类,该类从Base 类“继承”并改写了 Bar() 方法(该方法实现时先调用了基类的 Bar() 方法)。

在“派生类” Foo 没有改写“基类” Base 的成员方法时,相应的方法就被“继承”,例如在上面的例子中,调用foo.Foo() 和调用 foo.Base.Foo() 效果一致。

此外,在 Go 语言中你还可以随心所欲地修改内存布局,如:

type Foo struct {
	... // 其他成员
	Base
}

这段代码从语义上来说,和上面给的例子并无不同,但内存布局发生了改变。“基类” Base 的数据放在了“派生类” Foo 的最后。

另外,在 Go 语言中,你还可以以指针方式从一个类型“派生”

type Foo struct {
	*Base
	...
}

这段 Go 代码仍然有“派生”的效果,只是 Foo 创建实例的时候,需要外部提供一个 Base 类实例的指针。

另外,我们必须关注一下接口组合中的名字冲突问题,比如如下的组合:

type X struct {
	Name string
}

type Y struct {
	X
	Name string
}

组合的类型和被组合的类型都包含一个 Name 成员,会不会有问题呢?

答案是否定的。所有的 Y 类型的 Name 成员的访问都只会访问到最外层的那个 Name 变量,X.Name 变量相当于被隐藏起来了。

那么下面这样的场景呢:

type Logger struct {
	Level int
}

type Y struct {
	*Logger
	Name string
	*log.Logger
}

显然这里会有问题。因为之前已经提到过,匿名组合类型相当于以其类型名称(去掉包名部分)作为成员变量的名字。按此规则,Y 类型中就相当于存在两个名为 Logger 的成员,虽然类型不同。

因此,我们预期会收到编译错误。有意思的是,这个编译错误并不是一定会发生的。假如这两个 Logger在定义后再也没有被用过,那么编译器将直接忽略掉这个冲突问题,直至开发者开始使用其中的某个 Logger

Woman结构体中包含匿名字段 Person,那么 Person 中的属性也就属于 Woman 对象。

package main

import "fmt"

type Person struct {
	name string
}

type Man struct {
	Person
	sex string
}

func main() {
	man := Man{Person{"wohu"}, "男"}
	fmt.Println(man.name) // wohu
	fmt.Println(man.sex)  // 男
}
package main

import "fmt"

type parent struct {
	val int
}

type child struct {
	parent
	num int
}

func main() {
	c := child{parent{1}, 2}
	fmt.Println(c.num)
	fmt.Println(c.val)
}

3. 多态

在面向对象中,多态的特征为:不同对象中同种行为的不同实现方式。在 Go 语言中可以使用接口实现这一特征。

package main

import (
	"fmt"
)

// 正方形
type Square struct {
	side float32
}

// 长方形
type Rectangle struct {
	length, width float32
}

// 接口 Shaper
type Shaper interface {
	Area() float32
}

// 计算正方形的面积
func (sq *Square) Area() float32 {
	return sq.side * sq.side
}

// 计算长方形的面积
func (r *Rectangle) Area() float32 {
	return r.length * r.width
}

func main() {
// 创建并初始化 Rectangle 和 Square 的实例,由于这两个实例都实现了接口中的方法,
//所以这两个实例,都可以赋值给接口 Shaper 
	r := &Rectangle{10, 2}
	q := &Square{10}

	// 创建一个 Shaper 类型的数组
	shapes := []Shaper{r, q}
	// 迭代数组上的每一个元素并调用 Area() 方法
	for n, _ := range shapes {
		fmt.Println("矩形数据: ", shapes[n])
		fmt.Println("它的面积是: ", shapes[n].Area())
	}
}

/*
矩形数据:  &{10 2}
它的面积是:  20
图形数据:  &{10}
它的面积是:  100
*/
package main

import "fmt"

type act interface {
	write()
}

type xiaoming struct {
}

type xiaobai struct {
}

func (xm *xiaoming) write() {
	fmt.Println("xiaoming write")
}

func (xf *xiaobai) write() {
	fmt.Println("xiaobai write")
}

func main() {
/*
> 接口特点:
> + 接口只有方法声明、没有实现,没有数据字段
> + 接口可以匿名嵌入其它接口,或者嵌入到结构中

> 接口是用来定义行为的类型,这些被定义的行为不由接口直接实现,
> 而是由用户定义的类型实现,**一个实现了这些方法的具体类型是这个接口类型的实例。**

**如果用户定义的类型实现了某个接口类型声明的一组方法,
那么这个用户定义的类型的值就可以赋给这个接口类型的值。
这个赋值会把用户定义的类型存入接口类型的值。**
*/
	var w act

	xm := xiaoming{}
	xb := xiaobai{}

	w = &xm
	w.write()

	w = &xb
	w.write()
}

输出结果:

xiaoming write
xiaobai write

或者以下代码,将结构体初始化封装为函数。

// 创建初始化函数,初始化结构体对象,返回为接口对象
func NewXiaoming(xm xiaoming) act {
	return &xm
}

// 创建初始化函数,初始化结构体对象,返回为接口对象
func NewXiaobai(xb xiaobai) act {
	return &xb
}

func main() {
	m := NewXiaoming(xiaoming{})
	m.write()

	b := NewXiaobai(xiaobai{})
	b.write()
}

示例

Go 中的接口可以说是方法特征的集合表达。要实现其接口,只需要实现接口中的所有方法即可。

package main

import (
	"fmt"
)

type animal interface {
	run()
	breath()
}

type dog struct {
	legs int
	nose string
}

type fish struct {
	fin  string
	gill string
}

func (d dog) run() {
	fmt.Printf("Dog runs with %d legs\n", d.legs)
}

func (d dog) breath() {
	fmt.Printf("Dog breath with %s\n", d.nose)
}

func (f fish) run() {
	fmt.Printf("Fish runs with %s\n", f.fin)
}

func (f fish) breath() {
	fmt.Printf("Fisn breath with %s\n", f.gill)
}

func behavior(an animal) {
	an.run()
	an.breath()
}

func main() {
	d := dog{
		legs: 4,
		nose: "nose",
	}

	f := fish{
		fin:  "fin",
		gill: "gill",
	}

	behavior(d)
	behavior(f)
}

输出结果:

Dog runs with 4 legs
Dog breath with nose
Fish runs with fin
Fisn breath with gill

程序首先定义了一个 animal 接口,它有 run()breath() 两个方法。接着定义了两种类型,分别是 dogfish,显然这两种类型的动物都拥有 animal 动物类的两个方法,因而各自实现了它们。最后一个带有 animal 参数的 behavior 函数的出现很好地诠释了面向对象中多态的构造形式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值