聊聊Go裡面的閉包

秦懷雜貨店 發表於 2022-11-22
Go

以前寫 Java 的時候,聽到前端同學談論閉包,覺得甚是新奇,後面自己寫了一小段時間 JS,雖只學到皮毛,也大概瞭解到閉包的概念,現在工作常用語言是 Go,很多優雅的程式碼中總是有閉包的身影,看來不瞭解個透是不可能的了,本文讓我來科普(按照自己水平隨便瞎扯)一下:

1、什麼是閉包?

在真正講述閉包之前,我們先鋪墊一點知識點:

  • 函數語言程式設計
  • 函式作用域
  • 作用域的繼承關係

    ## 1.1 前提知識鋪墊

1.2.1 函數語言程式設計

函數語言程式設計是一種程式設計正規化,看待問題的一種方式,每一個函式都是為了用小函式組織成為更大的函式,函式的引數也是函式,函式返回的也是函式。我們常見的程式設計正規化有:

  • 指令式程式設計:

    • 主要思想為:關注計算機執行的步驟,也就是一步一步告訴計算機先做什麼再做什麼。
    • 先把解決問題步驟規範化,抽象為某種演算法,然後編寫具體的演算法去實現,一般只要支援過程化程式設計正規化的語言,我們都可以稱為過程化程式語言,比如 BASIC,C 等。
  • 宣告式程式設計:

    • 主要思想為:告訴計算機應該做什麼,但是不指定具體要怎麼做,比如 SQL,網頁程式設計的 HTML,CSS。
  • 函數語言程式設計:

    • 只關注做什麼而不關注怎麼做,有一絲絲宣告式程式設計的影子,但是更加側重於”函式是第一位“的原則,也就是函式可以出現在任何地方,引數、變數、返回值等等。

函數語言程式設計可以認為是物件導向程式設計的對立面,一般只有一些程式語言會強調一種特定的程式設計方式,大多數的語言都是多正規化語言,可以支援多種不同的程式設計方式,比如 JavaScript ,Go 等。

函數語言程式設計是一種思維方式,將電腦運算視為函式的計算,是一種寫程式碼的方法論,其實我應該聊函數語言程式設計,然後再聊到閉包,因為閉包本身就是函數語言程式設計裡面的一個特點之一。

在函數語言程式設計中,函式是頭等物件,意思是說一個函式,既可以作為其它函式的輸入引數值,也可以從函式中返回值,被修改或者被分配給一個變數。(維基百科)

一般純函式程式語言是不允許直接使用程式狀態以及可變物件的,函數語言程式設計本身就是要避免使用 共享狀態可變狀態,儘可能避免產生 副作用

函數語言程式設計一般具有以下特點:

  1. 函式是第一等公民:函式的地位放在第一位,可以作為引數,可以賦值,可以傳遞,可以當做返回值。
  2. 沒有副作用:函式要保持純粹獨立,不能修改外部變數的值,不修改外部狀態。
  3. 引用透明:函式執行不依賴外部變數或者狀態,相同的輸入引數,任何情況,所得到的返回值都應該是一樣的。

1.2.2 函式作用域

作用域(scope),程式設計概念,通常來說,一段程式程式碼中所用到的名字並不總是有效/可用的,而限定這個名字的可用性的程式碼範圍就是這個名字的作用域

通俗易懂的說,函式作用域是指函式可以起作用的範圍。函式有點像盒子,一層套一層,作用域我們可以理解為是個封閉的盒子,也就是函式的區域性變數,只能在盒子內部使用,成為獨立作用域。

image-20221112163921104

函式內的區域性變數,出了函式就跳出了作用域,找不到該變數。(裡層函式可以使用外層函式的區域性變數,因為外層函式的作用域包括了裡層函式),比如下面的 innerTmep 出了函式作用域就找不到該變數,但是 outerTemp 在內層函式里面還是可以使用。

image-20221112164640101

不管是任何語言,基本存在一定的記憶體回收機制,也就是回收用不到的記憶體空間,回收的機制一般和上面說的函式的作用域是相關的,區域性變數出了其作用域,就有可能被回收,如果還被引用著,那麼就不會被回收。

