01. 元表,类,手搓 OOP
Lua Table
Lua 里,table 是唯一的复合数据结构,数组、字典、对象、类等等全都是 table:
lua
local t = {} -- 空 table
t.name = "bird" -- 像对象一样用
t[1] = "first" -- 像数组一样用元表 metatable 可以给 table 定义特殊行为,当 lua 做某些操作找不到对应方法时,会去元表里找“元方法”。
最关键的两个元方法:
| 元方法 | 触发时机 | 用途 |
|---|---|---|
__index | 访问 table 中不存在的 key | 实现继承/原型查找 |
__call | 把 table 当函数调用 t(...) | 让 table 变成"构造器" |
lua
local t = {}
local mt = {
__index = function(table, key)
return "默认值" -- 访问不存在的 key 时触发
end,
__call = function(table, ...)
return "被调用了" -- 把 table 当函数调用时触发
end
}
setmetatable(t, mt)
print(t.anything) -- → "默认值"
print(t()) -- → "被调用了"__index 有两种写法:
lua
-- 写法1:__index 是一个函数
mt.__index = function(t, k) return "默认" end
-- 写法2:__index 是另一个 table(更常用)
mt.__index = another_table
-- 当访问 t.xxx 找不到时,自动去 another_table 里找
-- 这就是"原型链"/"继承"的底层机制!深入 Class
lua
-- 源码来自 https://github.com/vrld/hump/blob/master/class.lua
local function include_helper(to, from, seen)
if from == nil then
return to
elseif type(from) ~= 'table' then
return from
elseif seen[from] then
return seen[from]
end
seen[from] = to
for k,v in pairs(from) do
k = include_helper({}, k, seen) -- keys might also be tables
if to[k] == nil then
to[k] = include_helper({}, v, seen)
end
end
return to
end
-- deeply copies `other' into `class'. keys in `other' that are already
-- defined in `class' are omitted
local function include(class, other)
return include_helper(class, other, {})
end
-- returns a deep copy of `other'
local function clone(other)
return setmetatable(include({}, other), getmetatable(other))
end
local function new(class)
-- mixins
class = class or {} -- class can be nil
local inc = class.__includes or {}
if getmetatable(inc) then inc = {inc} end
for _, other in ipairs(inc) do
if type(other) == "string" then
other = _G[other]
end
include(class, other)
end
-- class implementation
class.__index = class
class.init = class.init or class[1] or function() end
class.include = class.include or include
class.clone = class.clone or clone
-- constructor call
return setmetatable(class, {__call = function(c, ...)
local o = setmetatable({}, c)
o:init(...)
return o
end})
end
-- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons).
if class_commons ~= false and not common then
common = {}
function common.class(name, prototype, parent)
return new{__includes = {prototype, parent}}
end
function common.instance(class, ...)
return class(...)
end
end
-- the module
return setmetatable({new = new, include = include, clone = clone},
{__call = function(_,...) return new(...) end})lua
-- to: 拷贝目标
-- from: 拷贝来源
-- seen 避免循环引用
local function include_helper(to, from, seen)
if from == nil then
-- from 为空,没东西拷贝,直接返回 to
return to
elseif type(from) ~= 'table' then
-- from 不是 table,其他类型直接返回 from 就是拷贝了
return from
elseif seen[from] then
-- 这个 from 我们之前处理过了(循环引用保护)
-- 直接返回之前处理过的结果
return seen[from]
end
-- from 正在拷贝到 to,如果后续递归遇到 from 了,直接返回 to 防止死循环
seen[from] = to
-- 遍历 from 的每个键值对
for k,v in pairs(from) do
k = include_helper({}, k, seen) -- key 本身也有可能是 table,递归深拷贝
-- 只有 to 中没有这个 k 时才写入,保证子类能够覆盖父类的方法
if to[k] == nil then
to[k] = include_helper({}, v, seen)
end
end
-- 返回拷贝后的结果
return to
endlua
-- 把 other 的所有字段拷贝进 class,但不覆盖 class 已有的字段,用于继承
local function include(class, other)
return include_helper(class, other, {})
end
-- 创建一个全新的空 table {},把 other 的字段全拷进去,还复制元表,用于克隆
local function clone(other)
return setmetatable(include({}, other), getmetatable(other))
endlua
local function new(class)
-- nil 时给个空表
class = class or {}
-- 继承的类
local inc = class.__includes or {}
-- 可以是单个 __includes = BaseState 也可以多个 __includes = {BaseState, BaseState2}
-- 单个包成数组统一处理
if getmetatable(inc) then inc = {inc} end
for _, other in ipairs(inc) do
-- 如果是字符串,表示是全局变量,直接获取
if type(other) == "string" then
other = _G[other]
end
-- 把父类的字段拷贝进来(不覆盖子类已有的)
include(class, other)
end
-- class 的实例在查找 key 时能回头找 class
class.__index = class
-- 找构造函数:先找 init,再找 [1],最后空函数兜底
class.init = class.init or class[1] or function() end
-- 给每个类附上 include 和 clone 方法,方便以后使用
class.include = class.include or include
class.clone = class.clone or clone
return setmetatable(class, {__call = function(c, ...)
local o = setmetatable({}, c) -- 构建空表,元表指向 class
o:init(...) -- 调用构造函数
return o -- 返回实例
end})
endlua
-- .new .include .clone 三个方法
-- 元表的 __call 还让其可以直接调用
-- Class.new({ __includes = BaseState }) 等价于 Class {__includes = BaseState}
return setmetatable({new = new, include = include, clone = clone},
{__call = function(_,...) return new(...) end})全流程图解
以昨天的 TitleScreenState 为例,串起所有步骤:
BaseState = Class {}
↓
new({}) 被调用
↓
BaseState = {
__index = BaseState, ← 自指
init = function()end,
...
}
元表 = { __call = 构造器 }TitleScreenState = Class { __includes = BaseState }
↓
new({ __includes = BaseState }) 被调用
↓
include(class, BaseState) ← 把 BaseState 的字段拷进来
↓
然后子类自己定义 update/render 覆盖掉空实现
↓
TitleScreenState = {
__index = TitleScreenState,
init = <来自BaseState的空函数>,
enter = <来自BaseState的空函数>,
exit = <来自BaseState的空函数>,
update = <子类自己定义的>, ← 拷贝时已存在,父类的没覆盖它
render = <子类自己定义的>,
}
元表 = { __call = 构造器 }TitleScreenState()
↓
触发 __call
↓
o = setmetatable({}, TitleScreenState)
o:init() ← 空函数,啥也不做
return o
↓
o = {} ← 自身没有任何字段!
元表 = TitleScreenStateo:update(dt)
↓
o 里找 update → 没有
↓
查元表 __index = TitleScreenState
↓
TitleScreenState.update 找到了!
↓
执行,并把 o 作为 self 传入语法糖
令人困惑的 o:method():
lua
o:update(dt)
-- 完全等价于
o.update(o, dt) -- 自动把 o 作为第一个参数(即 self)传入省略括号的函数调用:
lua
-- 普通调用(需要括号)
f(expr)
-- 特殊1:参数是「字符串字面量」时,可以省略括号
f "hello" -- 等价于 f("hello")
f 'hello' -- 等价于 f("hello")
-- 特殊2:参数是「table 构造器」时,可以省略括号
f {} -- 等价于 f({})
f { x = 1 } -- 等价于 f({ x = 1 })