物件導向閉包方法 |
|
這是在比較一個物件閉包方法和一個非常單純的 table 為基礎的物件方法。大多數情況下,將水手物件的方法視為元表的其中一部分才算是慣用法,如此一來,每個水手實例就不會需要一個雜湊作為所有函式的用途。這個關鍵的設計方針表示以下的記憶體比較完全沒有用。閉包方法的記憶體開銷明顯會比採用 table 實作物件的可靠方法不佳。
此頁面說明了 物件導向教學 的替代方案
請先閱讀前面提到的頁面,了解替代方法的差異。
Lua 中最常見的 OOP 方式會像這樣
mariner = {} function mariner.new () local self = {} self.maxhp = 200 self.hp = self.maxhp function self:heal (deltahp) self.hp = math.min (self.maxhp, self.hp + deltahp) end function self:sethp (newhp) self.hp = math.min (self.maxhp, newhp) end return self end -- Application: local m1 = mariner.new () local m2 = mariner.new () m1:sethp (100) m1:heal (13) m2:sethp (90) m2:heal (5) print ("Mariner 1 has got "..m1.hp.." hit points") print ("Mariner 2 has got "..m2.hp.." hit points")
以及輸出
Mariner 1 has got 113 hit points Mariner 2 has got 95 hit points
我們在這裡實際上是用冒號將物件(「自我」表格)傳遞至函式。但有必要這麼做嗎?
我們可以用不同的方式獲得相當相同的效能
mariner = {} function mariner.new () local self = {} local maxhp = 200 local hp = maxhp function self.heal (deltahp) hp = math.min (maxhp, hp + deltahp) end function self.sethp (newhp) hp = math.min (maxhp, newhp) end function self.gethp () return hp end return self end -- Application: local m1 = mariner.new () local m2 = mariner.new () m1.sethp (100) m1.heal (13) m2.sethp (90) m2.heal (5) print ("Mariner 1 has got "..m1.gethp ().." hit points") print ("Mariner 2 has got "..m2.gethp ().." hit points")
在這裡,我們不只封裝了變數 `maxhp` 和 `hp`,也封裝了對 `self` 的參照(請注意 `function self.heal` 取代 `function self:heal` - 不再有「自我」糖語)。這種方式會岔開,因為每次呼叫 `mariner.new ()` 都會建構一個新的獨立閉包。很難不注意到除了存取私有變數 `hp` 之外,所有方法的效能都有改善(第一個範例中的 `self.hp` 比第二個範例中的 `self.gethp()` 更快)。但讓我們看看下一個範例。
-------------------- -- 'mariner module': -------------------- mariner = {} -- Global private variables: local idcounter = 0 local defaultmaxhp = 200 local defaultshield = 10 -- Global private methods local function printhi () print ("HI") end -- Access to global private variables function mariner.setdefaultmaxhp (value) defaultmaxhp = value end -- Global public variables: mariner.defaultarmorclass = 0 function mariner.new () local self = {} -- Private variables: local maxhp = defaultmaxhp local hp = maxhp local armor local armorclass = mariner.defaultarmorclass local shield = defaultshield -- Public variables: self.id = idcounter idcounter = idcounter + 1 -- Private methods: local function updatearmor () armor = armorclass*5 + shield*13 end -- Public methods: function self.heal (deltahp) hp = math.min (maxhp, hp + deltahp) end function self.sethp (newhp) hp = math.min (maxhp, newhp) end function self.gethp () return hp end function self.setarmorclass (value) armorclass = value updatearmor () end function self.setshield (value) shield = value updatearmor () end function self.dumpstate () return string.format ("maxhp = %d\nhp = %d\narmor = %d\narmorclass = %d\nshield = %d\n", maxhp, hp, armor, armorclass, shield) end -- Apply some private methods updatearmor () return self end ----------------------------- -- 'infested_mariner' module: ----------------------------- -- Polymorphism sample infested_mariner = {} function infested_mariner.bless (self) -- No need for 'local self = self' stuff :) -- New private variables: local explosion_damage = 700 -- New methods: function self.set_explosion_damage (value) explosion_damage = value end function self.explode () print ("EXPLODE for "..explosion_damage.." damage!!\n") end -- Some inheritance: local mariner_dumpstate = self.dumpstate -- Save parent function (not polluting global 'self' space) function self.dumpstate () return mariner_dumpstate ()..string.format ("explosion_damage = %d\n", explosion_damage) end return self end function infested_mariner.new () return infested_mariner.bless (mariner.new ()) end --------------- -- Application: --------------- local function printstate (m) print ("Mariner [ID: '"..m.id.."']:") print (m.dumpstate ()) end local m1 = mariner.new () local m2 = mariner.new () m1.sethp (100) m1.heal (13) m2.sethp (90) m2.heal (5) printstate (m1) printstate (m2) print ("UPGRADES!!\n") mariner.setdefaultmaxhp (400) -- We've got some upgrades here local m3 = mariner.new () printstate (m3) local im1 = infested_mariner.new () local im2 = infested_mariner.bless (m1) printstate (im1) printstate (im2) im2.explode ()
輸出
Mariner [ID: '0']: maxhp = 200 hp = 113 armor = 130 armorclass = 0 shield = 10 Mariner [ID: '1']: maxhp = 200 hp = 95 armor = 130 armorclass = 0 shield = 10 UPGRADES!! Mariner [ID: '2']: maxhp = 400 hp = 400 armor = 130 armorclass = 0 shield = 10 Mariner [ID: '3']: maxhp = 400 hp = 400 armor = 130 armorclass = 0 shield = 10 explosion_damage = 700 Mariner [ID: '0']: maxhp = 200 hp = 113 armor = 130 armorclass = 0 shield = 10 explosion_damage = 700 EXPLODE for 700 damage!!
一切都解釋得相當清楚。我們很乾淨快速地獲得了所有常見的 OOP 技巧。
開戰時刻。競技場是「Intel(R) Core(TM)2 Duo CPU T5550 @ 1.83GHz」。對手是
-- Table approach -------------------- -- 'mariner module': -------------------- mariner = {} -- Global private variables: local idcounter = 0 local defaultmaxhp = 200 local defaultshield = 10 -- Global private methods local function printhi () print ("HI") end -- Access to global private variables function mariner.setdefaultmaxhp (value) defaultmaxhp = value end -- Global public variables: mariner.defaultarmorclass = 0 local function mariner_updatearmor (self) self.armor = self.armorclass*5 + self.shield*13 end local function mariner_heal (self, deltahp) self.hp = math.min (self.maxhp, self.hp + deltahp) end local function mariner_sethp (self, newhp) self.hp = math.min (self.maxhp, newhp) end local function mariner_setarmorclass (self, value) self.armorclanss = value self:updatearmor () end local function mariner_setshield (self, value) self.shield = value self:updatearmor () end local function mariner_dumpstate (self) return string.format ("maxhp = %d\nhp = %d\narmor = %d\narmorclass = %d\nshield = %d\n", self.maxhp, self.hp, self.armor, self.armorclass, self.shield) end function mariner.new () local self = { id = idcounter, maxhp = defaultmaxhp, armorclass = mariner.defaultarmorclass, shield = defaultshield, updatearmor = mariner_updatearmor, heal = mariner_heal, sethp = mariner_sethp, setarmorclass = mariner_setarmorclass, setshield = mariner_setshield, dumpstate = mariner_dumpstate, } self.hp = self.maxhp idcounter = idcounter + 1 self:updatearmor () return self end ----------------------------- -- 'infested_mariner' module: ----------------------------- -- Polymorphism sample infested_mariner = {} local function infested_mariner_set_explosion_damage (self, value) self.explosion_damage = value end local function infested_mariner_explode (self) print ("EXPLODE for "..self.explosion_damage.." damage!!\n") end local function infested_mariner_dumpstate (self) return self:mariner_dumpstate ()..string.format ("explosion_damage = %d\n", self.explosion_damage) end function infested_mariner.bless (self) self.explosion_damage = 700 self.set_explosion_damage = infested_mariner_set_explosion_damage self.explode = infested_mariner_explode -- Uggly stuff: self.mariner_dumpstate = self.dumpstate self.dumpstate = infested_mariner_dumpstate return self end function infested_mariner.new () return infested_mariner.bless (mariner.new ()) end
以及
-- Closure approach -------------------- -- 'mariner module': -------------------- mariner = {} -- Global private variables: local idcounter = 0 local defaultmaxhp = 200 local defaultshield = 10 -- Global private methods local function printhi () print ("HI") end -- Access to global private variables function mariner.setdefaultmaxhp (value) defaultmaxhp = value end -- Global public variables: mariner.defaultarmorclass = 0 function mariner.new () local self = {} -- Private variables: local maxhp = defaultmaxhp local hp = maxhp local armor local armorclass = mariner.defaultarmorclass local shield = defaultshield -- Public variables: self.id = idcounter idcounter = idcounter + 1 -- Private methods: local function updatearmor () armor = armorclass*5 + shield*13 end -- Public methods: function self.heal (deltahp) hp = math.min (maxhp, hp + deltahp) end function self.sethp (newhp) hp = math.min (maxhp, newhp) end function self.gethp () return hp end function self.setarmorclass (value) armorclass = value updatearmor () end function self.setshield (value) shield = value updatearmor () end function self.dumpstate () return string.format ("maxhp = %d\nhp = %d\narmor = %d\narmorclass = %d\nshield = %d\n", maxhp, hp, armor, armorclass, shield) end -- Apply some private methods updatearmor () return self end ----------------------------- -- 'infested_mariner' module: ----------------------------- -- Polymorphism sample infested_mariner = {} function infested_mariner.bless (self) -- No need for 'local self = self' stuff :) -- New private variables: local explosion_damage = 700 -- New methods: function self.set_explosion_damage (value) explosion_damage = value end function self.explode () print ("EXPLODE for "..explosion_damage.." damage!!\n") end -- Some inheritance: local mariner_dumpstate = self.dumpstate -- Save parent function (not polluting global 'self' space) function self.dumpstate () return mariner_dumpstate ()..string.format ("explosion_damage = %d\n", explosion_damage) end return self end function infested_mariner.new () return infested_mariner.bless (mariner.new ()) end
table 方法的速度測試碼
assert (loadfile ("tables.lua")) () local mariners = {} local m = mariner.new () for i = 1, 1000000 do for j = 1, 50 do -- Poor mariner... m:sethp (100) m:heal (13) end end
closures 方法的速度測試碼
assert (loadfile ("closures.lua")) () local mariners = {} local m = mariner.new () for i = 1, 1000000 do for j = 1, 50 do -- Poor mariner... m.sethp (100) m.heal (13) end end
結果
tables: real 0m47.164s user 0m46.944s sys 0m0.006s closures: real 0m38.163s user 0m38.132s sys 0m0.007s
table 方法的記憶體使用量測試碼
assert (loadfile ("tables.lua")) () local mariners = {} for i = 1, 100000 do mariners[i] = mariner.new () end print ("Memory in use: "..collectgarbage ("count").." Kbytes")
closures 方法的記憶體使用量測試碼
assert (loadfile ("closures.lua")) () local mariners = {} for i = 1, 100000 do mariners[i] = mariner.new () end print ("Memory in use: "..collectgarbage ("count").." Kbytes")
結果
tables: Memory in use: 48433.325195312 Kbytes closures: Memory in use: 60932.615234375 Kbytes
沒有贏家,也沒有輸家。讓我們看看我們到底得到了什麼...
+---------------------------------------+---------------------------+-------------------------------------------------+ | Subject | Tables approach | Closured approach | +---------------------------------------+---------------------------+-------------------------------------------------+ | Speed test results | 47 sec. | 38 sec. | | Memory usage test results | 48433 Kbytes | 60932 Kbytes | | Methods declaration form | Messy | More clean | | Private methods | Fine | Fine | | Public methods | Fine | Fine | | Private variables | Not available | Fine | | Public variables | Fine | Fine | | Polymorphism | Fine | Fine | | Function overriding | Ugly (namespace flooding) | Fine (if we grant access only to direct parent) | | Whole class definition (at my taste) | Pretty messy | Kinda clean | +---------------------------------------+---------------------------+-------------------------------------------------+
如你所見,這兩種方法在功能上非常相似,但它們在表現方式上卻有顯著差異。方法的選擇主要取決於程式設計師的美學偏好。我個人會比較喜歡對大型物件使用閉包方法,並對只有資料(沒有函式參照)的小型物件使用 table。順帶一提,別忘了 meta-table。