Lua面向对象编程详解

前言

Lua并非严格意义上的面向对象语言,在语言层面上并没有直接提供诸如class这样的关键字,也没有显式的继承语法和virtual函数,但Lua提供了一种创建这些面向对象要素的能力。

Lua面向对象编程简述

Lua中的table就是一种对象
1.table与对象一样可以拥有状态
2.table也是对象一样拥有一个独立于其值的标识(一个self)
3.table与对象一样具有独立于创建者和创建地的生命周期
先看看以下代码:

local tab1 = {a = 1, b = 2}
local tab2 = {a = 1, b = 2}
if tab1 == tab2 then
    print("tab1 == tab2")
else
    print("tab1 ~= tab2")   -->tab1 ~= tab2
end 

上述说明两个具有相同值的对象(table)是两个不同的对象。对象有其自己的操作,同样table也有这些操作:

Account = {balance = 0}
function Account.withdraw( v )
     Account.balance = Account.balance - v
end
Account.withdraw(100.00)
print(Account.balance)   -->-100

a = Account
Account = nil
a.withdraw(100.00)  --> attempt to index global 'Account' (a nil value)

上面的代码创建了一个新函数,并将该函数存入Account对象的withDraw字段中,然后我们就可以调用该函数了。不过,这个特定对象还必须存储在特定的全局变量中。如果改变了对象的名称,withDraw就再也不能工作了,出现attempt to index global 'Account' (a nil value)
这种行为违反了前面提到的对象特性,即对象拥有独立的生命周期。而我们可以通过增加一个参数来表示接受者,这个参数通常称为self或this:

Account = {balance = 0}
function Account.withdraw( self, v )
    self.balance = self.balance - v
end
--基于修改后代码的调用:
a1 = Account
Account = nil 
a1.withdraw(a1,100.00)
--正常工作。
print(a1.balance)  -->-100 

使用self参数是所有面向对象语言的一个核心。大多数面向对象语言都对程序员隐藏了self参数, Lua只需要使用冒号,则能隐藏该参数,重写为:

function Account:withdraw( v )
    self.balance = self.balance - v
end

调用时可写为:

a1:withdraw(100.00)

冒号的作用是在方法定义中添加一个额外的隐藏参数,以及在一个方法调用中添加一个额外的实参。冒号只是一种语法便利,并没有引入任何新的东西。
现在的对象已有一个标识、状态和状态之上的操作。不过还缺乏一个类系统、继承和私密性。

一个类就像是一个创建对象的模具。在Lua中则没有类的概念,不过要在Lua中去模拟类并不困难。在Lua中,要表示一个类,只需创建一个专用作其他对象的原型。原型也是一种常规的对象,也就是说我们可以直接通过原型去调用对应的方法。当其它对象(类的实例)遇到一个未知操作时,原型会先查找它。
在Lua中实现原型是非常简单的,比如有两个对象a和b,要让b作为a的原型,只需要以下代码就可以完成:

setmetatable(a, {__index = b})

在此以后,a就会在b中查找所有它没有的操作。若将b称为是对象a的“类”,就仅仅是术语上的变化。现在我就从最简单的开始,要创建一个实例对象,必须要有一个原型,就是所谓的“类”,看以下代码:

local Account = {} -- 一个原型

好了,现在有了原型,那如何使用这个原型创建一个“实例”呢?接着看以下代码:

--new可看作为构造函数
function Account:new(o)  
     o = o or {}  -- 如果用户没有提供table,则创建一个
     setmetatable(o, self)
     self.__index = self
     return o
end

当调用Account:new时,self就相当于Account。接着,我们就可以调用Account:new来创建一个实例了。再看:

local a = Account:new{balance = 100} -- 这里使用原型Account创建了一个对象
a:deposit(100.00)

上面这段代码是如何工作的呢?首先使用Account:new创建了一个新的实例对象,并将Account作为新的实例对象a的元表。再当我们调用a:deposit(100.00)函数时,就相当于a.deposit(a),冒号就只是一个“语法糖”,只是一种方便的写法。我们创建了一个实例对象a,当调用deposit时,就会查找a中是否有deposit字段,没有的话,就去搜索它的元表,所以,最终的调用情况如下:

getmetatable(a).__index.deposit(a, 100.00)

a的元表是Account,Account的__index也是Account。因此,上面的调用也可以使这样的:

Account.deposit(a, 100.00)

