【问题标题】:Why does Lua have no "continue" statement?为什么 Lua 没有“继续”语句?
【发布时间】:2024-04-13 20:05:01
【问题描述】:

在过去的几个月里,我一直在与 Lua 打交道,我真的很喜欢其中的大部分功能,但我仍然缺少其中的一些东西:

  • 为什么没有continue
  • 有哪些解决方法?

【问题讨论】:

  • 自从提出这个问题后,Lua 得到了一个goto 语句,可以用来实现 continue。请参阅下面的答案。

标签: loops lua language-design


【解决方案1】:

在 Lua 5.2 中,最好的解决方法是使用 goto:

-- prints odd numbers in [|1,10|]
for i=1,10 do
  if i % 2 == 0 then goto continue end
  print(i)
  ::continue::
end

LuaJIT 从 2.0.1 版本开始支持此功能

【讨论】:

  • 我希望他们有一天能包含一个实际的continuegoto 替换看起来不太好,需要更多行。此外,如果您在一个函数中有多个循环执行此操作,并且都使用::continue::,这不会造成麻烦吗?为每个循环起一个名字听起来不是一件体面的事情。
  • goto 赋予语言表现力,因为continue 继续在哪里?取而代之的是 continue 2continue 3 在嵌套循环时,命名为 goto 使代码更清晰,甚至更强大,为编码人员提供了选择。也许付出的代价是多写一行代码,但它仍然实现了一个真正的“一条路”,而不是一些现在实现一件事的语言,因为它不是那么奇怪,需要实现 N 种方法来做同样的事情以晦涩难懂的方式。
  • 这个论点可以扩展到break,甚至returnbreak到哪里?和return 去哪里?此外,break 功能也可以通过goto 来实现,创造的不仅仅是“一条路”。但是 Lua 有这两个关键字,而且非常有用。
【解决方案2】:

语言管理词法范围的方式会导致同时包含 gotocontinue 的问题。例如,

local a=0
repeat 
    if f() then
        a=1 --change outer a
    end
    local a=f() -- inner a
until a==0 -- test inner a

循环体内local a 的声明掩盖了名为a 的外部变量,并且该局部变量的范围扩展到until 语句的条件,因此该条件正在测试最里面的a

如果存在continue,则必须在语义上对其进行限制,使其仅在条件中使用的所有变量都进入范围后才有效。这是一个难以向用户记录并在编译器中强制执行的条件。已经讨论了围绕这个问题的各种建议,包括使用repeat ... until 循环样式禁止continue 的简单答案。到目前为止,还没有一个足够引人注目的用例将它们包含在语言中。

解决方法通常是反转会导致执行continue 的条件,并在该条件下收集循环体的其余部分。所以,下面的循环

-- not valid Lua 5.1 (or 5.2)
for k,v in pairs(t) do
  if isstring(k) then continue end
  -- do something to t[k] when k is not a string
end

可以写

-- valid Lua 5.1 (or 5.2)
for k,v in pairs(t) do
  if not isstring(k) then 
    -- do something to t[k] when k is not a string
  end
end

这很清楚,除非您有一系列精心挑选的控制循环操作的剔除,否则通常不会成为负担。

【讨论】:

  • 来自 python 背景,这是一个令人困惑的答案,因为那里的每个作用域在运行之前都已经知道它的局部变量是什么。 IE。在达到until... 的情况下,我预计会出现未绑定的局部变量错误。
  • 在 Lua 5.2 中引入goto 之前,Lua 社区对此进行了很多讨论。当然,goto 也有同样的问题。他们最终决定,无论运行时和/或代码生成成本是为了保护它,都值得拥有一个灵活的goto 来模拟continue 和多级break。您必须搜索 Lua list archives 以获取相关线程以获取详细信息。既然他们确实介绍了goto,那显然不是不可逾越的。
  • 没有继续写代码没有什么“足够清楚”。将代码嵌套在应该使用 continue 的条件中是一个新手错误,并且编写这样丑陋的代码的需要不应该得到任何同情。绝对没有任何借口。
  • 这种解释毫无意义。 local 是仅编译器指令 - 在 local 和变量使用之间的运行时指令无关紧要 - 您无需更改编译器中的任何内容即可保持相同的作用域行为。是的,这可能不是那么明显,需要一些额外的文档,但是,再次重申,它需要对编译器进行零更改。我的答案中的repeat do break end until true 示例已经完全生成编译器将继续使用的相同字节码,唯一的区别是continue 你不需要丑陋的额外语法来使用它。
  • 您可以测试内部变量说明有缺陷的设计。条件在内部范围之外,它不应该访问其中的变量。考虑 C 中的等价物:do{int i=0;}while (i == 0); 失败,或者在 C++ 中:do int i=0;while (i==0); 也失败(“未在此范围内声明”)。不幸的是,现在在 Lua 中改变它为时已晚。
