1. 程式人生 > >Lua中關於table物件引用傳遞的注意事項

Lua中關於table物件引用傳遞的注意事項

內容導航

前言

最近寫了挺長一段時間的Lua,發現Lua這個語言真的是很隨意,產生這種感覺的根本原因應該是它把“函式” 作為了“第一類值”,也就是說函式也可以作為變數的“值”,這使得Lua可以隨處定義函式,進而改變邏輯的走向,整個流程任你擺佈。

雖說把一個函式來回設定方便了許多,但是同樣帶來了一些不容易發現的問題,如果搞不清定義域和引用關係,常常會導致程式錯誤,比如最近用Lua寫按鈕的觸發事件時,使用公有函式建立了對應的閉包,一開始感覺table的引用有問題,寫了很多中轉的程式碼,最後發現直接就可以使用,浪費了不少時間,最後仔細分析就是閉包最根本的形式,只不過被業務邏輯給干擾了視線,接下來我們一起看看,table和閉包究竟會發生什麼關係!

程式碼測試

  1. table作為函式引數時的操作
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
example 1:
1   3
3   5
a   1

以上為去掉table中的偶數的程式碼,直接操作引數data_tb就可以對傳入的data_table進行改變,這樣的邏輯一般不會出錯,接著我們看下接下來需求,直接將表中資料清空。

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
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++程式碼:

#include <iostream>
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. 看一個標準閉包實現的計數器
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());
example 3:
1
2
3

這段程式碼的不同之處就在於變數count,這是一個標準的計數器,也是一個標準的閉包,也就是說Lua支援這樣的語法,閉包中可以在定義之後一直引用外部的變數,並且在返回函式的整個使用生命週期內都可以引用這個變數,加入外部修改了這個變數,閉包中引用的值也會改變,換句話來說就是閉包這種引用是引用的變數,而不是僅僅儲存了一個值。

  1. lua中常見的table引用
print("\nexample 4:");
local t1 = {i = 1};
local t2 = t1;
t1.i = 666;
print(t2.i)
example 4:
666

這個例子類似於前面“過濾掉偶數”的程式碼,首先定義了表t1,然後定義了變數t2引用了變數t1,實際上這裡t2不是定義了變數t1本身,而是引用了t1的值,也就是引用的是{i=1},這裡通過t1.i = 666也可以影響到變數t2,其實這個例子看不出引用的究竟是變數t1還是t1的值,可以接著看下面的例子。

print("\nexample 5:");
local t1 = {i = 1};
local t2 = t1;
t1 = {i = 11};
print(t2.i)
example 5:
1

通過這個例子就很清楚了,前面的部分和上個例子一致,但是後面直接給變數t1賦值時並沒有改變t2的值,由此可以看出t1t2已經“分離”了。

  1. table引用和閉包結合的例子
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();
example 6:
6

這個例子應該會有猜錯結果的人,我自己就是在類似的程式碼中搞糊塗的,一種想法是函式outer定義的時候變數t的值已經定義了,還有一種就是認為在返回函式show的時候變數t的值會定義,但是這兩種想法都是錯誤的,實際上是呼叫函式show的時候才給t賦值,這時變數t引用的是擁有最新值的tb,所以t.i的值是6,如果你猜對了這個例子的結果,接下來看看下面的程式碼。

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();
example 7:
1

如果清楚了上個例子的執行過程,就應該很容易知道這個例子的結果,其中變數t的值是在呼叫函式outer時確定的,所以後面的賦值tb = {i = 7};對變數t的值沒有影響。

總結

  1. lua中操作變數注意值和引用,其實很多語言都有這種區分。
  2. 注意閉包可以訪問外部變數的特性,程式中使用起來非常方便。
  3. 實際使用過程中往往還夾雜著業務邏輯,要學會挖掘本質問題,這樣往往可以看到真正的執行邏輯。

測試原始碼