6.方法(go语言学习笔记)

6.方法(go语言学习笔记)


目录

  1. 定义
  2. 匿名字段
  3. 方法集
  4. 表达式

1. 定义

  1. 方法是与对象实例绑定的特殊函数。
  2. 方法是面向对象编程的基本概念,用于维护和展示对象的自身状态。对象是内敛的,每个实例对象都有各自不同的独立特征,以属性和方法来暴露对外通信接口。
  3. 普通函数则专注于算法流程,通过接收参数来完成特定逻辑运算,并返回最终结果。
  4. 换句话说,方法是有关联状态的,而函数通常没有。
  5. 方法和函数定义的语法区别在于前者有前置实例接收参数,编译器以此确定方法所属类型。在某些语言中,尽管没有显示定义,但会在调用时隐式传递this实例参数。
  6. 可以为当前包,以及除接口和指针以外的任何类型定义方法。
type N int

func (n N) toString() string {
    
    
	return fmt.Sprintf("%#x", n)
}

func main() {
    
    
	var a N = 25
	println(a.toString())
}
  1. 方法同样不支持重载。接收参数名没有限制,按照惯例会选用简短有意义的名称(不推荐用this,self)。如方法内部并不引用实例,可省略参数名,仅保留类型。
type N int

func (N) test() {
    
    
	println("hi!")
}
  1. 方法可看作特殊的函数,那么receiver的类型自然可以是基础类型或指针类型。这会关系到调用时对象实例是否被复制。
type N int

func (n N) value() {
    
    
	n++
	fmt.Printf("v:%p,%v\n", &n, n)

}

func (n *N) pointer() {
    
    
	(*n)++
	fmt.Printf("v:%p,%v\n", n, *n)
}

func main() {
    
    
	var a N = 25

	a.value()
	fmt.Printf("a: %p,%v\n",&a,a)
	a.pointer()
	fmt.Printf("a: %p,%v\n",&a,a)
}
  1. 输出
v: 0xc000018090,26
a: 0xc000018088,25
v: 0xc000018088,26
a: 0xc000018088,26

  1. 可使用实例值或指针调用方法,编译器会根据方法receiver类型自动在基础类型和指针类型间转换。
func main() {
    
    
	var a N = 25
	p := &a

	a.value() //26,但a的值没变
	p.pointer() // a=26,p是a的地址

	p.value()   //27, 但a的值还是26
	p.pointer() //a=27
}
  1. 输出
v: 0xc0000a4010,26
v: 0xc0000a4008,26
v: 0xc0000a4040,27
v: 0xc0000a4008,27
  1. 不能用多级纸质调用方法。
    在这里插入图片描述
  2. 指针类型的receiver必须是合法指针(包括nil),或能获取示例地址。
  3. 如何选择方法的receiver类型?
    1. 要修改实例状态,用 *T
    2. 无需修改状态的小对象或固定值,建议用 T
    3. 大对象建议用 *T,以减少复制成本。
    4. 引用类型、字符串、函数等指针包装对象,直接用T
    5. 若包含 Mutex 等同步字段,用 *T,避免因复制造成锁操作无效
    6. 其他无法确定的情况,都用 *T

2. 匿名字段

  1. 可以像访问匿名字段成员那样调用其方法,由编译器负责查找。
type data struct {
    
    
	sync.Mutex
	buf [1024]byte
}

func main() {
    
    
	d := data{
    
    }
	d.Lock() //编译器会处理为 sync.(*Mutex).Lock() 调用
	defer d.Unlock()
}
  1. 方法也有同名隐蔽问题,但利用这种特性,可以实现类似覆盖(override)操作。
type user struct{
    
    }

type manager struct {
    
    
	user
}

func (user) toString() string {
    
    
	return "user"
}

func (m manager) toString() string  {
    
    
	return m.user.toString()+"; manager"
}

func main() {
    
    
	var m manager
	println(m.toString())
	println(m.user.toString())
}
  1. 输出
