最近写了挺长一段时间的Lua,发现Lua这个语言真的是很随意,产生这种感觉的根本原因应该是它把“函数” 作为了“第一类值”,也就是说函数也可以作为变量的“值”,这使得Lua可以随处定义函数,进而改变逻辑的走向,整个流程任你摆布。

虽说把一个函数来回设置方便了许多,但是同样带来了一些不容易发现的问题,如果搞不清定义域和引用关系,常常会导致程序错误,比如最近用Lua写按钮的触发事件时,使用公有函数创建了对应的闭包,一开始感觉table的引用有问题,写了很多中转的代码,最后发现直接就可以使用,浪费了不少时间,最后仔细分析就是闭包最根本的形式,只不过被业务逻辑给干扰了视线,接下来我们一起看看,table和闭包究竟会发生什么关系!

代码测试

  1. 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 (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_tbdata_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 testn";
    }

    int main()
    {
    char szContent[32] = "help";

    change_string(szContent);
    cout << szContent << endl;

    return 0;
    }

    分析一下这段代码的输出结果,如果你能知道结果为50lp,那说明你的C++水平已经超过了入门级别,理解了这段代码有助于清楚的理解前两段Lua代码。

  2. 看一个标准闭包实现的计数器

    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支持这样的语法,闭包中可以在定义之后一直引用外部的变量,并且在返回函数的整个使用生命周期内都可以引用这个变量,加入外部修改了这个变量,闭包中引用的值也会改变,换句话来说就是闭包这种引用是引用的变量,而不是仅仅保存了一个值。

  3. 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的值,由此可以看出t1t2已经“分离”了。

  4. 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的值没有影响。

总结

  1. lua中操作变量注意值和引用,其实很多语言都有这种区分。
  2. 注意闭包可以访问外部变量的特性,程序中使用起来非常方便。
  3. 实际使用过程中往往还夹杂着业务逻辑,要学会挖掘本质问题,这样往往可以看到真正的运行逻辑。

测试源码

示例传送门:lua中table引用