Skip to content

投稿

Switch文の存在意義を検証する

2015年12月6日 • 17分で読める

Switch文の存在意義を検証する

約50年間、switch文(case文とも呼ばれる)はプログラミングの不可欠な部分でした。しかし近年、switch文はその有用性を失ったと主張する人もいます。さらに進んで、switch文をコードスメルとラベル付けする人もいます。

1952年、Stephen Kleeneが論文『Introduction to Metamathematics』でswitch文を考案しました。最初の注目すべき実装は1958年のALGOL 58でした。その後、switch文は不朽のC言語に含まれ、ご存知の通り、現代のほとんどのプログラミング言語に影響を与えました。

現在に至るまで、事実上すべての言語にswitch文があります。しかし、いくつかの言語はswitch文を省略しています。最も注目すべきはSmalltalkです。

これは私の好奇心をそそりました。なぜSmalltalkからswitch文が除外されたのでしょうか?

Dolphin Smalltalkの作成者/支持者の一人であるAndy Bowerは、SmalltalkがSwitch文を除外した理由について次のような考えを共有しました:

C++からSmalltalkに初めて来たとき、完全に成熟した言語とされるものがswitch/case構文をサポートしていないことが理解できませんでした。結局のところ、BASICから「構造化プログラミング」に最初に移行したとき、switchはスライスパン以来の最高のものの一つだと思っていました。しかし、Smalltalkがswitchをサポートしていなかったため、この欠陥を克服する方法を探し、理解しなければなりませんでした。正しい答えは、もちろん、ポリモーフィズムを使用し、オブジェクト自体に正しいコード片にディスパッチさせることです。そのとき、それは「欠陥」ではなく、SmalltalkがC++で慣れ親しんだよりもはるかに細かい粒度のOOP設計を強制していることに気づきました。switch文が利用可能だったら、これを学ぶのにもっと時間がかかったか、さらに悪いことに、今でもSmalltalkでC++/Java疑似オブジェクトスタイルでプログラミングしているかもしれません。
通常のOOPでは、switch文は実際には必要ないと主張します。非OOPの世界とのインターフェース時(オブジェクトではなく単なる整数であるWM_XXXX Windowsメッセージの受信とディスパッチなど)には、switch文が有用でしょう。これらの状況では、代替手段(Dictionaryからのディスパッチなど)があり、それらが発生する回数は追加の構文の包含を正当化しません。

Andyは正しかったのでしょうか?switch文なしの方が良いのでしょうか?他の言語もswitch文を除外することで恩恵を受けるでしょうか?

この質問に光を当てるため、switch文、辞書、ポリモーフィズムの比較をまとめました。これを対決と呼びましょう。最高の実装が勝利しますように!

各実装には、1つのパラメータ(整数)を取り、文字列を返すメソッドがあります。各実装を検証するために循環的複雑度保守性指数を使用します。その後、3つの実装すべてを総合的に見ていきます。

コードです。

Switch文

保守性指数72
循環的複雑度6
    public class SwitchWithFourCases
    {
        public string SwitchStatment(int color)
        {
            var colorString = "Red";

            switch (color)
            {
                case 1:
                    colorString = "Green";
                    break;

                case 2:
                    colorString = "Blue";
                    break;

                case 3:
                    colorString = "Violet";
                    break;

                case 4:
                    colorString = "Orange";
                    break;

            }

            return colorString;
        }
    }

辞書

保守性指数73
循環的複雑度3
public class DictionaryWithFourItems
{ 
    public string Dictionary(int color)
    {
        var colorString = "Red";
        var colors = new Dictionary<int, string> {{1, "Green"}, {2, "Blue"}, {3, "Violet"}, {4, "Orange"}};
        var containsKey = colors.ContainsKey(color);
        if (containsKey)
        {
            colorString = colors[color];
        }

        return colorString;
    }
}

ポリモーフィズム

総保守性指数94
総循環的複雑度15

インターフェース

