最小化闭包

lua-users home
wiki

概述

每当在执行期间遇到一个函数声明或表达式时,就会形成闭包。这会将实际函数与词法作用域变量绑定起来,从而形成一个闭包

效率

如果一个生成闭包的操作反复发生,但函数本身实际上并未利用词法作用域,那么将闭包操作移到循环外部会更有效率。

注意:这取决于闭包的实现方式。将来,智能编译器可能会使其变得不必要。(在 Lua 5 中,每个闭包都有自己的“函数环境”(即全局变量)。因此,即使没有上值,闭包在创建时也可能不尽相同。--RiciLake)

评论

在下面的代码中...

function create()
  local e = {}
  e.x = 0
  e.update = function(self)    -- mark
    self.x = self.x + 1
  end
  return e
end

e1 = create()
e2 = create()
e3 = create()

在标记的行中,'update' 函数创建了多少个副本?是一个,在 e1、e2 和 e3 之间共享,还是三个,每个副本对应一个 e1、e2 和 e3?

我计划在我当前的 Lua 项目中大量使用这种方式,我有点担心内存效率等问题。-- NickDavies?

每次调用 create() 函数时都会创建一个闭包,但这只会创建一个函数。-- NickTrout

这似乎是一个相当明确的陈述,即闭包不会被共享,即使它们是相同的(尽管至少函数代码是共享的)。如果效率是一个问题,那么最好创建一个单一的闭包并在各处传递它。例如:

-- Using a 'do/end' block so that the local 'temp' does not pollute
-- the name space.
do
  -- The local variable 'temp' is assigned a closure.
  local function temp(self)
    self.x = self.x + 1
  end

  -- Define the function 'create'. Every time it executes it
  -- assigns a reference to the closure stored in 'temp'.
  function create()
    local e = {}
    e.x = 0
    e.update = temp
    return e
  end
end

这里的关键是(据我所知)每次遇到“函数”表达式/语句时都会创建一个闭包。所以如果你只执行一次,你就可以节省内存重复。

如果一个“函数”表达式被绑定了不止一次,这只有在它在其闭包中包装了词法作用域变量时才是必需的,否则它会效率低下(即使它可能看起来更整洁)。

例如,如果你有类似这样的东西:

function create(init)
  local e = {}
  e.x = 0
  e.reset = function temp(self) self.x = init end
  return e
end

那么你需要单独的闭包,因为每个单独的闭包实例都引用了周围词法作用域中不同的 'init' 变量!

所以

e1 = create(111)
e2 = create(222)
e1:reset() -- resets to '111'
e2:reset() -- resets to '222'.

-- PeterHill

值得一提的是,闭包占用的空间并不比词法绑定的变量多多少。我个人会选择可读性而不是效率,因为从长远来看,可读性就是效率。但如果你真的想最大限度地减少分配,你可能会想把上面的代码写成这样:

do
  local reset

  function reset(self)
    self.x = self[reset]
  end

  function create(init)
    return {
      x = 0,
      [reset] = init,
      reset = reset
    }
  end
end

在这里,我们利用了每个闭包都是唯一的这一事实,以便在新创建的表中可以使用一个唯一的键,这不会干扰任何可能稍后创建的其他键。这不仅避免了创建闭包,还避免了创建词法绑定的变量。当然,这会在表中添加一个键值对,但在大多数情况下,这会是免费的。如果你甚至担心这一点(也许对象实际上正好有八个键,额外的键会迫使它拥有 16 个键的存储空间),你可以将常量重置函数放入元表:

do
  local funcs = {}
  local meta = {__index = funcs}

  function funcs.reset(self)
    self.x = self[funcs]
  end

  function create(init)
    return setmetatable({
      x = 0,
      [funcs] = init
    }, meta)
  end
end

现在,我们使用一个表作为唯一键。我个人认为这种东西不吸引人,因为它真的很难读,而且通常最好将内部变量隐藏在 upvalues 中,在那里它们才真正隐藏,并且访问速度更快。在这种情况下,当然,甚至可能不希望尝试隐藏 init 值;它可能是暴露的对象接口的一部分。将公共对象函数放入元表的这种技术可以节省相当多的空间和时间,如果有很多函数的话。-- RiciLake


RecentChanges · preferences
编辑 · 历史
最后编辑于 2007 年 8 月 4 日下午 3:04 GMT (差异)