【问题标题】:Is there ever a reason to use goto in modern .NET code?有没有理由在现代 .NET 代码中使用 goto?
【发布时间】:2011-02-02 07:01:22
【问题描述】:

我刚刚在 .NET 基础库的反射器中找到了这段代码...

    if (this._PasswordStrengthRegularExpression != null)
    {
        this._PasswordStrengthRegularExpression = this._PasswordStrengthRegularExpression.Trim();
        if (this._PasswordStrengthRegularExpression.Length == 0)
        {
            goto Label_016C;
        }
        try
        {
            new Regex(this._PasswordStrengthRegularExpression);
            goto Label_016C;
        }
        catch (ArgumentException exception)
        {
            throw new ProviderException(exception.Message, exception);
        }
    }
    this._PasswordStrengthRegularExpression = string.Empty;
Label_016C:
    ... //Other stuff

我已经听过所有“你不应该使用 goto,因为害怕永远流放地狱”的说法。我一直非常重视 MS 编码员,虽然我可能不同意他们的所有决定,但我始终尊重他们的推理。

那么 - 我缺少这样的代码是否有充分的理由?这段代码摘录是由一个无能的开发人员拼凑而成的吗?还是 .NET 反射器返回的代码不准确?

我希望有一个很好的理由,而我只是盲目地想念它。

感谢大家的意见

【问题讨论】:

  • 我猜你是通过 Reflector 找到的。这不一定是他们编写的实际代码。
  • 我可以说,无论高低评价“微软程序员”都不是一个好主意。一家公司太大、太多样化,无法对其技能水平进行粗略分析。如果没有公平份额的坏苹果,你就无法经营这么大的组织,如果没有雇佣几个好人,你就无法拥有那么多钱。
  • @Daniel Ballinger - 如果您在短时间内发现它是会员提供商的一部分,那么您应该花 3 分钟的时间来弄清楚是哪一个 ;)
  • 有,有些人只是害怕给出例子,因为害怕被否决。 goto 应该只用于向前跳跃(这里是一个例子:stackoverflow.com/questions/863172/…),向后跳跃已经由循环提供,另一个很好的例子是 Donald Knuth 的素数生成器,我在我的 C 书中读到它(被借了,忘记归还),代码很优雅,我似乎无法在网络中找到它。
  • 我认为所有GOTO 语句都应该与描述性标签配对...例如OVER_THEREHELLJAILBED

标签: c# .net goto


【解决方案1】:

反射器并不完美。此方法的实际代码可从参考源获得。它位于 ndp\fx\src\xsp\system\web\security\admembershipprovider.cs:

        if( passwordStrengthRegularExpression != null )
        { 
            passwordStrengthRegularExpression = passwordStrengthRegularExpression.Trim();
            if( passwordStrengthRegularExpression.Length != 0 ) 
            { 
                try
                { 
                    Regex regex = new Regex( passwordStrengthRegularExpression );
                }
                catch( ArgumentException e )
                { 
                    throw new ProviderException( e.Message, e );
                } 
            } 
        }
        else 
        {
            passwordStrengthRegularExpression = string.Empty;
        }

请注意它如何未能检测到最后一个 else 子句并使用 goto 进行补偿。它几乎肯定会被 if() 语句中的 try/catch 块绊倒。

显然,您会更喜欢实际的源代码,而不是反编译的版本。 cmets 本身就很有帮助,您可以指望来源是准确的。好吧,大多数情况下是准确的,一个有缺陷的后处理工具删除了微软程序员的名字,造成了一些轻微的损害。标识符有时用破折号代替,代码重复两次。可以下载源码here

【讨论】:

  • 甜蜜 - 感谢您发布。那段代码看起来好多了:D 对不起,我怀疑你们 MS 程序员,一定是睡眠不足影响了我的判断;)
  • 不会让您失望,但 .NET 框架代码包含 goto 语句。不多,但他们总是澄清代码流。
  • 和往常一样,应该注意的是,goto 仍然占有一席之地。它们是一个有用的工具,就像任何其他工具一样......只要它没有被滥用(像许多其他工具一样),它仍然应该是一个可行的工具。
  • 还应该注意的是,所有循环和分支都是真正的 goto 并且查看 IL(特别是优化的发布代码)你无法从其他模型中区分原始。 stackoverflow.com/questions/2542289/…
  • 我喜欢这个答案实际上并没有回答主题行中的问题。
