元表 (Metatable)

元表(metatable)是 Lua 提供的一种机制,用于改变表(table)的行为。 通过对元表(metatable)的设置可以自定义表(table)的操作行为,如算术运算、索引等,甚至可以用来实现面向对象机制。

有两个涉及元表的操作函数:

  • 使用setmetatable函数可以设置一个表的元表,第一个参数为目标的表,第二个参数为提供的元表,返回值是第一个参数,无论是否接收第一个参数,这里的修改都是有效的,因为传参过程相对于引用传递;
  • 使用getmetatable函数可以获取一个表的元表,提供的参数就是目标的表。

运算符元方法

对于常见的运算符,有对应的元方法

  • __add 对应运算符 +
  • __sub 对应运算符 -
  • __mul 对应运算符 *
  • __div 对应运算符 /
  • __mod 对应运算符 %
  • __unm 对应运算符 -
  • __eq 对应运算符 ==
  • __lt 对应运算符 <
  • __le 对应运算符 <=
  • __concat 对应运算符 ..

例如用自定义的加法行为实现表的相加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local t1 = { value = 10 }
local t2 = { value = 20 }

local mt = {
__add = function(a, b)
return { value = a.value + b.value }
end
}

setmetatable(t1, mt)
setmetatable(t2, mt)

local t3 = t1 + t2
print(t3.value) -- 30

还有一个__tostring元方法,用于自定义 tostring 函数行为,这个函数会被print自动调用,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local t = {
name = "Alice",
age = 30
}

local mt = {
__tostring = function(table)
return "Person: " .. table.name .. ", " .. table.age .. " years old"
end
}

setmetatable(t, mt)

-- 将表转换为字符串
print(tostring(t)) -- Person: Alice, 30 years old

-- 也可以直接打印表,效果相同,
print(t) -- Person: Alice, 30 years old

重要元方法

有几个重要的涉及表的固有行为的元方法:

  • __index: 自定义索引的访问行为。
  • __newindex: 自定义新索引的赋值行为。
  • __call:自定义调用行为

Lua 在查找一个表中的元素时,会涉及到__index元方法,按照如下规则处理:

  1. 在表中查找:如果找到,直接返回该元素;找不到则继续
  2. 判断该表是否有元表:如果没有元表,返回 nil;如果有元表则继续
  3. 判断元表有没有设置__index
    • 如果__index没有定义,则返回nil
    • 如果__index是一个表,则进入这个表,从头重复上述步骤;
    • 如果__index是一个函数,则传递原表和键给这个函数,并返回该函数的返回值。

关于__index是表的例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local defaults = {
name = "Unknown",
age = 0
}

local t = {}
local mt = {
__index = defaults
}
setmetatable(t, mt)

print(t.name) -- 输出: Unknown
print(t.age) -- 输出: 0

t.name = "Alice"
print(t.name) -- 输出: Alice,因为现在 t 表中有 name 键

关于__index是函数的例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local t = {}
local mt = {
__index = function(table, key)
if key == "name" then
return "Unknown"
elseif key == "age" then
return 0
else
return nil
end
end
}
setmetatable(t, mt)

print(t.name) -- 输出: Unknown
print(t.age) -- 输出: 0

t.name = "Alice"
print(t.name) -- 输出: Alice,因为现在 t 表中有 name 键

Lua 在处理对表中不存在的键进行赋值的情况,会涉及到__newindex元方法,按照如下规则处理:

  1. 在表中查找要赋值的键:如果键存在,直接更新表中该键的值;找不到则继续
  2. 判断该表是否有元表:如果没有元表,直接在表中添加这个键值对;如果有元表则继续
  3. 判断元表有没有设置__newindex
    • 如果__newindex没有定义,则回到原表中添加键值对;
    • 如果__newindex是一个表,则进入这个表,从头重复上述步骤;
    • 如果__newindex是一个函数,则传递原表、键和值作为参数,并返回该函数的返回值。

关于__newindex是表的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local backup = {}

local t = {}
local mt = {
__newindex = backup -- 将赋值操作委托给 backup 表
}
setmetatable(t, mt)

t.a = 1 -- 实际上值被存储在 backup 表中
print(t.a) -- 输出: nil,因为 a 没有存储在 t 中
print(backup.a) -- 输出: 1,因为 a 被存储在 backup 表中

t.b = 2
print(t.b) -- 输出: nil,因为 b 没有存储在 t 中
print(backup.b) -- 输出: 2,因为 b 被存储在 backup 表中

关于__newindex是函数的例子

1
2
3
4
5
6
7
8
9
10
local t = {}
local mt = {
__newindex = function(table, key, value)
print("Attempt to set " .. key .. " to " .. value)
end
}
setmetatable(t, mt)

t.a = 1 -- 输出: Attempt to set a to 1
print(t.a) -- 输出: nil,因为 a 并没有真的被设置

