另一种类实现

lua-users home
wiki

代码可以在以下地址找到:https://github.com/jpatte/yaci.lua.

欢迎大家贡献。请随时 fork 仓库,提交问题和建议更改!

介绍

这当然不是完全原创的,但我认为它对其他一些 Lua 用户或爱好者来说可能有用。我见过几种关于类的实现,它们建议如何使用元表来模拟面向对象方面的特性,例如实例化或继承等(例如,参见 面向对象教程使用元表的 Lua 类继承教程类和方法示例 以及 简单 Lua 类),但我认为应该可以比这更进一步,通过添加一些额外的功能和设施。这就是为什么我在这里建议另一种实现,它主要基于其他实现,但其中包含了一些额外的内容。我并不声称它是最好的方法,但我认为它对除了我之外的一些人可能有用,因此我想在这里分享它;)

请注意,这段代码的设计目的是尽可能方便使用;因此,这当然不是最快的做事方式。它相当复杂,它大量使用元表、上值、代理... 我尝试过对其进行大量优化,但我不是专家,因此可能有一些捷径我还不了解。

我现在将描述你可以用它做什么。欢迎任何意见和建议!

特性

内容

这段代码只“导出”两件事:基类 'Object' 和一个函数 'newclass()'。

类定义

基本上有两种方法可以定义一个新类:通过调用 'newclass()' 或使用 'Object:subclass()'。这些函数返回新类。

创建类时,你应该为它指定一个名称。这不是绝对必要的,但它可能会有所帮助(用于调试目的等)。如果你没有给出任何名称,该类将被称为“未命名”。拥有多个未命名的类不是问题。

当你使用 'Object:subclass()' 时,新类将是 'Object' 的直接子类。但是 'newclass()' 接受第二个参数,它可以是 'Object' 以外的另一个超类(如果你没有给出任何类,将选择类 'Object';这意味着所有类都是 'Object' 的子类)。请注意,每个类都有 'subclass()' 方法,因此你也可以使用它。

让我们举个例子来说明这一点

-- 'LivingBeing' is a subclass of 'Object'
LivingBeing = newclass("LivingBeing")

-- 'Animal' is a subclass of 'LivingBeing'
Animal = newclass("Animal", LivingBeing)

-- 'Vegetable' is another subclass of 'LivingBeing'
Vegetable = LivingBeing:subclass("Vegetable")

Dog = newclass("Dog", Animal)   -- create some other classes...
Cat = Animal:subclass("Cat")
Human = Animal:subclass("Human")

Tree = newclass("Tree", Vegetable)

请注意,'newclass()' 的确切代码是

function newClass(name, baseClass)
 baseClass = baseClass or Object
 return baseClass:subClass(name)
end
它只是为了方便而添加的。

方法定义

方法以一种相当自然的方式创建

function Animal:eat()
  print "An animal is eating..."
end

function Animal:speak()
  print "An animal is speaking..."
end

function Dog:eat()
  print "A dog is eating..."
end

function Dog:speak()
  print "Wah, wah!"
end

function Cat:speak()
  print "Meoow!"
end

function Human:speak()
  print "Hello!"
end 

方法 'init()' 被视为构造函数。因此

function Animal:init(name, age)
  self.name = name
  self.age = age
end

function Dog:init(name, age, master)
  self.super:init(name, age)   -- notice call to superclass's constructor
  self.master = master
end

function Cat:init(name, age)
  self.super:init(name, age)
end

function Human:init(name, age, city)
  self.super:init(name, age)
  self.city = city
end

子类可以通过字段 'super' 调用其超类的构造函数(见下文)。注意 'Object:init()' 存在但什么也不做,因此不需要调用它。

事件定义

您也可以为类实例定义事件,与方法完全相同

function Animal:__tostring()
  return "An animal called " .. self.name .. " and aged " .. self.age 
end

function Human:__tostring()
  return "A human called " .. self.name .. " and aged " .. self.age .. ",
         living at " .. self.city
