使用 Eventtable 实现类和方法

lua-users home
wiki


[!] 版本通知:以下代码适用于较旧的 Lua 版本,Lua 4。它在 Lua 5 下无法运行。由于 Lua 5.0 已经完成,此页面已过时。请参阅 使用元表实现 Lua 类

Lua 4.0 和 Lua 4.1 (work4) 之间的区别就像 AWK 和 Perl 5 之间的区别。新的元表和词法作用域非常适合在 Lua 中创建类,但需要对标签方法有一定的了解。

注意:在 Lua 4.1 (work4) 中,metatable() 命令已更改为 metatable() -- Dominik Wagner

注意:我更改了文章以反映此更改。-- JamesHearn

要了解标签方法,首先将 gettable 和 settable 事件设置为 print 函数。这样,您就可以看到在发生表访问时传递给事件函数的参数。

$ lua
Lua 4.1 (work4)  Copyright (C) 1994-2001 TeCGraf, PUC-Rio
> t = { 11,22,33 , one = "a", two = "b", three = "c" }
> foreach(t,print)
1       11
2       22
3       33
one     a
three   c
two     b
> mt = { gettable = print, settable = print }
> metatable( t, mt )
> x = t[2]
table: 0x80631d0        2
> = t
table: 0x80631d0
> x = t.three
table: 0x80631d0        three
> x = t["three"]
table: 0x80631d0        three
> t.one = 'rat'
table: 0x80631d0        one     rat
> t[1] = 99
table: 0x80631d0        1       99
> = x
nil
>
由此我们可以看到,当您尝试在表 't' 中查找值时,gettable 方法会将表 't' 和索引作为参数传递。我们还可以看到,当您尝试将表成员设置为某个值时,settable 方法会将表 't'、索引和新值传递给它。

settable 和 gettable 事件很简单,但 index 事件更有用。当您尝试从表中获取表中不存在的值时,会调用 index 事件。您可以针对这些情况执行一些特殊操作,例如在另一个表中查找或执行您想要的任何操作。

> foreach(t,print)
1       11
2       22
3       33
one     a
three   c
two     b
> mt = { index = print }
> metatable( t, mt )
> x = t.fish
table: 0x80631d0        fish
> = t
table: 0x80631d0
> x = t[44]
table: 0x80631d0        44
> x = t['zzz']
table: 0x80631d0        zzz
> = x
nil
>
我们可以看到,index 事件会像 gettable 事件一样获取表 't' 和索引,但可以将 index 事件视为未定义表值的特殊 gettable。请注意,如果您使用 gettable 事件,则 index 事件将永远不会被调用。

Lua 4.1 (work4) 允许您将这些标签方法设置为函数或其他表以供访问。

> foreach(t,print)
1       11
2       22
3       33
one     a
three   c
two     b
> u = { cow = 'big' }
> mt.index = u
> foreach(u,print)
cow     big
> = t.cow
big
>

以下内容是您在 Perl 中创建类所需的一切。

类的对象只是一个表,其元表设置为另一个表,该表是类。

您可以将所有类方法放在类表中,并将 index 方法设置为类表。请注意,下面的函数 'class' 如何将其 index 成员设置为自身。

然后,当您执行类似 t:fun(4) 的方法调用时,这相当于 t.fun(t,4),如果表 't' 不包含函数 'fun',它将使用 index 方法在类表中查找。

------------------------------------------------------------------------------
--
function class( t )
  t = t or {}
  t.index = t
  t.new = new
  t.copy = copy
  return t
end

------------------------------------------------------------------------------
function new( class, init )
  init = init or {}
  return metatable( init, class )
end

------------------------------------------------------------------------------
function classof( x )
  return type(x)=='table' and metatable(x)
end

------------------------------------------------------------------------------
function copy( obj )
  local newobj = classof( obj ):new()
  for n,v in obj do newobj[n] = v end
  return newobj
end

------------------------------------------------------------------------------
------------------------------------------------------------------------------

B = class{
  name = 'Bob',
}

function B:who()
  print(self.name)
end

function B:hypotenuse()
  local x,y = self.x, self.y
  return sqrt( x*x + y*y )
end

a = B:new{
  x = 4,
  y = 99,
}

b = B:new{
  x = 5,
  y = 12,
}

print( b:hypotenuse() )