1.2.3 作用域的繼承關係

所謂作用域繼承,就是前面說的小盒子可以繼承外層大盒子的作用域,在小盒子可以直接取出大盒子的東西,但是大盒子不能取出小盒子的東西,除非發生了逃逸(逃逸可以理解為小盒子的東西給出了引用,大盒子拿到就可以使用)。一般而言,變數的作用域有以下兩種:

  • 全域性作用域:作用於任何地方
  • 區域性作用域:一般是程式碼塊,函式、包內,函式內部宣告/定義的變數叫區域性變數作用域僅限於函式內部

1.2 閉包的定義

“多數情況下我們並不是先理解後定義,而是先定義後理解“,先下定義,讀不懂沒關係

閉包(closure)是一個函式以及其捆綁的周邊環境狀態(lexical environment,詞法環境)的引用的組合。 換而言之,閉包讓開發者可以從內部函式訪問外部函式的作用域。 閉包會隨著函式的建立而被同時建立。

一句話表述:

$$ 閉包 = 函式 + 引用環境 $$

以上定義找不到 Go語言 這幾個字眼,聰明的同學肯定知道,閉包是和語言無關的,不是 JavaScript 特有的,也不是 Go 特有的,而是函數語言程式設計語言的特有的,是的,你沒有看錯,任何支援函數語言程式設計的語言都支援閉包,Go 和 JavaScript 就是其中之二, 目前 Java 目前版本也是支援閉包的,但是有些人可能認為不是完美的閉包,詳細情況文中討論。

1.3 閉包的寫法

1.3.1 初看閉包

下面是一段閉包的程式碼:

import "fmt"

func main() {
    sumFunc := lazySum([]int{1, 2, 3, 4, 5})
    fmt.Println("等待一會")
    fmt.Println("結果:", sumFunc())
}
func lazySum(arr []int) func() int {
    fmt.Println("先獲取函式,不求結果")
    var sum = func() int {
        fmt.Println("求結果...")
        result := 0
        for _, v := range arr {
            result = result + v
        }
        return result
    }
    return sum
}

輸出的結果:

先獲取函式,不求結果
等待一會
求結果...
結果: 15

可以看出,裡面的 sum() 方法可以引用外部函式 lazySum() 的引數以及區域性變數,在lazySum()返回函式 sum() 的時候,相關的引數和變數都儲存在返回的函式中,可以之後再進行呼叫。

上面的函式或許還可以更進一步,體現出捆綁函式和其周圍的狀態,我們加上一個次數 count

import "fmt"

func main() {
    sumFunc := lazySum([]int{1, 2, 3, 4, 5})
    fmt.Println("等待一會")
    fmt.Println("結果:", sumFunc())
    fmt.Println("結果:", sumFunc())
    fmt.Println("結果:", sumFunc())
}

func lazySum(arr []int) func() int {
    fmt.Println("先獲取函式,不求結果")
    count := 0
    var sum = func() int {
        count++
        fmt.Println("第", count, "次求結果...")
        result := 0
        for _, v := range arr {
            result = result + v
        }
        return result
    }
    return sum
}

上面程式碼輸出什麼呢?次數 count 會不會發生變化,count明顯是外層函式的區域性變數,但是在記憶體函式引用(捆綁),內層函式被暴露出去了,執行結果如下:

先獲取函式,不求結果
等待一會
第 1 次求結果...
結果: 15
第 2 次求結果...
結果: 15
第 3 次求結果...
結果: 15

結果是 count 其實每次都會變化,這種情況總結一下:

  • 函式體內巢狀了另外一個函式,並且返回值是一個函式。
  • 內層函式被暴露出去,被外層函式以外的地方引用著,形成了閉包。

此時有人可能有疑問了,前面是lazySum()被建立了 1 次,執行了 3 次,但是如果是 3 次執行都是不同的建立,會是怎麼樣呢?實驗一下:

import "fmt"

