C#でのswitchステートメントのフォールスルー?


365

スイッチステートメントのフォールスルーは、構成を愛することとswitch対比することの私の個人的な主な理由の1つです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;
}

string[]sは関数の外で宣言する必要があるので、賢い人々はうんざりしています。まあ、そうです、これは単なる例です。

コンパイラは次のエラーで失敗します。

コントロールは、1つのケースラベル(「ケース3:」)から別のケースラベルに移行できません
コントロールは、1つのケースラベル(「ケース2:」)から別のケースラベルに移行できません。

どうして?そして、3つifのs なしでこの種の動作を実現する方法はありますか?

回答:


648

他の場所で提供し回答のコピー/貼り付け)

落下switch- caseSはでないコードを持たないことによって達成することができる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;
}

121
この特定の例では、gotoは有害とは見なされていません。
Thomas Owens、

78
くそー-私は1.0の初期の頃からC#でプログラミングをしていて、今までこれを見たことがありません。見に行くだけで、毎日新しいことを学びます。
エリックフォーブス

37
元気です、エリック。/ I /がそれについて知っていた唯一の理由は、私が虫眼鏡でECMA-334仕様を読んだコンパイラ理論オタクだからです。
アレックスライマン

13
@Dancrumb:機能が作成された時点では、C#にはまだ「ソフト」キーワード(「yield」、「var」、「from」、「select」など)が追加されていなかったため、3つの実際のオプションがありました:1 )「フォールスルー」をハードキーワードにする(変数名として使用できない)、2)そのようなソフトキーワードをサポートするために必要なコードを記述する、3)すでに予約されているキーワードを使用する。#1は、移植するコードにとって大きな問題でした。#2は、私が理解している限りでは、かなり大きなエンジニアリングタスクでした。#3には副次的な利点がありました。他の開発者が、事実の後にコードを読んだ後、gotoの基本コンセプトから機能について学ぶことができました
Alex Lyman

10
これはすべて、明示的なフォールスルーの新しい/特別なキーワードについて話します。彼らは単に「続行」キーワードを使用したのではないでしょうか?言い換えると。スイッチから抜け出すか、次のケースに進みます(フォールスルー)。
Tal Even-Tov 2013年

44

「なぜ」は私が感謝している偶発的なフォールスルーを回避することです。これは、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に近すぎます-スコーピングなどの点で役立つ変更がいくつかあります。パターンマッチングなどを実行できるよりスマートなスイッチはおそらく役に立ちますが、実際にはスイッチから「一連の条件の確認」に変更されています。 -その時点で別の名前が必要になる可能性があります。


1
これが、私の心の中のスイッチとif / elseifの違いです。スイッチは、単一の変数のさまざまな状態をチェックするためのものです。一方、if / elseifは、接続されているものをいくつでもチェックするために使用できますが、必ずしも単一または同じ変数であるとは限りません。
Matthew Scharley、2008年

2
偶発的なフォールスルーを防ぐことが目的であれば、コンパイラの警告の方が良かったと思います。:あなたの文が代入されている場合場合は、1つを持っているだけのようif (result = true) { }
タルでもサートフ

3
@ TalEven-Tov:コンパイラの警告は、ほとんど常にコードを修正して改善できる場合に適しています。個人的には、暗黙的なブレイクを好むので、最初は問題になりませんが、それは別の問題です。
Jon Skeet 2013年

26

スイッチのフォールスルーは、歴史的に、現代のソフトウェアにおけるバグの主要な原因の1つです。言語設計者は、処理せずに直接次のケースにデフォルト設定しない限り、ケースの最後にジャンプすることを必須にすることを決定しました。

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

23
それがない理由私はよく理解したことがない「ケース1、2:」
BCS

11
@David Pfeffer:そうですcase 1, 2:。そうすることを可能にする言語でもそうです。私が理解できないことは、現代の言語がそれを許可することを選択しない理由です。
BCS

@BCSにgotoステートメントを使用すると、複数のコンマ区切りのオプションを処理するのが難しい場合があります。
ペンガート

@pengut:case 1, 2:単一のラベルで複数の名前が付いていると言う方が正確かもしれません。-FWIW、私は、フォールスルーを禁止するほとんどの言語が「連続したケースラベル」インジウムの特別なケースではなく、ケースラベルを次のステートメントの注釈として扱い、(1つまたは詳細)ジャンプするケースラベル。
BCS

