用“揹包”去理解Go語言中的閉包

go_9發表於2019-02-21

在函式程式設計中經常用到閉包。閉包是什麼,它是怎麼產生的及用來解決什麼問題呢。給出字面的定義先:閉包是由函式及其相關的引用環境組合而成的實體(即:閉包=函式+引用環境)。這個從字面上很難理解,特別對於一直使用命令式語言進行程式設計的程式設計師們。本文將結合例項程式碼進行解釋。

函式是什麼 地球人都知道:函式只是一段可執行程式碼,編譯後就“固化”了,每個函式在記憶體中只有一份例項,得到函式的入口點便可以執行函式了。在函數語言程式設計語言中,函式是一等公民(First class value:第一類物件,我們不需要像命令式語言中那樣藉助函式指標,委託操作函式),函式可以作為另一個函式的引數或返回值,可以賦給一個變數。函式可以巢狀定義,即在一個函式內部可以定義另一個函式,有了巢狀函式這種結構,便會產生閉包問題。如:

package main

import "fmt"

func AntherExFunc(n int) func() {
    n++
    return func() {
        fmt.Println(n)
    }
}

func ExFunc(n int) func() {
    return func() {
        n++ 
        fmt.Println(n)
    }
}

func main() {
    myAnotherFunc:=AntherExFunc(20)
    fmt.Println(myAnotherFunc)  //0x48e3d0
    myAnotherFunc()     //21
    myAnotherFunc()     //21

    myFunc:=ExFunc(10)
    fmt.Println(myFunc)  //0x48e340
    myFunc()       //11
    myFunc()       //12

}

我們來一點點分析一下這些程式碼,我們可以先把myAnotherFunc、myFunc變數的型別列印出來,可以看到他們的型別是地址。你可以理解為這個地址指向一個揹包,這個揹包裡面裝著一些變數和一個函式。就如myAnotherFunc變數揹包裡面裝著n變數和fmt.Println(n)函式。當在main函式中調這行程式碼的時候myAnotherFunc:=AntherExFunc(20),這個揹包就形成了,myAnotherFunc()程式碼是什麼意思呢?就是說我在揹包裡面拿出函式對這個揹包裡面的變數進行一系列的操作,並且把這個變數返回出去。

那我為什麼又定義了一個ExFunc()函式呢?仔細看程式碼就知道了,我呼叫了兩次myFunc()函式,返回的是11和12,而呼叫了兩次myAnotherFunc()函式返回的都是21,為什麼會這樣呢?大家要記住兩點。

  1. 內函式(ExFunc函式中的return語句)對外函式的變數(n)的修改,是對變數的引用。
  2. 變數被引用後,它所在的函式結束,這變數也不會馬上被銷燬。

知道這兩點就可以理解myAnotherFunc()函式兩次呼叫結果都是一樣的了。因為因為他根本就沒有對n變數修改,那你又要問一句可是他上面有n++這行程式碼啊????這你又怎麼去解釋呢?這個很好解釋啦,我在main()函式呼叫100W次myAnotherFunc()函式,只是呼叫相應揹包內的函式,n++這行程式碼根本就沒有跑,他就相當於一個變數儲存在這個揹包中。好好理解一下。

最後給出引用環境的定義

在函式式語言中,當內嵌函式體內引用到體外的變數時,將會把定義時涉及到的引用環境和函式體打包成一個整體(閉包)返回。現在給出引用環境的定義就容易理解了:引用環境是指在程式執行中的某個點所有處於活躍狀態的約束(一個變數的名字和其所代表的物件之間的聯絡)所組成的集合。閉包的使用和正常的函式呼叫沒有區別。

由於閉包把函式和執行時的引用環境打包成為一個新的整體,所以就解決了函式程式設計中的巢狀所引發的問題。如上述程式碼段中,當每次呼叫ExFunc函式時都將返回一個新的閉包例項,這些例項之間是隔離的,分別包含呼叫時不同的引用環境現場。不同於函式,閉包在執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項。

相關文章