最小化闭包 |
|
每当在执行期间遇到一个函数声明或表达式时,就会形成闭包。这会将实际函数与词法作用域变量绑定起来,从而形成一个闭包。
如果一个生成闭包的操作反复发生,但函数本身实际上并未利用词法作用域,那么将闭包操作移到循环外部会更有效率。
注意:这取决于闭包的实现方式。将来,智能编译器可能会使其变得不必要。(在 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