前言
最近写了挺长一段时间的Lua,发现Lua这个语言真的是很随意,产生这种感觉的根本原因应该是它把“函数” 作为了“第一类值”,也就是说函数也可以作为变量的“值”,这使得Lua可以随处定义函数,进而改变逻辑的走向,整个流程任你摆布。
虽说把一个函数来回设置方便了许多,但是同样带来了一些不容易发现的问题,如果搞不清定义域和引用关系,常常会导致程序错误,比如最近用Lua写按钮的触发事件时,使用公有函数创建了对应的闭包,一开始感觉table的引用有问题,写了很多中转的代码,最后发现直接就可以使用,浪费了不少时间,最后仔细分析就是闭包最根本的形式,只不过被业务逻辑给干扰了视线,接下来我们一起看看,table和闭包究竟会发生什么关系!
代码测试
- table作为函数参数时的操作 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15- print("\nexample 1:"); 
 data_table = {a = 1, b = 2, 3, 4, 5, 6};
 function filter(data_tb)
 for k,v in pairs(data_tb) do
 if v % 2 == 0 then
 data_tb[k] = nil;
 end
 end
 end
 -- 过滤掉偶数
 filter(data_table);
 for k,v in pairs(data_table) do
 print(k,v)
 end- 1 
 2
 3
 4- example 1: 
 1 3
 3 5
 a 1- 以上为去掉table中的偶数的代码,直接操作参数 - data_tb就可以对传入的- data_table进行改变,这样的逻辑一般不会出错,接着我们看下接下来需求,直接将表中数据清空。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- print("\nexample 2:"); 
 data_table = {a = 1, b = 2, 3, 4, 5, 6};
 function destroy(data_tb)
 data_tb = {};
 end
 -- 销毁整个表
 destroy(data_table);
 for k,v in pairs(data_table) do
 print(k,v)
 end- 1 
 2
 3
 4
 5
 6
 7- example 2: 
 1 3
 2 4
 3 5
 4 6
 b 2
 a 1- 看到这次的输出可能有些人就感到奇怪了,怎么上个例子改变元素可以,而这里直接给变量 - data_tb赋值,变成空表怎么不行了?这是因为- data_tb是对变量- data_table的整体引用,所以可以通过- data_tb来改变变量- data_table内部的值,但是当执行- data_tb = {};代码时表示- data_tb不再引用- data_table,而去引用- {}了,也就是- data_tb和- data_table脱离了关系,这一点可以类比C++代码:- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 using namespace std;
 void change_string(char* pStr)
 {
 pStr[0] = '5';
 pStr[1] = '0';
 pStr = "only test\n";
 }
 int main()
 {
 char szContent[32] = "help";
 
 change_string(szContent);
 cout << szContent << endl;
 return 0;
 }- 分析一下这段代码的输出结果,如果你能知道结果为 - 50lp,那说明你的C++水平已经超过了入门级别,理解了这段代码有助于清楚的理解前两段Lua代码。
- 看一个标准闭包实现的计数器 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13- print("\nexample 3:"); 
 function counter()
 local count = 0;
 return function()
 count = count + 1;
 return count;
 end
 end
 func = counter();
 print(func());
 print(func());
 print(func());- 1 
 2
 3
 4- example 3: 
 1
 2
 3- 这段代码的不同之处就在于变量 - count,这是一个标准的计数器,也是一个标准的闭包,也就是说Lua支持这样的语法,闭包中可以在定义之后一直引用外部的变量,并且在返回函数的整个使用生命周期内都可以引用这个变量,加入外部修改了这个变量,闭包中引用的值也会改变,换句话来说就是闭包这种引用是引用的变量,而不是仅仅保存了一个值。
- lua中常见的table引用 - 1 
 2
 3
 4
 5- print("\nexample 4:"); 
 local t1 = {i = 1};
 local t2 = t1;
 t1.i = 666;
 print(t2.i)- 1 
 2- example 4: 
 666- 这个例子类似于前面“过滤掉偶数”的代码,首先定义了表 - t1,然后定义了变量- t2引用了变量- t1,实际上这里- t2不是定义了变量- t1本身,而是引用了- t1的值,也就是引用的是- {i=1},这里通过- t1.i = 666也可以影响到变量- t2,其实这个例子看不出引用的究竟是变量- t1还是- t1的值,可以接着看下面的例子。- 1 
 2
 3
 4
 5- print("\nexample 5:"); 
 local t1 = {i = 1};
 local t2 = t1;
 t1 = {i = 11};
 print(t2.i)- 1 
 2- example 5: 
 1- 通过这个例子就很清楚了,前面的部分和上个例子一致,但是后面直接给变量 - t1赋值时并没有改变- t2的值,由此可以看出- t1和- t2已经“分离”了。
- table引用和闭包结合的例子 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13- print("\nexample 6:"); 
 local tb = {i= 1};
 function outer()
 return function()
 local t = tb;
 print(t.i);
 end
 end
 local show = outer();
 tb = {i = 6};
 show();- 1 
 2- example 6: 
 6- 这个例子应该会有猜错结果的人,我自己就是在类似的代码中搞糊涂的,一种想法是函数 - outer定义的时候变量- t的值已经定义了,还有一种就是认为在返回函数- show的时候变量- t的值会定义,但是这两种想法都是错误的,实际上是调用函数- show的时候才给- t赋值,这时变量- t引用的是拥有最新值的- tb,所以- t.i的值是6,如果你猜对了这个例子的结果,接下来看看下面的代码。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13- print("\nexample 7:"); 
 local tb = {i= 1};
 function outer()
 local t = tb;
 return function()
 print(t.i);
 end
 end
 local show = outer();
 tb = {i = 7};
 show();- 1 
 2- example 7: 
 1- 如果清楚了上个例子的运行过程,就应该很容易知道这个例子的结果,其中变量 - t的值是在调用函数- outer时确定的,所以后面的赋值- tb = {i = 7};对变量- t的值没有影响。
总结
- lua中操作变量注意值和引用,其实很多语言都有这种区分。
- 注意闭包可以访问外部变量的特性,程序中使用起来非常方便。
- 实际使用过程中往往还夹杂着业务逻辑,要学会挖掘本质问题,这样往往可以看到真正的运行逻辑。