user; manager
user

3. 方法集

  1. 类型有一个与之相关的方法集(method set),这决定了它是否实现了某个接口
    1. 类型T方法集包含所有receiver T方法
    2. 类型 *T 方法集包含所有 receiver T+ *T方法
    3. 匿名嵌入S,T方法集包含所有receiver S方法
    4. 匿名嵌入 *S,T方法集包含所有receiver S+*S 方法。
    5. 匿名嵌入 S 或 *S,*T方法集包含所有 receiver S+ *S 方法。
  2. 方法集仅影响接口实现和方法表达式转换,与通过实例或实例指针调用方法无关。实例并不使用方法集,而是直接调用。
  3. 很显然,匿名字段是为方法集准备的。否则,完全没必要为少写个字段名而大费周章。
  4. 面向对象的三大特征:封装,继承,多态。Go仅实现了部分特征,更倾向于”组合优于继承“思想。
  5. 将模块分解成互相独立的更小单元,分别处理不同方面的需求,最后以匿名嵌入方式组合在一起,共同实现对外接口。
  6. 其简短一致的调用方式,更是隐藏了内部实现细节。

4. 表达式

  1. 方法和函数一样,除直接调用,还可以赋值给变量,作为参数传递。依照具体引用方式的不同,可分为expression和value两种状态

1. Method Expression

  1. 通过类型引用的 method expression 会被还原为普通函数样式,receiver是第一参数,调用时必须显式传参。至于类型,可以是T 或者 *T,只要目标方法存在于改类型方法集中即可。
type N int

func (n N) test() {
    
    
	fmt.Printf("test.n: %p,%d\n", &n, n)
}

func main() {
    
    
	var n N = 25
	fmt.Printf("main.n: %p, %d\n",&n,n)

	f1 := N.test  // func(n N)
	f1(n)
	
	f2 := (*N).test  // func(n *N)
	f2(&n)  		//按方法集中的签名传递正确类型的参数
}
  1. 输出
main.n:0xc000018088, 25
test.n: 0xc0000180a0,25
test.n: 0xc0000180b0,25

  1. 尽管*N方法集包装的test方法receiver类型不同,但编译器会保证按原定义类型拷贝传值。
  2. 当然,也可直接表达式方法调用。
func main() {
    
    
	var n N = 25
	N.test(n)
	(*N).test(&n)
}

2. Method Value

  1. 基于实例或指针引用的method value,参数前面不会改变,依旧按正常方式调用。
  2. 但当method value被赋值给变量或者参数传递时,会立即计算并复制该方法执行所需的receiver对象,与其绑定,以便在稍后执行时,能隐式传入receiver参数。
func main() {
    
    
	var n N = 100
	p := &n

	n++
	f1 := n.test

	n++
	f2 := p.test

	n++
	fmt.Printf("main.n:%p, %v\n",p,n)

	f1()
	f2()
}
  1. 输出
main.n:0xc000096008, 103
test.n: 0xc000096020,101
test.n: 0xc000096030,102
  1. 编译器会为method value生成一个包装函数,实现间接调用。至于receiver复制,和闭包的实现方法基本相同,打包成funcval,经由DX寄存器传递。
  2. 当然,如果目标方法的receiver是指针类型,那么被复制的仅是指针。
type N int

func (n *N) test() {
    
    
	fmt.Printf("test.n: %p,%d\n", n, *n)
}

func main() {
    
    
	var n N = 100
	p := &n

	n++
	f1 := n.test

	n++
	f2 := p.test

	n++
	fmt.Printf("main.n:%p, %v\n",p,n)

	f1()
	f2()
}
  1. 输出
main.n:0xc000096008, 103
test.n: 0xc000096008,103
test.n: 0xc000096008,103
  1. 只要receiver参数类型正确,使用nil同样可以执行。

猜你喜欢

转载自blog.csdn.net/weixin_41910694/article/details/111356464