在lua中,只有唯一的一种数据结构,表。通过表,却可以用来实现类一样的形式,其关键是对于表的元方法的使用,可以有很多奇妙的用处。

Lua中,所有的值都可以有一个有一个元表(metatable),元表中的元素定义了对于值特定操作的方法,就叫做元方法。当然,这些元表是有默认值的,但我们可以通过改变值元表中的元方法来进行特定操作行为的改变。

可以通过setmetatable()函数来修改一个表的元表,而其他类型的值元表只能通过 C API来改变。

默认情况下,值是没有元表的,所以我们手动去设置,但 string库为字符串类型设置了一个元表。

表table

lua中没有复杂的数据结构,只有表。通过代码:

t = {}

就建立了一个表。可以验证,其确实是没有元表的:

print(getmetatable(t))

getmetatable()用来获取一个表的元表。

输出将是nil。而对于字符串:

print(getmetatable("hello world")

其输出会是类似table: 0x ...这样。表示其有一个元表。

元表的元素

一个完整的元表,看起来应该是这样的:

mt = { "__ev" = method, ... }

其中ev可以是,__add, _sub, __mul, __div, __mod, __pow, __unm, __idiv, __band, __bor, __bxor, __bnot, __shl, __shr, __concat, __len, __eq, __lt, __le, __index, __newindex。这里面的键被成为事件。

__index事件

我们重点关注一下__index这个事件。

假如我们有一个表 t = {1, x = 2, y = 3,},那么,t.x 与 t[“x”]的值应该是一样的:

t = {1, x = 2, y = 3,}
print(t.x, t["x"])
print(t.n)

以 键 作为索引访问表的元素时,这是正确的。但如果是整数作为索引来访问表元素的话,这就是不对的。t[1],不会等于 t.1,而会出现一个错误。

我们特意用t.n来访问一个表中不存在的元素,很明显,其输出是nil

我们现在来看一下,官方对于 __index事件的说明:

索引访问table[key]。这个事件会在table不是一个表,或key在表中不存在的时候发生。

元方法可以是函数和一个表。如果是函数,以table, key作为参数调用这个元方法,函数返回的结果,就是这个索引访问操作的结果;如果元方法是一个表,那么就以key来索引访问这个 作为元方法的表 中元素。(这个索引访问走的常规流程,也有可能引发另外一次元方法的调用)

我们现在给表t,设置一个元表mtmt内定义了__index的元方法。

t = {1, x = 2, y = 3,}
print(t.x, t["x"])
print(t.n)

mt = {}
mt.__index = function (table, key) print "this key is not here"; return 10 end

setmetatable(t, mt)

print(t.n)

输出将是:

    this key is not here

由于访问了不存在的索引,所以触发了__index事件。

现在,我们把mt__index的元方法设置为一个表:

t = {1, x = 2, y = 3, func = print}

print "------------"
mt = {}
mt.__index = function (table, key) print "oh, I'm in table a, but not in table t"; return 20 end

setmetatable(t, mt)
print(t.n)
print(t.n)

print "------------"
a = { n = 10}
mt.__index = a
setmetatable(t, mt)

print(t.n)
a.n = 10
print(t.n)

输出将是:

------------
oh, I'm in table a, but not in table t
20
oh, I'm in table a, but not in table t
20
------------
10
20

当元方法是一个函数时,索引访问的结果,是元方法调用的结果,同时不存在键不会被加上;而当元方法是一个表时,会从作为元方法的那个表内取出对应的值来 加到当前表上。

到这里,想必你已经发现了什么。

函数是匿名的

Lua中,所有的函数都是匿名的。

function (v)
return v
end

其实与:

foo = function (v) return v end

是等价的。

调用函数的时候,括号是必须的。但在只有一个参数,且参数是字符串或表的时候可以省略。

也就是说:

print("hello world") 与 print "hello world"等价。

表中的函数

所以:


t = {}

function t.func ()
return "one"
end
print(t.func())

t.func = function () return "one" end
print(t.func())

后面对t.func进行赋值的两种形式是等价的,但第一中形式看起来会更加易读一些。

面向对象

表也是一个对象。一个对象,简单来说,会具有状态(属性),方法,可以通过方法来改变自身状态等等。

我们来假设一个钱包的情况。

wallet = { remain = 0 }
function wallet.pay(v)
wallet.remain = wallet.remain - v
end

print(a.remain)
wallet.pay(10)
print(a.remain)

输出是什么?

0
-10

wallet.pay()调用影响了a的值,这说明Lua中,值存储于内存中,变量只是对其的一个引用。

我们再来看另外一个问题:

wallet = nil
a.pay(10)

输出是:

attempt to index a nil value (global 'wallet')

我们销毁了wallet变量,这个时候a也无法工作了。这是因为在pay调用中,所操作的对象是wallet。 而我们需要的,是操作a本身。

在面向对象的概念中,一个对象,调用方法,叫做向这个对象发送消息,换言之,对象就是消息的接收着。

在上面的例子者,消息的接收者是a,而操纵的对象却是wallet,这是一种非常不好的做法,我们也应在 方法的内部去 操纵全局变量。

我们需要的,其实是一种操纵消息接收者自身的机制。幸好,Lua提供了这个机制,通过:冒号来调用方法,即可在方法内部使用self这个代表自身的对象。

将上面的代码进行修改:

wodediannaodeMacBook-Air:lua shouzheng.zhang$ lua 1.lua
wallet = { remain = 0 }
function wallet:pay(v)
self.remain = self.remain - v
end

a = wallet

print(a.remain)
wallet:pay(10)
print(a.remain, wallet.remain)


wallet = nil
a:pay(10)
print(a.remain)

如此,通过:调用方法(函数),就少了这么多的麻烦事情了。

继承

回到前面那个问题,walleta引用的对象都是一样的,所以会造成相互调用间出现影响的情况。

而通过 __index事件一节我们看到,对于 一个表 t,中不存在的元素,其会通过其 元表t.mt__index事件的元方法来寻找。而当 元方法是个表时, 还会直接通过 元方法表 中对应的索引值 来初始化自身表内的 键-值对。

那么,我们可以通过把一个 表  作为表  的__index元方法表,这样,b就能 继承到 a 的所有元素。

a = { remain = 0 }
function a:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end

function a:withdraw(v)
self.remain = self.remain + v
end

function a:pay(v)
self.remain = self.remain - v
end

这里,我们可以把 看成一个类,其方法 new() 创建表o,并把表 a 作为其 元表,同时把 元表 __index事件的值设置为 a,就可以让 oa 取得任何其不具有的元素。