切换语句在c#中失败?

Switch语句切换是我个人喜欢switch vs. if/else if结构的主要原因之一。下面是一个例子:

static string NumberToWords(int number)
{
string[] numbers = new string[]
{ "", "one", "two", "three", "four", "five",
"six", "seven", "eight", "nine" };
string[] tens = new string[]
{ "", "", "twenty", "thirty", "forty", "fifty",
"sixty", "seventy", "eighty", "ninety" };
string[] teens = new string[]
{ "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
"sixteen", "seventeen", "eighteen", "nineteen" };


string ans = "";
switch (number.ToString().Length)
{
case 3:
ans += string.Format("{0} hundred and ", numbers[number / 100]);
case 2:
int t = (number / 10) % 10;
if (t == 1)
{
ans += teens[number % 10];
break;
}
else if (t > 1)
ans += string.Format("{0}-", tens[t]);
case 1:
int o = number % 10;
ans += numbers[o];


break;
default:
throw new ArgumentException("number");
}
return ans;
}

聪明的人畏缩,因为__abc0应该在函数之外声明:好吧,它们是,这只是一个例子。

编译器失败,出现以下错误:

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

为什么?有没有办法在没有三个__abc的情况下获得这种行为?

218943 次浏览

他们为c#改变了switch语句(来自C/Java/ c++)的行为。我想原因是人们忘记了失败,导致了错误。我读过的一本书说使用goto进行模拟,但对我来说这听起来不像是一个好的解决方案。

一个跳转语句,例如break是 每个案例块后要求, 包括最后一个块是否 case语句或默认值 声明。只有一个例外,不像 c++的switch语句),c#没有 支撑着一种含蓄的坠落 一个箱子连着另一个箱子。一个 例外是如果case语句有 没有代码。< / p >

——c# switch()文档

他们故意省略了这种行为,以避免这种行为不是由意志决定的,而是引起问题的。

它只能在case部分没有语句的情况下使用,例如:

switch (whatever)
{
case 1:
case 2:
case 3: boo; break;
}

开关失效在历史上是现代软件的主要漏洞来源之一。语言设计人员决定强制在case的末尾跳转,除非您默认直接跳转到下一个case而不进行处理。

switch(value)
{
case 1:// this is still legal
case 2:
}

你可以“转到案例标签” http://www.blackwasp.co.uk/CSharpGoto.aspx < / p >

goto语句是一个简单的命令,它无条件地将程序的控制权转移到另一个语句。该命令经常受到批评,一些开发人员主张将其从所有高级编程语言中删除,因为它会导致意大利面条式代码< em > < / em >。当有太多的goto语句或类似的跳转语句,代码变得难以阅读和维护时,就会发生这种情况。然而,也有程序员指出,如果仔细使用goto语句,可以为一些问题提供一个优雅的解决方案……

你忘记在case 3中添加“break;”语句。在情形2中,你把它写到if块中。 因此试试这个:

case 3:
{
ans += string.Format("{0} hundred and ", numbers[number / 100]);
break;
}




case 2:
{
int t = (number / 10) % 10;
if (t == 1)
{
ans += teens[number % 10];
}
else if (t > 1)
{
ans += string.Format("{0}-", tens[t]);
}
break;
}


case 1:
{
int o = number % 10;
ans += numbers[o];
break;
}


default:
{
throw new ArgumentException("number");
}

(复制/粘贴我在别处提供的答案)

通过switch-case可以通过在case(参见case 0)中没有代码来实现,或者使用特殊的goto case(参见case 1)或goto default(参见case 2)表单:

switch (/*...*/) {
case 0: // shares the exact same code as case 1
case 1:
// do something
goto case 2;
case 2:
// do something else
goto default;
default:
// do something entirely different
break;
}

“为什么”是为了避免意外摔倒,对此我很感激。这是C和Java中常见的错误来源。

解决办法是使用goto,例如。

switch (number.ToString().Length)
{
case 3:
ans += string.Format("{0} hundred and ", numbers[number / 100]);
goto case 2;
case 2:
// Etc
}

