C# switch ステートメントの制限 - なぜですか?
-
09-06-2019 - |
質問
switch ステートメントを作成する場合、case ステートメントで何をオンにできるかについて 2 つの制限があるようです。
たとえば(そして、はい、私は知っていますが、あなたがこの種のことをしている場合、それはおそらくあなたのことを意味します) オブジェクト指向 (OO) アーキテクチャは不確かです - これは単なる不自然な例です!)、
Type t = typeof(int);
switch (t) {
case typeof(int):
Console.WriteLine("int!");
break;
case typeof(string):
Console.WriteLine("string!");
break;
default:
Console.WriteLine("unknown!");
break;
}
ここで、switch() ステートメントは「整数型の値が予期される」という理由で失敗し、case ステートメントは「定数値が予期される」という理由で失敗します。
なぜこのような制限が設けられているのでしょうか?またその根本的な正当化は何ですか?switch ステートメントを使用する理由がわかりません もっている 静的解析のみに屈する理由、およびスイッチをオンにする値が整数 (つまり、プリミティブ) でなければならない理由。正当化とは何でしょうか?
解決
これは私のオリジナルの投稿で、議論を巻き起こしました... それは間違っているから:
スイッチステートメントは、大きなif-elseステートメントと同じものではありません。各ケースは一意であり、静的に評価する必要があります。Switchステートメントは、ケースの数に関係なく、一定の時間分岐を実行します。IF-ELSEステートメントは、各条件が真実であることがわかるまで、各条件を評価します。
実際、C# の switch ステートメントは次のようになります。 ない 常に定数時間ブランチ。
場合によっては、コンパイラは CIL switch ステートメントを使用します。これは実際にはジャンプ テーブルを使用した定数時間分岐です。ただし、指摘されているようにまれなケースでは、 アイヴァン・ハミルトン コンパイラはまったく別のものを生成する場合があります。
これは実際には、さまざまな C# switch ステートメント (一部は疎、一部は密) を作成し、ildasm.exe ツールで結果の CIL を確認することで非常に簡単に検証できます。
他のヒント
C# switch ステートメントと CIL switch 命令を混同しないことが重要です。
CIL スイッチはジャンプ テーブルであり、一連のジャンプ アドレスへのインデックスを必要とします。
これは、C# スイッチのケースが隣接している場合にのみ役立ちます。
case 3: blah; break;
case 4: blah; break;
case 5: blah; break;
しかし、そうでない場合はほとんど役に立ちません。
case 10: blah; break;
case 200: blah; break;
case 3000: blah; break;
(使用するスロットが 3 つだけで、サイズが約 3000 エントリのテーブルが必要になります)
隣接しない式を使用すると、コンパイラは線形の if-else-if-else チェックの実行を開始する場合があります。
より大きな非隣接式セットの場合、コンパイラはバイナリ ツリー検索から開始し、最後に最後のいくつかの項目を if-else-if-else することがあります。
隣接する項目の塊を含む式セットの場合、コンパイラはバイナリ ツリー検索を実行し、最後に CIL スイッチを実行する場合があります。
これには「かもしれない」と「かもしれない」がたくさんあり、コンパイラに依存します (Mono または Rotor では異なる場合があります)。
隣接するケースを使用して、私のマシン上で結果を複製しました。
10 方向スイッチの実行にかかる合計時間、10000 回の反復 (ミリ秒):25.1383
10 方向スイッチあたりのおおよその時間 (ミリ秒):0.0025138350 通りのスイッチを実行する合計時間、10000 回の反復 (ミリ秒):26.593
50 方向スイッチあたりのおおよその時間 (ミリ秒):0.00265935000 通りのスイッチを実行する合計時間、10000 回の反復 (ミリ秒) :23.7094
5000 ウェイスイッチあたりのおおよその時間 (ミリ秒):0.0023709450000 通りのスイッチを実行する合計時間、10000 回の反復 (ミリ秒):20.0933
50000 方向スイッチあたりのおおよその時間 (ミリ秒):0.00200933
次に、隣接しない case 式も使用しました。
10 方向スイッチの実行にかかる合計時間、10000 回の反復 (ミリ秒):19.6189
10 方向スイッチあたりのおおよその時間 (ミリ秒):0.00196189500 通りのスイッチを実行する合計時間、10000 回の反復 (ミリ秒) :19.1664
500 ウェイ スイッチごとのおおよその時間 (ミリ秒) :0.001916645000 通りのスイッチを実行する合計時間、10000 回の反復 (ミリ秒) :19.5871
5000 ウェイスイッチあたりのおおよその時間 (ミリ秒):0.00195871隣接しない 50,000 case switch ステートメントはコンパイルされません。
「式が長すぎるか複雑すぎるため、'ConsoleApplication1.Program.Main(string[])' 付近でコンパイルできません
ここで面白いのは、バイナリ ツリー検索が CIL 切り替え命令よりも (おそらく統計的にはそうではなく) 少し速く見えることです。
ブライアン、あなたは「」という言葉を使いましたね。絶え間ない」、これは計算量理論の観点から非常に明確な意味を持ちます。単純化された隣接する整数の例では O(1) (定数) とみなされる CIL が生成される可能性がありますが、疎な例は O(log n) (対数)、クラスター化された例はその中間に位置し、小さな例は O(n) (線形) になります。 )。
これは、静的な文字列の状況には対処していません。 Generic.Dictionary<string,int32>
が作成される可能性があり、最初の使用時に明確なオーバーヘッドが発生します。ここでのパフォーマンスは、のパフォーマンスに依存します。 Generic.Dictionary
.
をチェックすると、 C# 言語仕様 (CILスペックではありません)「15.7.2スイッチステートメント」は「一定の時間」について言及していないか、基礎となる実装がCILスイッチ命令を使用していることもあります(そのようなことを仮定することに非常に注意してください)。
結局のところ、最新のシステムでの整数式に対する C# の切り替えはマイクロ秒未満の操作であり、通常は心配する必要はありません。
もちろん、これらの時間はマシンや条件によって異なります。私はこれらのタイミング テストには注意を払いません。私たちが話しているマイクロ秒の長さは、実行されている「実際の」コードに比べると小さく見えます (そして、何らかの「実際のコード」を含める必要があります。そうしないとコンパイラーが分岐を最適化してしまいます)。システム内のジッター。私の答えは使用に基づいています イル・ダズム C# コンパイラによって作成された CIL を調べます。もちろん、CPU が実行する実際の命令は JIT によって作成されるため、これは最終的なものではありません。
x86 マシンで実際に実行された最終 CPU 命令をチェックしたところ、次のような単純な隣接セット スイッチが行われていることを確認できました。
jmp ds:300025F0[eax*4]
二分木検索には次のようなものが含まれます。
cmp ebx, 79Eh
jg 3000352B
cmp ebx, 654h
jg 300032BB
…
cmp ebx, 0F82h
jz 30005EEE
まず思い浮かぶ理由は、 歴史的な:
ほとんどの C、C++、および Java プログラマーはそのような自由に慣れていないため、それらを要求しません。
もう 1 つの、より正当な理由は、 言語の複雑さは増すだろう:
まず第一に、オブジェクトを比較する必要があるのは、 .Equals()
または ==
オペレーター?場合によっては両方が有効です。これを行うには新しい構文を導入する必要がありますか?プログラマが独自の比較方法を導入できるようにすべきでしょうか?
さらに、オブジェクトのスイッチをオンにすると、 switch ステートメントに関する根本的な前提を打ち破る. 。switch ステートメントを管理する 2 つの規則があり、オブジェクトのスイッチオンが許可されている場合、コンパイラーはこれらの規則を適用できません (「 C# バージョン 3.0 の言語仕様, §8.7.2):
- スイッチラベルの値が 絶え間ない
- スイッチラベルの値が 明確な (そのため、特定の switch-expression に対して選択できる switch ブロックは 1 つだけです)
非定数値が許可されるという仮定のケースで、次のコード例を考えてみましょう。
void DoIt()
{
String foo = "bar";
Switch(foo, foo);
}
void Switch(String val1, String val2)
{
switch ("bar")
{
// The compiler will not know that val1 and val2 are not distinct
case val1:
// Is this case block selected?
break;
case val2:
// Or this one?
break;
case "bar":
// Or perhaps this one?
break;
}
}
コードは何をするのでしょうか?case ステートメントの順序が変更されたらどうなるでしょうか?実際、C# が switch フォールスルーを違法とした理由の 1 つは、switch ステートメントが恣意的に再配置される可能性があることです。
これらのルールには理由があって設けられています。そのため、プログラマは 1 つの case ブロックを見るだけで、そのブロックに入る正確な条件を確実に知ることができます。前述の switch ステートメントが 100 行以上になる場合 (実際にはそうなるでしょう)、そのような知識は非常に貴重です。
ちなみに、VB は同じ基礎アーキテクチャを持っているため、より柔軟な対応が可能です。 Select Case
ステートメント (上記のコードは VB で動作します) を使用しても、可能な場合には効率的なコードが生成されるため、技術的な制約による引数を慎重に考慮する必要があります。
ほとんどの場合、これらの制限は言語設計者によって設けられています。根底にある正当化は、言語の歴史、理想、またはコンパイラ設計の簡素化との互換性である可能性があります。
コンパイラは次のことを選択する場合があります (実際に選択します)。
- 大きな if-else ステートメントを作成する
- MSILスイッチ命令(ジャンプテーブル)を使用する
- Generic.Dictionaryを作成しますu003Cstring,int32>、最初の使用時にそれを入力し、genyc.dictionary <> :: trygetValue()を呼び出して、インデックスをMSILスイッチ命令に渡す(ジャンプテーブル)
- if-elses&msil "switch"ジャンプの組み合わせを使用する
switch ステートメントは定数時間分岐ではありません。コンパイラーは (ハッシュ バケットなどを使用して) ショートカットを見つける可能性がありますが、より複雑なケースでは、より複雑な MSIL コードが生成され、一部のケースは他のケースよりも先に分岐します。
String の場合を処理するために、コンパイラーは (ある時点で) a.Equals(b) (および場合によっては a.GetHashCode() ) を使用することになります。コンパイラがこれらの制約を満たすオブジェクトを使用することは簡単だと思います。
静的な case 式の必要性については...これらの最適化の一部 (ハッシュ、キャッシュなど) は、case 式が決定的でない場合は利用できません。しかし、コンパイラが単純な if-else-if-else の道を選択することがあるのはすでに見てきました...
編集: ロマックス - 「typeof」演算子の理解が正しくありません。"typeof" 演算子は、型の System.Type オブジェクトを取得するために使用されます (そのスーパータイプやインターフェイスとは何の関係もありません)。オブジェクトと特定の型の実行時の互換性をチェックするのは、「is」演算子の仕事です。ここでオブジェクトを表現するために「typeof」を使用することは無関係です。
この話題について、ジェフ・アトウッド氏は次のように述べています。 switch ステートメントはプログラミングの残虐行為です. 。慎重に使用してください。
多くの場合、テーブルを使用して同じタスクを実行できます。例えば:
var table = new Dictionary<Type, string>()
{
{ typeof(int), "it's an int!" }
{ typeof(string), "it's a string!" }
};
Type someType = typeof(int);
Console.WriteLine(table[someType]);
switch ステートメントが静的解析のみに屈する必要がある理由がわかりません。
確かに、そうではありません 持っている に、そして多くの言語は実際に動的 switch ステートメントを使用します。ただし、これは、「case」句の順序を変更するとコードの動作が変わる可能性があることを意味します。
ここには、「スイッチ」に関する設計上の決定の背後にある興味深い情報がいくつかあります。 C# の switch ステートメントはフォールスルーを許可しないように設計されているのに、ブレークが必要なのはなぜですか?
動的な case 式を許可すると、次の PHP コードのような怪物が発生する可能性があります。
switch (true) {
case a == 5:
...
break;
case b == 10:
...
break;
}
率直に言って、これを使用する必要があります if-else
声明。
マイクロソフトはついにあなたの声を聞きました!
C# 7 では次のことが可能になります。
switch(shape)
{
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
case Rectangle s when (s.Length == s.Height):
WriteLine($"{s.Length} x {s.Height} square");
break;
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
default:
WriteLine("<unknown shape>");
break;
case null:
throw new ArgumentNullException(nameof(shape));
}
これが理由ではありませんが、C# 仕様のセクション 8.7.2 には次のように記載されています。
switch ステートメントの制御タイプは switch 式によって確立されます。switch 式の型が sbyte、byte、short、ushort、int、uint、long、ulong、char、string、または enum-type の場合、それが switch ステートメントの支配型になります。それ以外の場合は、switch 式の型から次の可能な管理型のいずれかへのユーザー定義の暗黙的な変換 (§6.4) が 1 つだけ存在する必要があります。sbyte、byte、short、ushort、int、uint、long、ulong、char、string。このような暗黙的な変換が存在しない場合、またはそのような暗黙的な変換が複数存在する場合には、コンパイル時エラーが発生します。
C# 3.0 仕様は次の場所にあります。http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20仕様.doc
上記のJudahの答えは私にアイデアを与えてくれました。を使用して、上記の OP のスイッチ動作を「偽装」できます。 Dictionary<Type, Func<T>
:
Dictionary<Type, Func<object, string, string>> typeTable = new Dictionary<Type, Func<object, string, string>>();
typeTable.Add(typeof(int), (o, s) =>
{
return string.Format("{0}: {1}", s, o.ToString());
});
これにより、switch ステートメントと同じスタイルで動作を型に関連付けることができます。IL にコンパイルすると、スイッチ スタイルのジャンプ テーブルの代わりにキーが設定されるという追加の利点があると思います。
コンパイラが switch ステートメントを次のように自動的に変換できなかった根本的な理由はないと思います。
if (t == typeof(int))
{
...
}
elseif (t == typeof(string))
{
...
}
...
しかし、それで得られるものはあまりありません。
整数型の case ステートメントを使用すると、コンパイラでさまざまな最適化を行うことができます。
重複はありません (コンパイラーが検出する case ラベルの重複を除く)。あなたの例では、継承により複数の型に一致する可能性があります。最初の一致を実行する必要がありますか?全部ですか?
コンパイラは、すべての比較を回避するために、ジャンプ テーブルによって整数型に対して switch ステートメントを実装することを選択できます。0 ~ 100 の整数値を持つ列挙型をオンにしている場合は、switch ステートメントごとに 1 つずつ、合計 100 個のポインターを含む配列が作成されます。実行時には、オンになっている整数値に基づいて配列からアドレスを検索するだけです。これにより、100 件の比較を実行するよりも実行時のパフォーマンスが大幅に向上します。
によると switch ステートメントのドキュメント オブジェクトを暗黙的に整数型に変換する明確な方法がある場合、それは許可されます。各 case ステートメントが次のように置き換えられる動作を期待していると思います if (t == typeof(int))
, ただし、その演算子に過負荷がかかると、ワームの缶が丸ごと開いてしまいます。== オーバーライドを誤って記述した場合、switch ステートメントの実装の詳細が変更されると、動作が変わります。比較を整数型と文字列、および整数型に減らすことができる (そしてそうするつもりである) ものに減らすことで、潜在的な問題を回避します。
書きました:
「switch ステートメントは、ケースの数に関係なく、定数時間分岐を実行します。」
言語で許可されているため、 弦 switch ステートメントで使用される型 コンパイラはこの型の定数時間分岐実装のコードを生成できないため、if-then スタイルを生成する必要があると思われます。
@mweerden - ああ、なるほど。ありがとう。
私には C# と .NET の経験はあまりありませんが、言語設計者は、限られた状況を除いて型システムへの静的アクセスを許可していないようです。の の種類 キーワードはオブジェクトを返すため、実行時にのみアクセスできます。
Henk は「型システムへの静的アクセスを禁止する」という点でうまくいったと思います。
もう 1 つのオプションは、数値や文字列のように入力順序がないということです。したがって、型スイッチは二分探索ツリーを構築できず、線形探索のみを構築します。
同意する このコメント 多くの場合、テーブル駆動のアプローチを使用する方が優れていることがわかります。
C# 1.0 では、ジェネリックスと匿名デリゲートがなかったため、これは不可能でした。C# の新しいバージョンには、これを機能させるための足場が備わっています。オブジェクト リテラルの表記法があると便利です。
私は C# についてはほとんど知識がありませんが、汎用性を高めることを考えずに、他の言語で行われているように単純に切り替えが行われたか、開発者が C# を拡張する価値がないと判断したのではないかと思います。
厳密に言えば、これらの制限を設ける理由はないというのは全くその通りです。その理由は、許可されたケースでは実装が非常に効率的であるためではないかと疑う人もいるかもしれません (Brian Ensink が示唆しているように (44921))、しかし、実装が非常に効率的であるとは思えません。if ステートメント) 整数といくつかのランダムなケースを使用する場合 (例:345、-4574、1234203)。そして、いずれにせよ、すべて(または少なくともそれ以上)にそれを許可し、特定の場合((ほぼ)連続した数字など)にのみ効果的であると言うことに何の害があるでしょうか。
ただし、lomaxx (44918).
編集:@ヘンク(44970):文字列が最大限に共有されている場合、同じ内容の文字列も同じメモリ位置へのポインタになります。次に、ケースで使用される文字列がメモリに連続して格納されていることを確認できれば、スイッチを非常に効率的に実装できます (つまり、2 つの比較、1 つの加算、2 つのジャンプの順序で実行されます)。