非缓存实现相当简单。它在表中使用一个 _parents = {} 成员来创建一个继承链。
我通常为所有表设置此回退标记方法,这意味着创建继承与创建具有适当成员的表一样简单-- ******************************************************** -- index tag method AdvProtoIndex(t,f) -- -- This is a 'simple' version of the multiple inheritance -- tag method. It does not cache and thus it is quite slow. -- However, if you think something strange is happening, you -- can fall back to this version and see if the strangeness -- goes away. function AdvProtoIndex (t,f) if f == '_parents' then -- to avoid loop if (OldIndex) then return OldIndex(t,f) else return nil; end end local p = t["_parents"]; if (type(p) == 'table') then local cur_index = 1; -- start at 1 local cur_data; repeat cur_data = p[cur_index]; if (type(cur_data) == 'table') then local result = cur_data[f]; if (result ~= nil) then return result; -- we found a match end else if (OldIndex) then return OldIndex(t,f); else return nil; end end cur_index = cur_index + 1; -- do next parent until (cur_data == nil); return nil; -- we didn't find a match else return nil; end end
使用上述简单实现,很容易创建严重影响运行时性能的继承链,因为继承的方法调用或实例数据访问可能导致 2n 次查找,其中 n 是整个链中继承的基类的数量。a_base = { a_number = 1 } b_base = { b_number = 2 } ab_class = { _parents = { a_base, b_base } } print(ab_class.a_number); -- yields "1" print(ab_class.b_number); -- yields "2"
提供了一个性能优化的实现,其功能基本相同,但速度大大提高。
---------------------------------------------------------- -- AdvProtoIndexWithCache -- -- This inheritance tag method handles multiple inheritance via a -- "_parent = {}" table. It caches information in the top-level table -- to make lookups fast. -- -- Example: -- -- This tag method is applied to all tables, so all you have to do to -- get a magic inheritance table is to do this: -- -- BaseObj1 = { a_value = "a" } -- BaseObj2 = { b_value = "b" } -- MyClass = { _parents = { BaseObj2, BaseObj1 } } -- MyInstance = { _parents = { MyClass } -- function setupMultipleInheritenceForTag(a_tag) -- I like to setup my tag methods within a function because -- then stuff like these private declarations can be -- referenced with upvalues and disappear. :) local NIL_OBJECT = { magic_nil_object = 1} local SLOT_REF_TAG = newtag() local OldIndex = gettagmethod(tag({}),"index") local debug_mode = nil AdvProtoIndexWithCache = function (t,f, instance, depth) if (f == '_parents') or (f == '_slotcache') then -- to avoid loop if (%OldIndex) then return %OldIndex(t,f) else return nil; end end if instance == nil then -- we are the instance! instance = t end if depth == nil then depth = 0 end -- get out the parent table local p = rawgettable(t,"_parents") local cache = rawgettable(instance,"_slotcache"); if cache then local item = rawgettable(cache,f) if item then if item == %NIL_OBJECT then return nil elseif tag(item) == %SLOT_REF_TAG then return item.obj[f] else return item end end else -- if we are the instance AND we had a _parents -- slot, then create the slot cache! if (instance == t) and (p) then cache = {} rawsettable(t,"_slotcache",cache); -- make the slot cache! end end if (type(p) == 'table') then local cur_index = 1; -- start at 1 local cur_data; repeat cur_data = p[cur_index]; if (%debug_mode) then print("---------", cur_index, depth) end if (type(cur_data) == 'table') then if (%debug_mode) then printTables(cur_data) end -- local result = cur_data[f]; local result = rawgettable(cur_data,f); if (%debug_mode and (result ~= nil)) then print("value: ", result) end -- if we found the slot in us, then we need -- to do the caching, because after we return -- it's not possible to make a SLOT_REF if ((result ~= nil) and (cache)) then rawsettable(instance,f,result); end if (result == nil) then result = AdvProtoIndexWithCache(cur_data,f,instance, depth + 1) end if (result ~= nil) then if (%debug_mode and (result ~= nil)) then print("value: ", result) end return result; -- we found a match end else local result if (%OldIndex) then result = %OldIndex(t,f); else result = nil; end if cache then if (type(result) == "function") then rawsettable(cache,f,result); elseif result == nil then rawsettable(cache,f,%NIL_OBJECT) else local slot_ref = { obj = cur_data } settag(slot_ref,%SLOT_REF_TAG); rawsettable(cache,f,slot_ref); end end return result; -- we found a match end cur_index = cur_index + 1; -- do next parent until (cur_data == nil); if cache then rawsettable(cache,f,%NIL_OBJECT) end return nil; -- we didn't find a match else return nil; end end settagmethod(a_tag,"index",AdvProtoIndexWithCache); -- Lua 3.1 method end
在缓存中,函数直接指向,其他项使用称为“SLOT_REF”的项指向。SLOT_REF 是一种特殊类型的表,它是一种按引用而不是按值进行缓存的缓存。它包含对原始值的表和索引的引用,因此,如果原始数据值发生变化,此 SLOT_REF 将正确指向新的数据值。这不会对方法执行,因为已决定方法查找的性能比保留更改基类中方法的能力更重要。
此机制的另一种实现可以通过取消 SLOT_REF 而变得更快,而是使用某种方法使 slotcache 无效(例如维护反向 slotcache 依赖项列表)。每当基类方法或函数发生更改时,反向依赖链的 slotcache 将失效。如果将“_slotcache”更改为在对象的“类”级别而不是像现在这样在“实例”级别发生,这可能会导致仅适度的额外数据保留。
重要的是要认识到,由于缓存版本将信息直接存储在单个扁平表中,因此如果基类方法已在缓存表中,则可能会忽略对它们的更改。实际上,更改基类的方法是一种不频繁的操作。使用此机制时,应完全避免此做法。
由于 Lua(我认为错误地)将 nil 覆盖为 false 和空数据元素的含义,因此无法使用 nil 值覆盖继承的对象成员。这是因为设置 self.a = nil 将导致删除“a”成员,从而导致缺失索引标记方法触发,该方法查阅 _parents 或 _slotcache 表以查找继承的元素“a”。我还没有找到解决此问题的办法。
解决方案的完整代码,包括一些有用的实用程序函数,请参见 此处。