end
可以使用任何事件,除了 '__index' 和 '__newindex',它们是面向对象实现所必需的。您可以使用此功能来定义像 '__add'、'__eq' 等等的操作符。'__tostring' 是这里一个非常有用的事件,因此类 'Object' 为它实现了一个默认版本,它简单地返回一个字符串 "a xxx",其中 'xxx' 是实例类的名称。

实例创建

每个类都有一个 'new()' 方法,用于实例化。所有参数都转发到实例的构造函数。

Robert = Human:new("Robert", 35, "London")
Garfield = Cat:new("Garfield",  18)
如果您直接“调用”类,结果是相同的
Mary = Human("Mary", 20, "New York")
Albert = Dog("Albert", 5, Mary)

类服务

除了 'subclass()' 和 'new()' 之外,每个类还拥有其他几个方法

实例服务

每个实例都允许访问其类(及其超类)的构造函数中定义的变量。它们还有一个 'class()' 方法返回它们的类,以及一个字段 'super' 用于访问超类的成员(如果您覆盖了它)。例如

A = newclass("A")
function A:test() print(self.a) end
function A:init(a) self.a = a end
B = newclass("B", A)
function B:test() print(self.a .. "+" .. self.b) end
function B:init(b) self.super:init(5) self.b = b end

b = B:new(3)
b:test()   -- prints "5+3"
b.super:test()   -- prints "5"
print(b.a)   -- prints "5"
print(b.super.a)   -- prints "5"
超类的成员在调用 "self.super:init()" 方法时创建(并初始化)。您通常应该在构造函数的开头调用此方法来初始化它们。

注意,由于 'b' 是 'B' 的实例,'b.super' 只是一个 'A' 的实例(所以要小心,这里 'super' 是动态的,而不是静态的)。

静态变量

每次为类定义新方法时,它都会进入一个“静态”表(这样我们就不会将类方法与类服务混合)。这个表可以通过一个“静态”字段访问。这主要是为了允许访问类中的静态变量。示例

A = newclass("A")
function A:init(a) self.a = a end
A.test = 5   -- a static variable in A

a = A(3)
prints(a.a)   -- prints 3
prints(a.test)   -- prints 5
prints(A.test)   -- prints nil (!)
prints(A.static.test)   -- prints 5

结束

呼 - 我想就这些了。:) 再次感谢您的任何意见和评论。但这是我在这里的第一次提交,所以不要太狠地批评我:D -- Julien Patte,2006 年 1 月 19 日 (julien.patte AT gmail DOT com)

最后一分钟的说明:我刚刚发现了 SimpleLuaClasses,之前没有看到。我惊讶(和高兴)地发现我们的实现之间存在如此多的相似之处,至少在人们使用它的方式上是如此。但是,这里只有事件需要复制以进行继承,每个实例都包含其超类的实例,还有一些其他细节。

我对这段代码仍然不太满意。即使是实例变量(除了函数)在这里也是“虚拟的”,这是一个很大的问题,因为它可能会带来一些奇怪的错误,如果超类和子类使用了一些具有相同名称的变量。但我猜这很难解决:/ -- Julien Patte

错误修复

我发现了一个微妙但令人困惑的错误:实例变量通常是虚拟的(用 C++ 的话说就是“受保护的”),除了涉及从子类函数内部调用覆盖的超类函数的一些特定情况,例如当 B:Update() 在内部调用 self.super:Update()(即 A:Update())时。这会导致多个具有相同名称但具有不同值的变量分散在多个继承级别上,这往往会导致问题,令人困惑,并且通常让人不高兴。

解决此问题的办法是在

function c_istuff.__newindex(inst,key,value)
  if inst.super[key] ~= nil then inst.super[key] = value;
  else rawset(inst,key,value); end
end

在 Julien 非常巧妙的代码的 subClass 函数中的 function c_istuff.__index(inst,key) ... end 之后添加。现在它应该可以正常工作了。但要小心,因为修复这个问题可能会破坏依赖于它被破坏的东西——具体来说,是超类变量名称在子类中被无意中重复使用的情况。

-- Damian Stewart (damian AT frey DOT co DOT nz),2006 年 10 月 6 日

初学者笔记

关于 Lua 和/或 YaciCode? 初学者的几点评论。

实例变量

