ここで答えを追加するには、これと関連して反対の質問を検討する価値があると思います。なぜCはそもそもフォールスルーを許可したのですか?
もちろん、どのプログラミング言語にも2つの目標があります。
- コンピュータに指示を与えます。
- プログラマーの意図を記録しておきます。
したがって、プログラミング言語の作成は、これら2つの目標を最もよく果たす方法の間のバランスです。一方では、コンピューター命令に変換する方が簡単です(マシンコード、ILのようなバイトコード、または実行時に命令が解釈されるかどうかにかかわらず)。コンパイルまたは解釈のプロセスが効率的で信頼性が高く、出力がコンパクト。極端に言えば、最も簡単なコンパイルはコンパイルがまったくない場所なので、この目標は、アセンブリ、IL、または生のオペコードでさえも書くことになります。
逆に言えば、目的を達成するための手段ではなく、言語がプログラマーの意図を表現するほど、プログラムの作成時とメンテナンス時の両方でプログラムが理解しやすくなります。
今でswitch
は、それを同等のif-else
ブロックチェーンまたは類似のものに変換することによって常にコンパイルすることができましたが、値を受け取り、それからオフセットを計算する特定の一般的なアセンブリパターンへのコンパイルを許可するように設計されました(テーブルを検索するかどうかにかかわらず)値の完全なハッシュ、または値の実際の算術によってインデックスが付けられます*)。この時点で注目に値するのは、今日、C#コンパイルがswitch
同等のものif-else
になり、ハッシュベースのジャンプアプローチを使用する場合があることです(C、C ++、および同等の構文を持つ他の言語でも同様です)。
この場合、フォールスルーを許可する理由は2つあります。
それはとにかく自然に起こります:ジャンプテーブルを一連の命令に構築し、以前の命令のバッチの1つになんらかのジャンプまたは戻り値が含まれていない場合、実行は自然に次のバッチに進みます。switch
Cを使用してジャンプテーブルを使用し、マシンコードを使用する場合、フォールスルーを許可することは「ちょうど起こる」ことでした。
アセンブリで記述したコーダーは、すでに同等のものに慣れています。アセンブリでジャンプテーブルを手動で記述する場合、特定のコードブロックがリターンで終わるか、テーブルの外でジャンプするか、またはそのまま続行するかを検討する必要があります。次のブロックへ。このように、break
コーダーにとっても、必要なときにコーダーが明示的に追加することは「自然」でした。
したがって、当時は、生成されたマシンコードとソースコードの表現力の両方に関連するため、コンピュータ言語の2つの目標のバランスを取ることは合理的な試みでした。
しかし、40年後のいくつかの理由により、状況はまったく同じではありません。
- 今日のCのコーダーは、アセンブリの経験がほとんどまたはまったくない可能性があります。他の多くのCスタイル言語のコーダーは、さらに可能性が低くなります(特にJavascript!)。「人々が組立てに慣れている」という概念はもはや関係ありません。
- 最適化の改善は、アプローチが最も効率的であると考え
switch
られたif-else
ために変化する可能性、またはジャンプテーブルアプローチの特に難解なバリアントに変化する可能性が高くなることを意味します。上位レベルと下位レベルのアプローチ間のマッピングは、以前ほど強力ではありません。
- 経験によれば、フォールスルーは標準ではなく少数のケースである傾向にあります(Sunのコンパイラーの調査で
switch
は、同じブロックに複数のラベル以外のフォールスルーが使用されているブロックの3%が見つかりました。ここでのケースは、この3%が実際には通常よりもはるかに高かったことを意味します)。ですから、研究された言語は、珍しいものを普通のものよりも容易に仕出します。
- 経験によれば、フォールスルーは、誤って行われた場合と、コードを保守している誰かが正しいフォールスルーを見逃した場合の両方で、問題の原因となる傾向があることを示しています。後者はフォールスルーに関連するバグへの微妙な追加です。コードに完全にバグがない場合でも、フォールスルーが問題を引き起こす可能性があるためです。
これらの最後の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#コードではなくするために省略しました)。
したがって、要約すると:
- C#は、40年前のCコード(最近のCも同様)のように、最適化されていないコンパイラー出力に直接関係しなくなりました。これにより、フォールスルーのインスピレーションの1つが無関係になります。
- C#は
break
、暗黙的なだけでなく、Cとの互換性を維持し、同様の言語に精通している人による言語の学習を容易にし、移植を容易にします。
- C#は、過去40年間問題を引き起こしていることが十分に文書化されている、バグまたは誤解されているコードのソースを削除します。
- C#は、Cによる既存のベストプラクティス(ドキュメントのフォールスルー)をコンパイラーによって実施可能にします。
- C#は、異常なケースをより明示的なコードを持つものにし、通常のケースは、コードを持つものが自動的に書き込むだけにします。
- C#は、Cで使用されているのと同じ
goto
ベースのアプローチを使用して、異なるcase
ラベルから同じブロックをヒットします。他のいくつかのケースに一般化するだけです。
- C#は、ステートメントをラベルとして機能
goto
させることで、Cベースのアプローチよりも、C ベースのアプローチをより便利で明確にしcase
ます。
全体として、かなり合理的な設計決定
* BASICの一部の形式では、GOTO (x AND 7) * 50 + 240
脆弱なため禁止goto
などの特に説得力のあるケースを実行できますが、低レベルのコードが以下に基づいてジャンプできるような方法と同等の、より高い言語が表示されます。値の算術。これは、手動で保守する必要があるものではなく、コンパイルの結果である場合にはるかに合理的です。特に、ダフのデバイスの実装は、nop
フィラーの追加を必要とせずに命令の各ブロックが同じ長さになることが多いため、同等のマシンコードまたはILに適しています。
†合理的な例外として、ダフのデバイスがここに再び登場します。これと同様のパターンで操作が繰り返されるという事実は、その効果に対する明確なコメントがなくても、フォールスルーの使用を比較的明確にするのに役立ちます。