func main() {
    sumFunc := lazySum([]int{1, 2, 3, 4, 5})
    fmt.Println("等待一會")
    fmt.Println("結果:", sumFunc())

    sumFunc1 := lazySum([]int{1, 2, 3, 4, 5})
    fmt.Println("等待一會")
    fmt.Println("結果:", sumFunc1())

    sumFunc2 := lazySum([]int{1, 2, 3, 4, 5})
    fmt.Println("等待一會")
    fmt.Println("結果:", sumFunc2())
}

func lazySum(arr []int) func() int {
    fmt.Println("先獲取函式,不求結果")
    count := 0
    var sum = func() int {
        count++
        fmt.Println("第", count, "次求結果...")
        result := 0
        for _, v := range arr {
            result = result + v
        }
        return result
    }
    return sum
}

執行的結果如下,每次執行都是第 1 次:

先獲取函式,不求結果
等待一會
第 1 次求結果...
結果: 15
先獲取函式,不求結果
等待一會
第 1 次求結果...
結果: 15
先獲取函式,不求結果
等待一會
第 1 次求結果...
結果: 15

從以上的執行結果可以看出:

閉包被建立的時候,引用的外部變數count就已經被建立了 1 份,也就是各自呼叫是沒有關係的

繼續丟擲一個問題,如果一個函式返回了兩個函式,這是一個閉包還是兩個閉包呢?下面我們實踐一下:

一次返回兩個函式,一個用於計算加和的結果,一個計算乘積:

import "fmt"

func main() {
    sumFunc, productSFunc := lazyCalculate([]int{1, 2, 3, 4, 5})
    fmt.Println("等待一會")
    fmt.Println("結果:", sumFunc())
    fmt.Println("結果:", productSFunc())
}

func lazyCalculate(arr []int) (func() int, func() int) {
    fmt.Println("先獲取函式,不求結果")
    count := 0
    var sum = func() int {
        count++
        fmt.Println("第", count, "次求加和...")
        result := 0
        for _, v := range arr {
            result = result + v
        }
        return result
    }

    var product = func() int {
        count++
        fmt.Println("第", count, "次求乘積...")
        result := 0
        for _, v := range arr {
            result = result * v
        }
        return result
    }
    return sum, product
}

執行結果如下:

先獲取函式,不求結果
等待一會
第 1 次求加和...
結果: 15
第 2 次求乘積...
結果: 0

從上面結果可以看出,閉包是函式返回函式的時候,不管多少個返回值(函式),都是一次閉包,如果返回的函式有使用外部函式變數,則會繫結到一起,相互影響:

image-20221119001944927

閉包繫結了周圍的狀態,我理解此時的函式就擁有了狀態,讓函式具有了物件所有的能力,函式具有了狀態。

1.3.2 閉包中的指標和值

上面的例子,我們閉包中用到的都是數值,如果我們傳遞指標,會是怎麼樣的呢?

import "fmt"
func main() {
    i := 0
    testFunc := test(&i)
    testFunc()
    fmt.Printf("outer i = %d\n", i)
}
func test(i *int) func() {
    *i = *i + 1
    fmt.Printf("test inner i = %d\n", *i)
    return func() {
        *i = *i + 1
        fmt.Printf("func inner i = %d\n", *i)
    }
}

執行結果如下:

test inner i = 1
func inner i = 2
outer i = 2

可以看出如果是指標的話,閉包裡面修改了指標對應的地址的值,也會影響閉包外面的值。這個其實很容易理解,Go 裡面沒有引用傳遞,只有值傳遞,那我們傳遞指標的時候,也是值傳遞,這裡的值是指標的數值(可以理解為地址值)。

當我們函式的引數是指標的時候,引數會複製一份這個指標地址,當做引數進行傳遞,因為本質還是地址,所以內部修改的時候,仍然可以對外部產生影響。

閉包裡面的資料其實地址也是一樣的,下面的實驗可以證明:

func main() {
    i := 0
    testFunc := test(&i)
    testFunc()
    fmt.Printf("outer i address %v\n", &i)
}
func test(i *int) func() {
    *i = *i + 1
    fmt.Printf("test inner i address %v\n", i)
    return func() {
        *i = *i + 1
        fmt.Printf("func inner i address %v\n", i)
    }
}

