【Lua】Luaでオブジェクト指向プログラミングをする

Luaオブジェクト指向プログラミングをする方法についてまとめます。

はじめに

本記事ではLuaオブジェクト指向プログラミングを行う方法についてまとめます。
Luaについての基礎知識については以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

メタテーブル

さてLuaオブジェクト指向プログラミングをするためには前提知識としてメタテーブルを理解する必要があります。
Luaでは全ての値がメタテーブルを持ちますが、特にテーブルに対してメタテーブルを設定すると、テーブルの動作を上書きできます。

この仕組みを理解するために以下のような3次元の値を表現するVector3テーブルを定義します。

local Vector3 = {}
Vector3.new = function(x, y, z)
    local obj = {}
    obj.x = x
    obj.y = y
    obj.z = z
    obj.log = function(self)
        print(string.format("{%d, %d, %d}", self.x, self.y, self.z))
    end
    return obj
end

次にこの三次元の値を加算することを考えます。
まずは以下のように普通に+演算しで加算してみます。

local pos1 = Vector3.new(1, 2, 3)
pos1:log() -- {1, 2, 3}
local pos2 = Vector3.new(4, 5, 6)
pos2:log() -- {4, 5, 6}

-- 加算できないのでこれはエラーになる
local pos3 = pos1 + pos2
pos3:log()

すると、attempt to perform arithmetic on a table valueというメッセージとともにエラーが出力されます。
テーブル同士は加算できないためエラーになっています。

ここで、テーブル同士を加算できるようにするためにメタテーブルを使います。

local Vector3 = {}
Vector3.new = function(x, y, z)
    local obj = {}
    obj.x = x
    obj.y = y
    obj.z = z
    obj.log = function(self)
        print(string.format("{%d, %d, %d}", self.x, self.y, self.z))
    end
    return obj
end

-- Vector3同士を加算する関数を定義
local AddVector3 = function(a, b)
    local x = a.x + b.x
    local y = a.y + b.y
    local z = a.z + b.z
    return Vector3.new(x, y, z)
end

local pos1 = Vector3.new(1, 2, 3)
local pos2 = Vector3.new(4, 5, 6)

-- pos1の加算演算子をAddVector3で上書きする
setmetatable(pos1, {__add = AddVector3})
local pos3 = pos1 + pos2
pos3:log() -- {5, 7, 9}

このようにメタテーブルの__addキーに加算用の関数を代入しておくと、そのテーブルの加算処理の挙動を上書きすることができます。
以上がメタテーブルの基本的な使い方です。

メタテーブルの__indexキーについて

さて前節ではメタテーブルに__addキーを設定することで加算処理を上書きすることができました。
メタテーブルに使えるキーには他にも様々なものが存在します。

そのうちの一つとして、__indexキーがあります。
これはテーブルへのインデックスアクセスを上書きするものです。

例として、以下のように2次元の値を表すテーブルを定義して、
テーブルに存在しない変数zを出力しようとしてみます。

local Vector2 = {}
Vector2.new = function(x, y)
    local obj = {}
    obj.x = x
    obj.y = y
    return obj
end

local pos1 = Vector2.new(1, 2)

print(pos1.z) -- nil

結果はテーブルにzが存在しないためnilになります。
ここで、以下のようにインデックスアクセスを変数zを持つVector3テーブルで上書きしてみます。

local Vector2 = {}
Vector2.new = function(x, y)
    local obj = {}
    obj.x = x
    obj.y = y
    return obj
end

local Vector3 = {}
Vector3.new = function(x, y, z)
    local obj = {}
    obj.x = x
    obj.y = y
    obj.z = z
    return obj
end

local pos1 = Vector2.new(1, 2)
local pos2 = Vector3.new(3, 4, 5)

-- pos1のインデックスアクセスをpos2で上書きする
setmetatable(pos1, {__index = pos2})
print(pos1.z) -- 5

インデックスアクセスが上書きされてnilではなく5と出力されることが確認できました。

ちなみにもしVector2に変数zが定義されていた場合には、
インデックスアクセスを上書きしてもVector2に定義したものが優先されます。

local Vector2 = {}
Vector2.new = function(x, y)
    local obj = {}
    obj.x = x
    obj.y = y
    obj.z = 100  -- こっちが優先される
    return obj
end

オブジェクト指向プログラミング

それでは以上の知識を用いてLuaオブジェクト指向プログラミングを行います。
実装方法はいくつか考えられますが、今回は以下のサイトの方法を紹介させていただきます。

invalid-log.blogspot.com

まず以下のようにInstance関数を定義します。
この中で前節の__indexキーが使用されていることがわかります。

function Instance(class, super, ...)
    local self = (super and super.new(...) or {})
    setmetatable(self, {__index = class})
    setmetatable(class, {__index = super})
    return self
end

クラスはこの関数を使って以下のように定義します。

Foo = {
    new = function(name)
        local self = Instance(Foo) -- インスタンス作成
        self.name = name
        return self
    end;

    -- メソッド
    log = function(self)
        print('foo')
    end;
}

このクラスは以下のように使います。

local foo = Foo.new('fooExample')
foo:log() -- foo: fooExample

また、継承したクラスは以下のように定義します。

Bar = {
    new = function(name, message)
        local self = Instance(Bar, Foo, name)
        self.message = message
        return self
    end;

    -- メソッドをオーバーライド
    log = function(self)
        -- Foo.log(self) -- 親クラスの関数を呼ぶのはこんな感じに
        print('bar: '..self.name..' / '..self.message)
    end;
}

これは以下のようにして使います。

local bar = Bar.new('barExample', 'messageExample')
bar:log() -- bar: barExample / messageExample

関連

light11.hatenadiary.com

参考

milkpot.sakura.ne.jp

invalid-log.blogspot.com