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
がそれぞれ排他だった - 型による分岐が入ったことで、上記の例でいう
7
⊃int
かつ正の数 ⊃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
句にたどり着くのは必ず一番最後です。