switchステートメントの拡張

C# 7では、switchステートメントのcase句に、値だけでなく、型を書けるようになりました。 型による条件の書き方は、前節のis演算子と同様です。 また、型による条件に加えて、when句というものを付けて追加の条件式を書くこともできます。

switch(変数)
{
    case  変数:
        // 型が一致しているときにここに来る
        // その型に変換した結果が変数に入っている
        break;
    case  変数 when 条件式:
        // 型が一致していて、かつ、条件式満たしているときにここに来る
        break;
    case :
        // 通常の値による条件との混在も可能
        break;
      ・
      ・
      ・
    default:
        // どの条件も満たさない時に実行される
        break;
}

例えば以下のような書き方ができます。

static void F(object obj)
{
    switch (obj)
    {
        case string s:
            Console.WriteLine("string #" + s.Length);
            break;
        case 7:
            Console.WriteLine("7の時だけここに来る");
            break;
        case int n when n > 0:
            Console.WriteLine("正の数の時にここに来る " + n);
            // ただし、上から順に判定するので、7 の時には来なくなる
            break;
        case int n:
            Console.WriteLine("整数の時にここに来る" + n);
            // 同上、0 以下の時にしか来ない
            break;
        default:
            Console.WriteLine("その他");
            break;
    }
}

上から逐次判定

C# 6までの、値による分岐しかなかったswitchステートメントとはちょっと違う部分があります。 以下の点に気を付けてください。

  • 条件の範囲が被る場合がある
    • 値による分岐の場合は、各 case がそれぞれ排他だった
    • 型による分岐が入ったことで、上記の例でいう 7intかつ正の数 ⊃ int のように、被りが起こり得る
  • 条件は上から順に判定する
    • 被りがない場合なら順序を気にする必要はなかった
      • なので、「ジャンプ テーブル化」(後述)という最適化手法が使えていた
    • 型による分岐を1つでも含むと、この前提が崩れて、ジャンプ テーブル化できない(逐次判定しかしない)

ジャンプ テーブル化の説明のために、以下のようなswitchを考えましょう。

switch(n)
{
    case 0: return "zero";
    case 1: return "one";
    case 2: return "two";
    case 3: return "three";
    case 4: return "four";
    case 5: return "five";
    case 6: return "six";
    case 7: return "seven";
    case 8: return "eight";
    case 9: return "nine";
    default: return "other";
}

こういうswitchであれば、以下のように、辞書を引いて結果を得ることもできるはずです。

var map = new Dictionary<int, string>
{
    { 0, "zero" },
    { 1, "one" },
    { 2, "two" },
    { 3, "three" },
    { 4, "four" },
    { 5, "five" },
    { 6, "six" },
    { 7, "seven" },
    { 8, "eight" },
    { 9, "nine" },
};

string s;
if (map.TryGetValue(n, out s)) return s;
else return "other";

caseの個数が少ないうちは普通に上から順に等値判定していく方が軽いんですが、 case数が増えれば増えるほど、辞書化した方が有利になります。

そこで、C# のswitchステートメント(というか、.NETの中間言語のswitch命令)では、caseの数が多い場合にこういう辞書を使った最適化を行うようになっています。 正確にいうと、辞書の値は条件分岐によるジャンプ先が入っていて、goto的な命令との組み合わせで実現されます。 そこで、「ジャンプ先のテーブルを引く」という意味で「ジャンプ テーブル化」と呼ばれます。

繰り返しになりますが、caseに型による条件を書いてしまうと、こういうジャンプ テーブル化ができなくなります。 というより、コンパイル結果的にはswitch命令が使えず、if-elseを繰り返すようなコードにコンパイルされます。 上から順に逐次判定になるので、case数があまりにも多いと実行性能的にあまりよくないので注意してください。

また、上の方のcaseにあるほど判定が速いことになります。 以下のように、一番上のcaseと一番下のcaseでは、かなりパフォーマンスに差が出ます。 (なので、パフォーマンスが気になるなら、発生頻度が高いものほど上の方に書く必要があります。)

using System;
using System.Diagnostics;

class Program
{
    static void Main()
    {
        var sw = new Stopwatch();

        // bool 型は一番先頭 = 速い
        object t = true;
        sw.Start();
        for (int i = 0; i < 100000; i++) TypeSwitch(t);
        sw.Stop();
        Console.WriteLine("bool   " + sw.Elapsed); // かなり速いはず

        // double 型は一番末尾 = 遅い
        object d = 1.1;
        sw.Restart();
        for (int i = 0; i < 100000; i++) TypeSwitch(d);
        sw.Stop();
        Console.WriteLine("string " + sw.Elapsed); // 手元の環境では5倍くらい遅かった

        // どの case にもない型。default 句に行く
        var s = DateTime.UtcNow;
        sw.Restart();
        for (int i = 0; i < 100000; i++) TypeSwitch(s);
        sw.Stop();
        Console.WriteLine("string " + sw.Elapsed); // 一番最後まで判定するので遅い
    }

    static int TypeSwitch(object x)
    {
        switch (x)
        {
            default: return -1; // ちなみに、default 句はどこに書こうと必ず一番最後
            case bool _: return 0; // 前から順に判定ということは、bool の時が一番早い
            case sbyte _: return 1;
            case byte _: return 2;
            case short _: return 3;
            case ushort _: return 4;
            case int _: return 5;
            case uint _: return 6;
            case long _: return 7;
            case ulong _: return 8;
            case float _: return 9;
            case double _: return 10; // 逆に double の時は凄く遅い
        }
    }
}

ちなみに、この例でも書いてありますが、逐次判定になっていたとしてもdefault句にたどり着くのは必ず一番最後です。

更新履歴

ブログ