31

我刚刚在 .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 反射器返回不准确的代码?

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

感谢大家的投入

4

19 回答 19

44

反光板并不完美。此方法的实际代码可从参考源获得。它位于 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 块绊倒。

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

于 2010-03-30T02:09:29.203 回答
12

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

于 2010-03-30T01:37:10.957 回答
12

我见过 goto 用来打破嵌套循环:

如何在 Objective-C 中跳出两个嵌套的 for 循环?

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

于 2010-03-30T01:48:27.317 回答
11

goto 在 .NET 中有几个有效的用途(特别是 C#):

模拟 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)用 a 显式终止或用语句break重新执行的构造来编写无限循环。continue当我们试图模拟递归方法调用但对递归的潜在范围没有任何控制时,可能会发生这种情况。

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

goto 的这种用法更值得商榷,但在极少数情况下,作为一个选项仍然值得牢记。

于 2010-07-02T02:20:34.337 回答
8

我对 goto 并不疯狂,但说它们永远无效是愚蠢的。

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

此外,我们不是都看到过编码很差的条件构造,以至于它们使 goto 看起来是良性的吗?

于 2010-03-30T05:22:42.667 回答
5

您可以使用 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
于 2010-07-02T17:28:37.493 回答
4

你不应该看反射器代码。

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

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

于 2010-03-30T01:34:48.153 回答
3

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

于 2010-03-30T02:04:00.367 回答
3

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

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

于 2010-03-30T01:32:54.270 回答
2

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

于 2010-03-30T01:47:14.057 回答
2

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

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

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

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

于 2010-03-30T01:55:20.610 回答
2

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

于 2010-03-30T01:50:16.113 回答
1

关于这一点:

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

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

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

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

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

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

考虑到这一点,我会警惕任何因为使用goto.

我当然不是想为自己辩护goto——只是争辩说,它的使用并不表示无能,无论如何。

于 2010-03-30T04:51:35.137 回答
1

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

我有一个关于不同循环技术的示例,当您为发布进行编译时,这些技术都被编译为 100% 相同的 IL。 查看其他答案

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

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

于 2010-03-30T11:34:38.740 回答
0

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

于 2010-03-30T02:02:40.953 回答
0

这可能不是最好的例子,但它确实展示了一个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);
}
于 2010-07-02T03:05:30.930 回答
0

我不喜欢那个代码。

我更愿意将正则表达式存储在成员中,并在设置时对其进行验证,从而避免在阅读时对逻辑的所有需求。

于 2010-03-30T02:12:04.097 回答
0

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

于 2010-03-30T01:36:50.453 回答
-1

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

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

于 2010-03-30T02:01:51.057 回答