輸出如下, 因此可以推斷出,閉包如果引用外部環境的指標資料,只是會複製一份指標地址資料,而不是複製一份真正的資料(==先留個問題:複製的時機是什麼時候呢==):

test inner i address 0xc0003fab98
func inner i address 0xc0003fab98
outer i address 0xc0003fab98

1.3.2 閉包延遲化

上面的例子彷彿都在告訴我們,閉包建立的時候,資料就已經複製了,但是真的是這樣麼?

下面是繼續前面的實驗:

func main() {
    i := 0
    testFunc := test(&i)
    i = i + 100
    fmt.Printf("outer i before testFunc  %d\n", i)
    testFunc()
    fmt.Printf("outer i after testFunc %d\n", i)
}
func test(i *int) func() {
    *i = *i + 1
    fmt.Printf("test inner i = %d\n", *i)
    return func() {
        *i = *i + 1
        fmt.Printf("func inner i = %d\n", *i)
    }
}

我們在建立閉包之後,把資料改了,之後執行閉包,答案肯定是真實影響閉包的執行,因為它們都是指標,都是指向同一份資料:

test inner i = 1
outer i before testFunc  101
func inner i = 102
outer i after testFunc 102

假設我們換個寫法,讓閉包外部環境中的變數在宣告閉包函式的之後,進行修改:

import "fmt"

func main() {
    sumFunc := lazySum([]int{1, 2, 3, 4, 5})
    fmt.Println("等待一會")
    fmt.Println("結果:", sumFunc())
}
func lazySum(arr []int) func() int {
    fmt.Println("先獲取函式,不求結果")
    count := 0
    var sum = func() int {
        fmt.Println("第", count, "次求結果...")
        result := 0
        for _, v := range arr {
            result = result + v
        }
        return result
    }
    count = count + 100
    return sum
}

實際執行結果,count 會是修改後的值:

等待一會
第 100 次求結果...
結果: 15

這也證明了,實際上閉包並不會在宣告var sum = func() int {...}這句話之後,就將外部環境的 count繫結到閉包中,而是在函式返回閉包函式的時候,才繫結的,這就是延遲繫結

如果還沒看明白沒關係,我們再來一個例子:

func main() {
    funcs := testFunc(100)
    for _, v := range funcs {
        v()
    }
}
func testFunc(x int) []func() {
    var funcs []func()
    values := []int{1, 2, 3}
    for _, val := range values {
        funcs = append(funcs, func() {
            fmt.Printf("testFunc val = %d\n", x+val)
        })
    }
    return funcs
}

上面的例子,我們閉包返回的是函式陣列,本意我們想入每一個 val 都不一樣,但是實際上 val都是一個值,==也就是執行到return funcs 的時候(或者真正執行閉包函式的時候)才繫結的 val值==(關於這一點,後面還有個Demo可以證明),此時 val的值是最後一個 3,最終輸出結果都是 103:

testFunc val = 103
testFunc val = 103
testFunc val = 103

以上兩個例子,都是閉包延遲繫結的問題導致,這也可以說是 feature,到這裡可能不少同學還是對閉包繫結外部變數的時機有疑惑,到底是返回閉包函式的時候繫結的呢?還是真正執行閉包函式的時候才繫結的呢?

下面的例子可以有效的解答:

import (
    "fmt"
    "time"
)

func main() {
    sumFunc := lazySum([]int{1, 2, 3, 4, 5})
    fmt.Println("等待一會")
    fmt.Println("結果:", sumFunc())
    time.Sleep(time.Duration(3) * time.Second)
    fmt.Println("結果:", sumFunc())
}
func lazySum(arr []int) func() int {
    fmt.Println("先獲取函式,不求結果")
    count := 0
    var sum = func() int {
        count++
        fmt.Println("第", count, "次求結果...")
        result := 0
        for _, v := range arr {
            result = result + v
        }
        return result
    }
    go func() {
        time.Sleep(time.Duration(1) * time.Second)
        count = count + 100
        fmt.Println("go func 修改後的變數 count:", count)
    }()
    return sum
}

輸出結果如下:

