C#switchステートメントの制限-なぜですか?


140

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ステートメント静的分析のみに屈する必要がある理由や、スイッチがオンになっている値が整数(つまり、プリミティブ)でなければならない理由はわかりません。正当化とは何ですか?



組み込み型をオンにする別のオプションは、TypeCode Enumを使用することです。
Erik Philips、

回答:


98

これは私の元の投稿であり、いくつかの議論を引き起こしました... それが間違っているので

switchステートメントは、大きなif-elseステートメントと同じではありません。各ケースは一意であり、静的に評価される必要があります。switchステートメントは、ケースの数に関係なく、一定の時間分岐を行います。if-elseステートメントは、真の条件が見つかるまで各条件を評価します。


実際、C#のswitchステートメントは常に一定の時間の分岐であると限りません

場合によっては、コンパイラはCILスイッチステートメントを使用しますが、これは実際にジャンプテーブルを使用した一定時間の分岐です。ただし、Ivan Hamiltonによって指摘された希薄なケースでは、コンパイラーが何か他のものを完全に生成する場合があります。

これは実際に、さまざまなC#スイッチステートメントを記述し、一部をスパース、一部を高密度にし、ildasm.exeツールを使用して結果のCILを確認することで、非常に簡単に確認できます。


4
他の回答(私のものを含む)に記載されているように、この回答で行われた主張は正しくありません。私は削除をお勧めします(この(おそらく一般的な)誤解の強制を回避する場合のみ)。
mweerden 2008

以下の私の投稿を見てください。私の意見では、switchステートメントが一定の時間分岐を行うことを私が結論的に示しています。
Brian Ensink

ブライアン、返信ありがとうございます。Ivan Hamiltonの返信をご覧ください((48259)[ beta.stackoverflow.com/questions/44905/#48259])。つまり、C#のステートメントとは異なる(CILの)switch 命令について話していますswitch
mweerden 2008

文字列をオンにしたときにコンパイラが一定時間の分岐を生成するとは思わない。
Drew Noakes、

これは、C#7.0のswitch caseステートメントでのパターンマッチングにも適用できますか?
B.ダレンオルソン

114

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スイッチを実行できます。

これは「mays」と「mights」でいっぱいで、コンパイラに依存します(MonoまたはRotorとは異なる場合があります)。

隣接するケースを使用して私のマシンに結果を複製しました:

10ウェイスイッチを実行する合計時間、10000回の反復(ms):25.1383
10ウェイスイッチあたりの概算時間(ms):0.00251383

50ウェイスイッチを実行する合計時間、10000回の反復(ミリ秒):26.593
50ウェイスイッチあたりの概算時間(ミリ秒):0.0026593

5000ウェイスイッチを実行する合計時間、10000回の反復(ms):23.7094
5000ウェイスイッチあたりの概算時間(ms):0.00237094

50000ウェイスイッチを実行する合計時間、10000反復(ミリ秒):
50000ウェイスイッチあたりの概算時間20.0933 (ミリ秒):0.00200933

次に、隣接しないcase式も使用しました。

10ウェイスイッチを実行する合計時間、10000回の反復(ミリ秒):19.6189
10ウェイスイッチあたりの概算時間(ミリ秒):0.00196189

500ウェイスイッチを実行する合計時間、10000回の反復(ミリ秒):19.1664
500ウェイスイッチあたりの概算時間(ミリ秒):0.00191664

5000ウェイスイッチを実行する合計時間、10000回の反復(ミリ秒):
5000ウェイスイッチあたりの概算時間(ms):19.5871:0.00195871

隣接していない50,000ケーススイッチステートメントはコンパイルされません。
「式が長すぎるか複雑すぎて、 'ConsoleApplication1.Program.Main(string [])'の近くでコンパイルできません。

ここで面白いのは、バイナリツリー検索がCILスイッチ命令よりも(おそらく統計的には)少し高速に表示されることです。

ブライアン、あなたは「定数」という言葉を使いました。これは、計算の複雑さの理論の観点から非常に明確な意味を持っています。単純な隣接整数の例では、O(1)(定数)と見なされるCILが生成される可能性がありますが、まばらな例はO(log n)(対数)、クラスター化された例はその中間にあり、小さな例はO(n)(線形)です)。

これは、静的Generic.Dictionary<string,int32>が作成される可能性があるStringの状況にも対応しておらず、最初の使用時に明確なオーバーヘッドが発生します。ここでのパフォーマンスは、のパフォーマンスに依存しGeneric.Dictionaryます。

(CIL仕様ではなく)C#言語仕様を確認すると、「15.7.2 switchステートメント」で「一定の時間」について言及されていないか、基になる実装がCIL switch命令を使用していることもわかります(想定に十分注意してください)そのようなもの)。

結局のところ、現代のシステムでの整数式に対するC#の切り替えはサブマイクロ秒の操作であり、通常は心配する必要はありません。


もちろん、これらの時間はマシンと条件に依存します。私はこれらのタイミングテストに注意を払いません、私たちが話しているマイクロ秒の持続時間は、実行されている「実際の」コードによって小さくなります(そして、いくつかの「実際のコード」を含める必要があります。そうしないと、コンパイラがブランチを最適化します)。システムのジッタ。私の回答は、IL DASMを使用して、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

あなたの実験の結果は私を少し驚かせます。ブライアンと交換しましたか?彼の結果は、あなたのサイズがそうでない間、サイズの増加を示しています。私は何かが足りないのですか?いずれにせよ、明確な返事をありがとう。
mweerden 2008

このような小さな操作でタイミングを正確に計算することは困難です。コードやテスト手順は共有しませんでした。隣接するケースで彼の時間が増える理由がわかりません。鉱山は10倍高速だったので、環境とテストコードは大きく異なる場合があります。
Ivan Hamilton

23

頭に浮かぶ最初の理由は歴史的です:

ほとんどのC、C ++、およびJavaプログラマーは、そのような自由を持つことに慣れていないため、それらを要求しません。

より有効なもう1つの理由は、言語の複雑さが増すことです。

まず、オブジェクトを演算子と比較する.Equals()か、それとも==演算子と比較する必要がありますか?どちらも有効な場合があります。これを行うには新しい構文を導入する必要がありますか?プログラマーが独自の比較方法を導入できるようにする必要がありますか?

さらに、オブジェクトの切り替えを許可すると、switchステートメントに関する基本的な前提破られます。オブジェクトのスイッチオンが許可されている場合、コンパイラーが実施できないswitchステートメントを管理する2つのルールがあります(C#バージョン3.0言語仕様、§8.7.2を参照)。

  • スイッチラベルの値が一定であること
  • スイッチラベルの値が異なること(特定のスイッチ式に対して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;
    }
}

コードは何をしますか?ケースステートメントが並べ替えられた場合はどうなりますか?実際、C#がswitchのフォールスルーを違法にした理由の1つは、switchステートメントが任意に再配置される可能性があることです。

これらのルールは、理由があるために用意されています。プログラマが、1つのケースブロックを調べることで、ブロックが入力される正確な条件を確実に知ることができるようにするためです。前述のswitchステートメントが100行以上になると(そうなると思います)、そのような知識は非常に貴重です。


2
スイッチの順序変更に注意してください。ケースにコードが含まれていない場合、フォールスルーは合法です。例:ケース1:ケース2:Console.WriteLine( "Hi"); ブレーク;
Joel McBeth、2011年

10

ちなみに、同じ基本アーキテクチャを持つVBは、はるかに柔軟なSelect Caseステートメントを可能にし(上記のコードはVBで機能します)、これが可能な場合でも効率的なコードを生成するため、技術的な制約による引数を慎重に検討する必要があります。


1
Select CaseエンVBは非常に柔軟で、スーパー時間の節約です。とても恋しいです。
Eduardo Molteni

@EduardoMolteni F#に切り替えます。PascalとVBのスイッチは、比較するとばか子のように見えます。
Luaan

10

ほとんどの場合、これらの制限は言語デザイナーのために実施されています。根本的な理由は、言語の歴史との互換性、理想、またはコンパイラー設計の単純化かもしれません。

コンパイラーは以下を選択できます(実際に選択します)。

  • 大きなif-elseステートメントを作成する
  • MSIL切り替え命令を使用する(ジャンプテーブル)
  • Generic.Dictionary <string、int32>を作成し、最初の使用時にデータを入力し、Generic.Dictionary <> :: TryGetValue()を呼び出して、インデックスがMSILスイッチ命令(ジャンプテーブル)に渡されるようにします
  • if-elsesとMSIL "スイッチ"ジャンプの組み合わせを使用する

switchステートメントは、一定時間の分岐ではありません。コンパイラーは(ハッシュバケットなどを使用して)ショートカットを見つけることがありますが、より複雑なケースでは、より複雑なMSILコードが生成され、他のケースよりも早く分岐する場合があります。

Stringのケースを処理するために、コンパイラーは(ある時点で)a.Equals(b)(および場合によってはa.GetHashCode())を使用します。コンパイラがこれらの制約を満たすオブジェクトを使用するのは簡単なことだと思います。

静的なcase式の必要性については... case式が確定的でない場合、それらの最適化(ハッシュ、キャッシングなど)の一部は利用できません。しかし、コンパイラーが単純化したif-else-if-elseの道をとにかく選ぶこともあるのはすでに見てきました...

編集:lomaxx- "typeof"演算子の理解が正しくありません。"typeof"演算子は、型のSystem.Typeオブジェクトを取得するために使用されます(スーパータイプやインターフェイスとは関係ありません)。特定のタイプのオブジェクトの実行時の互換性をチェックすることは、「is」演算子の仕事です。ここでの "typeof"の使用によるオブジェクトの表現は重要ではありません。


6

ジェフ・アトウッドによれば、このトピックについての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]);

7
証拠のない誰かのTwitter投稿を真剣に引用していますか?少なくとも信頼できるソースにリンクします。
Ivan Hamilton、

4
信頼できるソースからのものです。問題のTwitter投稿は、あなたが見ているサイトの作者であるJeff Atwoodからのものです。:-)興味があれば、ジェフはこのトピックに関するブログ投稿をいくつか持っています。
ユダガブリエルヒマンゴ2009年

ジェフ・アトウッドが書いたかどうかに関係なく、それは完全なBSだと思います。switchステートメントがステートマシンの処理にどの程度適しているか、そしてenum型の値に基づいてコードフローを変更する他の例はおかしいです。また、enum型の変数をオンに切り替えると、intellisenseが自動的にswitchステートメントに値を入力することも偶然ではありません。
Jonathon Reinhart、2012年

@JonathonReinhartええ、それがポイントだと思いますswitch。ステートメントを使用するよりも、ポリモーフィックコードを処理するためのより良い方法があります。彼は状態マシンを書くべきではないと言っているのではなく、素敵な特定の型を使用することで同じことができるというだけです。もちろん、非常に複雑な状態を簡単にカバーできる型を持つF#などの言語では、これははるかに簡単です。たとえば、状態が型の一部になる弁別ユニオンを使用してswitch、パターンマッチングでを置き換えることができます。または、たとえば、インターフェースを使用します。
Luaan

古い回答/質問ですが、(間違っている場合は修正してください)Dictionary最適化されたswitchステートメントよりもかなり遅くなると思いました...?
ポール、

6

switchステートメントが静的解析のみに影響を与える必要がある理由はわかりません

確かに、そうする必要はありません。実際、多くの言語では動的切り替えステートメントを使用しています。ただし、これは、「case」句の順序を変更すると、コードの動作が変わる可能性があることを意味します。

ここで "switch"に入った設計決定の背後にいくつかの興味深い情報があります。なぜC#のswitchステートメントはフォールスルーを許可しないように設計されていますが、それでもブレークが必要なのですか?

動的なケース式を許可すると、次のPHPコードのような怪物につながる可能性があります。

switch (true) {
    case a == 5:
        ...
        break;
    case b == 10:
        ...
        break;
}

率直に言って、if-elseステートメントを使用する必要があります。


1
私がPHPについて気に入っているのは(今はC#に移行しているため)、それは自由です。これにより、不正なコードを作成する自由が
生まれ

5

ついにマイクロソフトはあなたの言うことを聞きました!

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

3

これは理由ではありませんが、C#仕様セクション8.7.2では次のように述べられています。

switchステートメントの管理タイプは、switch式によって確立されます。スイッチ式のタイプがsbyte、byte、short、ushort、int、uint、long、ulong、char、string、またはenum-typeの場合、それがswitchステートメントの管理タイプです。それ以外の場合、スイッチ式のタイプから、sbyte、byte、short、ushort、int、uint、long、ulong、char、stringの可能な管理タイプのいずれかに、ユーザー定義の暗黙の変換(6.4)が1つだけ存在する必要があります。 。そのような暗黙の変換が存在しない場合、またはそのような暗黙の変換が複数存在する場合、コンパイル時エラーが発生します。

C#3.0の仕様は次の場所にあります:http : //download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc


3

上記のユダの答えは私にアイデアを与えました。上記を使用して、上記の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にコンパイルすると、スイッチスタイルのジャンプテーブルの代わりにキー設定されるという追加の利点があると思います。


0

コンパイラーがswitchステートメントを自動的に次のように変換できなかった根本的な理由はないと思います。

if (t == typeof(int))
{
...
}
elseif (t == typeof(string))
{
...
}
...

しかし、それによって得られるものはあまりありません。

整数型のcaseステートメントにより、コンパイラーはいくつかの最適化を行うことができます。

  1. 重複はありません(コンパイラが検出するケースラベルを複製しない限り)。あなたの例では、tは継承のために複数の型に一致する可能性があります。最初の一致を実行する必要がありますか?それらすべて?

  2. コンパイラーは、すべての比較を回避するために、ジャンプテーブルによって整数型に対してswitchステートメントを実装することを選択できます。0から100までの整数値を持つ列挙型を切り替える場合は、switchステートメントごとに1つずつ、100個のポインターを持つ配列を作成します。実行時に、スイッチがオンになっている整数値に基づいて配列からアドレスを検索するだけです。これにより、100回の比較を実行するよりも実行時のパフォーマンスが大幅に向上します。


1
ここで注意すべき重要な複雑さがあることであるNETのメモリモデルは、(架空の、無効にあなたの擬似コードは正確に同じではない作る特定の強力な保証を持っているC# switch (t) { case typeof(int): ... }あなたの翻訳は、その変数を意味しているためにt しなければならない場合は二回、メモリからフェッチされt != typeof(int)、後者だろう一方で、 (推定的に)常にt 1回だけの値を読み取ります。この違いは、これらの優れた保証に依存する並行コードの正確性を損なう可能性があります。この詳細については、Windowsで
Glenn Slayden 2017

0

switchステートメントのドキュメントによれば、オブジェクトを暗黙的に整数型に変換する明確な方法がある場合、それは許可されます。各case文でに置き換えられる動作を期待していると思いますが、if (t == typeof(int))その演算子をオーバーロードすると、ワームの缶全体が開かれます。==オーバーライドを誤って記述した場合、switchステートメントの実装の詳細が変更されると、動作が変更されます。整数型や文字列との比較を減らし、整数型に減らすことができる(そして意図されている)ことにより、潜在的な問題を回避します。


0

書きました:

「switchステートメントは、ケースの数に関係なく、一定の時間分岐を行います。」

言語は文字列型をswitchステートメントで使用できるので、コンパイラーはこの型の定数時間ブランチ実装のコードを生成できず、if-thenスタイルを生成する必要があると思います。

@mweerden-なるほど。ありがとう。

C#と.NETの経験はあまりありませんが、言語設計者は狭い状況を除いて型システムへの静的アクセスを許可していないようです。typeofをキーワードに戻りオブジェクトこれは、実行時にのみアクセスできるように。


0

ヘンクは「型システムへの静的アクセスがない」ことでそれを釘付けにしたと思います

もう1つのオプションは、数値や文字列の場合と同じようにタイプする順序がないことです。したがって、型の切り替えでは、バイナリ検索ツリーを作成できません。線形検索のみです。


0

多くの場合、テーブル駆動アプローチを使用する方が良いというこのコメントに同意します。

C#1.0では、ジェネリックと匿名のデリゲートがないため、これは不可能でした。C#の新しいバージョンには、これを機能させるための足場があります。オブジェクトリテラルの表記があることも役立ちます。


0

C#についてはほとんど知識がありませんが、どちらか一方のスイッチが他の言語で発生するため、より一般的にすることを考えずに単純に取られたか、開発者が拡張する価値はないと判断しました。

厳密に言うと、これらの制限を課す理由がないということは絶対に正しいことです。その理由は、許可されたケースでは実装が非常に効率的である(Brian Ensink(44921)によって示唆された)のではないかと思われるかもしれませんが、整数といくつかのランダムなケースを使用すると、実装が非常に効率的であると疑います(if-statements) (例:345、-4574、1234203)。そして、いずれにせよ、それをすべて(または少なくともそれ以上)許可し、特定のケース((ほぼ)連続した数値など)に対してのみ効率的であると言うことの害は何ですか。

ただし、lomaxx(44918)で指定された理由などの理由により、型を除外したい場合があることは想像できます。

編集:@Henk(44970):文字列が最大限に共有されている場合、同じ内容の文字列は同じメモリ位置へのポインタにもなります。次に、ケースで使用される文字列がメモリに連続して格納されることを確認できれば、スイッチを非常に効率的に実装できます(つまり、2つの比較、加算、2つのジャンプの順序で実行)。


0

C#8では、スイッチ式を使用してこの問題をエレガントかつコンパクトに解決できます。

public string GetTypeName(object obj)
{
    return obj switch
    {
        int i => "Int32",
        string s => "String",
        { } => "Unknown",
        _ => throw new ArgumentNullException(nameof(obj))
    };
}

その結果、以下が得られます。

Console.WriteLine(GetTypeName(obj: 1));           // Int32
Console.WriteLine(GetTypeName(obj: "string"));    // String
Console.WriteLine(GetTypeName(obj: 1.2));         // Unknown
Console.WriteLine(GetTypeName(obj: null));        // System.ArgumentNullException

新機能の詳細については、こちらをご覧ください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.