一、前言

  目前在Unity游戏开发中,比较流行的两种语言就是Lua和C#。通常的做法是:C#做些核心的功能和接口供Lua调用,Lua主要做些UI模块和一些业务逻辑。这样既能在保持一定的游戏运行效率的同时,又可以让游戏具备热更新的功能。无论我们有意或者无意,其实我们经常会在Unity游戏开发中使用到闭包。那么,马三今天就要和大家来谈谈Lua和C#中的闭包,下面首先让我们先来谈谈Lua中的闭包。

二、Lua中的闭包

  相信,对于经常使用Javascript的前端开发者来说,闭包这个概念一定不会陌生,在Javascript开发中,一些高级的应用都需要闭包来实现。而对于传统的C++开发者或者C#开发者来说,闭包这个词或多或少都会有些玄之又玄的感觉。那么,在开讲之前,让我们先来了解几个Lua中基础知识和概念,这样有助于我们理解Lua闭包。

1.一些前提概念

  词法定界:当一个函数内嵌套另一个函数的时候,内函数可以访问外部函数的局部变量,这种特征叫做词法定界。如下面这段代码,func2作为func1的内嵌函数,可以自由地访问属于func1的局部变量i : 

function func1()
local i = --upvalue
local func2 = function()
print(i+)
end
i =
return func2
end local f = func1()
print(f()) --输出102

  第一类值:在Lua中,函数是一个值,它可以存在于变量中、可以作为函数参数,也可以作为返回值return。还是以上面的代码举例,我们将一个内嵌在func1中的函数赋值给局部变量func2,并将func2这个变量在函数结尾return。

  upvalue:内嵌函数可以访问外部函数已经创建的局部变量,而这些局部变量则称为该内嵌函数的外部局部变量(即upvalue)。在我们的第一个例子中,func1的局部变量i就是内嵌函数func2的upvalue。

2.什么是Lua闭包

  好了有了以上的概念以后,我们也该引入Lua中闭包的概念了。闭包是由函数和与其相关的引用环境组合而成的实体,闭包=函数+引用环境

  在第一个例子中,func1函数返回了一个函数,而这个返回的函数就是闭包的组成部分中的函数;引用环境就是变量i所在的环境。实际上,闭包只是在形式和表现上像函数,但实际上不是函数。我们都知道,函数就是一些可执行语句的组合体,这些代码语句在函数被定义后就确定了,并不会再执行时发生变化,所以函数只有一个实例。而闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例,就好比相同的类代码,可以创建不同的类实例一样。

  用一句比较通俗和不甚严谨的话来讲:子函数可以使用父函数中的局部变量,这种行为就叫做闭包。这种说法其实就说明了闭包的一种表象,让我们从外在形式上,能更好的理解什么是闭包。

  对于学习C++或者是C#之类的语言入门的朋友,可能对闭包理解起来比较吃力(至少马三是这样,一会明白一会糊涂,看了很多文章、写了很多代码以后才理解,笨得要命~ o(≧口≦)o)。其实我们可以把Lua中的闭包和C++中的类做一下类比。闭包是数据和行为的结合体,这就好比C++中的类,有一些成员变量(Lua中的upvalue)+成员方法(Lua中的内嵌函数)。这样就使得闭包具有较好的抽象能力,在某些场合下,我们需要记住某次调用函数完成以后数据的状态,就好比C++中的static类型的变量,每次调用完成以后,static类型的变量并不会被清除。使用闭包就可以很好的完成该功能,比如利用Lua闭包特性实现一个简单地迭代器,在下面的小节中我们会介绍到。

3.典型Lua闭包例子

  1.闭包的数据隔离

function counter()
local i =
return function() --匿名函数,闭包
i = i +
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 +
print(n)
end
return func1,func2
end local f1,f2 = shareVar() --创建闭包,f1,f2两个闭包共享同一份upvalue f1() -- 输出1024
f2() -- 输出1034
f1() -- 输出1034
f2() -- 输出1044

  乍一看起来,这个概念和第一个概念矛盾啊,其实他们之间并不矛盾。在Lua中,同一闭包创建的其他的闭包共享一份upvalue。闭包在创建之时其需要的变量就已经不在堆栈上,而是引用更外层外部函数的局部变量(即upvalue)。在上面的例子中,f1,f2共享同一份upvalue,这是因为f1、f2都是由同一个闭包shareVar(1024)创建的,所以他们引用的upvalue(变量n)实际也是同一个变量,而它们的upvalue引用都会指向同一个地方。说白了就是func1和func2的引用环境是一样,它们的上下文是一样的。再类比一下我们比较熟悉的C++,就好比C++类中有两个不同的成员函数,它们都可以对类中的同一个成员变量进行访问和修改。这第二点概念尤其要和第一点概念进行区分,它们很容易混淆。

  3.利用闭包实现迭代器功能