【解决方案3】:

您可以将循环体包裹在额外的repeat until true 中,然后在里面使用do break end 以获得继续的效果。当然,如果您还打算真正将break 跳出循环,则需要设置其他标志。

这将循环 5 次,每次打印 1、2 和 3。

for idx = 1, 5 do
    repeat
        print(1)
        print(2)
        print(3)
        do break end -- goes to next iteration of for
        print(4)
        print(5)
    until true
end

这种结构甚至可以转化为 Lua 字节码中的文字一个操作码 JMP

$ luac -l continue.lua 

main <continue.lua:0,0> (22 instructions, 88 bytes at 0x23c9530)
0+ params, 6 slots, 0 upvalues, 4 locals, 6 constants, 0 functions
    1   [1] LOADK       0 -1    ; 1
    2   [1] LOADK       1 -2    ; 3
    3   [1] LOADK       2 -1    ; 1
    4   [1] FORPREP     0 16    ; to 21
    5   [3] GETGLOBAL   4 -3    ; print
    6   [3] LOADK       5 -1    ; 1
    7   [3] CALL        4 2 1
    8   [4] GETGLOBAL   4 -3    ; print
    9   [4] LOADK       5 -4    ; 2
    10  [4] CALL        4 2 1
    11  [5] GETGLOBAL   4 -3    ; print
    12  [5] LOADK       5 -2    ; 3
    13  [5] CALL        4 2 1
    14  [6] JMP         6   ; to 21 -- Here it is! If you remove do break end from code, result will only differ by this single line.
    15  [7] GETGLOBAL   4 -3    ; print
    16  [7] LOADK       5 -5    ; 4
    17  [7] CALL        4 2 1
    18  [8] GETGLOBAL   4 -3    ; print
    19  [8] LOADK       5 -6    ; 5
    20  [8] CALL        4 2 1
    21  [1] FORLOOP     0 -17   ; to 5
    22  [10]    RETURN      0 1

【讨论】:

  • 这个答案很好,但仍然需要 3 行而不是 1 行。 (如果正确支持“继续”)它比 goto 标签更漂亮更安全,因为嵌套循环可能需要避免名称冲突。
  • 然而,它确实避免了 goto 的“真正”问题,因为您不必为每个伪继续发明一个新的标识符/标签,并且它不像代码那样容易出错随着时间的推移而修改。 我同意 continue 会很有用,但这个 IMO 是下一个最好的东西(它确实需要两行重复/直到与更正式的“继续;”.. 即便如此,如果你很关心行数,你总是可以写“重复”和“直到真正结束”,例如:gist.github.com/wilson0x4d/f8410719033d1e0ef771)
  • 很高兴看到人们真正考虑性能,甚至在 SO 上提供 luac 输出!有一个当之无愧的赞成票:)
【解决方案4】:

Straight from the designer of Lua himself:

我们对“继续”的主要关注是,还有其他几个控制结构(在我们看来)或多或少与“继续”一样重要,甚至可能取代它。 (例如,用标签中断 [如在 Java 中] 甚至更通用的 goto。)“继续”似乎并不比其他控制结构机制更特别,只是它出现在更多语言中。 (Perl 实际上有两个“继续”语句,“下一步”和“重做”。两者都很有用。)

