2011年10月25日 星期二

Lua -- 物件導向 Object-Oriented Programming

來來來,好久沒有新教學文了,今天就來談談在Lua裡面實現物件導向寫法。其實呢,Lua並不是一個物件導向的語言,罷特~~ Lua裡面神通廣大的table當然可以辦到這件事。今天就來講解,Lua如何辦到物件導向。


PS:請注意,由於網頁顯示的關係,以下_setmetatable這個函式請把_拿掉。_不應該出現

其實呢,Lua裡面的Table就是一個物件了。而且他可以有屬於自己的方法(method),如以下範例:
Acount = {balance = 0}
function Account.withdraw (value)
    Account.balance = Account.balance - value
end
實行的時候,直接採用Account.withdraw(100.00)就可以了。
但是呢,這樣的寫法其實不好,因為上述方法裡面直接去修改了Account.balance的數值,這樣是不好的方式。因此呢,可以改用以下方式:

Account = {balance = 0}
function Account:withdraw (value)
    self.balance = self.balance - value
end
以上這樣的寫法,如同:
function Account.withdraw(self, value)
    self.balance = self.balance - value
end
所以,冒號的用法,是隱藏了一個運算function,他會將物件本身self給傳遞進去,這樣一來,即使你的物件不叫做Account也可以做這些方法喔。例如:
Account2 = Account; Account = nil
Account2:withdraw(100.00)
這樣就ok了。
不然你自己傳遞進去也可以,例如:
Account2.withdraw(Account2, 100.00)
也是一樣的結果。

OK,有了table之後,來看看怎樣建立一個類別吧(Class)。
類別是創造物件的一個模具,也就是說,依照這樣的模具(類別)去建立一個物件,且具有其類別的特性(狀態)與方法(行為)。在Lua中其實沒有類別的概念,但是,可以透過table特有的metatable去模擬類別。例如,要讓a去實現b, b是a的原型:
_setmetatable (a, {__index = b})

這樣的作法,未來即使在a中沒有的特性或方法,他會往b去找尋。因此,達到繼承的概念。
以銀行帳戶為一個物件之觀念,以下為範例:
Account = {balance = 0, withdraw = function (self, value)
                        self.balance = self.balance - valve
                        end }
function Account:deposit(value)
    self.balance = self.balance + value
end
因此在建立一個新物件的時候,會採用:
function Account:new(Object)
    Object = Object or {} -- 如果沒有輸入任何資料,則建立一個空table
    _setmetatable(Object, self)-- 設定Object物件的metatable是Account (Self = Account)
    self.__index = self    -- Account的metatable是他自己
    return Object           -- 回傳這個物件
end
<感謝ykhuang建議與補充,物件的實作是以metatable加上metatable的metamethod來完成,因此上面範例中,是以Account作為metatable,其metamethod __index是以Account本身作為未來查詢索引根據>
因此,我要建立一個帳號的物件:
Big = Account:new({balance = 0})
然後呢,我就可以直接使用Account這個類別的方法了。
Big:deposit(100.00)

上述例子,我在解釋一遍:
當創建新的Account的時候,Big這個物件將Account作為他的metatable。因此,當要使用deposit這個方法的時候,雖然在Big裡面找不到,但是因為Big繼承了Account(以Account作為metatable),其metamethod__index是以Account作為查詢的依據,所以lua會往Account的方法裡面去找尋。所以,Big就可以使用deposit這個方法。當然,也繼承了Account.balance這個特性。

但是,如果再Big這個帳戶裡面的deposit方法跟Account不一樣怎麼辦呢?很簡單,你就直接覆蓋過去就好,例如:
function Big:deposit(value)
    self.balance = self.balance - value - 100
end
這樣,當Big要使用deposit這個方法,他會先在Big裡面找到方法,因此Account這個deposit就被覆蓋掉了。

