Lua面向对象

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++里面的privatepublicprotected,这与Lua的设计初衷有关,Lua定位于小型程序开发,参与一个工程的人不会很多,自行约束非要实现私密性的话Lua也不是可行,只是不能再使用table和元表的方式了。

可使用上述方式实现,在闭包中定义一个tableupvalue,然后把闭包函数都定义在这个table中,然后返回这个table,使用key访问内部方法。使用闭包实现对象的方式比使用table效率高并实现了绝对的私密性,但无法实现继承,相当于简单的小对象,甚至可以在闭包中仅定义一个方法,然后通过key来判断调用的是什么方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,743评论 0 38
  • 君有疾否by入似我闻 文案 “世誉,我心不假。”楚明允将手隐入袖中掐了自己一把,言辞深情。 苏世誉的笑容忽然深了,...
    我叫隔壁老王阅读 267评论 0 0
  • 永遇乐 苏轼 明月如霜,好风如水,清景无限。 曲港跳鱼,圆荷泻露,寂寞无人见。 紞如三鼓,铮然一叶,黯黯梦云惊...
    顧勇詩書阅读 284评论 0 4
  • 在和丽表姐的谈话中我知道燕表姐竟然也怀孕三个多月了。 你们这是做什么?准备凑一块做月子吗? 丽表姐笑笑:那当然了,...
    喜暮雨阅读 347评论 1 2
  • 今天从网上看到一篇的文章,名字就叫《你想过自己会孤独终老吗》文章主旨是质疑一个奔三的朋友找了一个不怎么喜欢也不怎么...
    哈哈哈max阅读 598评论 0 2