深入理解 Scala 中的闭包(Closures)

摘要

本文通过 Scala 语言来实现一个简单的闭包,并且通过 Opcode 来深入理解 Scala 中闭包的实现原理。

一个简单的例子

闭包,简单的理解就是:函数内部的变量不在其作用于时,仍然可以从外部进行访问,听上去有些抽象;
下面我们来通过一个简单的例子实现 Scala 中的闭包,代码如下:

object Closures {
  
  def main(args: Array[String]): Unit = {
    val addOne = makeAdd(1)
    val addTwo = makeAdd(2)
    
    println(addOne(1))
    println(addTwo(1))
  }
  
  def makeAdd(more: Int) = (x: Int) => x + more
  
  def normalAdd(a: Int, b: Int) = a + b
  
}

我们定义了一个函数 makeAdd,输入参数是 Int 类型,返回的是一个函数(其实可以看成函数,后面我们会深入去研究到底是什么),同样我们定义了一个普通的函数 normalAdd 来进行比较,main 方法中,首先我们通过调用 makeAdd 来定义了两个 val:addOne 和 addTwo 并分别传入 1 和 2,然后执行并打印 addOne(1) 和 addTwo(2),运行的结果是 2 和 3。

分析

接下来我们来详细的分析一下上面这个例子的 Opcode,通过 javap 命令来查看 Closures.class 的字节码:

  Last modified Feb 14, 2017; size 1311 bytes
  MD5 checksum 02722b1fa1195c63a6dd0c8615db8c26
  Compiled from "Closures.scala"