先獲取函式,不求結果
等待一會
第 1 次求結果...
結果: 15
go func 修改後的變數 count: 101
第 102 次求結果...
結果: 15

第二次執行閉包函式的時候,明顯 count被裡面的 go func()修改了,也就是呼叫的時候,才真正的獲取最新的外部環境,但是在宣告的時候,就會把環境預留儲存下來。

其實本質上,Go Routine的匿名函式的延遲繫結就是閉包的延遲繫結,上面的例子中,go func(){}獲取到的就是最新的值,而不是原始值0

總結一下上面的驗證點:

  • 閉包每次返回都是一個新的例項,每個例項都有一份自己的環境。
  • 同一個例項多次執行,會使用相同的環境。
  • 閉包如果逃逸的是指標,會相互影響,因為繫結的是指標,相同指標的內容修改會相互影響。
  • 閉包並不是在宣告時繫結的值,宣告後只是預留了外部環境(逃逸分析),真正執行閉包函式時,會獲取最新的外部環境的值(也稱為延遲繫結)。
  • Go Routine的匿名函式的延遲繫結本質上就是閉包的延遲繫結。

2、閉包的好處與壞處?

2.1 好處

純函式沒有狀態,而閉包則是讓函式輕鬆擁有了狀態。但是凡事都有兩面性,一旦擁有狀態,多次呼叫,可能會出現不一樣的結果,就像是前面測試的 case 中一樣。那麼問題來了:

Q:如果不支援閉包的話,我們想要函式擁有狀態,需要怎麼做呢?

A: 需要使用全域性變數,讓所有函式共享同一份變數。

但是我們都知道全域性變數有以下的一些特點(在不同的場景,優點會變成缺點):

  • 常駐於記憶體之中,只要程式不停會一直在記憶體中。
  • 汙染全域性,大家都可以訪問,共享的同時不知道誰會改這個變數。

閉包可以一定程度最佳化這個問題:

  • 不需要使用全域性變數,外部函式區域性變數在閉包的時候會建立一份,生命週期與函式生命週期一致,閉包函式不再被引用的時候,就可以回收了。
  • 閉包暴露的區域性變數,外界無法直接訪問,只能透過函式操作,可以避免濫用。

除了以上的好處,像在 JavaScript 中,沒有原生支援私有方法,可以靠閉包來模擬私有方法,因為閉包都有自己的詞法環境。

2.2 壞處

函式擁有狀態,如果處理不當,會導致閉包中的變數被誤改,但這是編碼者應該考慮的問題,是預期中的場景。

閉包中如果隨意建立,引用被持有,則無法銷燬,同時閉包內的區域性變數也無法銷燬,過度使用閉包會佔有更多的記憶體,導致效能下降。一般而言,能共享一份閉包(共享閉包區域性變數資料),不需要多次建立閉包函式,是比較優雅的方式。

3、閉包怎麼實現的?

從上面的實驗中,我們可以知道,閉包實際上就是外部環境的逃逸,跟隨著閉包函式一起暴露出去。

我們用以下的程式進行分析:

import "fmt"

func testFunc(i int) func() int {
    i = i * 2
    testFunc := func() int {
        i++
        return i
    }
    i = i * 2
    return testFunc
}
func main() {
    test := testFunc(1)
    fmt.Println(test())
}

執行結果如下:

5

先看看逃逸分析,用下面的命令列可以檢視:

 go build --gcflags=-m main.go

image-20221120223253318

可以看到 變數 i被移到堆中,也就是本來是區域性變數,但是發生逃逸之後,從棧裡面放到堆裡面,同樣的 test()函式由於是閉包函式,也逃逸到堆上。

下面我們用命令列來看看彙編程式碼:

go tool compile -N -l -S main.go

生成程式碼比較長,我擷取一部分:

