为函数参数定义默认值

109
在Lua维基上,我找到了一种为丢失的参数定义默认值的方法:
function myfunction(a,b,c)
    b = b or 7
    c = c or 5
    print (a,b,c)
end

那是唯一的方式吗?PHP风格的myfunction (a,b=7,c=5)似乎不起作用。并不是说Lua方式不起作用,我只是想知道这是否是唯一的方法。

7个回答

108
如果你想要像PHP或Python一样使用命名参数和默认值,你可以使用一个表构造器来调用你的函数:
myfunction{a,b=3,c=2}

(这在 Lua 中的许多地方都可以看到,例如 LuaSocket 协议模块的高级形式IUPLua 的构造函数。)
(该函数本身可能具有如下签名:)
function myfunction(t)
    setmetatable(t,{__index={b=7, c=5}})
    local a, b, c =
      t[1] or t.a, 
      t[2] or t.b,
      t[3] or t.c
    -- function continues down here...
end

任何在参数表中缺失的值都将从其元表中的__index表中获取(请参阅有关元表的文档)。
当然,使用表构造函数和函数可以实现更高级的参数样式-您可以编写所需的任何内容。例如,此处是一个构造函数的函数,该函数从定义参数名称和默认值的表以及接受常规参数列表的函数中获取命名或位置参数表。
作为非语言级功能,这些调用可以更改以提供新的行为和语义:
  • 变量可以被赋予多个名称
  • 位置变量和关键字变量可以交替 - 定义两者都可以优先考虑任一(或导致错误)
  • 可以创建仅限关键字的无位置变量,以及无名称的仅限位置变量
  • 通过解析字符串可以完成相当冗长的表构造
  • 如果使用除1个表以外的其他东西调用函数,则可以直接使用参数列表
编写参数转换器的一些有用函数是unpack(在5.2中移动到table.unpack),setfenv(在5.2中已弃用,使用新的_ENV构造)和select(从给定的参数列表返回单个值,或使用'#'返回列表的长度)。

1
我刚开始使用Lua,暂时会保持简单,但是你的帖子非常有用。以后可能会派上用场。谢谢。 - ripat
1
在大多数情况下,保持简单就是正确的方式。元参数接口仅在具有大量可选属性(例如UI对象)的对象中才真正必要。 - Stuart P. Bentley
我认为这个答案才是解决问题的答案,而不是当前被标记为已接受的答案。感谢启示 :) - Undo
3
需要注意的是,在这个回复中使用的 x or default 表达式并不完全等同于参数默认值,而只是一种简单的解决方法,只有在 nilfalse 都是无效的参数值时才有效。假设布尔类型参数 x 的默认值为 true,调用者传递了一个显式的 false,那么 x or true 将返回 true,即使实际上传入了 false。更好的版本应该是 if x == nil then x = default end,这样也更容易理解;但它仍然无法处理显式的 nil 参数。 - Jesper
哦,嘿,这现在是被接受的答案!不错。 (为了记录,并将@Undo的评论放入上下文中,jpjacobs在下面的答案长期以来一直是被接受的答案:我差点因此获得了第二个“大众化”徽章。) - Stuart P. Bentley
显示剩余3条评论

53

在我看来,没有其他的方法。这正是Lua的心态:没有花哨的东西,除了一些语法糖外,没有多余的方式来完成简单的事情。


13
下面由斯图尔特·P·本特利的答案完全否定了这个答案,其中函数与表构造函数一起调用。这是 Lua 的优势之一:虽然它没有很多花哨的东西,但 Lua 的基本构件允许您做无限的事情,这对于语言的小巧和简单来说确实是非常了不起的。 - Colin D Bennett
@ColinDBennett 谢谢你发言:看起来 OP 已经阅读了您的评论并在那年 12 月相应更改了接受的答案。(现在为了明确起见是“上面的 Stuart P. Bentley 的答案”) - Stuart P. Bentley
这是正确的答案,但真正的原因是在Lua中参数通过栈传递,因此调用 f(a=1) 将需要检查 arg a 的位置 _每次调用 f_,这比仅使用定位参数调用 f 要昂贵得多。如果Lua是一种静态语言,在这种语言中,这些信息将是静态的并且在编译时已知,则具有命名参数不会产生运行时成本,并且现在可能已成为一项功能。 - capr

24

从技术上讲,有b = b == nil and 7 or b(应该在false是有效值的情况下使用,因为false or 7的结果为7),但这可能不是您要寻找的。


