Switch 语句

lua-users home
wiki

问题

Lua 缺少 C 风格的 switch[1] 语句。这个问题在邮件列表中出现过很多次。这里讨论了一些模拟相同效果的方法。

第一个要问的问题是,为什么我们可能想要一个 switch 语句而不是像这样的比较链

local is_canadian = false
function sayit(letters)
  for _,v in ipairs(letters) do
    if     v == "a" then print("aah")
    elseif v == "b" then print("bee")
    elseif v == "c" then print("see")
    elseif v == "d" then print("dee")
    elseif v == "e" then print("eee")
    elseif v == "f" then print("eff")
    elseif v == "g" then print("gee")
    elseif v == "h" then print("aych")
    elseif v == "i" then print("eye")
    elseif v == "j" then print("jay")
    elseif v == "k" then print("kay")
    elseif v == "l" then print("el")
    elseif v == "m" then print("em")
    elseif v == "n" then print("en")
    elseif v == "o" then print("ooh")
    elseif v == "p" then print("pee")
    elseif v == "q" then print("queue")
    elseif v == "r" then print("arr")
    elseif v == "s" then print("ess")
    elseif v == "t" then print("tee")
    elseif v == "u" then print("you")
    elseif v == "v" then print("vee")
    elseif v == "w" then print("doubleyou")
    elseif v == "x" then print("ex")
    elseif v == "y" then print("why")
    elseif v == "z" then print(is_canadian and "zed" or "zee")
    elseif v == "?" then print(is_canadian and "eh" or "")
    else                 print("blah")
    end
  end
end
sayit{'h','e','l','l','o','?'}

当有许多这样的测试时,比较链并不总是最有效的。如果 letters 中的元素数量为 M,测试数量为 N,则复杂度为 O(M*N),或者可能是二次的。一个不太重要的考虑是每个测试都包含 "v ==" 的语法冗余。这些问题(虽然可能很小)在其他地方也得到了注意([Python PEP 3103])。

如果我们将此重写为一个查找表,代码可以在线性时间内运行,O(M),并且没有冗余,因此逻辑更容易随意修改

do
  local t = {
    a = "aah",
    b = "bee",
    c = "see",
    d = "dee",
    e = "eee",
    f = "eff",
    g = "gee",
    h = "aych",
    i = "eye",
    j = "jay",
    k = "kay",
    l = "el",
    m = "em",
    n = "en",
    o = "ooh",
    p = "pee",
    q = "queue",
    r = "arr",
    s = "ess",
    t = "tee",
    u = "you",
    v = "vee",
    w = "doubleyou",
    x = "ex",
    y = "why",
    z = function() return is_canadian and "zed" or "zee" end,
    ['?'] = function() return is_canadian and "eh" or "" end
  }
  function sayit(letters)
    for _,v in ipairs(letters) do
      local s = type(t[v]) == "function" and t[v]() or t[v] or "blah"
      print(s)
    end
  end
end
sayit{'h','e','l','l','o','?'}

C 编译器可以在一定条件下通过所谓的跳转表对 switch 语句进行优化,至少在合适的条件下。[2]

请注意,表的构造放在块之外,以避免为每次使用重新创建表(表的构造会导致堆分配)。这提高了性能,但副作用是将查找表从其使用位置移得更远。我们可以通过以下微小的更改来解决这个问题

do
  local t
  function sayit(letters)
    t = t or {a = "ahh", .....}
    for _,v in ipairs(letters) do
      local s = type(t[v]) == "function" and t[v]() or t[v] or "blah"
      print(s)
    end
  end
end
sayit{'h','e','l','l','o','?'}

以上是一个实用的解决方案,它是下面更详细方法的基础。下面的一些解决方案更适合语法糖或概念验证,而不是推荐的做法。

简单的函数表

可以使用一个表来实现 switch 语句的简单版本,该表将 case 值映射到一个操作。这在 Lua 中非常有效,因为表是按键值进行哈希的,避免了重复的 if <case> then ... elseif ... end 语句。

action = {
  [1] = function (x) print(1) end,
  [2] = function (x) z = 5 end,
  ["nop"] = function (x) print(math.random()) end,
  ["my name"] = function (x) print("fred") end,
}
用法(注意,在下面的示例中,您也可以将参数传递给调用的函数):-
action[case](params)
这是上面代码的伪代码
switch (caseVariable) 
  case 1: print(1)
  case 2: z=5
  case "nop": print(math.random())
  case "my name": print("fred")
end

使用 loadstring() 调用的表元素

这是一个使用 loadstring() 函数并调用 case 表中每个元素的简洁紧凑的版本。此方法类似于 Python 的 eval() 方法,并且看起来很不错。它允许以格式化的方式添加参数。