【讨论】:

  • 我喜欢承认:在解释“我们不会这样做”之后,“两者都有用”
  • 通过在 5.2 中添加“goto”构造(撰写此答案时尚未发布)。请参阅this answer from 2012,在 5.2.0 发布后。
  • 正确 - 因为“goto”被公认为是一个不错的编程结构。 (结束讽刺)嗯嗯。
  • 但这听起来并不比“我只是忘记将continue 放入 Lua,抱歉。”更合理。
【解决方案5】:

第一部分在FAQ 中回答,正如slain 指出的那样。

至于解决方法,您可以将循环体包装在一个函数中,并从该函数中提前return,例如

-- Print the odd numbers from 1 to 99
for a = 1, 99 do
  (function()
    if a % 2 == 0 then
      return
    end
    print(a)
  end)()
end

或者,如果您想要 breakcontinue 功能,请让本地函数执行测试,例如

local a = 1
while (function()
  if a > 99 then
    return false; -- break
  end
  if a % 2 == 0 then
    return true; -- continue
  end
  print(a)
  return true; -- continue
end)() do
  a = a + 1
end

【讨论】:

  • 请不要。您在每次迭代时创建闭包环境,这是对内存和 GC 周期的巨大浪费。
  • 去检查collectgarbage("count") 即使在你简单的 100 次尝试之后,然后我们会谈谈。这种“过早”的优化让一个高负载项目免于上周每分钟重启一次。
  • @OlegV.Volkov 虽然这个例子确实给 GC 带来了相对较高的负载,但它并没有泄漏——所有的临时闭包都会被收集。我不知道你的项目,但 IME 大多数重复重启都是由于泄漏。
【解决方案6】:

我以前从未使用过 Lua,但我在 Google 上搜索了一下并想出了这个:

http://www.luafaq.org/

检查question 1.26

这是一个常见的抱怨。 Lua 的作者认为 continue 只是许多可能的新控制流机制之一(事实上它不能与 repeat/until 的范围规则一起工作,这是次要因素。)

在 Lua 5.2 中,有一个 goto 语句可以很容易地用来做同样的工作。

