淺探scala閉包

維基百科----閉包

In programming languages, a closure (also lexical closure or function closure) is a technique for implementing lexically scoped name binding in a language with first-class functions.
從概念角度看,在具備 first-class functions 的編程語言中,閉包是一種實現 詞法作用域級別的名字綁定 的技術。

Operationally, a closure is a record storing a function together with an environment.
從實現角度看,閉包是保存 函數 + 上下文環境 的記錄。

到此我們看到一條關於閉包的定義: 閉包=函數+上下文環境
想要理解這句話,我們就要知道兩個概念

  • 函數
  • 上下文環境

函數大家都能理解,我們繼續看看維基百科對於上下文環境的定義和分析

The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.
(上下文)環境是 自由變量名字 與該名字的 值/引用 的映射(name -> value),注意變量的值、引用是閉包 創建 時確定的。

自由變量:

  • 在函數的 enclosing scope 中定義,但在函數內部使用的變量;
  • 既不在函數參數列表中,也不是函數局部變量的變量;

A closure—unlike a plain function—allows the function to access those captured variables through the closure’s copies of their values or references, even when the function is invoked outside their scope.
被捕獲變量:即閉包使用的自由變量,閉包中一旦引用自由變量,則稱該變量被閉包捕獲。

若語言不支持 first-class functions,則閉包與普通函數並無區別,若支持 first-class function 則兩者區別巨大:

  • 閉包的上下文環境會保存被捕獲的自由變量值、引用的 副本
  • 閉包作爲函數返回值返回後,調用閉包,閉包中依然保存其 定義時 的自由變量的值

這裏需要注意的是,閉包捕獲的是變量本身,即是對變量本身的引用,這意味着:

  • 閉包外部對自由變量的修改,在閉包內部是可見的;
  • 閉包內部對自由變量的修改,在閉包外部也是可見的。
  • 這裏的修改是指重新給變量賦值而非重新聲明變量

總結如下:

  • 閉包 = 函數 + 上下文環境
  • 上下文環境 = 自由變量名字 -> 值/引用(key -> value)
  • 自由變量 = 在函數的 enclosing scope 定義,在函數內部引用的變量
  • 上下文環境中保存的 value/reference 是閉包 定義 時的 副本
  • 不同場景中,自由變量的值可以不同,進而閉包也可以不同

如此一來對於閉包的理解大概可以解釋成:
“first-class function with lexical scope”,即具備詞法作用域的第一等函數

  • 詞法作用域
    一言以蔽之,“作用域就是一套規則,用於確定在何處以及如何查找變量(標識符)的規則”。在這句話中讀到一個關鍵點 查找變量(標識符)而詞法作用域是靜態的作用域,在你書寫代碼時就確定了。
  • 第一等函數
    在 Scala 中函數是一等公民,這意味着不僅可以定義函數並調用它們,還可以將它們作爲值進行傳遞

我的理解

到此我們將維基百科的概念看的差不多了,要說一說上面的總結,將以上總結成一句話就是:

閉包是一個函數,是一個包含自由變量的函數,我們在修改自由變量時,函數也會發生改變。
OR 返回值依賴於聲明在函數外部的一個或多個變量,那麼這個函數就是閉包函數。

看一個栗子

var more = 10
// addMore 一個閉包函數:其捕獲了自由變量 more
val addMore = (x: Int) => x + more

上面的代碼中有兩個參數,一個是綁定的參數x,需要人爲的傳入,還有一個自由變量more,本身more是沒有任何意義的,但是在執行函數addMore時,在上下文中捕獲了這一變量,且依賴於這一變量,所以他是一個閉包函數

在看一個栗子

object Closures {
  def main(args: Array[String]): Unit = {
    val addone = makeadd(1) //我們僞造more自由變量12
    val addtwo = makeadd(2)

    println(addone(1))//賦值x=1
    println(addtwo(1))
  }
  def makeadd(more: Int) = (x :Int) => x+ more //捕獲more
}

我們定義了一個函數 makeAdd,輸入參數是 Int 類型,返回的是一個函數(其實可以看成函數,後面我們會深入去研究到底是什麼),main 方法中,首先我們通過調用 makeAdd 來定義了兩個 val:addOne 和 addTwo 並分別傳入 1 和 2,然後執行並打印 addOne(1) 和 addTwo(2),運行的結果是 2 和 3。

這樣就引出了閉包的另一種定義方式:

在創建函數時,如果需要捕獲自由變量,那麼包含指向被捕獲變量的引用的函數就被稱爲閉包函數。

我們可以理解成上面代碼的addone和addtwo,即是兩個閉包函數

修改自由變量和重新聲明的區別

1、修改自由變量

// 聲明 more 變量
scala> var more = 10
more: Int = 10

// more 變量必須已經被聲明,否則下面的語句會報錯
scala> val addMore = (x: Int) => {x + more}
addMore: Int => Int = $$Lambda$1076/1844473121@876c4f0

scala> addMore(10)
res7: Int = 20

// 注意這裏是給 more 變量賦值,而不是重新聲明 more 變量
scala> more=1000
more: Int = 1000

scala> addMore(10)
res8: Int = 1010

可以看出,閉包函數捕獲的自由變量也跟着改變了,輸出結果發生變化

2、重新聲明變量

// 第一次聲明 more 變量
scala> var more = 10
more: Int = 10

// 創建閉包函數
scala> val addMore10 = (x: Int) => {x + more}
addMore10: Int => Int = $$Lambda$1077/1144251618@1bdaa13c

// 調用閉包函數
scala> addMore10(9)
res9: Int = 19

// 重新聲明 more 變量
scala> var more = 100
more: Int = 100

// 創建新的閉包函數
scala> val addMore100 = (x: Int) => {x + more}
addMore100: Int => Int = $$Lambda$1078/626955849@4d0be2ac

// 引用的是重新聲明 more 變量
scala> addMore100(9)
res10: Int = 109

// 引用的還是第一次聲明的 more 變量
scala> addMore10(9)
res11: Int = 19

// 對於全局而言 more 還是 100
scala> more
res12: Int = 100

在重新聲明瞭more變量之後,原來的閉包函數並沒有發生變化,這是閉包函數中的副本機制:

自由變量可能隨着程序的改變而改變,從而產生多個副本,但是閉包永遠指向創建時候有效的那個變量副本。

這是由虛擬機來實現的,虛擬機會保證 more 變量在重新聲明後,原來的被捕獲的變量副本繼續在堆上保持存活。

懷疑柯里化是閉包的一種慣用方法???

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章