koturnの日記

転職したい社会人2年生の技術系日記.ブログ上のコードはコピペ自由です.

C#のシングルトン

背景

最近,UnityでC#を書くので,普通のC#を書くときに使っていたコードスニペットを整理していた. その中にシングルトンの実装が含まれているのを見て,そういえばこの実装はスレッドセーフなのか?と疑問に思ったのがきっかけで,C#のシングルトンについて調べ直した. この記事では備忘録としてシングルトンの各種実装をまとめた.

4つのシングルトン実装

実装その1(非スレッドセーフ)

単純な実装は下記の通り.

初回に Instance プロパティを通じてインスタンスの取得を試みたとき,インスタンスの作成と保持を行い,保持したインスタンスを返す. 2回目以降は保持したインスタンスを返す. いわゆる遅延初期化という実装である.

当然だがスレッドセーフではない. また,些細なものだが,アクセスの度にnullチェックが入るのが気になる.

public sealed class Singleton
{
    private static Singleton _instance;

    public static Singleton Instance => _instance ??= new Singleton();

    public string Name { get; }

    private Singleton()
    {
        Name = nameof(Singleton);
    }
}

実装その2(スレッドセーフっぽいけど保証はされないらしい)

なればとクラス自体の初期化が行われた際にインスタンスを作成する下記の実装を考える.

一見スレッドセーフっぽいが,実はスレッドセーフであることは保証されないとのこと.

静的コンストラクタが定義されていないクラスには,beforefieldinit属性が付加される. そして,beforefieldinit属性が付与されていると,

  1. 確実に一回だけ、type initializer が呼ばれる
  2. type initializer が完了するまで、他のスレッドが静的フィールドにアクセスしたとしても待たされる

という事項がCLI仕様として保証されないらしい

また,初期化タイミングがその型の静的フィールドへの初回アクセス時になるとは限らず,それ以前になることもあるかもしれないらしい.

public sealed class Singleton
{
    public static Singleton Instance { get; } = new Singleton();

    public string Name { get; }

    private Singleton()
    {
        Name = nameof(Singleton);
    }
}

実装その3(スレッドセーフ(コンパイラ依存らしいが))

というわけで,静的コンストラクタを定義すればよい. 中身は空でもよい.

実際,その2の生成コードとその3の生成コードを比較すると,前者にはbeforefieldinit属性が付与されており,後者には付与されていないことが確認できた.

ただし,これはコンパイラ依存であるらしく,コンパイラ非依存のシングルトンにするためには,別の手法が必要になる.

個人的にはこの実装でよいと思うが....

public sealed class Singleton
{
    public static Singleton Instance { get; } = new Singleton();

    static Sigleton()
    {
        // beforefieldinit属性を付与されないための静的コンストラクタ
        // 空だが消去してはならない
    }

    public string Name { get; }

    private Singleton()
    {
        Name = nameof(Singleton);
    }
}

下記のような静的コンストラクタで初期値代入を行うようにしてもよいと思う.

public sealed class Singleton
{
    public static Singleton Instance { get; }

    static Sigleton()
    {
        Instance = new Singleton();
    }

    public string Name { get; }

    private Singleton()
    {
        Name = nameof(Singleton);
    }
}

実装その4(スレッドセーフ(コンパイラ非依存))

最初の遅延初期化の案にロックを使用するようにすれば,確実にスレッドセーフなシングルトン実装になる. いわゆる,Double Checked Lockingである.

ロックはそのスレッドから見て,シングルトンインスタンスが生成されていない場合にのみ行うので,アクセスの度にロック取得を行なうわけではない.

ただ,getプロパティのバイトコードサイズが手元のコンパイル結果だと83 Bytesになっており,lockも含むので,実行時にインライン展開されるかどうかはかなりあやしい(未確認).

public sealed class Singleton
{
    private static Singleton _instance;

    private static readonly object _syncRoot = new object();

    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (_syncRoot)
                {
                    if (_instance == null)
                    {
                        _instance = new Singleton();
                    }
                }
            }
            return _instance;
        }
    }

    public string Name { get; }

    private Singleton()
    {
        Name = nameof(Singleton);
    }
}

ジェネリックなシングルトン実装

複数のシングルトンクラスの実装を行うと,似たようなコードを書いて実装という形になる. ここを何とかしたいと人間考えるものである.

なので,ジェネリックなシングルトンクラスを考える. ベースとしては前述の実装その3を採用する.

先に結論を書くと,一長一短であり,理想的な実装はない. 個別にシングルトンクラスを定義するのが一番よい.

実装その1(コンストラクタをpublicにせざるを得ない)

C++でいうところのCRTPに似た形でジェネリックなシングルトンクラスの実装を行う. C++のフレンドクラスのような機能はC#にはないため,シングルトンにするクラスのコンストラクタを public にしなければ,ジェネリッククラス側でコンストラクタが不可視のため,コンパイルできない.

public abstract class Singleton<T>
    where T : class, new()
{
    public static T Instance { get; }

    static Singleton()
    {
        Instance = new T();
    }
}

public sealed class Foo : Singleton<Foo>
{
    public string Name { get; }

    public Foo()
    {
        Name = nameof(Foo);
    }
}

実装その2(リフレクションを利用)

リフレクションを利用すれば,privateなコンストラクタへアクセスできる.

しかし,

  1. インスタンスの生成のコストが高い.
  2. 引数有りコンストラクタを定義すると,デフォルトコンストラクタが定義されなくなり,実行時エラーになる.コンパイルエラーにはできない.

という問題がある.

public abstract class Singleton<T>
    where T : class
{
    public static T Instance { get; }

    static Singleton()
    {
        Instance = (T)Activator.CreateInstance(typeof(T), true);
    }
}

public sealed class Foo : Singleton<Foo>
{
    public string Name { get; }

    private Foo()
    {
        Name = nameof(Foo);
    }
}

まとめ

様々なシングルトンクラスの実装を紹介した.

静的コンストラクタの有無によって,beforefieldinit属性の有無も変化し,それによってCLIの仕様として,スレッドセーフな初期化が保証されるかどうかが変わるため,遅延初期化でない実装では,空でも静的コンストラクタを定義した方がよい.

また,完璧なジェネリックシングルトンクラスの実装はできない.

参考