【解决方案2】:

您可以使用 GOTO 以更好的性能执行递归。维护起来要困难得多,但如果您需要这些额外的周期,您可能愿意支付维护负担。

这是一个简单的例子,结果如下:

class Program
{
    // Calculate (20!) 1 million times using both methods.
    static void Main(string[] args)
    {
        Stopwatch sw = Stopwatch.StartNew();
        Int64 result = 0;
        for (int i = 0; i < 1000000; i++)
            result += FactR(20);
        Console.WriteLine("Recursive Time: " + sw.ElapsedMilliseconds);

        sw = Stopwatch.StartNew();
        result = 0;
        for (int i = 0; i < 1000000; i++)
            result += FactG(20);
        Console.WriteLine("Goto Time: " + sw.ElapsedMilliseconds);
        Console.ReadLine();
    }

    // Recursive Factorial
    static Int64 FactR(Int64 i)
    {
        if (i <= 1)
            return 1;
        return i * FactR(i - 1);
    }

    // Recursive Factorial (using GOTO)
    static Int64 FactG(Int64 i)
    {
        Int64 result = 1;

    Loop:
        if (i <= 1)
            return result;

        result *= i;
        i--;
        goto Loop;
    }

这是我在我的机器上得到的结果:

 Recursive Time: 820
 Goto Time: 259

【讨论】:

  • 为什么不直接使用while循环?
【解决方案3】:

Goto 在编写解析器和词法分析器时经常很有用。

【讨论】:

  • 我不是专家,但我的解析器工作人员告诉我,由于各种原因,递归下降通常不是最佳解决方案。
  • @jeffamaphone - 与我在回答中描述的问题相同。手写解析器使用递归下降和优先解析的某种组合。解析器生成器自动生成状态模型,并自动生成代码来实现该模型,并且该代码可能使用也可能不使用 goto。原则是存在的——goto 可能是实现状态转换的最佳方式——但由于它很少发生在手写代码中,因此证明并不多。而且在任何情况下,许多解析器生成器都不会生成 goto,因为其他问题意味着 goto 不一定适用。
  • 嗯,你必须通过循环在StatemenList等东西中实现尾递归。但除此之外,你真的可以通过递归体面来解决问题。如果性能显着提高并且实际测量过,我只会切换到表格。递归体面的解析器更容易调试,所以我个人坚持使用它们。
  • 更正:编写词法分析器时经常使用 Goto。
  • 了解它们的用处的一种简单的方法是查看一些 ANTLR 生成的代码,看看有多少 gotos,然后尝试在没有它们的情况下重写词法分析器/解析器。
【解决方案4】:

这可能不是最好的例子,但它确实表明goto 非常方便。

private IDynamic ToExponential(Engine engine, Args args)
{
    var x = engine.Context.ThisBinding.ToNumberPrimitive().Value;

    if (double.IsNaN(x))
    {
        return new StringPrimitive("NaN");
    }

    var s = "";

    if (x < 0)
    {
        s = "-";
        x = -x;
    }

    if (double.IsPositiveInfinity(x))
    {
        return new StringPrimitive(s + "Infinity");
    }

    var f = args[0].ToNumberPrimitive().Value;
    if (f < 0D || f > 20D)
    {
        throw new Exception("RangeError");
    }

    var m = "";
    var c = "";
    var d = "";
    var e = 0D;
    var n = 0D;

    if (x == 0D)
    {
        f = 0D;
        m = m.PadLeft((int)(f + 1D), '0');
        e = 0;
    }
    else
    {
        if (!args[0].IsUndefined) // fractionDigits is supplied
        {
            var lower = (int)Math.Pow(10, f);
            var upper = (int)Math.Pow(10, f + 1D);
            var min = 0 - 0.0001;
            var max = 0 + 0.0001; 

            for (int i = lower; i < upper; i++)
            {
                for (int j = (int)f;; --j)
                {
                    var result = i * Math.Pow(10, j - f) - x;
                    if (result > min && result < max)
                    {
                        n = i;
                        e = j;
                        goto Complete;
                    }
                    if (result <= 0)
                    {
                        break;
                    }
                }

                for (int j = (int)f + 1; ; j++)
                {
                    var result = i * Math.Pow(10, j - f) - x;
                    if (result > min && result < max)
                    {
                        n = i;
                        e = j;
                        goto Complete;
                    }
                    if (result >= 0)
                    {
                        break;
                    }
                }
            }
        }
        else
        {
            var min = x - 0.0001;
            var max = x + 0.0001; 

            // Scan for f where f >= 0
            for (int i = 0;; i++)
            {
                // 10 ^ f <= n < 10 ^ (f + 1)
                var lower = (int)Math.Pow(10, i);
                var upper = (int)Math.Pow(10, i + 1D);
                for (int j = lower; j < upper; j++)
                {
                    // n is not divisible by 10
                    if (j % 10 == 0)
                    {
                        continue;
                    }

                    // n must have f + 1 digits
                    var digits = 0;
                    var state = j;
                    while (state > 0)
                    {
                        state /= 10;
                        digits++;
                    }
                    if (digits != i + 1)
                    {
                        continue;
                    }

                    // Scan for e in both directions
                    for (int k = (int)i; ; --k)
                    {
                        var result = j * Math.Pow(10, k - i);
                        if (result > min && result < max)
                        {
                            f = i;
                            n = j;
                            e = k;
                            goto Complete;
                        }
                        if (result <= i)
                        {
                            break;
                        }
                    }
                    for (int k = (int)i + 1; ; k++)
                    {
                        var result = i * Math.Pow(10, k - i);
                        if (result > min && result < max)
                        {
                            f = i;
                            n = j;
                            e = k;
                            goto Complete;
                        }
                        if (result >= i)
                        {
                            break;
                        }
                    }
                }
            }
        }

    Complete:

        m = n.ToString("G");
    }

    if (f != 0D)
    {
        m = m[0] + "." + m.Substring(1);
    }

    if (e == 0D)
    {
        c = "+";
        d = "0";
    }
    else
    {
        if (e > 0D)
        {
            c = "+";
        }
        else
        {
            c = "-";
            e = -e;
        }
        d = e.ToString("G");
    }

    m = m + "e" + c + d;
    return new StringPrimitive(s + m);
}

【讨论】:

    【解决方案5】:

