前言
Lua并非嚴(yán)格意義上的面向?qū)ο笳Z言属划,在語言層面上并沒有直接提供諸如class這樣的關(guān)鍵字饭望,也沒有顯式的繼承語法和virtual函數(shù)相速,但Lua提供了一種創(chuàng)建這些面向?qū)ο笠氐哪芰Α?/p>
Lua面向?qū)ο缶幊毯喪?/p>
Lua中的table就是一種對象
1.table與對象一樣可以擁有狀態(tài)
2.table也是對象一樣擁有一個獨立于其值的標(biāo)識(一個self)
3.table與對象一樣具有獨立于創(chuàng)建者和創(chuàng)建地的生命周期
先看看以下代碼:
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)
上面的代碼創(chuàng)建了一個新函數(shù)敷扫,并將該函數(shù)存入Account對象的withDraw字段中哀蘑,然后我們就可以調(diào)用該函數(shù)了。不過葵第,這個特定對象還必須存儲在特定的全局變量中绘迁。如果改變了對象的名稱,withDraw就再也不能工作了卒密,出現(xiàn)attempt to index global 'Account' (a nil value)
這種行為違反了前面提到的對象特性缀台,即對象擁有獨立的生命周期。而我們可以通過增加一個參數(shù)來表示接受者哮奇,這個參數(shù)通常稱為self或this:
Account = {balance = 0}
function Account.withdraw( self, v )
self.balance = self.balance - v
end
--基于修改后代碼的調(diào)用:
a1 = Account
Account = nil
a1.withdraw(a1,100.00)
--正常工作膛腐。
print(a1.balance) -->-100
使用self參數(shù)是所有面向?qū)ο笳Z言的一個核心。大多數(shù)面向?qū)ο笳Z言都對程序員隱藏了self參數(shù)鼎俘, Lua只需要使用冒號哲身,則能隱藏該參數(shù),重寫為:
function Account:withdraw( v )
self.balance = self.balance - v
end
調(diào)用時可寫為:
a1:withdraw(100.00)
冒號的作用是在方法定義中添加一個額外的隱藏參數(shù)贸伐,以及在一個方法調(diào)用中添加一個額外的實參律罢。冒號只是一種語法便利,并沒有引入任何新的東西棍丐。
現(xiàn)在的對象已有一個標(biāo)識误辑、狀態(tài)和狀態(tài)之上的操作。不過還缺乏一個類系統(tǒng)歌逢、繼承和私密性巾钉。
類
一個類就像是一個創(chuàng)建對象的模具。在Lua中則沒有類的概念秘案,不過要在Lua中去模擬類并不困難砰苍。在Lua中潦匈,要表示一個類,只需創(chuàng)建一個專用作其他對象的原型赚导。原型也是一種常規(guī)的對象茬缩,也就是說我們可以直接通過原型去調(diào)用對應(yīng)的方法。當(dāng)其它對象(類的實例)遇到一個未知操作時吼旧,原型會先查找它凰锡。
在Lua中實現(xiàn)原型是非常簡單的,比如有兩個對象a和b圈暗,要讓b作為a的原型掂为,只需要以下代碼就可以完成:
setmetatable(a, {__index = b})
在此以后,a就會在b中查找所有它沒有的操作员串。若將b稱為是對象a的“類”勇哗,就僅僅是術(shù)語上的變化。現(xiàn)在我就從最簡單的開始寸齐,要創(chuàng)建一個實例對象欲诺,必須要有一個原型,就是所謂的“類”渺鹦,看以下代碼:
local Account = {} -- 一個原型
好了瞧栗,現(xiàn)在有了原型,那如何使用這個原型創(chuàng)建一個“實例”呢海铆?接著看以下代碼:
--new可看作為構(gòu)造函數(shù)
function Account:new(o)
o = o or {} -- 如果用戶沒有提供table迹恐,則創(chuàng)建一個
setmetatable(o, self)
self.__index = self
return o
end
當(dāng)調(diào)用Account:new時,self就相當(dāng)于Account卧斟。接著殴边,我們就可以調(diào)用Account:new來創(chuàng)建一個實例了。再看:
local a = Account:new{balance = 100} -- 這里使用原型Account創(chuàng)建了一個對象
a:deposit(100.00)
上面這段代碼是如何工作的呢珍语?首先使用Account:new創(chuàng)建了一個新的實例對象锤岸,并將Account作為新的實例對象a的元表。再當(dāng)我們調(diào)用a:deposit(100.00)函數(shù)時板乙,就相當(dāng)于a.deposit(a)是偷,冒號就只是一個“語法糖”,只是一種方便的寫法募逞。我們創(chuàng)建了一個實例對象a蛋铆,當(dāng)調(diào)用deposit時,就會查找a中是否有deposit字段放接,沒有的話刺啦,就去搜索它的元表,所以纠脾,最終的調(diào)用情況如下:
getmetatable(a).__index.deposit(a, 100.00)
a的元表是Account玛瘸,Account的__index也是Account蜕青。因此,上面的調(diào)用也可以使這樣的:
Account.deposit(a, 100.00)
所以糊渊,其實我們可以看到的是右核,實例對象a表中并沒有deposit方法,而是繼承自Account方法的渺绒,但是傳入deposit方法中的self確是a贺喝。這樣就可以讓Account(這個“類”)定義操作。除了方法芒篷,a還能從Account繼承所有的字段搜变。
繼承不僅可以用于方法采缚,還可以作用于字段针炉。因此,一個類不僅可以提供方法扳抽,還可以為實例中的字段提供默認(rèn)值篡帕。看以下代碼:
--[[
在這段代碼中贸呢,我們可以將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可以視為構(gòu)造函數(shù)
function Account:new(o)
o = o or {} -- 如果用戶沒有提供table楞陷,則創(chuàng)建一個
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創(chuàng)建了一個對象b
b:deposit() -->100
b:deposit() -->200
在Account表中有一個balance字段怔鳖,默認(rèn)值為0;當(dāng)我創(chuàng)建了實例對象b時固蛾,并沒有提供balance字段结执,在deposit函數(shù)中,由于b中沒有balance字段艾凯,就會查找元表Account献幔,最終得到了Account中balance的值,等號右邊的self.balance的值就來源自Account中的balance趾诗。調(diào)用b:deposit()時蜡感,其實就調(diào)用以下代碼:
b.deposit(b)
在deposit的定義中,就會變成這樣子:
b.deposit = getmetatable(b).__index.balance + 100
第一次調(diào)用deposit時恃泪,等號左側(cè)的self.balance就是b.balance郑兴,就相當(dāng)于在b中添加了一個新的字段balance;當(dāng)?shù)诙握{(diào)用deposit函數(shù)時贝乎,由于b中已經(jīng)有了deposit字段杈笔,所以就不會去Account中尋找deposit字段了。
繼承
由于類也是對象(準(zhǔn)確地說是一個原型)糕非,它們也可以從其它類(原型)獲得(繼承)方法蒙具。這種行為就是繼承球榆,可以很容易的在Lua中實現(xiàn)。
假設(shè)有一個基類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
現(xiàn)在需要從這個Account類派生出一個子類SpecialAccount 禁筏,則需要創(chuàng)建一個空的類持钉,從基類繼承所有的操作:
SpecialAccount = Account:new()
現(xiàn)在,我創(chuàng)建了一個Account類的一個實例對象篱昔,在Lua中每强,現(xiàn)在SpecialAccount 既是Account類的一個實例對象,也是一個原型州刽,就是所謂的類空执,就相當(dāng)于SpecialAccount 類繼承自Account類。再如下面的代碼:
s = SpecialAccount:new{limit=1000.00}
SpecialAccount 從Account繼承了new穗椅;不過辨绊,在執(zhí)行SpecialAccount :new時,它的self參數(shù)表示為SpecialAccount 匹表,所以s的元表為SpecialAccount 门坷,SpecialAccount 中字段__index的值也是SpecialAccount 。然后袍镀,我們就會看到默蚌,s繼承自SpecialAccount ,而SpecialAccount 又繼承自Account。當(dāng)執(zhí)行s:deposit(100.00)時,Lua在s中找不到deposit字段砌些,就會查找SpecialAccount ;如果仍然找不到deposit字段锦茁,就查找Account,最終會在Account中找到deposit字段绣硝◎呤疲可以這樣想一下,如果在SpecialAccount 中存在了deposit字段鹉胖,那么就不會去Account中再找了握玛。所以,我們就可以在SpecialAccount 中重定義deposit字段甫菠,從而實現(xiàn)特殊版本的deposit函數(shù)挠铲。
多重繼承
實現(xiàn)單繼承時,依靠的是為子類設(shè)置metatable寂诱,設(shè)置其metatable為父類拂苹,并將父類的__index設(shè)置為其本身的技術(shù)實現(xiàn)的。而多繼承也是一樣的道理痰洒,在單繼承中瓢棒,如果子類中沒有對應(yīng)的字段浴韭,則只需要在一個父類中尋找這個不存在的字段;而在多重繼承中脯宿,如果子類沒有對應(yīng)的字段念颈,則需要在多個父類中尋找這個不存在的字段。
Lua會在多個父類中逐個的搜索deposit字段连霉。這樣榴芳,我們就不能像單繼承那樣,直接指定__index為某個父類跺撼,而是應(yīng)該指定__index為一個函數(shù)窟感,在這個函數(shù)中指定搜索不存在的字段的規(guī)則。這樣便可實現(xiàn)多重繼承歉井。這里就出現(xiàn)了兩個需要去解決的問題:
保存所有的父類柿祈;
指定一個搜索函數(shù)來完成搜索任務(wù)。
-- 在多個父類中查找字段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
-- 為這個新類建立一個新的構(gòu)造函數(shù)
function c:new(o)
o = o or {}
setmetatable(o, self)
-- self.__index = self 這里不用設(shè)置了酣难,在上面已經(jīng)設(shè)置了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
-- 創(chuàng)建一個c類谍夭,它的父類是CA和CB
local c = createClass(CA, CB)
-- 使用c類創(chuàng)建一個實例對象
local objectC = c:new{name = "Paul"}
-- 設(shè)置objectC對象一個新的名字
objectC:setName("John")
local newName = objectC:getName()
print(newName)
使用createClass創(chuàng)建了一個類(原型)黑滴,將CA和CB設(shè)置為這個類(原型)的父類(原型)憨募;在創(chuàng)建的這個類(原型)中,設(shè)置了該類的__index為一個search函數(shù)袁辈,在這個search函數(shù)中尋找在創(chuàng)建的類中沒有的字段菜谣;
創(chuàng)建的新類中,有一個構(gòu)造函數(shù)new晚缩;這個new和之前的單繼承中的new區(qū)別不大尾膊,很好理解;
調(diào)用new構(gòu)造函數(shù)荞彼,創(chuàng)建一個實例對象冈敛,該實例對象有一個name字段;
調(diào)用object:setName(“John”)語句鸣皂,設(shè)置一個新的名字抓谴;但是在objectC中沒有這個字段,怎么辦寞缝?好了癌压,去父類找,先去CA找荆陆,一下子就找到了滩届,然后就調(diào)用了這個setName,setName中的self指向的是objectC被啼;設(shè)置以后帜消,就相當(dāng)于修改了objectC字段的name值棠枉;
調(diào)用objectC:getName(),objectC還是沒有這個字段泡挺。找吧术健,CA也沒有,那就接著找粘衬,在CB中找到了荞估,就調(diào)用getName,在getName中的self指向的是objectC稚新。所以勘伺,在objectC:getName中返回了objectC中name的值,就是“John”褂删。
飞醉。
私密性
我們都知道,在C++或Java中屯阀,對于類中的成員函數(shù)或變量都有訪問權(quán)限的缅帘。public,protected和private這幾個關(guān)鍵字還認(rèn)識吧难衰。那么在Lua中呢钦无?Lua中是本身就是一門“簡單”的腳本語言,本身就不是為了大型項目而生的盖袭,所以失暂,它的語言特性中,本身就沒有帶有這些東西鳄虱,那如果非要用這樣的保護的東西弟塞,該怎么辦?我們還是“曲線救國”拙已。思想就是通過兩個table來表示一個對象决记。一個table用來保存對象的私有數(shù)據(jù);另一個用于對象的操作倍踪。對象的實際操作時通過第二個table來實現(xiàn)的系宫。為了避免未授權(quán)的訪問,保存對象的私有數(shù)據(jù)的表不保存在其它的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())
這種設(shè)計給予存儲在self table中所有東西完全的私密性癞志。當(dāng)調(diào)用newObject返回以后往枷,就無法直接訪問這個table了。只能通過newObject中創(chuàng)建的函數(shù)來訪問這個self table;也就相當(dāng)于self table中保存的都是私有的错洁,外部是無法直接訪問的秉宿。大家可能也注意到了,我在訪問函數(shù)時屯碴,并沒有使用冒號描睦,這個主要是因為,我可以直接訪問的self table中的字段导而,所以是不需要多余的self字段的忱叭,也就不用冒號了。