Lua极简入门(十)——面向对象

在介绍完Lua的基础知识包括元表,函数式编程之后,终于到了Lua面向对象编程。虽然并不打算使用Lua进行大型应用系统(程序)的开发,最多可能是嵌入到某个系统之间,如在Redis中使用Lua脚本完成一些操作,或者使用Nginx+Lua完成服务限流或者日志收集,负载均衡;另外比如我这里目前计划使用Lua+Angular进行一个Web前端的项目开发;这些工作仍然绕不开面向对象。

面向对象编程OOP(Object Oriented Programming),作为一种编程思想随着互联网的发展,已经深入到现代系统编程的方方面面;OOP将对象作为程序的基础单元,包含了数据和操作函数;面向对象程序设计时,将业务中的事务进行抽象封装成多个对象,程序在执行时,数据和消息将在每个对象间流转,并按一定的操作流程依次执行。

类和实例是面向对象的重要概念,对于Java、C#等面向对象语言来说,一般都具有关键词class来标识和定义一个类,并组织类模板;对于Lua来说,并不具备类的概念,table是Lua的最基础对象,借助于tableLua也可以模拟类,但每个对象需要自己定义行为和状态。

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

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,774评论 0 38
  • 在lua原生语法特性中是不具备面向对象设计的特性。因此,要想在lua上像其他高级语言一样使用面向对象的设计方法有以...
    杰嗒嗒的阿杰阅读 11,720评论 3 20
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • 真是在过劳动节呀,看天气大好,各种的劳动清理扫除在这个阳光明媚的节日里开始了。 第一天,帮助小花园里的植物们清理枯...
    monica梅阅读 173评论 0 3
  • 第一步: win + R,打开“运行” 第二步: 输入cmd 第三步: 输入color a或color b改成自己...
    JingWenxing阅读 637评论 0 0