switch = function(cases,arg)
  return assert (loadstring ('return ' .. cases[arg]))()
end

local case = 3

local result = switch({
  [0] = "0",
  [1] = "2^1+" .. case,
  [2] = "2^2+" .. case,
  [3] = "2^3+" .. case
},
case
)
print(result)

Case 方法

此版本使用函数 switch(table) 向传递给它的表添加一个方法 case(table,caseVariable)

function switch(t)
  t.case = function (self,x)
    local f=self[x] or self.default
    if f then
      if type(f)=="function" then
        f(x,self)
      else
        error("case "..tostring(x).." not a function")
      end
    end
  end
  return t
end
用法
a = switch {
  [1] = function (x) print(x,10) end,
  [2] = function (x) print(x,20) end,
  default = function (x) print(x,0) end,
}

a:case(2)  -- ie. call case 2 
a:case(9)

Caseof 方法表

这是另一个“switch”语句的实现。它基于 Luiz Henrique de Figueiredo 在 1998 年 12 月 8 日的邮件列表中提出的 switch 语句,但对象/方法关系已反转,以在实际使用中实现更传统的语法。空 case 变量也得到处理 - 专门为它们提供了一个可选子句(我想要的东西),或者它们可以回退到默认子句。(很容易更改)还支持 case 语句函数的返回值。

function switch(c)
  local swtbl = {
    casevar = c,
    caseof = function (self, code)
      local f
      if (self.casevar) then
        f = code[self.casevar] or code.default
      else
        f = code.missing or code.default
      end
      if f then
        if type(f)=="function" then
          return f(self.casevar,self)
        else
          error("case "..tostring(self.casevar).." not a function")
        end
      end
    end
  }
  return swtbl
end
以下是示例用法
c = 1
switch(c) : caseof {
    [1]   = function (x) print(x,"one") end,
    [2]   = function (x) print(x,"two") end,
    [3]   = 12345, -- this is an invalid case stmt
  default = function (x) print(x,"default") end,
  missing = function (x) print(x,"missing") end,
}

-- also test the return value
-- sort of like the way C's ternary "?" is often used
-- but perhaps more like LISP's "cond"
--
print("expect to see 468:  ".. 123 +
  switch(2):caseof{
    [1] = function(x) return 234 end,
    [2] = function(x) return 345 end
  })

Switch 返回函数而不是表

另一个更“C-like”的 switch 语句的实现。基于上面 Dave 的代码。还支持 case 语句函数的返回值。

function switch(case)
  return function(codetable)
    local f
    f = codetable[case] or codetable.default
    if f then
      if type(f)=="function" then
        return f(case)
      else
        error("case "..tostring(case).." not a function")
      end
    end
  end
end
示例用法
for case = 1,4 do
  switch(case) {
    [1] = function() print("one") end,
    [2] = print,
    default = function(x) print("default",x) end,
  }
end
请注意,这可以工作,但每次使用 switch 时都会用函数闭包破坏 gc(就像此页面上的大多数示例一样)。尽管如此,我喜欢它的工作方式。只是不要在现实生活中使用它 ;-) --PeterPrade

Switch 返回可调用表而不是函数

这个与上面的语法完全相同,但写得更简洁,并且区分了默认情况和包含“default”一词的字符串。

Default, Nil = {}, function () end -- for uniqueness
function switch (i)
  return setmetatable({ i }, {
    __call = function (t, cases)
      local item = #t == 0 and Nil or t[1]
      return (cases[item] or cases[Default] or Nil)(item)
    end
  })
end
Nil 这里是一个空函数,因为它会生成一个唯一的值,并满足return 语句调用中的[幂零] 要求,同时仍然具有true 的值以允许它在and or 三元运算符中使用。然而,在 Lua 5.2 中,如果存在值,函数可能不会创建新值,如果您最终使用switch 来比较函数,这会导致问题。如果真的发生了这种情况,解决方案是使用另一个表定义Nilsetmetatable({}, { __call = function () end })

可以通过添加if type(item) == "string" then item = string.lower(item) end 来创建一个不区分大小写的变体,前提是表的所有键都以相同的方式完成。范围可以通过 cases 表上的 __index 函数元表来表示,但这会打破幻觉:switch (case) (setmetatable({}, { __index = rangefunc }))

示例用法

switch(case) {
  [1] = function () print"number 1!" end,
  [2] = math.sin,
  [false] = function (a) return function (b) return (a or b) and not (a and b) end end,
  Default = function (x) print"Look, Mom, I can differentiate types!" end, -- ["Default"] ;)
  [Default] = print,
  [Nil] = function () print"I must've left it in my other jeans." end,
}
但是,我无法说任何关于它的资源使用情况,尤其是与这里其他示例相比。

