在介紹完Lua的基礎(chǔ)知識包括元表,函數(shù)式編程之后,終于到了Lua面向?qū)ο缶幊?。雖然并不打算使用Lua進(jìn)行大型應(yīng)用系統(tǒng)(程序)的開發(fā),最多可能是嵌入到某個系統(tǒng)之間,如在Redis中使用Lua腳本完成一些操作,或者使用Nginx+Lua完成服務(wù)限流或者日志收集,負(fù)載均衡;另外比如我這里目前計劃使用Lua+Angular進(jìn)行一個Web前端的項目開發(fā);這些工作仍然繞不開面向?qū)ο蟆?/p>
面向?qū)ο缶幊蘋OP(Object Oriented Programming),作為一種編程思想隨著互聯(lián)網(wǎng)的發(fā)展,已經(jīng)深入到現(xiàn)代系統(tǒng)編程的方方面面;OOP將對象作為程序的基礎(chǔ)單元,包含了數(shù)據(jù)和操作函數(shù);面向?qū)ο蟪绦蛟O(shè)計時,將業(yè)務(wù)中的事務(wù)進(jìn)行抽象封裝成多個對象,程序在執(zhí)行時,數(shù)據(jù)和消息將在每個對象間流轉(zhuǎn),并按一定的操作流程依次執(zhí)行。
類
類和實例是面向?qū)ο蟮闹匾拍?,對于Java、C#等面向?qū)ο笳Z言來說,一般都具有關(guān)鍵詞class來標(biāo)識和定義一個類,并組織類模板;對于Lua來說,并不具備類的概念,table是Lua的最基礎(chǔ)對象,借助于tableLua也可以模擬類,但每個對象需要自己定義行為和狀態(tài)。
以Person對象為例,借助table很容易實現(xiàn)一個類,這個類具有兩個屬性,和一個方法。
Person = { name = "ray", age = 0 }
Person.show = function(name, age)
Person.name = name
Person.age = age
print("姓名:" .. Person.name .. ",年齡:" .. Person.age)
end
Person.show("ray", 12)
-->> 姓名:ray,年齡:12
在這段代碼中,看似實現(xiàn)了一個類,但其實只是對方法的一個封裝,無法從該類創(chuàng)建不同的實例,類只是模板,使用類模板,可以創(chuàng)建出不同的實例,是面向?qū)ο蟮闹饕卣鳌H缟鲜鍪纠?,如果按照J(rèn)ava的方法聲明對象:
person = Person -- 按照Lua的特點(diǎn),只是將person指向了Person,并沒有聲明實例
Person = nil -- Person消亡,person也消亡
person.show("ray", 12) -- 異常,說明這并不是類,只是一個方法,只有一個對象的聲明周期
-->> attempt to index a nil value (global 'Person')
這個例子說明按這種方式,只是定義了一個方法。類是對事物的一種抽象,如Person應(yīng)該是對人的一種抽象,而應(yīng)用該類模板,可以聲明ray等等實例,其描述一個現(xiàn)實具體的人,按這種方式理解,當(dāng)生命多個實例時,每個實例的聲明周期都是獨(dú)立的,并不相互影響。Java中使用this來描述當(dāng)前實例,Lua中可以使用self作為接收者,描述當(dāng)前實例對象。
Person = { name = "ray", age = 0 }
Person.show = function(self, name, age)
self.name = name
self.age = age
print("姓名:" .. self.name .. ",年齡:" .. self.age)
end
person = Person
Person = nil
person.show(person, "ray", 12)
-->> 姓名:ray,年齡:12
每個方法都放置self參數(shù)太麻煩了,Lua也可以像Java一樣,編碼時對this實現(xiàn)隱藏,Lua可以隱藏self參數(shù),實現(xiàn)在編碼時不必顯式聲明self。Lua在聲明時,使用:達(dá)到隱藏self的目的。
Person = {}
function Person:setInfo(name, age)
self.name = name
self.age = age
end
function Person:show()
print("name:" .. self.name .. ", age: " .. self.age)
end
person = Person
Person:setInfo("ray", 12)
person:show()
-->> ray 12
使用:只是簡化了顯式self參數(shù)的傳入,包括調(diào)用和聲明時的傳遞,其他的和傳入self功能一致。如,聲明時使用:,調(diào)用時,使用.并傳入self效果一致。
-- 上例最后一步
person.show(person)
-->> ray 12
到這里L(fēng)ua使用table解決了類的獨(dú)立生命周期、隱藏self的問題,但是目前編寫的對象讓然不能稱之為類,最基礎(chǔ)的,沒有辦法從上述定義中,獨(dú)立聲明多個實例。比如上例聲明了person對象后,將無法再次聲明第二個實例。
person = Person
Person:setInfo("ray", 12)
person2 = Person
person2:setInfo("hh", 13)
person:show()
person2:show()
-->> name:hh, age: 13
-->> name:hh, age: 13
對于Java來說,類就是個抽象事物的模板,使用new關(guān)鍵詞,可以創(chuàng)建任意的實例,每一個實例都是具有模板中抽象的事務(wù)的獨(dú)立對象。Lua由于沒有類的概念,使用table模擬類時,如上例,聲明的對象將是同一個對象,這和類的表現(xiàn)不一致。為了解決獨(dú)立實例的問題,只能自己定義類的形態(tài)和行為。
在元表一章中,介紹過不同原型實現(xiàn)集成的功能,使用setmetatable、__index進(jìn)行元表設(shè)置,可以很容易的實現(xiàn)一個原型從另一個原型繼承。
當(dāng)訪問一個table中的字段時,Lua會先從table中查找該字段,如果存在,則返回該字段的值;如果沒有,則檢查該table是否具有元表,如果沒有元表,則返回nil;如果有元表,則會從元表中查找__index元方法,如果沒有該元方法,返回nil;如果有__index元方法,則從該方法中查找指定字段。__index方法可以返回一個函數(shù)、也可以返回一個table
仍然使用上述示例,使用元表編程的方式,對這個Person對象進(jìn)行修改,提供一個類似Java的new實例的方法,當(dāng)創(chuàng)建一個新的對象時,將該對象繼承Person的所有對象及方法,通過setmetatable讓新對象的原型指向self,并設(shè)置__index索引也指向self。
Person = {}
function Person:new(p)
-- 初始化,防止p(table)為空
p = p or {}
-- sefl為p的原型
setmetatable(p, self)
self.__index = self
-- 返回創(chuàng)建的實例,此時p將具備Person的所有對象
return p
end
function Person:show()
print("name:" .. self.name .. ", age: " .. self.age)
end
person = Person:new({ name = "ray", age = 12 })
person2 = Person:new({ name = "hh", age = 13 })
person:show()
person2:show()
-->> name:ray, age: 12
-->> name:hh, age: 13
在本例中,當(dāng)創(chuàng)建一個對象時,person=Person:new,在該方法中,設(shè)置了self為其元表(setmetatable(p, self)),即person的元表為Person;因此當(dāng)調(diào)用person:show()時,其實際調(diào)用為person.show(person),查找索引時會先從person的table中查找,未找到,則查找__index條目,上例中設(shè)置了self的__index為self本身,此時__index的元表也是Person,那么此時的調(diào)用為Person.show(person),找到show方法并執(zhí)行。
將類的定義抽象,并劃定步驟,那么Lua在創(chuàng)建一個類時,只需要兩步:
- 創(chuàng)建一個基礎(chǔ)原型table
- 創(chuàng)建一個實例化方法,并設(shè)置關(guān)聯(lián)元表以及__index
- 其他的方法定義均為table:functionName
A = {} -- 可具有默認(rèn)數(shù)據(jù)
function A:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
類定義完成后,在訪問屬性和方法時,
.訪問屬性,如A.b,:訪問方法,如A:function()
繼承
在面向?qū)ο缶幊讨?,繼承是另外一個非常重要的方面,比如當(dāng)我們要定義各個品牌的汽車時,哈弗、吉利、奇瑞等,汽車都是四個轱轆、四個門等,我們自然會想到,需要抽象出一個基礎(chǔ)類,讓其他品牌汽車都繼承基礎(chǔ)類。
以下步驟實現(xiàn)一個基礎(chǔ)的汽車對象,定義了四個車輪、四個車門和一個原型的方向盤,并提供了一個打印汽車基礎(chǔ)信息的方法:
Car = { wheel = 4, door = 4, steeringWheel = "circular" }
function Car:new(c)
c = c or {}
setmetatable(c, self)
self.__index = self
return c
end
function Car:showCarInfo()
print("車輪:" .. self.wheel .. ",車門:" .. self.door .. ",方向盤形狀:" .. self.steeringWheel)
end
基礎(chǔ)類定義完成后,我們將重新定義一個新的跑車類,讓跑車集成汽車類,第一步,先使用汽車類創(chuàng)建出一個默認(rèn)對象,并讓跑車指向該對象,此時,跑車將和轎車類具備一模一樣的方法。
SportCar = Car:new()
s = SportCar:new()
s:showCarInfo()
-->> 車輪:4,車門:4,方向盤形狀:circular
如果只是改變汽車的基礎(chǔ)屬性,或者是新增加新的屬性,則可以直接使用new方法傳遞對象的方式實現(xiàn)即可,并不需要新增代碼,如跑車的車門數(shù)量為2,此時仍然使用基類的創(chuàng)建方法即可完成。
SportCar = Car:new()
s = SportCar:new { door = 2 }
s:showCarInfo()
-->> 車輪:4,車門:2,方向盤形狀:circular
為了更清晰的明確繼承和新類的定義方法,可以重寫new函數(shù),如下:
SportCar = Car:new()
function SportCar:new(s)
s = s or Car:new(s)
setmetatable(s, self)
self.__index = self
return s
end
s = SportCar:new { door = 2, steeringWheel = "Hexagon" } -- 方向盤為更酷的六邊形
s:showCarInfo()
-->> 車輪:4,車門:2,方向盤形狀:Hexagon
實現(xiàn)基礎(chǔ)的對象繼承后,可以對新的跑車,添加額外的方法,比如跑車的最高時速可達(dá)200公里。
function SportCar:getMaxSpeed()
return self.maxSpeed .. "公里"
end
s = SportCar:new { door = 2, steeringWheel = "Hexagon", maxSpeed = 200 }
print(s:getMaxSpeed())
-->> 200公里
面向?qū)ο缶幊讨?,具有重寫方法的概念,對于實現(xiàn)了集成的Lua對象來說,也具備該功能。我們實現(xiàn)了跑車類后,新增最高時速,那么基類中的展示汽車的基礎(chǔ)屬性方法顯然無法滿足我們的需求,此時可以重寫該方法。
function SportCar:showCarInfo()
print("跑車:車輪:" .. self.wheel .. ",車門:" .. self.door .. ",方向盤形狀:" .. self.steeringWheel .. ",最高時速:" .. self:getMaxSpeed())
end
s = SportCar:new { door = 2, steeringWheel = "Hexagon", maxSpeed = 200 }
s:showCarInfo()
-->> 跑車:車輪:4,車門:2,方向盤形狀:Hexagon,最高時速:200公里
至此類的繼承已經(jīng)實現(xiàn)完成,將上述散亂的代碼合并在一起,如下:
Car = { wheel = 4, door = 4, steeringWheel = "circular" }
function Car:new(c)
c = c or {}
setmetatable(c, self)
self.__index = self
return c
end
function Car:showCarInfo()
print("車輪:" .. self.wheel .. ",車門:" .. self.door .. ",方向盤形狀:" .. self.steeringWheel)
end
SportCar = Car:new()
function SportCar:new(s)
s = s or Car:new(s)
setmetatable(s, self)
self.__index = self
return s
end
function SportCar:getMaxSpeed()
return self.maxSpeed .. "公里"
end
function SportCar:showCarInfo()
print("跑車:車輪:" .. self.wheel .. ",車門:" .. self.door .. ",方向盤形狀:" .. self.steeringWheel .. ",最高時速:" .. self:getMaxSpeed())
end
c = Car:new()
s = SportCar:new { door = 2, steeringWheel = "Hexagon", maxSpeed = 200 }
c:showCarInfo()
s:showCarInfo()
-->> 車輪:4,車門:4,方向盤形狀:circular
-->> 跑車:車輪:4,車門:2,方向盤形狀:Hexagon,最高時速:200公里
訪問限制
訪問限制是面向?qū)ο蟮牧硗庖粋€方面,對于Java來說,可以通過private、protected、public很容易實現(xiàn)訪問權(quán)限控制,而對于Lua來說,類都是不具備的,私密控制同樣沒有;Lua是使用table進(jìn)行的模擬實現(xiàn)類,那么和Lua閉包相結(jié)合,也可以實現(xiàn)私密訪問。
function Car()
local _M = {
wheel = 4, door = 4, steeringWheel = "circular"
}
function _M:new(c)
c = c or {}
setmetatable(c, self)
for k, v in pairs(self) do
if not o[k] then
o[k] = v
end
end
self.__index = self
return c
end
local function run()
print("普通轎車,100公里每小時速度進(jìn)行行駛")
end
function _M:showCarInfo()
print("車輪:" .. self.wheel .. ",車門:" .. self.door .. ",方向盤形狀:" .. self.steeringWheel)
run()
end
return _M
end
c = Car()
c:showCarInfo()
c.run() -- 外部無法使用
-->> 車輪:4,車門:4,方向盤形狀:circular
-->> 普通轎車,100公里每小時速度進(jìn)行行駛
這種方式的實現(xiàn)原理是采用了兩個元表,公開的方法,都放入到_M元表中,并于最后返回,不公開的方法,都存儲在本身元表中。
一般情況下,對于模塊(類)的定義可以固化為如下形式
local _M = {
_VERSION = "1.0",
_NAME = "Http 方法封裝"
}
-- 1. 私有方法放置在這里
local function joinParam(param)
local str = ""
for i, v in pairs(param) do
if str ~= "" then
str = str .. "&"
end
str = str .. i .. "=" .. v
end
return str
end
local function request(url, param, method)
return "向" .. url .. "發(fā)起" .. method .. "方法,傳遞參數(shù):" .. joinParam(param)
end
-- 2. new方法
function _M:new()
local o = o or {}
setmetatable(o, self)
for k, v in pairs(self) do
if not o[k] then
o[k] = v
end
end
self.__index = self
return o
end
-- 3. 公開的方法
function _M:get(url, param)
return request(url, param, "GET")
end
-- 4. 返回_M對象
return _M
在其他類中引用該對象發(fā)起http請求
-- testHttp為上述類的文件名,如果有路徑也需要定義,如path.fileName
local http = require("testHttp"):new()
local content = http:get("http://baidu.com", { uid = "ray", pwd = "111111" })
print(content)
-->> 向http://baidu.com發(fā)起GET方法,傳遞參數(shù):uid=ray&pwd=111111