《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, n)
}

func (n *N) pointer() { // func pointer(n *N)
	(*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)
}

输出:

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

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

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

四、表达式

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

Method Expression

通过类型引用的 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)				// 按方法集中的签名传递正确类型的参数
}

输出:

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, 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()
}

输出:

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, 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)
}

输出:

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, *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\n", 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
}
时间: 2024-08-12 22:09:00

《Go学习笔记 . 雨痕》方法的相关文章

《Go学习笔记 . 雨痕》类型

一.基本类型 清晰完备的预定义基础类型,使得开发跨平台应用时无须过多考虑符合和长度差异. 类型 长度 默认值 说明 bool 1 false   byte 1 0 uint8 int, uint 4, 8 0 默认整数类型,依据目标平台,32 或 64 位 int8, uint8 1 0 -128 ~ 127,0 ~ 125 int16, uint16 2 0 -32,768 ~ 32,767,0 ~ 65,535 int32, uint32 4 0 -21亿 ~ 21亿,0 ~ 42亿 int

大龙的学习笔记之“虚方法,抽象方法,重写,抽象类,接口”

虚方法:可以重写,也可以不重写,英文名:virtual 抽象方法:必须重写,必须放在抽象类中,英文名:abstract 重写:可以重写三种方法:virtual,abstract,override,英文名:override 抽象类:可以有抽象方法,也可以有非抽象方法,英文名:abstract 接口:和抽象类相似,但是里面只有未实现的方法,英文名:interface 大龙的学习笔记之"虚方法,抽象方法,重写,抽象类,接口",布布扣,bubuko.com

2015 IOS 学习笔记 面向对象 初始化方法 ——蓝懿教育

今天学习了面向对象以及初始化方法,这个在实际应用中比较重要,也比较抽象,所以要具体在实例中才能理解. ————————面向对象有三大特性—————— 一.封装 封装是对象和类概念的主要特性.它是隐藏内部实现,稳定外部接口,可以看作是“包装”.封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏. 好处:使用更简单变量更安全可以隐藏内部实现细节开发速度加快 OC中一个类可以继承另一个类,被继承的类成为超类(superclass),继承的

2015 IOS 学习笔记 for循环 方法练习 ——蓝懿教育

今天学习了for循环以及方法,内容有些复杂,不过万变不离其宗 在以后的学习中容易掌握,因为涉及范围广阔,使用率高.所以多加练习后会掌握. ————————for循环—————————— for循环概念: for循环编程语言中的语句之一,用于循环执行.for循环是开界的,它的一般形式为: for(; <<span se-mark="1">条件表达式>; ) 语句: 初始化总是一个赋值语句, 它用来给循环控制变量赋初值: 条件表达式是一个关系表达式, 它决定什么时候

lodash学习笔记之Array方法

今天周末在家无聊学习一下lodash. lodash目前的中文资料很少.而且api好像还被墙了.下面说一下lodash的arrary相关的方法. 1. chunk   英 [t???k]    顾名思义,是对数组进行分块的方法 n. 大块:矮胖的人或物 用法: _.chunk(array,number)  根据number对array进行均等的分块,如果array不能被number平分,则会留下一个余下的块. _.chunk(['a','b','c','d'],-1); //当 size<=1的

机器学习-斯坦福:学习笔记4-牛顿方法

牛顿方法 本次课程大纲: 1.  牛顿方法:对Logistic模型进行拟合 2. 指数分布族 3.  广义线性模型(GLM):联系Logistic回归和最小二乘模型 复习: Logistic回归:分类算法 假设给定x以为参数的y=1和y=0的概率: 求对数似然性: 对其求偏导数,应用梯度上升方法,求得: 本次课程介绍的牛顿方法是一种比梯度上升快很多的方法,用于拟合Logistic回归 1. 牛顿方法 假设有函数,需要找使=0的 步骤: 1)       给出一个的初始值 2)       对求导

iOS学习笔记6-GET POST方法

GET 在请求URL后面以?的形式跟上发给服务器的参数,多个参数之间用&隔开,比如http://ww.test.com/login?username=123&pwd=234&type=JSON 注意:由于浏览器和服务器对URL长度有限制,因此在URL后面附带的参数是有限制的,通常不能超过1KB POST 发给服务器的参数全部放在请求体中 理论上,POST传递的数据量没有限制(具体还得看服务器的处理能力) 选择GET和POST的建议 (1)如果要传递大量数据,比如文件上传,只能用PO

PHP学习笔记——10.魔术方法

1.定义 是指在某些特定情况下会自动调用的方法 PHP提供了多个魔术方法,都是以__开头的 2.构造方法 __construct 当实例化对象时,会自动调用该方法 可用于在实例化对象时传参,以控制类的属性 class 类名 { private 属性名 = null; public function __construct ($形参) { $this -> 属性名 = $形参; } } $对象 = new 类名('实参'); echo $对象 -> 属性名; 3.析构方法 __destruct

VBA学习笔记之粘贴方法

Option Explicit 'Worksheet.Paste 方法 '将"剪贴板"中的内容粘贴到工作表上. '表达式.Paste(Destination, Link) '表达式 一个代表 Worksheet 对象的变量. Sub 粘贴() Range("B1:B6").Copy Range("c9") '这一句等于下面两句,但下面两句也有用处 Range("B1:B6").Copy '复制区域无公式 Sheet1.Past