Lua 中一张表 table 就是一个对象(注意不是类,是对象)。他具有以下的特性:
Lua 并没有类这一概念,而是通过 “基于原型” 的方式进行组织,而原型其实也是一个实例对象。
换而言之,我们通过元表的方式,让 Lua 的实例对象在找不到对应的属性或方法时,从其元表的 __index
属性指定的表(即上面所说的原型,其实他也是实例对象)或方法中获取,而如果这里指定的表也没有对应的属性,则继续往该表(也就是原型)的元表继续查找,直到查找到或全部查找完还没有找到对应的属性或方法为止。
在类中,this(在 Lua 中是 self) 很重要,因为他可以让不同实例调用同一方法不会有互相干扰。
举个例子
下面的例子中,withdraw
方法内部固定写了 Account
,所以只要将 Account
删除,则会有异常
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)
,改变后的代码:
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
的参数,方法内部就可以进行使用了,具体代码如下:
冒号定义的方法,也可以使用点方式调用,只是需要传入多一个参数指向自己
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)
首先,创建一个 Account ,这个就类似 java、kotlin 中的类,但其实在 Lua 中,他是一个 table 实例( 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 返回,他就是我们需要的对象了。
然后,进行创建和使用,这样的两个实例就不会互相影响。
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
属性。
一图胜千言
从上面的图可以知道,我们只需要在这个元表的搜索链插入我们需要的节点,则能够达到继承的效果。
还是基于上面的 Account 代码,继承一个 SpecialAccount 类(其实也是一个实例对象,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
中定义的方法
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 ,所以元表链就建立起来了。
一图胜千言
和 java、kotlin 不太一样的是,Lua 的对象可以自行添加一些属性或方法,当然也可以重写父类的属性或方法,因为本质上 Lua 的对象就是一个表。
基于上面的代码,我们对 person 进行重写 getLimit
方法,这样在调用 withdraw
方法时,内部调用的就是 person 对象的 getLimit
方法
-- 给 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
在 java、kotlin 中,没有多继承这一概念。而在 Lua 中,可以非常容易的实现,因为搜索链的本质是基于元表中的 __index
方法。之前都是设置的表,如果需要多重继承,只需要设置为方法,在方法中实现相应的搜索方式即可。
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 类,进行多继承使用
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 没有私有性机制,一般把需要私有性的名称最后加上一个下画线,用于区分全局和私有。
除了约定的方式外,还可以使用闭包来达到私有性
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吧,码字不易,请多多支持)
如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀。
公众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。