C# Singleton(シングルトン)とは?スレッドセーフな実装方法と使いどころをサンプルコードで解説

はじめに

C#で「Singleton(シングルトン)」は、アプリケーション全体で1つだけ共有したいオブジェクトを扱うための定番パターンです。設定情報、ログ出力、キャッシュ、接続管理など、どこから参照しても同じ状態を使いたい場面でよく登場します。

一方で、Singletonは便利な反面、実装を誤るとマルチスレッド環境で複数生成されたり、テストがしづらくなったり、設計が硬直化したりします。C#では、lock を使った実装、static readonly、静的コンストラクター、Lazy<T> など複数のやり方がありますが、用途に応じた選択が重要です。

この記事では、C# Singletonの基本から、スレッドセーフな実装、実務での使いどころ、避けるべきケースまでを、サンプルコードとともにわかりやすく解説します。

1. C#のSingleton(シングルトン)とは?

1-1. Singletonパターンの基本:インスタンスを1つだけに制限する設計

Singletonパターンとは、あるクラスのインスタンスをアプリケーション内で1つに制限し、その唯一のインスタンスをどこからでも参照できるようにする設計手法です。

通常、クラスは new で何度でも生成できます。しかしSingletonでは、外部からの生成を禁止し、内部で生成した1つのインスタンスだけを公開します。これにより、共有状態を一元管理できます。

たとえば、アプリ全体で共通の設定値を保持するクラスや、ログを書き込むクラスは、複数インスタンスよりも1つのインスタンスで管理したほうが都合がよいことがあります。

1-2. C#でSingletonが使われる理由

C#でSingletonが使われる主な理由は、次のようなものです。

まず、アプリケーション全体で同じデータや処理を共有したい場合に向いています。設定、キャッシュ、ログ、乱数生成器、リソース管理などは、複数のオブジェクトに分散させるより、1つにまとめたほうが扱いやすくなります。

また、C#はオブジェクト指向言語であり、クラスの責務を分けながら設計することが重視されます。その中でSingletonは、「状態を持つが、1つだけ存在させたい」という要件に対する自然な解決策としてよく採用されます。

1-3. staticクラスとの違い

Singletonと似たものに static クラスがあります。どちらも「1つだけの存在」として使われることがありますが、性質は異なります。

static クラスはインスタンス化できず、すべてのメンバーが静的です。状態を持たないユーティリティ関数の集まりに向いています。一方でSingletonは、インスタンスとして存在し、必要に応じて状態を保持できます。

つまり、計算処理や文字列操作のように状態を持たない機能なら static クラス、設定値やキャッシュのように状態を持ちたいならSingleton、という使い分けが基本です。

ただし、Singletonはインスタンスとして扱えるため、インターフェースを実装したり、DIコンテナに登録したりしやすいという利点があります。テスト容易性の面では static クラスより柔軟です。

1-4. Singletonで解決できる課題と注意すべき課題

Singletonは、アプリケーション全体で共有すべき情報や処理をまとめるのに役立ちます。たとえば、設定の重複読み込みを防いだり、ログ出力先を一元化したり、重い初期化処理を1回だけにしたりできます。

しかし、同時に注意点もあります。状態をグローバルに共有するため、どこから変更されたのか追いにくくなります。また、依存関係が見えにくくなり、テスト時に差し替えが難しくなることもあります。

さらに、マルチスレッド環境では、実装を誤るとインスタンスが複数作られる危険があります。C# Singletonは「作ること」よりも、「安全に1つだけを保つこと」が重要です。

2. まず押さえるC# Singletonの基本実装

2-1. 最小構成のSingletonサンプルコード

まずは、最小構成のC# Singletonを見てみましょう。

C#
public sealed class Singleton
{
private static readonly Singleton _instance = new Singleton();

private Singleton()
{
}

public static Singleton Instance
{
get { return _instance; }
}

public void DoSomething()
{
Console.WriteLine("Singleton instance is working.");
}
}

使い方は次のようになります。

C#
Singleton.Instance.DoSomething();

この実装では、クラスの外から new Singleton() できず、Instance を通じて唯一のインスタンスだけを取得できます。

2-2. privateコンストラクターで外部生成を防ぐ

Singletonの基本は、コンストラクターを private にすることです。これにより、外部から new でインスタンスを作れなくなります。

C#
private Singleton()
{
}

これがないと、呼び出し側が自由にインスタンスを作れてしまい、「1つだけ」という制約が崩れます。Singletonでは、まず外部生成を封じることが出発点です。

