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 語法的簡化版本。這是 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() 函式,並呼叫表格中每一個案例的元素來做到的簡潔版本。這個方法很接近 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)

Caseof 方法表格

這是另一個「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
  })

Switch 回傳函數而不是表格

更「類似 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
注意這個方法有效,但每次使用 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
此處的Null 是空函數,因為它會產生一個唯一值,並且滿足 return 語句呼叫中的[冪零]要求,同時仍然具有 true 的值以允許它用於and or 三元表達式。但在 Lua 5.2 中,如果函數存在,函數可能不會建立新值,這會造成問題,如果你不知何故最終使用 switch 來比較函數。若真的發生這種情況,一個解決方案是以另一個表格定義 Nullsetmetatable({}, { __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)
  )

除了比對值之外,其他個案表達式型別

以下是來自 TheGreyKnight?,可以處理範圍、串列及預設動作等狀況。同時也支援不符合狀況和直通(例如,繼續下一個敘述)。檢查「-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!'

使用代碼濾鏡巨集實作的 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 敘述而言,語法上稍微簡潔一點,因此效能相同。

可以使用代碼濾鏡巨集來實作(請參閱 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

目前沒有針對正規運算式的字串比對進行特別處理,不過可以透過防護措施來解決。可以很輕鬆地新增適當的支援,而且很可能包括在未來的版本中。

相關資源

* 實作模式比對擴充功能分步教學[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 敘述?任何可以達成這件事的人,都會學到一開始為什麼沒有必要這麼做。--找麻煩的人

如何避免: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"


上述的程式碼是由 lua-l 取得,或由 Lua 使用者提供。感謝 LHF、DaveBollinger?EricTetzPeterPrade
最近變更 · 偏好設定
編輯 · 歷史記錄
最後編輯於 2016 年 12 月 20 日上午 10:50 GMT (差異)