基础技能树-25 方法
基础技能树-25 方法
2017-11-23 13:34 by 李永京, … 阅读, … 评论, 收藏, 编辑
本节内容
- 基础技能树 系列文章导航
- 方法与函数的差异
- 方法调用方式,如何传递receiver、self、this
- 方法可被内联吗[付费阅读]
- 匿名字段方法,是继承调用?还是语法糖?[付费阅读]
- 匿名字段方法调用
方法与函数的差异
方法与函数的差异很简单,首先,他们都代表了一定的代码,通常情况下,函数并没有状态一说,它像一个工厂,把原材料输入进去,结果生成一个产品,执行完了就结束了。函数更多的是一种行为上的集合,可以理解为材料加工,代工行业。方法,首先有个对象实例的存在,对象实例实际上是个体、特征的一个集合,方法是围绕这个特征,要么用数据来驱动行为,比如你饿了需要找吃的,饿了代表抽象的数据;要么用行为改变数据。方法肯定是跟当前实例的状态捆绑在一起的,所以方法是具备状态的。方法的调用是有顺序的。方法要么是显示实例数据,要么是修改实例数据,它围绕着是这个实例。我们设计方法的时候,首先确定实例,比如设计方法叫吃饭,实例叫张三,命名不是叫张三吃饭,肯定设计为叫张三.吃饭,因为吃饭是和张三捆在一起的。方法有前缀的,有上下文的,有引用的。
函数通常我们认为最好是没有特征的,没有上下文状态的,执行完了就没了。因为我们知道函数的状态是存在栈桢上,同一个函数被两个线程调用,他们的状态是分离的,完全不一样的,各自持有。
实例有一个方法,同时线程1、线程2执行这个方法的时候,除了栈桢上的数据以外,还会影响实例的状态。很显然对象或者方法在并发情况下存在数据竞争的问题,因为我们不能保证线程1和线程2同时调用方法的时候对实例内部的数据做出什么样的修改。
所以当我们去做高并发算法时候,尽可能避免使用面向对象这种范例,因为这会涉及到状态的共享。像一些高并发算法,函数语言通常会两个函数,各自准备所有参数,线程1、线程2都复制一份,各自改变,执行完了结果进行合并,两个线程执行时绝对不共享同一个对象,所以面向对象对于高并发编程时候存在一些麻烦,通常会加锁,加锁会造成把并发变成了串行。在OOP编程领域当一个对象被复制了以后它就变成两个独立的实例了,OOP所有的方法围绕着单个实例进行。
方法调用方式,如何传递receiver、self、this
方法调用时候会隐性的传递当前实例的指针,不同的语言不同的做法,这东西究竟怎么传递的,很普通函数调用有什么区别?因为我们调用方法时候并没有传递这样的东西,在汇编层面究竟怎么传递的?
$ cat test.go
package main
type N int
func (n *N) Inc() {
*n++
}
func main() {
var n N = 0x100
n.Inc()
println(n)
}
简单的代码,创建了一个类型N,给这个类型N定义了一个Inc方法,在main函数中创建一个实例,调用了Inc方法
$ go build -gcflags "-N -l" -o test test.go #编译
$ gdb test
$ l
$ l
$ b 11
$ b 6
$ r
$ l
$ p/x &n #$1 = 0xc42003bf68 拿到n的信息f68
$ set disassembly-flavor intel
$ disass
Dump of assembler code for function main.main:
0x00000000004509f0 <+0>: mov rcx,QWORD PTR fs:0xfffffffffffffff8
0x00000000004509f9 <+9>: cmp rsp,QWORD PTR [rcx+0x10]
0x00000000004509fd <+13>: jbe 0x450a4b <main.main+91>
0x00000000004509ff <+15>: sub rsp,0x18 #main函数分配栈桢空间(三个8)
0x0000000000450a03 <+19>: mov QWORD PTR [rsp+0x10],rbp
0x0000000000450a08 <+24>: lea rbp,[rsp+0x10]
0x0000000000450a0d <+29>: mov QWORD PTR [rsp+0x8],0x100 #把n存入rsp+0x8,本地变量
0x0000000000450a16 <+38>: lea rax,[rsp+0x8] #把n的地址放到rax中
=> 0x0000000000450a1b <+43>: mov QWORD PTR [rsp],rax #rsp空间就是存放n的地址,其实就是编译器隐式的把this参数放在第一个参数位置
0x0000000000450a1f <+47>: call 0x4509b0 <main.(*N).Inc>
0x0000000000450a24 <+52>: call 0x423890 <runtime.printlock>
0x0000000000450a29 <+57>: mov rax,QWORD PTR [rsp+0x8]
0x0000000000450a2e <+62>: mov QWORD PTR [rsp],rax
0x0000000000450a32 <+66>: call 0x424070 <runtime.printint>
0x0000000000450a37 <+71>: call 0x423b40 <runtime.printnl>
0x0000000000450a3c <+76>: call 0x423920 <runtime.printunlock>
0x0000000000450a41 <+81>: mov rbp,QWORD PTR [rsp+0x10]
0x0000000000450a46 <+86>: add rsp,0x18
0x0000000000450a4a <+90>: ret
0x0000000000450a4b <+91>: call 0x448790 <runtime.morestack_noctxt>
0x0000000000450a50 <+96>: jmp 0x4509f0 <main.main>
End of assembler dump.
$ c #执行到inc方法内部
$ set disassembly-flavor intel #设置intel样式
$ disass
Dump of assembler code for function main.(*N).Inc:
0x00000000004509b0 <+0>: sub rsp,0x10 #inc方法分配栈桢空间
0x00000000004509b4 <+4>: mov QWORD PTR [rsp+0x8],rbp
0x00000000004509b9 <+9>: lea rbp,[rsp+0x8]
=> 0x00000000004509be <+14>: mov rax,QWORD PTR [rsp+0x18] #把N的指针放到rax中
0x00000000004509c3 <+19>: test BYTE PTR [rax],al
0x00000000004509c5 <+21>: mov rax,QWORD PTR [rax]
0x00000000004509c8 <+24>: mov QWORD PTR [rsp],rax
0x00000000004509cc <+28>: mov rcx,QWORD PTR [rsp+0x18]
0x00000000004509d1 <+33>: test BYTE PTR [rcx],al
0x00000000004509d3 <+35>: inc rax #调用方法n++
0x00000000004509d6 <+38>: mov QWORD PTR [rcx],rax
0x00000000004509d9 <+41>: mov rbp,QWORD PTR [rsp+0x8]
0x00000000004509de <+46>: add rsp,0x10
0x00000000004509e2 <+50>: ret
End of assembler dump.
很显然,我们可以看到我们调用n.Inc()
的时候,虽然我们没有在参数里面传递receiver、self、this引用,但是编译器实际上替我们完成了这样的操作,编译器会隐式的帮我们传递这样的参数。这就是在书上经常看到的当你调用一个方法的时候,编译器会隐式的帮你传递对象实例的引用。
$ cat test1.go
package main
type N int
func (n *N) Inc() {
*n++
}
func (n *N) Add(x int) {
*n += N(x)
}
func main() {
var n N = 0x100
n.Inc()
n.Add(0x200)
println(n)
}
增加一个参数,这时候编译器理论上需要传2个参数
$ go build -gcflags "-N -l" -o test test1.go #编译
$ gdb test
$ l
$ l
$ b 16
$ r
$ set disassembly-flavor intel
$ disass
Dump of assembler code for function main.main:
0x0000000000450a30 <+0>: mov rcx,QWORD PTR fs:0xfffffffffffffff8
0x0000000000450a39 <+9>: cmp rsp,QWORD PTR [rcx+0x10]
0x0000000000450a3d <+13>: jbe 0x450aa2 <main.main+114>
0x0000000000450a3f <+15>: sub rsp,0x20 #main函数分配栈桢空间(4个8)
0x0000000000450a43 <+19>: mov QWORD PTR [rsp+0x18],rbp
0x0000000000450a48 <+24>: lea rbp,[rsp+0x18]
0x0000000000450a4d <+29>: mov QWORD PTR [rsp+0x10],0x100 #把n存入rsp+0x10
0x0000000000450a56 <+38>: lea rax,[rsp+0x10]
0x0000000000450a5b <+43>: mov QWORD PTR [rsp],rax
0x0000000000450a5f <+47>: call 0x4509b0 <main.(*N).Inc>
0x0000000000450a64 <+52>: lea rax,[rsp+0x10] #把n的地址放到rax中
=> 0x0000000000450a69 <+57>: mov QWORD PTR [rsp],rax #rsp空间就是存放n的地址,其实就是编译器隐式的把this参数放在这个位置
0x0000000000450a6d <+61>: mov QWORD PTR [rsp+0x8],0x200 #把200存入rsp+0x8
0x0000000000450a76 <+70>: call 0x4509f0 <main.(*N).Add>
0x0000000000450a7b <+75>: call 0x423890 <runtime.printlock>
0x0000000000450a80 <+80>: mov rax,QWORD PTR [rsp+0x10]
0x0000000000450a85 <+85>: mov QWORD PTR [rsp],rax
0x0000000000450a89 <+89>: call 0x424070 <runtime.printint>
0x0000000000450a8e <+94>: call 0x423b40 <runtime.printnl>
0x0000000000450a93 <+99>: call 0x423920 <runtime.printunlock>
0x0000000000450a98 <+104>: mov rbp,QWORD PTR [rsp+0x18]
0x0000000000450a9d <+109>: add rsp,0x20
0x0000000000450aa1 <+113>: ret
0x0000000000450aa2 <+114>: call 0x448790 <runtime.morestack_noctxt>
0x0000000000450aa7 <+119>: jmp 0x450a30 <main.main>
End of assembler dump.
很显然,当我们调用n.Add(0x200)
的时候,实际上是有两个参数,第一个参数是对象的引用,可能是个复制品也可能是个指针。接下来会把后面的参数依次往后补上。这就告诉我们隐式传递怎么实现的。有没有注意到这种调用跟普通的函数没有什么区别,换句话说,可以还原成Add(n, 0x200)
。所以在汇编中,没有方法这么一说,方法是出现在你的语言层面。很显然调用方法时,除了显式参数以外,还需要隐式传递实例的指针。
方法可被内联吗[付费阅读]
$ go build -gcflags "-m" -o test test1.go
# command-line-arguments
./test.1.go:5:6: can inline (*N).Inc
./test.1.go:9:6: can inline (*N).Add
./test.1.go:13:6: can inline main
./test.1.go:15:7: inlining call to (*N).Inc
./test.1.go:16:7: inlining call to (*N).Add
./test.1.go:5:10: (*N).Inc n does not escape
./test.1.go:9:19: (*N).Add n does not escape
./test.1.go:15:7: main n does not escape
./test.1.go:16:7: main n does not escape
可以看到方法也是可以内联的,这个内联以后会是什么样子的?
$ go tool objdump -s "main\.main" test
TEXT main.main(SB) /root/lyj/test.1.go
test.1.go:13 0x4509b0 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX
test.1.go:13 0x4509b9 483b6110 CMPQ 0x10(CX), SP
test.1.go:13 0x4509bd 763e JBE 0x4509fd
test.1.go:13 0x4509bf 4883ec18 SUBQ $0x18, SP
test.1.go:13 0x4509c3 48896c2410 MOVQ BP, 0x10(SP)
test.1.go:13 0x4509c8 488d6c2410 LEAQ 0x10(SP), BP
test.1.go:10 0x4509cd 48c744240801030000 MOVQ $0x301, 0x8(SP)
test.1.go:17 0x4509d6 e8b52efdff CALL runtime.printlock(SB)
test.1.go:17 0x4509db 488b442408 MOVQ 0x8(SP), AX
test.1.go:17 0x4509e0 48890424 MOVQ AX, 0(SP)
test.1.go:17 0x4509e4 e88736fdff CALL runtime.printint(SB)
test.1.go:17 0x4509e9 e85231fdff CALL runtime.printnl(SB)
test.1.go:17 0x4509ee e82d2ffdff CALL runtime.printunlock(SB)
test.1.go:18 0x4509f3 488b6c2410 MOVQ 0x10(SP), BP
test.1.go:18 0x4509f8 4883c418 ADDQ $0x18, SP
test.1.go:18 0x4509fc c3 RET
test.1.go:13 0x4509fd e88e7dffff CALL runtime.morestack_noctxt(SB)
test.1.go:13 0x450a02 ebac JMP main.main(SB)
直接把计算结果算出来MOVQ $0x301, 0x8(SP)
。
方法调用变成了很普通的函数内联。所以方法是站在语言层面的东西,在汇编层面上不存在方法一说。区别在于方法是一种特殊的函数,编译器必须帮你插入对象实例的引用,这个传参过程是由编译器帮你完成。
所以方法并没有你想象的那么复杂,方法可以看成一种很特殊的函数语法糖。
匿名字段方法,是继承调用?还是语法糖?[付费阅读]
匿名字段方法调用
很普通调用没有什么区别,因为一旦语法糖被还原了以后就变成了普通的调用。