2-3. staticフィールド/プロパティでインスタンスを公開する

唯一のインスタンスを外部に渡すには、static フィールドや static プロパティを使います。

static であるため、クラスのインスタンスがなくても参照できます。これにより、Singleton.Instance のような形でどこからでもアクセスできます。

フィールドを直接公開するより、プロパティにしたほうが後から実装を変更しやすく、読み取り専用の意図も明確になります。

2-4. sealedを付けて継承による複製リスクを避ける

Singletonには sealed を付けることがよくあります。

C#
public sealed class Singleton
{
}

継承を禁止することで、派生クラスから振る舞いを変えられるリスクを減らせます。Singletonは「1つの実装を1つだけ使う」ことが重要なので、継承の余地を残さないほうが設計が明確になります。

3. C#でSingletonを実装するときに起きやすい問題

3-1. マルチスレッド環境でインスタンスが複数生成される問題

Singletonで最も有名な問題は、マルチスレッド環境で同時に初期化され、複数のインスタンスが作られてしまうことです。

たとえば、Instance にアクセスした2つのスレッドが、まだインスタンスが生成されていないタイミングで同時に new を実行すると、1つに絞り込めない可能性があります。

そのため、Singletonは「ただ1つのインスタンスを保持する」だけでなく、「生成処理が競合しない」ことを保証しなければなりません。

3-2. 遅延初期化と早期初期化の違い

Singletonには、最初からインスタンスを作る「早期初期化」と、最初に使われたときに作る「遅延初期化」があります。

早期初期化は実装が簡単でスレッドセーフにしやすい反面、使わない場合でも起動時に生成されます。遅延初期化は無駄な生成を避けられますが、スレッドセーフの配慮が必要になります。

どちらがよいかは、初期化コストと使用頻度で決まります。軽いオブジェクトなら早期初期化でも十分ですし、初期化に時間がかかるなら遅延初期化が有効です。

3-3. lockの使い方を間違えたときのパフォーマンス低下

スレッドセーフにしようとして lock を多用すると、今度はパフォーマンスに影響が出ることがあります。特に、毎回のアクセスで不要に lock を取る実装は避けたいところです。

lock は正しく使えば有効ですが、アクセス頻度が高いSingletonでは、できるだけ初回だけ競合し、それ以降は低コストで取得できるようにしたいです。実装の仕方次第で、速度と安全性のバランスが大きく変わります。

3-4. テストしにくい・依存関係が隠れるという設計上の問題

Singletonはグローバルにアクセスできるため、便利な反面、どのクラスが何に依存しているのか見えにくくなります。

たとえば、メソッドの引数に依存オブジェクトが現れず、内部で Singleton.Instance を直接参照していると、コードを見ただけでは依存関係が分かりません。結果として、変更に弱く、モックにも差し替えにくくなります。

このため、Singletonは「何でも入れる箱」ではなく、本当に共有が必要な責務だけに絞ることが大切です。

4. スレッドセーフなC# Singletonの実装方法

4-1. lockを使った基本的なスレッドセーフ実装

もっとも基本的なスレッドセーフ実装は、lock を使う方法です。

