大阪市中央区 システムソフトウェア開発会社

営業時間:平日09:15〜18:15
MENU

C#開発における落とし穴~switch編~

株式会社クローバーフィールドの経営理念
著者:大林真也
公開日:2020/06/23
最終更新日:2020/06/23
カテゴリー:技術情報

ごきげんよう、皆様。

技術担当の大林です。

 

前回に引き続き、C#の開発における落とし穴、所謂うっかりやらかしたり勘違いしやすい部分について少し書こうと思います。

 

※当記事はVisualStudioでの開発を想定して執筆しております。
開発環境次第では当てはまらない事もある事を、あらかじめご了承ください。

 

さて、今回はswitch文について少し触れようと思います。

 

基本的に、数値の比較であればswitch文はコンパイラの方でジャンプテーブルを作成して上手いこと高速に判定してくれる処理になるようになっています。

ただ、ここで少し気になる事が出てきます。

 

そう、実はC#のswitch文では、通常の数値比較の他に、型比較も行う事が出来るのです。

型の判定やキャスト等、結構重い処理のはずなのですが、こういった型比較の方でも高速で判定出来るようになっているのでしょうか……?

 

気になったので、下記のようなコードを作成しました。

単純に、継承だけを行ったクラスを使って、速度を図ろうという試みです。

 

public class TestObjBase
{
}
public class TestObjA : TestObjBase
{
}
public class TestObjB : TestObjBase
{
}
public class TestObjC : TestObjBase
{
}
public class TestObjD : TestObjBase
{
}
var list = new List();

//検証用にクラス作成
for (int i = 0; i < 1000000; i++)
{
  list.Add(new TestObjA());
}

TestSwitch();

void TestSwitch()
{
  string testName = "TestSwitch";
  //コスト検証
  var sw = new Stopwatch();
  sw.Start();
  foreach (var obj in list)
  {
    switch (obj)
    {
      case TestObjA objA:
        break;
      case TestObjB objB:
        break;
      case TestObjC objC:
        break;
      case TestObjD objD:
        break;
      default:
        break;
    }
  }
  sw.Stop();
  Console.WriteLine(string.Format("{0}:{1}msec", testName, sw.Elapsed.TotalMilliseconds));
}

 

結果は

TestSwitch:6.758msec

 

さて、今度は後の方にあるcaseで比較している型に少し差し替えてみましょう。

 

var list = new List();

//検証用にクラス作成
for (int i = 0; i < 1000000; i++)
{
//  list.Add(new TestObjA()); 
  list.Add(new TestObjD());
}

TestIf();
TestSwitch();

 

結果は

TestSwitch:19.8408msec

 

目に見えて遅くなっていますね。

それもそのはず、型をキャスト可能かどうか判別するような複雑な処理においては、ジャンプテーブルを作成して高速化することなんて出来ないので、実質全てif~elseif~else文で判定しているのと同じことをやらないと行けないわけです。

ちなみにこちら、caseの順番を下のように書き換えてあげると、TestObjAで比較した時と同等の実行時間になります。

 

void TestSwitch()
{
  string testName = "TestSwitch";
  //コスト検証
  var sw = new Stopwatch();
  sw.Start();
  foreach (var obj in list)
  {
    switch (obj)
    {
      case TestObjD objD:
        break;
      case TestObjB objB:
        break;
      case TestObjC objC:
        break;
      case TestObjA objA:
        break;
      default:
        break;
    }
  }
  sw.Stop();
  Console.WriteLine(string.Format("{0}:{1}msec", testName, sw.Elapsed.TotalMilliseconds));
}

 

おまけで、if文で同じような処理を記載したものも記載しておきます。

 

var list = new List();

//検証用にクラス作成
for (int i = 0; i < 1000000; i++)
{
  list.Add(new TestObjD());
}

TestIf();
TestSwitch();
void TestIf()
{
  string testName = "TestIf";
  //コスト検証
  var sw = new Stopwatch();
  sw.Start();
  foreach (var obj in list)
  {
    if(obj is TestObjA objA)
    {
    }
    else if (obj is TestObjA objB)
    {
    }
    else if (obj is TestObjA objC)
    {
    }
    else if (obj is TestObjA objD)
    {
    }
    else
    {
    }
  }
  sw.Stop();
  Console.WriteLine(string.Format("{0}:{1}msec", testName, sw.Elapsed.TotalMilliseconds));
}

void TestSwitch()
{
  string testName = "TestSwitch";
  //コスト検証
  var sw = new Stopwatch();
  sw.Start();
  foreach (var obj in list)
  {
    switch (obj)
    {
      case TestObjA objA:
        break;
      case TestObjB objB:
        break;
      case TestObjC objC:
        break;
      case TestObjD objD:
        break;
      default:
        break;
    }
  }
  sw.Stop();
  Console.WriteLine(string.Format("{0}:{1}msec", testName, sw.Elapsed.TotalMilliseconds));
}

 

結果は

TestIf:17.5013msec
TestSwitch:18.9551msec

 

誤差はあれど、同等の実行時間になりましたね。

ちなみにTestObjAの方で実行しても、同等の6~7msec程度の実行時間になります。

 

また、全て該当せずdefaultに行き着く場合、一番末尾のcaseと一致した時同様、最遅の結果になってしまいます。

やむを得ない場合は仕方ないですが、可能なら型判定用の整数値を持たせるなり、不要なクラス等を型比較の処理を通さないようにしたりと、上手く工夫する必要があります。

 

switch文=ジャンプテーブル使ってるから記述の順番気にしなくて良いやとか、とりあえずあれもこれもcaseに入れとこうみたいなノリで使ってしまうと、とても遅いコードを作ってしまう事になるので要注意です。

 

では!

 

見ていただき、ありがとうございました!

 

    上に戻る