Lua Object-Oriented Programming(OOP) オブジェクト指向

注意:勉強がてらの覚え書き、間違っている可能性もあります。
鵜呑み注意。

テーブルを利用したオブジェクト指向プログラムライク

Account = {balance = 0}
function Account.withDraw(v)
    Account.balance = Account.balance - v
end

この定義は Accountオブジェクト内に新しい関数とフィールドを作成する.次のように呼び出せる。

Account.withDraw(100.00)

しかしこの方法だと次のケースで問題が起きる

a = Account; Account = nil
a.withDraw(100.00) -- ERROR

withDraw関数内でAccountが参照されているが、この関数を呼ぶ前にAccountにnilを渡している。

もう少しスマートに
Account = {balance = 0}
function Account.withDraw(self, v)
    self.balance = self.balance - v
end

呼び出し↓

a1 = Account; Account = nil
a1.withDraw(a1, 100.00) --OK

withDraw関数呼び出しの前にAccountにnilが渡されても問題ない.
ついでにselfパラメータを使うことで,別オブジェクトでも同様にwithDraw関数を使える.↓

a2 = {balance = 0, withDraw = Account.withDraw}
a2.withDraw(a2, 290.00)

ごくわずかだがOOPに近づいたがまだまだ遠い。
関数にアクセスするためにわざわざインスタンスを渡すのは面倒だ。

もう少しスマートに

次のように関数を宣言するとselfが自動で引数に入る.

function Account:withDraw(v)
    self.balance = self.balance - v
end

そして次のように呼ぶ.↓

a3:withDraw(100.00)

AccountとwithDrawの間を.(ピリオド)ではなく:(コロン)で繋ぐだけでselfという隠しパラメータが引数に追加される.
コロンは便宜上のためだけの構文になる。

ごくわずかだがOOPに近づいたがまだまだ遠い。
複数のAccountインスタンスを生成したい問題を解決できていない。

メタテーブルを利用したオブジェクト指向プログラムライク

メタテーブルとはテーブルと同じ。
ただしテーブル内の値は特定の演算をしたときのみ使われる。
テーブル内の値は決まっている。

  • __add : +演算
  • __sub : -演算
  • __mul : *演算
  • __div : /演算
  • __mod : %演算
  • __pow : ^(累乗)演算
  • __unm : 単項演算
  • __concat : 連結演算
  • __len : #演算
  • __eq : ==演算
  • __lt : <演算
  • __le : <=演算
  • __index : table[key]インデックスアクセス
  • __newindex : table[key] = valueインデックス代入
  • __call : 値を関数呼び出したとき

所謂C++でいうところのoperator*()などになる。

setmetatable(a, {__index = b})

aオブジェクトが持っていないときは,bへオペレーションを探す。

function Account:new(o)
    o = o or {} --もしUserが提供しない場合は生成される.
    setmetatable(o, self)
    self.__index = self
    return o
end

oオブジェクトのメタテーブルをself(=Account)に決定する。
selfにindexアクセスをした場合はself(=Account)を探すように決定する.
oオブジェクトが持っていないときは, self(=Account)へオペレーションを探す。
(indexに設定している理由は、関数呼び出しはテーブルのindexアクセスによるもののため. Account.drawWithはAccount["drawWith"]と同じ)

a = Account:new()
a:deposit(100.00)

↑新しいAccountインスタンスが生成される。
a:deposit(100.00)が呼ばれたとき、テーブルa内にdepositが見つからない場合はメタテーブルの__indexを調査する。
展開するとこんな感じになる。

getmetatable(a).__index.deposit(a, 100.00)

↑aのメタテーブルのキーとなる__indexの中にあるオブジェクトAccountのdeposit関数をコールしている。
つまりは

Account.deposit(a,100.00)

になる。これは初めの方で説明したもの。
aオブジェクトに対してAccountのdeposit関数を使っているのと同じ。
これでAccountのインスタンスが作成でき、
aのインスタンスメソッドが呼ぶことができた。

継承

とりあえず次のような基底クラスみたいなAccountクラスがあったとする。

Account = {balance = 0}
function Account:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

function Account:deposit(v)
    self.balance = self.balance + v
end

function Account:withDraw(v)
    if v > self.balance then
        error "金がねぇ"
    end
    self.balance = self.balance - v
end

AccountのサブクラスSpecialAccountを定義する.

SpecialAccount = Account:new()

これでSpecialAccountはAccountのインスタンスになる。
そして次で変数追加.

s = SpecialAccount:new{limite = 1000.00}

selfはSpecialAccountを参照しています。
そしてselfはmetatableの__indexに登録されています。
つまりこれで s はSpecialAccountのインスタンスになります。
しかもAccountを継承してます。
↓この式を評価すると

s:deposit(100.00)

sの中にdepositない。SpecialAccountを見に行く。
SpecialAccountの中にdepositない。Accountを見に行く。
Accountの中にdepositある。Account.depositを実行。
こんな感じになります。

オーバーライドを実現

上の評価の流れでいくと、SpecialAccountにwithDrawを定義したらAccountのwithDrawを呼ぶ前にSpecialAccountのwithDrawを呼ぶことになります。

function SpecialAccount:withDraw(v)
    if v - self.balance >= self.getLimite() then
        error "金欠"
    end
    self.balance = self.balance - v
end
function SpecialAccount:getLimite()
    return self.limit or 0
end

参考:http://www.lua.org/pil/index.html:Programming in Lua (first edition)
参考:http://milkpot.sakura.ne.jp/lua/lua51_manual_ja.html:Lua 5.1 リファレンスマニュアル(エンコードによる文字化け注意)