初めに
プログラマの皆さんのほとんどはif文やswitch文のあるプログラミング言語を最低一つは使っていると思います。
ifとswitchのどちらを使うか迷ったことはありませんか?
この記事はそういう時switchが使えるならswitchを使いましょうという内容になります。
(あくまで個人の見解です)
この記事はサンプルにC#を使いますが、ifとswitchに焦点を置いているのでC#がわからなくとも理解できる内容だと思います。
本記事で伝えたいことは以下の通りです。
範囲を列挙するような箇所では積極的にswitchを使いましょう。
switchを使うべき理由
- 可読性の観点からswitchが使えるときにはswitchを使うべき
- 4case以上ではコンパイラの最適化によりif文より高速に処理される
それではswitch文の魅力を詳しく掘り下げてみましょう!
可読性について
if文で実現できるならそっちのがシンプルじゃね?
switchなんて余計な機能を覚えなくていいし統一いいんじゃないかと、
そう思われる方も少なくないでしょう。
switch文のいいところの一つに可読性があげられます。
まずは比較を見てみましょう。
if文
int IfStatement(string x) {
if (x is "aaa") {
return 0;
}
else if (x is "bbb") {
return 1;
}
else if (x is "ccc") {
return 2;
}
else if (x is "ddd") {
return 3;
}
else if (x is "eee") {
return 4;
}
else if (x is "fff") {
return 5;
}
else {
return -1;
}
}
switch文
int SwitchStatement(string x) {
switch (x) {
case "aaa": return 0;
case "bbb": return 1;
case "ccc": return 2;
case "ddd": return 3;
case "eee": return 4;
case "fff": return 5;
default: return -1;
}
}
パターンマッチ(存在する言語のみ)
C#の場合returnしかないswitch文はこのように表現できます。
このような形式はパターンマッチと呼ばれJavaやPHP、
その他多くの関数型言語にも同等の機能が存在します。
int SwitchExpression(string x) {
return x switch {
"aaa" => 0,
"bbb" => 1,
"ccc" => 2,
"ddd" => 3,
"eee" => 4,
"fff" => 5,
_ => -1
};
}
いかがでしょう。
なんとなくswitchのほうが読みやすくないですか?
ifとelse ifを並べる方法のデメリットとして、その他の条件分岐を含めることができるため予測しづらいというのもあります。
例えばこのような場合。
int IfStatement(string x) {
if (x is "aaa") {
return 0;
}
else if (x is "bbb") {
return 1;
}
else if (x is "ccc") {
return 2;
}
else if (x is "ddd" || x is "eee") {
return 3;
}
else if (x is "eee") {
return 4;
}
else {
return -1;
}
}
x is "ddd" || x is "eee"
のところで、別の条件が混入していることに気づかない場合があります。これが数十個の条件の中に混入しておくと見落とす可能性があります。
switchだと、条件が1つだという保証ができるので、可読性がif文よりはいいように思います。
したがって範囲を列挙するような箇所では積極的にswitchで書いてしまうのが良さそうですよね。
C#の場合はswitchでもwhenを使って条件分岐を行うことができますが、それでもif文よりは読み手に意図を伝えやすいと思います。
int SwitchStatement(string x) {
switch (x) {
case "aaa": return 0;
case "bbb": return 1;
case "ccc": return 2;
case "ddd" when x is "eee": return 3;
case "eee": return 4;
default: return -1;
}
}
パフォーマンス的な話
可読性については、人による感じ方があるでしょうから腑に落ちていない人もいると思います。
しかし、適切な場所でswitchを利用すると多くの場合はif文より高速に処理されます。
結論から言うと量が少ない場合はifのほうが高速だが、4case以上ではswitchのほうが高速です。
その理由を説明したいと思います。
これはC#の場合はコンパイラによる最適化が行われるからですが、多くのほかの言語でも同様の最適化が行われていると思います。
caseにあたる部分が文字列の場合は、このように展開されます。
コンパイル前
int SwitchExpression(string x) {
return x switch {
"aaa" => 0,
"bbb" => 1,
"ccc" => 2,
"ddd" => 3,
"eee" => 4,
"fff" => 5,
_ => -1
};
}
コンパイル後
internal static int <<Main>$>g__SwitchExpression|0_0(string x)
{
if (!(x == "aaa"))
{
if (!(x == "bbb"))
{
if (!(x == "ccc"))
{
if (!(x == "ddd"))
{
if (!(x == "eee"))
{
if (x == "fff")
{
return 5;
}
return -1;
}
return 4;
}
return 3;
}
return 2;
}
return 1;
}
return 0;
}
結構アグレッシブな変換が行われていますね。
二分探索になることで if文が実行される回数がlog2(case数)回程度になります。
さらにcaseの数がふえると
caseの数が増えるこのようになります。
長くなるので折りたたみます
[System.Runtime.CompilerServices.NullableContext(1)]
[CompilerGenerated]
internal static int << Main >$> g__SwitchStatement | 0_0(string x){
uint num = < PrivateImplementationDetails >.ComputeStringHash(x);
if (num <= 2422201537u) {
if (num <= 1268133694) {
if (num <= 876991330) {
if (num != 297093209) {
if (num != 313303468) {
if (num == 876991330 && x == "aaa") {
return 0;
}
}
else if (x == "www") {
return 22;
}
}
else if (x == "nnn") {
return 13;
}
}
else if (num != 911839723) {
if (num != 954117621) {
if (num == 1268133694 && x == "uuu") {
return 20;
}
}
else if (x == "rrr") {
return 17;
}
}
else if (x == "lll") {
return 11;
}
}
else if (num <= 1620362378) {
if (num != 1426598844) {
if (num != 1488952485) {
if (num == 1620362378 && x == "yyy") {
return 24;
}
}
else if (x == "bbb") {
return 1;
}
}
else if (x == "ggg") {
return 6;
}
}
else if (num <= 1911768552) {
if (num != 1803512071) {
if (num == 1911768552 && x == "kkk") {
return 10;
}
}
else if (x == "ppp") {
return 15;
}
}
else if (num != 2339852366u) {
if (num == 2422201537u && x == "vvv") {
return 21;
}
}
else if (x == "eee") {
return 4;
}
}
else if (num <= 3275687728u) {
if (num <= 2771066003u) {
if (num != 2733657754u) {
if (num != 2754164196u) {
if (num == 2771066003u && x == "ttt") {
return 19;
}
}
else if (x == "ooo") {
return 14;
}
}
else if (x == "iii") {
return 8;
}
}
else if (num != 2813343901u) {
if (num != 3003732817u) {
if (num == 3275687728u && x == "sss") {
return 18;
}
}
else if (x == "fff") {
return 5;
}
}
else if (x == "zzz") {
return 25;
}
}
else if (num <= 3815745472u) {
if (num != 3399078982u) {
if (num != 3433313295u) {
if (num == 3815745472u && x == "ccc") {
return 2;
}
}
else if (x == "xxx") {
return 23;
}
}
else if (x == "mmm") {
return 12;
}
}
else if (num <= 3890285453u) {
if (num != 3848007555u) {
if (num == 3890285453u && x == "jjj") {
return 9;
}
}
else if (x == "ddd") {
return 3;
}
}
else if (num != 3973371039u) {
if (num == 4058663250u && x == "qqq") {
return 16;
}
}
else if (x == "hhh") {
return 7;
}
return -1;
}
[CompilerGenerated]
internal sealed class <PrivateImplementationDetails>
{
internal static uint ComputeStringHash(string s)
{
uint num = default(uint);
if (s != null)
{
num = 2166136261u;
int num2 = 0;
while (num2 < s.Length)
{
num = (s[num2] ^ num) * 16777619;
num2++;
}
}
return num;
}
}
もう原型がないですよね...
<PrivateImplementationDetails>.ComputeStringHash
によって文字列のハッシュコードが計算され、そこから二部探索的に目的のコード部分を探り当てるコードに変換されます。
数値の場合も同様な展開が行われ最適化されます。
速度計測
実際に速度を計測してみました。
検証マシン
- CPU Intel Core i9 12700K
- Memory DDR4 3200MHz
- OS Windows 11
aaa、bbb、ccc ... zzzのまでの配列を作る。
配列の要素数回だて関数呼び出す処理を10000000回繰り返す。
var texts = new []{ "aaa", "bbb", ..., "zzz" };
for (int i = 0; i < 10000000; i++) {
foreach (var text in texts) {
_ = IfOrSwitch(text);
}
}
計測に利用したコード
using System.Diagnostics;
var texts = GenerateAAA2ZZZ().ToArray();
DoSwitch();
DoIf();
void DoSwitch() {
var sw = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++) {
foreach (var text in texts) {
_ = SwitchStatement(text);
}
}
sw.Stop();
Console.WriteLine($"switch: {sw.ElapsedMilliseconds}ms");
}
void DoIf() {
var sw = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++) {
foreach (var text in texts) {
_ = IfStatement(text);
}
}
sw.Stop();
Console.WriteLine($"if: {sw.ElapsedMilliseconds}ms");
}
IEnumerable<string> GenerateAAA2ZZZ() {
for (int i = 97; i <= 122; i++) {
var c = (char)i;
yield return new string(new char[] { c, c, c });
}
}
int IfStatement(string x) {
if (x is "aaa") {
return 0;
}
else if (x is "bbb") {
return 1;
}
else if (x is "ccc") {
return 2;
}
else if (x is "ddd") {
return 3;
}
else if (x is "eee") {
return 4;
}
else if (x is "fff") {
return 5;
}
else if (x is "ggg") {
return 6;
}
else if (x is "hhh") {
return 7;
}
else if (x is "iii") {
return 8;
}
else if (x is "jjj") {
return 9;
}
else if (x is "kkk") {
return 10;
}
else if (x is "lll") {
return 11;
}
else if (x is "mmm") {
return 12;
}
else if (x is "nnn") {
return 13;
}
else if (x is "ooo") {
return 14;
}
else if (x is "ppp") {
return 15;
}
else if (x is "qqq") {
return 16;
}
else if (x is "rrr") {
return 17;
}
else if (x is "sss") {
return 18;
}
else if (x is "ttt") {
return 19;
}
else if (x is "uuu") {
return 20;
}
else if (x is "vvv") {
return 21;
}
else if (x is "www") {
return 22;
}
else if (x is "xxx") {
return 23;
}
else if (x is "yyy") {
return 24;
}
else if (x is "xxx") {
return 25;
}
else {
return -1;
}
}
int SwitchStatement(string x) {
switch (x) {
case "aaa": return 0;
case "bbb": return 1;
case "ccc": return 2;
case "ddd": return 3;
case "eee": return 4;
case "fff": return 5;
case "ggg": return 6;
case "hhh": return 7;
case "iii": return 8;
case "jjj": return 9;
case "kkk": return 10;
case "lll": return 11;
case "mmm": return 12;
case "nnn": return 13;
case "ooo": return 14;
case "ppp": return 15;
case "qqq": return 16;
case "rrr": return 17;
case "sss": return 18;
case "ttt": return 19;
case "uuu": return 20;
case "vvv": return 21;
case "www": return 22;
case "xxx": return 23;
case "yyy": return 24;
case "zzz": return 25;
default: return -1;
}
}
結果
switch: 1274ms
if: 1850ms
結論
switch文を使いましょう😎