在介绍完Lua的基础知识包括元表,函数式编程之后,终于到了Lua面向对象编程。虽然并不打算使用Lua进行大型应用系统(程序)的开发,最多可能是嵌入到某个系统之间,如在Redis中使用Lua脚本完成一些操作,或者使用Nginx+Lua完成服务限流或者日志收集,负载均衡;另外比如我这里目前计划使用Lua+Angular进行一个Web前端的项目开发;这些工作仍然绕不开面向对象。
面向对象编程OOP(Object Oriented Programming),作为一种编程思想随着互联网的发展,已经深入到现代系统编程的方方面面;OOP将对象作为程序的基础单元,包含了数据和操作函数;面向对象程序设计时,将业务中的事务进行抽象封装成多个对象,程序在执行时,数据和消息将在每个对象间流转,并按一定的操作流程依次执行。
类
类和实例是面向对象的重要概念,对于Java、C#等面向对象语言来说,一般都具有关键词class
来标识和定义一个类,并组织类模板;对于Lua来说,并不具备类的概念,table
是Lua的最基础对象,借助于table
Lua也可以模拟类,但每个对象需要自己定义行为和状态。
以Person
对象为例,借助table很容易实现一个类,这个类具有两个属性,和一个方法。
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
在这段代码中,看似实现了一个类,但其实只是对方法的一个封装,无法从该类创建不同的实例,类只是模板,使用类模板,可以创建出不同的实例,是面向对象的主要特征。如上述示例,如果按照Java的方法声明对象:
person = Person -- 按照Lua的特点,只是将person指向了Person,并没有声明实例
Person = nil -- Person消亡,person也消亡
person.show("ray", 12) -- 异常,说明这并不是类,只是一个方法,只有一个对象的声明周期
-->> attempt to index a nil value (global 'Person')
这个例子说明按这种方式,只是定义了一个方法。类是对事物的一种抽象,如Person
应该是对人
的一种抽象,而应用该类模板,可以声明ray
等等实例,其描述一个现实具体的人,按这种方式理解,当生命多个实例时,每个实例的声明周期都是独立的,并不相互影响。Java中使用this
来描述当前实例,Lua中可以使用self
作为接收者,描述当前实例对象。
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
参数太麻烦了,Lua也可以像Java一样,编码时对this
实现隐藏,Lua可以隐藏self
参数,实现在编码时不必显式声明self
。Lua在声明时,使用:
达到隐藏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
参数的传入,包括调用和声明时的传递,其他的和传入self
功能一致。如,声明时使用:
,调用时,使用.
并传入self
效果一致。
-- 上例最后一步
person.show(person)
-->> ray 12
到这里Lua使用table
解决了类的独立生命周期、隐藏self
的问题,但是目前编写的对象让然不能称之为类
,最基础的,没有办法从上述定义中,独立声明多个实例。比如上例声明了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
关键词,可以创建任意的实例,每一个实例都是具有模板中抽象的事务的独立对象。Lua由于没有类的概念,使用table模拟类时,如上例,声明的对象将是同一个对象,这和类的表现不一致。为了解决独立实例的问题,只能自己定义类的形态和行为。
在元表
一章中,介绍过不同原型实现集成的功能,使用setmetatable
、__index
进行元表设置,可以很容易的实现一个原型从另一个原型继承。
当访问一个table中的字段时,Lua会先从table中查找该字段,如果存在,则返回该字段的值;如果没有,则检查该table是否具有元表,如果没有元表,则返回nil;如果有元表,则会从元表中查找__index元方法,如果没有该元方法,返回nil;如果有__index元方法,则从该方法中查找指定字段。__index方法可以返回一个函数、也可以返回一个table
仍然使用上述示例,使用元表编程的方式,对这个Person
对象进行修改,提供一个类似Java的new
实例的方法,当创建一个新的对象时,将该对象继承Person
的所有对象及方法,通过setmetatable
让新对象的原型指向self
,并设置__index
索引也指向self
。
Person = {}
function Person:new(p)
-- 初始化,防止p(table)为空
p = p or {}
-- sefl为p的原型
setmetatable(p, self)
self.__index = self
-- 返回创建的实例,此时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
在本例中,当创建一个对象时,person=Person:new
,在该方法中,设置了self
为其元表(setmetatable(p, self)
),即person的元表为Person
;因此当调用person:show()
时,其实际调用为person.show(person)
,查找索引时会先从person的table中查找,未找到,则查找__index
条目,上例中设置了self
的__index
为self本身,此时__index
的元表也是Person
,那么此时的调用为Person.show(person)
,找到show
方法并执行。
将类的定义抽象,并划定步骤,那么Lua在创建一个类时,只需要两步:
- 创建一个基础原型table
- 创建一个实例化方法,并设置关联元表以及__index
- 其他的方法定义均为table:functionName
A = {} -- 可具有默认数据
function A:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
类定义完成后,在访问属性和方法时,
.
访问属性,如A.b
,:
访问方法,如A:function()
继承
在面向对象编程中,继承是另外一个非常重要的方面,比如当我们要定义各个品牌的汽车时,哈弗、吉利、奇瑞等,汽车都是四个轱辘、四个门等,我们自然会想到,需要抽象出一个基础类,让其他品牌汽车都继承基础类。
以下步骤实现一个基础的汽车对象,定义了四个车轮、四个车门和一个原型的方向盘,并提供了一个打印汽车基础信息的方法:
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()
s = SportCar:new()
s:showCarInfo()
-->> 车轮:4,车门:4,方向盘形状:circular
如果只是改变汽车的基础属性,或者是新增加新的属性,则可以直接使用new方法传递对象的方式实现即可,并不需要新增代码,如跑车的车门数量为2,此时仍然使用基类的创建方法即可完成。
SportCar = Car:new()
s = SportCar:new { door = 2 }
s:showCarInfo()
-->> 车轮:4,车门:2,方向盘形状:circular
为了更清晰的明确继承和新类的定义方法,可以重写new函数,如下:
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
实现基础的对象继承后,可以对新的跑车,添加额外的方法,比如跑车的最高时速可达200公里。
function SportCar:getMaxSpeed()
return self.maxSpeed .. "公里"
end
s = SportCar:new { door = 2, steeringWheel = "Hexagon", maxSpeed = 200 }
print(s:getMaxSpeed())
-->> 200公里
面向对象编程中,具有重写方法的概念,对于实现了集成的Lua对象来说,也具备该功能。我们实现了跑车类后,新增最高时速,那么基类中的展示汽车的基础属性方法显然无法满足我们的需求,此时可以重写该方法。
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公里
至此类的继承已经实现完成,将上述散乱的代码合并在一起,如下:
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公里
访问限制
访问限制是面向对象的另外一个方面,对于Java来说,可以通过private
、protected
、public
很容易实现访问权限控制,而对于Lua来说,类都是不具备的,私密控制同样没有;Lua是使用table进行的模拟实现类,那么和Lua闭包相结合,也可以实现私密访问。
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公里每小时速度进行行驶")
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公里每小时速度进行行驶
这种方式的实现原理是采用了两个元表,公开的方法,都放入到_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 .. "发起" .. method .. "方法,传递参数:" .. 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
在其他类中引用该对象发起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发起GET方法,传递参数:uid=ray&pwd=111111