• 周二. 12 月 10th, 2024

    GO语言方法

    root

    10 月 14, 2019 #Golang, #方法

    6.1 定义

    方法是与对象实例绑定的特殊函数
    方法和函数定义语法区别的在于前者有前置实例接收参数(receiver),编译器以此确定方法所属类型。
    可以为当前包,以及除接口和指针以外的任何类型定义方法
    type N int
    func (n N) toString() string {
    	return fmt.Sprintf("%#x", n)
    }
    func main() {
    	var a N = 25
    	println(a.toString())
    }
    
    
    方法不支持重载。
    receiver参数名没有限制,按惯例选用简短有意义的名称(不推荐this,self).
    如方法内部不引用实例,可省略参数名,仅保留类型
    type N int
    func (N) test() {
        println("hi!")
    }
    
    方法可看作特殊的函数,那么receiver的类型自然可以是基础类型或指针类型。这会关系到调用时对象实例是否被复制
    type N int
    func (n N) value() {
    	n++
    	fmt.Printf("v:%p, %v\n", &n, n)
    }
    func (n *N) pointer() {
    	(*n)++
    	fmt.Printf("p:%p, %v\n", n, *n)
    }
    func main() {
    	var a N = 25
    	a.value()
    	a.pointer()
    	fmt.Printf("a:%p, %v\n", &a, a)
    }
    
    可使用实例值或指针调用方法,编译器会根据方法receiver类型自动在基础类型和指针类型间转换, 不能用多级指针调用方法,指针类型的receiver必须是合法指针(包括nil),或能获取实例地址.
    如何选择方法的receiver类型
    • 要修改实例状态, 用*T
    • 无须修改状态的小对象或固定值,建议用T
    • 大对象建议用*T,以减少复制成本
    • 引用类型、字符串、函数等指针包装对象,直接用T
    • 若包含Mutex等同步字段,用*T, 避免因复制造成锁操作无效
    • 其他无法确定的情况,都用*T.

    6.2 匿名字段

    可以像访问匿名字段成员那样调用其方法,由编译器负责查找
    type data struct {
    	sync.Mutex
    	buf [1024]byte
    }
    
    func main() {
    	d := data{}
    	d.Lock()	//编译会处理为sync.(*Mutex).Lock()调用
    	defer d.Unlock()
    }
    
    方法也会有同名遮蔽问题。但利用这种特性,可实现类似覆盖操作
    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())
    }
    
    尽管能直接访问匿名字段的成员和方法,但它们依然不属于继承关系。

    6.3 方法集

    类型有一个与之相关的方法集,这决定了它是否实现某个接口。
    • 类型T方法集包含所有receiver T方法
    • 类型*T方法集包含所有receiver T + *T 方法
    • 匿名嵌入S, T方法集包含所有receiver S方法
    • 匿名嵌入*S, T方法集包含所有receiver S + *S方法
    • 匿名嵌入S 或*S, *T方法集包含所有receiver S + *S方法。

    6.4 表达式

    通过类型引用的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
    	f1(n)
    
    	f2 := (*N).test
    	f2(&n)
    }
    
    尽管*N方法集包装的test方法receiver类型不同,但编译器会保证原定义类型拷贝传值。当然可直接以表达式方式调用
    func main() {
        var n N = 25
        N.test(n)
        (*N).test(&n)
    }
    
    基于实例或指针引用的method value,参数签名不会改变,依旧按正常方式调用。
    当method value被赋值给变量或作为参数传递时,会立即计算并复制该方法执行所需的receiver对象,与其绑定,以便在稍后执行时,能隐式传入receiver参数
    type N int
    func (n N) test() {
    	fmt.Printf("test.n:%p, %v\n", &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\n", p, n)
    
    	f1()
    	f2()
    }
    
    编译器会为method value生万一个包装函数,实现间接调用。至于receiver复制,和闭包的实现方法基本相同,打包成funcval,经由DX寄存器传递
    当method value作为参数时, 会复制含reeiver在内的整个method value
    type N int
    func (n N) test() {
    	fmt.Printf("test.n:%p, %v\n", &n, n)
    }
    func call(m func()) {
    	m()
    }
    func main() {
    	var n N = 100
    	p := &n
    	fmt.Printf("main.n:%p, %v\n", p, n)
    
    	n++
    	call(n.test)
    
    	n++
    	call(p.test)
    }
    
    如果目标方法的receiver是指针类型,那么被复制的仅是指针
    type N int
    func (n *N) test() {
    	fmt.Printf("test.n: %p, %v\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()		//延迟调用 n==103
    	f2()
    }
    
    只要receiver参数类型正确,使用nil同样可以执行

    root

    发表回复