Lua 面向对象

一、前言

Lua 中一张表 table 就是一个对象(注意不是类,是对象)。他具有以下的特性:

  1. 表和对象都可以拥有状态
  2. 表和对象都拥有一个与其值无关的标识(self)
  3. 两个具有相同值的对象是两个不同的对象,而一个对象可以具有多个不同的值
  4. 表和对象一样,具有与创建者和被创建者位置无关的生命周期

二、类

1、 Lua 的类

Lua 并没有类这一概念,而是通过 "基于原型" 的方式进行组织,而原型其实也是一个实例对象。

换而言之,我们通过元表的方式,让 Lua 的实例对象在找不到对应的属性或方法时,从其元表的 __index 属性指定的表(即上面所说的原型,其实他也是实例对象)或方法中获取 ,而如果这里指定的表也没有对应的属性,则继续往该表(也就是原型)的元表继续查找,直到查找到或全部查找完还没有找到对应的属性或方法为止。

2、self 字段

在类中,this(在 Lua 中是 self) 很重要,因为他可以让不同实例调用同一方法不会有互相干扰

举个例子

下面的例子中,withdraw 方法内部固定写了 Account ,所以只要将 Account 删除,则会有异常

lua 复制代码
local Account = { balance = 0 }
function Account.withdraw(v)
    Account.balance = Account.balance - v
end

Account.withdraw(100)
print(Account.balance)              --> -100

local a, Account = Account, nil
a.withdraw(100)                     --> 这里会报错,因为 withdraw 内部使用了 Account ,而这个值已经被移除

所以这里如果有一个可以指向自身的指针就可以避免这一问题,则将 function Account.withdraw(v) 方法多加一个 self 参数,来决定函数内部的操作是针对哪个实例,变成为 function Account.withdraw(self, v) ,改变后的代码:

lua 复制代码
local Account = { balance = 0 }
function Account.withdraw(self, v)
    self.balance = self.balance - v
end

local a = Account
Account = nil
a.withdraw(a, 100)
print(a.balance)

但是这样的使用就会麻烦一些,每次使用都需要将自己传入,Lua 有一个语法糖,如果第一个参数是指向自身,则可以使用冒号(:)进行调用。

同时也可以使用冒号(:)进行定义方法,这样会自动在参数的最前面添加一个 self 的参数,方法内部就可以进行使用了,具体代码如下:

冒号定义的方法,也可以使用点方式调用,只是需要传入多一个参数指向自己

lua 复制代码
local Account = { balance = 0 }
-- 使用 : 就相当于 function Account.withdraw(self, v)
function Account:withdraw(v)
    self.balance = self.balance - v
end

local a = Account
Account = nil
a:withdraw(100)
print(a.balance)

-- 和上面一样
a.withdraw(a, 100)
print(a.balance)

3、类的 "实例化"

首先,创建一个 Account ,这个就类似 java、kotlin 中的类,但其实在 Lua 中,他是一个 table 实例( Lua 中没有类概念)。

lua 复制代码
local Account = { balance = 0 }
function Account:new(o)
    o = o or {}
    -- 将自己设置为 __index 的元方法
    self.__index = self
    -- 将自己设置为 o 的元表,这样就会调用 self 的 __index 方法或表,这里就是 self 自身表
    setmetatable(o, self)
    return o
end
-- 使用 : 就相当于 function Account.withdraw(self, v)
function Account:withdraw(v)
    self.balance = self.balance - v
end

Account 给自己添加一个 __index 元方法,并指向自己,然后将自己设置给新创建的 table 作为元表,最后将该 table 返回,他就是我们需要的对象了。

然后,进行创建和使用,这样的两个实例就不会互相影响。

lua 复制代码
local a = Account:new()
a:withdraw(100)
print("a.balance", a.balance)       --> a.balance	-100
-- 这里调用 withdraw 后,a 和 b 自身就有 balance 字段了,也就不需要进行元表的查询
local b = Account:new()
print("b rawget", rawget(b, "balance")) --> b rawget	nil
b:withdraw(1000)
print("b.balance", b.balance)           --> b.balance	-1000
print("b rawget", rawget(b, "balance")) --> b rawget	-1000

print("a.balance", a.balance)           --> a.balance	-100

值得一提的是,在经过 withdraw 方法之后, a 和 b 两个实例就都有了各自的 balance 属性。

一图胜千言

4、继承

从上面的图可以知道,我们只需要在这个元表的搜索链插入我们需要的节点,则能够达到继承的效果。

还是基于上面的 Account 代码,继承一个 SpecialAccount 类(其实也是一个实例对象,Lua 中没有类概念,只是我们叫法的区分)

lua 复制代码
local SpecialAccount = Account:new()
-- 重写了 withdraw 方法
function SpecialAccount:withdraw(v)
    print(self, "SpecialAccount withdraw")
    if v - self.balance >= self:getLimit() then
        error "insufficient funds"
    end
    self.balance = self.balance - v