22

ここで答えを追加するには、これと関連して反対の質問を検討する価値があると思います。なぜCはそもそもフォールスルーを許可したのですか?

もちろん、どのプログラミング言語にも2つの目標があります。

  1. コンピュータに指示を与えます。
  2. プログラマーの意図を記録しておきます。

したがって、プログラミング言語の作成は、これら2つの目標を最もよく果たす方法の間のバランスです。一方では、コンピューター命令に変換する方が簡単です(マシンコード、ILのようなバイトコード、または実行時に命令が解釈されるかどうかにかかわらず)。コンパイルまたは解釈のプロセスが効率的で信頼性が高く、出力がコンパクト。極端に言えば、最も簡単なコンパイルはコンパイルがまったくない場所なので、この目標は、アセンブリ、IL、または生のオペコードでさえも書くことになります。

逆に言えば、目的を達成するための手段ではなく、言語がプログラマーの意図を表現するほど、プログラムの作成時とメンテナンス時の両方でプログラムが理解しやすくなります。

今でswitchは、それを同等のif-elseブロックチェーンまたは類似のものに変換することによって常にコンパイルすることができましたが、値を受け取り、それからオフセットを計算する特定の一般的なアセンブリパターンへのコンパイルを許可するように設計されました(テーブルを検索するかどうかにかかわらず)値の完全なハッシュ、または値の実際の算術によってインデックスが付けられます*)。この時点で注目に値するのは、今日、C#コンパイルがswitch同等のものif-elseになり、ハッシュベースのジャンプアプローチを使用する場合があることです(C、C ++、および同等の構文を持つ他の言語でも同様です)。

この場合、フォールスルーを許可する理由は2つあります。

  1. それはとにかく自然に起こります:ジャンプテーブルを一連の命令に構築し、以前の命令のバッチの1つになんらかのジャンプまたは戻り値が含まれていない場合、実行は自然に次のバッチに進みます。switchCを使用してジャンプテーブルを使用し、マシンコードを使用する場合、フォールスルーを許可することは「ちょうど起こる」ことでした。

  2. アセンブリで記述したコーダーは、すでに同等のものに慣れています。アセンブリでジャンプテーブルを手動で記述する場合、特定のコードブロックがリターンで終わるか、テーブルの外でジャンプするか、またはそのまま続行するかを検討する必要があります。次のブロックへ。このように、breakコーダーにとっても、必要なときにコーダーが明示的に追加することは「自然」でした。

したがって、当時は、生成されたマシンコードとソースコードの表現力の両方に関連するため、コンピュータ言語の2つの目標のバランスを取ることは合理的な試みでした。

しかし、40年後のいくつかの理由により、状況はまったく同じではありません。

  1. 今日のCのコーダーは、アセンブリの経験がほとんどまたはまったくない可能性があります。他の多くのCスタイル言語のコーダーは、さらに可能性が低くなります(特にJavascript!)。「人々が組立てに慣れている」という概念はもはや関係ありません。
  2. 最適化の改善は、アプローチが最も効率的であると考えswitchられたif-elseために変化する可能性、またはジャンプテーブルアプローチの特に難解なバリアントに変化する可能性が高くなることを意味します。上位レベルと下位レベルのアプローチ間のマッピングは、以前ほど強力ではありません。
  3. 経験によれば、フォールスルーは標準ではなく少数のケースである傾向にあります(Sunのコンパイラーの調査でswitchは、同じブロックに複数のラベル以外のフォールスルーが使用されているブロックの3%が見つかりました。ここでのケースは、この3%が実際には通常よりもはるかに高かったことを意味します)。ですから、研究された言語は、珍しいものを普通のものよりも容易に仕出します。
  4. 経験によれば、フォールスルーは、誤って行われた場合と、コードを保守している誰かが正しいフォールスルーを見逃した場合の両方で、問題の原因となる傾向があることを示しています。後者はフォールスルーに関連するバグへの微妙な追加です。コードに完全にバグがない場合でも、フォールスルーが問題を引き起こす可能性があるためです。

これらの最後の2つのポイントに関連して、K&Rの現在のエディションからの次の引用を検討してください。