"".testFunc STEXT size=218 args=0x8 locals=0x38 funcid=0x0 align=0x0
        0x0000 00000 (main.go:5)        TEXT    "".testFunc(SB), ABIInternal, $56-8
        0x0000 00000 (main.go:5)        CMPQ    SP, 16(R14)
        0x0004 00004 (main.go:5)        PCDATA  $0, $-2
        0x0004 00004 (main.go:5)        JLS     198
        0x000a 00010 (main.go:5)        PCDATA  $0, $-1
        0x000a 00010 (main.go:5)        SUBQ    $56, SP
        0x000e 00014 (main.go:5)        MOVQ    BP, 48(SP)
        0x0013 00019 (main.go:5)        LEAQ    48(SP), BP
        0x0018 00024 (main.go:5)        FUNCDATA        $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
        0x0018 00024 (main.go:5)        FUNCDATA        $1, gclocals·d571c0f6cf0af59df28f76498f639cf2(SB)
        0x0018 00024 (main.go:5)        FUNCDATA        $5, "".testFunc.arginfo1(SB)
        0x0018 00024 (main.go:5)        MOVQ    AX, "".i+64(SP)
        0x001d 00029 (main.go:5)        MOVQ    $0, "".~r0+16(SP)
        0x0026 00038 (main.go:5)        LEAQ    type.int(SB), AX
        0x002d 00045 (main.go:5)        PCDATA  $1, $0
        0x002d 00045 (main.go:5)        CALL    runtime.newobject(SB)
        0x0032 00050 (main.go:5)        MOVQ    AX, "".&i+40(SP)
        0x0037 00055 (main.go:5)        MOVQ    "".i+64(SP), CX
        0x003c 00060 (main.go:5)        MOVQ    CX, (AX)
        0x003f 00063 (main.go:6)        MOVQ    "".&i+40(SP), CX
        0x0044 00068 (main.go:6)        MOVQ    "".&i+40(SP), DX
        0x0049 00073 (main.go:6)        MOVQ    (DX), DX
        0x004c 00076 (main.go:6)        SHLQ    $1, DX
        0x004f 00079 (main.go:6)        MOVQ    DX, (CX)
        0x0052 00082 (main.go:7)        LEAQ    type.noalg.struct { F uintptr; "".i *int }(SB), AX
        0x0059 00089 (main.go:7)        PCDATA  $1, $1
        0x0059 00089 (main.go:7)        CALL    runtime.newobject(SB)
        0x005e 00094 (main.go:7)        MOVQ    AX, ""..autotmp_3+32(SP)
        0x0063 00099 (main.go:7)        LEAQ    "".testFunc.func1(SB), CX
        0x006a 00106 (main.go:7)        MOVQ    CX, (AX)
        0x006d 00109 (main.go:7)        MOVQ    ""..autotmp_3+32(SP), CX
        0x0072 00114 (main.go:7)        TESTB   AL, (CX)
        0x0074 00116 (main.go:7)        MOVQ    "".&i+40(SP), DX
        0x0079 00121 (main.go:7)        LEAQ    8(CX), DI
        0x007d 00125 (main.go:7)        PCDATA  $0, $-2
        0x007d 00125 (main.go:7)        CMPL    runtime.writeBarrier(SB), $0
        0x0084 00132 (main.go:7)        JEQ     136
        0x0086 00134 (main.go:7)        JMP     142
        0x0088 00136 (main.go:7)        MOVQ    DX, 8(CX)
        0x008c 00140 (main.go:7)        JMP     149
        0x008e 00142 (main.go:7)        CALL    runtime.gcWriteBarrierDX(SB)
        0x0093 00147 (main.go:7)        JMP     149
        0x0095 00149 (main.go:7)        PCDATA  $0, $-1
        0x0095 00149 (main.go:7)        MOVQ    ""..autotmp_3+32(SP), CX
        0x009a 00154 (main.go:7)        MOVQ    CX, "".testFunc+24(SP)
        0x009f 00159 (main.go:11)       MOVQ    "".&i+40(SP), CX
        0x00a4 00164 (main.go:11)       MOVQ    "".&i+40(SP), DX
        0x00a9 00169 (main.go:11)       MOVQ    (DX), DX
        0x00ac 00172 (main.go:11)       SHLQ    $1, DX
        0x00af 00175 (main.go:11)       MOVQ    DX, (CX)
        0x00b2 00178 (main.go:12)       MOVQ    "".testFunc+24(SP), AX
        0x00b7 00183 (main.go:12)       MOVQ    AX, "".~r0+16(SP)
        0x00bc 00188 (main.go:12)       MOVQ    48(SP), BP
        0x00c1 00193 (main.go:12)       ADDQ    $56, SP
        0x00c5 00197 (main.go:12)       RET
        0x00c6 00198 (main.go:12)       NOP
        0x00c6 00198 (main.go:5)        PCDATA  $1, $-1
        0x00c6 00198 (main.go:5)        PCDATA  $0, $-2
        0x00c6 00198 (main.go:5)        MOVQ    AX, 8(SP)
        0x00cb 00203 (main.go:5)        CALL    runtime.morestack_noctxt(SB)
        0x00d0 00208 (main.go:5)        MOVQ    8(SP), AX
        0x00d5 00213 (main.go:5)        PCDATA  $0, $-1
        0x00d5 00213 (main.go:5)        JMP     0

