目前在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 请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面若有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!