あるケースから別のケースへのフォールスルーは堅牢ではなく、プログラムが変更されると分解されやすくなります。単一の計算の複数のラベルを除いて、フォールスルーは控えめに使用し、コメントする必要があります。

論理的には不要ですが、形式の問題として、最後のケース(ここではデフォルト)の後に休憩を入れます。いつか最後に別のケースが追加されるとき、この防御的なプログラミングのビットはあなたを救います。

したがって、馬の口から見ると、Cのフォールスルーには問題があります。フォールスルーには常にコメントを付けて記録することをお勧めします。これは、何か通常とは異なることを記録するという一般的な原則の応用です。それが実際に正しいのに初心者のバグがあります。

そして、あなたがそれについて考えるとき、このようなコード:

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

されたフォールスルーの明示的なコードの中を作るために何かを追加し、それだけで検出することができるものではありません(またはその欠如に検出することができます)コンパイラによって。

そのため、C#でフォールスルーを明示的にオンにする必要があるという事実は、フォールスルーですでに明示されているため、他のCスタイルの言語でうまく書いた人にペナルティを与えることはありません。†

最後に、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#は、1つの状況で複数のコードブロックにヒットする必要がある1つの状況に対処するためのすでに通常の方法に基づいて構築され、switchフォールスルーもカバーするように一般化されました。また、Cで新しいラベルを追加する必要がありますがcase、C#でラベルとして使用できるため、両方のケースがより便利で自己文書化されました。C#では、below_sixラベルをgoto case 5削除して、何をしているのかが明確になるように使用できます。(追加する必要もありますbreakのためにdefault、上記のCコードを明らかにC#コードではなくするために省略しました)。

したがって、要約すると:

  1. C#は、40年前のCコード(最近のCも同様)のように、最適化されていないコンパイラー出力に直接関係しなくなりました。これにより、フォールスルーのインスピレーションの1つが無関係になります。
  2. C#はbreak、暗黙的なだけでなく、Cとの互換性を維持し、同様の言語に精通している人による言語の学習を容易にし、移植を容易にします。
  3. C#は、過去40年間問題を引き起こしていることが十分に文書化されている、バグまたは誤解されているコードのソースを削除します。
  4. C#は、Cによる既存のベストプラクティス(ドキュメントのフォールスルー)をコンパイラーによって実施可能にします。
  5. C#は、異常なケースをより明示的なコードを持つものにし、通常のケースは、コードを持つものが自動的に書き込むだけにします。
  6. C#は、Cで使用されているのと同じgotoベースのアプローチを使用して、異なるcaseラベルから同じブロックをヒットします。他のいくつかのケースに一般化するだけです。
  7. C#は、ステートメントをラベルとして機能gotoさせることで、Cベースのアプローチよりも、C ベースのアプローチをより便利で明確にしcaseます。

全体として、かなり合理的な設計決定


* BASICの一部の形式では、GOTO (x AND 7) * 50 + 240脆弱なため禁止gotoなどの特に説得力のあるケースを実行できますが、低レベルのコードが以下に基づいてジャンプできるような方法と同等の、より高い言語が表示されます。値の算術。これは、手動で保守する必要があるものではなく、コンパイルの結果である場合にはるかに合理的です。特に、ダフのデバイスの実装は、nopフィラーの追加を必要とせずに命令の各ブロックが同じ長さになることが多いため、同等のマシンコードまたはILに適しています。

†合理的な例外として、ダフのデバイスがここに再び登場します。これと同様のパターンで操作が繰り返されるという事実は、その効果に対する明確なコメントがなくても、フォールスルーの使用を比較的明確にするのに役立ちます。


17

あなたは「ケースラベルに行く」ことができ ますhttp://www.blackwasp.co.uk/CSharpGoto.aspx

gotoステートメントは、プログラムの制御を無条件に別のステートメントに移す単純なコマンドです。このコマンドは、スパゲッティコードにつながる可能性があるため、一部の開発者がすべての高水準プログラミング言語からの削除を主張することで批判されることがよくあります。これは、gotoステートメントまたは同様のジャンプステートメントが多すぎて、コードの読み取りと保守が困難になる場合に発生します。ただし、gotoステートメントを注意深く使用すると、いくつかの問題に対してエレガントなソリューションを提供することを指摘するプログラマーがいます...


8

彼らは意志によって使用されなかったが問題を引き起こしたときに回避するために、この動作を設計により省略しました。

