物件導向閉包方法

lua-users home
Wiki

注意事項

這是在比較一個物件閉包方法和一個非常單純的 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。

另請參閱


最新變更 · 偏好設定
編輯 · 歷史記錄
最後編輯日期為 2023 年 2 月 1 日,格林威治標準時間上午 6:04 (diff)