【讨论】:

    【解决方案7】:

    Lua 是轻量级的脚本语言,它希望尽可能小。例如,许多一元操作,如前/后增量是不可用的

    您可以使用 goto like 代替 continue

    arr = {1,2,3,45,6,7,8}
    for key,val in ipairs(arr) do
      if val > 6 then
         goto skip_to_next
      end
         # perform some calculation
      ::skip_to_next::
    end
    

    【讨论】:

    • 为了理解他人,事情需要简单易读。对于使用,每个人都有自己的方法来执行相同的操作。
    【解决方案8】:

    我们可以如下实现,它会跳过偶数

    local len = 5
    for i = 1, len do
        repeat 
            if i%2 == 0 then break end
            print(" i = "..i)
            break
        until true
    end
    

    O/P:

    i = 1
    i = 3
    i = 5
    

    【讨论】:

      【解决方案9】:

      我们多次遇到这种情况,我们只是使用一个标志来模拟继续。我们也尽量避免使用 goto 语句。

      示例:代码打算打印从 i=1 到 i=10 的语句,除了 i=3。此外,它还会打印“循环开始”、“循环结束”、“如果开始”和“如果结束”来模拟代码中存在的其他嵌套语句。

      size = 10
      for i=1, size do
          print("loop start")
          if whatever then
              print("if start")
              if (i == 3) then
                  print("i is 3")
                  --continue
              end
              print(j)
              print("if end")
          end
          print("loop end")
      end
      

      通过使用测试标志将所有剩余的语句括起来直到循环的结束范围来实现。

      size = 10
      for i=1, size do
          print("loop start")
          local continue = false;  -- initialize flag at the start of the loop
          if whatever then
              print("if start")
              if (i == 3) then
                  print("i is 3")
                  continue = true
              end
      
              if continue==false then          -- test flag
                  print(j)
                  print("if end")
              end
          end
      
          if (continue==false) then            -- test flag
              print("loop end")
          end
      end
      

      我并不是说这是最好的方法,但它对我们来说非常有效。

      【讨论】:

        【解决方案10】:

        再次使用反相,您可以简单地使用以下代码:

        for k,v in pairs(t) do
          if not isstring(k) then 
            -- do something to t[k] when k is not a string
        end
        

        【讨论】:

        • 倒置的问题是通常在一个系列中有多个条件(例如验证用户输入)。并且因为在此过程中的任何一点都可能需要短路,因此反转意味着必须连续嵌套条件(而不是“这很糟糕吗?然后逃跑;否则这很糟糕?然后逃跑”,这非常简单,你最终会得到类似“这样可以吗?那么这样可以吗?那可以吗?然后这样做”这样的代码,这是非常过分的。
        【解决方案11】:

        为什么没有继续?

        因为没有必要¹。开发人员需要它的情况很少。

        A) 当你有一个非常简单的循环时,比如 1 或 2 线,那么你可以把循环条件反过来,它仍然很可读。

        B) 当您编写简单的过程代码(也就是我们在上个世纪如何编写代码)时,您还应该应用结构化编程(也就是我们在上个世纪如何编写更好的代码)

        C) 如果你正在编写面向对象的代码,你的循环体应该包含不超过一到两个方法调用,除非它可以用一个或两个行来表示(在这种情况下,请参阅 A)

        D) 如果您正在编写函数式代码,只需为下一次迭代返回一个普通的尾调用。

        您想要使用 continue 关键字的唯一情况是,如果您想像 Python 一样编写 Lua,但事实并非如此。²

        有什么解决方法?

        除非 A) 适用,在这种情况下不需要任何变通方法,您应该进行结构化、面向对象或函数式编程。这些是 Lua 构建的范式,所以如果你竭尽全力避免它们的模式,你就会与这种语言作斗争。³


        一些澄清:

        ¹ Lua 是一种非常简约的语言。它试图拥有尽可能少的功能,而continue 语句在这个意义上并不是必不可少的功能。

        我认为Roberto Ierusalimschy2019 interview 中很好地捕捉到了这种极简主义哲学:

        添加那个,那个,那个,把那个说出来,最后我们明白最后的结论不会让大多数人满意,我们不会把每个人都想要的所有选项都放进去,所以我们什么都不放。最后,严格模式是一个合理的折衷方案。

        ² 似乎有大量程序员从其他语言转向 Lua,因为他们尝试编写脚本的任何程序都碰巧使用它,而且他们中的许多人似乎不想写任何东西他们选择的语言,这导致了许多问题,例如“为什么 Lua 没有 X 功能?”

        Matzrecent interview 中描述了与 Ruby 类似的情况:

        最受欢迎的问题是:“我来自语言 X 社区;你能不能将语言 X 的功能介绍给 Ruby?”,或类似的问题。而我通常对这些请求的回答是……“不,我不会那样做”,因为我们有不同的语言设计和不同的语言开发政策。

        ³有几种方法可以解决这个问题;一些用户建议使用goto,在大多数情况下这是一个足够好的近似值,但很快就会变得非常难看,并且会因嵌套循环而完全中断。使用gotos 还会让您面临在向其他人展示您的代码时收到一份 SICP 副本的危险。

        【讨论】:

        • 我投了反对票,因为第一句话显然是错误的,其余的答案没有帮助。
        • 没用?可能是;这是一个基于意见的答案。不过,第一句话显然是正确的; continue 可能是一个方便的功能,但这并不是必需。很多人在没有它的情况下使用 Lua 就很好,所以除了一个对任何编程语言都不是必需的简洁功能之外,真的没有其他任何理由。
        • 这不是争论:当人们别无选择时,你不能说人们“没有它就很好”。
        • 我想我们只是对“必要”有不同的定义。
        • 重复/直到也没有必要。 for 循环、字符串连接运算符、命名函数语法和无数其他语法都不是。从上述意义上考虑该功能是否“不必要”完全是题外话。