跳到主要内容
信息
· 文章中可能会出现一些错误,希望大佬们可以在评论区指出;

10-面向对象编程

在之前的表章节中提到,Lua语言中的一张表就是一个对象,可以用表实现OOP。

通常支持OOP的编程语言认为:类在对象的创建中扮演了模子的作用,对象是类的实例。虽然元表概念和该观点类似,但将元表直接作为类使用在后续会变得麻烦。可以参考基于原型的语言(例如Self,JavaScript)的设计思路来在Lua语言中模拟类。在这些语言中,对象不属于类,每个对象可以有一个原型。原型也是一种普通的对象,当对象(类的实例)遇到一个未知操作时首先会在原型中查找。因此,按这种思想表示类需要创建一个专门被用作其他对象(类的实例)的原型对象。

封装​

按照上述思路以及元表相关的知识,下例代码实现了一个用于实例化对象的原型对象Object,并通过实例化函数Object:New()创建了一个实例o1:

local Object = { id = 1 }
Object.__index = Object

function Object:New()
-- 实例会找类的方法
return setmetatable({}, self)
end

function Object:Test()
print("Object:Test, id = " .. self.id)
end

local o1 = Object:New()
o1:Test() --> Object:Test, id = 1
o1.id = 2
o1:Test() --> Object:Test, id = 2

当Lua:

  • 对o1进行字段读取o1.Test或o1.id时,如果o1中不存在该字段,Lua会检查它的元表Object,该元表的__index指向Object,因此继续在Object中查找该字段;
  • 执行o1.id = 2时,Lua会直接在so1中创建字段id,之后再次读取会返回这里的定义;

上述过程说明,可以利用原型对象和元表机制在 Lua 中模拟 OOP。

继承​

单继承​

由于类本身也是普通对象,因此它们也可以从其他类中获得方法。这种行为使得继承可以很容易地在 Lua 中实现。

下例代码实现了专门用于创建子类的函数 Object:SubClass()。使用它创建一个从基类 Object 继承方法的子类 SubObject,再通过继承得到的实例化函数 SubObject:New() 创建实例 so1:

function Object:SubClass()
local cls = {}
cls.__index = cls
-- 子类的元表为父类
return setmetatable(cls, self)
end

local SubObject = Object:SubClass()

function SubObject:Test()
print("SubObject: id = " .. self.id)
end

local so1 = SubObject:New()
so1:Test() --> SubObject: id = 1

从上例代码中可以发现,Lua:

  • 在调用函数SubObject:New()时,类SubObject自身没有定义New()函数。Lua会检查它的元表Object,该元表的__index指向Object,因此最终在Object中找到New();
  • so1在执行Test()时,由于找不到函数Test()和字段id,通过元表SubObject的__index字段在SubObject中查找找到Test(),找不到字段id,继续通过SubObject的元表Object的__index字段在Object中找到id;

因此,整个继承关系可以概括为:so1 --元表--> SubObject --元表--> Object,其中,so1 -> SubObject负责实例对类成员的查找,而SubObject-> Object负责子类对父类成员的查找/继承。

多继承​

多继承意味着一个类可以具有多个基类,也就是说不能通过使用其中一个父类的方法来创建子类,而是定义一个独立函数CreateClass(...)来创建子类,它的参数为子类的所有父类。

在封装和单继承的实现中,元表的元方法__index字段为元表本身,实例/子类找不到字段就会继续在它的元表处(即子类/父类)查找。这种实现仅支持单继承,无法满足一个类可以拥有多个父类(即多继承)的需求。实际上,__index的值还可以是一个函数,当Lua不能在原来的表中找到字段就会执行这个函数,可以利用这个特性实现子类在多个父类中查找字段的需求。

综上,多继承的一种实现如下例所示:

示例:多继承的一种实现点击展开/折叠代码
-- 在表parentList中查找key, 找不到就返回nil
local function Search(key, parentList)
for i = 1, #parentList do
local value = parentList[i][key]
if value ~= nil then return value end
end
return nil
end

-- 多继承实现
function CreateClass(...)
local cls = {} -- 子类
local parents = { ... } -- 父类列表

-- 子类cls会查找父类列表的方法
setmetatable(cls, {
__index = function (_, key)
return Search(key, parents)
end
})

-- 子类cls的实例查cls
cls.__index = cls

-- 子类cls的构造函数
function cls:New()
return setmetatable({}, self)
end

return cls
end

local Person = Object:SubClass()
function Person:Walk()
print("Walking...")
end

local Cat = Object:SubClass()
function Cat:Meow()
print("Meow~")
end

local CatMan = CreateClass(Person, Cat)
local Bob = CatMan:New()
Bob:Walk() --> Walking...
Bob:Meow() --> Meow~

可以发现多继承类CatMan的实例Bob均可以调用父类Person的Walk()函数以及另一个父类Cat的Meow()函数。Lua语言在元表中的搜索思路和单继承中的类似,只是在最后__index环节变为执行Search()函数在多个父类中查询字段了,这里不再赘叙。

多态​

多态就是相同方法名,不同子类有不同的处理逻辑,因此通过继承直接重写对应方法即可。但是如果想要保留父类逻辑(例如Super::)还需要在创建子类的函数SubClass()中添加元表字段__superClass记录父类信息:

示例:多态点击展开/折叠代码
function Object:SubClass()
local cls = {}
-- 子类cls的实例查cls
cls.__index = cls
-- 子类cls的实例可以调用父类方法
cls.__superClass = self
-- 子类cls会查找父类的方法
return setmetatable(cls, self)
end

local Animal = Object:SubClass()
Animal.name = ""

function Animal:Say()
print("I am an animal, my name is " .. self.name)
end

local Cat = Animal:SubClass()
Cat.name = "Cat"
function Cat:Say()
-- 多态实现方法1: 通过__superClass字段调用父类方法, 注意
-- 这里不能使用冒号:调用父类方法,否则会传入
-- 父类的表信息,而不是自己
self.__superClass.Say(self)
print("Meow~")
end

local kitty = Cat:New()
kitty.name = "Kitty"
kitty:Say() --> I am an animal, my name is Kitty\n Meow~

local Dog = Animal:SubClass()
Dog.name = "Dog"
function Dog:Say()
-- 多态实现方法2: 直接通过父类方法调用,同样不能用冒号
Animal.Say(self)
print("Wolf!")
end

local doggy = Dog:New()
doggy.name = "Doggy"
doggy:Say() --> I am an animal, my name is Doggy\n Wolf!

参考资料​