在现代.NET代码中,是否有使用goto语句的理由?

32

我刚刚在.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,否则你将被永远流放到地狱”的说法。我一直很尊重微软的编码人员,虽然我可能不同意他们的决定,但我总是尊重他们的理由。

那么,这种代码存在什么好的理由是我忽略了的吗?这段代码是不是由一个无能的开发者拼凑起来的?还是.NET反编译器返回了不准确的代码?

我希望有个好的理由存在,而我只是盲目地错过了它。

感谢大家的意见。


26
我猜你是通过反编译软件找到这段代码的。但这并不一定是他们实际编写的代码。 - Foole
11
我可以说将“微软程序员”高看或低看都不是一个好主意。这是一家规模太大且业务领域太多元化的公司,不允许对其技能水平进行粗略分析。在这样庞大的组织中,有些人可能表现不佳,但另一方面,他们也有足够的财力来聘请一些优秀的人才。 - MaxGuernseyIII
1
@Daniel Ballinger - 如果你在那么短的时间内就发现它是成员提供程序的一部分,那么你应该只需要3分钟就能找出它是哪一个 ;) - BenAlabaster
1
有些人只是害怕举例子,因为他们担心会被踩。goto应该仅用于向前跳转(这里有一个例子:https://dev59.com/CXRA5IYBdhLWcg3wsgFP#863392),向后跳转已经由循环提供,另一个很好的例子是Donald Knuth的质数生成器,我在我的C书中读到过(借来的,忘了归还),代码优雅,但我似乎找不到它在网络上的位置。 - Michael Buen
6
我认为所有的GOTO语句都应该与描述性的标签配对,比如OVER_THERE(那边)、HELL(地狱)、JAIL(监狱)和BED(床)。 - Armstrongest
显示剩余3条评论
20个回答

45

Reflector并不完美。此方法的实际代码可从Reference Source获取,位于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块所搞砸。

显然,您会更倾向于使用实际的源代码而不是反编译版本。注释本身非常有帮助,并且您可以相信源代码的准确性。好吧,大多数情况下是准确的,但有些小损伤来自于一个有缺陷的后处理工具,该工具删除了Microsoft程序员的名称。标识符有时会被替换为破折号,并且代码会重复两次。您可以从这里下载源代码。


谢谢你的发布。那段代码看起来好多了 :D 抱歉我怀疑了你们微软程序员,可能是睡眠不足影响了我的判断力 ;) - BenAlabaster
3
抱歉让你失望,但.NET框架代码中包含有goto语句。虽然数量不多,但是它们使用的地方总是可以澄清代码流程。 - Hans Passant
一如既往,应该注意到goto语句仍然有其用武之地。它们是一种有用的工具,就像其他任何工具一样……只要不被滥用(像许多其他工具一样),它仍然是一个可行的工具。 - jrista
1
还应该注意到,所有循环和分支实际上都是goto,并且查看IL(特别是经过优化的发布代码),您无法区分原始模型和其他模型。 - Matthew Whited
1
我喜欢这个回答实际上并没有回答主题行中的问题。 - marknuzz

12

这可能不是源代码中的内容,这只是反汇编代码的显示方式。


但是你仍然会有一个名为'_PasswordStrengthRegularExpression'的变量,而不是一些随机字符集吗? - John MacIntyre
2
约翰:是的,你会。那是一个成员字段(请注意this.前缀),而不是局部变量。成员字段名称在编译代码中保留(除非被混淆,微软没有对.NET框架库进行混淆)。 - itowlson
1
@John - 嗯,Reflector可以获取原始成员名称,但并不总是能够反向工程编译器优化。这是我的经验。 - heisenberg
那很有道理。谢谢。 - John MacIntyre
+1 表示指出可能实际上没有在代码中。 - MaxGuernseyIII

12

1
你不能使用 break [n] 吗? - Armstrongest
@Atomiton:Java有标记的break。在我看来,C#也应该加上它们。我几乎从不需要它们,但是当我需要时,没有它们编码起来很麻烦。 - BlueRaja - Danny Pflughoeft
@Gary,PHP有break n;continue n; - Rob
2
我只能想象随着代码的维护、复制和重新定位,如果新程序员没有正确更新“n”,那么使用“break n”引入了什么样的错误。 - Eric J.
@EricJ. - 很好的观察。这可能就是为什么Java使用带标签的break而不是带数字的break的原因。 - ArtOfWarfare
显示剩余2条评论

