zoukankan      html  css  js  c++  java
  • 《Go学习笔记 . 雨痕》方法

    一、定义

    方法 是与对象实例绑定的特殊函数。

    方法 是面向对象编程的基本概念,用于维护和展示对象的自身状态。对象是内敛的,每个实例都有各自不同的独立特征,以 属性 和 方法 来暴露对外通信接口。普通函数则专注于算法流程,通过接收参数来完成特定逻辑运算,并返回最终结果。换句话说,方法是有关联状态的,而函数通常没有。

    方法 和 函数 定义语法区别的在于前者有 前置实例 接收参数(receiver),编译器以此确定方法所属类型。在某些语言里,尽管没有显示定义,但会在调用时隐式传递 this 实例参数。

    可以为 当前包,以及除 接口 和 指针 以外的任何类型定义方法。 

    type N int
    
    func (n N) toString() string {
    	return fmt.Sprintf("%#x", n)
    }
    
    func main()  {
    	var a N = 5
    	println(a.toString())
    }

    输出:

    0x19

    方法同样不支持重载(overload)。receiver 参数名没有限制,按惯例会选用简短有意义的名称(不推荐使用 this、self)。如果 方法内部并不引用实例,可省略参数名,仅保留类型。

    type N int
    
    func (N) test() {
    	println("hi!")
    }

    方法 可看作特殊的函数,那么 receiver 的类型自然可以是 基础类型指针类型。这会关系到调用时对象实例是否被复制。

    type N int
    
    func (n N) value() { // func value(n N)
    	n++
    	fmt.Printf("v: %p, %v
    ", &n, n)
    }
    
    func (n *N) pointer() { // func pointer(n *N)
    	(*n)++
    	fmt.Printf("p: %p, %v
    ", n, *n)
    }
    
    func main()  {
    	var a N = 25
    
    	a.value()
    	a.pointer()
    
    	fmt.Printf("a: %p, %v
    ", &a, a)
    }

    输出:

    v: 0xc42000a290, 26  // receiver 被复制
    p: 0xc42000a268, 26
    a: 0xc42000a268, 26

    可使用 实例值指针 调用方法,编译器会根据方法 receiver 类型自动在 基础类型 和 指针类型 间转换。

    func main()  {
    	var a N = 25
    	p := &a
    
    	a.value()
    	a.pointer()
    
    	p.value()
    	p.pointer()
    }

    输出:

    v: 0xc42000a290, 26
    p: 0xc42000a268, 26
    
    v: 0xc42000a2c0, 27
    p: 0xc42000a268, 27

    不能用多级指针调用方法。

    func main()  {
    	var a N = 25
    
    	p := &a
    	p2 := &p
    
    	p2.value()		// 错误:calling method value with receiver p2 (type **N)
    					// requires explicit dereference
    
    	p2.pointer()	// 错误:calling method pointer with receiver p2 (type **N)
    					// requires explicit dereference
    }

    指针类型的 receiver 必须是合法指针(包括 nil),或能获取实例地址。

    type X struct {}
    
    func (x *X) test() {
    	println("hi!", x)
    }
    
    func main()  {
    	var a *X
    	a.test() // 相当于 test(nil)
    
    	X{}.test() // 错误:cannot take the address of X literal
    }

    将方法看作普通函数,就很容易理解 receiver 的传参方式。

    如何选择方法的 receiver 类型?

    • 要修改实例状态,用 *T;
    • 无须修改状态的 小对象固定值,建议用 T;
    • 大对象建议用 *T,以减少复制成本;
    • 引用类型、字符串、函数 等指针包装对象,直接用 T;
    • 若包含 Mutex 等同步字段,用 *T,避免因复制造成锁操作无效;
    • 其他无法确定的情况,都用 *T;

    二、匿名字段

    可以像访问匿名字段成员那样调用方法,由编译器负责查找。

    type data struct {
    	sync.Mutex
    	buf [1024]byte
    }
    
    func main()  {
    	d := data{}
    	d.Lock() // 编译器会处理为 sync.(*Mutex).Lock() 调用
    	defer d.Unlock()
    }

    方法也会有同名遮蔽问题。但利用这种特性,可实现类似覆盖(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())
    }

    输出:

    user; manager
    user

    尽管能直接访问匿名字段的 成员 和 方法,但它们依然不属于继承关系。

    三、方法集

    类型有一个与之相关的方法集(method set),这决定了它是否实现某个接口。

    • 类型 T 方法集 包含所有 receiver T 方法;
    • 类型 *T 方法集 包含所有 receiver T + *T 方法;
    • 匿名嵌入 S,T 方法集 包含所有 receiver S 方法;
    • 匿名嵌入 *S,T 方法集 包含所有 receiver S + *S 方法;
    • 匿名嵌入 S 或 *S,*T 方法集 包含所有 receiver S + *S 方法;

    可利用反射(reflect)测试这些规则。

    type S struct {}
    
    type T struct {
    	S // 匿名嵌入字段
    }
    
    func (S) SVal() {}
    func (*S) SPtr() {}
    func (T) TVal() {}
    func (*T) TPtr() {}
    
    // 显示方法集里所有方法名字
    func methodSet(a interface{})  {
    	t := reflect.TypeOf(a)
    
    	for i, n := 0, t.NumMethod(); i < n; i++ {
    		m := t.Method(i)
    		fmt.Println(m.Name, m.Type)
    	}
    }
    
    func main()  {
    	var t T
    
    	methodSet(t)              // 显示 T 方法集
    	println("----------")
    	methodSet(&t)             // 显示 *T 方法集
    }
    SVal func(main.T)
    TVal func(main.T)
    ----------
    SPtr func(*main.T)
    SVal func(*main.T)
    TPtr func(*main.T)
    TVal func(*main.T)

    输出结果符合预期,但我们也注意到某些方法的 receiver 类型发生了改变。真实情况是,这些都是由编译器按方法集所需自动生成的额外包装方法。

    $ nm test | grep "main."
    ...

    方法集 仅影响 接口实现 和 方法表达式转换,与通过 实例实例指针 调用方法无关。实例并不使用方法集,而是直接调用(或通过隐式字段名)。

    很显然,匿名字段就是为方法集准备的。否则,完全没必要为少写个字段名而大费周章。

    面向对象的三大特征“封装”、“继承”和“多态”,Go 仅实现了部分特征,它更倾向于“组合优先于继承”这种思想。将模块分解成相互独立的更小单元,分别处理不同方面的需求,最后以匿名嵌入方式组合到一起,共同实现对外接口。而且其简短一致的调用方式,更是隐藏了内部实现细节。

    组合没有父子依赖,不会破坏封装。且整体和布局松耦合,可任意增加来实现扩展。各单元持有单一职责,互无关联,实现和维护更加简单。

    尽管接口也是多态的一种实现形式,但我认为应该和基于继承体系的多态分离开来。

    四、表达式

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

    Method Expression

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

    type N int
    
    func (n N) test() {
    	fmt.Printf("test.n: %p, %d
    ", &n, n)
    }
    
    func main()  {
    	var n N = 25
    	fmt.Printf("main.n: %p, %d
    ", &n, n)
    
    	f1 := N.test		// func(n N)
    	f1(n)
    
    	f2 := (*N).test		// func(n *N)
    	f2(&n)				// 按方法集中的签名传递正确类型的参数
    }

    输出:

    main.n: 0xc42008c030, 25
    test.n: 0xc42008c048, 25
    test.n: 0xc42008c058, 25

    尽管 *N 方法集包装的 test() 方法 receiver 类型不同,但编译器会保证按原定义类型拷贝传值。

    当然,也可直接以表达式方式调用。

    Method Value

    基于 实例 或 指针引用 的 method value,参数签名不会改变,依旧按正常方式调用。但当 method value 被赋值给变量或作为参数传递时,会立即计算并复制该方法执行所需的 receiver 对象,与其绑定,以便在稍后执行时,能隐式传入 receiver 参数。

    type N int
    
    func (n N) test() {
    	fmt.Printf("test.n: %p, %v
    ", &n, n)
    }
    
    func main()  {
    	var n N = 100
    	p := &n
    
    	n++
    	f1 := n.test // 因为 test 方法的 receiver 是 N 类型,所以复制 n,等于 101
    
    	n++
    	f2 := p.test // 复制 *p,等于 102
    
    	n++
    	fmt.Printf("main.n: %p, %v
    ", p, n)
    
    	f1()
    	f2()
    }

    输出:

    main.n: 0xc42000a268, 103
    test.n: 0xc42000a2a0, 101
    test.n: 0xc42000a2b0, 102

    编译器会为 method value 生成一个包装函数,实现间接调用。至于 receiver 复制,和闭包的实现方法基本相同,打包成 funcval,经由 DX 寄存器传递。

    当 method value 作为参数时,会复制含 receiver 在内的整个 method value。

    type N int
    
    func (n N) test()  {
    	fmt.Printf("test.n: %p, %v
    ", &n, n)
    }
    
    func call(m func())  {
    	m()
    }
    
    func main() {
    	var n N = 100
    	p := &n
    
    	fmt.Printf("main.n: %p, %v
    ", p, n)
    
    	n++
    	call(n.test)
    
    	n++
    	call(p.test)
    }

    输出:

    main.n: 0xc420072188, 100
    test.n: 0xc4200721c0, 101
    test.n: 0xc4200721d0, 102

    当然,如果目标方法的 receiver 是指针类型,那么被复制的仅是指针(注:指针值,及指针指向的内容没有变!)。

    type N int
    
    func (n *N) test()  {
    	fmt.Printf("test.n: %p, %v
    ", n, *n)
    }
    
    func main() {
    	var n N = 100
    	p := &n
    
    	n++
    	f1 := n.test // 因为 test 方法的 receiver 是 *N 类型,所以复制 &n
    
    	n++
    	f2 := p.test // 复制 p 指针
    
    	n++
    	fmt.Printf("main.n: %p, %v
    ", p, n)
    
    	f1() // 延迟调用,n == 103
    	f2()
    }

    输出:

    main.n: 0xc420072188, 103
    test.n: 0xc420072188, 103
    test.n: 0xc420072188, 103

    只要 receiver 参数类型正确,使用 nil 同样可以执行。

    type N int
    
    func (N) value() {}
    func (*N) pointer() {}
    
    func main()  {
    	var p *N
    
    	p.pointer()          // method value
    	(*N)(nil).pointer()  // method value
    	(*N).pointer(nil)    // method expression
    
    	//p.value()          // 报错:panic: runtime error: invalid memory address or nil pointer dereference
    }
    
  • 相关阅读:
    Linux基本网络设置(IP配置等,网卡驱动缓存,网卡中断)
    posix多线程有感线程高级编程(进程的优先级)
    posix多线程有感线程高级编程(均衡负载CPU绑定)
    posix多线程有感线程高级编程(线程调度以及优先级设置)
    linux多线程和锁
    posix多线程有感—sysconf系统变量
    Linux环境下获取网卡连接状态
    posix多线程有感线程高级编程(线程和fork,exec)
    posix多线程有感线程高级编程(线程堆栈)
    Day64:codeblocks安装 for Mac
  • 原文地址:https://www.cnblogs.com/52php/p/6347375.html
Copyright © 2011-2022 走看看