关于 lua 的 function

参考链接

Programming in Lua : 5.2

关于多返回值

固定参数

lua 中允许返回多个值,并且返回顺序是固定的,
返回值可以被多个声明的 local 变量接收,例如:

function r()
    return "v1", "v2", "v3"
end

local a, b, c, d = r()
print(a, b, c, d)

-> v1   v2  v3  nil

同样的,函数在正常情况下声明的 固定参数,也可以用这些返回值填充:

function r()
    return "v1", "v2", "v3"
end

function p(a, b, c)
    print(a, b, c)
end

p(r())

-> v1   v2  v3

指定返回值

如果只需要使用第二个变量,可以用约定俗成的写法写一个 弃元(dummy)

local _, x = string.find(s, p)

也可以使用 select 指定需要用的返回值

-- select(number, func)
print( select(1, string.find("hello world", " hel")))
print( select(2, string.find("hello world", " hel")))

关于 ...

... 作为(参数数量)可变长的参数,称为 不定参数, 类似于 C# 中的 params 定义,需要放在固定长参数后面,例如:

function g(a, b, ...) end

arg table

lua 用 ... 指代调用方法时产生的一系列参数,其在 function 中隐式地作为 arg 变量(table类型),
它的 key 是参数的序号(从1开始),value 是参数的值(保持原有类型)
且 arg 拥有一个 n 成员指代参数数量(因此当你遍历整个 table 时会额外打印一个 n 的键值对)

例如:

function sample(fixed, ...)
    print("固定参数:"..fixed)

    print("不定参数长度:", arg.n)

    for k,v in pairs(arg) do
        print("key:"..k, "tvalue:"..tostring(v), "ttype:"..type(v))
    end
end

sample(
"fixed",
"hello world", 9999, false, 3.0
)

使用时,直接访问参数位置即可:

function r(...)
    print("参数1", arg[1]);
    print("参数3", arg[3]);
end

r(1)

由于 lua 的特性,即使用户没有填入 参数3 也不会引发异常

positional

lua 天生不支持 positional 这种操作
但可以借助 os.rename 实现类似的功能
参考: https://www.lua.org/pil/5.3.html

unpack

arg 变量是 lua 默认将 ... 进行 打包(pack) 后的一个 table
如果你想将 ... 中可变长的参数,作为固定参数调用另一个方法,则需要进行 解包(unpack)
解包一般用:

unpack(arg)

例如:

function send(...)
    print(arg.n)
    local a, b, c = unpack(arg)
    print(a, b, c)
end

send("a", "b");

-> 2
-> a    b   nil

这个例子中,使用 unpack 完成对不定长参数的转换后,得到的多个返回值可以像 function 的返回值那样被多个 local 变量接收

function func(arg1, arg2, arg3)
    print("arg1:", arg1);
    print("arg2:", arg2);
    print("arg3:", arg3);
end

function send(...)
    func(unpack(arg))
end

send("a", "b");

pack

arg 参数相当于默认对 ... 进行打包后产生的一个 table,lua 提供了一个 table.pack 函数1 允许你在 function 参数以外的其他地方手动进行打包,

但是这个打包函数会在使用者传入 nil 时遍历中断而产生非预期结果,因此很多地方会用一种 SafePack 的方式,如下:

---@param ... any 
---@return table
SafePack = function(...)
    local t = {...}
    -- select('#', ...) 返回 不定参数 的数量
    t.n = select('#', ...)
    return t 
end

self 和 第一类值的意义

引用传递

lua 语言中的 function 作为 第一类值 和常规的 string、number 是一样的地位
可以把function的赋值理解为传递了一个指针
当我们 “调用” 这个指针的内容时,调用者可以传递参数进去,例如:

local fun = print
fun("hello world")

它和普通值类型的区别在于,它提供了一个 构造 的能力,
就好像 table 可以用 {} 进行 构造, 这两种概念是一样的(以至于你可以在 table 构造时声明一个 function,参考下文)

因为 function 本身是一个值类型,因此这种调用方式也是可以的:

-- 作为 table 的一个成员
instance["fun"](123)

-- 取它的引用
local func = instance["fun"]
func(123)

模拟面向对象

例如这个例子,我们指定一个 local function

local function localFun(caller, value)
    print(caller.name, value)
end

local instance = {}
instance.name = "some instance"
instance.fun = localFun

instance.fun(instance, "hello world")

这时,为了让 “无主” 的 local function 能够一定程度上和实例进行绑定,我们约定俗成地会将实例作为第一个参数传入
但每个函数对于实例的叫法声明可能没有规范,有的会叫 this、self、instance、caller……
lua 自带一种语法糖来规范这种调用(并不是特殊机制)

