技術ノート 8キャッシュを使用しない実装は非常にシンプルです。_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
通常、すべてのテーブルに対してこのフォールバックタグメソッドを設定します。つまり、継承の作成は、適切なメンバーを持つテーブルを作成するだけという簡単な作業になります。
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"
上記のような単純な実装を使用すると、継承チェーンを作成しやすくなりますが、実行時のパフォーマンスに深刻な影響を与える可能性があります。なぜなら、継承されたメソッド呼び出しやインスタンスデータへのアクセスにより、2n回のルックアップが発生する可能性があるためです。ここで、nは、チェーン全体で継承された基底クラスの数です。ほとんど同じ機能を持ちながら、大幅に高速なパフォーマンス最適化実装を提供します。
----------------------------------------------------------
-- 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`を偽と空のデータ要素の両方の意味としてオーバーライドしているため、継承されたオブジェクトメンバーを`nil`値で上書きする方法がありません。これは、self.a = nilを設定すると、「a」メンバーが削除され、欠落インデックスタグメソッドが起動し、_parentsまたは_slotcacheテーブルを参照して継承された要素「a」を検索するためです。この問題に対する回避策はまだ見つかっていません。
いくつかの便利なユーティリティ関数を含む、ソリューションの完全なコードはこちらで提供されています。