C#中标签的用途是什么?

13

标签和GOTO在C#中被认为是不良行为,据我所知,在C#中没有使用它的理由。

那么在C#中标签有什么用处?


3
它会让其他程序员有理由抱怨你的代码 :-) - Ian Ringrose
可能是Does anyone still use [goto] in C# and if so why?的重复问题。 - Ian Ringrose
13个回答

23

标签和goto本身并没有问题。问题在于人们倾向于滥用它们,这会引起问题。

典型的标签使用方式

OperationStart:
  if ( !TrySomeOperation() ) {
    if ( MaybeFixOperation() ) { 
      goto OperationStart;
    }
  }

你需要做出一些断言来确保不会陷入无限循环,但在合理的保证下,这段代码本质上没有任何问题。


我认为这是goto最常见的用法。 - dr. evil
7
这段代码本质上没有什么问题,除了它不太易读之外。 如果你把它写成:while(!TrySomeOperation() && MaybeFixOperation()) {},那就更明显了,你可能会一直循环(可能无限循环)。 - Aiua
1
-1 对 Aiua 的评论。做手飞过头的动作 - Samuel
1
我认为这是唯一一个允许的代码示例(o; - Michael Piendl
标签和goto本身并没有问题。但是编译器中的优化器(或者你的.NET运行时)会根据代码的可预测性进行操作。如果你的代码不可预测,你的优化器将无法对其进行有用的处理。虽然你的例子只是一个简单的循环,但是有很多情况下,标签和goto会导致不可预测(因此无法优化)的结果。这个视频解释了一些实际工作原理:https://www.youtube.com/watch?v=FnGCDLhaxKU - atlaste

10

虽然它们是不受好评的做法,但这并不意味着应该关闭使用它们的任何可能性。尽管它们可能从来不会真正被“需要”,但有时它们是最佳选择。


7
当你实现一个小的有限状态机时,你可以为每个状态使用一个标签goto进行状态转换。这是实现有限状态机的标准方法之一,如果代码注释指向文档中的状态机图表,可以产生清晰的代码。
有时问题域包含大量的状态机(例如,电信协议通常由有限状态机定义),大多数情况下你不会经常看到有限状态机。
如果你正在编写一个简单的编译器输出C#,那么Gotoslabels对于生成的代码也非常有用。

DFA实际上被广泛使用,通常用于标记器和解析器中。这种方法会导致方法变得非常庞大...你知道吗,如果你在IL中有一个非常大的方法,它不会被.NET运行时优化,对吧?出于各种原因,如果你问我,最好让DFA编译成状态模式——即使它是机器生成的代码。 - atlaste

4

代码生成器有时会使用标签和goto语句。在某些情况下,这可以简化代码生成逻辑。出于可读性的原因,通常不建议使用goto语句,但是如果代码不打算被阅读,则这一点无关紧要。

我还看到过这样的情况:反编译器由于中间语言(IL)而感到困惑,输出了goto语句,而不是人类精心设计的指令序列。如果goto语句是非法的,那么反编译器可能必须完全放弃该代码部分。这样,至少它可以生成一些可以往返转换的内容。


3

为什么要用goto和label?

原因其实很简单。汇编语言和IL不支持像fordobreakcontinuewhile这样的高级指令。隐式地,循环代码(或者更一般地说:分支代码)会被编译成分支。goto

请注意,即使有通常更好的替代方案(在这一点上,我想指出有像状态模式这样的设计模式),也不能轻易地实现我所能想到的所有构造,而没有使用'goto'。

如果你设计一个类似C#的语言,它是IL的“高级汇编语言”,那么至少支持IL中的所有内容是有意义的。在这里,分支/标签是低级结构,goto/label是C#的投影(加上作用域作为高级结构)。

从编译器的角度来看

最终,如果您深入了解编译器的工作原理,会发现有可预测的代码和不可预测的代码之分。编译器中优化部分花费了大量精力将您代码中使用的结构规范化。一旦规范化,就会使用模式进行优化。
这里就出现了问题。如果您使用了while循环、for循环或foreach循环,它们肯定会生成一个规范化的模式。毕竟,循环是您代码中可以被优化的头号内容。但是,如果您使用奇怪的分支,它们将无法被优化 - 因为它不会被规范化为标准模式,因此也不会被模式匹配器捕获。
这个评论不仅关于代码模式的可预测性 - 它还涉及到数据流的可预测性。同样,如果它是可预测的,您的编译器可以做得更多。在各种情况下,像break和continue这样的结构也会导致更不可预测的分支;标签和goto只会让您更容易地到达那里。结果是相同的:如果发生这种情况,您的性能将受到影响。

所以虽然所有编译器都将循环转换为特定形式的分支,但IL编译器是一种SSA编译器,就像LLVM一样。这个Chandler的视频解释了一些关于它如何工作的知识: https://www.youtube.com/watch?v=FnGCDLhaxKU


2

没有使用 goto 的标签是无用的,它们什么也不做。

使用 goto 被认为是一种不好的实践。但有一种情况是无法避免的:跳出嵌套循环:

foreach(...) {
  foreach(...) {
    if(...) {
      goto OuterLabel;
    }
  }
}
OuterLabel:

在这种情况下,使用break语句只会中断最内层的循环。

2
为什么不将这两个foreach循环放在它们自己的方法中,然后你可以使用return语句来跳出。我看到的大多数goto示例都在长方法中,我不喜欢长方法。 - Ian Ringrose
同意,使用单独的方法是一个好的方法,因为“return”语句能够跳出整个方法。 - Michael Damatov
7
有些人(包括我自己)不喜欢仅仅为了创造而去创造方法。 - Perchik
创建一个方法,让您可以为foreach循环命名,从而使代码自我记录。 - Ian Ringrose

2

我认为这是一项营销决策。

微软希望各种类型的开发人员都使用他们的C#语言,如果添加标签,可以使某些程序员更容易地进行转换。它还可以更轻松地将旧代码移植到他们的语言中...


记录一下:我不是微软的仇恨者.. 我和其他人一样喜欢C#.. 而且我认为添加标签和goto语句是一个明智的举动... - Arcturus

1

你有没有考虑使用这篇文章来为LabelForRejectGoTo做参考? - Ben

1

我在某处读到,大多数情况下,goto应该只用于向前跳转,因此很可能只用于早期循环终止和继续外部循环,因为C#中没有标记的循环(与Java不同)。而且有一些算法可以通过goto来优雅地表达,而不是按结构化方式执行。


1
这是我使用goto语句的方式。对于嵌套循环,有时候直接跳转到某个标签比试图弄清楚如何退出哪个循环更容易。 - Perchik

1

没有它们,做 switch 语句很困难。


case和default语句是标签。您可以通过以下方式查看: switch (x) { case 1: if (oneEqualsTwo) goto case 2; break; case 2: break; }这在C#的ECMA规范中最清晰地说明了:http://www.csharpfriends.com/Spec/index.aspx?specID=15.7.2.htm - mancaus

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