冒号语法糖

上面提到的写法在 lua 里有语法糖,即:

-- 第一个参数仍然声明为实例,但这次调用者不需要传
function localFun(caller, value)
    print(caller.name, value)
end

local instance = {}
instance.name = "some class"
instance.fun = localFun

-- 采用冒号进行调用
-- 默认会传一个实例(class)
instance:fun("hello world")

同样的,如果 function 不是无主的,而是 table 的一个成员,那么 function 也可以省略第一位参数的声明,使用缺省的 self 指代实例(类似于 C# 的 self 指针)

local instance = {}
instance.name = "some class"

-- 方法也用冒号申明
function instance:fun(value)
    print(self.name, value)
end

instance:fun("hello world")

但这并不意味着这个 function 就是私有的、是绑定的,
它随时可能被别人拿去用,甚至把 function 内部用的 self 偷换成别的实例(哪怕这不是调用者期望的)

闭包问题

闭包的概念

闭包作用域的概念可以参考:Programming in Lua : 6.1

lua 里的 全局变量,是真正意义上的全局,所有在一个 environment(lua虚拟机)里的内容都共享
而在 lua 里也有作用域,被称为 lexical scoping(语法域),例如 function 和 end 的语法区间内
而这些作用域内可以使用上层的变量,称为 external local variable(外部局部变量), 在这个上层作用域结束之前,这个外部局部变量都会保持它的值,这会出现在很多场合,例如

  • 迭代器
  • 模拟类的静态变量 等等

委托和self传递的问题

lua 并没有 .NET 那么完善、灵活的机制,
当你尝试在 lua 中使用 delegate 机制的时候,你的 delegate 在申明的时刻就绑定了一个 实例,例如:

local tool = {}
tool.name = "tool"
function tool:CallTool()
    print(self.name)
end

local instance = {}
instance.name = "instance"
function instance:CallInstance()
    instance:callback()
end

instance.callback = tool.CallTool
instance:CallInstance()

-> instance

这种问题的关键在于,一定要牢记 self 只是一个语法糖,并不是什么很牛逼的机制
你在某个 table|module 内部用 冒号 定义了一个 function,只是说它默认是和这个 table|module 关联上的,不代表它完全归属于这个实例
任何时候,其他的 table|module 都可以将它视为 第一类值,把引用给“偷”过去,然后换一个 self 进行调用
为此,在 lua 中的 delegate 必须注明调用时的实例,这个过程有人称之为 bind,例如:

---闭包绑定
Bind = function(self, func)
    -- assert(self == nil or type(self) == "table")
    assert(func ~= nil and type(func) == "function")
    if self == nil then
        return function(...)
            return func(...)
        end
    else
        return function(...)
            return func(self, ...)
        end
    end
end

匿名方法

这种未命名的方法定义被称为匿名方法:

local fun = function(...)
    print(...)
end

正常来说需要区分 .: 的语法糖

local instance = {}
instance.name = "instance"
instance.fun = function(self, a)
    print(self.name, a)
end

instance.fun(instance, 1)
instance:fun(1)

-> instance        1
-> instance        1

然而在 table 构造时声明,似乎可以更加灵活

local instance = {
    name = "instance",
    fun1 = function(a)
        print(a)
    end,
    fun2 = function(self, a)
        print(self.name, a)
    end
}

instance.fun1(1)
instance:fun2(2)

-> 1
-> instance        2

匿名方法的定义等价于你创建了一个 指针(或者说第一类值)
因此在一些需要callback的场合你可以直接传入一个匿名方法(但要注意 闭包绑定self的问题)
官方把这称为 higher-order(高阶调用),这种用法只是 function 作为第一类值的一种灵活用法,并不是这种值的特权

嵌套调用的细节

Programming in Lua : 6.3
这篇文章有提到,lua 在方法嵌套时是 proper tail 的调用机制,例如

function f(x)
    return g(x)
end

当 f 调用完成时, f 的资源就可以释放了,不需要和一些语言一样保持一个方法栈
这也直接使得 lua 不需要限制递归层数,并且在单独的 lua 环境里不会因为 死循环、恶性递归(并非真正意义上的递归) 导致 StackOverflow2


  1. SciTE 默认的 5.1版本是没有的,需要更高版本 
  2. https://www.lua.org/pil/6.3.html : Because a proper tail call uses no stack space, there is no limit on the number of “nested” tail calls that a program can make. For instance, we can call the following function with any number as argument; it will never overflow the stack 

Leave A Reply

Your email address will not be published. Required fields are marked *