lua 中神奇的表(table)

lua 中神奇的表(table)

來自專欄 Linux15 人贊了文章

最近在嘗試配置 awesome WM,因此粗略地學習了一下 lua 。 在學習過程中,我完全被 表((表))在 lua 中的應用所鎮住了。

表在 lua 中真的是無處不在:首先,它可以作為字典和數組來用;此外,它還可以被用於設置閉包環境、模塊;甚至可以用來模擬對象和類。

字典

表最基礎的作用就是當成字典來用。 它的鍵可以是除了 nil 之外的任何類型的值。

t={}t[{}] = "table" -- key 可以是表t[1] = "int" -- key 可以是整數t[1.1] = "double" -- key 可以是小數t[function () end] = "function" -- key 可以是函數t[true] = "Boolean" -- key 可以是布爾值t["abc"] = "String" -- key 可以是字元串t[io.stdout] = "userdata" -- key 可以是userdatat[coroutine.create(function () end)] = "Thread" -- key可以是thread

當把表當成字典來用時,可以使用 pairs 函數來進行遍歷。

for k,v in pairs(t) do print(k,"->",v)end

運行結果為:

1 -> int1.1 -> doublethread: 0x220bb08 -> Threadtable: 0x220b670 -> tableabc -> Stringfile (0x7f34a81ef5c0) -> userdatafunction: 0x220b340 -> functiontrue -> Boolean

從結果中你還可以發現,使用 pairs 進行遍歷時的順序是隨機的,事實上相同的語句執行多次得到的結果是不一樣的。

表 中的鍵最常見的兩種類型就是整數型和字元串類型。 當鍵為字元串時,表 可以當成結構體來用。同時形如 t["field"] 這種形式的寫法可以簡寫成 t.field 這種形式。

數組

當鍵為整數時,表 就可以當成數組來用。而且這個數組是一個 索引從 1 開始 、沒有固定長度、可以根據需要自動增長的數組。

a = {}for i=0,5 do -- 注意,這裡故意寫成了i從0開始 a[i] = 0end

當將表當成數組來用時,可以通過長度操作符 # 來獲取數組的長度:

print(#a)

結果為:

5

你會發現, lua 認為數組 a 中只有 5 個元素,到底是哪 5 個元素呢?我們可以使用使用 ipairs 對數組進行遍歷:

for i,v in ipairs(a) do print(i,v)end

結果為:

1 02 03 04 05 0

從結果中你會發現 a 的 0 號索引並不認為是數組中的一個元素,從而也驗證了 lua 中的數組是從 1 開始索引的

另外,將表當成數組來用時,一定要注意索引不連貫的情況,這種情況下 # 計算長度時會變得很詭異。

a = {}for i=1,5 do a[i] = 0enda[8] = 0 -- 雖然索引不連貫,但長度是以最大索引為準print(#a)a[100] = 0 -- 索引不連貫,而且長度不再以最大索引為準了print(#a)

結果為:

88

而使用 ipairs 對數組進行遍歷時,只會從 1 遍歷到索引中斷處。

for i,v in ipairs(a) do print(i,v)end

結果為:

1 02 03 04 05 0

環境(命名空間)

lua 將所有的全局變數/局部變數保存在一個常規表中,這個表一般被稱為全局或者某個函數(閉包)的環境。

為了方便,lua 在創建最初的全局環境時,使用全局變數 _G 來引用這個全局環境。因此,在未手工設置環境的情況下,可以使用 -G[varname] 來存取全局變數的值。

for k,v in pairs(_G) do print(k,"->",v)endrawequal -> function: 0x41c2a0require -> function: 0x1ea4e70_VERSION -> Lua 5.3debug -> table: 0x1ea8ad0string -> table: 0x1ea74b0xpcall -> function: 0x41c720select -> function: 0x41bea0package -> table: 0x1ea4820assert -> function: 0x41cc50pcall -> function: 0x41cd10next -> function: 0x41c450tostring -> function: 0x41be70_G -> table: 0x1ea2b80coroutine -> table: 0x1ea4ee0unpack -> function: 0x424fa0loadstring -> function: 0x41ca00setmetatable -> function: 0x41c7e0rawlen -> function: 0x41c250bit32 -> table: 0x1ea8fc0utf8 -> table: 0x1ea8650math -> table: 0x1ea7770collectgarbage -> function: 0x41c650rawset -> function: 0x41c1b0os -> table: 0x1ea6840pairs -> function: 0x41c950arg -> table: 0x1ea9450table -> table: 0x1ea5130tonumber -> function: 0x41bf40io -> table: 0x1ea5430loadfile -> function: 0x41cb10error -> function: 0x41c5c0load -> function: 0x41ca00print -> function: 0x41c2e0dofile -> function: 0x41cbd0rawget -> function: 0x41c200type -> function: 0x41be10getmetatable -> function: 0x41cb80module -> function: 0x1ea4e00ipairs -> function: 0x41c970

從 lua 5.2 開始,可以通過修改 _ENV 這個值(lua 5.1 中的 setfenv 從 5.2 開始被廢除)來設置某個函數的環境,從而讓這個函數中的執行語句在一個新的環境中查找全局變數的值。

a=1 -- 全局變數中a=1local env={a=10,print=_G.print} -- 新環境中a=10,並且確保能訪問到全局的print函數function f1() local _ENV=env print("in f1:a=",a) a=a*10 -- 修改的是新環境中的a值endf1()print("globally:a=",a)print("env.a=",env.a)in f1:a= 10globally:a= 1env.a= 100

另外,新創建的閉包都繼承了創建它的函數的環境。

模塊

lua 中的模塊也是通過返回一個表來供模塊使用者來使用的。 這個表中包含的是模塊中所導出的所有東西,包括函數和常量。

定義模塊的一般模板為:

module(模塊名, package.seeall)

其中 module(模塊名) 的作用類似於:

local modname = 模塊名local M = {} -- M即為存放模塊所有函數及常數的table_G[modname] = Mpackage.loaded[modname] = Msetmetatable(M,{__index=_G}) -- package.seeall可以使全局環境_G對當前環境可見local _ENV = M -- 設置當前的運行環境為 M,這樣後續所有代碼都不需要限定模塊名了,所定義的所有函數自動變成M的成員<函數定義以及常量定義>return M -- module函數會幫你返回module table,而無需手工返回

對象

lua 中之所以可以把表當成對象來用是因為:

  1. 函數在 lua 中是一類值,你可以直接存取表中的函數值。 這使得一個表既可以有自己的狀態,也可以有自己的行為:

    Account = {balance = 0}

    function Account.withdraw(v)

    Account.balance = Account.balance - v

    end
  2. lua 支持閉包,這個特性可以用來模擬對象的私有成員變數:

    function new_account(b)

    local balance = b

    return {withdraw = function (v) balance = balance -v end,

    get_balance = function () return balance end

    }

    end

    a1 = new_account(1000)

    a1.withdraw(10)

    print(a1.get_balance())

    990

不過,上面第一種定義對象的方法有一個缺陷,那就是方法與 Account 這個名稱綁定死了。 也就是說,這個對象的名稱必須為 Accout 否則就會出錯。

a = AccountAccount = nila.withdraw(10) -- 會報錯,因為Accout.balance不再存在

為了解決這個問題,我們可以給 withdraw 方法多一個參數用於指向對象本身。

Account = {balance=100}function Account.withdraw(self,v) self.balance = self.balance - venda = AccountAccount = nila.withdraw(a,10) -- 沒問題,這個時候 self 指向的是a,因此會去尋找 a.balanceprint(a.balance)90

不過由於第一個參數 self 幾乎總是指向調用方法的對象本身,因此 lua 提供了一種語法糖形式 object:method(...) 用於隱藏 self 參數的定義及傳遞。這裡冒號的作用有兩個,其在定義函數時往函數中地一個參數的位置添加一個額外的隱藏參數 sef, 而在調用時傳遞一個額外的隱藏參數 self 到地一個參數位置。 即 function object:method(v) end 等價於 function object.method(self,v) end, object:method(v) 等價於 object.method(object,v)

當涉及到類和繼承時,就要用到元表和元方法了。事實上,對於 lua 來說,對象和類並不存在一個嚴格的劃分。

當一個對象被另一個表的 __index 元方法所引用時,表就能引用該對象中所定義的方法,因此也就可以理解為對象變成了表的類。

類定義的一般模板為:

function 類名:new(o) o = o or {} setmetatable(o,{__index = self}) return oend

或者:

function 類名:new(o) o = o or {} setmetatable(o,self) self.__index = self return oend

相比之下,第二種寫法可以多省略一個表。

另外有一點我覺得有必要說明的就是 lua 中的元方法是在元表中定義的,而不是對象本身定義的,這一點跟其他面向對象的語言比較不同。


推薦閱讀:

想學習一個優秀解釋器源碼,有推薦的嗎?Lua 怎麼樣?
有哪些比較方便好用、界面美觀的Lua項目工具(高端Lua編輯器,或者叫Lua IDE)?
lua中數值for循環的理解
用 Nginx + Lua(OpenResty) 開發高性能 Web 應用

TAG:Lua | 遊戲開發 | 數據結構 |