Lua的設(shè)計初衷并非意圖構(gòu)建完整的應(yīng)用,而是在應(yīng)用程序中為應(yīng)用提供靈活的擴(kuò)展和定制功能,所以Lua僅提供了基礎(chǔ)的數(shù)學(xué)運算和字符串處理等函數(shù)庫,而并未涵蓋程序設(shè)計的各個方面。
Lua作為腳本語言甚至沒有原生態(tài)的提供面向?qū)ο蟮闹С郑鷳B(tài)的Lua中是沒有類這個概念的。不過Lua提供的table
表這個強(qiáng)大的數(shù)據(jù)結(jié)構(gòu)卻賦予開發(fā)者自行實現(xiàn)面向?qū)ο笠饬x上的類的能力。
類
在Lua中,使用表和函數(shù)實現(xiàn)面向?qū)ο螅瑢⒑瘮?shù)和相關(guān)數(shù)據(jù)放置于同一個表中就形成了一個對象。
-- 銀行賬戶 account.lua
local _M = {}
-- 元表
local mt = { __index = _M }
-- 新建賬戶,初始化賬戶余額
function _M:new(balance)
balance = balance or 0
-- setmetable將_M作為新建表的原型
return setmetatable( {balance = balance}, mt )
end
-- 存款
function _M:deposit(v)
self.balance = self.balance + v
end
-- 取款
function _M:withdraw(v)
if self.balance>v then
self.balance = self.balance - v
else
error("insufficient funds")
end
end
-- 返回
return _M
-- test.lua
local Account = require("account")
local account1 = Account:new()
account1:deposit(100) -- 100
print( account1.balance )
account1:withdraw(10) -- 90
print( account1.balance )
local account2 = Account:new()
account2:deposit(50)
print( account2.balance ) -- 50
類與對象
- 對象是真實世界中的實體,對象與實體是一一對應(yīng)的,也就是說現(xiàn)實世界中每個實體都是一個對象,它是一種具體的概念。
- 類是具備某些共同特征的實體的集合,是一種抽象的概念。
Lua中table
就是一種對象,這句話從3個方面可以得到證實:
- 表和對象一樣可以擁有狀態(tài)
- 表和對象一樣擁有一個獨立于其值的標(biāo)識
- 表和對象一樣具有獨立于創(chuàng)建者和創(chuàng)建地的生命周期
-- 創(chuàng)建對象
local Account = {
balance = 0
}
-- 創(chuàng)建函數(shù)并將其存入對象的字段中
function Account.withdraw(val)
Account.balance = Account.balance - val
end
-- 使函數(shù)只針對特定對象工作
function Account:disposit(val)
self.balance = self.balance + val
end
一個類就像是一個創(chuàng)建對象的模具,由于Lua中沒有類的概念,每個對象只能自定義行為和形態(tài)。Lua中模擬類可參照基于原型的語言,每個對象有有一個原型(prototype)。原型也是一種對象,當(dāng)其他對象遇到未知操作時,原型會像查找它。因此,要表示一個類,只需要創(chuàng)建一個專門用來作為其他對象的原型。類和原型都是一種組織對象間共享行為的方式。簡單來說,Lua本身沒有類的概念,只有table
表,而面向?qū)ο蟮膶崿F(xiàn)只不過是將表與父類的表連在一起,沒有某個變量時就去父類查找。
需求:銀行賬戶(Account)可存款(deposit)和取款(withdraw),取款時當(dāng)前賬戶(balance)余額不足則提示用戶。
-- 創(chuàng)建類
local Account = {
balance = 0
}
-- 實例化通過類創(chuàng)建對象
function Account:new(tbl)
-- 默認(rèn)初始化為空表
local tbl = tbl or {}
-- 設(shè)置自身作為元表
setmetatable(tbl, self)
-- 新對象繼承元表中的行為
self.__index = self
return tbl
end
-- 創(chuàng)建函數(shù)并將其存入對象的字段中
function Account:withdraw(val)
-- self:使函數(shù)只針對特定對象工作
if val > self.balance then
error "insufficient funds"
end
self.balance = self.balance - val
end
function Account:disposit(val)
self.balance = self.balance + val
end
-- test
local obj = Account:new{balance = 10}
obj:disposit(100)
print(obj.balance)
obj:withdraw(20)
print(obj.balance)
實現(xiàn)Lua面向?qū)ο罂煞纸鉃轭惖亩x和類的實例化兩個問題,類的定義主要是實現(xiàn)繼承,即怎么樣子類擁有父類的方法集。類的實例化需要解決實例如何共享類的方法集,但獨享自己的成員變量實例。
Cocos2dx-lua中有一個class
函數(shù)實現(xiàn)了類的基礎(chǔ),包括單繼承和多重繼承。Cocos2dx-lua中class
的實現(xiàn)中,子類在定義時復(fù)制所有基類的方法,在實例化時將該類作為metatable
的__index
賦值給實例。
-- 類
-- classname 類名
-- ... 父類 可選參數(shù) 類型為table或function
function class(classname, ...)
local cls = {__cname = classname}
local supers = {...} --父類
-- 遍歷父類
for _,super in ipairs(supers) do
-- 判斷父類類型
local superType = type(super)
assert(
superType=='nil' or superType=='table' or superType=='function',
string.format("create class %s width invalid super class type %s", classname, superType)
)
if superType == 'function' then
assert(cls.__create==nil, string.format("create class %s with more than one creating function"), classname)
-- 若父類為函數(shù)則讓cls的create方法指向它
cls.__create = super
elseif superType == 'table' then
if super[".isclass"] then
assert(cls.__create==nil, string.format("create class %s with more than one creating function", classname))
-- 若父類是原生cocos類
cls.__create = function()
super:create()
end
else
-- 若父類為自定義類
cls.__supers = cls.__supers or {}
-- 保存cls的多個父類的table表
cls.__supers[#cls.__supers+1] = super
-- 將遍歷到第一個table作為cls的父類
if not cls.super then
cls.super = super
end
end
else
-- 若父類既不是table也不是function則報錯
-- 若父類不存在則不會執(zhí)行到此處,因此父類可以為nil。
error(string.format("create class %s with invalid super type", classname), 0)
end
end
-- 設(shè)置類,設(shè)置類的第一索引對象為自己,若實例對象找不到某參數(shù)的話,會查找該類是否包含該參數(shù),若不包含則向父類查找。
cls.__index = cls
if not cls.__supers or #cls.__supers==1 then
-- 若類僅有一個父類即單繼承則設(shè)置cls的父類為其元表
setmetatable(cls, {__index=cls.super})
else
-- 若類為多繼承,其索引是一個函數(shù),查找元素時會執(zhí)行該函數(shù)。
setmetatable(cls, {__index=function(_, key)
local supers = cls.__supers
for i=1,#supers do
local super = supers[i]
if super[key] then
return super[key]
end
end
end})
end
-- 判斷類的構(gòu)造器
if not cls.ctor then
cls.ctor = function() end
end
-- 類實例化方法
cls.new = function(...)
local instance
-- 判斷類是否存在create方法若存在則調(diào)用,若不存在則為普通類。
if cls.__create then
-- 存在create方法的類通過__index和元表的index,一級一級向上查找,直到原生cocos類。
instance = cls.__create(...)
else
-- 普通類即自定義的類時無create方法的
instance = {}
end
-- 設(shè)置實例的元表索引,誰調(diào)用了new就將其設(shè)置為實例的元表索引
setmetatableindex(instance, cls)
instance.class = cls
-- 實例調(diào)用構(gòu)造器
instance:ctor(...)
-- 返回實例
return instance
end
-- 創(chuàng)建類的實例
cls.create = function(_, ...)
return cls.new(...)
end
-- 返回類
return cls
end
-- 設(shè)置實例的元表索引
setmetatableindex = function(table, index)
if type(table)=='userdata' then
local peer = tolua.getpeer(table)
if not peer then
peer = {}
tolua.setpeer(table, peer)
end
setmetatableindex(peer, index)
else
local metatable = getmetatable(table)
if not metatable then metatable = {} end
if not metatable.__index then
metatable.__index = index
setmetatable(table, metatable)
elseif metatable.__index ~= index then
setmetatableindex(metatable, index)
end
end
end
繼承
繼承可用元表實現(xiàn),它提供了在父類中查找存在的方法和變量的機(jī)制。在Lua中不推薦使用繼承方式完成構(gòu)造的,這樣做引入的問題可能比解決的問題要多。
-- str_base.lua
local _M = {}
local mt = {__index = _M}
function _M.upper(str)
return string.upper(str)
end
return _M
-- str_extend.lua
local StrBase = require("str_base")
local _M = {}
-- 使用元表實現(xiàn)繼承,由于元表提供在父類中查找存在的方法和變量的機(jī)制。
_M = setmetatable(_M, {__index = StrBase})
function _M.lower(str)
return string.lower(str)
end
return _M
-- test.lua
local StrExtend = require("str_extend")
print( StrExtend.upper("Hello"), StrExtend.lower("Hello") )
由于類也是對象,類可從其他類中獲得方法,這種行為就是一種繼承。
需求:設(shè)置特殊賬戶并限制透支
-- 創(chuàng)建類
local Account = {
balance = 0
}
-- 實例化通過類創(chuàng)建對象
function Account:new(tbl)
-- 默認(rèn)初始化為空表
local tbl = tbl or {}
-- 設(shè)置自身作為元表
setmetatable(tbl, self)
-- 新對象繼承元表中的行為
self.__index = self
return tbl
end
-- 創(chuàng)建函數(shù)并將其存入對象的字段中
function Account:withdraw(val)
-- self:使函數(shù)只針對特定對象工作
if val > self.balance then
error "insufficient funds"
end
self.balance = self.balance - val
end
function Account:disposit(val)
self.balance = self.balance + val
end
-- 繼承,從類中派生子類
local SpecialAccount = Account:new()
-- 子類覆寫父類方法
function SpecialAccount:withdraw(val)
if val-self.balance >= self:getLimit() then
error "insufficient funds"
end
self.balance = self.balance - val
end
function SpecialAccount:getLimit()
return self.limit or 0
end
-- test
local obj = SpecialAccount:new{limit = 50}
obj:withdraw(10)
print(obj.balance, obj.limit)
成員私有性
Lua中的對象設(shè)計不提供私有性訪問機(jī)制,部分原因是使用通用數(shù)據(jù)結(jié)構(gòu)table
來表示對象的結(jié)果。Lua并沒有打算被用來進(jìn)行大型程序設(shè)計,相反,Lua的目標(biāo)定于小型或中型的編程設(shè)計,通常是作為大型系統(tǒng)的一部分。Lua避免太冗余和太多的人為限制。如果你不想訪問一個對象內(nèi)的東西就不要訪問。
If you do not want to access something inside an object, just do not do it.
然后,Lua的另一個目標(biāo)是靈活性,提供程序員元機(jī)制(meta-mechanisms),通過它你可以實現(xiàn)很多不同的機(jī)制。雖然Lua中基本的面向?qū)ο笤O(shè)計并不提供私有性訪問的機(jī)制,仍可用不同的方式來實現(xiàn)他。
設(shè)計的基本思想是:每個對象用2個表來表示,一個描述狀態(tài),一個描述操作(接口)。對象本身通過第2個表來訪問,也就是說,通過接口來訪問對象。為了避免未授權(quán)的訪問,表示狀態(tài)的表中不涉及到操作,表示操作的表也不涉及到狀態(tài)。取而代之的是狀態(tài)被保存在方法的閉包內(nèi)。
另外,在動態(tài)語言中引入成員私有性并沒有太大的必要,反而會顯著增加運行時的開銷,畢竟這種檢查無法像許多靜態(tài)語言那樣在編譯期完成。
在Lua中,成員的私有性,使用類似于函數(shù)閉包的形式來實現(xiàn)。將對象作為方法的upvalue
,本身是很巧妙的,但會讓子類繼承變得困難,同時構(gòu)造函數(shù)動態(tài)創(chuàng)建了函數(shù),會導(dǎo)致構(gòu)造函數(shù)無法被JIT編譯。
例如:使用工廠方法創(chuàng)建新的賬戶實例,通過工廠方法對外提供的閉包來暴露對外接口,而不想暴露在外的如balance
成員變量,則被很好的隱藏起來。
-- 使用工廠方法創(chuàng)建賬戶實例
function AccountInstance(initBalance)
local self = {
balance = initBalance
}
-- 使用閉包實現(xiàn)成員的私有性
local getBalance = function()
return self.balance
end
local deposit = function(v)
self.balance = self.balance + v
end
local withdraw = function(v)
self.balance = self.balance - v
end
return {
getBalance = getBalance,
deposit = deposit,
withdraw = withdraw
}
end
local account = AccountInstance(100)
account.deposit(10)
print( account.getBalance(), account.balance ) -- 110 nil
account.withdraw(10)
print( account.getBalance(), account.balance ) -- 100 nil
首先,函數(shù)創(chuàng)建一個表用來描述對象的內(nèi)部狀態(tài),并保存在局部變量self
內(nèi)。然后,函數(shù)為對象的每個方法創(chuàng)建閉包,也就是說,嵌套的函數(shù)實例。最后,函數(shù)創(chuàng)建并返回外部對象,外部對象中將局部變量名指向最終要實現(xiàn)的方法。這兒的關(guān)鍵點在于:這些方法沒有使用額外的參數(shù)self
,代替的是直接訪問self
。因為沒有這個額外的參數(shù),因此不能使用冒號語法來訪問這些對象,含住只能像其它函數(shù)一樣調(diào)用。
這種設(shè)計實現(xiàn)了任何存儲在self
表中的部分都是私有的,工廠方法返回實例后,沒有什么方法可以直接訪問對象,只能通過工廠函數(shù)中定義的函數(shù)來訪問。
例如:給某些取款用戶享有額外的10%的存款上限
function AccountInstance(initBalance)
local self = {
balance = initBalance,
limit = 1314
}
local withdraw = function(v)
self.balance = self.balance - v
end
local deposit = function(v)
self.balance = self.balance + v
end
local extra = function()
if self.balance > self.limit then
return self.balance * 0.01
else
return 0
end
end
local balance = function()
return self.balance + extra()
end
return {
withdraw = withdraw,
deposit = deposit,
balance = balance
}
end
local account = AccountInstance(100)
account.deposit(2000)
print(account.balance()) --2121
這樣,對于用戶而言就沒有辦法直接訪問extra
函數(shù),也就實現(xiàn)了Lua Private Function
。
使用table
來實現(xiàn)面向?qū)ο蟮木幊谭绞剑瑤缀蹩梢詫崿F(xiàn)所有面向?qū)ο蟮木幊烫匦裕鼪]有也不想去實現(xiàn)的就是對象的私密性,也就是C++里面的private
、public
、protected
,這與Lua的設(shè)計初衷有關(guān),Lua定位于小型程序開發(fā),參與一個工程的人不會很多,自行約束非要實現(xiàn)私密性的話Lua也不是可行,只是不能再使用table
和元表的方式了。
可使用上述方式實現(xiàn),在閉包中定義一個table
的upvalue
,然后把閉包函數(shù)都定義在這個table
中,然后返回這個table
,使用key
訪問內(nèi)部方法。使用閉包實現(xiàn)對象的方式比使用table
效率高并實現(xiàn)了絕對的私密性,但無法實現(xiàn)繼承,相當(dāng)于簡單的小對象,甚至可以在閉包中僅定義一個方法,然后通過key
來判斷調(diào)用的是什么方法。