public final class com.learn.scala.Closures$
  minor version: 0
  major version: 50
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
   #1 = Utf8               com/learn/scala/Closures$
   #2 = Class              #1             // com/learn/scala/Closures$
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               Closures.scala
   #6 = Utf8               MODULE$
   #7 = Utf8               Lcom/learn/scala/Closures$;
   #8 = Utf8               
   #9 = Utf8               ()V
  #10 = Utf8               
  #11 = NameAndType        #10:#9         // "":()V
  #12 = Methodref          #2.#11         // com/learn/scala/Closures$."":()V
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               makeAdd
  #16 = Utf8               (I)Lscala/Function1;
  #17 = NameAndType        #15:#16        // makeAdd:(I)Lscala/Function1;
  #18 = Methodref          #2.#17         // com/learn/scala/Closures$.makeAdd:(I)Lscala/Function1;
  #19 = Utf8               scala/Predef$
  #20 = Class              #19            // scala/Predef$
  #21 = Utf8               Lscala/Predef$;
  #22 = NameAndType        #6:#21         // MODULE$:Lscala/Predef$;
  #23 = Fieldref           #20.#22        // scala/Predef$.MODULE$:Lscala/Predef$;
  #24 = Utf8               scala/Function1
  #25 = Class              #24            // scala/Function1
  #26 = Utf8               apply$mcII$sp
  #27 = Utf8               (I)I
  #28 = NameAndType        #26:#27        // apply$mcII$sp:(I)I
  #29 = InterfaceMethodref #25.#28        // scala/Function1.apply$mcII$sp:(I)I
  #30 = Utf8               scala/runtime/BoxesRunTime
  #31 = Class              #30            // scala/runtime/BoxesRunTime
  #32 = Utf8               boxToInteger
  #33 = Utf8               (I)Ljava/lang/Integer;
  #34 = NameAndType        #32:#33        // boxToInteger:(I)Ljava/lang/Integer;
  #35 = Methodref          #31.#34        // scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
  #36 = Utf8               println
  #37 = Utf8               (Ljava/lang/Object;)V
  #38 = NameAndType        #36:#37        // println:(Ljava/lang/Object;)V
  #39 = Methodref          #20.#38        // scala/Predef$.println:(Ljava/lang/Object;)V
  #40 = Utf8               this
  #41 = Utf8               args
  #42 = Utf8               [Ljava/lang/String;
  #43 = Utf8               addOne
  #44 = Utf8               Lscala/Function1;
  #45 = Utf8               addTwo
  #46 = Utf8               com/learn/scala/Closures$$anonfun$makeAdd$1
  #47 = Class              #46            // com/learn/scala/Closures$$anonfun$makeAdd$1
  #48 = Utf8               (I)V
  #49 = NameAndType        #10:#48        // "":(I)V
  #50 = Methodref          #47.#49        // com/learn/scala/Closures$$anonfun$makeAdd$1."":(I)V
  #51 = Utf8               more
  #52 = Utf8               I
  #53 = Utf8               normalAdd
  #54 = Utf8               (II)I
  #55 = Utf8               a
  #56 = Utf8               b
  #57 = Methodref          #4.#11         // java/lang/Object."":()V
  #58 = NameAndType        #6:#7          // MODULE$:Lcom/learn/scala/Closures$;
  #59 = Fieldref           #2.#58         // com/learn/scala/Closures$.MODULE$:Lcom/learn/scala/Closures$;
  #60 = Utf8               Code
  #61 = Utf8               LocalVariableTable
  #62 = Utf8               LineNumberTable
  #63 = Utf8               Signature
  #64 = Utf8               (I)Lscala/Function1;
  #65 = Utf8               SourceFile
  #66 = Utf8               InnerClasses
  #67 = Utf8               ScalaInlineInfo
  #68 = Utf8               Scala
{
  public static final com.learn.scala.Closures$ MODULE$;
    descriptor: Lcom/learn/scala/Closures$;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

  public static {};
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: new           #2                  // class com/learn/scala/Closures$
         3: invokespecial #12                 // Method "":()V
         6: return

  public void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: aload_0
         1: iconst_1
         2: invokevirtual #18                 // Method makeAdd:(I)Lscala/Function1;
         5: astore_2
         6: aload_0
         7: iconst_2
         8: invokevirtual #18                 // Method makeAdd:(I)Lscala/Function1;
        11: astore_3
        12: getstatic     #23                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        15: aload_2
        16: iconst_1
        17: invokeinterface #29,  2           // InterfaceMethod scala/Function1.apply$mcII$sp:(I)I
        22: invokestatic  #35                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
        25: invokevirtual #39                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
        28: getstatic     #23                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        31: aload_3
        32: iconst_1
        33: invokeinterface #29,  2           // InterfaceMethod scala/Function1.apply$mcII$sp:(I)I
        38: invokestatic  #35                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
        41: invokevirtual #39                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
        44: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      45     0  this   Lcom/learn/scala/Closures$;
            0      45     1  args   [Ljava/lang/String;
            6      38     2 addOne   Lscala/Function1;
           12      32     3 addTwo   Lscala/Function1;
      LineNumberTable:
        line 6: 0
        line 7: 6
        line 9: 12
        line 10: 28

  public scala.Function1 makeAdd(int);
    descriptor: (I)Lscala/Function1;
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: new           #47                 // class com/learn/scala/Closures$$anonfun$makeAdd$1
         3: dup
         4: iload_1
         5: invokespecial #50                 // Method com/learn/scala/Closures$$anonfun$makeAdd$1."":(I)V
         8: areturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/learn/scala/Closures$;
            0       9     1  more   I
      LineNumberTable:
        line 13: 0
    Signature: #64                          // (I)Lscala/Function1;

  public int normalAdd(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lcom/learn/scala/Closures$;
            0       4     1     a   I
            0       4     2     b   I
      LineNumberTable:
        line 15: 0
}
SourceFile: "Closures.scala"
InnerClasses:
     public final #47; //class com/learn/scala/Closures$$anonfun$makeAdd$1
Error: unknown attribute
  ScalaInlineInfo: length = 0x18
   01 01 00 04 00 0A 00 09 01 00 0D 00 0E 01 00 0F
   00 10 01 00 35 00 36 01
Error: unknown attribute
  Scala: length = 0x0

我们先来看一下 makeAdd 部分(为了方便理解,下文中的图中只是简单的表示,类似常量池的部分并没有在图中表现出来)
首先通过 new 实例化了一个 class com/learn/scala/Closures$$anonfun$makeAdd$1,图中的 Heap 中简单的表示为 makeAdd;

深入理解 Scala 中的闭包(Closures)_第1张图片
a1.jpeg

dup 命令就是复制一份上一步骤分配的空间的引用,并压入 operand stack 的栈底

深入理解 Scala 中的闭包(Closures)_第2张图片
a2.jpeg

iload_1 就是将 LocalVariableTable 中的 more 压入 operand stack

深入理解 Scala 中的闭包(Closures)_第3张图片
a3.jpeg

然后 invokespecial 将栈中的两个值 pop 出来执行 init 操作,也就是将 more 的具体值传入到 makeAdd 的初始化操作的函数中(可以认为是构造函数),然后将得到的新的实例化对象的引用压入 operand stack

深入理解 Scala 中的闭包(Closures)_第4张图片
a4.jpeg

最后使用 areturn 返回这个引用,即将 initedmakeAddRef pop 出来

由此可以看出,Scala 中实际上是在 Heap 中创建了一个 makeAdd 的实例化对象,所以 more 变量在下次调用的时候依然可以使用,而普通方法的局部变量在调用的时候是压入 Operand stack 中的,计算完成之后就会 pop 出,所以在函数的调用完成后就不能在访问这个变量,下面我们来通过 main 方法中的具体执行来验证此结论。

我们只看关键的部分:
iconst_1:将 1 压入 operand stack
invokevirtual #18 将执行上面我们分析过的 makeAdd 函数调用的部分,调用的时候将 1 作为参数传入进行初始化,并将最终得到的实例对象的引用压入 operand stack
astore_2 将上一步骤的引用 pop 出来并存储到 Local Variable Array 中
至此执行了 main 方法中的 val addOne = makeAdd(1)

7、8、11 重复执行了上面的步骤,即代码中的第二行:val addTwo = makeAdd(2)
此时,heap 有两个对象的实例,然后后面的 15、16、17、22、25 和 31、32、33、38、41 分别执行了println(addOne(1))println(addTwo(1))的部分,比较关键的部分是 invokeinterface 的时候执行了具体对象的 apply 方法,并将 1 作为参数传入,其它部分在此不再详细说明。

实际上我们可以在 bin 目录下看到一个名为Closures$$anonfun$makeAdd$1的类文件,我们用 javap 命令可以看到,实际上是调用了实例对象的 apply 方法:

public final int apply(int);
    descriptor: (I)I
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: invokevirtual #23                 // Method apply$mcII$sp:(I)I
         5: ireturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/learn/scala/Closures$$anonfun$makeAdd$1;
            0       6     1     x   I
      LineNumberTable:
        line 13: 0

至此,我们验证了我们的结论,Scala 实现闭包的方法是在 heap 中保存了使用不同参数初始化而产生的不同对象,对象中保存了变量的状态,然后调用具体对象的 apply 方法而最后产生不同的结果。

结论

与 Java 中使用内部类实现闭包相比,Scala 中为函数创建了一个对象 Function1 来保存变量的状态,然后具体执行的时候调用对应实例的 apply 方法,实现了函数作用域外也可以访问函数内部的变量。
希望本文对大家有所帮助,并欢迎大家批评指正文中有可能出现的纰漏。

你可能感兴趣的:(深入理解 Scala 中的闭包(Closures))