基础技能树-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)

方法调用变成了很普通的函数内联。所以方法是站在语言层面的东西,在汇编层面上不存在方法一说。区别在于方法是一种特殊的函数,编译器必须帮你插入对象实例的引用,这个传参过程是由编译器帮你完成。

所以方法并没有你想象的那么复杂,方法可以看成一种很特殊的函数语法糖。

匿名字段方法,是继承调用?还是语法糖?[付费阅读]

匿名字段方法调用

很普通调用没有什么区别,因为一旦语法糖被还原了以后就变成了普通的调用。

版权声明:本文为lyj原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:http://www.cnblogs.com/lyj/p/foundation_25_method.html