跳转至内容

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
end
lua
-- 把 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))
end
lua
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})
end
lua
-- .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 = {}     ← 自身没有任何字段!
元表 = TitleScreenState
o: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 })