Lua的设计初衷并非意图构建完整的应用,而是在应用程序中为应用提供灵活的扩展和定制功能,所以Lua仅提供了基础的数学运算和字符串处理等函数库,而并未涵盖程序设计的各个方面。
Lua作为脚本语言甚至没有原生态的提供面向对象的支持,原生态的Lua中是没有类这个概念的。不过Lua提供的table
表这个强大的数据结构却赋予开发者自行实现面向对象意义上的类的能力。
类
在Lua中,使用表和函数实现面向对象,将函数和相关数据放置于同一个表中就形成了一个对象。
-- 银行账户 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
类与对象
- 对象是真实世界中的实体,对象与实体是一一对应的,也就是说现实世界中每个实体都是一个对象,它是一种具体的概念。
- 类是具备某些共同特征的实体的集合,是一种抽象的概念。
Lua中table
就是一种对象,这句话从3个方面可以得到证实:
- 表和对象一样可以拥有状态
- 表和对象一样拥有一个独立于其值的标识
- 表和对象一样具有独立于创建者和创建地的生命周期
-- 创建对象
local Account = {
balance = 0
}
-- 创建函数并将其存入对象的字段中
function Account.withdraw(val)
Account.balance = Account.balance - val
end
-- 使函数只针对特定对象工作
function Account:disposit(val)
self.balance = self.balance + val
end
一个类就像是一个创建对象的模具,由于Lua中没有类的概念,每个对象只能自定义行为和形态。Lua中模拟类可参照基于原型的语言,每个对象有有一个原型(prototype)。原型也是一种对象,当其他对象遇到未知操作时,原型会像查找它。因此,要表示一个类,只需要创建一个专门用来作为其他对象的原型。类和原型都是一种组织对象间共享行为的方式。简单来说,Lua本身没有类的概念,只有table
表,而面向对象的实现只不过是将表与父类的表连在一起,没有某个变量时就去父类查找。
需求:银行账户(Account)可存款(deposit)和取款(withdraw),取款时当前账户(balance)余额不足则提示用户。
-- 创建类
local Account = {
balance = 0
}
-- 实例化通过类创建对象
function Account:new(tbl)
-- 默认初始化为空表
local tbl = tbl or {}
-- 设置自身作为元表
setmetatable(tbl, self)
-- 新对象继承元表中的行为
self.__index = self
return tbl
end
-- 创建函数并将其存入对象的字段中
function Account:withdraw(val)
-- self:使函数只针对特定对象工作
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)
实现Lua面向对象可分解为类的定义和类的实例化两个问题,类的定义主要是实现继承,即怎么样子类拥有父类的方法集。类的实例化需要解决实例如何共享类的方法集,但独享自己的成员变量实例。
Cocos2dx-lua中有一个class
函数实现了类的基础,包括单继承和多重继承。Cocos2dx-lua中class
的实现中,子类在定义时复制所有基类的方法,在实例化时将该类作为metatable
的__index
赋值给实例。
-- 类
-- classname 类名
-- ... 父类 可选参数 类型为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)
-- 若父类为函数则让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则报错
-- 若父类不存在则不会执行到此处,因此父类可以为nil。
error(string.format("create class %s with invalid super type", classname), 0)
end
end
-- 设置类,设置类的第一索引对象为自己,若实例对象找不到某参数的话,会查找该类是否包含该参数,若不包含则向父类查找。
cls.__index = cls
if not cls.__supers or #cls.__supers==1 then
-- 若类仅有一个父类即单继承则设置cls的父类为其元表
setmetatable(cls, {__index=cls.super})
else
-- 若类为多继承,其索引是一个函数,查找元素时会执行该函数。
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
-- 判断类的构造器
if not cls.ctor then
cls.ctor = function() end
end
-- 类实例化方法
cls.new = function(...)
local instance
-- 判断类是否存在create方法若存在则调用,若不存在则为普通类。
if cls.__create then
-- 存在create方法的类通过__index和元表的index,一级一级向上查找,直到原生cocos类。
instance = cls.__create(...)
else
-- 普通类即自定义的类时无create方法的
instance = {}
end
-- 设置实例的元表索引,谁调用了new就将其设置为实例的元表索引
setmetatableindex(instance, cls)
instance.class = cls
-- 实例调用构造器
instance:ctor(...)
-- 返回实例
return instance
end
-- 创建类的实例
cls.create = function(_, ...)
return cls.new(...)
end
-- 返回类
return cls
end
-- 设置实例的元表索引
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
继承
继承可用元表实现,它提供了在父类中查找存在的方法和变量的机制。在Lua中不推荐使用继承方式完成构造的,这样做引入的问题可能比解决的问题要多。
-- 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 = {}
-- 使用元表实现继承,由于元表提供在父类中查找存在的方法和变量的机制。
_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") )
由于类也是对象,类可从其他类中获得方法,这种行为就是一种继承。
需求:设置特殊账户并限制透支
-- 创建类
local Account = {
balance = 0
}
-- 实例化通过类创建对象
function Account:new(tbl)
-- 默认初始化为空表
local tbl = tbl or {}
-- 设置自身作为元表
setmetatable(tbl, self)
-- 新对象继承元表中的行为
self.__index = self
return tbl
end
-- 创建函数并将其存入对象的字段中
function Account:withdraw(val)
-- self:使函数只针对特定对象工作
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中的对象设计不提供私有性访问机制,部分原因是使用通用数据结构table
来表示对象的结果。Lua并没有打算被用来进行大型程序设计,相反,Lua的目标定于小型或中型的编程设计,通常是作为大型系统的一部分。Lua避免太冗余和太多的人为限制。如果你不想访问一个对象内的东西就不要访问。
If you do not want to access something inside an object, just do not do it.
然后,Lua的另一个目标是灵活性,提供程序员元机制(meta-mechanisms),通过它你可以实现很多不同的机制。虽然Lua中基本的面向对象设计并不提供私有性访问的机制,仍可用不同的方式来实现他。
设计的基本思想是:每个对象用2个表来表示,一个描述状态,一个描述操作(接口)。对象本身通过第2个表来访问,也就是说,通过接口来访问对象。为了避免未授权的访问,表示状态的表中不涉及到操作,表示操作的表也不涉及到状态。取而代之的是状态被保存在方法的闭包内。
另外,在动态语言中引入成员私有性并没有太大的必要,反而会显著增加运行时的开销,毕竟这种检查无法像许多静态语言那样在编译期完成。
在Lua中,成员的私有性,使用类似于函数闭包的形式来实现。将对象作为方法的upvalue
,本身是很巧妙的,但会让子类继承变得困难,同时构造函数动态创建了函数,会导致构造函数无法被JIT编译。
例如:使用工厂方法创建新的账户实例,通过工厂方法对外提供的闭包来暴露对外接口,而不想暴露在外的如balance
成员变量,则被很好的隐藏起来。
-- 使用工厂方法创建账户实例
function AccountInstance(initBalance)
local self = {
balance = initBalance
}
-- 使用闭包实现成员的私有性
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
首先,函数创建一个表用来描述对象的内部状态,并保存在局部变量self
内。然后,函数为对象的每个方法创建闭包,也就是说,嵌套的函数实例。最后,函数创建并返回外部对象,外部对象中将局部变量名指向最终要实现的方法。这儿的关键点在于:这些方法没有使用额外的参数self
,代替的是直接访问self
。因为没有这个额外的参数,因此不能使用冒号语法来访问这些对象,含住只能像其它函数一样调用。
这种设计实现了任何存储在self
表中的部分都是私有的,工厂方法返回实例后,没有什么方法可以直接访问对象,只能通过工厂函数中定义的函数来访问。
例如:给某些取款用户享有额外的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
函数,也就实现了Lua Private Function
。
使用table
来实现面向对象的编程方式,几乎可以实现所有面向对象的编程特性,但它没有也不想去实现的就是对象的私密性,也就是C++里面的private
、public
、protected
,这与Lua的设计初衷有关,Lua定位于小型程序开发,参与一个工程的人不会很多,自行约束非要实现私密性的话Lua也不是可行,只是不能再使用table
和元表的方式了。
可使用上述方式实现,在闭包中定义一个table
的upvalue
,然后把闭包函数都定义在这个table
中,然后返回这个table
,使用key
访问内部方法。使用闭包实现对象的方式比使用table
效率高并实现了绝对的私密性,但无法实现继承,相当于简单的小对象,甚至可以在闭包中仅定义一个方法,然后通过key
来判断调用的是什么方法。