1
如果您不检查“false”,一种更简单的方法是将变量放在前面,将默认值放在最后。 b = b or 7 - Rebs
2
由于 OP 在问题中提到了这一点,我觉得提及它是多余的(问题是关于除了 b = b or 7 以外定义默认变量的方法)。 - Stuart P. Bentley

9
到目前为止,我发现唯一有意义的做法是像这样进行操作:
function new(params)
  params = params or {}
  options = {
    name = "Object name"
  }

  for k,v in pairs(params) do options[k] = v end

  some_var = options.name
end

new({ name = "test" })
new()

4

一如既往,“Lua 给你力量,你构建机制”。首先要区分的是命名参数和常用的参数列表

参数列表

假设你的所有参数都在参数列表中给出,它们都将被初始化。此时,你无法区分“未传递”和“作为nil传递”之间的区别-两者都将简单地为nil。设置默认值的选项有:

  1. 如果你希望得到一个真值(不是nilfalse),可以使用or运算符。在这种情况下,即使给定的值是false,也可以进行默认设置。
  2. 使用显式的nil检查param == nil,可以采用if param == nil then param = default end或典型的 Lua 三元结构param == nil and default or param

如果发现自己经常重复使用第2点的模式,可能需要声明一个函数:

function default(value, default_value)
    if value == nil then return default_value end
    return value
end

(是否使用全局或局部范围来处理此函数是另一个问题,我在这里不会详细讨论。)

我已经包括了以下示例的三种方式:

