前言
Lua并非嚴格意義上的面向對象語言,在語言層面上并沒有直接提供諸如class這樣的關鍵字,也沒有顯式的繼承語法和virtual函數,但Lua提供了一種創建這些面向對象要素的能力。
Lua面向對象編程簡述
Lua中的table就是一種對象
1.table與對象一樣可以擁有狀態
2.table也是對象一樣擁有一個獨立于其值的標識(一個self)
3.table與對象一樣具有獨立于創建者和創建地的生命周期
先看看以下代碼:
local tab1 = {a = 1, b = 2}
local tab2 = {a = 1, b = 2}
if tab1 == tab2 then
print("tab1 == tab2")
else
print("tab1 ~= tab2") -->tab1 ~= tab2
end
上述說明兩個具有相同值的對象(table)是兩個不同的對象。對象有其自己的操作,同樣table也有這些操作:
Account = {balance = 0}
function Account.withdraw( v )
Account.balance = Account.balance - v
end
Account.withdraw(100.00)
print(Account.balance) -->-100
a = Account
Account = nil
a.withdraw(100.00) --> attempt to index global 'Account' (a nil value)
上面的代碼創建了一個新函數,并將該函數存入Account對象的withDraw字段中,然后我們就可以調用該函數了。不過,這個特定對象還必須存儲在特定的全局變量中。如果改變了對象的名稱,withDraw就再也不能工作了,出現attempt to index global 'Account' (a nil value)
這種行為違反了前面提到的對象特性,即對象擁有獨立的生命周期。而我們可以通過增加一個參數來表示接受者,這個參數通常稱為self或this:
Account = {balance = 0}
function Account.withdraw( self, v )
self.balance = self.balance - v
end
--基于修改后代碼的調用:
a1 = Account
Account = nil
a1.withdraw(a1,100.00)
--正常工作。
print(a1.balance) -->-100
使用self參數是所有面向對象語言的一個核心。大多數面向對象語言都對程序員隱藏了self參數, Lua只需要使用冒號,則能隱藏該參數,重寫為:
function Account:withdraw( v )
self.balance = self.balance - v
end
調用時可寫為:
a1:withdraw(100.00)
冒號的作用是在方法定義中添加一個額外的隱藏參數,以及在一個方法調用中添加一個額外的實參。冒號只是一種語法便利,并沒有引入任何新的東西。
現在的對象已有一個標識、狀態和狀態之上的操作。不過還缺乏一個類系統、繼承和私密性。
類
一個類就像是一個創建對象的模具。在Lua中則沒有類的概念,不過要在Lua中去模擬類并不困難。在Lua中,要表示一個類,只需創建一個專用作其他對象的原型。原型也是一種常規的對象,也就是說我們可以直接通過原型去調用對應的方法。當其它對象(類的實例)遇到一個未知操作時,原型會先查找它。
在Lua中實現原型是非常簡單的,比如有兩個對象a和b,要讓b作為a的原型,只需要以下代碼就可以完成:
setmetatable(a, {__index = b})
在此以后,a就會在b中查找所有它沒有的操作。若將b稱為是對象a的“類”,就僅僅是術語上的變化。現在我就從最簡單的開始,要創建一個實例對象,必須要有一個原型,就是所謂的“類”,看以下代碼:
local Account = {} -- 一個原型
好了,現在有了原型,那如何使用這個原型創建一個“實例”呢?接著看以下代碼:
--new可看作為構造函數
function Account:new(o)
o = o or {} -- 如果用戶沒有提供table,則創建一個
setmetatable(o, self)
self.__index = self
return o
end
當調用Account:new時,self就相當于Account。接著,我們就可以調用Account:new來創建一個實例了。再看:
local a = Account:new{balance = 100} -- 這里使用原型Account創建了一個對象
a:deposit(100.00)
上面這段代碼是如何工作的呢?首先使用Account:new創建了一個新的實例對象,并將Account作為新的實例對象a的元表。再當我們調用a:deposit(100.00)函數時,就相當于a.deposit(a),冒號就只是一個“語法糖”,只是一種方便的寫法。我們創建了一個實例對象a,當調用deposit時,就會查找a中是否有deposit字段,沒有的話,就去搜索它的元表,所以,最終的調用情況如下:
getmetatable(a).__index.deposit(a, 100.00)
a的元表是Account,Account的__index也是Account。因此,上面的調用也可以使這樣的:
Account.deposit(a, 100.00)
所以,其實我們可以看到的是,實例對象a表中并沒有deposit方法,而是繼承自Account方法的,但是傳入deposit方法中的self確是a。這樣就可以讓Account(這個“類”)定義操作。除了方法,a還能從Account繼承所有的字段。
繼承不僅可以用于方法,還可以作用于字段。因此,一個類不僅可以提供方法,還可以為實例中的字段提供默認值。看以下代碼:
--[[
在這段代碼中,我們可以將Account視為class的聲明,如Java中的:
public class Account
{
public float balance = 0;
public Account(Account o);
public void deposite(float f);
}
--這里balance是一個公有的成員變量。
--]]
local Account = {balance = 0}
--new可以視為構造函數
function Account:new(o)
o = o or {} -- 如果用戶沒有提供table,則創建一個
setmetatable(o, self)
self.__index = self
return o
end
function Account:deposit()
self.balance = self.balance + 100
print(self.balance)
end
local b = Account:new{} -- 這里使用原型Account創建了一個對象b
b:deposit() -->100
b:deposit() -->200
在Account表中有一個balance字段,默認值為0;當我創建了實例對象b時,并沒有提供balance字段,在deposit函數中,由于b中沒有balance字段,就會查找元表Account,最終得到了Account中balance的值,等號右邊的self.balance的值就來源自Account中的balance。調用b:deposit()時,其實就調用以下代碼:
b.deposit(b)
在deposit的定義中,就會變成這樣子:
b.deposit = getmetatable(b).__index.balance + 100
第一次調用deposit時,等號左側的self.balance就是b.balance,就相當于在b中添加了一個新的字段balance;當第二次調用deposit函數時,由于b中已經有了deposit字段,所以就不會去Account中尋找deposit字段了。
繼承
由于類也是對象(準確地說是一個原型),它們也可以從其它類(原型)獲得(繼承)方法。這種行為就是繼承,可以很容易的在Lua中實現。
假設有一個基類Account:
local 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("insufficient funds") end
self.balance = self.balance - v
end
現在需要從這個Account類派生出一個子類SpecialAccount ,則需要創建一個空的類,從基類繼承所有的操作:
SpecialAccount = Account:new()
現在,我創建了一個Account類的一個實例對象,在Lua中,現在SpecialAccount 既是Account類的一個實例對象,也是一個原型,就是所謂的類,就相當于SpecialAccount 類繼承自Account類。再如下面的代碼:
s = SpecialAccount:new{limit=1000.00}
SpecialAccount 從Account繼承了new;不過,在執行SpecialAccount :new時,它的self參數表示為SpecialAccount ,所以s的元表為SpecialAccount ,SpecialAccount 中字段__index的值也是SpecialAccount 。然后,我們就會看到,s繼承自SpecialAccount ,而SpecialAccount 又繼承自Account。當執行s:deposit(100.00)時,Lua在s中找不到deposit字段,就會查找SpecialAccount ;如果仍然找不到deposit字段,就查找Account,最終會在Account中找到deposit字段。可以這樣想一下,如果在SpecialAccount 中存在了deposit字段,那么就不會去Account中再找了。所以,我們就可以在SpecialAccount 中重定義deposit字段,從而實現特殊版本的deposit函數。
多重繼承
實現單繼承時,依靠的是為子類設置metatable,設置其metatable為父類,并將父類的__index設置為其本身的技術實現的。而多繼承也是一樣的道理,在單繼承中,如果子類中沒有對應的字段,則只需要在一個父類中尋找這個不存在的字段;而在多重繼承中,如果子類沒有對應的字段,則需要在多個父類中尋找這個不存在的字段。
Lua會在多個父類中逐個的搜索deposit字段。這樣,我們就不能像單繼承那樣,直接指定__index為某個父類,而是應該指定__index為一個函數,在這個函數中指定搜索不存在的字段的規則。這樣便可實現多重繼承。這里就出現了兩個需要去解決的問題:
保存所有的父類;
指定一個搜索函數來完成搜索任務。
-- 在多個父類中查找字段k
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 c = {} -- 新類
local parents = {...}
-- 類在其元表中搜索方法
setmetatable(c, {__index = function (t, k) return search(k, parents) end})
-- 將c作為其實例的元表
c.__index = c
-- 為這個新類建立一個新的構造函數
function c:new(o)
o = o or {}
setmetatable(o, self)
-- self.__index = self 這里不用設置了,在上面已經設置了c.__index = c
return o
end
-- 返回新的類(原型)
return c
end
-- 一個簡單的類CA
local CA = {}
function CA:new(o)
o = o or {}
setmetatable(o, {__index = self})
self.__index = self
return o
end
function CA:setName(strName)
self.name = strName
end
-- 一個簡單的類CB
local CB = {}
function CB:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function CB:getName()
return self.name
end
-- 創建一個c類,它的父類是CA和CB
local c = createClass(CA, CB)
-- 使用c類創建一個實例對象
local objectC = c:new{name = "Paul"}
-- 設置objectC對象一個新的名字
objectC:setName("John")
local newName = objectC:getName()
print(newName)
使用createClass創建了一個類(原型),將CA和CB設置為這個類(原型)的父類(原型);在創建的這個類(原型)中,設置了該類的__index為一個search函數,在這個search函數中尋找在創建的類中沒有的字段;
創建的新類中,有一個構造函數new;這個new和之前的單繼承中的new區別不大,很好理解;
調用new構造函數,創建一個實例對象,該實例對象有一個name字段;
調用object:setName(“John”)語句,設置一個新的名字;但是在objectC中沒有這個字段,怎么辦?好了,去父類找,先去CA找,一下子就找到了,然后就調用了這個setName,setName中的self指向的是objectC;設置以后,就相當于修改了objectC字段的name值;
調用objectC:getName(),objectC還是沒有這個字段。找吧,CA也沒有,那就接著找,在CB中找到了,就調用getName,在getName中的self指向的是objectC。所以,在objectC:getName中返回了objectC中name的值,就是“John”。
。
私密性
我們都知道,在C++或Java中,對于類中的成員函數或變量都有訪問權限的。public,protected和private這幾個關鍵字還認識吧。那么在Lua中呢?Lua中是本身就是一門“簡單”的腳本語言,本身就不是為了大型項目而生的,所以,它的語言特性中,本身就沒有帶有這些東西,那如果非要用這樣的保護的東西,該怎么辦?我們還是“曲線救國”。思想就是通過兩個table來表示一個對象。一個table用來保存對象的私有數據;另一個用于對象的操作。對象的實際操作時通過第二個table來實現的。為了避免未授權的訪問,保存對象的私有數據的表不保存在其它的table中,而只是保存在方法的closure中。看一段代碼:
function newObject(defaultName)
local self = {name = defaultName}
local setName = function (v) self.name = v end
local getName = function () return self.name end
return {setName = setName, getName = getName}
end
local objectA = newObject("John")
objectA.setName("John") -- 這里沒有使用冒號訪問
print(objectA.getName())
這種設計給予存儲在self table中所有東西完全的私密性。當調用newObject返回以后,就無法直接訪問這個table了。只能通過newObject中創建的函數來訪問這個self table;也就相當于self table中保存的都是私有的,外部是無法直接訪問的。大家可能也注意到了,我在訪問函數時,并沒有使用冒號,這個主要是因為,我可以直接訪問的self table中的字段,所以是不需要多余的self字段的,也就不用冒號了。