目前在Unity遊戲開發中,比較流行的兩種語言就是Lua和C#。一般的作法是:C#作些核心的功能和接口供Lua調用,Lua主要作些UI模塊和一些業務邏輯。這樣既能在保持必定的遊戲運行效率的同時,又可讓遊戲具有熱更新的功能。不管咱們有意或者無心,其實咱們常常會在Unity遊戲開發中使用到閉包。那麼,馬三今天就要和你們來談談Lua和C#中的閉包,下面首先讓咱們先來談談Lua中的閉包。html
相信,對於常用Javascript的前端開發者來講,閉包這個概念必定不會陌生,在Javascript開發中,一些高級的應用都須要閉包來實現。而對於傳統的C++開發者或者C#開發者來講,閉包這個詞或多或少都會有些玄之又玄的感受。那麼,在開講以前,讓咱們先來了解幾個Lua中基礎知識和概念,這樣有助於咱們理解Lua閉包。前端
詞法定界:當一個函數內嵌套另外一個函數的時候,內函數能夠訪問外部函數的局部變量,這種特徵叫作詞法定界。以下面這段代碼,func2做爲func1的內嵌函數,能夠自由地訪問屬於func1的局部變量i : git
function func1() local i = 100 --upvalue local func2 = function() print(i+1) end i = 101 return func2 end local f = func1() print(f()) --輸出102
第一類值:在Lua中,函數是一個值,它能夠存在於變量中、能夠做爲函數參數,也能夠做爲返回值return。仍是以上面的代碼舉例,咱們將一個內嵌在func1中的函數賦值給局部變量func2,並將func2這個變量在函數結尾return。github
upvalue:內嵌函數能夠訪問外部函數已經建立的局部變量,而這些局部變量則稱爲該內嵌函數的外部局部變量(即upvalue)。在咱們的第一個例子中,func1的局部變量i就是內嵌函數func2的upvalue。數組
好了有了以上的概念之後,咱們也該引入Lua中閉包的概念了。閉包是由函數和與其相關的引用環境組合而成的實體,閉包=函數+引用環境。閉包
在第一個例子中,func1函數返回了一個函數,而這個返回的函數就是閉包的組成部分中的函數;引用環境就是變量i所在的環境。實際上,閉包只是在形式和表現上像函數,但實際上不是函數。咱們都知道,函數就是一些可執行語句的組合體,這些代碼語句在函數被定義後就肯定了,並不會再執行時發生變化,因此函數只有一個實例。而閉包在運行時能夠有多個實例,不一樣的引用環境和相同的函數組合能夠產生不一樣的實例,就比如相同的類代碼,能夠建立不一樣的類實例同樣。函數
用一句比較通俗和不甚嚴謹的話來說:子函數可使用父函數中的局部變量,這種行爲就叫作閉包。這種說法其實就說明了閉包的一種表象,讓咱們從外在形式上,能更好的理解什麼是閉包。post
對於學習C++或者是C#之類的語言入門的朋友,可能對閉包理解起來比較吃力(至少馬三是這樣,一會明白一會糊塗,看了不少文章、寫了不少代碼之後才理解,笨得要命~ o(≧口≦)o)。其實咱們能夠把Lua中的閉包和C++中的類作一下類比。閉包是數據和行爲的結合體,這就比如C++中的類,有一些成員變量(Lua中的upvalue)+成員方法(Lua中的內嵌函數)。這樣就使得閉包具備較好的抽象能力,在某些場合下,咱們須要記住某次調用函數完成之後數據的狀態,就比如C++中的static類型的變量,每次調用完成之後,static類型的變量並不會被清除。使用閉包就能夠很好的完成該功能,好比利用Lua閉包特性實現一個簡單地迭代器,在下面的小節中咱們會介紹到。學習
1.閉包的數據隔離優化
function counter() local i = 0 return function() --匿名函數,閉包 i = i + 1 return i end end counter1 = counter() counter2 = counter() -- counter1,counter2 是創建在同一個函數,同一個局部變量的不一樣實例上面的兩個不一樣的閉包 -- 閉包中的upvalue各自獨立,調用一次counter()就會產生一個新的閉包 print(counter1()) -- 輸出1 print(counter1()) -- 輸出2 print(counter2()) -- 輸出1 print(counter2()) -- 輸出2
上面的代碼中,註釋已經解釋地很詳細了。儘管看起來counter1,counter2是由同一個函數和同一個局部變量建立的閉包。可是其實它們是不一樣實例上面的兩個不一樣的閉包。閉包中的upvalue各自獨立,調用一次counter()就會產生一個新的閉包。有點像工廠函數同樣,每調用一次counter()都會new出來一個新的對象,不一樣的對象之間的數據,固然也就是隔離的了。
2.閉包的數據共享
function shareVar(n) local function func1() print(n) end local function func2() n = n + 10 print(n) end return func1,func2 end local f1,f2 = shareVar(1024) --建立閉包,f1,f2兩個閉包共享同一份upvalue f1() -- 輸出1024 f2() -- 輸出1034 f1() -- 輸出1034 f2() -- 輸出1044
乍一看起來,這個概念和第一個概念矛盾啊,其實他們之間並不矛盾。在Lua中,同一閉包建立的其餘的閉包共享一份upvalue。閉包在建立之時其須要的變量就已經不在堆棧上,而是引用更外層外部函數的局部變量(即upvalue)。在上面的例子中,f1,f2共享同一份upvalue,這是由於f一、f2都是由同一個閉包shareVar(1024)建立的,因此他們引用的upvalue(變量n)實際也是同一個變量,而它們的upvalue引用都會指向同一個地方。說白了就是func1和func2的引用環境是同樣,它們的上下文是同樣的。再類比一下咱們比較熟悉的C++,就比如C++類中有兩個不一樣的成員函數,它們均可以對類中的同一個成員變量進行訪問和修改。這第二點概念尤爲要和第一點概念進行區分,它們很容易混淆。
3.利用閉包實現迭代器功能
--- 利用閉包實現iterator,iterator是一個工廠,每次調用都會產生一個新的閉包,該閉包內部包括了upvalue(t,i,n) --- 所以每調用一次該函數都會產生閉包,那麼該閉包就會根據記錄上一次的狀態,以及返回table中的下一個元素 function iterator(t) local i = 0 local n = #t return function() i = i + 1 if i <= n then return t[i] end end end testTable = {1,2,3,"a","b"} -- while中使用迭代器 iter1 = iterator(testTable) --調用迭代器產生一個閉包 while true do local element = iter1() if nil == element then break; end print(element) end -- for中使用迭代器 for element in iterator(testTable) do --- 這裏的iterator()工廠函數只會被調用一次產生一個閉包函數,後面的每一次迭代都是用該閉包函數,而不是工廠函數 print(element) end
利用閉包咱們能夠很方便地實現一個迭代器,例如上面代碼中的iterator。iterator是一個工廠,每次調用都會產生一個新的閉包,該閉包內部包括了upvalue(t,i,n),所以每調用一次該函數都會產生閉包,那麼該閉包就會根據記錄上一次的狀態,以及返回table中的下一個元素,從而實現了迭代器的功能。須要額外注意的是:迭代器只是一個生成器,他本身自己不帶循環。咱們還須要在循環裏面去調用它才行。
在while循環的那段例子代碼中,咱們首先調用迭代器建立一個閉包,而後不斷地調用它就能夠獲取到表中的下一個元素了,就好像是遊標同樣。而因爲 for ... in ... do 的這種寫法很具備迷惑性,因此在for循環中使用迭代器的話,咱們須要注意:這裏的iterator()工廠函數只會被調用一次產生一個閉包函數,後面的每一次迭代都是用該閉包函數,而不是工廠函數。相信許多朋友此時會和馬三同樣產生一個疑問,爲何在for循環中使用迭代器,iterator()工廠函數只會被調用一次呢?難道不是每次判斷執行條件的時候都去執行一次iterator函數嗎?其實這和Lua語言對for...in...do這種控制結構的內部實現方式有關。for in在本身內部保存三個值:迭代函數、狀態常量、控制變量。for...in 這種寫法實際上是一種語法糖,在《Programming in Lua》中給出的等價代碼是:
do local _f,_s,_var = iter,tab,var while true do local _var,value = _f(_s, _var) if not _var then break end body end end
怎麼樣,for...in 的內部實現代碼和咱們在while中調用Iterator的方式是否是很相似?Iterator(table)函數返回一個匿名函數做爲迭代器,該迭代函數會忽略掉傳給它的參數table和nil,table和控制變量已被保存在迭代函數中,所以將上面的for循環展開後應該是這個樣子:
iter = iterator(testTable) element,value = iter(nil,nil)--忽略參數,value置爲nil if(element) then repeat print(element) element,value = iter(nil,element)--忽略參數 until(not element) end
咱們在上面花了很大的篇幅來介紹Lua的閉包,其實在C#中也是有閉包概念的。因爲咱們已經有了以前的Lua閉包基礎,因此再理解C#中的閉包概念也就不那麼困難了。照例在開講以前咱們仍是先介紹一些C#中的基礎知識與概念,一邊有助於咱們的理解。
變量做用域:在C#裏面,變量做用域有三種,一種是屬於類的,咱們常稱之爲field(字段/屬性);第二種則屬於函數的,咱們一般稱之爲局部變量;還有一種,其實也是屬於函數的,不過它的做用範圍更小,它只屬於函數局部的代碼片斷,這種一樣稱之爲局部變量。這三種變量的生命週期基本均可以用一句話來講明,每一個變量都屬於它所寄存的對象,即變量隨着其寄存對象生而生和消亡。
對應三種做用域咱們能夠這樣說,類裏面的變量是隨着類的實例化而生,同時伴隨着類對象的資源回收而消亡(固然這裏不包括非實例化的static和const對象)。而函數(或代碼片斷)的變量也隨着函數(或代碼片斷)調用開始而生,伴隨函數(或代碼片斷)調用結束而自動由GC釋放,它內部變量生命週期知足先進後出的特性。
那麼,有沒有例外的狀況呢?答案固然是有的,它就是咱們的今天的主角:C#閉包。
委託:委託是一個類,它定義了方法的類型,使得能夠將方法看成另外一個方法的參數來進行傳遞,這種將方法動態地賦給參數的作法,能夠避免在程序中大量使用If-Else(Switch)語句,同時使得程序具備更好的可擴展性。(關於委託的講解,網上已經有不少文章了,這裏再也不贅述,籠統一點你能夠把委託簡單地理解爲函數指針)
閉包其實就是使用的變量已經脫離其做用域,卻因爲和做用域存在上下文關係,從而能夠在當前環境中繼續使用其上文環境中所定義的一種函數對象。(本質上和Lua閉包的概念沒有什麼不一樣,只是換種說法罷了)
首先讓咱們來看下面這一段C#代碼:
public class TCloser { public Func<int> T1() { var n = 999; return () => { Console.WriteLine(n); return n; }; } } class Program { static void Main() { var a = new TCloser(); var b = a.T1(); Console.WriteLine(b()); } }
從上面的代碼咱們不難看到,變量n其實是屬於函數T1的局部變量,它原本的生命週期應該是伴隨着函數T1的調用結束而被釋放掉的,但這裏咱們卻在返回的委託b中仍然能調用它,這裏正是C#閉包的特性。在T1調用返回的匿名委託的代碼片斷中咱們用到了n,而在編譯器看來,這些都是合法的,由於返回的委託b和函數T1存在上下文關係,也就是說匿名委託b是容許使用它所在的函數或者類裏面的局部變量的,因而編譯器經過一系列操做使b中調用的函數T1的局部變量自動閉合,從而使該局部變量知足新的做用範圍。
因此對於C#中的閉包,你就能夠像以前介紹的Lua閉包那樣理解它。因爲返回的匿名函數對象是在函數T1中生成的,所以至關於它是屬於T1的一個屬性。若是你把T1的對象級別往上提高一個層次就很好理解了,這裏就至關於T1是一個類,而返回的匿名對象則是T1的一個屬性,對屬性而言,它能夠調用它所寄存的對象T1的任何其餘屬性或者方法,包括T1寄存的對象TCloser內部的其餘屬性。若是這個匿名函數會被返回給其餘對象調用,那麼編譯器會自動將匿名函數所用到的方法T1中的局部變量的生命週轉期自動提高,並與匿名函數的生命週期相同,這樣就稱之爲閉合。
若是你想了解C#編譯器是如何操做,使得閉包產生的,能夠去反編譯一下C#程序,而後觀察它的IL代碼(如何反編譯並查看IL代碼,馬三已經在《【小白學C#】淺談.NET中的IL代碼》這篇博客中作了詳細的介紹) 。C#的閉包,其實只是編譯器對IL代碼作了一些操做而已,它仍然沒有脫離C#對象生命週期的規則。它將須要修改做用域的變量直接封裝到返回的類中,變成類的一個屬性n,從而保證了變量的生命週期不會隨函數T1調用結束而結束,由於變量n在這裏已經成了返回的類的一個屬性了。
在C#中,閉包其實和類中其餘屬性、方法是同樣的,它們的原則都是下一層能夠任意調用上一層定義的各類設定,但上一層則不具有訪問下一層設定的能力。比如一個類中方法裏能夠自由訪問類中的全部屬性和方法,而閉包又能夠訪問它的上一層即方法中的各類設定。但類不能夠訪問方法的局部變量,同理,方法也不能夠訪問其內部定義的匿名函數所定義的局部變量。在咱們工做中常常會用到的匿名委託、Lamda和LINQ,他們本質上都會使用到閉包這個特性。
不管是在Javascript、Lua仍是C#開發中,閉包的使用至關普遍,也正是因爲閉包和各類語法糖的存在,才使得咱們的代碼更加簡潔,使用更方便。靈活、可靠地使用閉包,能夠爲咱們的程序代碼增光添彩,優化代碼結構,益處多多。總之,閉包是一個好理解而又難理解的東西,咱們應該多寫多練,多參與到各種項目開發中,以提升本身的理解層次。
本篇博客中的示例代碼託管在Github:https://github.com/XINCGer/Unity3DTraining/tree/master/SomeTest/Closure 歡迎fork!
做者:馬三小夥兒
出處:http://www.cnblogs.com/msxh/p/8283865.html 請尊重別人的勞動成果,讓分享成爲一種美德,歡迎轉載。另外,文章在表述和代碼方面若有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!