使用 vararg 函数构建 case 列表

为了更多“愚蠢的 Lua 技巧”,这里还有另一个实现:(编辑:默认功能必须放在 ... 参数的最后)

function switch(n, ...)
  for _,v in ipairs {...} do
    if v[1] == n or v[1] == nil then
      return v[2]()
    end
  end
end

function case(n,f)
  return {n,f}
end

function default(f)
  return {nil,f}
end
示例用法
switch( action,
  case( 1, function() print("one")     end),
  case( 2, function() print("two")     end),
  case( 3, function() print("three")   end),
  default( function() print("default") end)
  )

除了匹配值之外的 case 表达式类型

这里有一个来自 TheGreyKnight? 的例子,它可以处理表示范围、列表和默认操作的 case。它还支持不匹配 case 和穿透(即继续执行下一条语句)。检查 "-fall" 后缀的部分可能可以做得更有效率,但我认为这个版本更容易阅读。用作 case 主体的函数传递一个参数,它是 switch 表达式的最终形式(一个我多年来一直渴望在 switch 中拥有的功能)支持函数 contain(x, valueList) 和 range(x, numberPair) 仅仅测试 x 是否是表 valueList 中的值或 numberPair 的两个元素指定的闭合范围内的数字。

function switch(term, cases)
  assert(type(cases) == "table")
  local casetype, caseparm, casebody
  for i,case in ipairs(cases) do
    assert(type(case) == "table" and count(case) == 3)
    casetype,caseparm,casebody = case[1],case[2],case[3]
    assert(type(casetype) == "string" and type(casebody) == "function")
    if
        (casetype == "default")
      or  ((casetype == "eq" or casetype=="") and caseparm == term)
      or  ((casetype == "!eq" or casetype=="!") and not caseparm == term)
      or  (casetype == "in" and contain(term, caseparm))
      or  (casetype == "!in" and not contain(term, caseparm))
      or  (casetype == "range" and range(term, caseparm))
      or  (casetype == "!range" and not range(term, caseparm))
    then
      return casebody(term)
    else if
        (casetype == "default-fall")
      or  ((casetype == "eq-fall" or casetype == "fall") and caseparm == term)
      or  ((casetype == "!eq-fall" or casetype == "!-fall") and not caseparm == term)
      or  (casetype == "in-fall" and contain(term, caseparm))
      or  (casetype == "!in-fall" and not contain(term, caseparm))
      or  (casetype == "range-fall" and range(term, caseparm))
      or  (casetype == "!range-fall" and not range(term, caseparm))
    then
      casebody(term)
    end end
  end
end
示例用法
switch( string.lower(slotname), {
  {"", "sk", function(_)
    PLAYER.sk = PLAYER.sk+1
  end },
  {"in", {"str","int","agl","cha","lck","con","mhp","mpp"}, function(_)
    PLAYER.st[_] = PLAYER.st[_]+1
  end },
  {"default", "", function(_)end} --ie, do nothing
})

另一种形式

function switch (self, value, tbl, default)
    local f = tbl[value] or default
    assert(f~=nil)
    if type(f) ~= "function" then f = tbl[f] end
    assert(f~=nil and type(f) == "function")
    return f(self,value)
end
它避免了重复函数,因为如果 tbl 的条目是字符串/数字,它会将此值作为要查找的 case。我称之为 *多个 case 语句*。示例用法
local tbl = {hello = function(name,value) print(value .. " " .. name .. "!") end,
bonjour = "hello", ["Guten Tag"] = "hello"}

switch("Steven","hello",tbl,nil) -- prints 'hello Steven!'
switch("Jean","bonjour",tbl,nil) -- prints 'bonjour Jean!'
switch("Mark","gracias",tbl,function(name,val) print("sorry " .. name .. "!") end) -- prints 'sorry Mark!'

使用 Token Filter Macros 实现的 Case 语句

我的感觉是 switch 是错误的模型,但我们应该将 Pascal 的 case 语句视为更合适的灵感来源。以下是一些可能的形式

    case (k)
      is 10,11: return 1
      is 12: return 2
      is 13 .. 16: return 3	  
      else return 4
    endcase
    ......
    case(s)
        matches '^hell': return 5
        matches '(%d+)%s+(%d+)',result:
            return tonumber(result[1])+tonumber(result[2])
        else return 0
    endcase

您可以在 is 后提供多个值,甚至提供一个值的范围。matches 是特定于字符串的,可以接受一个额外的参数,该参数将填充结果捕获。

