使用元表实现 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

使用有效索引对表进行索引将返回存储在该索引处的 value。

使用未定义索引对表进行索引将返回 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 事件执行相同的操作,我们会看到有一个第三个参数,它是存储在索引处的新的 value。

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 库,我们可以创建 table 对象。

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 GMT (差异)