function f(x, y, z, w)
    x = x or 1
    y = y == nil and 2 or y
    if z == nil then z = 3 end
    w = default(w, 4
    print(x, y, z, w)
end
f()
f(1)
f(1, 2)
f(1, 2, 3)
f(1, 2, 3, 4)

请注意,这也允许省略中间的参数;末尾的nil参数也将被视为缺失。
f(nil)
f(nil, 2, 3)
f(nil, 2, nil, 4)
f(1, 2, 3, nil)

可变参数

Lua的一个较少被人知晓的特性是实际上可以确定传递了多少个参数,包括通过select函数区分显式传递的nil参数和"无参数"。让我们使用这个特性来重写我们的函数:

function f(...)
    local n_args = select("#", ...) -- number of arguments passed
    local x, y, z, w = ...
    if n_args < 4 then w = 4 end
    if n_args < 3 then z = 3 end
    if n_args < 2 then y = 2 end
    if n_args < 1 then x = 1 end
    print(x, y, z, w)
end
f() -- prints "1 2 3 4"
f(nil) -- prints "nil 2 3 4"
f(1, nil) -- prints "1 nil 3 4"
f(1, nil, 3) -- prints "1 nil 3 4"
f(nil, nil, nil, nil) -- prints 4x nil

注意事项:(1)参数列表被拖入函数中,影响可读性;(2)手动编写这样的代码相当繁琐,应该将其抽象出来,可以使用一个“包装函数”wrap_defaults({1, 2, 3, 4}, f),根据需要提供默认值。具体实现留给读者作为练习(提示:一种直接的方法是先将参数收集到一个垃圾表中,然后在设置默认值后解包)。

表调用

Lua提供了一种语法糖,用于以单个表作为唯一参数调用函数:f{...}等同于f({...})。此外,{f(...)}可以用于捕获由f返回的可变参数(注意:如果f返回nil,则表的列表部分会有空洞)。

表还允许将“命名参数”实现为表字段:表允许混合使用列表和哈希部分,因此f{1, named_arg = 2}在Lua中是完全有效的。

在限制方面,表调用的优点是它只在堆栈上留下一个参数 - 表,而不是多个参数。对于递归函数来说,这使得堆栈溢出的问题可以稍后解决。由于PUC Lua大大增加了堆栈限制到约1M,这已经不再是一个问题;然而,LuaJIT的堆栈限制仍然是约65k,而PUC Lua 5.1甚至更低,约为15k。
就性能和内存消耗而言,表调用显然更差:它需要Lua构建一个垃圾表,直到垃圾回收器将其清除之前,这将浪费内存。因此,在频繁调用的热点中可能不应该使用垃圾参数表。与直接从堆栈获取值相比,索引哈希映射显然也更慢。
话虽如此,让我们来看一下实现表默认值的方法:
解包/解构

unpack(在后续版本中为table.unpack)可用于将表转换为变长参数,可以像参数列表一样对待;但请注意,在Lua中,列表部分不能具有尾部的nil值,因此无法区分"无值"和nil。解包/解构到局部变量还有助于性能,因为它消除了重复的表索引。

function f(params)
    local x, y, z, w = unpack(params)
    -- use same code as if x, y, z, w were regular params
end
f{1, 2, nil}

如果你使用了命名字段,你将需要显式地解构它们:
function f(params)
    local x, y, z, w = params.x, params.y, params.z, params.w
    -- use same code as if x, y, z, w were regular params
end
f{x = 1, w = 4}

可以混搭:

function f(params)
    local x, y, z = unpack(params)
    local w = params.w
    -- use same code as if x, y, z, w were regular params
end
f{1, 2, w = 4}

元表

__index 元表字段可用于设置一个表,如果 params.namenil,则使用 name 进行索引,并为 nil 值提供默认值。在传递的表上设置元表的一个主要缺点是,传递的表的元表将会丢失,这可能导致调用方端出现意外行为。你可以使用 getmetatablesetmetatable 在操作完 params 后恢复元表,但这样做会比较繁琐,所以我建议不要这样做。

不好的

function f(params)
    setmetatable(params, {__index = {x = 1, y = 2, z = 3, w = 4}})
    -- use params.[xyzw], possibly unpacking / destructuring
end
f{x = 1}

除了可能是垃圾参数表之外,每次调用该函数都会创建一个垃圾元表和一个垃圾默认表。这很糟糕。由于元表是常量,只需将其从函数中拖出来,使其成为上值即可。
好的。
local defaults_metatable = {__index = {x = 1, y = 2, z = 3, w = 4}}
function f(params)
    setmetatable(params, defaults_metatable)
    -- use params.[xyzw], possibly unpacking / destructuring
end

避免使用元表

如果您想要一个没有元表困扰的默认表格,请再次考虑编写一个辅助函数,用于填充具有默认值的表格:

local function complete(params, defaults)
    for param, default in pairs(defaults) do
        if params[param] == nil then
            params[param] = default
        end
    end
end

这将更改params表,正确设置默认值;使用方式为params = complete(params, defaults)。再次提醒,请将defaults表拖出函数。


1
我认为第二个代码块中的 z == 3 应该是 z = 3,但是Stack Overflow不允许我进行一个字符的编辑,而且我对这个主题不是很熟悉,无法以其他有意义的方式做出贡献。 - Hactar
1
@Hactar修复了,谢谢你指出来。 - Luatic

3

如果你的函数不希望参数值传入布尔类型 false 或者 nil,那么你推荐的方法是可行的:

function test1(param)
  local default = 10
  param = param or default
  return param
end

--[[
test1(): [10]
test1(nil): [10]
test1(true): [true]
test1(false): [10]
]]

如果您的函数允许布尔值false 作为参数值传递,但不允许nil,则可以按照Stuart P. Bentley的建议检查是否存在nil,只要默认值不是布尔值false
function test2(param)
  local default = 10
  param = (param == nil and default) or param
  return param
end

--[[
test2(): [10]
test2(nil): [10]
test2(true): [true]
test2(false): [false]
]]

当默认值为布尔值 false 时,上述方法会出现问题:

function test3(param)
  local default = false
  param = (param == nil and default) or param
  return param
end

--[[
test3(): [nil]
test3(nil): [nil]
test3(true): [true]
test3(false): [false]
]]

有趣的是,将条件检查的顺序颠倒确实允许布尔值 false 成为默认值,并且在名义上更具性能优势。
function test4(param)
  local default = false
  param = param or (param == nil and default)
  return param
end

--[[
test4(): [false]
test4(nil): [false]
test4(true): [true]
test4(false): [false]
]]

这种方法似乎与直觉相反,但经过进一步的研究发现它们有一定的巧妙之处。

如果您想要函数的默认参数允许传递nil值,则需要使用更加丑陋的可变参数,例如:

function test5(...)
  local argN = select('#', ...)
  local default = false
  local param = default
  if argN > 0 then
    local args = {...}
    param = args[1]
  end
  return param
end

--[[
test5(): [false]
test5(nil): [nil]
test5(true): [true]
test5(false): [false]
]]

当然,使用可变参数会完全阻碍函数参数的自动补全和linting功能。


1
简短的回答是这是最简单和最好的方法。在lua中,默认情况下变量等于nil。这意味着如果我们不向lua函数传递参数,则参数存在但为nil,lua程序员使用此lua属性来设置默认值。
此外,这不是设置默认值的一种方式,但您可以使用以下函数。
此函数将创建一个错误,如果您没有将值传递给参数。
function myFn(arg1 , arg2)
err = arg1 and arg2
if not err then error("argument") end
-- or
if not arg1 and arg2 then error("msg") end

但这不是一个好方法,最好不要使用这个功能

在图表中显示可选参数为[, arg]

function args(a1 [,a2])
-- some
end
function args ( a1 [,a2[,a3]])
-- some
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接