保守性指数100
循環的複雑度1
public interface IColor
{
    string ColorName { get; }
}

ファクトリー

保守性指数76
循環的複雑度4
public class ColorFactory
{
    public string GetColor(int color)
    {
        IColor defaultColor = new RedColor();
        var colors = GetColors();
        var containsKey = colors.ContainsKey(color);
        if (containsKey)
        {
            var c = colors[color];
            return c.ColorName;
        }

        return defaultColor.ColorName;
    }

    private static IDictionary<int, IColor> GetColors()
    {
        return new Dictionary<int, IColor>
        {
            {1, new GreenColor()}, 
            {2, new BlueColor()}, 
            {3, new VioletColor()}, 
            {4, new OrangeColor()}, 
            {5, new MagentaColor()}
        };
    }
}

実装

保守性指数97
循環的複雑度2
public class BlueColor : IColor
{
    public string ColorName => "Blue";
}

public class RedColor : IColor
{
    public string ColorName => "Red";
}

public class GreenColor : IColor
{
    public string ColorName => "Green";
}

public class MagentaColor : IColor
{
    public string ColorName => "Magenta";
}

public class VioletColor : IColor
{
    public string ColorName => "Violet";
}

結果

結果に入る前に、循環的複雑度と保守性指数を定義しましょう:

  • 循環的複雑度は論理分岐の測定値です。数値が低いほど良いです。
  • 保守性指数はコードの保守性を測定します。0から100のスケールです。数値が高いほど良いです。
循環的複雑度保守性指数
Switch文672
辞書373
ポリモーフィズム1594

まず循環的複雑度を検証します。

循環的複雑度の結果は分かりやすいです。辞書実装が最もシンプルです。これが最良のソリューションを意味するでしょうか?いいえ、保守性指数を評価するときに分かるように。

私が考えたように、ほとんどの人は循環的複雑度が最も低い実装が最も保守しやすいと思うでしょう — 他にどうあり得るでしょうか?

私たちのシナリオでは、循環的複雑度が最も低い実装が最も保守しやすいわけではありません。実際、私たちのシナリオでは、その逆です。最も複雑な実装が最も保守しやすいのです!驚愕!

覚えているように、保守性指数のスコアが高いほど良いです。要点を言うと、ポリモーフィズムが最高の保守性指数スコアを持っています — しかし、最高の循環的複雑度も持っています。どういうことでしょうか?それは正しくないように思えます。

なぜ最も複雑な実装が最も保守しやすいのでしょうか?これに答えるには、保守性指数を理解する必要があります。

保守性指数は4つのメトリクスで構成されています:循環的複雑度、コード行数、コメント数、そしてHalstead体積です。最初の3つのメトリクスは比較的よく知られていますが、最後のHalstead体積は比較的知られていません。循環的複雑度と同様に、Halstead体積はコードの複雑さを客観的に測定しようとします。

簡単に言うと、Halstead体積はコード内の可動部品(変数、システムコール、算術、コーディング構造など)の数を測定します。可動部品の数が多いほど複雑さが増します。可動部品の数が少ないほど複雑さが低くなります。これは、ポリモーフィック実装が保守性指数で高いスコアを獲得する理由を説明します;クラスには可動部品がほとんどまたは全くありません。Halstead体積を見る別の方法は、「可動部品」の密度を測定することです。

ソフトウェアが変更されないとしたら、それは何でしょうか?現実世界を反映するために、変更を導入しています。各実装に新しい色を追加しました。

以下が修正された結果です。

循環的複雑度保守性指数
Switch文770
辞書373
ポリモーフィズム1795

switch文とポリモーフィックアプローチの両方が循環的複雑度で1単位増加しましたが、興味深いことに、辞書は増加しませんでした。最初は困惑しましたが、辞書は色をデータとして扱い、他の2つの実装は色をコードとして扱うことに気づきました。要点を述べます。