__call元方法允许表像函数一样被调用,将__call设置为函数的例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local counter = {}
local mt = {
__call = function(table, increment)
table.count = (table.count or 0) + (increment or 1)
return table.count
end
}
setmetatable(counter, mt)

-- 调用表,就像调用函数一样
print(counter()) -- 输出: 1
print(counter()) -- 输出: 2
print(counter(5)) -- 输出: 7
print(counter(10)) -- 输出: 17

面向对象

Lua 本身不是一种面向对象的语言,但是我们可以使用已有的语法来模拟面向对象的特性,具体实现有很多种方案:

  • 基于表和元表实现面向对象是最常见的方案;
  • 基于闭包也可以实现面向对象。

基于元表的示例(一)

一个简单的基于元表的面向对象例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
-- 定义 Person 类
local Person = {}
Person.__index = Person -- 设置元表的 __index 字段指向 Person 自身

-- 构造函数
Person.new = function(name, age)
local self = {} -- 创建一个新的空表表示对象
setmetatable(self, Person) -- 将对象的元表设置为 Person
self.name = name
self.age = age
return self
end

-- 定义一个方法
Person.greet = function(self)
print("Hello, my name is " .. self.name)
end

-- 定义继承函数
Person.extend = function(self)
local subclass = {}
for k, v in pairs(self) do
subclass[k] = v
end
subclass.__index = subclass
subclass.super = self
setmetatable(subclass, self)
return subclass
end


-- 创建 Student 类,继承自 Person
local Student = Person.extend(Person)

-- 构造函数
Student.new = function(name, age, major)
local self = Person.new(name, age)
setmetatable(self, Student)
self.major = major
return self
end

-- 定义一个新方法
Student.study = function(self)
print(self.name .. " is studying " .. self.major)
end

-- 创建对象
local alice = Student.new("Alice", 20, "Computer Science")
alice.greet(alice) -- 输出: Hello, my name is Alice
alice.study(alice) -- 输出: Alice is studying Computer Science

冒号语法糖

上面使用原生的Lua语法的做法实在是太过繁琐了,Lua使用冒号:为面向对象机制提供了一点语法糖,让面向对象的语句稍微简化了一点。

例如对于方法的调用来说,使用:会自动将调用者自身作为第一个参数传递给函数。

1
2
3
alice.greet(alice)
-- 简化为
alice:greet()

对于方法的定义也支持使用:简化,此时约定加上self作为函数的第一个参数,self不需要出现在函数形参列表中,但是仍然可以在函数体中正常使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Student.study = function(self)
print(self.name .. " is studying " .. self.major)
end

-- 等价于
function Student.study(self)
print(self.name .. " is studying " .. self.major)
end

-- 等价于
function Student:study()
-- 函数体中仍然可以使用self
print(self.name .. " is studying " .. self.major)
end

这时不能使用Student:study = function ...的语法,不符合约定。

基于元表的示例(二)

使用冒号语法糖对前面的例子进行简化,可以得到如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
-- 定义 Person 类
local Person = {}
Person.__index = Person -- 设置元表的 __index 字段指向 Person 自身

-- 构造函数
function Person:new(name, age)
local self = setmetatable({}, Person) -- 创建一个新的空表表示对象
self.name = name
self.age = age
return self
end

-- 定义一个方法
function Person:greet()
print("Hello, my name is " .. self.name)
end

-- 定义继承函数
function Person:extend()
local subclass = setmetatable({}, self)
subclass.__index = subclass
return subclass
end

-- 创建 Student 类,继承自 Person
local Student = Person:extend()

-- 构造函数
function Student:new(name, age, major)
local self = Person.new(self, name, age)
setmetatable(self, Student)
self.major = major
return self
end

-- 定义一个新方法
function Student:study()
print(self.name .. " is studying " .. self.major)
end

-- 创建对象
local alice = Student:new("Alice", 20, "Computer Science")
alice:greet() -- 输出: Hello, my name is Alice
alice:study() -- 输出: Alice is studying Computer Science

基于闭包的示例

基于闭包的面向对象例子如下,这里我们仍然使用了冒号:语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
-- 定义 Person 类
function Person(name, age)
-- 私有属性
local self = {}
local _name = name
local _age = age

-- 公有方法
function self:getName()
return _name
end

function self:getAge()
return _age
end

function self:greet()
print("Hello, my name is " .. _name)
end

return self
end

-- 定义 Student 类,继承自 Person
function Student(name, age, major)
-- 调用 Person 构造函数
local self = Person(name, age)

-- 私有属性
local _major = major

-- 公有方法
function self:getMajor()
return _major
end

function self:study()
print(self:getName() .. " is studying " .. _major)
end

-- 继承 Person 的 greet 方法,保持不变

return self
end

-- 创建对象
local alice = Student("Alice", 20, "Computer Science")
alice:greet() -- 输出: Hello, my name is Alice
alice:study() -- 输出: Alice is studying Computer Science