C#
 Computer >> コンピューター >  >> プログラミング >> C#

C#のシングルトンクラスにsealed(封印)を付けるべき理由


C#における sealed(封印)キーワードは、そのクラスが継承できないことを意味します。一方、コンストラクタを private で宣言すると、クラスの外部からインスタンスを生成できなくなります。この2つは似ているようで役割が異なり、シングルトンパターンを実装するうえでは両方が重要になります。

privateコンストラクタだけでは不十分な理由

一見すると、コンストラクタをprivateにすれば誰もインスタンスを生成できず、シングルトンは完成したように思えます。しかし実際には、privateコンストラクタを持つ基底クラスであっても、そこから派生クラスを作成し、派生クラス側でpublicコンストラクタを定義すれば、結果的に基底クラスのインスタンスを生成できてしまいます。

ここで押さえるべきポイントは次の2つです。

  • コンストラクタは継承されない:基底クラスのコンストラクタがすべてprivateでも、それがそのまま派生クラスに引き継がれるわけではありません。
  • 派生クラスは必ず最初に基底クラスのコンストラクタを呼び出す:アクセスできるのであれば、派生クラスの生成時に基底クラスも初期化されてしまいます。

特に注意したいのは、入れ子(ネスト)クラスは外側のクラスのprivateメンバーにアクセスできるという点です。そのため、privateコンストラクタを持つクラスでも、入れ子クラスから継承されれば自由にインスタンスを作られてしまいます。

sealedを付けることで防げること

クラスを sealed としてマークしておけば、そもそも継承自体が不可能になるため、第三者が慎重に設計されたシングルトンの仕組みを簡単に回避してしまうことを防げます。これにより、「インスタンスは必ず1つだけ」というシングルトンの保証が守られます。

コード例:sealedを付けなかった場合の問題

次のコードは、sealed を付けなかった場合にシングルトンが破られる様子を示しています。

static class Program {
    static void Main(string[] args) {
        Singleton fromStudent = Singleton.GetInstance;
        fromStudent.PrintDetails("学生からの呼び出し");

        Singleton fromEmployee = Singleton.GetInstance;
        fromEmployee.PrintDetails("従業員からの呼び出し");

        Console.WriteLine("-------------------------------------");

        // 継承によってシングルトンを回避できてしまう
        Singleton.DerivedSingleton derivedObj = new Singleton.DerivedSingleton();
        derivedObj.PrintDetails("派生クラスからの呼び出し");
        Console.ReadLine();
    }
}

public class Singleton {
    private static int counter = 0;
    private static object obj = new object();

    private Singleton() {
        counter++;
        Console.WriteLine("Counter Value " + counter.ToString());
    }

    private static Singleton instance = null;

    public static Singleton GetInstance {
        get {
            if (instance == null)
                instance = new Singleton();
            return instance;
        }
    }

    public void PrintDetails(string message) {
        Console.WriteLine(message);
    }

    // sealed がないため継承できてしまう
    public class DerivedSingleton : Singleton {
    }
}

実行結果

Counter Value 1
学生からの呼び出し
Counter Value 2
従業員からの呼び出し
-------------------------------------
Counter Value 3
派生クラスからの呼び出し

何が起きているのか

GetInstance プロパティ経由では同じインスタンスが再利用されるため、カウンターは通常「1」までしか増えません。ところが、入れ子クラスの DerivedSingletonSingleton を継承しており、外側のクラスのprivateコンストラクタにアクセスできるため、new によって新たなインスタンスを生成できてしまいます。これではシングルトンパターンの意味がありません。

解決策:sealedを付ける

クラス宣言を次のように変更するだけで問題は解決します。

public sealed class Singleton {
    ...
}

sealed を付ければ DerivedSingleton のような派生クラスは作成できず、コンパイルエラーになります。その結果、インスタンスが1つしか存在しないことが言語レベルで保証されます。「privateコンストラクタ+sealed+静的プロパティによる遅延生成」という組み合わせが、C#における基本的なシングルトン実装の定型です。

補足:スレッドセーフについて

上記のシンプルな実装は、マルチスレッド環境では同時アクセスによって複数のインスタンスが生成される可能性があります。実運用では Lazy<T> 型や lock ステートメントを使ったスレッドセーフな実装を採用するのが安全です。

  1. C#のConsoleクラスとは?主要プロパティの使い方を実例付きで解説

    C#のConsoleクラスは、コンソールアプリケーションにおける標準入力・標準出力・標準エラー出力の各ストリームを表すクラスです。テキストの表示や入力の読み取りだけでなく、文字色や背景色の変更、カーソル位置やサイズの制御など、コンソール操作に関するさまざまな機能が提供されています。ここでは、Consoleクラスの代表的なプロパティの使い方を、サンプルコードと実行結果とあわせて解説します。Console.CursorLeftプロパティコンソール上のカーソル位置(左端から数えた列位置)を変更するには、Console.CursorLeftプロパティを使用します。サンプルコードusing System

  2. なぜクラスを作るのか?抽象化の重要性と良いクラス設計の基本

    前回の記事「なぜnilを使うのか」に続き、普段当たり前と思って使っているものについても、同じ問いを立ててみるのは良いアイデアではないでしょうか。 例えば… 巨大で醜いコードの塊を1つ作るのではなく、なぜ私たちはクラスを使うのでしょうか? 基本的なレベルで言えば、コードとデータを論理的な単位に整理するためにクラスを使います。しかし、それだけではありません。 クラスを使うと、抽象化(abstraction)を作り出せるのです。 抽象化とは何か? 実は、私たちは毎日、毎分、抽象化を利用しています。 今この記事を書いているキーボードは、次のようなもので構成されています。 ケーブル Cherry MX