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
語法的簡化版本。這是 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()
函式,並呼叫表格中每一個案例的元素來做到的簡潔版本。這個方法很接近 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」語句的實作。這個實作是基於路易斯·亨里克·菲格雷多 1998 年 12 月 8 日的清單訊息中提供的 switch 語句,但物件/方法關係已翻轉,以在實際使用中達成較為傳統的語法。Nil 個案變數也會處理 - 有個選用條款特別為它們而設(我想要的東西),或它們可以回退到預設條款。(容易變更)個案語句函數的回傳值也得到支援。
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」switch 語句的另一種實作。基於 Dave 上方的程式碼。個案語句函數的回傳值也得到支援。
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
Null
是空函數,因為它會產生一個唯一值,並且滿足 return
語句呼叫中的[冪零]
要求,同時仍然具有 true
的值以允許它用於and or
三元表達式。但在 Lua 5.2 中,如果函數存在,函數可能不會建立新值,這會造成問題,如果你不知何故最終使用 switch
來比較函數。若真的發生這種情況,一個解決方案是以另一個表格定義 Null
:setmetatable({}, { __call = function () end })
。可透過加入 if type(item) == "string" then item = string.lower(item) end
來製作一個不分大小寫的變異,前提是表格的所有金鑰都是以相同方式處理。範圍可以由一個 __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
敘述而言,語法上稍微簡潔一點,因此效能相同。
可以使用代碼濾鏡巨集來實作(請參閱 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
目前沒有針對正規運算式的字串比對進行特別處理,不過可以透過防護措施來解決。可以很輕鬆地新增適當的支援,而且很可能包括在未來的版本中。
相關資源
* 實作模式比對擴充功能分步教學[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 敘述?任何可以達成這件事的人,都會學到一開始為什麼沒有必要這麼做。--找麻煩的人
如何避免:a)線性搜尋選項、b)每次使用案例時避免產生垃圾,以及 c)因為 if-elseif-else 解決方案很醜。條件重複 N 次,這會模糊且複雜化程式碼。數字上的簡單切換可以快速跳到要執行的程式碼,而且不需要像以下程式碼一樣每次都產生閉包或表格。
實際上,我從未將此程式碼用作 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 陳述式。當你想要使用穿透式時,真正的問題才會開始。我目前正在使用一個資料庫,而且我需要能夠更新它。由於它需要一次更新一個步驟,因此你會跳到更新為下一個版本的位址,然後穿透式地進行,直到你到達最新的版本。不過,這樣一來你需要使用運算式 goto 或 switch 陳述式。但 Lua 兩者都沒有。--Xandaros
"@Xandrous 現在有一個 goto"