前言
最近写了挺长一段时间的Lua,发现Lua这个语言真的是很随意,产生这种感觉的根本原因应该是它把“函数” 作为了“第一类值”,也就是说函数也可以作为变量的“值”,这使得Lua可以随处定义函数,进而改变逻辑的走向,整个流程任你摆布。
虽说把一个函数来回设置方便了许多,但是同样带来了一些不容易发现的问题,如果搞不清定义域和引用关系,常常会导致程序错误,比如最近用Lua写按钮的触发事件时,使用公有函数创建了对应的闭包,一开始感觉table的引用有问题,写了很多中转的代码,最后发现直接就可以使用,浪费了不少时间,最后仔细分析就是闭包最根本的形式,只不过被业务逻辑给干扰了视线,接下来我们一起看看,table和闭包究竟会发生什么关系!
代码测试
table作为函数参数时的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15print("\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)
end1
2
3
4example 1:
1 3
3 5
a 1以上为去掉table中的偶数的代码,直接操作参数
data_tb
就可以对传入的data_table
进行改变,这样的逻辑一般不会出错,接着我们看下接下来需求,直接将表中数据清空。1
2
3
4
5
6
7
8
9
10
11print("\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)
end1
2
3
4
5
6
7example 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
13print("\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
4example 3:
1
2
3这段代码的不同之处就在于变量
count
,这是一个标准的计数器,也是一个标准的闭包,也就是说Lua支持这样的语法,闭包中可以在定义之后一直引用外部的变量,并且在返回函数的整个使用生命周期内都可以引用这个变量,加入外部修改了这个变量,闭包中引用的值也会改变,换句话来说就是闭包这种引用是引用的变量,而不是仅仅保存了一个值。lua中常见的table引用
1
2
3
4
5print("\nexample 4:");
local t1 = {i = 1};
local t2 = t1;
t1.i = 666;
print(t2.i)1
2example 4:
666这个例子类似于前面“过滤掉偶数”的代码,首先定义了表
t1
,然后定义了变量t2
引用了变量t1
,实际上这里t2
不是定义了变量t1
本身,而是引用了t1
的值,也就是引用的是{i=1}
,这里通过t1.i = 666
也可以影响到变量t2
,其实这个例子看不出引用的究竟是变量t1
还是t1
的值,可以接着看下面的例子。1
2
3
4
5print("\nexample 5:");
local t1 = {i = 1};
local t2 = t1;
t1 = {i = 11};
print(t2.i)1
2example 5:
1通过这个例子就很清楚了,前面的部分和上个例子一致,但是后面直接给变量
t1
赋值时并没有改变t2
的值,由此可以看出t1
和t2
已经“分离”了。table引用和闭包结合的例子
1
2
3
4
5
6
7
8
9
10
11
12
13print("\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
2example 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
13print("\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
2example 7:
1如果清楚了上个例子的运行过程,就应该很容易知道这个例子的结果,其中变量
t
的值是在调用函数outer
时确定的,所以后面的赋值tb = {i = 7};
对变量t
的值没有影响。
总结
- lua中操作变量注意值和引用,其实很多语言都有这种区分。
- 注意闭包可以访问外部变量的特性,程序中使用起来非常方便。
- 实际使用过程中往往还夹杂着业务逻辑,要学会挖掘本质问题,这样往往可以看到真正的运行逻辑。