面向对象闭包方法

lua-users home
wiki

警告

这是将闭包用于对象的方案与极其简单的基于表的对象方案进行比较。在大多数情况下,将水手对象的函数作为元表的组成部分,以便每个水手实例不需要为所有函数使用哈希表,这将被视为惯用方式。这个关键的设计点意味着下面的内存比较毫无意义。与使用表实现对象的合理方法相比,闭包方法的内存开销显然会更不理想。

简介

此页面描述了 面向对象教程 的替代方法。

请先阅读上面提到的页面,以了解替代方法的差异。

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

我们实际上在这里使用冒号来将对象('self' 表)传递给函数。但我们必须这样做吗?

简单案例

我们可以用不同的方式获得几乎相同的功能

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` - 没有更多 `self` 糖)。这是因为每次调用 `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

表方法的速度测试代码

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

闭包方法的速度测试代码

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

表方法的内存使用测试代码

assert (loadfile ("tables.lua")) ()

local mariners = {}

for i = 1, 100000 do
   mariners[i] = mariner.new ()
end

print ("Memory in use: "..collectgarbage ("count").." Kbytes")

闭包方法的内存使用测试代码

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                                     |
+---------------------------------------+---------------------------+-------------------------------------------------+

如您所见,两种方法在功能上非常相似,但在表示方面存在显著差异。方法的选择主要取决于程序员的美学偏好。我个人更喜欢对大型对象使用闭包方法,而对小型对象(只有数据,没有函数引用)使用表。顺便说一下,不要忘记元表。

另请参阅


最近更改 · 偏好设置
编辑 · 历史
最后编辑于 2023 年 2 月 1 日凌晨 6:04 GMT (差异)