所以,其实我们可以看到的是,实例对象a表中并没有deposit方法,而是继承自Account方法的,但是传入deposit方法中的self确是a。这样就可以让Account(这个“类”)定义操作。除了方法,a还能从Account继承所有的字段。

继承不仅可以用于方法,还可以作用于字段。因此,一个类不仅可以提供方法,还可以为实例中的字段提供默认值。看以下代码:

 --[[ 
 在这段代码中,我们可以将Account视为class的声明,如Java中的: 
 public class Account  
 { 
    public float balance = 0; 
    public Account(Account o); 
    public void deposite(float f); 
 } 
--这里balance是一个公有的成员变量。
 --]]
local Account = {balance = 0}

--new可以视为构造函数
function Account:new(o) 
     o = o or {}  -- 如果用户没有提供table,则创建一个
     setmetatable(o, self)
     self.__index = self
     return o
end

function Account:deposit()
     self.balance = self.balance + 100
     print(self.balance)
end

local b = Account:new{} -- 这里使用原型Account创建了一个对象b
b:deposit() -->100
b:deposit() -->200

在Account表中有一个balance字段,默认值为0;当我创建了实例对象b时,并没有提供balance字段,在deposit函数中,由于b中没有balance字段,就会查找元表Account,最终得到了Account中balance的值,等号右边的self.balance的值就来源自Account中的balance。调用b:deposit()时,其实就调用以下代码:

b.deposit(b)

在deposit的定义中,就会变成这样子:

b.deposit = getmetatable(b).__index.balance + 100

第一次调用deposit时,等号左侧的self.balance就是b.balance,就相当于在b中添加了一个新的字段balance;当第二次调用deposit函数时,由于b中已经有了deposit字段,所以就不会去Account中寻找deposit字段了。

继承

由于类也是对象(准确地说是一个原型),它们也可以从其它类(原型)获得(继承)方法。这种行为就是继承,可以很容易的在Lua中实现。
假设有一个基类Account:

local Account = {balance = 0}

function Account:new(o)
     o = o or {}
     setmetatable(o, self)
     self.__index = self
     return o
end

function Account:deposit( v )
    self.balance = self.balance + v
end

function Account:withdraw( v )
    if v > self.balance then error("insufficient funds") end 
    self.balance = self.balance - v
end

现在需要从这个Account类派生出一个子类SpecialAccount ,则需要创建一个空的类,从基类继承所有的操作:

SpecialAccount = Account:new()

现在,我创建了一个Account类的一个实例对象,在Lua中,现在SpecialAccount 既是Account类的一个实例对象,也是一个原型,就是所谓的类,就相当于SpecialAccount 类继承自Account类。再如下面的代码:

s = SpecialAccount:new{limit=1000.00}

SpecialAccount 从Account继承了new;不过,在执行SpecialAccount :new时,它的self参数表示为SpecialAccount ,所以s的元表为SpecialAccount ,SpecialAccount 中字段__index的值也是SpecialAccount 。然后,我们就会看到,s继承自SpecialAccount ,而SpecialAccount 又继承自Account。当执行s:deposit(100.00)时,Lua在s中找不到deposit字段,就会查找SpecialAccount ;如果仍然找不到deposit字段,就查找Account,最终会在Account中找到deposit字段。可以这样想一下,如果在SpecialAccount 中存在了deposit字段,那么就不会去Account中再找了。所以,我们就可以在SpecialAccount 中重定义deposit字段,从而实现特殊版本的deposit函数。

多重继承

实现单继承时,依靠的是为子类设置metatable,设置其metatable为父类,并将父类的__index设置为其本身的技术实现的。而多继承也是一样的道理,在单继承中,如果子类中没有对应的字段,则只需要在一个父类中寻找这个不存在的字段;而在多重继承中,如果子类没有对应的字段,则需要在多个父类中寻找这个不存在的字段。

Lua会在多个父类中逐个的搜索deposit字段。这样,我们就不能像单继承那样,直接指定__index为某个父类,而是应该指定__index为一个函数,在这个函数中指定搜索不存在的字段的规则。这样便可实现多重继承。这里就出现了两个需要去解决的问题:

保存所有的父类;
指定一个搜索函数来完成搜索任务。

-- 在多个父类中查找字段k
local function search(k, pList)
    for i = 1, #pList do
        local v = pList[i][k]
        if v then
            return v
        end
    end