end
-- 增加方法
function SpecialAccount:getLimit()
    print(self, "SpecialAccount getLimit")
    return self.limit or 0
end

创建对象,则用 SpecialAccount 进行创建,进行调用 withdraw 方法时,这时调用的则就是 SpecialAccount 中定义的方法

lua 复制代码
local person = SpecialAccount:new({ limit = 1000 })
person:withdraw(10)
print(person.balance)
--> table: 0x600002f4c240	Account new
--> table: 0x600002f4c4c0	SpecialAccount withdraw
--> table: 0x600002f4c4c0	SpecialAccount getLimit
--> -10

这里关键在于 local person = SpecialAccount:new({ limit = 1000 }) 的时候,new 方法内部的 self 指向的是 SpecialAccount ,所以元表链就建立起来了。

一图胜千言

5、为实例添加额外方法

和 java、kotlin 不太一样的是,Lua 的对象可以自行添加一些属性或方法,当然也可以重写父类的属性或方法,因为本质上 Lua 的对象就是一个表。

基于上面的代码,我们对 person 进行重写 getLimit 方法,这样在调用 withdraw 方法时,内部调用的就是 person 对象的 getLimit 方法

lua 复制代码
-- 给 person 自定义一个方法
function person:getLimit()
    print(self, "person getLimit")
    return self.balance * 0.10
end
-- 这个时候的限制就变为了自身的 getLimit
person.balance = 100000
person:withdraw(10)     
print(person.balance)       
--> table: 0x600002f4c4c0	SpecialAccount withdraw
--> table: 0x600002f4c4c0	person getLimit
--> 99990

6、多重继承

在 java、kotlin 中,没有多继承这一概念。而在 Lua 中,可以非常容易的实现,因为搜索链的本质是基于元表中的 __index 方法。之前都是设置的表,如果需要多重继承,只需要设置为方法,在方法中实现相应的搜索方式即可。

lua 复制代码
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 = { ... }

    c.__index = function(t, k)
        local v = search(k, parents)
        -- 这里可以保存下来,只是后续修改方法的定义就会比较困难,因为不会再走元表链
        -- t[k] = v
        return v
    end

    function c:new(o)
        o = o or {}
        setmetatable(o, c)
        return o
    end

    return c
end

只需要通过 createClass 方法,传入需要继承的父类,然后在 __index 的函数中搜索这些父类是否有满足的属性或方法即可。其余的操作和单继承是一样的。

继续使用之前的 Account 类,我们多编写一个 Named 类,进行多继承使用

lua 复制代码
local Named = {}
function Named:getname()
    return self.name
end
function Named:setname(n)
    self.name = n
end

local NamedAccount = createClass(Named, Account)
local account = NamedAccount:new { name = "jiang", balance = 10000 }
print(account:getname())                --> jiang
account:setname("jiang peng yong")      
print(account:getname())                 --> jiang peng yong
account:withdraw(100)
print(account.balance)                  --> 9900

一图胜千言

三、私有性

Lua 没有私有性机制,一般把需要私有性的名称最后加上一个下画线,用于区分全局和私有。

除了约定的方式外,还可以使用闭包来达到私有性

lua 复制代码
function newAccount(initialBalance)
    local self = { balance = initialBalance }

    local withdraw = function(v)
        self.balance = self.balance - v
    end

    local deposit = function(v)
        self.balance = self.balance + v
    end

    local getBalance = function()
        return self.balance
    end

    return {
        withdraw = withdraw,
        deposit = deposit,
        getBalance = getBalance
    }
end

local account = newAccount(1000)
account.deposit(10000)
account.withdraw(59)
print(account.getBalance())     --> 10941

在上面的代码中,self 外部不可访问,从而达到私有性。

四、写在最后

Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)

如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀

公众号搜索 "江澎涌",更多优质文章会第一时间分享与你。

相关推荐
hong16168813 分钟前
VSCode中配置C/C++环境
c语言·c++·vscode
小羊在奋斗28 分钟前
【C++】探秘二叉搜索树
c++·人工智能·神经网络·机器学习
白葵新1 小时前
PCL addLine可视化K近邻
c++·人工智能·算法·计算机视觉·3d
Crossoads1 小时前
【数据结构】排序算法---快速排序
c语言·开发语言·数据结构·算法·排序算法
MustardJim1 小时前
Visual Studio 引入外部静态库与动态库
c++·visual studio
挽月0011 小时前
C++单例模式
开发语言·c++·单例模式
机器视觉知识推荐、就业指导1 小时前
Qt/C++ TCP调试助手V1.1 新增图像传输与接收功能(附发布版下载链接)
c++·qt·tcp/ip
小立爱学习2 小时前
Linux 给 vmlinux 添加符号
linux·c语言
wx200411022 小时前
Codeforces Round 973 (Div. 2) - D题
数据结构·c++·算法
Crossoads2 小时前
【数据结构】排序算法---基数排序
c语言·开发语言·数据结构·算法·排序算法