Lua 類別和元表

lua-users home
wiki

此頁面說明如何使用元表在 Lua 中實作類別。以下範例同時適用於 Lua 5.0 和 5.1。

Lua 已從應用程式延伸語言轉變成極具彈性的指令碼語言。Lua 5.0 不像 Java 或 Ruby 是個物件導向語言。相反地,Lua 讓你可以隨意實作類別。這是一大優點,也是一大缺點。超級使用者喜歡這種自由度,但新手有時會感到困惑。

元表是 Lua 為資料表加入魔法的方式。假設 t 是一個標準的資料表,如下所示

Lua 5.0.3  Copyright (C) 1994-2006 Tecgraf, PUC-Rio
> t = { 11, 22, 33, you='one', me='two' }
> table.foreach(t,print)
1       11
2       22
3       33
me      two
you     one
> 
> = t[2]
22
> = t.me
two
> = t.fred
nil

使用有效索引對資料表建立索引會傳回儲存在該索引的值。

使用未定義索引對資料表建立索引會傳回 nil。如果加上點魔法,我們可以嘗試建立索引到另一個資料表,而不是傳回 nil。我們甚至可以提供自己的函式來處理未定義的索引。

這些用於自訂特定 Lua 行為的函式在 Lua 早期版本稱為 *後備解決方案*。在 Lua 4.0 中,它們稱為 *標籤方法*。現在在 Lua 5.0 中(很大程度要感謝 Edgar Toernig),這些函式稱為 *後設方法*,而且儲存在稱為 *元表* 的資料表中。

我們可以自訂的行為有特殊名稱,而且會稱為 *事件*。將新索引加入資料表稱為 *newindex* 事件。嘗試從資料表讀取未定義的索引稱為 *index* 事件。

要查看存取未定義索引時發生什麼事,讓我們列印傳遞給用於 *index* 事件的後設方法的引數。

Lua 5.0.3  Copyright (C) 1994-2006 Tecgraf, PUC-Rio
> t = { 11, 22, 33, you='one', me='two' }
> mt = { __index = print }
> = t.you 
one
> = t.fred
nil
> setmetatable(t, mt)
> x = t.fred
table: 0x8075e80        fred
> = x
nil
> = t
table: 0x8075e80
> 

請注意第一個引數是資料表 t,而第二個引數是索引 fred

如果對 *newindex* 事件執行相同的操作,我們會看到有第三個引數,也就是儲存在索引中的新值。

Lua 5.0.3  Copyright (C) 1994-2006 Tecgraf, PUC-Rio
> t = { 11, 22, 33, you='one', me='two' }
> mt = { __newindex = print }
> setmetatable(t, mt)
> t[4] = 'rat'
table: 0x8075e80        4       rat
> 

如前所述,我們可以指定資料表而不是函式,而且會存取該資料表。

Lua 5.0.3  Copyright (C) 1994-2006 Tecgraf, PUC-Rio
> t = { 11, 22, 33, you='one', me='two' }
> s = { }
> mt = { __newindex = s, __index = _G }
> setmetatable(t, mt)
> = t.you
one
> x = 'wow'
> = t.x
wow
> t[5] = 99
> table.foreach(s, print)
5       99
> 

以下說明如何實作向量類別。我們有一個方法資料表和一個元表。每個物件都有另外一個資料表。所有物件共享同一個方法資料表和同一個元表。

請記住 v1:mag() 就像是 v1.mag(v1),因此 Lua 會嘗試在 v1 中查詢 mag,這將會觸發 index 事件,然後在資料表 Vector 中查詢 mag