次のように、ケース部分にステートメントがない場合にのみ使用できます。

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

4

彼らは、c#の(C / Java / C ++からの)switchステートメントの動作を変更しました。その理由は、人々がフォールスルーを忘れ、エラーが発生したためだと思います。私が読んだ1冊の本では、gotoを使用してシミュレーションするように言われていましたが、これは私にとって良い解決策のようには思えません。


2
C#はgotoをサポートしていますが、フォールスルーはサポートしていませんか?ワオ。そして、それだけではありません。C#は、このように動作することを私が知っている唯一の言語です。
Matthew Scharley、2008年

最初はあまり好きではありませんでしたが、「フォールスルー」は実際には災害のレシピです(特にジュニアプログラマーの間で)。ケース)「Kenny」は、スイッチケースでエレガントなGotoを使用することを強調するリンクを投稿しました。
プレッツェル、

それは私の意見では大したことではありません。99%の確率でフォールスルーしたくないので、過去にバグに焼き付けられました。
ケン

1
「これは私にとって良い解決策のように聞こえません」-それが何のためにあるので、あなたについてそれを聞いて申し訳ありませんgoto case。フォールスルーに対するその利点は、明示的であるということです。ここの一部の人々goto caseは、彼らが問題を理解せずに「後藤」に対して教化されていて、自分自身で考えることができないことを単に示すことに反対している。ダイクストラが「有害と見なされるGOTO」を書いたとき、彼は制御フローを変更する他の手段がなかった言語に取り組んでいました。
ジムバルター2013年

@JimBalter、そしてその上でダイクストラを引用する何人の人々がKnuthを引用するgotoでしょうか。
Jon Hanna

1

gotoキーワードでc ++のようなフォールスルーを実現できます。

例:

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;
}

9
誰かだけが2年前に投稿したとしたら!

0

最後のブロックがケースステートメントであるかデフォルトステートメントであるかを含め、各ケースブロックの後には、ブレークなどのジャンプステートメントが必要です。1つの例外を除いて(C ++のswitchステートメントとは異なり)、C#は1つのケースラベルから別のケースラベルへの暗黙のフォールスルーをサポートしません。1つの例外は、caseステートメントにコードがない場合です。

- C#スイッチ()のドキュメント


1
この動作が文書化されていることに気づきました。なぜそれがそうであるのか、そして古い動作を得るための代替手段があるのか​​を知りたいのです。
Matthew Scharley、2008年

0

各caseステートメントの後には、それがデフォルトのケースであっても、breakまたはgotoステートメントが必要です。


2
誰かだけが2年前に投稿したとしたら!

1
@Poldie初めておもしろかった...シルパは、すべてのケースでブレークや後藤を必要とせず、独自のコードですべてのケースに対応しています。コードをうまく共有する複数のケースを持つことができます。
マーベリック

0

Xamarinのコンパイラーが実際にこれを間違っており、フォールスルーを許可していることを付け加えておくだけの簡単なメモ。おそらく修正されていますが、リリースされていません。実際に失敗しているいくつかのコードでこれを発見し、コンパイラーは文句を言わなかった。


0

スイッチ(C#リファレンス)は言う

C#では、最後のセクションを含むスイッチセクションの終わりが必要です。

したがってbreak;defaultセクションにもを追加する必要があります。そうしないと、コンパイラエラーが引き続き発生します。


Thx、これは私を助けました;)
CareTaker22

-1

C#は、switch / caseステートメントによるフォールスルーをサポートしていません。理由はわかりませんが、実際にはサポートされていません。リンケージ


これは間違っています。他の回答を参照してください... 暗黙のフォールスルーの効果は、明示的に 取得できますgoto case。これは、C#デザイナーによる意図的な賢明な設計の選択でした。
ジムバルター2013年

-11

「ブレーク」を追加するのを忘れました。ケース3にステートメントを記述します。ケース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");
}

2
これは非常に間違った出力を生成します。仕様により、switchステートメントを省略しました。問題は、C#コンパイラーが他のほとんどの言語にこの制限がないときに、これをエラーとして認識する理由です。
Matthew Scharley、2008年

5
理解するのになんと驚くべき失敗か。そして、あなたはこれを削除するために5年を過ごしました、そしてそれをまだやっていませんか?マインドボグリング。
ジムバルター2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.