title: 函數式編程:匿名和閉包
date: 2022-07-03 21:38:00
toc: false
index_img: http://api.btstu.cn/sjbz/?lx=m_dongman&cid=8
category:
- Go
tags: - Java
- 創建
- 支持
- 環境
- 請求
- 字串
- 不同
- 出現
- 列印
- add
- 離開
- JavaScript
- 方法
- 函數
匿名函數的定義和使用#
匿名函數是一種沒有指定函數名的函數聲明方式(與之相對的,有名字的函數被稱為具名函數),在很多編程語言中都有實現和支持,比如 PHP、JavaScript(想想 Ajax 請求的實現)等,Go 語言中也提供了對匿名函數的支持,並且形式上和其他語言類似:
func(a, b int) int {
return a * b
}
和其他語言一樣,Go 匿名函數也可以賦值給一個變數或者直接執行:
// 1、將匿名函數賦值給變數
mul := func(a, b int) int {
return a * b
}
// 調用匿名函數 mul
fmt.Println(mul(1, 2))
// 2、定義時直接調用匿名函數
func(a, b int) {
fmt.Println(a * b)
} (1, 2)
匿名函數與閉包#
要解答這個問題,我們需要先了解閉包的概念。
所謂閉包指的是引用了自由變數(未綁定到特定對象的變數,通常在函數外定義)的函數,被引用的自由變數將和這個函數一同存在,即使已經離開了創造它的上下文環境也不會被釋放(比如傳遞到其他函數或對象中)。簡單來說,「閉」的意思是「封閉外部狀態」,即使外部狀態已經失效,閉包內部依然保留了一份從外部引用的變數。
顯然,閉包只能通過匿名函數實現,我們可以把閉包看作是有狀態的匿名函數,反過來,如果匿名函數引用了外部變數,就形成了一个閉包(Closure)。
閉包的價值在於可以作為持有外部變數的函數對象或者匿名函數,對於類型系統而言,這意味著不僅要表示數據還要表示代碼。支持閉包的語言都將函數作為第一類對象(firt-class object,有的地方也譯作第一级對象、一等公民等,都是一個意思),Go 語言也不例外,這意味 Go 函數和普通 Go 數據類型(整型、字串、數組、切片、字典、結構體等)具有同等的地位,可以賦值給變數,也可以作為參數傳遞給其他函數,還能夠被函數動態創建和返回。
注:所謂第一類對象指的是運行期可以被創建並作為參數傳遞給其他函數或賦值給變數的實體,在絕大多數語言中,數值和基本類型都是第一類對象,在支持閉包的編程語言中(比如 Go、PHP、JavaScript、Python 等),函數也是第一類對象,而像 C、C++ 等不支持匿名函數的語言中,函數不能在運行期創建,所以在這些語言中,函數不是第一類對象。
簡單理解就是:閉包和匿名函數一起出現,如果引用了外部變數則是閉包!可以配合下面的代碼加深理解!
package main
import "fmt"
var iotaVal int
func iotaFunc() int {
val := 1 << (10 * iotaVal)
iotaVal++
return val
}
func main() {
fmt.Printf("%d: KB -> %-14d byte \n", iotaVal, iotaFunc())
fmt.Printf("%d: MB -> %-14d byte \n", iotaVal, iotaFunc())
fmt.Printf("%d: GB -> %-14d byte \n", iotaVal, iotaFunc())
fmt.Printf("%d: TB -> %-14d byte \n", iotaVal, iotaFunc())
fmt.Printf("%d: PB -> %-14d byte \n", iotaVal, iotaFunc())
}
運行結果:
1: KB -> 1 byte
2: MB -> 1024 byte
3: GB -> 1048576 byte
4: TB -> 1073741824 byte
5: PB -> 1099511627776 byte
匿名函數的常見使用場景#
下面我們來看幾個 Go 匿名函數的典型使用場景。
保證局部變數的安全性#
匿名函數內部聲明的局部變數無法從外部修改,從而確保了安全性(類似類的私有屬性):
var j int = 1
f := func() {
var i int = 1
fmt.Printf("i, j: %d, %d\n", i, j)
}
f()
j += 2
f()
上述代碼打印結果如下:
i, j: 1, 1
i, j: 1, 3
在上面的示例中,匿名函數引用了外部變數,所以同時也是個閉包,變數 f 指向的閉包引用了局部變數 i 和 j,i 在閉包內部定義,其值被隔離,不能從外部修改,而變數 j 在閉包外部定義,所以可以從外部修改,閉包持有的只是其引用。
將匿名函數作為函數參數#
匿名函數除了可以賦值給普通變數外,還可以作為參數傳遞到函數中進行調用,就像普通數據類型一樣:
add := func(a, b int) int {
return a + b
}
// 將函數類型作為參數
func(call func(int, int) int) {
fmt.Println(call(1, 2))
}(add)
當我們將函數聲明數據類型時,需要嚴格指定每個參數和返回值的類型,這才是一個完整的函數類型,因此 add 函數對應的函數類型是 func (int, int) int。
也可以將第二個匿名函數提取到 main 函數外,成為一個具名函數 handleAdd,然後定義不同的加法算法實現函數,並將其作為參數傳入 handleAdd:
func main() {
// 普通的加法操作
add1 := func(a, b int) int {
return a + b
}
// 定義多種加法算法
base := 10
add2 := func(a, b int) int {
return a*base + b
}
handleAdd(1, 2, add1) // 3
handleAdd(1, 2, add2) // 1*10 + 2 = 12
}
// 將匿名函數作為參數
func handleAdd(a, b int, call func(int, int) int) {
fmt.Println(call(a, b))
}
上述代碼打印結果如下:
3
12
在這個示例中,第二個匿名函數 add2 引用了外部變數 base,形成了一个閉包,在調用 handleAdd 外部函數時傳入了閉包 add2 作為參數,add2 閉包在外部函數中執行時,雖然作用域離開了 main 函數,但是還是可以訪問到變數 base。
這樣一來,就可以通過一個函數執行多種不同加法實現算法,提升了代碼的重用性,我們可以基於這個功能特性實現一些更複雜的業務邏輯,比如 Go 官方 net/http 包底層的路由處理器也是這麼實現的:
// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
將匿名函數作為函數返回值#
最後,還可以將匿名函數作為函數返回值:
// 將函數作為返回值類型
func deferAdd(a, b int) func() int {
return func() int {
return a + b
}
}
func main() {
// 此時返回的是匿名函數
addFunc := deferAdd(1, 2)
// 這裡才會真正執行加法操作
fmt.Println(addFunc())
}
上述代碼打印結果如下:
3
3
在上面這個示例代碼中,調用 deferAdd 函數返回的是一個匿名函數,但是這個匿名函數引用了外部函數傳入的參數,因此形成閉包,只要這個閉包存在,這些持有的參數變數就一直存在,即使脫離了 deferAdd 函數的作用域,依然可以訪問它們。
另外調用 deferAdd 方法時並沒有執行閉包,只有運行 addFunc () 時才會真正執行閉包中的業務邏輯(這裡是加法運算),因此,我們可以通過將函數返回值聲明為函數類型來實現業務邏輯的延遲執行,讓執行時機完全掌握在開發者手中。
嘗試閱讀代碼並得到程序執行的結果:
// 將函數作為返回值類型
func deferAdd(a, b int) func() int {
return func() int {
a++
return a + b
}
}
func main() {
// 此時返回的是匿名函數
addFunc := deferAdd(1, 2)
// 這裡才會真正執行加法操作
fmt.Println(addFunc())
fmt.Println(addFunc())
}