保守性指数に注目すると、switch文のみが保守性で減少しました。ポリモーフィズムの保守性スコアは改善し、それでも複雑さも増加しています(減少することを好みます)。上で述べたように、これは直感に反します。

私たちの比較は、辞書が複雑さの観点から無限にスケールできることを示しています。ポリモーフィックアプローチは断然最も保守しやすく、より多くのシナリオが追加されるにつれて保守性が向上するようです。switch文は新しいシナリオが追加されたときに複雑さが増加し、保守性が減少します。新しいシナリオを追加する前でも、最悪の循環的複雑度と保守性指数の測定値を持っていました。

GoogleのJem Finchは、switch文の欠点について次のような考えを共有しました:

1. ポリモーフィックメソッドの実装は、語彙的に互いに分離されています。変数は、switch文の別の分岐の無関係なコードに影響を与えるリスクなしに、追加、削除、変更などができます。

2. ポリモーフィックメソッドの実装は、終了すると仮定して、正しい場所に戻ることが保証されています。C/C++/Javaのようなフォールスルー言語のswitch文は、switchの後の文ではなく次のcaseブロックに戻ることを確実にするために、エラーが起こりやすい「break」文を必要とします。

3. ポリモーフィックメソッドの実装の存在は、コンパイラによって強制でき、ポリモーフィックメソッドの実装が欠けている場合、プログラムのコンパイルを拒否します。switch文はそのような網羅性チェックを提供しません。

4. ポリモーフィックメソッドのディスパッチは、他のソースコードへのアクセス(または再コンパイル)なしに拡張可能です。switch文に別のcaseを追加するには、元のディスパッチコードへのアクセスが必要で、1つの場所だけでなく、関連するenumがswitchされているすべての場所で必要です。

5. … ポリモーフィックメソッドをswitch装置とは独立してテストできます。著者が示した例のようにswitchするほとんどの関数は、別々にテストできない他のコードを含みます;一方、仮想メソッド呼び出しは別々にテストできます。

6. ポリモーフィックメソッド呼び出しは定数時間ディスパッチを保証します。自然に線形時間構造(フォールスルーを持つswitch文)を定数時間構造に変換するのに十分にスマートなコンパイラは必要ありません。

残念ながら、または幸運にも、あなたの陣営によりますが、ほとんどの言語にはswitch文があり、それらは近いうちにどこにも行きません。これを念頭に置いて、switch文をコンパイルするときに内部で何が起こっているかを知ることは良いことです。

発生する可能性のある3つのswitch文最適化があります:

  1. If-elseif文 – switch文のcaseが少数または疎な場合(10、250、1000などの非増分値)、if-elseif文に変換されます。
  2. ジャンプテーブル – 隣接するcaseの大きなセット(1、2、3、4、5)では、コンパイラはswitch文をジャンプテーブルに変換します。ジャンプテーブルは本質的に、メモリ内の関数へのポインタ(goto文と考えてください)を持つハッシュテーブルです。
  3. 二分探索 – 疎なcaseの大きなセットでは、コンパイラはデータベースのインデックスの動作と同様に、caseを迅速に識別するために二分探索を実装できます。疎で隣接するcaseの大きな数がある特別な場合では、コンパイラは3つの最適化の組み合わせを使用します。

まとめ

オブジェクト指向の世界では、1952年に考案されたswitch文は、ソフトウェアエンジニアの主力です。注目すべき例外は、設計者がswitch文を除外することを選択したSmalltalkです。

同等の代替実装、辞書、およびポリモーフィズムと比較すると、switch文はそれほど良い結果を示しませんでした。

switch文は存続しますが、私たちの比較が示したように、switch文にはより良い代替手段があります。

実装はGithubで利用可能です。

著者:Chuck Conwayはソフトウェアエンジニアリングと生成AIを専門としています。ソーシャルメディアで彼とつながりましょう:X (@chuckconway) または YouTube をご覧ください。

↑ トップに戻る

こちらもおすすめ