C語言中文網 目錄
首頁 > Go語言教程 > Go語言函數 閱讀:5,227

Go語言閉包(Closure)——引用了外部變量的匿名函數

閉包是引用了自由變量的函數,被引用的自由變量和函數一同存在,即使已經離開了自由變量的環境也不會被釋放或者刪除,在閉包中可以繼續使用這個自由變量。因此,簡單的說:

函數 + 引用環境 = 閉包

同一個函數與不同引用環境組合,可以形成不同的實例,如下圖所示。

圖:閉包與函數引用

一個函數類型就像結構體一樣,可以被實例化。函數本身不存儲任何信息,只有與引用環境結合后形成的閉包才具有“記憶性”。函數是編譯期靜態的概念,而閉包是運行期動態的概念。

其它編程語言中的閉包

閉包(Closure)在某些編程語言中也被稱為 Lambda 表達式。

閉包對環境中變量的引用過程,也可以被稱為“捕獲”,在 C++11 標準中,捕獲有兩種類型:引用和復制,可以改變引用的原值叫做“引用捕獲”,捕獲的過程值被復制到閉包中使用叫做“復制捕獲”。

在 Lua 語言中,將被捕獲的變量起了一個名字叫做 Upvalue,因為捕獲過程總是對閉包上方定義過的自由變量進行引用。

閉包在各種語言中的實現也是不盡相同的。在 Lua 語言中,無論閉包還是函數都屬于 Prototype 概念,被捕獲的變量以 Upvalue 的形式引用到閉包中。

C++ 與 C# 中為閉包創建了一個類,而被捕獲的變量在編譯時放到類中的成員中,閉包在訪問被捕獲的變量時,實際上訪問的是閉包隱藏類的成員。

在閉包內部修改引用的變量

閉包對它作用域上部變量的引用可以進行修改,修改引用的變量就會對變量進行實際修改,通過下面的例子來理解:
// 準備一個字符串
str := "hello world"

// 創建一個匿名函數
foo := func() {
   
    // 匿名函數中訪問str
    str = "hello dude"
}

// 調用匿名函數
foo()
代碼說明如下:
  • 第 2 行,準備一個字符串用于修改。
  • 第 5 行,創建一個匿名函數。
  • 第 8 行,在匿名函數中并沒有定義 str,str 的定義在匿名函數之前,此時,str 就被引用到了匿名函數中形成了閉包。
  • 第 12 行,執行閉包,此時 str 發生修改,變為 hello dude。

代碼輸出:
hello dude。

示例:閉包的記憶效應

被捕獲到閉包中的變量讓閉包本身擁有了記憶效應,閉包中的邏輯可以修改閉包捕獲的變量,變量會跟隨閉包生命期一直存在,閉包本身就如同變量一樣擁有了記憶效應。

累加器的實現:
package main

import (
    "fmt"
)

// 提供一個值, 每次調用函數會指定對值進行累加
func Accumulate(value int) func() int {

    // 返回一個閉包
    return func() int {

        // 累加
        value++

        // 返回一個累加值
        return value
    }
}

func main() {

    // 創建一個累加器, 初始值為1
    accumulator := Accumulate(1)

    // 累加1并打印
    fmt.Println(accumulator())

    fmt.Println(accumulator())

    // 打印累加器的函數地址
    fmt.Printf("%p\n", accumulator)

    // 創建一個累加器, 初始值為1
    accumulator2 := Accumulate(10)

    // 累加1并打印
    fmt.Println(accumulator2())

    // 打印累加器的函數地址
    fmt.Printf("%p\n", accumulator2)
}
代碼說明如下:
  • 第 8 行,累加器生成函數,這個函數輸出一個初始值,調用時返回一個為初始值創建的閉包函數。
  • 第 11 行,返回一個閉包函數,每次返回會創建一個新的函數實例。
  • 第 14 行,對引用的 Accumulate 參數變量進行累加,注意 value 不是第 11 行匿名函數定義的,但是被這個匿名函數引用,所以形成閉包。
  • 第 17 行,將修改后的值通過閉包的返回值返回。
  • 第 24 行,創建一個累加器,初始值為 1,返回的 accumulator 是類型為 func()int 的函數變量。
  • 第 27 行,調用 accumulator() 時,代碼從 11 行開始執行匿名函數邏輯,直到第 17 行返回。
  • 第 32 行,打印累加器的函數地址。

對比輸出的日志發現 accumulator 與 accumulator2 輸出的函數地址不同,因此它們是兩個不同的閉包實例。

每調用一次 accumulator 都會自動對引用的變量進行累加。

示例:閉包實現生成器

閉包的記憶效應進程被用于實現類似于設計模式中工廠模式的生成器。下面的例子展示了創建一個玩家生成器的過程。

玩家生成器的實現:
package main

import (
    "fmt"
)

// 創建一個玩家生成器, 輸入名稱, 輸出生成器
func playerGen(name string) func() (string, int) {

    // 血量一直為150
    hp := 150

    // 返回創建的閉包
    return func() (string, int) {

        // 將變量引用到閉包中
        return name, hp
    }
}

func main() {

    // 創建一個玩家生成器
    generator := playerGen("high noon")

    // 返回玩家的名字和血量
    name, hp := generator()

    // 打印值
    fmt.Println(name, hp)
}
代碼輸出如下:
high noon 150

代碼說明如下:
  • 第 8 行,playerGen() 需要提供一個名字來創建一個玩家的生成函數。
  • 第 11 行,聲明并設定 hp 變量為 150。
  • 第 14~18 行,將 hp 和 name 變量引用到匿名函數中形成閉包。
  • 第 24 行中,通過 playerGen 傳入參數調用后獲得玩家生成器。
  • 第 27 行,調用這個玩家生成器函數,可以獲得玩家的名稱和血量。

閉包還具有一定的封裝性,第 11 行的變量是 playerGen 的局部變量,playerGen 的外部無法直接訪問及修改這個變量,這種特性也與面向對象中強調的封裝性類似。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo