首页 > 解决方案 > 如何在 Lua 中使用基类和子类构造函数模拟简单继承(教程)

问题描述

如何在 Lua 中使用父类和子类构造函数模拟简单继承?

如果您对此发表评论或将有趣的信息编辑到其中,我将提供并接受和回答,并将不胜感激。

标签: oopinheritancelua

解决方案


这是在 Lua 中模拟基本父/子类继承的指南。


我假设读者了解 Lua 的基本特性和语法。以下陈述尤其重要:

  • Lua 中没有类或对象,只有表。
  • 一个表可以有一个元表。
  • 如果表的元mtt包含元__index方法(因此),则尝试通过分配给的内容来解析对mt.__index不存在的键 in的访问。tmt.__index
  • 如果t调用这样的函数t.foo()t:foo()t其本身foo()作为第一个参数被传递给调用self。在函数内部,t它本身可以作为self.

基类

代码

Base = {}
function Base:new(name)
    Base.__index = Base
    local obj = {}
    setmetatable(obj, Base)
    obj.name = name
    return obj
end

function Base:sayName() 
    print(self.name..": My name is "..self.name..".")
end

由于该函数的Base:new(name)作用,Base现在可以将其视为一个新类,并Base:new(name)作为其构造函数。请记住,这Base确实是一张位于内存某处的表格,而不是一些抽象的“类蓝图”。它包含一个函数Base:sayName()。这就是我们可以称之为关于 OOP 术语的方法。

但是Base:new(name)let如何表现Base得像一个类?

解释

Base.__index = Base

Base表得到一个__index元方法,即它自己。每当Base用作元表时,对不存在索引的搜索将被重定向到...Base本身。(等等。)该行也可以写为self.__index = self, 因为Base调用:new(name),因此selfBase其中。我更喜欢第一个版本,因为它清楚地显示了正在发生的事情。此外,Base.__index = Base可以超出Base:new(name),但是,为了清楚起见,我更喜欢将所有“设置”都发生在一个范围内(“构造函数”)。

local obj = {}
setmetatable(obj, Base)

obj被创建为一个新的空表。它将成为我们所认为的“类”的对象Base。现在Base是 的元表obj。由于Base具有__index,对不存在的键的访问将obj被重定向到分配给 的内容Base.__index。由于Base.__indexisBase本身,对 in 中不存在的键的访问obj将被重定向到Base(例如,它会找到的地方Base:sayName())!

obj.name = name
return obj

obj(!) 获取一个新条目,一个member,构造函数参数被分配给该条目。然后obj返回,我们将其解释为class 的对象Base

示范

b = Base:new("Mr. Base")
b:sayName()

这将打印“Mr. Base:我的名字是 Mr. Base”。正如预期的那样。如上所述,通过 metatable-机制b找到,因为它没有这样的密钥。存在于内部(“类表”)和内部(“对象表”)。sayName()__indexsayName()Basenameb

儿童班

代码

Child = {}
function Child:new(name, age) -- our child class takes a second argument
    Child.__index = Child
    setmetatable(Child, {__index = Base}) -- this is different!
    local obj = Base:new(name, age)       -- this is different!
    setmetatable(obj, Child)
    obj.age = age
    return obj
end

function Child:sayAge()
    print(self.name..": I am "..tonumber(self.age).." years old.")
end

代码几乎与基类的代码完全相同!Child:new(name, age)在(即构造函数)中添加第二个参数并不是特别值得注意。Base也可以有多个参数。但是,添加了里面的第二和第三行Child:new(name, age),这就是Child继承”的原因Base.

请注意,Base可能包含Base.__index,这使得它在用作元表时很有用本身没有元表。

解释

setmetatable(Child, {__index = Base})

在这一行中,我们为类表分配了一个元Child表。这个元表包含一个__index元方法,它被设置为Base类表。因此,Child类表将尝试通过类表解析对不存在的键的访问Base。因此,Child类表可以访问它自己的所有方法和所有Base的方法!

local obj = Base:new(name, age)
setmetatable(obj, Child)

在第一行中,Base创建了一个新的对象表。此时,它的__index元方法指向Base类表。然而,就在第二行,它的元表被分配给Child类表。obj不会失去对类方法的访问权的原因Base在于,我们之前将不成功的键访问权重定向ChildBase(通过提供Child适当的元表)!由于obj是作为Base对象表创建的,因此它包含其所有成员。此外,它还包含Child对象表的成员(一旦将它们添加到Child:new(name, age)“构造函数”中。它Child通过自己的元方法找到类表的方法。它在Base类表通过Child类表中的元方法。

注意:Base对象表”是指由Base:new(name). “Base类表”是指实际的Base表。请记住,Lua 中没有类/对象!类Base表和Base对象表一起模仿了我们认为的 OOP 行为。当然,这同样适用Child

Child此外,在内部定义 ' 元表的赋值Child:new(name, age)允许我们调用Base' 的“构造函数”并将name参数传递给它!

示范

c = Child:new("Mrs. Child", 42)
c:sayName()
c:sayAge()

这将打印“Mrs. Child:我的名字是 Mrs. Child”。“Mrs. Child:我今年 42 岁。” 正如预期的那样。

结论

上面的部分描述了如何在 Lua 中实现 OOP 行为。了解这一点很重要

  • 基本方法存在于Base类表中
  • 基成员存在于Base由返回的对象表中Base:new()
  • 子方法位于Child类表中
  • 子成员位于由Child返回的对象表中Child:new()

引用正确的表是由表的元表完成的。

  • 所有类表都将自己分配给它们的__index键。当用作元表时,它们引用自己(即类方法所在的位置)。每个“类”只有一个类表。
  • 基类没有元表。它们被用作元表。
  • 子类表也一个元表,即{__index = Base}(将调用重定向到Base类表,即Base类方法所在的位置)。
  • 所有对象表都将其对应的类表分配为元表。由于类表已经设置了它们的__index元方法,对对象表的调用可以重定向到类方法所在的类表。如果它是一个子类表(这意味着它也有一个元表),重定向可能会发生得更远。可以有任意多个对象表。

原理图概览

使用上图继续:如果Child对象表c尝试访问Base方法会发生什么,例如c:sayName()?嗯:有c钥匙sayName()吗?不,它有元表吗?是:(Child班级Child表)。Child有元方法吗__index?是的。它指向哪里?对Child自己。ChildsayName()钥匙吗?没有Child。有元表吗?是的。它有__index元方法吗?是的。它指向哪里?到Base班级表。它有sayName()钥匙吗?是的!:-)

笔记

我不是 Lua 专家!到目前为止,我只在 Lua 中编写了一些脚本,但在过去的几天里,我试图把我的想法围绕在这个问题上。我发现了很多不同的、有时令人困惑的解决方案,最后得出了这个,我称之为最简单但透明的解决方案。如果您发现任何错误或警告,请不要犹豫发表评论!


推荐阅读