end

function createClass(...)
    local c = {} -- 新类
    local parents = {...}

    -- 类在其元表中搜索方法
    setmetatable(c, {__index = function (t, k) return search(k, parents) end})

    -- 将c作为其实例的元表
    c.__index = c

    -- 为这个新类建立一个新的构造函数
    function c:new(o)
        o = o or {}
        setmetatable(o, self)

        -- self.__index = self 这里不用设置了,在上面已经设置了c.__index = c
        return o
    end

    -- 返回新的类(原型)
    return c
end

-- 一个简单的类CA
local CA = {}
function CA:new(o)
    o = o or {}
    setmetatable(o, {__index = self})
    self.__index = self
    return o
end

function CA:setName(strName)
    self.name = strName
end

-- 一个简单的类CB
local CB = {}
function CB:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

function CB:getName()
    return self.name
end

-- 创建一个c类,它的父类是CA和CB
local c = createClass(CA, CB)

-- 使用c类创建一个实例对象
local objectC = c:new{name = "Paul"}

-- 设置objectC对象一个新的名字
objectC:setName("John")
local newName = objectC:getName()
print(newName)

使用createClass创建了一个类(原型),将CA和CB设置为这个类(原型)的父类(原型);在创建的这个类(原型)中,设置了该类的__index为一个search函数,在这个search函数中寻找在创建的类中没有的字段;
创建的新类中,有一个构造函数new;这个new和之前的单继承中的new区别不大,很好理解;
调用new构造函数,创建一个实例对象,该实例对象有一个name字段;
调用object:setName(“John”)语句,设置一个新的名字;但是在objectC中没有这个字段,怎么办?好了,去父类找,先去CA找,一下子就找到了,然后就调用了这个setName,setName中的self指向的是objectC;设置以后,就相当于修改了objectC字段的name值;
调用objectC:getName(),objectC还是没有这个字段。找吧,CA也没有,那就接着找,在CB中找到了,就调用getName,在getName中的self指向的是objectC。所以,在objectC:getName中返回了objectC中name的值,就是“John”。

私密性

我们都知道,在C++或Java中,对于类中的成员函数或变量都有访问权限的。public,protected和private这几个关键字还认识吧。那么在Lua中呢?Lua中是本身就是一门“简单”的脚本语言,本身就不是为了大型项目而生的,所以,它的语言特性中,本身就没有带有这些东西,那如果非要用这样的保护的东西,该怎么办?我们还是“曲线救国”。思想就是通过两个table来表示一个对象。一个table用来保存对象的私有数据;另一个用于对象的操作。对象的实际操作时通过第二个table来实现的。为了避免未授权的访问,保存对象的私有数据的表不保存在其它的table中,而只是保存在方法的closure中。看一段代码:

function newObject(defaultName)
     local self = {name = defaultName}
     local setName = function (v) self.name = v end
     local getName = function () return self.name end
     return {setName = setName, getName = getName}
end

local objectA = newObject("John")
objectA.setName("John") -- 这里没有使用冒号访问
print(objectA.getName())

这种设计给予存储在self table中所有东西完全的私密性。当调用newObject返回以后,就无法直接访问这个table了。只能通过newObject中创建的函数来访问这个self table;也就相当于self table中保存的都是私有的,外部是无法直接访问的。大家可能也注意到了,我在访问函数时,并没有使用冒号,这个主要是因为,我可以直接访问的self table中的字段,所以是不需要多余的self字段的,也就不用冒号了。

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

推荐阅读更多精彩内容

  • 1.1程序块:Lua执行的每段代码,例如一个源代码文件或者交互模式中输入的一行代码,都称为一个程序块 1.2注释:...
    c_xiaoqiang阅读 2,572评论 0 9
  • 第一篇 语言 第0章 序言 Lua仅让你用少量的代码解决关键问题。 Lua所提供的机制是C不擅长的:高级语言,动态...
    testfor阅读 2,562评论 1 7
  • 2.5 面向对象编程 来源:2.5 Object-Oriented Programming 译者:飞龙 协议:...
    布客飞龙阅读 672评论 0 34
  • 很久都没上来写文章了,对于大多数人来说这里更像一个发现机会的平台,在我这更像一个私密的小窝,说一些不足为人道的话。...
    Mocha_young阅读 205评论 0 0
  • vjjjjhvccfvvbb
    大王大阅读 325评论 0 0