匿名函数和闭包

本教程讨论了 Lua 中匿名函数闭包背后的基础知识。首先,让我们了解一个典型的 Lua 函数看起来是什么样

local function myFunction( parameters )
    -- Function content
end

它在后台创建了一个名为 myFunction 的变量,该变量只能在此代码块中使用,并且存储在该代码块在内存中的位置。例如,如果您执行print( myFunction ),控制台会打印类似于 0x4a8cd834 的内容。基本上,当函数被调用时,Lua 将跳转到内存中的这个位置。

Lua 还允许您使用以下方式定义函数

local myFunction = function( parameters )
    -- Function content
end

在此符号中,很明显您正在将一个变量分配给一个代码块,更确切地说,是该代码块的内存地址。归根结底,两种符号都完成相同的事情,但第二种符号允许一些额外的灵活性。使用这种形式编写时,您实际上是从一个未命名的代码块中取出一个部分,并为其命名。

匿名函数

Lua 还允许您在不使用名称的情况下编写函数。这称为匿名函数。为了理解常规函数和匿名函数之间的区别,让我们看一些示例

local function myFunction( parameters )
    -- Function content
end

timer.performWithDelay( 1000, myFunction, 10 )

在此情况下,timer.performWithDelay() 函数指定了一段 1000 毫秒的时间间隔、一个要执行的函数,以及要重复的整数计数。实际上,myFunction() 将以 1000 毫秒的间隔触发 10 次,但 Lua 在内部执行的是将 myFunction() 函数的内存地址传递给 timer.performWithDelay() 函数。

这意味着您还可以编写以下任意一个命令

timer.performWithDelay( 1000,
    function()
        print( "Hello World" )
    end,
10 )
timer.performWithDelay( 1000, function() print( "Hello World" ); end, 10 )

在第二个示例中,注意以下命令后的分号 (;)print( "Hello World" )如果您在同一行中使用带有多个命令的匿名函数,那么每个命令都必须以分号结尾,以便向 Lua 指示每个新命令的开始位置。

在这两种情况下,您无需使用引用函数内存地址的变量,只需创建一个未命名的匿名函数,该函数将在计时器触发时执行。

何时使用

匿名函数在您需要对 onComplete 或回调监听函数进行编码时最好用,例如在 timer.performWithDelay()transition.to()audio.play() 中 — 但您应该多久使用一次?从代码可读性和可持续性的角度来看,可能不太经常使用,但如果函数仅在您代码的特定范围内使用,并且相对较短,那么匿名函数是一种完全有效的编码技术。

请注意,有时您不能使用匿名函数。一个具体示例是当您将函数分配给事件监听器时。因为 Solar2D 中的事件监听器系统基于存储指定用于处理事件的函数的地址,所以您在通过 object:removeEventListener() 删除事件监听器时传入的地址必须与通过 object:addEventListener() 传入的地址相匹配。

Lua 闭包

Lua 闭包是另一种有用的技术。它真正发挥作用的时候是将参数传递给不寻找额外参数的函数处理程序。为了说明这一点,让我们看一个 transition.to() 示例

local function myFunction( parameters )
    -- Function content
end

transition.to( playerObject, { time=1000, alpha=0, onComplete=myFunction } )

在此示例中, myFunction() 将传递一个参数 target,它正在执行过渡的对象。但我们假设显示对象是一个数组的成员,您需要更多了解对象本身在被调用的函数中。这可以通过 **Lua 闭包** 完成

local function myFunction( parameters )
    -- Function content
end

local balls = {}
balls[1].ball = display.newImageRect( "greenball.png", 64, 64 )
balls[1].color = "green"
balls[2].ball = display.newImageRect( "redball.png", 64, 64 )
balls[2].color = "red"

for i = 1,#balls do
    transition.to( balls[i].ball,
        { time=250, x=200,
            onComplete=function( target )
                myFunction( target, balls[i].color )
            end
        }
    )
end

在此示例中,您不是将 myFunction() 作为回调的一部分来调用,而是调用一个匿名函数。该匿名函数采用 transition.to() 提供的接收到的 target 参数,然后在其内部用 target 对象引用和球的 color 属性调用您的 **已命名** 函数。

闭包不仅仅是匿名函数。当您有一个函数位于函数内部时,子函数可以访问它所有父函数的局部对象或 **upvalue**。通常,当一个函数结束时,任何局部作用域变量都会超出范围,当您再次调用该函数时,从头开始。请考虑此示例

local function counter()

    local i = 0
    while i < 10 do
        i = i + 1
    end
    return i
end

print( counter() )

在这种情况下,当您调用 counter() 函数时,它将打印值 10,因为当函数的作用域结束时,它的局部变量也会结束。

现在,让我们修改该函数,以实际返回执行计数的函数

local function counter()

    local i = 0
    return function ()
        i = i + 1
        return i
    end
end

local counter1 = counter()

print( counter1() )  --> 1
print( counter1() )  --> 2

在第 10 行之后,我们在 counter1 变量中有一个新函数,当我们多次运行它时,Lua 会意识到匿名函数可能仍然会被使用,它应该保留它当前作用域中 i 的当前值。这就是 Lua 闭包可以为您做的事情。

为了扩展此概念,如果您要创建另一个计数器,则会出现以下结果

local counter2 = counter()

print( counter1() )  --> 3
print( counter2() )  --> 1
print( counter1() )  --> 4
print( counter2() )  --> 2

从本质上讲,匿名函数的每个版本都持有自己的 i 并保持其在自身内具有作用域。因此,当您创建第二个计数器时, i 对于该实例重置为 0

结论

如您所见,匿名函数和闭包提供了一个强大的方式来跟踪逐函数基础上的变量实例。在 Solar2D 的上下文中,这允许您将参数传递给通常无法控制传入内容的函数。