golang中的面向对象

x33g5p2x  于2021-12-30 转载在 Go  
字(2.7k)|赞(0)|评价(0)|浏览(343)

声明方法

声明方法的语法和声明普通函数非常类似,只是在函数名字前面加上一个参数,这个参数把这个方法绑定到它对应的类型上。

func (e Employee) ToString() (description string) {
	return fmt.Sprintf("[%d, %s], from %s", e.ID, e.Name, e.Address)
}

注意这里的(e Employee),这里的Employee说明了这个ToString()方法属于Employee类型的方法,而e就类似其它语言中的thisself,只是golang没有使用这种特殊名称,而是常常用类型的第一个小写字母来表示。

在golang中,可以将一个方法绑定到任意的类型上(指针类型和接口类型除外),包括内置的基础类型,这也有些类似 C# 语言中的扩展方法。

继承

type Employee struct {
	ID            int
	Name, Address string
}

func (e Employee) ToString() (description string) {
	return fmt.Sprintf("[%d, %s], from %s", e.ID, e.Name, e.Address)
}

func (e Employee) SayHello() string {
	return fmt.Sprintf("hello, I'm %s", e.Name)
}
type EmployeeManager struct {
	Employee     // 匿名成员
	ManagerLevel int
}

func (e EmployeeManager) ToString() (description string) {
	return fmt.Sprintf("%s , level %d", e.Employee.ToString(), e.ManagerLevel)
}
var manager = EmployeeManager{
	Employee: Employee{
		ID:      2,
		Name:    "fooManager",
		Address: "beijing",
	},
	ManagerLevel: 4,
}

fmt.Println(manager.SayHello())

关于匿名成员,之前的文章已经讨论过了,是用来实现继承的有效方式,可阅读 golang中的struct。从上面的代码可以看出来,匿名成员的方法也是可以直接使用的。在运行时,SayHello方法接收到的实参是 manager.Employee,而不是manager

而这里面的两个类型都包含了ToString方法,但它们并没有什么关系,因为它们的类型分别是Employee.ToStringEmployeeManager.ToString

从这里可以看出,编译器在在里面做了很多工作,导致在运行时分得特别清。到目前为止我们还没有看到golang中的多态成分。

::由于golang可以以匿名成员的方式实现继承的效果,那就意味着它可以实现多继承的效果。::

方法接收者是指针类型的情况

方法的类型也可以声明为类型的指针形式,比如:

func (e *Employee) ToString() (description string) {
	return fmt.Sprintf("[%d, %s], from %s", e.ID, e.Name, e.Address)
}

结构体类型与指针类型,在调用时,编译器会给予强大的支持,比如,如果方法的接收者是枚举指针类型,而实际传递的实参是枚举类型,那么编译器会隐式地通过&操作将实参转换为指针进行传递,而如果你定义的接收者是枚举类型但实际调用时传递了个指针类型,编译器会隐式地通过*操作将指针指向的值进行传递。

但是,与普通函数一样,如果你定义的接收者是枚举类型,那么在实际调用时会采用值传递的方式,也就意味着如果你修改了成员的值,原来的参数并不会发生变化。

func (e *EmployeeManager) Promote() {
	e.ManagerLevel++
}

这里必须使用指针传递,否则成员的改动无效。

总结:指针类型或枚举类型,编译器都给予充分支持,怎么写都能行,但在运行时,指针类型会保留值的改动。

习惯:如果一个类型的任何一个方法使用了指针接受者,那么所有的方法都应该采用指针类型的接受者,即便有些只读的方法并不需要。

接收者的实参可以是nil

没错,接收者可以是nil,golang的机制会将nil传递给方法并运行,至于会不会引发宕机异常,要看你有没有引用nil的引用。

func (e *Employee) SayHello() string {
	if e == nil {
		return "hello, I'm nobody"
	}
	return fmt.Sprintf("hello, I'm %s", e.Name)
}

var nilEmployee *Employee
fmt.Println(nilEmployee.SayHello())	// 输出 hello, I'm nobody

方法变量和方法表达式

这部分的目的是将一个方法像函数那样来调用。

toString := employee.ToString		
fmt.Println(toString())				// func() string
fmt.Printf("%T\n", toString)

这里的toString是一个::方法变量::,也可以说是一个绑定了接收者的函数变量,这时它的类型等同于去掉接收者的函数类型 。

promote := (*EmployeeManager).Promote
fmt.Printf("%T\n", promote)				// func(*main.EmployeeManager)
promote(&manager)
fmt.Println(manager.ToString())

这里的promote是一个::方法表达式::,它是一个函数,这时它的类型等同于把接收者作为了函数的第一个参数。从理解上来讲,它更像一个普通函数。

注意,方法调用是变量.f(),方法变量是变量.f,方法表达式是T.f(*T).f

这两个机制就实现了从方法到函数的转换。

golang中的封装

golang只有一种控制可见性的机制,就是大写字母开头会导出,在包外可见,如果以小写字母开头,则在包内可见。

所以在golang中,对象的封装只能通过在结构体中定义小写字母开头的成员来实现,并且封装的单元是包,而不是类。

从封装的目的上看,封装主要是通过隐藏内部信息来降低外部使用它的复杂度,从这个意义上来讲,包内封装的必要性确实没那么大。

相关文章