又一個類別實作

lua-users home
wiki

程式碼可以在這裡找到:https://github.com/jpatte/yaci.lua

歡迎大家提供貢獻。請不要猶豫,貢獻分支儲存庫、發表問題並建議變更!

簡介

這當然不是什麼原創,但我認為這對其他一些 Lua 使用者或愛好者來說可能很有用。我已經看過許多類別的實作,暗示如何使用元資料表來模擬物件導向的層面,例如實體化或繼承等等(例如參閱 ObjectOrientationTutorialLuaClassesWithMetatableInheritanceTutorialClassesAndMethodsExampleSimpleLuaClasses),但我認為在增加一些額外的功能和設施後,應該可以更進一步。這是我在此建議另一個實作的原因,它主要是建立在其他實作之上,但加入了一些額外內容。我並沒有假裝這是最好的方式,但我想它對我以外的一些人來說可能很有用,所以我希望在此分享;)

請注意,這個程式碼已經設計成盡可能易於使用;因此這絕對不是執行動作最快的途徑。它相當複雜,大量使用元資料表、upvalue、代理......我已經嘗試盡量最佳化它,但我不是專家,所以可能有一些我不知道的捷徑。

現在我來描述你能用它做什麼。歡迎任何意見和建議!

特色

內容

這個程式碼只「匯出」兩個東西:基礎類別「物件」和函式「newclass()」。

類別定義

基本上有 2 種定義新類別的方法:呼叫「newclass()」或使用「Object:subclass()」。這些函式會傳回新類別。

在建立類別時,你應為它指定名稱。這並非絕對必要,但可以提供幫助(例如用於除錯)。如果你沒有提供任何名稱,類別將被稱為「Unnamed」。有數個未命名類別並非問題。

當你使用「Object:subclass()」時,新類別將會是「物件」的直接子類別。不過「newclass()」接受一個第二引數,它可以是類別「物件」以外的另一個超類別(如果你沒有提供任何類別,它將選擇「物件」類別;這表示所有類別都是「物件」的子類別)。請注意,每個類別都有「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」呼叫其超級類別的建構函式(詳見以下)。請注意「物件: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
任何事件都可以使用,但 OO 實作中需要的 '__index' 和 '__newindex' 除外。你可以使用這個特性來定義像 '__add'、'__eq' 等的運算子。'__tostring' 在這裡是一個非常有用的事件,因此「物件」類別為它實作一個預設版本,它單純會傳回一個字串「一個 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。我驚訝(又開心)地發現我們的實作有這麼多相似之處,至少在使用方式上是如此。然而,這裡繼承時只須複製事件,每個執行個體均包含其超類別的一個執行個體,還有其他一些附加資訊。

我對這段程式碼仍不完全滿意。即使執行個體變數(函數以外的變數)在這裡是「虛擬的」,這也是一個很大的問題,因為如果超類別和子類別使用名稱相同的變數,可能會導致一些怪異的錯誤。但我猜這很難輕易獲得協助 :/ -- 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() 方法中定義類別的所有執行個體變數,對於想要設為 nil 的任何變數,請使用 false。這與 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)(也就是 self 等於 myB)。myB.foo 不存在於 myB 或更高階層,所以 foo 這個鍵值會加到 myB 的資料表。在釋放時,會呼叫 B.free(myB)self 等於 myB)。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 的執行個體,如果 A 定義了 foo() 而 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 中沒有「虛擬」或「最後」關鍵字。因此,最佳行為是什麼?

在撰寫這段程式碼時,我認為所有方法都應該預設為虛擬,這就是我以這種方式組織這些方法的原因。但我認為,Frederic 報告的錯誤太嚴重,不能接受。

因此,我撰寫了一個新版本的「YaciCode?」,修正這些錯誤、停用預設虛擬方法,並新增了一些類別函式來提供虛擬方法和型別轉換功能。可以在這裡找到新程式碼:檔案:wiki_insecure/users/jpatte/YaciCode12.lua。如果可以的話,我希望在編輯上方筆記時有人認可,以便新增有關新功能的說明。我執行的所有測試都執行良好,但我可能漏掉了某些事項;而且,如果您有「更適合」的做事方式,請不吝指教 :-)

以下是有關此版本主要變更事項的幾個筆記:

內部重新組織

為了管理型別轉換和明確的虛擬方法,我必須將一些項目新增至程式碼,特別是一個將實例物件與其元資訊 (使用者無法看到) 關聯的弱表格 metaObj。這些資訊包含物件的類別、其「超級物件」、其「下層物件」等。這主要用於型別轉換的實作:現在可以將 myB.super 轉換回 B 實例 (亦即 myB 本身),這是因為元資訊中有從 myB.supermyB 的連結。此表格可以在未來的版本中用來儲存每個實例的其他資訊。

類別實例的 __index 元方法比以前複雜一些,以便將 myB:foo(myB) 轉換為 A.foo(myB.super),而不是在 foo() 定義於 A 的層級時轉換為 A.foo(myB);這個「簡單」的變更修復了 Frederic 在上面提到的兩個錯誤。

類別也可以具有一些元資訊,特別是它們維護著虛擬方法列表。每次建立一個實例時,「虛擬表格」會直接複製到實例中(及其所有「超實例」中)。這表示虛擬方法比類別宣告的簡單方法具有更高的優先權(這正是我們想要的:如果 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 而不是 myB 作為 self。但是,如果我們想要讓 whoami() 成為虛擬的呢?換句話說,我們如何用 B:whoami() 覆寫 A:whoami(),即使在 A 的層級中(僅針對 B 的實例)?那麼,你只要寫入 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 引進變更的說明;我希望這些改良會有用處。如果您認為還有更好的作法或在某個地方發現錯誤,請不要猶豫,提供回饋 ;-)

如果您有可能,在更新整個頁面之前,新版會提供在 檔案: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 (差異)