您**必须**在 init() 方法中定义类的所有实例变量,使用 false 来表示您希望为 nil 的任何内容。这与 Lua 如何管理表以及 YaciCode? 用于提供继承的方法有关,并且也与上面的错误有关。如果您没有这样做,您将在从子类函数内部调用覆盖的超类函数时遇到意外情况。

A = newclass("A")

function A:init(a_data)
  self.a_data = a_data
  self.foo = nil
end

function A:setFoo(foo)
  self.foo = foo
end

function A:free()
  if self.foo then self.foo = nil end
end

B = A:subclass("B")

function B:init(a_data, b_data)
  self.super:init(a_data)
  self.b_data = b_data
  self.b_table = {'some', 'values', 'here'}
end

function B:free()
  self.b_table = nil
  self.super:free()
  if self.foo then print("self.foo still exists!!!") end
end

-- and now some calls
myA = A:new("a_data")
myB = B:new("a_data2", "b_data")

myB:setFoo({'some', 'more', 'values'})

myB:free()
-- will print "self.foo still exists" !!!

myB:setFoo() 调用 A.setFoo(myB)(即,selfmyB)。myB.foo 不存在于 myB 或层次结构中的更高位置,因此 foo 键被添加到 myB 表中。在释放时,会调用 B.free(myB)selfmyB)。self:super:free() 调用 A.free(self.super),即 A.free **不** 使用 myB 调用,而是使用 myB.super 调用,这是一个由 YaciCode?维护的类 A 的伪对象,它**没有**foo 实例变量!问题是,self.foo = nilA:init() 没有任何副作用。它没有创建 foo 实例变量。

然而,如果你执行 self.foo=false,它确实会创建一个 foo 实例变量,当 myB:setFoo() 调用 A.setFoo(myB) 时,myB.foo 不存在,但它在层次结构中更高(值为 false),在这种情况下,它最终会被 foo 函数参数替换。使用 false 的好处是,它使像 if self.foo then 这样的测试在 self.foonilfalse 时都能正常工作。

self.super 的动态性

上面的文本提到 self.super 是动态的。在实践中,这意味着如果你在层次结构的每个级别上定义函数,并且该函数调用其超类,那么你可能应该这样做。

A = newclass("A")

function A;init(...) ... end

function A:free()
  print("A:free()")
end

B = A:subclass("B")

function B:init(...) ... end

function B:free()
  self.super:free()
  print("B:free()")
end

C = B:subclass("C")

function C:init(...) ... end
-- Note C has no "free" method

-- code
myC = C:new()

myC:free()
-- prints:
B:free()
B:free()
A:free()
-- i.e. B:free is called **twice**

发生的事情是 myC:free() 等同于 C.free(myC)。由于 C 没有 free 方法,但 B 有,最终调用的是 B.free(myC)。在这个函数中,我们执行 self.super:free(),它实际上是 myC.super.free(myC.super)。事实证明 myC.super.free 是(再次)B.free,所以实际上调用的是 B.free(myC.super),而 B.free 最终被调用两次,一次使用原始对象作为参数,另一次使用 YaciCode? 在幕后维护的“伪”超类对象。

这可能会产生不希望有的副作用,所以最好明确定义 C.free(),即使只是为了执行 self.super:free()...

注意,init() 也会发生同样的情况,但由于所有类都定义了它,所以不会有这种副作用。

-- Frederic Thomas (fred AT thomascorner DOT com), 2007 年 2 月 22 日

1.2 版

好吧,Frederic 指出了一个非常令人讨厌的问题...

基本上,人们希望这样说:“目前,如果 B 是 A 的子类,myB 是 B 的实例,如果 foo() 在 A 中定义,而在 B 中没有定义,那么 myB:foo() 等同于 A.foo(myB);虽然我们需要的是等效形式 A.foo(myB.super)。”这听起来很合理,因为 Frederic 提到的两个错误正是这一事实的直接结果。这种改变并不难实现,上面的两个示例代码只需将实例的 __index 元方法替换为更复杂的东西,就可以正常工作。

但是... 虚拟方法怎么办?如果想要从 A 中定义的 foo() 方法中调用 B 中定义的虚拟方法 bar() 呢?如果我们应用了这种改变,这种事情将不再可能,因为从 A 中定义的方法将无法访问 B 中的虚拟字段——就像现在的情况一样,因为 foo() 接收 myB 作为参数而不是 myB.super,因此它可以访问 B 级别定义的方法。

以下是一个示例来说明这一点

A = newclass("A")

function A:whoami()
  return "A"
end

function A:test()
  print(self:whoami())
end

B = newclass("B", A)

function B:whoami()		-- is it a virtual function?
  return "B"
end

myB = B()
myB:test() -- what should be printed here? "A" or "B"?

例如,Java 用户希望看到 "B"(因为 Java 中的方法默认是虚拟的),而 C++ 用户则希望看到 "A",因为 whoami() 没有声明为 "虚拟"。问题是:Lua 中没有 "虚拟" 或 "最终" 关键字。那么最佳行为是什么呢?

在编写代码时,我认为所有方法都应该默认是虚拟的,这就是我以这种方式组织事物的原因。但在我看来,弗雷德里克报告的错误太重要了,无法接受。

因此,我编写了 "YaciCode?" 的新版本,其中修复了这些错误,禁用了默认虚拟,并添加了一些新的类函数来提供虚拟方法和强制转换功能。新代码可以在以下位置找到:Files:wiki_insecure/users/jpatte/YaciCode12.lua。如果可能,我希望在编辑上面的笔记以添加有关新功能的解释之前获得大家的认可。我进行的所有测试都运行良好,但我可能遗漏了一些东西;再次,如果您看到 "更合适的方法",请随时发表评论 :-)

以下是一些关于此版本主要更改的说明

内部重组

为了管理强制转换和显式虚拟,我必须在代码中添加一些东西,特别是弱表 metaObj,它将实例对象与其元信息(对用户不可见)相关联。这些信息涉及对象的类、它的 "超对象"、它的 "下级对象" 等。这主要用于强制转换实现:将 myB.super 强制转换回 B 实例(即 myB 本身)现在是可能的,因为在元信息中存在从 myB.supermyB 的链接。此表可用于在将来的版本中存储有关每个实例的任何其他信息。

类实例的 __index 元方法比以前更复杂,以便将 myB:foo(myB) 转换为 A.foo(myB.super) 而不是 A.foo(myB),如果 foo() 在 A 级别定义;这种 "简单" 的更改修复了弗雷德里克提到的两个错误。

类也可能有一些元信息,特别是它们维护着其虚拟方法的列表。每次创建实例时,"虚拟表" 会直接复制到实例(及其所有 "超实例")中。这意味着虚拟方法比类声明的简单方法具有更高的优先级(这正是我们想要的:如果 A 和 B 定义了虚拟方法 foo()B.foo 必须比 A.foo() 在层次结构的每个级别上具有更高的优先级)。

顺便说一下,由于已经存在 `Object` 类,我正在考虑引入 `Class` 类。所有类都将是 `Class` 的实例;例如,应该写 `A = Class:new("A")` 或 `A = Class("A")` 而不是 `A = newclass("A")`。这并不难实现,并且会使代码更加“同质化”。你对此有什么看法?

虚拟

我们到了。正如我所说,虚拟现在默认情况下被禁用(这是由于新的 `__index` 元方法)。在我给出的关于 `whoami()` 函数的示例代码中,当前的实现将打印 "A",因为 `A:test()` 将 `myB.super` 作为 `self` 而不是 `myB`。但是,如果我们想让 `whoami()` 成为虚拟的怎么办?换句话说,我们如何在 A 的级别(并且仅针对 B 的实例)用 `B:whoami()` 覆盖 `A:whoami()`?好吧,你只需要写 `A:virtual("whoami")` 来显式地声明 `whoami()` 为虚拟的。这必须写在任何方法之外,并且在方法定义之后。因此

A = newclass("A")

function A:whoami()
  return "A"
end
A:virtual("whoami") -- whoami() is declared virtual

function A:test()
  print(self:whoami())
end

B = newclass("B", A)

function B:whoami() -- now yes, whoami() is virtual
  return "B"
end
					-- no need to declare it again
myB = B()
myB:test() -- will print "B"

也可以将某些方法声明为 *抽象*(即纯虚方法);你只需要在调用 `A:virtual()` 时带上抽象方法的名称,而无需定义它。如果你尝试调用它而没有在层次结构中定义它,则会引发错误。以下是一个示例

A = newclass("A")

A:virtual("whoami") -- whoami() is abstract

function A:test()
  print(self:whoami())
end

B = newclass("B", A)

function B:whoami() -- define whoami() here
  return "B"
end
					
myB = B()
myB:test() -- will print "B"

myA = A()  -- no error here! 
myA:test() -- but will raise an error here

“受保护”和“私有”属性

Damian 在这里写道:“这会导致多个同名变量但具有不同值散布在多个继承级别上,这往往会破坏事物,令人困惑,并且通常让人不高兴。”

嗯,我个人倾向于相反的观点。我认为封装原则也应该应用于类及其子类之间;这意味着子类的实例不应该了解其超类中声明的属性。它可能可以访问超类提供的某些方法和服务,但它不应该知道这些服务是如何实现的。这是父类的业务,而不是子类的业务。在实践中,我认为类中的每个属性都应该声明为“私有”:如果一个类及其子类使用相同名称的属性来进行各自的业务,那么它们之间不应该有任何干扰。如果超类服务的实现需要改变,对子类的影响应该最小,这主要是因为子类不知道更高层级使用的确切属性。

这是两种截然相反的观点,很难(甚至不可能)判断谁对谁错。所以我们能说的最好的话可能是“让用户决定他们想做什么”:-)

现在可以根据属性初始化的顺序在类中定义“受保护”和“私有”属性。请注意,“受保护”和“私有”在这里并不是最好的术语(因为没有真正的保护机制),我们应该更准确地谈论类及其子类之间“共享”和“非共享”属性。您还会注意到,这种区分是由子类本身(而不是超类)做出的,子类可以在其构造函数中决定超类的某些属性是否应该被共享或覆盖。

请看以下示例

A = newclass("A")

function A:init(x)
  self.x = x
  self.y = 1  -- attribute 'y' is for internal use only
end

function A:setY_A(y)
  self.y = y
end

function A:setX(x)
  self.x = x
end

function A:doYourJob()
  self.x = 0   -- change attributes values
  self.y = 0
  -- do something here...
end


B = A:subclass("B")

function B:init(x,y)
  self.y = y              -- B wants to have its own 'y' attribute (independant from A.y)
  self.super:init(x)      -- initialise A.x (and A.y)
                          -- x is shared between A and B
end

function B:setY(y)
  self.y = y
end

function B:setY_B(y)
  self.y = y
end

function B:doYourJob()
  self.x = 5
  self.y = 5
  self.super:doYourJob()  -- look at A:doYourJob
  print(self.x)           -- prints "0": B.x has been modified by A
  print(self.y)           -- prints "5": B.y remains (safely) unchanged
end


myB = B(3,4)
print(myB.x, myB.y, myB.super.x, myB.super.y) -- prints "3 4 3 1"

myB:setX(5)
myB:setY(6)
print(myB.x, myB.y, myB.super.x, myB.super.y) -- prints "5 6 5 1"

myB:setY_A(7)
myB:setY_B(8)
print(myB.x, myB.y, myB.super.x, myB.super.y) -- prints "5 8 5 7"

myB:doYourJob()

您可以看到属性 'x' 和 'y' 的不同行为来自构造函数中初始化的顺序。第一个定义属性的类将获得该属性的所有权,即使某些超类在初始化过程中“稍后”声明了相同名称的属性。我个人建议在构造函数的开头初始化所有“非共享”属性,然后调用超类的构造函数,最后使用超类的某些方法。相反,如果您想访问超类定义的属性,您可能需要在超类的构造函数完成之前不要设置其值。

我希望这个解决方案对每个人都适用 ;-)

类型转换

现在出现另一个问题:将 `myB:foo()` 转换为 `A.foo(myB.super)` 会导致关于 `myB` 的部分信息“丢失”。`foo()` 位于 A 的级别;但如果我们想从 `foo()` 访问 B 级别定义的特定(非虚拟)方法/属性怎么办?答案是:我们应该能够将 `myB.super` “转换”回 `myB`。

这可以通过两个新的类方法来实现:`cast()` 和 `trycast()`。一个简单的例子是...

A = newclass("A")

function A:foo()
  print(self.b) -- prints "nil"! There is no field 'b' at A's level
  aB = B:cast(self)  -- explicit casting to a B
  print(aB.b)  -- prints "5"
end

B = newclass("B",A)

function B:init(b) 
	self.b = b
end

myB = B(5)
myB:foo()

C:cast(x) 尝试通过在层次结构中向上和向下搜索,在 'x' 中找到对应于类 C 的“子对象”或“超对象”。直观地,我们将有 `myB.super == A:cast(myB)` 和 `myB == B:cast(myB.super)`。当然,这适用于超过 2 个级别的继承。如果转换失败,将引发错误。

C:trycast(x) 做完全相同的事情,只是在转换不可能时它只返回 `nil`,而不是引发错误。`C:made(x)` 已经存在,现在已被修改,如果 `C:trycast(x)` 不返回 nil,即如果转换可能,则返回 `true`。

让我们看另一个例子

A = newclass("A")
function A:asA() return self end

B = newclass("B",A)
function B:asB() return self end

C = newclass("C",B)

D = newclass("D",A) -- subclass of A

a, b, c, d = A(), B(), C(), D()

b_asA = b:asA()
c_asA = c:asA()
c_asB = c:asB()

print( A:made(c) ) -- true
print( A:made(d) ) -- true

print( B:made(a) ) -- false
print( B:made(c) ) -- true
print( B:made(d) ) -- false

print( C:made(b) ) -- false
print( C:made(c) ) -- true
print( C:made(d) ) -- false

print( D:made(d) ) -- true
print( D:made(a) ) -- false

print( b_asA:class() , B:made(b_asA) ) -- class A, true

print( c_asA:class() , C:made(c_asA) ) -- class A, true
print( c_asB:class() , C:made(c_asB) ) -- class B, true

print( c:asA() == c.super.super ) -- true
print( C:cast( c:asA() ) == c ) -- true

最后一个例子(这样写并不是一个好习惯,但它仍然是转换操作的很好的例子)

A = newclass("A")

function A:printAttr() 
  local s
  if B:made(self) then s = B:cast(self) print(s.b)
  elseif C:made(self) then s = C:cast(self) print(s.c)
  elseif D:made(self) then s = D:cast(self) print(s.d)
  end
end 

B = newclass("B",A) 
function B:init() self.b = 2 end

C = newclass("C",A) 
function C:init() self.c = 3 end

D = newclass("D",A) 
function D:init() self.d = 4 end

manyA = { C(), B(), D(), B(), D(), C(), C(), B(), D() }

for _, a in ipairs(manyA) do
  a:printAttr()
end

评论

这是 1.2 版新版本引入的更改的描述;我希望这些改进会有所帮助。如果您认为可以做得更好,或者在某个地方发现错误,请随时提供反馈 ;-)

新版本可在 Files:wiki_insecure/users/jpatte/YaciCode12.lua 获取,如果可能的话,我希望在更新整个页面之前收到有关此版本的评论。非常感谢您的关注!

-- Julien Patte (julien.patte AT gmail DOT com), 2007 年 2 月 25 日

错误修复

Peter Bohac 报告了 1.2 版中关于 `class()` 方法的错误。作为副作用,默认的 `__tostring` 元方法(使用此方法)在实例被“打印”时会引发错误。错误修复非常简单

1 - 在第 149 行

function inst_stuff.class() return theclass end
它应该是“theClass”,而不是“theclass”...

2 - 在第 202 行之后,应该定义 Object:class() 方法

obj_inst_stuff.__newindex = obj_newitem
function obj_inst_stuff.class() return Object end

此错误已在 YaciCode12.lua 文件中修复。

-- Julien Patte (julien.patte AT gmail DOT com), 2007 年 3 月 19 日

另请参阅


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2023 年 6 月 14 日下午 6:57 GMT (差异)