可以看到閉包函式實際上底層也是用結構體new建立出來的:

image-20221120224413412

使用的就是堆上面的 i

image-20221120225532865

也就是返回函式的時候,實際上返回結構體,結構體裡面記錄了函式的引用環境。

4、淺聊一下

## 4.1 Java 支不支援閉包?

網上有很多種看法,實際上 Java 雖然暫時不支援返回函式作為返參,但是Java 本質上還是實現了閉包的概念的,所使用的的方式是內部類的形式,因為是內部類,所以相當於自帶了一個引用環境,算是一種不完整的閉包。

目前有一定限制,比如是 final 宣告的,或者是明確定義的值,才可以進行傳遞:

Stack Overflow上有相關答案:https://stackoverflow.com/que...

image-20221120233223203

4.2 函數語言程式設計的前景怎麼樣?

下面是Wiki的內容:

函數語言程式設計長期以來在學術界流行,但幾乎沒有工業應用。造成這種局面的主因是函數語言程式設計常被認為嚴重耗費CPU和儲存器資源[18] ,這是由於在早期實現函數語言程式設計語言時並沒有考慮過效率問題,而且面向函數語言程式設計特性,如保證參照透明性等,要求獨特的資料結構和演算法。[19]

然而,最近幾種函數語言程式設計語言已經在商業或工業系統中使用[20],例如:

  • Erlang,它由瑞典公司愛立信在20世紀80年代後期開發,最初用於實現容錯電信系統。此後,它已在NortelFacebookÉlectricité de FranceWhatsApp等公司作為流行語言建立一系列應用程式。[21][22]
  • Scheme,它被用作早期Apple Macintosh計算機上的幾個應用程式的基礎,並且最近已應用於諸如訓練模擬軟體和望遠鏡控制等方向。
  • OCaml,它於20世紀90年代中期推出,已經在金融分析,驅動程式驗證,工業機器人程式設計和嵌入式軟體靜態分析等領域得到了商業應用。
  • Haskell,它雖然最初是作為一種研究語言,也已被一系列公司應用於航空航天系統,硬體設計和網路程式設計等領域。

其他在工業中使用的函數語言程式設計語言包括多範型的Scala[23]F#,還有Wolfram語言Common LispStandard MLClojure等。

從我個人的看法,不看好純函式程式設計,但是函數語言程式設計的思想,我相信以後幾乎每門高階程式設計需要都會具備,特別期待 Java 擁抱函數語言程式設計。從我自己瞭解的語言看,像 Go,JavaScript 中的函數語言程式設計的特性,都讓開發者深愛不已(當然,如果寫出了bug,就是深惡痛疾)。

最近突然火了一波的原因,也是因為世界不停的發展,記憶體也越來越大,這個因素的限制幾乎要解放了。

我相信,世界就是絢麗多彩的,要是一種事物統治世界,絕無可能,更多的是百家爭鳴,程式語言或者程式設計正規化也一樣,後續可能有集大成者,最終最終歷史會篩選出最終符合人類社會發展的。

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,個人網站:http://aphysia.cn,技術之路不在一時,山高水長,縱使緩慢,馳而不息。