在我看来,开关/机箱的总体设计有点不太理想。它太接近C语言了——在范围等方面可以做一些有用的改变。可以说,一个更聪明的开关,可以进行模式匹配等将是有帮助的,但这实际上是从开关到“检查条件序列”的变化-在这一点上,可能需要一个不同的名称。

在每个case语句之后需要打破转到语句,即使它是默认的case。

你可以像c++一样通过goto关键字实现fall through。

例:

switch(num)
{
case 1:
goto case 3;
case 2:
goto case 3;
case 3:
//do something
break;
case 4:
//do something else
break;
case default:
break;
}

为了补充答案,我认为有必要考虑与此相关的相反问题,即:为什么C允许跌倒?

当然,任何编程语言都有两个目标:

  1. 为计算机提供指令。
  2. 留下程序员意图的记录。

因此,任何编程语言的创建都是在如何最好地服务于这两个目标之间取得平衡。一方面,它越容易转换成计算机指令(无论是机器代码、像IL这样的字节码,还是在执行时进行解释的指令),那么编译或解释的过程就越高效、可靠和输出紧凑。极端地说,这个目标导致我们只编写汇编、IL甚至原始操作代码,因为最简单的编译是根本没有编译的地方。

相反,语言越能表达程序员的意图,而不是为此目的所采取的手段,程序在编写和维护时就越容易理解。

现在,switch总是可以通过将其转换为等价的if-else块链或类似的方式来编译,但它被设计为允许编译为特定的通用程序集模式,在这种模式中,一个人获取一个值,从它计算偏移量(无论是通过查找一个由该值的完全哈希索引的表,还是通过对值*的实际算术)。在这一点上值得注意的是,今天,c#编译有时会将switch转换为等效的if-else,有时会使用基于哈希的跳转方法(C、c++和其他具有类似语法的语言也是如此)。

在这种情况下,允许失败有两个很好的理由:

  1. 无论如何,这都是自然发生的:如果你在一组指令中构建了一个跳转表,而前面的一批指令中有一个不包含某种类型的跳转或返回,那么执行就会自然地进行到下一批。允许失败是“刚刚发生”的事情;如果你将使用__abc0的C转换为使用跳表的机器代码。

  2. 用汇编编写代码的程序员已经习惯了等价的东西:在汇编中手工编写跳转表时,他们必须考虑给定的代码块是否会以返回结束,是否会跳转到表之外,或者只是继续到下一个代码块。因此,让编码器在必要时添加显式break是“自然的”;编码器也是如此。

因此,在当时,平衡计算机语言的两个目标是合理的尝试,因为它既涉及生成的机器代码,也涉及源代码的表达性。

然而,四十年后,情况不太一样了,原因如下:

  1. 如今使用C语言的程序员可能很少或根本没有汇编经验。许多其他c风格语言的程序员甚至更不可能这样做(尤其是Javascript!)任何“人们习惯的装配”的概念;已经无关紧要了。
  2. 优化中的改进意味着switch变成if-else的可能性更高,因为它被认为是最有效的方法,或者变成一个特别深奥的跳跃表方法的变体。高级和低级方法之间的映射不像以前那么强了。
  3. 经验表明,fall-through往往是少数情况,而不是常态(一项对Sun编译器的研究发现,在switch块中,有3%的块使用了fall-through,而不是在同一个块上使用多个标签,人们认为这里的用例意味着这3%实际上比正常情况要高得多)。因此,所研究的语言使不寻常的语言比普通的语言更容易被迎合。
  4. 经验表明,错误往往是问题的根源,无论是意外发生的情况,还是维护代码的人遗漏了正确的错误。后者是对与fall-through相关的错误的微妙补充,因为即使您的代码完全没有错误,您的fall-through仍然可能导致问题。

与上述最后两点相关的是,我们可以参考K&R最新版的一段话:

从一种情况失败到另一种情况不是健壮的,当程序被修改时很容易解体。除了对单个计算使用多个标签外,应该谨慎使用并注释fall-贯穿。