C#
public sealed class Singleton
{
private static Singleton? _instance;
private static readonly object _lock = new object();

private Singleton()
{
}

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

この方法は分かりやすく、確実です。初回だけでなく毎回 lock を取得するため、アクセス頻度が高い場合は性能面でやや不利です。

4-2. double-check lockingによる実装

lock のコストを抑えるために、double-check locking を使う方法があります。

C#
public sealed class Singleton
{
private static Singleton? _instance;
private static readonly object _lock = new object();

private Singleton()
{
}

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

この実装では、すでに生成済みなら lock を回避できます。ただし、実装の理解が少し難しく、メモリモデルへの配慮も必要になるため、扱いには注意が必要です。

4-3. static readonlyを使ったシンプルな実装

最もシンプルで安全性も高いのが、static readonly を使う早期初期化です。

C#
public sealed class Singleton
{
private static readonly Singleton _instance = new Singleton();

private Singleton()
{
}

public static Singleton Instance => _instance;
}

この方法は、クラスのロード時点で1回だけ生成されるため、スレッドセーフ性を意識しやすいです。初期化コストが低いなら、非常に扱いやすい実装です。

4-4. staticコンストラクターで初期化タイミングを制御する実装

静的コンストラクターを使って、初期化タイミングを制御する方法もあります。

C#
public sealed class Singleton
{
private static readonly Singleton _instance;

static Singleton()
{
_instance = new Singleton();
}

private Singleton()
{
}

public static Singleton Instance => _instance;
}

静的コンストラクターは、型が初めて使われるタイミングで1回だけ呼ばれます。明示的に初期化処理をまとめられるため、複雑な初期化が必要な場合に便利です。

4-5. Lazy<T>を使った推奨実装

C#でSingletonを実装するなら、Lazy<T> は非常に有力な選択肢です。遅延初期化とスレッドセーフ性を標準で備えているため、実装が簡潔になります。

C#
public sealed class Singleton
{
private static readonly Lazy<Singleton> _instance =
new Lazy<Singleton>(() => new Singleton());

private Singleton()
{
}

public static Singleton Instance => _instance.Value;
}

Lazy<T> を使うと、初回アクセス時だけ生成され、その後は同じインスタンスが返されます。自前で複雑な lock を書かなくてもよいため、保守しやすいのが大きな利点です。

4-6. 各実装方法のメリット・デメリット比較

lock を使う方法は分かりやすく、意図が明確です。ただし、毎回ロックが入るため、シンプルな割に少し重くなります。

double-check locking は高速化しやすい一方で、実装が難しく、保守時にミスが起きやすいです。

static readonly は簡潔で安全ですが、遅延初期化ではなく早期初期化になるため、使わない場合でも生成されます。

静的コンストラクターは制御しやすいですが、複雑になると見通しが悪くなることがあります。

Lazy<T> は総合力が高く、実務ではもっとも扱いやすい選択肢になりやすいです。特別な理由がなければ、まずは Lazy<T> を検討するのがよいでしょう。

5. Lazy<T>を使ったC# Singletonの実装をサンプルコードで解説

5-1. Lazy<T>がSingleton実装に向いている理由

Lazy<T> がSingletonに向いている理由は、初回アクセスまでインスタンス生成を遅らせられること、そしてスレッドセーフな初期化を標準で扱えることです。

Singletonでは「1つだけ作る」ことに加え、「必要になるまで作らない」ことも重要になる場合があります。たとえば、重い設定読み込みや外部接続を伴うクラスでは、起動直後に生成する必要がないことも多いです。

その点、Lazy<T> は余計な実装を増やさずに、自然な遅延初期化を実現できます。

5-2. Lazy<T>によるスレッドセーフなSingletonコード

実務で使いやすい基本形は次の通りです。

C#
public sealed class AppSettingsManager
{
private static readonly Lazy<AppSettingsManager> _instance =
new Lazy<AppSettingsManager>(() => new AppSettingsManager());

private AppSettingsManager()
{
// 初期化処理
}

public static AppSettingsManager Instance => _instance.Value;

public string AppName { get; set; } = "Sample App";
}

このクラスは、AppSettingsManager.Instance に初めてアクセスしたタイミングでインスタンスが生成されます。以降は同じインスタンスが使われます。

5-3. Valueプロパティで初回アクセス時に生成される仕組み

Lazy<T> は、Value プロパティに初めてアクセスしたときに内部の生成関数を実行します。

C#
public static AppSettingsManager Instance => _instance.Value;

この Value がポイントです。実際には new AppSettingsManager() がすぐに呼ばれるのではなく、必要になった瞬間まで待機します。これにより、起動時のコストを抑えやすくなります。

5-4. LazyThreadSafetyModeを使う場合の考え方

Lazy<T> には、スレッドセーフの動作を細かく制御できる LazyThreadSafetyMode があります。

C#
private static readonly Lazy<Singleton> _instance =
new Lazy<Singleton>(() => new Singleton(), LazyThreadSafetyMode.ExecutionAndPublication);

一般的なSingletonでは、ExecutionAndPublication を使うことが多いです。これは、複数スレッドが同時にアクセスしても、1回だけ生成し、その結果を安全に公開するモードです。

特殊な要件がなければ、デフォルト動作でも十分なことが多いですが、意図を明確にしたい場合はモードを指定しておくと安心です。

5-5. 実務で使いやすいSingletonクラスの完成形

実務向けに少し整理すると、次のような形になります。

C#
using System;
using System.Threading;

public sealed class LoggerService
{
private static readonly Lazy<LoggerService> _instance =
new Lazy<LoggerService>(() => new LoggerService(), LazyThreadSafetyMode.ExecutionAndPublication);

private LoggerService()
{
}

public static LoggerService Instance => _instance.Value;

public void Log(string message)
{
Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}");
}
}

呼び出し側は次のように使えます。

C#
LoggerService.Instance.Log("Application started.");

この形は、読みやすさ、保守性、スレッドセーフ性のバランスがよく、C# Singletonの基本形として覚えておく価値があります。

6. Singletonの使いどころ

6-1. 設定情報やアプリケーション全体で共有する状態

Singletonは、アプリケーション全体で共通の設定を保持したいときに向いています。

たとえば、接続先URL、機能フラグ、表示テーマ、アプリ名など、起動後に何度も参照される情報はSingletonで管理すると便利です。読み込みを一度にまとめられるため、管理コストを下げやすくなります。

6-2. ロガーやキャッシュ管理クラス

ロガーはSingletonの代表的な用途です。ログ出力先を1つにまとめることで、どこから出たログかを一元管理できます。

キャッシュ管理も相性がよいです。複数の場所でバラバラにキャッシュを持つと整合性が崩れやすいですが、1つの管理クラスに集約すれば、更新や削除のルールを統一しやすくなります。

6-3. DB接続管理で使う場合の注意点

DB接続管理にSingletonを使うこともありますが、注意が必要です。接続オブジェクトそのものをSingletonにしてしまうと、接続の使い回しや同時利用で問題が起きる場合があります。

DB関連では、接続プールや短命な接続の生成・破棄のほうが適切なことも多いです。Singletonにするなら、「接続そのもの」ではなく、「接続を生成する設定やファクトリ」を管理する役割にとどめるほうが安全です。

6-4. Unityやゲーム開発で使われるSingleton

Unityやゲーム開発では、Singletonはよく見かけるパターンです。ゲーム全体で1つのゲーム状態を管理したり、BGM制御やシーン間共有データを保持したりするために使われます。

ただし、UnityでもSingletonの乱用は危険です。オブジェクト間の結合が強くなりやすいため、必要最小限に留めることが大切です。

6-5. ASP.NET CoreではDIコンテナのSingletonライフタイムを検討する

ASP.NET Coreでは、自前でSingletonクラスを作るより、DIコンテナのSingletonライフタイムを使うのが自然なことが多いです。

DIコンテナに登録すると、アプリケーション全体で1つのインスタンスとして扱えます。しかも、依存関係が明示され、テストや差し替えもしやすくなります。

つまり、ASP.NET Coreでは「Singletonパターンを自分で書く」より、「DIでSingletonライフタイムを使う」ほうが設計としてきれいになりやすいです。

7. Singletonを使わないほうがよいケース

7-1. グローバル変数の代わりとして乱用するケース

Singletonは便利ですが、グローバル変数の代用品として何でも入れてしまうと設計が壊れます。

どこからでもアクセスできるという性質は、裏を返せばどこからでも書き換えられるということです。状態変更が追いにくくなり、バグの原因が見えづらくなります。

7-2. 状態を持ちすぎてテストが困難になるケース

Singletonが多くの状態を持つようになると、テスト時に前のテストの影響が残ることがあります。

たとえば、あるテストで設定を変更すると、別のテストに影響する可能性があります。こうなると、テストの独立性が失われ、再現性の低い不具合につながります。

7-3. 複数インスタンスが必要になる可能性があるケース

将来的に複数インスタンスが必要になる可能性があるなら、最初からSingletonにしないほうがよいことがあります。

たとえば、環境ごとに異なる設定を使いたい、複数の接続先を同時に扱いたい、テナントごとに独立した状態を持ちたい、といったケースです。Singletonにすると拡張の余地が狭くなります。

7-4. 依存性注入で管理したほうがよいケース

依存オブジェクトがあるなら、SingletonよりDIのほうが適していることが多いです。

DIを使うと、依存関係がコンストラクター引数に表れるため、コードの見通しがよくなります。また、テスト時にはモックやスタブを差し替えやすくなります。

7-5. Singletonがアンチパターンと呼ばれる理由

Singletonがアンチパターンと言われるのは、設計を便利に見せながら、実際には依存を隠し、変更を難しくすることがあるからです。

特に、大規模なコードベースでは、Singletonが増えるほど結合が強くなりやすく、修正の影響範囲が見えにくくなります。つまり、Singleton自体が悪いのではなく、使い方次第で保守性を下げやすいのです。

8. C# Singleton実装のベストプラクティス

8-1. 迷ったらLazy<T>またはDIコンテナを優先する

迷ったら、まずは Lazy<T> を使ったSingleton、あるいはDIコンテナのSingletonライフタイムを優先するとよいです。

自前の lock 実装は正しく書ければ問題ありませんが、実装ミスの余地が増えます。シンプルで安全な仕組みを選ぶほうが、長期保守では有利です。

8-2. 状態をできるだけ持たせない

Singletonには、できるだけ状態を持たせないほうが扱いやすくなります。

状態が増えるほど、どこで変更されたか追跡しづらくなります。可能であれば、Singletonは設定読み取りや処理の仲介にとどめ、可変状態は最小限に抑えましょう。

8-3. スレッドセーフ性を前提に設計する

Singletonを作るなら、最初からスレッドセーフ性を前提にしておくべきです。

あとからマルチスレッド対応を入れると、コード全体を見直す必要が出やすいです。最初から Lazy<T> や適切な同期を使っておけば、安全性を確保しやすくなります。

8-4. テストしやすいようにインターフェース化を検討する

Singletonをそのまま直接使うのではなく、インターフェースを用意して抽象化しておくと、テストしやすくなります。

C#
public interface ILoggerService
{
void Log(string message);
}

このようにしておけば、実装差し替えがしやすくなり、将来的にDIへ移行する場合もスムーズです。

8-5. 破棄処理が必要なリソースを持たせる場合の注意点

Singletonがファイル、DB接続、ソケットなどの破棄が必要なリソースを持つ場合は注意が必要です。

アプリケーション終了時に適切に破棄されないと、リソースリークにつながることがあります。IDisposable を実装し、ライフサイクルを明確にしておくことが重要です。

Singletonは「ずっと生き続ける」設計になりやすいため、終了時処理まで含めて考える必要があります。

9. C# Singletonのよくある質問

9-1. Singletonとstaticクラスはどちらを使うべき?

状態を持たない処理なら static クラス、状態を持ちたいならSingletonが基本です。

ただし、テストしやすさや拡張性を重視するなら、SingletonよりDIやインターフェース化を検討したほうがよい場合もあります。単純なユーティリティなら static クラスのほうが素直です。

9-2. Singletonは必ずスレッドセーフにする必要がある?

アプリケーションが単一スレッドで動くなら、厳密なスレッドセーフ性が不要なこともあります。

しかし、実際のC#アプリケーションでは、UI、バックグラウンド処理、非同期処理などで複数スレッドが関わることが多いです。そのため、基本的にはスレッドセーフにしておくのが無難です。

9-3. Lazy<T>とlock実装はどちらがよい?

一般的には Lazy<T> が扱いやすいです。

lock 実装は仕組みが分かりやすい一方、記述量が増え、ミスの余地もあります。Lazy<T> は標準機能として遅延初期化とスレッドセーフ性をまとめて扱えるため、実務では採用しやすいです。

9-4. SingletonはDIと併用できる?

はい、併用できます。

むしろ、ASP.NET Coreのような環境では、DIコンテナにSingletonライフタイムで登録するほうが自然です。自前Singletonを直接参照するより、DI経由で扱ったほうがテスト性と保守性が高くなります。

9-5. Singletonを使うとメモリリークする?

Singletonそのものが直接メモリリークを起こすわけではありませんが、長寿命であるがゆえに参照を保持し続けやすく、結果として不要なオブジェクトが解放されにくくなることはあります。

特にイベント購読や大きなキャッシュを持つ場合は注意が必要です。不要になった参照を明示的に解除し、リソース解放を適切に行うことが大切です。

まとめ

C# Singletonは、アプリケーション全体で1つだけ共有したいオブジェクトを管理するのに便利な設計パターンです。基本は private コンストラクターで外部生成を防ぎ、static メンバーで唯一のインスタンスを公開します。

実装方法には lock、double-check locking、static readonly、静的コンストラクター、Lazy<T> などがありますが、実務では Lazy<T> がもっとも扱いやすい選択肢になりやすいです。特にスレッドセーフ性と遅延初期化を両立しやすい点が魅力です。

ただし、Singletonは便利な一方で、状態の共有が強すぎるとテスト性や保守性を下げることがあります。グローバル変数のように乱用せず、設定管理、ログ、キャッシュなど、本当に1つである必要がある場面に絞って使うことが大切です。

迷ったときは、「状態を持たないなら static クラス」「状態を持つが1つにしたいなら Lazy<T> のSingleton」「依存関係があるならDI」を基準に考えると、C# Singletonを適切に使いやすくなります。