148

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

4

17 に答える 17

117

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 回の反復 (ms) : 25.1383
10 方向の切り替えあたりのおおよその時間 (ms) : 0.00251383

50 方向の切り替えを実行する合計時間、10000 回の反復 (ms) : 26.593
50 方向の切り替えあたりのおおよその時間 (ms) : 0.0026593

5000 通りの切り替え、10000 回の反復を実行する合計時間 (ms) : 23.7094
5000 通りの切り替えあたりのおおよその時間 (ms) : 0.00237094

50000 通りの切り替えを実行する合計時間、10000 回の反復 (ms) : 20.0933
50000 通りの切り替えあたりのおおよその時間 (ms) : 0.00200933

次に、隣接しないケース式も使用しました。

10 方向の切り替えを実行する合計時間、10000 回の反復 (ms) : 19.6189
10 方向の切り替えあたりのおおよその時間 (ms) : 0.00196189

500 通りの切り替えを実行する合計時間、10000 回の反復 (ms) : 19.1664
500 通りの切り替えあたりのおおよその時間 (ms) : 0.00191664

5000 通りの切り替え、10000 回の反復を実行する合計時間 (ms) : 19.5871
5000 通りの切り替えあたりのおおよその時間 (ms) : 0.00195871

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

ここで面白いのは、二分木検索が CIL switch 命令よりも少し (おそらく統計的にではなく) 速く表示されることです。

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

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

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

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


もちろん、これらの時間はマシンと条件によって異なります。これらのタイミング テストには注意を払いません。ここで話しているマイクロ秒の期間は、実行中の「実際の」コードよりも小さくなります (「実際のコード」を含める必要があります。そうしないと、コンパイラが分岐を最適化してしまいます)。システムのジッタ。私の答えは、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
于 2008-09-07T08:47:03.857 に答える
104

これは私の最初の投稿であり、いくつかの議論を巻き起こしました...間違っているためです:

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


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

場合によっては、コンパイラは CIL switch ステートメントを使用しますが、これは実際にはジャンプ テーブルを使用した一定時間の分岐です。ただし、 Ivan Hamiltonによって指摘されたようなまばらなケースでは、コンパイラはまったく別のものを生成する可能性があります。

これは、さまざまな C# の switch ステートメント (スパース、デンスなど) を記述し、ildasm.exe ツールを使用して結果の CIL を確認することで、実際には非常に簡単に確認できます。

于 2008-09-04T22:51:03.980 に答える
24

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

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

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

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

さらに、オブジェクトの切り替えを許可すると、switch ステートメントに関する根本的な前提が崩れます。オブジェクトをオンにすることが許可されている場合、コンパイラが強制できない switch ステートメントを制御する 2 つの規則があります ( C# バージョン 3.0 言語仕様、§8.7.2 を参照)。

  • スイッチ ラベルの値が定数であること
  • switch ラベルの値が異なること (特定の switch-expression に対して 1 つの switch ブロックのみを選択できるようにするため)

非定数の大文字と小文字の値が許可されたという仮定のケースで、次のコード例を検討してください。

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 行以上になると (そしてそれ以上になるでしょう)、そのような知識は非常に貴重です。

于 2008-09-05T13:11:22.020 に答える
10

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

于 2008-09-05T11:49:40.453 に答える
10

ほとんどの場合、これらの制限は言語の設計者によって設定されています。根底にある正当化は、言語の歴史、理想、またはコンパイラ設計の簡素化との互換性である可能性があります。

コンパイラは、次のことを選択する場合があります (実際に選択します)。

  • 大きな if-else ステートメントを作成する
  • MSIL スイッチ命令 (ジャンプ テーブル) を使用する
  • Generic.Dictionary<string,int32> を構築し、最初の使用時にそれを設定し、Generic.Dictionary<>::TryGetValue() を呼び出してインデックスを MSIL スイッチ命令 (ジャンプ テーブル) に渡します。
  • if-else と MSIL "switch" ジャンプの組み合わせを使用する

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

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

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

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

于 2008-09-05T11:33:44.737 に答える
6

トピックについては、Jeff Atwoodによると、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]);
于 2008-09-04T23:06:06.780 に答える
6

switch ステートメントが静的解析のみに屈しなければならない理由がわかりません

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

ここで「切り替え」に踏み切った設計上の決定の背後にある興味深い情報があります。

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

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

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

于 2009-06-04T20:40:15.803 に答える
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 にコンパイルすると、スイッチ スタイルのジャンプ テーブルの代わりにキーが設定されるという追加の利点があると思います。

于 2010-03-07T16:27:38.307 に答える
3

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

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

C# 3.0 仕様は次の場所にあり ます。

于 2008-09-04T22:54:32.800 に答える
0

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

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

于 2008-09-05T04:46:12.220 に答える
0

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

于 2008-09-04T23:01:59.990 に答える
0

私はC#の知識をほとんど持っていませんが、他の言語で行われているように、より一般的にすることを考えずに切り替えただけか、開発者がそれを拡張する価値がないと判断したのではないかと思います。

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

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

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

于 2008-09-04T23:16:22.933 に答える
0

書きました:

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

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

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

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

于 2008-09-04T23:28:34.857 に答える
0

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

別のオプションは、数値と文字列が可能な場合、型に順序がないことです。したがって、型スイッチはバイナリ検索ツリーを構築できず、線形検索のみになります。

于 2008-09-05T03:30:30.657 に答える
0

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

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

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

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

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

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

于 2008-09-04T22:56:26.900 に答える
0

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

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

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

于 2020-02-01T09:18:33.557 に答える