本文簡譯自一篇老外的博客,寫得不錯可惜我翻譯的太爛,簡譯如下。
(key--value常見翻譯為“鍵值對”,我翻譯為索引、值)
在這篇教程里我會介紹Lua中一個重要的概念: metatable(元表),掌握元表可以讓你更有效的
使用Lua。 每一個tabel都可以附加元表, 元表是帶有索引集合的表,它可以改變被附加表的行為。
看下例:
t = {} -- 普通表
mt = {} -- 元表,現在暫時什么也沒有
setmetatable(t, mt) -- 把mt設為t的元表
getmetatable(t) -- 這回返回mt
如你所見 getmetatable
和setmetatable
是主要的函數。 當然我們可以把上面的三行代碼合為:
t = setmetatable({}, {})
setmetatable
返回第一個參數, 因此我們可以使用這個簡短的表達式。現在,我們在元表里放些什
么呢? 元表可以包含任何東西,但是元表通常以"__"(兩個下划線)開頭的索引(當然string類型)
來調用,例如__index和__newindex。 和索引對應的值可以是表或者函數,例如:
t = setmetatable({}, {
__index = function(t, key)
if key == "foo" then
return 0
else
return table[key]
end
end
})
我們給__index索引分配了一個函數,
讓我們來看看這個索引是干啥的。
__index
元表里最常用的索引可能是__index,它可以包含表或函數。
當你通過索引來訪問表, 不管它是什么(例如t[4]
, t.foo
, 和t["foo"]
), 以及並沒有分配索引的值時,
Lua 會先在查找已有的索引,接着查找表的metatable里(如果它有)查找__index
索引。 如果
__index
包含了表, Lua會在__index包含的表里查找索引。
這聽起來很迷糊,讓我們看一個例子。
other = { foo = 3 }
t = setmetatable({}, { __index = other })
t.foo -- 3 ,現在__index包含的表{foo=3}查找
t.bar -- nil ,沒找到
如果__index
包含一個函數,當被它調用時,會把被訪問的表和索引作為參數傳入。從上面的例子來看,
我們可以使用帶有條件語句的索引,以及任意的Lua語句。因此在這種情況下,如果索引和字符串"foo"
相等,我們可以返回0,否則,我們可以查詢表中被使用的索引;當"foo"被使用時,
讓t
作為table
的
別名並返回0。(這句不是太懂,原文為:Therefore,in that example, if the key was equal to
the string "foo" we would return 0, otherwise we look up the table
table with the key that
was used; this makes t
an alias of table
that returns 0 when the key "foo" is used.)
你可能會疑問,怎么把表作為是第一個傳給__index
函數的參數。當你在多個表里使用相同的元表時,
這會很方便,並支持代碼復用和節省電腦資源。我們會在最下面的Vector
類里看到解釋。
--注:下面是我的一個例子
other = function(t,k) if k=="foo" then return 0 end end
t = setmetatable({}, { __index = other })
print(t.foo)
__newindex
下一個是__newindex
, 它和__index類似。
和 __index一樣,它可以包含函數和表。當你給表中不存在
的值賦值時,
Lua會在metatable里查找__newindex,調用順序和
__index
一樣。如果__newindex
是表,
索引和值會設置到指定的表:
other = {}
t = setmetatable({}, { __newindex = other })
t.foo = 3 --t里沒有foo,查看__newindex,並把foo=3傳給了other,並沒有給t里的foo賦值
other.foo – 3 故為3
t.foo – nil 故為 nil
和期望的一樣,__newindex
是函數時,當被調用時會傳遞表、索引、值三個參數。
t = setmetatable({}, {
__newindex = function(t, key, value)
if type(value) == "number" then
rawset(t, key, value * value)
else
rawset(t, key, value)
end
end
})
t.foo = "foo"
t.bar = 4
t.la = 10
t.foo -- "foo"
t.bar -- 16
t.la -- 100
當在t里創建新的索引時
,如果值是number,這個值會平方,否則什么也不做。下面介紹rawget
和rawset。
rawget
和 rawset
有時需要get 和set表的索引,不想使用metatable.你可能回猜想, rawget
允許你得到索引無需__index
,
rawset
允許你設置索引的值無需__newindex
(不,相對傳統元表的方式,這些不會提高速度)。為了避免陷
在無限循環里,你才需要使用它們。 在上面的例子里, t[key] = value * value將再次調用
__newindex
函數,
這讓你的代碼陷入死循環。使用rawset(t, key, value * value)
可以避免。
你可能看到,使用這些函數, 我們必須傳遞參數目標table, key, 當你使用rawset時還有value。
操作符
許多元表的索引是操作符 (如, +
, -
, 等),允許你使用表完成一些操作符運算。例如,我們想要一個表支持
乘法操作符(*
), 我們可以這樣做:
t = setmetatable({ 1, 2, 3 }, {
__mul = function(t, other) ,
new = {}
for i = 1, other do
for _, v in ipairs(t) do table.insert(new, v) end
end
return new
end
})
t = t * 2 -- { 1, 2, 3, 1, 2, 3 }
這允許我們創建一個
使用乘法操作符重復某些次數的新表。你也看的出來, __mul
和乘法相當的索引是,
與__index、
__newindex
不同,操作符索引只能是函數。 它們接受的第一個參數總是目標表, 接着
是右值 (除了一元操作符“-”,即索引
__unm
)。下面是操作符列表:
__add
: 加法(+
)__sub
: 減法(-
)__mul
: 乘法(*
)__div
: 除法(/
)__mod
: 取模(%
)__unm
: 取反(-)
, 一元操作符__concat
: 連接(..
)__eq
: 等於(==
)__lt
: 小於(<
)__le
:小於等於(<=
)
(只有==
, <
, <=
,因為你能通過上面的實現所有操作,事實上==
和<就足夠了
)
__call
接下來是__call
索引, 它允許你把表當函數調用,代碼示例:
t = setmetatable({}, {
__call = function(t, a, b, c, whatever)
return (a + b + c) * whatever
end
})
t(1, 2, 3, 4) –- 24 ,表t在調用時先查找__call,調用里面的函數,t便相當於函數了
和通常一樣在call里的函數,被傳遞了一個目標表,還有一些參數。__call
非常有用,經常用來在表和它
里面的函數之間轉發調用(原文it's used for is forwarding a call on a table to a function inside
that table.)。 kikito的 tween.lua 庫就是個例子tween.start
可以被自身調用(tween
). 另一個例子是
MiddleClass, 類里的new函數可以被類自身調用。
__tostring
最后一個是 __tostring。
如果實現它,那么tostring
可以把表轉化為string, 非常方便類似print的函數
使用。 一般情況下,當你把表轉為string時, 你需要"table: 0x<hex-code-here",但是你可以僅用
__tostring來解決
。示例:
t = setmetatable({ 1, 2, 3 }, {
__tostring = function(t)
sum = 0
for _, v in pairs(t) do sum = sum + v end
return "Sum: " .. sum
end
})
print(t) -- prints out "Sum: 6"
創建一個向量類
下面我們來封裝一個2D 向量類(感謝 hump.vector 的大量代碼)。代碼太長你可以查看gist #1055480,
代碼里有大量的metatable概念,(注意,如果你之前沒接觸面向對象可能會有點難)。
Vector = {}
Vector.__index = Vector
首先聲明了一個Vector
class, 設置了__index
索引指向自身。 這在干啥呢?你會發現我們把所有的元表
放到Vector類里了。
你將看到在Lua里實現OOP (Object-Oriented Programming)的最簡單方式。Vector
表代表類
, 它包含了所有方法,類的實例可以通過Vector.new
(如下) 創建了。
function Vector.new(x, y)
return setmetatable({ x = x or 0, y = y or 0 }, Vector)
end
它創建了一個新的
, 然后把metatable設置到帶有x、
y
屬性的表Vector
類。我們知道Vector
包含了所有的
元方法,特別是 __index。這意味着我們通過新表可以使用所有
Vector
里方法。
另外重要的一行是:
setmetatable(Vector, { __call = function(_, ...) return Vector.new(...) end })
這意味着我們可以創建一個新的Vector
實例通過 Vector.new或者僅
Vector。
最后重要的事,你可能沒注意冒號語法。當我們定義一個帶有冒號的函數時,如下:
function t:method(a, b, c)
-- ...
end
我們真正定義的是這個函數:
function t.method(self, a, b, c)
-- ...
end
這是一個語法糖,幫助我們使用OOP。當調用函數時,我們可以這樣使用冒號語法:
-- these are the same
t:method(1, 2, 3)
t.method(t, 1, 2, 3)
我們如何使用 Vector
類? 示例如下:
a = Vector.new(10, 10)
b = Vector(20, 11)
c = a + b
print(a:len()) -- 14.142135623731
print(a) -- (10, 10)
print(c) -- (30, 21)
print(a < c) -- true
print(a == b) -- false
因為Vector
里有__index,我們可以在實例里使用它的所有方法。
結論
感謝閱讀,我希望你學到了一些東西. 如果你有建議或疑問, 請留下評論comments section,我想聽到你的回復!