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

摘要

本文通過 Scala 語言來實(shí)現(xiàn)一個簡單的閉包紊遵,并且通過 Opcode 來深入理解 Scala 中閉包的實(shí)現(xiàn)原理。

一個簡單的例子

閉包,簡單的理解就是:函數(shù)內(nèi)部的變量不在其作用于時,仍然可以從外部進(jìn)行訪問痢艺,聽上去有些抽象仓洼;
下面我們來通過一個簡單的例子實(shí)現(xiàn) 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
  
}

我們定義了一個函數(shù) makeAdd堤舒,輸入?yún)?shù)是 Int 類型色建,返回的是一個函數(shù)(其實(shí)可以看成函數(shù),后面我們會深入去研究到底是什么)舌缤,同樣我們定義了一個普通的函數(shù) normalAdd 來進(jìn)行比較箕戳,main 方法中,首先我們通過調(diào)用 makeAdd 來定義了兩個 val:addOne 和 addTwo 并分別傳入 1 和 2友驮,然后執(zhí)行并打印 addOne(1) 和 addTwo(2)漂羊,運(yùn)行的結(jié)果是 2 和 3。

分析

接下來我們來詳細(xì)的分析一下上面這個例子的 Opcode卸留,通過 javap 命令來查看 Closures.class 的字節(jié)碼:

  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               <clinit>
   #9 = Utf8               ()V
  #10 = Utf8               <init>
  #11 = NameAndType        #10:#9         // "<init>":()V
  #12 = Methodref          #2.#11         // com/learn/scala/Closures$."<init>":()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        // "<init>":(I)V
  #50 = Methodref          #47.#49        // com/learn/scala/Closures$$anonfun$makeAdd$1."<init>":(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."<init>":()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<Ljava/lang/Object;Ljava/lang/Object;>;
  #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 "<init>":()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<java.lang.Object, java.lang.Object> 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."<init>":(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<Ljava/lang/Object;Ljava/lang/Object;>;

  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 部分(為了方便理解,下文中的圖中只是簡單的表示椭豫,類似常量池的部分并沒有在圖中表現(xiàn)出來)
首先通過 new 實(shí)例化了一個 class com/learn/scala/Closures$$anonfun$makeAdd$1耻瑟,圖中的 Heap 中簡單的表示為 makeAdd;

a1.jpeg

dup 命令就是復(fù)制一份上一步驟分配的空間的引用赏酥,并壓入 operand stack 的棧底

a2.jpeg

iload_1 就是將 LocalVariableTable 中的 more 壓入 operand stack

a3.jpeg

然后 invokespecial 將棧中的兩個值 pop 出來執(zhí)行 init 操作喳整,也就是將 more 的具體值傳入到 makeAdd 的初始化操作的函數(shù)中(可以認(rèn)為是構(gòu)造函數(shù)),然后將得到的新的實(shí)例化對象的引用壓入 operand stack

a4.jpeg

最后使用 areturn 返回這個引用裸扶,即將 initedmakeAddRef pop 出來

由此可以看出框都,Scala 中實(shí)際上是在 Heap 中創(chuàng)建了一個 makeAdd 的實(shí)例化對象,所以 more 變量在下次調(diào)用的時候依然可以使用呵晨,而普通方法的局部變量在調(diào)用的時候是壓入 Operand stack 中的魏保,計(jì)算完成之后就會 pop 出,所以在函數(shù)的調(diào)用完成后就不能在訪問這個變量摸屠,下面我們來通過 main 方法中的具體執(zhí)行來驗(yàn)證此結(jié)論谓罗。

我們只看關(guān)鍵的部分:
iconst_1:將 1 壓入 operand stack
invokevirtual #18 將執(zhí)行上面我們分析過的 makeAdd 函數(shù)調(diào)用的部分,調(diào)用的時候?qū)?1 作為參數(shù)傳入進(jìn)行初始化季二,并將最終得到的實(shí)例對象的引用壓入 operand stack
astore_2 將上一步驟的引用 pop 出來并存儲到 Local Variable Array 中
至此執(zhí)行了 main 方法中的 val addOne = makeAdd(1)

7檩咱、8、11 重復(fù)執(zhí)行了上面的步驟胯舷,即代碼中的第二行:val addTwo = makeAdd(2)
此時刻蚯,heap 有兩個對象的實(shí)例,然后后面的 15桑嘶、16炊汹、17、22不翩、25 和 31兵扬、32麻裳、33、38器钟、41 分別執(zhí)行了println(addOne(1))println(addTwo(1))的部分津坑,比較關(guān)鍵的部分是 invokeinterface 的時候執(zhí)行了具體對象的 apply 方法,并將 1 作為參數(shù)傳入傲霸,其它部分在此不再詳細(xì)說明疆瑰。

實(shí)際上我們可以在 bin 目錄下看到一個名為Closures$$anonfun$makeAdd$1的類文件,我們用 javap 命令可以看到昙啄,實(shí)際上是調(diào)用了實(shí)例對象的 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

至此穆役,我們驗(yàn)證了我們的結(jié)論,Scala 實(shí)現(xiàn)閉包的方法是在 heap 中保存了使用不同參數(shù)初始化而產(chǎn)生的不同對象梳凛,對象中保存了變量的狀態(tài)耿币,然后調(diào)用具體對象的 apply 方法而最后產(chǎn)生不同的結(jié)果。

結(jié)論

與 Java 中使用內(nèi)部類實(shí)現(xiàn)閉包相比韧拒,Scala 中為函數(shù)創(chuàng)建了一個對象 Function1 來保存變量的狀態(tài)淹接,然后具體執(zhí)行的時候調(diào)用對應(yīng)實(shí)例的 apply 方法,實(shí)現(xiàn)了函數(shù)作用域外也可以訪問函數(shù)內(nèi)部的變量叛溢。
希望本文對大家有所幫助塑悼,并歡迎大家批評指正文中有可能出現(xiàn)的紕漏。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末楷掉,一起剝皮案震驚了整個濱河市厢蒜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌烹植,老刑警劉巖斑鸦,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異刊橘,居然都是意外死亡鄙才,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門促绵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來攒庵,“玉大人,你說我怎么就攤上這事败晴∨埃” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵尖坤,是天一觀的道長稳懒。 經(jīng)常有香客問我,道長慢味,這世上最難降的妖魔是什么场梆? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任墅冷,我火速辦了婚禮,結(jié)果婚禮上或油,老公的妹妹穿的比我還像新娘寞忿。我一直安慰自己,他們只是感情好顶岸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布腔彰。 她就那樣靜靜地躺著,像睡著了一般辖佣。 火紅的嫁衣襯著肌膚如雪霹抛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天卷谈,我揣著相機(jī)與錄音杯拐,去河邊找鬼。 笑死世蔗,一個胖子當(dāng)著我的面吹牛藕施,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播凸郑,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼矛市!你這毒婦竟也來了芙沥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤浊吏,失蹤者是張志新(化名)和其女友劉穎而昨,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體找田,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡歌憨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了墩衙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片务嫡。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖漆改,靈堂內(nèi)的尸體忽然破棺而出心铃,到底是詐尸還是另有隱情,我是刑警寧澤挫剑,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布去扣,位于F島的核電站,受9級特大地震影響樊破,放射性物質(zhì)發(fā)生泄漏愉棱。R本人自食惡果不足惜唆铐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望奔滑。 院中可真熱鬧艾岂,春花似錦、人聲如沸档押。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽令宿。三九已至叼耙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間粒没,已是汗流浹背筛婉。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留癞松,地道東北人爽撒。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像响蓉,于是被迫代替她去往敵國和親硕勿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容