又一個類別實作 |
|
歡迎大家提供貢獻。請不要猶豫,貢獻分支儲存庫、發表問題並建議變更!
這當然不是什麼原創,但我認為這對其他一些 Lua 使用者或愛好者來說可能很有用。我已經看過許多類別的實作,暗示如何使用元資料表來模擬物件導向的層面,例如實體化或繼承等等(例如參閱 ObjectOrientationTutorial、LuaClassesWithMetatable、InheritanceTutorial、ClassesAndMethodsExample 和 SimpleLuaClasses),但我認為在增加一些額外的功能和設施後,應該可以更進一步。這是我在此建議另一個實作的原因,它主要是建立在其他實作之上,但加入了一些額外內容。我並沒有假裝這是最好的方式,但我想它對我以外的一些人來說可能很有用,所以我希望在此分享;)
請注意,這個程式碼已經設計成盡可能易於使用;因此這絕對不是執行動作最快的途徑。它相當複雜,大量使用元資料表、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
每個類別有一個「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"
請注意,由於「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 = nil
在 A:init()
中沒有任何副作用。它沒有建立 foo
的執行個體變數。
但如果你執行 self.foo=false
,它就會建立 foo
的執行個體變數,而且當 myB:setFoo()
呼叫 A.setFoo(myB)
時,myB.foo
並不存在,不過 在更高階層中存在(值為 false
),而且在這案例中,它最後會被 foo
函式參數取代。false
的好處是,如果進行 if self.foo then
這樣的測試,如果 self.foo 是 nil
或 false
都會運作相同。
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 日
嗯,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.super
到 myB
的連結。此表格可以在未來的版本中用來儲存每個實例的其他資訊。
類別實例的 __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
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 日