--- 利用闭包实现iterator,iterator是一个工厂,每次调用都会产生一个新的闭包,该闭包内部包括了upvalue(t,i,n)
--- 因此每调用一次该函数都会产生闭包,那么该闭包就会根据记录上一次的状态,以及返回table中的下一个元素
function iterator(t)
local i =
local n = #t
return function()
i = i +
if i <= n then
return t[i]
end
end
end testTable = {,,,"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

三、C#中的闭包

  我们在上面花了很大的篇幅来介绍Lua的闭包,其实在C#中也是有闭包概念的。由于我们已经有了之前的Lua闭包基础,所以再理解C#中的闭包概念也就不那么困难了。照例在开讲之前我们还是先介绍一些C#中的基础知识与概念,一边有助于我们的理解。

1.一些前提概念

  变量作用域:在C#里面,变量作用域有三种,一种是属于类的,我们常称之为field(字段/属性);第二种则属于函数的,我们通常称之为局部变量;还有一种,其实也是属于函数的,不过它的作用范围更小,它只属于函数局部的代码片段,这种同样称之为局部变量。这三种变量的生命周期基本都可以用一句话来说明,每个变量都属于它所寄存的对象,即变量随着其寄存对象生而生和消亡。

  对应三种作用域我们可以这样说,类里面的变量是随着类的实例化而生,同时伴随着类对象的资源回收而消亡(当然这里不包括非实例化的static和const对象)。而函数(或代码片段)的变量也随着函数(或代码片段)调用开始而生,伴随函数(或代码片段)调用结束而自动由GC释放,它内部变量生命周期满足先进后出的特性。

  那么,有没有例外的情况呢?答案当然是有的,它就是我们的今天的主角:C#闭包。

  委托:委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。(关于委托的讲解,网上已经有很多文章了,这里不再赘述,笼统一点你可以把委托简单地理解为函数指针)

2.什么是C#闭包?

  闭包其实就是使用的变量已经脱离其作用域,却由于和作用域存在上下文关系,从而可以在当前环境中继续使用其上文环境中所定义的一种函数对象。(本质上和Lua闭包的概念没有什么不同,只是换种说法罢了)

3.典型的C#闭包例子

  首先让我们来看下面这一段C#代码:

public class TCloser
{
public Func<int> T1()
{
var n = ;
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 
请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!

【Unity游戏开发】浅谈Lua和C#中的闭包的更多相关文章

  1. 【Unity游戏开发】你真的了解UGUI中的IPointerClickHandler吗?

    一.引子 马三在最近的开发工作中遇到了一个比较有意思的bug:“TableViewCell上面的某些自定义UI组件不能响应点击事件,并且它的父容器TableView也不能响应点击事件,但是TableV ...

  2. 【Unity游戏开发】用C#和Lua实现Unity中的事件分发机制EventDispatcher

    一.简介 最近马三换了一家大公司工作,公司制度规范了一些,因此平时的业余时间多了不少.但是人却懒了下来,最近这一个月都没怎么研究新技术,博客写得也是拖拖拉拉,周六周天就躺尸在家看帖子.看小说,要么就是 ...

  3. 关于Unity游戏开发方向找工作方面的一些个人看法

     这是个老生常谈,却又是谁绕不过去的话题,而对于每个人来说,所遇到的情况又不尽相同,别人的求职方式和路线不一定适合你,即使是背景很相似的两个人,有时候机遇也很重要. 我本人的工作经验只有一年,就业方式 ...

  4. 【游戏开发】在Lua中实现面向对象特性——模拟类、继承、多态

    一.简介 Lua是一门非常强大.非常灵活的脚本语言,自它从发明以来,无数的游戏使用了Lua作为开发语言.但是作为一款脚本语言,Lua也有着自己的不足,那就是它本身并没有提供面向对象的特性,而游戏开发是 ...

  5. 【Unity游戏开发】记一次解决 LuaFunction has been disposed 的bug的过程

    一.引子 RT,本篇博客记录的是马三的一次解决 LuaFunction has been disposed 的bug的全过程,事情还要从马三的自研框架 ColaFrameWork 说起.最近,马三在业 ...

  6. 【转载】【游戏开发】在Lua中实现面向对象特性——模拟类、继承、多态

    [游戏开发]在Lua中实现面向对象特性——模拟类.继承.多态   阅读目录 一.简介 二.前提知识 三.Lua中实现类.继承.多态 四.总结 回到顶部 一.简介 Lua是一门非常强大.非常灵活的脚本语 ...

  7. 喵的Unity游戏开发之路 - 游泳

    原文: https://mp.weixin.qq.com/s/-ERFNB1GRZ6UAkHOhP9UQw 很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学.为此我们精选了一套国外优秀 ...

  8. C# Unity游戏开发——Excel中的数据是如何到游戏中的 (二)

    本帖是延续的:C# Unity游戏开发——Excel中的数据是如何到游戏中的 (一) 上个帖子主要是讲了如何读取Excel,本帖主要是讲述读取的Excel数据是如何序列化成二进制的,考虑到现在在手游中 ...

  9. C# Unity游戏开发——Excel中的数据是如何到游戏中的 (三)

    本帖是延续的:C# Unity游戏开发——Excel中的数据是如何到游戏中的 (二) 前几天有点事情所以没有继续更新,今天我们接着说.上个帖子中我们看到已经把Excel数据生成了.bin的文件,不过其 ...

随机推荐

  1. 排序算法入门之堆排序(Java实现)

    堆排序 在学习了二叉堆(优先队列)以后,我们来看看堆排序.堆排序总的运行时间为O(NlonN). 堆的概念 堆是以数组作为存储结构. 可以看出,它们满足以下规律: 设当前元素在数组中以R[i]表示,那 ...

  2. iframe局部刷新的二种实现方法

    需求描述: 当页面有一部分是不变的或整个页面的图片很多时,可以考虑使用局部刷新,以提高整体的下载速度与用户体验.   1,iframe实现局部刷新的方法一 复制代码代码示例: <script t ...

  3. python抽象类+抽象方法实现接口(interface)

    #python没有类似于java和C#的接口类(interface),需要使用抽象类 和抽象方法来实现接口功能 #!/usr/bin/env python#_*_ coding:utf-8 _*_ f ...

  4. Mybatis 系列1

    第一篇教程, 就先简单地写个demo, 一起来认识一下mybatis吧. 为了方便,我使用了maven, 至于maven怎么使用, 我就不做介绍了.没用过maven的, 也不影响阅读. 一.Mybat ...

  5. 如何使你的Ajax应用内容可让搜索引擎爬行

    This document outlines the steps that are necessary in order to make your AJAX application crawlable ...

  6. iphone连接电脑itunes之后 C盘突然小很多被占了很多空间

    很有可能是你的iTunes开启了自动备份,把iphone上的数据都备份到了电脑上,而默认目录就是在C盘.我们可以找到并删除它,换C盘一个清白. 我的路径参考如下: C:\Users\scc\AppDa ...

  7. 编程之美2.18 数组分割 原创解O(nlogn)的时间复杂度求解:

    题目:有一个无序.元素个数为2n的正整数组,要求:如何能把这个数组分割为元素个数为n的两个数组,并使两个子数组的和最接近? 1 1 2 -> 1 1 vs  2 看题时,解法的时间复杂度一般都大 ...

  8. nginx+php+mysql+wordpress搭建简单站点 安装及配置过程

    环境 阿里云ECS云服务器 CPU:1核 内存:2G 操作系统:Centos 7.3 x64 地域:华北 2(华北 2 可用区 A) 系统盘:40G 安装及配置 主要使用 nginx . php 和 ...

  9. Ubuntu命令操作

    1../ 当前路径2.ls 列举当前路径下的所有文件及文件夹 默认情况不显示隐藏文件 ls -a 显示隐藏文件 ls -lah h是文件大小 l是显示文件3.cd 跳转路径4.pwd 查看当前所在路径 ...

  10. Python_NAT

    sockMiddle_server.py import sys import socket import threading #回复消息,原样返回 def replyMessage(conn): wh ...