在Lua裡面當然也可以多方繼承,但是就會比較麻煩一點囉。在Lua裡面,物件導向並不是原生的,所以,在多方繼承方面,並沒有這麼直接,而且有一點點的~”取巧”。該怎麼說呢,前面提到,當要繼承一個類別的時候,他直接承接了類別的所有特性與方法,這是透過__index去達成。但是,當要繼承多個類別的時候,就必須在__index裡面,建立一個function,然後去搜尋每一個被繼承的類別裡面有沒有這樣的方法存在。
例如:
local function search (k, plist)
    for i = 1, #plist do
        local v = plist[i][k]
        if v then return v end
    end
end

function createClass(...)
    local NewObject = {}
    local parents = {...}
    _setmetatable( NewObject, {__index = function (t, k ) 
         return search (k, parents)
         end})
    NewObject.__index = NewObject
    function NewObject:new(Object)
        Object = Object or  {}
        _setmetatable(Object, NewObject)
        return Object
    end
    return NewObject
end
那怎麼使用勒?看看下面:
假設有兩個要被繼承的物件,一個是上面的Account,一個是Named:
Named = {}
function Named:getname()
    return self.name
end
function Named:setname(n)
    self.name = n
end

--所以,直接調用createClass:
NamedAccount = createClass( Account, Named )
--就這樣,NamedAccount繼承了Account以及Named
--所以可以新建立一個新帳戶
account = NamedAccount:new({name = “Big”})
print(account:getname())  -- > Big
是不是就辦到了物件導向功能了呢?
蝦米,上面看不懂,好唄,我來解釋一下:
首先,我們有Account以及Named兩個物件,然後希望用一個account去繼承這兩個所含有的特性與方法。所以,要建立一個多重繼承的”建立類別”的function ==> createClass(Account, Named)

在createClass裡面,先建立一個NewObject = {},然後,parent是把輸入近來所有要被繼承的table(採用...代表所有輸入參數),然後設定NewObject的metatable是一個搜尋函數,搜尋輸入進去的table是否有被呼叫的方法。然後把這個NewObject的metatable為他自己。然後就可以建立一個新物件了。所以,new出來的這個物件也就有NewObject這個物件的特性與方法,而NewObject這個物件也是繼承了你輸入進去的物件(Account與Named)。

所以,當要使用account:getname()的時候,系統會去找account有沒有getname的方法,若沒有,就往account的上一層NamedAccount去找,那在NamedAccount繼承了Account以及Named,所以會在Named裡面找到getname的方法,就可以使用了。

但是由於每一次呼叫getname,系統都要去metatable裡面翻找,會耗系統速度。所以setmetatable()方法可以修改如下,以增加速度:
_setmetatable( NewObject, {__index = function (t, k ) 
                                                   local v = search(k, parents)
                                                   t[k] = v --把找到的函數保存在自己(t=self),以待下次使用
                                                   return v
                                                   end})

4 則留言:

  1. 一點意見,物件的實作是以metatable加上metatable的metamethod來完成,所以這個範例

    function Account:new(Object)
    Object = Object or {}
    _setmetatable(Object, self)
    self.__index = self -- Account的metatable是他自己
    return Object
    end

    建議解釋為 "Account作為metatable時,其metamethod __index以Account本身作為索引根據" 較不會混淆

    另外 Big = Account:new(balance = 0) 少了括號,應該是 Big = Account:new({balance = 0})

    回覆刪除
  2. Dear ykhuang:
    非常感謝你的建議與糾正。若我還有錯誤的地方,歡迎跟我聯絡。希望能把這語言推廣出去。

    big

    回覆刪除
  3. 大家互相啦,過陣子我應該也會開始認真的使用corona了,到時候可能還需要請教^^

    回覆刪除
  4. Account = {balance = 0, withdraw = function (self, value)
    self.balance = self.balance - valve
    end }
    請問這種宣告法,裡面的withdraw適合用途,可以怎樣運用呢?

    回覆刪除