lua元表


本文簡譯自一篇老外的博客,寫得不錯可惜我翻譯的太爛,簡譯如下。

(key--value常見翻譯為“鍵值對”,我翻譯為索引、值)

在這篇教程里我會介紹Lua中一個重要的概念: metatable(元表),掌握元表可以讓你更有效的

使用Lua。 每一個tabel都可以附加元表, 元表是帶有索引集合的表,它可以改變被附加表的行為。

看下例:

t = {} -- 普通表
mt = {} -- 元表,現在暫時什么也沒有
setmetatable(t, mt) -- 把mt設為t的元表
getmetatable(t) -- 這回返回mt

如你所見 getmetatablesetmetatable 是主要的函數。 當然我們可以把上面的三行代碼合為:

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,這個值會平方,否則什么也不做。下面介紹rawgetrawset。

 

rawgetrawset

有時需要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.)。 kikitotween.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

它創建了一個新的帶有x、y 屬性的表, 然后把metatable設置到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,我想聽到你的回復!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM