又一个类实现

lua-users home
wiki

代码可以在这里找到:https://github.com/jpatte/yaci.lua

欢迎大家贡献。请随时 fork 仓库、提交 issues 和建议修改!

引言

这当然不是什么全新的东西,但我认为它可能会对其他 Lua 用户或爱好者有所帮助。我见过许多类实现,它们展示了如何使用元表来模拟面向对象的方面,例如实例化或继承等(例如,请参阅 ObjectOrientationTutorialLuaClassesWithMetatableInheritanceTutorialClassesAndMethodsExampleSimpleLuaClasses),但我认为通过添加一些额外的特性和便利,可以做得更多。这就是为什么我在这里提出又一个实现,它主要基于其他实现,但增加了一些额外的东西。我不敢说它是最好的方式,但我认为它可能对除我以外的其他人有用,因此我乐于在此分享 ;)

请注意,此代码的设计宗旨是尽可能方便使用;因此,这肯定不是最快的实现方式。它相当复杂,大量使用了元表、upvalues、代理等。我尝试了大量的优化,但毕竟不是专家,所以可能存在我尚未知道的一些捷径。

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

特性

内容

此代码“导出”只有 2 个内容:基类 'Object',以及一个函数 'newclass()'。

类定义

基本上有两种定义新类的方式:调用 'newclass()' 或使用 'Object:subclass()'。这些函数都返回新类。

创建类时,应为其指定一个名称。这并非绝对必需,但可能会有所帮助(例如为了调试)。如果不提供名称,类将被命名为“Unnamed”。拥有多个未命名类也没有问题。

当你使用 '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',它们是 OO 实现所必需的。你可以使用此功能定义运算符,如 '__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' 是动态的,而不是静态的)。

静态变量

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

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,之前我没有看到它。我很惊讶(也很高兴)我们的实现有很多相似之处,至少在人们使用它的方式上是这样。然而,在这里只有事件必须复制才能继承,每个实例都包含其超类的一个实例,并且还有一些其他额外的细节。

我对这段代码仍然不完全满意。即使是实例变量(函数除外)在这里也是“虚拟的”,这是一个巨大的问题,因为它可能导致超类和子类使用同名变量时出现一些奇怪的 bug。但我猜这很难避免 :/ -- Julien Patte

Bug修复

我发现了一个细微但令人困惑的 bug:实例变量通常是虚拟的(C++ 语境下的“受保护”),除了某些特殊情况,例如当一个子类函数调用其超类函数时,例如当 B:Update() 内部调用 self.super:Update()(即 A:Update())时。这可能导致具有相同名称但值不同的多个变量散布在继承的多个层级中,这往往会破坏东西,令人困惑,并普遍让人不快。

修复此问题的方法是在 subClass 函数中的 function c_istuff.__index(inst,key) ... end 之后,紧接着添加以下内容

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 那些非常巧妙的代码中。现在一切都应该能正常工作了。不过要小心,修复此问题可能会破坏依赖于它处于损坏状态的情况——特别是,超类变量名被无意中重用于子类的情况。

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

新手笔记

给 Lua 和/或 YaciCode? 新手的几点说明。

实例变量

你**必须**在 init() 方法中定义类所有实例变量,对于任何你想要设为 nil 的值,请使用 false。这与 Lua 如何管理表以及 YaciCode? 提供继承的方法有关,也与上面的 bug 相关。如果你不这样做,在子类函数中调用被覆盖的超类函数时会遇到意外情况。

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.foomyB 或其上层继承结构中不存在,因此 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.foo 是 nilfalse 时都能正常工作。

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 的一个实例,myB:foo() 等同于 A.foo(myB),如果 foo() 定义在 A 中而不是 B 中;然而,我们需要的是等同于 A.foo(myB.super) 的形式”*。这听起来是对的,因为 Frederic 刚提到的两个 bug 是该事实的直接结果。这个改变并不难做,并且上面两个示例代码可以通过替换实例的 __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() 没有被声明为“virtual”。问题是:Lua 中没有“virtual”或“final”关键字。那么最好的行为是什么?

在编写代码时,我曾认为所有方法都应该默认是虚拟的,这就是我这样组织的原因。但 Frederic 报告的 bug 太重要了,不可接受,在我看来。

因此,我编写了“YaciCode?”的新版本,修复了这些 bug,禁用了默认虚拟,并添加了一些新的类函数来提供虚拟方法和类型转换功能。新代码可以在这里找到:Files:wiki_insecure/users/jpatte/YaciCode12.lua。如果可能,我想在编辑我的笔记以添加新功能说明之前获得大家的认可。我进行的每一次测试都运行良好,但可能有所遗漏;同样,如果你看到“更合适的方式”来做事情,欢迎提供任何评论 :-)

这里有几点关于这个版本主要变化的说明

内部重组

为了管理类型转换和显式虚拟,我不得不在代码中添加几个东西,特别是弱表 metaObj,它将实例对象与其元信息(用户不可见)关联起来。这些信息涉及对象的类、它的“superobject”、“lowerobject”等。这主要用于类型转换实现:现在可以将 myB.super 转换回 B 实例(即,myB 本身),因为在元信息中有一个从 myB.supermyB 的链接。该表可以在未来版本中用于存储关于每个实例的任何其他信息。

类实例的 __index 元方法比以前稍微复杂一些,目的是将 myB:foo(myB) 转换为 A.foo(myB.super) 而不是 A.foo(myB)(如果 foo() 定义在 A 级别);这个“简单的”改变修复了 Frederic 上面提到的两个 bug。

类也可以有一些元信息,特别是它们维护着一个虚拟方法列表。每次创建实例时,“虚拟表”都会直接复制到实例(以及它所有的“superinstances”)中。这意味着虚拟方法比类声明的简单方法具有更高的优先级(这正是我们想要的:如果 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 引入的更改的描述;我希望这些改进会有所帮助。如果您认为可以做得更好,或者发现了某个 bug,请随时提供反馈 ;-)

新版本可在 Files:wiki_insecure/users/jpatte/YaciCode12.lua 找到。如果可能,我想在更新整个页面之前收到一些关于此版本的评论。非常感谢您的关注!

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

Bug修复

Peter Bohac 报告了版本 1.2 中关于 class() 方法的一个 bug。作为副作用,默认的 __tostring 元方法(它使用此方法)在实例被“打印”时会引发错误。这个 bugfix 相当简单

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

这个 bug 已经在 YaciCode12.lua 文件中修复。

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

另请参阅


RecentChanges · preferences
编辑 · 历史
最后编辑于 2023 年 6 月 14 日 12:57 GMT (差异)