------------------------------------------------------------------------------
------------------------------------------------------------------------------
这里,类 'B' 是对象 'a' 和 'b' 的元表或方法类表。请注意 'B.index' 等于 'B'。
$ lua std.lua -i
> foreach(B,print)
copy    function: 0x8066038
index   table: 0x8066a08
who     function: 0x8065d40
hypotenuse      function: 0x80660c0
name    Bob
new     function: 0x80659b8
> foreach(a,print)
y       99
x       4
> foreach(b,print)
y       12
x       5
> print( b:hypotenuse() )
13
> = B
table: 0x8066a08
>
您可以覆盖类 'B' 的默认 'new' 方法,如下所示
>
> function B:new( x,y )
>> local obj = new( self, { x=x, y=y } )
>> return obj
>> end
>
> c = B:new(1,sqrt(3))
> foreach(c,print)
y       1.732050807568877
x       1
> = c:hypotenuse()
2
>
一个很好的技巧是使用 'debug' 函数来停止并在函数内部打印出值,然后只需输入 'cont' 即可继续。
> function B:something(x)
>> y = self
>> print(x,y)
>> debug()
>> print(x,y)
>> print"finish"
>> return x
>> end
>
> b:something(1234)
1234    table: 0x8066548
lua_debug> print(b)
table: 0x8066548
lua_debug> y=22
lua_debug> cont
1234    22
finish
>
最后一点。请注意,不要在全局表上使用标签方法。因为所有 Lua 的标准函数都只是全局值,除非您在局部变量中创建局部副本,否则当您尝试调用这些函数中的任何一个时,标签方法将进入无限循环。这就是为什么您有时会看到以下内容
local rawget, metatable, getglobal, gsub
    = rawget, metatable, getglobal, gsub

事件表是否需要兼任类成员的存储?例如,在 Python 类中,特殊成员使用下划线命名(例如,__index__),以避免与用户字段冲突。当 Lua 设计人员决定在事件表规范中添加一些新字段时,也会出现相同的冲突问题。

不,这只是 Peter 使用的一种便利方式。你可以使用不同的表来存储索引标签方法;这只是意味着需要处理更多的表。Peter 的代码中有一些奇怪的地方(例如,b:new() 被定义了,但它到底做了什么并不完全清楚),这些都是由于将索引表与事件表合并的决定造成的。我个人建议使用不同的表,即使这会使设计稍微复杂一些,原因是出于这个原因,以及 John 关于名称冲突的反对意见。__foo__ 是一种丑陋的解决方法,而不是解决方案:命名空间分离才是解决方案。-- RiciLake

Roberto 最早提出使用元表来存储类的成员方法。我喜欢这种方法,因为这样你就可以说

function some_class:some_method(some_params) does_something end

如果这是推荐的做法,那么系统的事件表字段应该明确命名以避免冲突。

以下代码应该可以解决 b:new() 问题,因为无论 new 传入的是类表还是类对象,obj.index 始终返回类表。

function class( t )
  t = t or {}
  t.index = t
  t.new = new
  t.copy = copy
  return t
end

function new( obj, init )
  init = init or {}
  return metatable( init, obj.index )
end

function copy( obj, init )
  local newobj = obj:new(init)
  for n,v in obj do newobj[n] = v end
  return newobj
end
$ lua -v std2.lua -i
Lua 4.1 (work4)  Copyright (C) 1994-2001 TeCGraf, PUC-Rio
> foreach(B,print)
copy    function: 0x8066fb0
index   table: 0x8067b40
who     function: 0x8066cb8
hypotenuse      function: 0x8067038
name    Bob
new     function: 0x8066b18
> foreach(b,print)
y       12
x       5
> w = b:new()
> foreach(w,print)
> =B
table: 0x8067b40
> =b.index
table: 0x8067b40
> =w.index
table: 0x8067b40
> z = b:new{ x=1,y=sqrt(3) } 
> = z:hypotenuse()
2
> foreach(z,print)
y       1.732050807568877
x       1
> zz = z:copy()
> foreach(zz,print)
y       1.732050807568877
x       1
> =z
table: 0x8068488
> =zz
table: 0x80686d0
> 
> b:who() 
Bob
> c=b:copy{ z=3 }
> foreach(c,print)
y       12
x       5
z       3
> c:who()
Bob
> 


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2006 年 12 月 31 日凌晨 12:13 GMT (差异)