声明:本篇文章由
五年一班 勿言整理
class是面向对象的重要一环,在饥荒关于其的实现中,表是其数据结构的载体,对于表来说,函数传参是传递引用的,你在函数中通过表引用做出的修改是影响本身的。
实现一个类得解决两个方面的问题,一个是对象的实例化,一个是类继承
饥荒对于类的实现依赖两个重要特性,一个是元表,一个是闭包,我假设阅读此文的人了解元表和它__index,__newindex,__call这三个元函数,了解闭包的具体含义
实现实例化,可以借助元表的__call元函数,这个函数会使得一个表可以像函数一样调用使用
实现继承,函数的继承就是一个浅拷贝,元素的继承通过实例化时调用父表的初始化函数
饥荒的类实现同时实现了数据代理,通过__index和__newindex进行数据的劫持和重定向
这里只是概述一下,如果你看不太明白,下面我会给出详细的解释,本文建议上下文综合理解
饥荒关于类的实现,文件位于scripts/class.lua中
因为类有继承和非继承两种构造,Class函数也有两种调用形态
继承构造
在UI部分,都是继承构造,不论是什么UI类,都是Widget派生而来,拿一个简单的Text举个例子
local Widget = require "widgets/widget"
local Text = Class(Widget, function(self, font, size, text, colour)
Widget._ctor(self, "Text") --注意这一行,这是继承实现的重要一环
self.inst.entity:AddTextWidget()
self.inst.TextWidget:SetFont(font)
self.font = font
self:SetSize(size)
self:SetColour(colour or { 1, 1, 1, 1 })
if text ~= nil then
self:SetString(text)
end
end)这个调用传入了两个参数,一个Widget表作为父表,一个匿名函数,这个匿名函数就是初始化对象数据所要调用的 匿名函数的内部有一个父表的_ctor函数调用,这个_ctor就是Widget表声明部分传入的匿名函数,通过调用父表的初始化匿名函数,达到元素继承的目的
非继承构造
在除开UI的其它地方,几乎都是这种形式,典型的就是components里,拿一个简单的举例子
local Eater = Class(function(self, inst)
self.inst = inst
self.eater = false
self.strongstomach = false
self.preferseating = { FOODGROUP.OMNI }
self.caneat = { FOODGROUP.OMNI }
self.oneatfn = nil
self.lasteattime = nil
self.ignoresspoilage = false
self.eatwholestack = false
self.healthabsorption = 1
self.hungerabsorption = 1
self.sanityabsorption = 1
end,
nil,
{
caneat = oncaneat,
})这个调用传入了三个参数,第一个是一个匿名函数,效果也是初始化对象的数据;第二个是nil;第三个是一个表,这个表关系着数据代理,我们后面拓展的时候再谈
看完了调用,我们现在来看一看Class函数最核心的实现 这是类的核心实现部分(数据代理不包括在内,所以第三个形参省去了)
function Class(base,_ctor)
local c={}
--部分1
if not _ctor and type(base)=="function" then
_ctor=base
base=nil
elseif type(base) == 'table' then
for k,v in pairs(base) do
c[k]=v
end
end
--部分2
c.__index=c
c._ctor=_ctor
--部分3
local mt={}
mt.__call=function(class_tbl,...)
local obj={}
setmetatable(obj,c)
if c._ctor then
c._ctor(obj,...)
end
return obj
end
setmetatable(c,mt)
return c
endClass函数里有三个最核心的表,一个c,一个mt,一个obj;先看部分3,在函数末尾,Class函数将mt表设置为了c的元表,然后返回了c,mt表中有__call元函数,__call元函数返回obj表
local Con = Class(...) --调用Class获取内部返回的c表
local obj_table = Con() --__call元函数的设置使得c表可以像函数一般调用,这个调用过程就是执行_call元函数的过程,拿到返回的obj表
local obj_s_table = Con() --每次这种调用都会产生一个新的obj表返回,这就是实例化的过程再来看部分1,上面提到,Class有两种调用方式,部分1的if就是分别处理两种构造
function Class(base,_ctor)
if not _ctor and type(base)=="function" then --这里对应非继承构造,调用的时候,base是匿名函数,__ctor是nil
_ctor=base --这里将__ctor设置成了匿名函数,统一下文匿名函数用_ctor表示
base=nil
elseif type(base) == 'table' then --这里对应继承构造,base就是父表,_ctor就是匿名函数
for k,v in pairs(base) do --这里遍历父表是为了对父表的函数做一个浅拷贝,实现方法的继承
c[k]=v
end
end执行完部分1,_ctor指向匿名函数,如果是继承构造,那么子表现在已经继承了父表的所有方法
再来看部分2,部分2比较简单,将c表的__index键指向自己,在__call内,会将c设置成obj的元表,在定义函数的时候,我们定义的函数都是c表的,那么通过设置元表,将__index指向自己,那么obj实例表就可以使用c表的函数了
然后是保存匿名函数到c表中,以_ctor为键值,这样__call元函数产生实例表obj的时候可以调用_ctor,对obj内的元素进行初始化。
顺带提一句,在调用__call元函数的时候,总是会默认会把对应的c表作为第一个实参,形参名为class_tbl,后面三个点的形参在lua里是不定长参数,你调用Class函数传入的匿名函数,即_ctor在__call内调用时接收的参数,第一个参数就是生成的实例表obj,后面就是你自定义任意数量的参数。
这里__call元函数是在Class函数内定义的,在执行Class的时候,它并没有调用。__call内能够拿到实际对应的c表依赖于lua函数闭包的特性,如果不是很明白闭包,请自行查阅资料。
local Con = Class(function(self,a,b,c) --__ctor的形参,形参self在实际调用时对应的就是生成的实例表
self.key_a=a
self.key_b=b
self.key_c=c
end)
local obj_table = Con(1,2,3) --实例化,这里将三个参数传入
local obj_s_table = Con(3,2,1) --同一个c表进行实例化,传入不同的参数
--最后我们产生的实例表obj_table和obj_s_table里面都会有三个元素:key_a,key_b,key_c
local Don = Class(Con,function(self,a,b,c,d) --继承构造
Con._ctor(self,a,b,c) --调用父类的_ctor给obj表初始化,相当于继承了父类的元素,十分重要的一行
self.key_d=d --子类的元素
end)
local obj_t_table = Don(5,6,7,8)
--最后我们生成的实例表obj_t_table内就有四个元素讲到这里,class的核心实现已经完成了,接下来讲一下数据代理
数据代理,即你操作表的数据都会先经过特定的函数进行处理,这就是__index和__newindex这两个元方法所完成的事。
在class.lua中,定义了如下两个局部函数
如果一个c表启用了数据代理,那么c中的__index和__newindex就会被分别设置为以下对应的两个函数
--这两个函数的代码和下面的一段文字一起阅读
local function __index(t, k)
local p = rawget(t, "_")[k]--先从obj表下的_表读取对应的键
if p ~= nil then --如果有直接返回
return p[1]
end
return getmetatable(t)[k] --没有则去元表,即c中查找
end
local function __newindex(t, k, v)
local p = rawget(t, "_")[k]
if p == nil then --如果不是被代理则直接在实例表中修改
rawset(t, k, v)
else --如果是被代理的,取出对应的值修改,并触发一个预设的函数
local old = p[1]
p[1] = v --修改
p[2](t, v, old) --这里是一个函数调用
end
end上面说过,实例表obj的元表是对应的c表,那么设置了这两个元函数就意味着对实例表obj的部分数据访问会触发__index,全部数据修改都会触发__newindex
如果你要访问的键在obj表本身就有,那么是不会触发__index的,所以在生成obj表时,将需要代理的部分预设好即可。不是需要代理的,直接从obj本身存取。
在这两个元函数中进行数据访存要使用rawget和rawset,避免自身的数据访存触发了自身,造成无限递归。
启用了数据代理,那么每一个实例表obj中就会多一个元素:下划线_
_是一个表,被代理的元素都会在这个表中有对应的键
上面提到过,Class函数和数据代理相关是调用时的第三个参数对应的表
我们继续用Eater组件做示例(省去了举例多余的键)
local function oncaneat(self, caneat, old_caneat)--结合上面的__newindex内的p[2](t, v, old)思考,v就是新值,old就是修改前的旧值
if old_caneat ~= nil then
clearcaneat(self, old_caneat)
end
if caneat ~= nil then
for i, v in ipairs(caneat) do
self.inst:AddTag((type(v) == "table" and v.name or v).."_eater")
end
end
end
local Eater = Class(function(self, inst)
self.inst = inst
self.eater = false
self.caneat = { FOODGROUP.OMNI } --需要代理的键
end,
nil,
{
caneat = oncaneat, --caneat是实例表中应该存在的一个键,oncaneat是上面定义的一个函数,修改这个被代理的键时,就会触发这里预设对应的函数
})接下来让我们修改对应的Class函数
function Class(base,_ctor,props)--第三个参数props
local c={}
--部分1
if not _ctor and type(base)=="function" then
_ctor=base
base=nil
elseif type(base) == 'table' then
for k,v in pairs(base) do
c[k]=v
end
end
--部分2
if props ~= nil then --如果props表存在,即有需要代理的键,则替换元函数
c.__index = __index
c.__newindex = __newindex
else
c.__index = c
end
c._ctor=_ctor
--部分3
local mt={}
mt.__call=function(class_tbl,...)
local obj={}
if props ~= nil then --如果需要代理,则遍历,在_表中生成对应的结构
obj._ = { _ = { nil, __dummy } } --对_做代理,禁止外部通过键访问到真正的_表,__dummy是一个空函数,不过通过rawget rawset还是能拿到
for k, v in pairs(props) do
obj._[k] = { nil, v }
end
end
setmetatable(obj,c)
if c._ctor then
c._ctor(obj,...)
end
return obj
end
setmetatable(c,mt)
return c
end--对于上面的Eater例子,实例化产生的obj表内数据部分的结构会是这样
{
["_"]={
["_"]={nil,__dummy},
["caneat"]={{ FOODGROUP.OMNI },oncaneat}
},
["inst"]=inst,
["eater"]=false
}显然,数据代理相当于为我们提供了一个天然的监听效果,当对应键的值被修改了,就可以立刻做出反应 在components这一块,部分带有replica的组件就是通过这种方式去同步对应的net
class.lua内还有其它的功能,有兴趣的朋友可以自行阅读,这里只讲一下关键部分的实现。
本人认知有限,难免有错误地方,如有发现,请与我联系。
2021/12/17 by 勿言