Lua 5.0.3  Copyright (C) 1994-2006 Tecgraf, PUC-Rio
> Vector = {}
> Vector_mt = { __index = Vector }
> 
> function Vector:new(x,y)
>>   return setmetatable( {x=x, y=y}, Vector_mt)
>> end
> 
> function Vector:mag()
>>   return math.sqrt(self:dot(self))
>> end
> 
> function Vector:dot(v)
>>   return self.x * v.x + self.y * v.y
>> end
> 
> v1 = Vector:new(3,4)
> table.foreach(v1,print)
y       4
x       3
> = v1:mag()
5
> v2 = Vector:new(2,1)
> = v2:dot(v1)
10
> 
> = Vector
table: 0x8076028
> table.foreach(Vector,print)
mag     function: 0x8078008
dot     function: 0x8078b58
new     function: 0x80773e8
> = v1, v2
table: 0x8079110        table: 0x8079a80
> = Vector_mt, getmetatable(v1), getmetatable(v2)
table: 0x80763b8        table: 0x80763b8        table: 0x80763b8
> table.foreach(Vector_mt,print)
__index table: 0x8076028
> 

如果你想要一個預設建構函式和一個複製建構函式,你可以建立名為 Class.lua 的檔案,如下所示

function Class(members)
  members = members or {}
  local mt = {
    __metatable = members;
    __index     = members;
  }
  local function new(_, init)
    return setmetatable(init or {}, mt)
  end
  local function copy(obj, ...)
    local newobj = obj:new(unpack(arg))
    for n,v in pairs(obj) do newobj[n] = v end
    return newobj
  end
  members.new  = members.new  or new
  members.copy = members.copy or copy
  return mt
end

然後將我們的 Vector 類別放入名為 Vec.lua 的檔案

require'Class'

Vector = {}

local Vector_mt = Class(Vector)

function Vector:new(x,y)
  return setmetatable( {x=x, y=y}, Vector_mt)
end

function Vector:mag()
  return math.sqrt(self:dot(self))
end

function Vector:dot(v)
  return self.x * v.x + self.y * v.y
end

然後如下進行測試

$ lua -lVec -i -v
Lua 5.0.3  Copyright (C) 1994-2006 Tecgraf, PUC-Rio
> v1 = Vector:new(3,4)
> table.foreach(v1,print)
y       4
x       3
> = v1:mag()
5
> v2 = Vector:new(2,1)
> = v2:dot(v1)
10
>
> table.foreach(Vector,print)
copy    function: 0x80692c0
dot     function: 0x8069300
mag     function: 0x80692e0
new     function: 0x8069398
>
> v3 = v1:copy()
> = v1, v2, v3
table: 0x80779d0        table: 0x8078428        table: 0x807a050
> table.foreach(v1,print)
y       4
x       3
> table.foreach(v3,print)
y       4
x       3
> 

如果將 Class 函式套用至 Lua 的 table lib,我們可以建立資料表物件。

require'Class'

Class(table)

function table:push(x)
  assert( x ~= nil, 'will not push nil into table')
  self:insert(x)
  return self, x
end

function table:map(func, ...)
  local R = table:new{}
  for name,value in pairs(self) do func(R,name,value,unpack(arg)) end
  return R
end

function table:imap(func, ...)
  local R = table:new{}
  for index,elem in ipairs(self) do func(R,index,elem,unpack(arg)) end
  return R
end

然後你不再需要輸入table.foreachtable.getn(t)

$ lua -lTable -i -v
Lua 5.0.3  Copyright (C) 1994-2006 Tecgraf, PUC-Rio
> t = table:new{ 11, 22, 33, you='one', me='two' }
> = t:getn()
3
> t:foreach(print)
1       11
2       22
3       33
me      two
you     one
>
> = t:concat','
11,22,33
> = table
table: 0x8067808
> = getmetatable(t)
table: 0x8067808
>
> s = t:copy()
> s:foreach(print)
1       11
2       22
3       33
me      two
you     one
> = s, t
table: 0x8079a58        table: 0x8077bb8
> 

相關條目


最新異動 · 喜好設定
編輯 · 歷史
最後編輯時間:2008 年 10 月 18 日下午 11:32(格林威治標準時間)(變更)