作为一种良好的形式,在最后一个case(这里的默认值)后面加上一个break,即使这在逻辑上是不必要的。有一天,当最后添加了另一个案例时,这一点防御性编程将拯救您。

所以,从马的嘴,摔倒在C是有问题的。始终用注释记录错误被认为是一种很好的实践,这是一个普遍原则的应用,即应该记录在哪里做了不寻常的事情,因为这将在以后的代码检查中绊倒,并且/或使您的代码看起来像新手的错误,而实际上它是正确的。

想想看,代码是这样的:

switch(x)
{
case 1:
foo();
/* FALLTHRU */
case 2:
bar();
break;
}

在代码中添加了一些使fall-through显式的东西,它只是不能被编译器检测到(或其缺失可以被检测到)。

因此,在c#中必须显式地使用fall-through这一事实并不会给那些用其他C风格语言写得很好的人增加任何惩罚,因为他们已经在他们的fall-through中显式了。†

最后,这里使用goto已经是C和其他此类语言的规范:

switch(x)
{
case 0:
case 1:
case 2:
foo();
goto below_six;
case 3:
bar();
goto below_six;
case 4:
baz();
/* FALLTHRU */
case 5:
below_six:
qux();
break;
default:
quux();
}

在这种情况下,我们希望在执行的代码中包含一个块,而不仅仅是为前面的块带来一个值,那么我们已经不得不使用goto。(当然,用不同的条件句也有避免这种情况的方法和方法,但几乎所有与这个问题相关的事情都是如此)。因此,c#建立在已经很正常的方式上,以处理一种情况,即我们想要在switch中击中多个代码块,并只是泛化它来涵盖失败。它还使这两种情况更方便和自文档化,因为我们必须在C中添加一个新标签,但可以在c#中使用case作为标签。在c#中,我们可以去掉below_six标签并使用goto case 5,这更清楚地表明我们正在做什么。(我们还必须为default添加break,我省略了这一点,只是为了使上面的C代码明显不是c#代码)。

总之:

  1. c#不再像40年前的C代码那样与未优化的编译器输出直接相关(现在的C代码也是如此),这使得fall-through的灵感之一变得无关紧要。
  2. c#与C保持兼容,不仅仅是因为有隐式的break,让熟悉类似语言的人更容易学习语言,也更容易移植。
  3. c#消除了一个可能的错误或错误代码的来源,这些错误或错误代码在过去四十年中一直被记录为引起问题的原因。
  4. c#使得现有的C最佳实践(文档失败)可由编译器强制执行。
  5. c#使不寻常的情况有更明确的代码,通常的情况有自动编写的代码。
  6. c#使用与C中相同的基于__abc0的方法从不同的case标签击中相同的块。它只是将其推广到其他一些情况。
  7. 通过允许case语句充当标签,c#使得基于__abc0的方法比C中更方便、更清晰。

总而言之,这是一个非常合理的设计决策


*某些形式的BASIC允许人们做GOTO (x AND 7) * 50 + 240这样的事情,虽然它很脆弱,因此是禁止goto的一个特别有说服力的例子,但它确实显示了一种高级语言等价的方式,低级代码可以基于对值的算术进行跳转,当它是编译的结果而不是必须手动维护的东西时,这要合理得多。Duff's Device的实现特别适合等效的机器代码或IL,因为每个指令块通常都是相同的长度,而不需要添加nop填充符。

†达夫的装置再次出现在这里,作为一个合理的例外。事实上,在这种模式和类似的模式下,操作的重复使得fall-through的使用相对清晰,即使没有明确的注释。

只是一个简短的说明,Xamarin的编译器实际上犯了这个错误,它允许fallthrough。这个问题应该已经解决了,但还没有发布。在一些代码中发现了这一点,而编译器并没有抱怨。

开关(c#参考)说

c#要求开关部分的结尾,包括最后一个,

所以你还需要在你的default部分中添加一个break;,否则仍然会出现编译器错误。