11

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

模拟switch语句的“fall-through”行为

那些有C++背景的人习惯于编写自动从一个case跳转到下一个case的switch语句,除非使用break明确终止。对于C#,只有简单(空)案例才会发生“fall-through”行为。

例如,在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++ 代码的输出结果为:

Case 1
Case 2
Default Case

下面是类似的 C# 代码:

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

按照目前的写法,这段代码无法编译通过。出现了多个编译错误,其中一个错误如下:

Control cannot fall through from one case label ('case 1:') to another

添加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的方式更具争议性,但仍值得在非常罕见的情况下作为一种选择留在你的脑海中。


8

我不是特别喜欢使用goto语句,但说它们从来没有用处是愚蠢的。

有一次,我在一段非常混乱的代码中使用了goto语句来修复一个缺陷。考虑到时间限制,重构代码并测试它是不切实际的。

而且,我们不是都见过那种编码非常糟糕的条件结构吗?它们让goto语句看起来无伤大雅。


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

3
为什么不直接使用 while 循环? - siride

4

不要查看反射器代码。

虽然如果您查看反编译的IL,您会看到到处都是goto。本质上,我们使用的所有循环和其他控制结构都转换为goto,只是通过将它们转换为我们的代码中的结构,使其更易读且易于维护。

顺便说一下,我不认为您发布的代码是使用goto的好地方,并且我很难想到一个合适的地方。


我同意,我认为这是编译器从原始源代码中更改的内容。 - Daniel Ballinger
1
有时候,找出如何利用.NET框架的最快方法就是通过反编译器。我知道使用反编译器会有一些陷阱和不准确之处,但有时候我看着代码就在想:“嗯,那个开发者当时在想什么呢?” - BenAlabaster
2
@Ben,我只是在开个玩笑,说“不应使用goto语句”。;) 无论何时剖析和学习,请使用任何必要手段。 - Anthony Pegram
2
@John - Reflector 可以获取变量/成员名称,但它并不能总是从编译器进行的优化中反向操作,以使反汇编代码看起来像源代码。 - heisenberg
1
微软已经公开了.NET框架库的实际源代码。因此,您可以实际下载并查看它们是否真的使用了goto,或者这只是反编译器无法从IL中找出被编译为IL跳转的原始源代码结构。 - itowlson
显示剩余10条评论

3

在我阅读和审查的许多行.NET代码中,我没有看到过Goto的有效用例。

在不支持带有finally块的结构化异常处理的语言中(例如PASCAL-结构化编程语言的鼻祖,以及经典C),策略性地使用GOTO可能会导致更易于理解的代码,当用于在嵌套循环内部执行清理时(而不是正确设置多个循环终止条件)。即使在那个年代,我也没有因为这个原因而使用goto(可能是因为害怕"永远流放地狱")。


4
我可以诚实地说,在写.NET代码的10年里,我从未在.NET应用程序中使用过goto。就好像我的大脑不认为它存在一样。 - BenAlabaster
1
如果我在实际程序中使用goto,我会确保添加一条注释来原谅我的罪过。 :) - Maynza

3

没有好的理由使用 goto。我最后一次编写 goto 语句是在1981年,自那以后我并没有想念这种语法结构。


3

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


我不是专家,但我在工作中的解析器告诉我,由于各种原因,递归下降通常不是最佳解决方案。 - i_am_jorf
2
@jeffamaphone - 和我在答案中描述的问题一样。手写解析器使用递归下降和优先级解析的混合方式。解析器生成器会自动生成状态模型,并生成代码来实现该模型,该代码可能使用或不使用goto语句。原则上是存在的 - goto可能是实现状态转换的最佳方式 - 但由于手写代码很少甚至从不出现这种情况,所以并不能证明太多。而且无论如何,许多解析器生成器不生成goto语句,因为其他问题意味着goto语句并不一定适用。 - user180247
你需要在像StatementList等的东西中通过循环来实现尾递归。但是,否则你可以真正通过递归下降来完成。只有在有显著性能提升并且已经进行了实际测量时,我才会切换到表格。递归下降解析器更容易调试,所以我个人喜欢使用它们。 - Carsten Kuckuk
更正:在编写词法分析器时,经常使用goto语句。 - Joshua
一个相对较容易理解它们的有用性的方法是查看ANTLR生成的一些代码,看看有多少goto,然后尝试在没有它们的情况下重写词法分析器/语法分析器。 - Allon Guralnek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接