    .NET(特别是 C#)中 goto 有多种有效用途:

    模拟 Switch 语句贯穿语义.

    那些来自 C++ 背景的人习惯于编写 switch 语句,除非用 break 显式终止,否则这些语句会自动从 case 到 case。对于 C#,只有琐碎(空)的情况会失败。

    例如,在 C++ 中

    int i = 1;
    switch (i)
    {
    case 1:
      printf ("Case 1\r\n");
    case 2:
      printf ("Case 2\r\n");
    default:
      printf ("Default Case\r\n");
      break;
    }
    

    在此 C++ 代码中,输出为:

    情况1 案例2 默认情况

    下面是类似的 C# 代码:

    int i = 1;
    switch (i)
    {
    case 1:
      Console.Writeline ("Case 1");
    case 2:
      Console.Writeline ("Case 2");
    default:
      Console.Writeline ("Default Case");
      break;
    }
    

    正如所写,这不会编译。有几个编译错误如下所示:

    控制不能从一个案例标签(“案例 1:”)转移到另一个案例标签

    添加 goto 语句使其工作:

    int i = 1;
    switch (i)
    {
    case 1:
        Console.WriteLine ("Case 1");
        goto case 2;
    case 2:
        Console.WriteLine("Case 2");
        goto default;
    default:
        Console.WriteLine("Default Case");
        break;
    }
    

    ...在 C# 中另一个有用的 goto 用法是...

    无限循环和展开递归

    我不会在这里详细介绍,因为它不太有用,但有时我们会使用 while(true) 构造编写无限循环,这些构造以 break 显式终止或使用 continue 语句重新执行。当我们尝试模拟递归方法调用但无法控制递归的潜在范围时,可能会发生这种情况。

    您显然可以将其重构为 while(true) 循环或将其重构为单独的方法,但也可以使用标签和 goto 语句。

    goto 的这种用法更值得商榷,但在极少数情况下仍然值得牢记在心。

    【讨论】:

      【解决方案6】:

      除了所有这些不错的有效东西之外,当您查看反汇编代码时,请记住开发人员可能在这些程序集上使用了混淆器。一种混淆技术是在 IL 中添加随机 goto

      【讨论】:

        【解决方案7】:

        正如其他人所展示的,您在反射器中看到的代码必然是在框架中编写的代码。编译器和优化器可以将代码更改为以类似方式运行的东西,只要它不改变代码所做的实际工作。还应该说明的是,编译器将所有分支和循环实现为 goto(IL 中的分支,或汇编中的跳转)。当运行发布模式并且编译器尝试将代码优化为与您的功能相同的最简单形式时来源。

        我有一个关于不同循环技术的示例,当您为发布进行编译时,这些技术都被编译为 100% 相同的 IL。 See Other Answer

        (我现在找不到它,但 Eric Lippert 发布了一篇关于 C# 编译器如何处理代码的注释。他提出的要点之一是如何将所有循环更改为 goto。)

        话虽如此,我对 goto 没有任何问题。如果有更好的循环结构,请使用它。但有时你需要一些东西,然后你可以挤出一些东西,foreach,while,do/while,但你不希望方法调用带来的额外混乱和痛苦(为什么要浪费 5 多行来将嵌套的 for 转换为递归方法。)

        【讨论】:

          【解决方案8】:

          我并不喜欢 goto,但说它们永远无效是愚蠢的。

          我曾经用过一个来修复一段特别混乱的代码中的缺陷。考虑到时间限制,重构代码并进行测试是不切实际的。

          此外,我们是不是都看到了条件构造,它们的编码非常糟糕,以至于让 goto 看起来是良性的?

          【讨论】:

            【解决方案9】:

            关于这一点:

            那么 - 有没有很好的理由编写代码 像我这样失踪?这是 代码提取只是由一个放在一起 烂开发商?或者是 .NET 反射器 返回不准确的代码?

            我不同意只有这三种可能性的前提。

            也许正如许多其他人所建议的那样,这根本不是库中真实源代码的准确反映。无论如何,我们都犯过(好吧,无论如何,有)为了以下目的编写代码“肮脏的方式”:

            1. 快速实现功能
            2. 快速修复错误
            3. 挤出轻微的性能提升(有时有理由,有时没有那么多)
            4. 其他一些当时有意义的原因

            这并不会使某人成为“糟糕的开发人员”。大多数诸如“你不能使用 goto”之类的准则主要是为了保护开发人员免受他们自己的伤害;它们不应被视为区分好开发人员和坏开发人员的关键。

            打个比方,考虑一下我们许多人在小学英语中学到的简单规则:永远不要用介词结束句子。这不是真正的规则;它是帮助防止人们说出诸如“汽车在哪里?”之类的指导方针。了解这个事实很重要;一旦您开始将其视为实际规则而不是指南,您会发现自己“纠正”人们的完美句子,例如“你害怕什么?”

            考虑到这一点,我会警惕任何因为使用goto而称另一位开发人员“垃圾”的开发人员。

            我当然不是要为 goto 本身辩护——只是争辩说,它的使用并不表示无论如何都无能。

            【讨论】:

              【解决方案10】:

              我不喜欢那个代码。

              我更喜欢将 Regex 存储在成员中,并在设置时对其进行验证,从而避免在读取时对逻辑的所有需求。

              【讨论】:

                【解决方案11】:

                看一下状态图。如果您认为最好使用的代码结构是最直接、最清楚地表达您的意图的代码结构,那么这些状态转换中的每一个都应该被编码为 goto。

                不过,这在现实世界中往往会失败。第一个问题是我们经常需要停止机器,退出到其他代码,然后再恢复机器——这意味着这些转换中的每一个都倾向于改变状态变量,用于识别开关中的正确状态/案例陈述。这实际上只是一种隐藏和延迟 goto 的方法——写入状态变量与写入程序计数器寄存器没有太大区别,真的。这只是实现“转到那里 - 但不是现在,以后”的一种方式。

                不过,在某些情况下,goto 可以很好地表达某种状态模型中正在发生的事情 - 我猜想一个例子就是医生有时使用的诊断流程图之一。如果您在不使用 goto 进行转换的情况下将其中一个实现为程序,那么您实际上只是通过加密代码的意图来让自己的生活变得困难。

                只是到目前为止,最常见的情况不太可能是手写代码。我编写了代码生成器,可以为各种状态模型(决策处理、常规语法解析等)中的转换生成 goto 语句,但我不记得上次在手写代码中使用 goto 是什么时候了。

                【讨论】:

                • 我有时认为状态变量不应该被视为数字持有者,而应该被视为不同版本代码之间的选择器。例如,一段代码有两个标志,这两个标志都可以独立地为真或假,相当于四段代码,其中两个标志都为假,一个第一个为真,一个(仅)第二个是真的,一个两者都是真的。对标志的条件测试变为“如果为假”或“如果为真”,而在一个版本的代码中写入标志是“转到”到另一个版本的相应位置。
                • 虽然这种方法可能看起来很极端,但它只是明确了标志所代表的实际状态转换。考虑一下,如果愿意,可以使用状态变量来编写只有一个“while”循环(围绕所有内容)和只有简单的“if”语句的程序,但这几乎不是表达事物的最清晰方式。许多程序员使用状态变量来“避免 goto”,但实际上他们所做的只是让状态转换更难于看到和理解。
                【解决方案12】:

                goto 至少对于 C 等语言中的清理工作是完全有效的,它在某种程度上模拟了异常的概念。我确信 .NET 有更好的方法来处理这样的事情,所以 goto 已经过时并且容易出错。

                【讨论】:

                • .Net 确实有更好的方法——它们被称为“方法”。 :)
                • @MusiGenesis:goto 和方法调用甚至不一样。尝试编写一个 Duff 的设备,嵌套循环中断/继续,快速重试而不使用 goto。您最终会得到一堆方法或嵌套的 if 和循环。我会自己去干净的goto。
                • @Matthew:简单。带有中断/继续的嵌套循环可以通过基本分解来实现——即将它们放入一个额外的方法中。快速重试也是如此。至于 Duff 的设备——你想在 C# 中使用它?!我宁愿让 JITter 自动展开我的循环或使用块复制算法。
                • @Konrad:我没有说它们不能变成方法。我只是说他们不一样。您可以在 C# 中编写非常高效的代码(您可能需要查看 Micro .Net 框架。)
                • @Matthew:你说,“尝试编写 Duff 的设备,嵌套循环中断/继续,不使用 goto 快速重试”——我向你展示了它是如何工作的。我对 .NET 的性能没有异议。我只是质疑试图智取 JITter 的有用性。
                【解决方案13】:

                当我写 FORTRAN 时,我什至从未使用 GO TO back 进行编码。

                我从来没有使用过它。我不明白为什么任何现代语言都会要求用户这样做。我会毫不含糊地说“不”。

                【讨论】:

                  【解决方案14】:

                  我见过 goto 用来跳出嵌套循环:

                  How can I break out of two nested for loops in Objective-C?

                  我认为以这种方式使用它没有任何问题。

                  【讨论】:

                  • 你不能用break [n]吗?
                  • @Atomiton: break 只会跳出内部循环。对于嵌套循环,goto 是一种一次性跳出所有循环的方法。
                  • 是的,我的意思是 break 2;break(2) 应该跳出循环及其父项...我认为这是 C# 功能,但在寻找它时,显然它不是吨。我发誓我在什么地方见过它,不过……用某种语言。
                  • @Atomiton:Java 标记了中断。恕我直言,C# 也应该添加它们。我几乎从不需要它们,但是当我这样做时,没有它们编写代码会很痛苦。
                  • 我只能想象随着时间的推移break n 引入了哪些错误,因为代码被维护、可能被复制和重新利用,并且新程序员没有正确更新n
                  【解决方案15】:

                  不,没有充分的理由使用goto。我上一次编写 goto 语句是在 1981 年,从那以后我就再也没有错过这个特定的构造。

                  【讨论】:

                    【解决方案16】:

                    我在很多很多行的 .NET 代码中都没有看到 Goto 的有效案例,无论是编写还是审查。

                    在不支持使用 finally 块进行结构化异常处理的语言中(PASCAL - 结构化编程语言的祖父,以及经典的 C 语言),使用 GOTO 的战术性使用可能会导致更容易理解代码:用于在嵌套循环内终止时执行清理(与正确设置多个循环终止条件相反)。即使在过去,我也没有出于这个原因亲自使用 goto(可能是因为害怕“永远流放地狱”)。

                    【讨论】:

                    • 老实说,在编写 .NET 代码的 10 年中,我从未在 .NET 应用程序中使用过 goto。就好像我的大脑没有计算出它的存在。
                    • 如果我曾经在实际程序中使用 goto,我会确保添加一条关于原谅我的罪过的评论。 :)
                    【解决方案17】:

                    它可能不在源代码中,这就是反汇编代码的样子。

                    【讨论】:

                    • 但是您是否还有一个标有“_PasswordStrengthRegularExpression”的变量而不是一些随机字符集?
                    • 约翰:是的,你会的。这是一个成员字段(注意this. 前缀),而不是局部变量。成员字段名称保留在编译后的代码中(除非经过混淆,Microsoft 没有对 .NET 框架库执行此操作)。
                    • @John - 好吧,Reflector 可以获取原始成员名称,但不能总是对编译器优化进行逆向工程。这就是我的经验。
                    • 这是有道理的。谢谢
                    • +1 指出它可能实际上不在代码中。
                    【解决方案18】:

                    有一个有效的情况 - 当您尝试模拟递归过程调用并以非递归代码返回时,或者执行类似的操作(这种要求也出现在 Prolog 解释器中)。但总的来说,除非您正在做一些需要微优化的事情,例如国际象棋程序或语言解释器,否则最好只使用常规过程堆栈并使用函数/过程调用。

                    【讨论】:

                    • 我完全同意......这就是我在回答中谈到的潜在无限(或非常大)重复代码块,其中递归是不可取的。当然,这可以重构为多种方法。给猫剥皮的方法有很多种。或者编写一个循环。 :)
                    【解决方案19】:

                    你不应该看反射器代码。

                    虽然如果你曾经查看过反汇编的 IL,你会看到到处都是 goto。本质上,我们使用的所有循环和其他控制结构无论如何都会转换为 goto,只是通过将它们转换为 我们的 代码中的结构,它变得更具可读性和更易于维护。

                    顺便说一句,我认为您发布的代码不是使用 goto 的好地方,而且我很难想出一个。

                    【讨论】:

                    • 我同意,我认为这是编译器对原始源代码的更改。
                    • 有时找出如何利用 .NET 框架的最快途径是通过反射器。我知道使用反射器时存在缺陷和不准确之处 - 但有时我查看代码并想“呃,那个开发人员在想什么?”
                    • @Ben,我只是在玩“你不能使用 goto”。 ;) 无论如何,在剖析和学习时使用任何必要的手段。
                    • @John - Reflector 可以获取变量/成员名称,但它不能总是从编译器进行的优化向后工作,以使反汇编代码看起来像源代码。
                    • Ben - Microsoft 提供了 .NET 框架库的实际源代码。所以你实际上可以把它拉下来,看看他们是否真的使用了 goto,或者这是否只是 Reflector 无法从 IL 中找出编译成 IL 跳转的原始源代码构造。
                    猜你喜欢
                    • 1970-01-01
                    • 2011-01-18
                    • 1970-01-01
                    • 2016-11-04
                    • 1970-01-01
                    • 2012-10-31
                    • 2011-11-25
                    • 2021-09-13
                    相关资源
                    最近更新 更多