这个 case 语句是对 elseif 语句链的语法糖,所以它的效率是一样的。

这可以使用 token filter macros 实现(参见 LuaMacros;源代码包含一个示例实现),因此人们可以直观地了解其在实践中的使用。不幸的是,有一个陷阱;如果 .. 周围没有空格,Lua 会抱怨格式错误的数字。此外,result 必须是全局的。

Metalua 的模式匹配

MetaLua 带有一个扩展,它执行结构化模式匹配,其中 switch/case 只是一个特例。上面的例子将读取

    -{ extension 'match' } -- load the syntax extension module
    match k with
    | 10 | 11              -> return 1
    | 12                   -> return 2
    | i if 13<=i and i<=16 -> return i-10 -- was 3 originally, but it'd a shame not to use bindings!
    | _                    -> return 4
    end

目前没有针对正则表达式字符串匹配的特殊处理,尽管可以通过 guard 来解决。适当的支持可以很容易地添加,并且可能会在将来的版本中包含。

相关资源

* 关于实现模式匹配扩展的分步教程[3],以及相应的源代码[4]

* 最新的优化实现[5]

面向对象方法

您可以在 SwitchObject 中找到完整的代码。

local fn = function(a, b) print(tostring(a) .. ' in ' .. tostring(b)) end
local casefn = function(a)
	if type(a) == 'number' then
		return (a > 10)
	end
end
local s = switch()
s:case(0, fn)
s:case({1,2,3,4}, fn)
s:case(casefn, fn)
s:case({'banana', 'kiwi', 'coconut'}, fn)
s:default(fn)

s:test('kiwi') -- this does the actual job

评论

我不明白。我是一个硬核的 C/C++ 程序员,但我从未渴望在 Lua 中使用 `switch`。为什么不把它发挥到极致,让 Lua 解析一个真正的 C switch 语句?任何能做到这一点的人都会明白为什么它在 Lua 中没有必要。--Troublemaker

如何避免:a) 对选项进行线性搜索,b) 避免每次使用 case 时都创建垃圾,以及 c) 因为 if-elseif-else 解决方案很丑陋。条件被重复 N 次,这会使代码变得模糊和复杂。一个简单的数字 switch 可以快速跳转到要执行的代码,并且不必像下面的代码那样每次都生成闭包或表。

实际上,我从未将这段代码用作 `switch` 语句。我认为它为 Lua 的特性提供了一个很好的示例代码,我可能有一天会使用它,但我从未使用过!:-) 我认为这是因为你可以使用关联数组/表来映射值,所以你可以设计出不需要 switch 语句的方法。当我想使用 switch 时,通常是在根据值类型进行切换时。--Alsopuzzled

我从未在 Lua 中真正需要过 `switch`,但这些例子让我绞尽脑汁试图理解它们为什么有效。Lua 的灵活性让我惊叹不已。我现在离 ZenOfLua 更近了一步。--Initiate

这些实现的问题是,它们要么无法访问局部变量,要么不仅创建一个闭包,而是为每个 switch 分支创建一个闭包,再加上一个表。因此,它们并不是 `switch` 语句的理想替代品。话虽如此,我还没有因为 Lua 中缺少 switch 语句而感到太痛苦。--MarkHamburg

查找表示例是一个完全实用且可读的解决方案,它完全没有这些问题。在页面中甚至提到了,超出该点的示例大多是非实用的思想实验。--Colin Hunt

我正在使用 LUA 为我创建的 Web 服务器模块编写脚本。它非常快(比我之前的 PHP 版本快得多)。在这里,switch 语句将非常适合处理所有 GET["subfunction"] 可能性。唯一的原因是,唯一可行的替代方案(if-elseif)很丑陋。其他替代方案如前所述非常漂亮,打开了新世界的大门,但却是对资源的极大浪费。--Scippie

编辑:也许我对“对资源的极大浪费”判断错了。这就是脚本的意义所在,语言的设计就是为了以这种方式处理。--Scippie

如果你可以用表格映射或使用elseif,你就不需要switch语句。真正的问题是当你想要使用fallthrough时。我目前正在使用一个数据库,我需要能够更新它。由于它需要一步一步地更新,你需要跳转到更新到下一个版本的代码块,然后一直执行到最新的版本。然而,为此,你需要使用计算goto或switch语句。而Lua两者都没有。--Xandaros

"@Xandrous 现在有goto了"


以上代码来自lua-l或由Lua用户捐赠。感谢LHF、DaveBollinger?EricTetzPeterPrade.
最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于2016年12月20日上午10:50 GMT (差异)