本篇文章是 Go 語言學習筆記之函數式編程系列文章的第二篇,上一篇介紹了函數基礎,這一篇文章重點介紹函數的重要應用之一: 閉包
空談誤國,實幹興邦,以具體代碼示例為基礎講解什麼是閉包以及為什麼需要閉包等問題,下面我們沿用上篇文章的示例代碼開始本文的學習吧!
斐波那契數列是形如 1 1 2 3 5 8 13 21 34 55 的遞增數列,即從第三個數開始,後一個數字是前兩個數字之和,保持此規律無限遞增...
開門見山,直接給出斐波那契數列生成器,接下來的文章慢慢深挖背後隱藏的奧秘,一個例子講清楚什麼是閉包.
「雪之夢技術驛站」: 如果還不了解 Go 語言的函數用法,可以參考上一篇文章: go 學習筆記之學習函數式編程前不要忘了函數基礎
Go 版本的斐波那契數列生成器
「雪之夢技術驛站」: Go 語言支持連續賦值,更加貼合思考方式,而其餘主流的程式語言可能不支持這種方式,大多採用臨時變量暫存的方式.
Go 版本的單元測試用例
「雪之夢技術驛站」: 循環調用 10 次斐波那契數列生成器,因此生成前十位數列: 1 1 2 3 5 8 13 21 34 55
背後有故事
小小的斐波那契數列生成器背後蘊藏著豐富的 Go 語言特性,該示例也是官方示例之一.
支持連續賦值,無需中間變量「雪之夢技術驛站」: Go 語言和其他主流的程式語言不同,它們大多數最多支持多變量的連續聲明而不支持連續賦值.
這也是 Go 語言特有的交換變量方式,a, b = b, a 語義簡單明確並不用引入額外的臨時變量.
「雪之夢技術驛站」: Go 語言實現變量交互的示例,a, b = b, a 表示變量直接交換.
而其他主流的程式語言的慣用做法是需要引入臨時變量,大多數代碼類似如下方式:
「雪之夢技術驛站」: Go 語言的多變量同時賦值特性體現的更多是一種聲明式編程思想,不關注具體實現,而引入臨時變量這種體現的則是命令式編程思維.
函數的返回值也可以是函數「雪之夢技術驛站」: Go 語言中的函數是一等公民,不僅函數的返回值可以是函數,參數,變量等等都可以是函數.
函數的返回值可以是函數,這樣的實際意義在於使用者可以擁有更大的靈活性,有時可以用作延遲計算,有時也可以用作函數增強.
先來演示一下延遲計算的示例:
函數的返回值可以是函數,由此實現類似於 i++ 效果的自增函數.因為 i 的初值是 0,所以每調用一次該函數, i 的值就會自增,從而實現 i++ 的效果.
再小的代碼片段也不應該忘記測試,單元測試繼續走起,順便看一下使用方法.
初始調用 autoIncrease 函數並沒有直接得到結果而是返回了函數引用,等到使用者覺得時機成熟後再次調用返回的函數引用即變量a ,這時候才會真正計算結果,這種方式被稱為延遲計算也叫做惰性求值.
繼續演示一下功能增強的示例:
因為要演示函數增強功能,沒有輸入哪來的輸出?
所以函數的入參應該也是函數,返回值就是增強後的函數.
這樣的話接下來要做的函數就比較清晰了,這裡我們定義 timeSpend 函數: 實現的功能是包裝特定類型的函數,增加計算函數運行時間的新功能並包裝成函數,最後返回出去給使用者.
為了演示包裝函數 timeSpend,需要定義一個比較耗時函數當做入參,函數名稱姑且稱之為為 slowFunc ,睡眠 1s 來模擬耗時操作.
無測試不編碼,繼續運行單元測試用例,演示包裝函數 timeSpend 是如何增強原函數 slowFunc 以實現功能增強?
「雪之夢技術驛站」: 測試結果顯示原函數 slowFunc 被當做入參傳遞給包裝函數 timeSpend 後實現了功能增強,不僅保留了原函數功能還增加了計時功能.
函數嵌套可能是閉包函數不論是引言部分的斐波那契數列生成器函數還是演示函數返回值的自增函數示例,其實這種形式的函數有一種專業術語稱為"閉包".
一般而言,函數內部不僅存在變量還有嵌套函數,而嵌套函數又引用了這些外部變量的話,那麼這種形式很有可能就是閉包函數.
什麼是閉包
如果有一句話介紹什麼是閉包,那麼我更願意稱其為流浪在外的人想念爸爸媽媽!
如果非要用比較官方的定義去解釋什麼是閉包,那隻好翻開https://en.wikipedia.org/wiki/Closure_%28computer_programming%29 看下有關閉包的定義:
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. Operationally, a closure is a record storing a function[a] together with an environment.[1] 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.[b] Unlike a plain function, a closure 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.
如果能夠直接理解英文的同學可以略過這部分的中文翻譯,要是不願意費腦理解英文的小夥伴跟我一起解讀中文吧!
閉包是一種技術
第一句話英文如下:
In programming languages, a closure, also lexical closureor function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions.
相應的中文翻譯:
閉包也叫做詞法閉包或者函數閉包,是函數優先程式語言中用於實現詞法範圍的名稱綁定技術.
概念性定義解釋後可能還是不太清楚,那麼就用代碼解釋一下什麼是閉包?
「雪之夢技術驛站」: 程式語言千萬種,前端後端和中臺;心有餘而力不足,大眾化 Js 帶上 Go .
Go 實現斐波那契數列生成器這是開篇引言的示例,直接照搬過來,這裡主要說明 Go 支持閉包這種技術而已,所以並不關心具體實現細節.
單元測試用例函數,連續 10 次調用斐波那契數列生成器,輸出斐波那契數列中的前十位數字.
Js 實現斐波那契數列生成器仿照 Go 語言的實現方式寫一個 Js 版本的斐波那契數列生成器,相關代碼如下:
同樣的,仿造測試代碼寫出 Js 版本的測試用例:
不僅僅是 Js 和 Go 這兩種程式語言能夠實現閉包,實際上很多程式語言都能實現閉包,就像是面向對象編程一樣,也不是某種語言專有的技術,唯一的區別可能就是語法細節上略有不同吧,所以記住了: 閉包是一種技術!
閉包存儲了環境
第二句英文如下:
Operationally, a closure is a recordstoring a function[a] together with an environment.
相應的中文翻譯:
在操作上,閉包是將函數[a]與環境一起存儲的記錄。
第一句我們知道了閉包是一種技術,而現在我們有知道了閉包存儲了閉包函數所需要的環境,而環境分為函數運行時所處的內部環境和依賴的外部環境,閉包函數被使用者調用時不會像普通函數那樣丟失環境而是存儲了環境.
如果是普通函數方式打開上述示例的斐波那契數列生成器:
可想而知,這樣肯定是不行的,因為函數內部環境是無法維持的,使用者每次調用 fibonacciWithoutClosure 函數都會重新初始化變量 a,b 的值,因而無法實現累加自增效果.
很顯然,函數內部定義的變量每次運行函數時都會重新初始化,為了避免這種情況,在不改變整體實現思路的前提下,只需要提升變量的作用範圍就能實現斐波那契數列生成器函數:
此時再次運行 10 次斐波那契數列生成器函數,如我們所願生成前 10 位斐波那契數列.
所以說普通函數 fibonacciWithoutClosure 的運行環境要麼是僅僅依賴內部變量維持的獨立環境,每次運行都會重新初始化,無法實現變量的重複利用;要麼是依賴了外部變量維持的具有記憶功能的環境,解決了重新初始化問題的同時引入了新的問題,那就是必須定義作用範圍更大的外部環境,增加了維護成本.
既然函數內的變量無法維持而函數外的變量又需要管理,如果能兩者結合的話,豈不是皆大歡喜,揚長補短?
對的,閉包基本上就是這種實現思路!
斐波那契數列生成器函數 fibonacci 的返回值是匿名函數,而匿名函數的返回值就是斐波那契數字.
如果不考慮函數內部實現細節,整個函數的語義是十分明確的,使用者初始化調用 fibonacci 函數時得到返回值是真正的斐波那契生成器函數,用變量暫存起來,當需要生成斐波那契數字的時候再調用剛才暫存的變量就能真正生成斐波那契數列.
現在我們再好好比較一下這種形式實現的閉包和普通函數的區別?
閉包函數 fibonacci 的內部定義了變量 a,b,最終返回的匿名函數中使用了變量 a,b,使用時間接生成斐波那契數字.普通函數 fibonacciWithoutClosure 的外部定義了變量 a,b,調用該函數直接生成斐波那契數字.閉包函數是延遲計算也就是惰性求值而普通函數是立即計算,兩者的調用方式不一樣.但是如果把視角切換到真正有價值部分,你會發現閉包函數隻不過是普通函數嵌套而已!
只不過 Go 並不支持函數嵌套,只能使用匿名函數來實現函數嵌套的效果,所以上述示例是會直接報錯的喲!
但是某些語言是支持函數嵌套的,比如最常用的 Js 語言就支持函數嵌套,用 Js 重寫上述代碼如下:
斐波那契數列生成器函數是 fibonacciDeduction,該函數內部真正實現生成器功能的卻是 fibonacciGenerator 函數,正是這個函數使用了變量 a,b ,相當於把外部變量打包綁定成運行環境的一部分!
「雪之夢技術驛站」: 閉包並不是某一種語言特有的技術,雖然各個語言的實現細節上有所差異,但並不妨礙整體理解,正如定義的第二句那樣: storing a **function**[a] together with an **environment**.
環境關聯了自由變量
第三句英文如下:
The environment is a mapping associating each free variableof 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
相應的中文翻譯:
環境是一種映射,它將函數的每個自由變量(在本地使用但在封閉範圍內定義的變量)與創建閉包時名稱綁定到的值或引用相關聯。
環境是閉包所處的環境,這裡強調的是外部環境,更確切的說是相對於匿名函數而言的外部變量,像這種被閉包函數使用但是定義在閉包函數外部的變量被稱為自由變量.
「雪之夢技術驛站」: 由於閉包函數內部使用了自由變量,所以閉包內部的也就關聯了自由變量的值或引用,這種綁定關係是創建閉包時確定的,運行時環境也會一直存在並不會發生像普通函數那樣無法維持環境.
自由變量這裡使用了一個比較陌生的概念: 自由變量(在本地使用但在封閉範圍內定義的變量)
很顯然,根據括號裡面的注釋說明我們知道: 所謂的自由變量是相對於閉包函數或者說匿名函數來說的外部變量,由於該變量的定義不受自己控制,所以對閉包函數自己來說就是自由的,並不受閉包函數的約束!
那麼按照這種邏輯繼續延伸猜測的話,匿名函數內部定義的變量豈不是約束變量?對於閉包函數而言的自由變量對於定義函數來說豈不是約束變量?
「雪之夢技術驛站」: 這裡的變量 a,b 相對於函數 fibonacciWithoutClosure 來說,是不是自由變量?或者說全局變量就是自由變量,對嗎?
值或引用
變量 a,b 定義在函數 fibonacci 內部,相對於匿名函數 func() int 來說是自由變量,在匿名函數中直接使用了變量 a,b 並沒有重新複製一份,所以這種形式的環境關聯的自由變量是引用.
再舉個引用關聯的示例,加深一下閉包的環境理解.
上述示例的 countByClosureButWrong 函數內部定義了變量數組 arr ,存儲的是匿名函數而匿名函數使用的是循環變量i .
這裡的循環變量的定義部分是在匿名函數的外部就是所謂的自由變量,變量 i 沒有進行拷貝所以也就是引用關聯.
運行這種閉包函數,最終的輸出結果都是 4 4 4,這是因為閉包的環境關聯的循環變量 i 是引用方式而不是值傳遞方式,所以閉包運行結束後的變量 i 已經是 4.
除了引用傳遞方式還有值傳遞方式,關聯自由變量時拷貝一份到匿名函數,使用者調用閉包函數時就能如願綁定到循環變量.
「雪之夢技術驛站」: 自由變量 i 作為參數傳遞給匿名函數,而 Go 中的參數傳遞只有值傳遞,所以匿名函數使用的變量 n 就可以正確綁定循環變量了,這也就是自由變量的值綁定方式.
「雪之夢技術驛站」: 自由變量通過值傳遞的方式傳遞給閉包函數,實現值綁定環境,正確綁定了循環變量 1 2 3 而不是 4 4 4
訪問被捕獲自由變量
第四句英文如下:
Unlike a plain function, a closure allows the function to access those captured variablesthrough the closure's copies of their values or references, even when the function is invoked outside their scope.
相應的中文翻譯:
與普通函數不同,閉包允許函數通過閉包的值的副本或引用訪問那些被捕獲的變量,即使函數在其作用域之外被調用
閉包函數和普通函數的不同之處在於,閉包提供一種持續訪問被捕獲變量的能力,簡單的理解就是擴大了變量的作用域.
自由變量 a,b 的定義發生在函數 fibonacci 體內,一般而言,變量的作用域也僅限於函數內部,外界是無法訪問該變量的值或引用的.
但是,閉包提供了持續暴露變量的機制,外界突然能夠訪問原本應該私有的變量,實現了全局變量的作用域效果!
「雪之夢技術驛站」: 普通函數想要訪問變量 a,b 的值或引用,定義在函數內部是無法暴露給調用者訪問的,只能提升成全局變量才能實現作用域範圍的擴大.
由此可見,一旦變量被閉包捕獲後,外界使用者是可以訪問這些被捕獲的變量的值或引用的,相當於訪問了私有變量!
怎麼理解閉包
閉包是一種函數式編程中實現名稱綁定的技術,直觀表現為函數嵌套提升變量的作用範圍,使得原本壽命短暫的局部變量獲得長生不死的能力,只要被捕獲到的自由變量一直在使用中,系統就不會回收內存空間!
知乎上關於閉包的眾多回答中,其中有一個回答言簡意賅,特意分享如下:
我叫獨孤求敗,我在一個山洞裡,裡面有世界上最好的劍法,還有最好的武器。我學習了裡面的劍法,拿走了最好的劍。離開了這裡。我來到這個江湖,快意恩仇。但是從來沒有人知道我這把劍的來歷,和我這一身的武功。。。那山洞就是一個閉包,而我,就是那個山洞裡唯一一個可以與外界交匯的地方。這山洞的一切對外人而言就像不存在一樣,只有我才擁有這裡面的寶藏!
這也是閉包定義中最後一句話表達的意思: 山洞是閉包函數,裡面的劍法和武器就是閉包的內部環境,而獨孤求敗劍客則是被捕獲的自由變量,他出生在山洞之外的世界,學成歸來後獨自闖蕩江湖.從此江湖上有了獨孤求敗的傳說和那把劍以及神秘莫測的劍法.
掌握閉包了麼
問題: 請將下述普通函數改寫成閉包函數?
回答: 閉包的錯誤示例以及正確示例
那麼,問題來了,原本普通函數就能實現的需求更改成閉包函數實現後,一不小心就弄錯了,為什麼還需要閉包?
閉包歸納總結
現在再次回顧一下斐波那契數列生成器函數,相信你已經讀懂了吧,有沒有看到閉包的影子呢?
但是,有沒有想過這麼一個問題: 為什麼需要閉包,閉包解決了什麼問題?
閉包不是某一種語言特有的機制,但常出現在函數式編程中,尤其是函數佔據重要地位的程式語言.閉包的直觀表現是函數內部嵌套了函數,並且內部函數訪問了外部變量,從而使得自由變量獲得延長壽命的能力.閉包中使用的自由變量一般有值傳遞和引用傳遞兩種形式,示例中的斐波那契數列生成器利用的是引用而循環變量示例用的是值傳遞.Go 不支持函數嵌套但支持匿名函數,語法層面的差異性掩蓋不了閉包整體的統一性.「雪之夢技術驛站」: 由於篇幅所限,為什麼需要閉包以及閉包的優缺點等知識的相關分析打算另開一篇單獨討論,敬請期待...
相關資料參考
https://www.liaoxuefeng.com/wiki/1022910821149312/1023021250770016https://www.zhihu.com/question/34210214https://lotabout.me/2016/thoughts-of-closure/如果你覺得本文對你有所幫助,歡迎點讚評論和轉發,來一波素質三連吧! 偷偷告訴你,點擊了解更多可以獲得最佳閱讀體驗喲!