Switch 语句 |
|
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()
函数并调用 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)
此版本使用函数 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)
这是另一个“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 })
另一个更“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
这个与上面的语法完全相同,但写得更简洁,并且区分了默认情况和包含“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
来比较函数,这会导致问题。如果真的发生了这种情况,解决方案是使用另一个表定义Nil
:setmetatable({}, { __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, }
为了更多“愚蠢的 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) )
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
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!'
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 带有一个扩展,它执行结构化模式匹配,其中 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]
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了"