C#シングルトン完全ガイド|実装方法・スレッドセーフ・DIでの使い方まで初心者向けに解説

はじめに

C#のシングルトンは、「アプリケーション内でインスタンスを1つだけ作り、その同じインスタンスを使い回す」ための設計パターンです。ログ出力、設定情報、キャッシュ、アプリ全体で共有するサービスなどで使われることがあります。

一方で、シングルトンは便利な反面、使い方を間違えるとグローバル変数のようになり、テストしにくいコードや依存関係が見えにくいコードを生みやすいという注意点もあります。特にC#では、マルチスレッド環境で安全に使えるか、ASP.NET CoreのDIコンテナではどのように扱うべきかを理解しておくことが重要です。

この記事では、C#のシングルトンについて、初心者にもわかるように基本概念から実装方法、スレッドセーフな書き方、Lazy<T>を使った推奨実装、staticクラスとの違い、DIでの使い方、テストしやすい設計まで順番に解説します。

1. C#のシングルトンとは?初心者向けに基本を解説

1-1. シングルトンパターンの意味

シングルトンパターンとは、クラスのインスタンスを1つだけに制限し、そのインスタンスへどこからでもアクセスできるようにするデザインパターンです。

通常、C#では次のようにnewを使えば、同じクラスから複数のインスタンスを作成できます。

C#
var service1 = new MyService();
var service2 = new MyService();

Console.WriteLine(ReferenceEquals(service1, service2)); // False

この場合、service1service2は別々のオブジェクトです。

一方、シングルトンでは、外部から自由にnewできないようにし、クラス自身が唯一のインスタンスを管理します。

C#
var service1 = MyService.Instance;
var service2 = MyService.Instance;

Console.WriteLine(ReferenceEquals(service1, service2)); // True

このように、どこから取得しても同じインスタンスを返すのがシングルトンの基本です。

1-2. インスタンスを1つだけに制限する理由

インスタンスを1つだけに制限する主な理由は、アプリケーション全体で共有したい状態や処理を一元管理するためです。

たとえば、アプリ全体で同じ設定情報を読みたい場合、毎回設定ファイルを読み込むよりも、1つの設定管理オブジェクトを使い回したほうが効率的です。また、ログ出力クラスやキャッシュ管理クラスのように、複数作る必要がないものもあります。

ただし、「1つだけでよい」と「シングルトンにすべき」は同じではありません。状態を持つシングルトンは、使い方によっては意図しない副作用を生みます。C#でシングルトンを使うときは、本当にアプリ全体で共有すべきインスタンスなのかを考えることが大切です。

1-3. C#でシングルトンが使われる代表的な場面

C#のシングルトンは、次のような場面で使われます。

利用場面
設定情報の管理アプリ設定、環境設定、接続先情報
ログ出力ファイルログ、コンソールログ、共通ロガー
キャッシュ管理メモリキャッシュ、マスターデータキャッシュ
共有サービス通知サービス、ID生成サービス
ゲーム開発ゲームマネージャー、サウンドマネージャー

ただし、ASP.NET CoreのようにDIコンテナが標準的に使われる環境では、自作のシングルトンパターンよりも、DIコンテナのAddSingletonでライフタイムを管理するほうが一般的です。Microsoftのドキュメントでも、DIコンテナではTransient、Scoped、Singletonのライフタイムを選択してサービスを登録できると説明されています。Microsoft Learn

1-4. シングルトンと通常のクラスの違い

通常のクラスとシングルトンの違いは、主にインスタンス生成の制御にあります。

比較項目通常のクラスシングルトン
インスタンス生成newで自由に作れる外部から自由に作れない
インスタンス数複数作れる原則1つだけ
コンストラクタpublicが多いprivateにする
取得方法new MyClass()MyClass.Instance
主な用途個別の状態を持つ処理全体で共有する処理

通常のクラスは、必要な数だけオブジェクトを作り、それぞれに別の状態を持たせられます。シングルトンは、アプリケーション全体で同じ状態や機能を共有したい場合に使います。

2. C#シングルトンの基本的な実装方法

2-1. privateコンストラクタで外部からの生成を防ぐ

シングルトンを実装する最初のポイントは、コンストラクタをprivateにすることです。

C#
public class SingletonService
{
private SingletonService()
{
}
}

コンストラクタがprivateになると、クラスの外側から次のようにnewできなくなります。

C#
// コンパイルエラー
var service = new SingletonService();

これにより、外部から勝手に複数のインスタンスが作られることを防げます。

2-2. staticフィールドで唯一のインスタンスを保持する

次に、クラス内部にstaticフィールドを用意して、唯一のインスタンスを保持します。

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

private SingletonService()
{
}
}

staticフィールドはクラスに属するメンバーです。通常のインスタンスごとに作られるフィールドとは異なり、クラス全体で共有されます。

2-3. Instanceプロパティでインスタンスを取得する

外部から唯一のインスタンスを取得できるように、public staticInstanceプロパティを用意します。

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

public static SingletonService Instance => _instance;

private SingletonService()
{
}
}

利用側は次のように書きます。

C#
var service = SingletonService.Instance;

これで、外部からnewはできないものの、Instanceを通じて同じインスタンスを取得できるようになります。

2-4. 最小構成のシングルトンサンプルコード

C#のシングルトンを最小構成で書くと、次のようになります。

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

public static SingletonService Instance => _instance;

private SingletonService()
{
}

public void Execute()
{
Console.WriteLine("SingletonService is running.");
}
}

利用例は次のとおりです。

C#
SingletonService.Instance.Execute();

sealedを付けているのは、継承によってシングルトンの意図が崩れることを防ぐためです。必須ではありませんが、シングルトンとして明確に設計するなら付けておくとよいでしょう。

3. C#シングルトンの主な実装パターン比較

3-1. eager initializationによる事前生成

eager initializationは、クラスが初期化されるタイミングでインスタンスを事前に作る方法です。

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

public static EagerSingleton Instance => _instance;

private EagerSingleton()
{
}
}

この方法はコードがシンプルで、読みやすいのがメリットです。C#のstatic初期化はランタイムによって管理されるため、基本的に自前で複雑なロック処理を書く必要がありません。staticコンストラクタはアプリケーションドメイン内で最大1回呼び出され、型に基づくロックされた領域で実行されると説明されています。Microsoft Learn

ただし、実際に使われない場合でもインスタンスが作られる可能性があるため、生成コストが高いオブジェクトには向かないことがあります。

3-2. lazy initializationによる遅延生成

lazy initializationは、最初に必要になったタイミングでインスタンスを作る方法です。

C#
public sealed class LazySingleton
{
private static LazySingleton? _instance;

public static LazySingleton Instance
{
get
{
if (_instance == null)
{
_instance = new LazySingleton();
}

return _instance;
}
}

private LazySingleton()
{
}
}

この実装は一見よさそうですが、マルチスレッド環境では問題があります。複数のスレッドが同時にInstanceへアクセスすると、_instance == nullの判定を同時に通過し、複数のインスタンスが生成される可能性があります。

そのため、この形のまま本番コードで使うのは避けるべきです。

3-3. lockを使ったスレッドセーフ実装

マルチスレッド環境で安全にするために、lockを使う方法があります。

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

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

return _instance;
}
}
}

private LockSingleton()
{
}
}

lockによって、同時に1つのスレッドだけがインスタンス生成処理に入れるようになります。

ただし、この実装では、インスタンス生成後もInstanceにアクセスするたびにlockが実行されます。多くのケースでは問題になりませんが、頻繁に呼ばれる処理では無駄が気になる場合があります。

3-4. double-check lockingを使った実装

double-check lockingは、lockの外側と内側で2回nullチェックを行う方法です。

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

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

return _instance;
}
}

private DoubleCheckSingleton()
{
}
}

この方法では、インスタンス作成後はlockに入らずに済みます。

ただし、初心者にとってはやや複雑です。volatileの意味やメモリ可視性の理解が必要になるため、C#ではより簡潔に書けるLazy<T>を使うほうが実用的です。

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

C#でシングルトンを遅延初期化したい場合、初心者にもおすすめなのがLazy<T>を使う方法です。

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

public static LazySingleton Instance => _instance.Value;

private LazySingleton()
{
}
}

Lazy<T>は、Valueプロパティに初めてアクセスされたタイミングで実際のオブジェクトを生成します。さらに、既定ではLazy<T>オブジェクトはスレッドセーフであり、複数スレッドからアクセスされた場合でも、最初にValueへアクセスしたスレッドが初期化を行い、その後のアクセスで同じデータを共有すると説明されています。Microsoft Learn

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

実装方法メリットデメリット初心者へのおすすめ度
eager initializationシンプル、理解しやすい使わなくても生成される可能性がある高い
単純なlazy initialization必要になるまで生成しないスレッドセーフではない低い
lock使用スレッドセーフにしやすい毎回lockがかかる
double-check lockinglock回数を減らせる実装が複雑中〜低
Lazy<T>遅延生成、スレッドセーフ、簡潔Lazy<T>の仕組みを理解する必要がある高い

C#でシングルトンを実装するなら、単純な事前生成かLazy<T>を使った遅延生成を選ぶのがわかりやすく安全です。

4. C#でスレッドセーフなシングルトンを実装する方法

4-1. マルチスレッド環境で起きる問題

Webアプリ、APIサーバー、デスクトップアプリ、ゲームなどでは、複数のスレッドから同じ処理が同時に呼ばれることがあります。

次のようなシングルトン実装は、マルチスレッド環境では危険です。

C#
public sealed class UnsafeSingleton
{
private static UnsafeSingleton? _instance;

public static UnsafeSingleton Instance
{
get
{
if (_instance == null)
{
_instance = new UnsafeSingleton();
}

return _instance;
}
}

private UnsafeSingleton()
{
}
}

問題は、複数のスレッドが同時に_instance == nullを確認する可能性があることです。

たとえば、スレッドAとスレッドBが同時にInstanceへアクセスすると、両方がnullと判断し、それぞれがnew UnsafeSingleton()を実行してしまう可能性があります。これでは「インスタンスは1つだけ」というシングルトンの前提が崩れます。

4-2. lockを使う場合の注意点

lockを使えば、同時に1つのスレッドだけが処理に入れるようになります。

C#
private static readonly object _lock = new object();

lock (_lock)
{
// ここには同時に1スレッドしか入れない
}

注意点は、ロック対象にpublicなオブジェクトや文字列を使わないことです。

C#
// 避けるべき例
lock ("singleton")
{
}

ロック対象は、外部から参照できないprivate static readonly objectを使うのが基本です。

C#
private static readonly object _lock = new object();

また、lockの中で時間のかかる処理や外部I/Oを行うと、他のスレッドが待たされます。シングルトンの初期化処理は、できるだけ短く、失敗しにくい処理にしておくことが大切です。

4-3. Lazy<T>がスレッドセーフに向いている理由

Lazy<T>は、遅延初期化を簡潔に実装するためのクラスです。C#のシングルトンでは、インスタンス生成をLazy<T>に任せることで、自前のlock処理を減らせます。

C#
private static readonly Lazy<MySingleton> _instance =
new Lazy<MySingleton>(() => new MySingleton());

このように書けば、_instance.Valueへ初めてアクセスされたタイミングでMySingletonが生成されます。既定のLazy<T>はスレッドセーフであり、複数スレッドから同時にアクセスされても同じ値を共有するため、シングルトン実装に向いています。Microsoft Learn

ただし、Lazy<T>が守ってくれるのは「初期化処理」のスレッドセーフ性です。生成されたオブジェクトのメソッドやフィールド操作がすべて自動でスレッドセーフになるわけではありません。状態を変更するシングルトンでは、内部状態の保護も別途考える必要があります。

4-4. static初期化がスレッドセーフになる仕組み

C#では、staticフィールドやstaticコンストラクタの初期化はランタイムによって制御されます。

たとえば、次のような実装です。

C#
public sealed class StaticSingleton
{
public static StaticSingleton Instance { get; } = new StaticSingleton();

private StaticSingleton()
{
}
}

このコードは非常に短く、実用的です。staticコンストラクタはアプリケーションドメイン内で最大1回呼び出され、型に基づくロック領域で実行されるため、static初期化では追加のロックを自分で書かなくてもよいとされています。Microsoft Learn

ただし、staticコンストラクタやstatic初期化子の中で、タスク待機、スレッド待機、明示的なロック、重い並列処理などを行うとデッドロックのリスクがあります。初期化処理はできるだけ単純にしておきましょう。

4-5. スレッドセーフなシングルトンの実装例

実務で使いやすいスレッドセーフなC#シングルトンの例は次のとおりです。

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

public static AppSettings Instance => _instance.Value;

public string ApplicationName { get; }

private AppSettings()
{
ApplicationName = "Sample Application";
}
}

利用例です。

C#
Console.WriteLine(AppSettings.Instance.ApplicationName);

この実装のポイントは次の3つです。

  1. コンストラクタをprivateにして外部生成を防ぐ

  2. Lazy<T>で初回アクセス時に生成する

  3. Instanceプロパティで唯一のインスタンスを返す

初心者がC#でシングルトンを書くなら、この形を基本として覚えるとよいでしょう。

5. Lazy<T>を使ったC#シングルトンの実装

5-1. Lazy<T>とは何か

Lazy<T>は、オブジェクトの生成を「実際に必要になるまで遅らせる」ためのクラスです。

通常、次のようにフィールドへ直接代入すると、そのクラスが初期化されるタイミングでオブジェクトが生成されます。

C#
private static readonly ExpensiveService _service = new ExpensiveService();

一方、Lazy<T>を使うと、Valueにアクセスされるまで生成を遅らせられます。

C#
private static readonly Lazy<ExpensiveService> _service =
new Lazy<ExpensiveService>(() => new ExpensiveService());
C#
var service = _service.Value;

重い初期化処理を持つクラスや、使われない可能性があるクラスでは、遅延初期化が役立ちます。

5-2. Lazy<T>で遅延初期化する書き方

Lazy<T>を使ったシングルトンの基本形は次のとおりです。

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

public static DatabaseConfig Instance => _instance.Value;

public string ConnectionString { get; }

private DatabaseConfig()
{
ConnectionString = "Server=localhost;Database=SampleDb;";
}
}

使う側は、通常のシングルトンと同じようにInstanceへアクセスします。

C#
var config = DatabaseConfig.Instance;
Console.WriteLine(config.ConnectionString);

DatabaseConfigのインスタンスは、Instanceが初めて呼ばれたタイミングで作成されます。

5-3. Lazy<T>を使うメリット

Lazy<T>を使うメリットは、主に次の3つです。

メリット内容
遅延初期化できる必要になるまでインスタンスを作らない
コードが簡潔自前の複雑なロック処理を減らせる
既定でスレッドセーフ複数スレッドからの初期化競合に対応しやすい

特に初心者にとっては、double-check lockingよりも読みやすく、実装ミスを減らしやすい点が大きなメリットです。

5-4. Lazy<T>使用時の注意点

Lazy<T>を使うときは、次の点に注意しましょう。

1つ目は、初期化中に例外が発生する可能性です。コンストラクタ内でファイル読み込みやネットワーク通信などを行うと、Valueへアクセスしたタイミングで例外が発生する可能性があります。

C#
private MySingleton()
{
// ここで例外が起きると、Instanceアクセス時に失敗する
}

2つ目は、生成されたオブジェクト自体のスレッドセーフ性です。Lazy<T>は初期化の競合を防ぐのに役立ちますが、生成後のオブジェクトの内部状態まですべて安全にしてくれるわけではありません。

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

public static CounterSingleton Instance => _instance.Value;

private int _count;

private CounterSingleton()
{
}

public void Increment()
{
_count++; // 複数スレッドでは競合する可能性がある
}
}

このような状態変更を行う場合は、lockやスレッドセーフなコレクション、Interlockedなどを検討する必要があります。

5-5. 初心者におすすめのシングルトン実装例

初心者におすすめのC#シングルトン実装は、次の形です。

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

public static Logger Instance => _instance.Value;

private Logger()
{
}

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

利用例です。

C#
Logger.Instance.Info("アプリケーションを開始しました。");

このコードは、次の条件を満たしています。

  • 外部からnewできない

  • インスタンスが1つだけ作られる

  • 初回アクセスまで生成を遅らせられる

  • 複雑なlock処理を書かなくてよい

  • 読みやすく保守しやすい

ただし、実務でログを扱う場合は、自作ロガーではなく、ILogger<T>などフレームワーク標準の仕組みを使うことも多いです。シングルトンの学習例としてはわかりやすいですが、実際のプロジェクトでは既存のロギング基盤やDIとの相性も考えましょう。

6. C#シングルトンとstaticクラスの違い

6-1. staticクラスとは何か

C#のstaticクラスは、インスタンス化できないクラスです。すべてのメンバーがstaticであり、次のようにクラス名から直接呼び出します。

C#
public static class MathUtility
{
public static int Add(int x, int y)
{
return x + y;
}
}

利用例です。

C#
int result = MathUtility.Add(10, 20);

staticクラスは、状態を持たない便利関数をまとめる場合に向いています。たとえば、文字列変換、数値計算、フォーマット処理などです。

6-2. シングルトンとstaticクラスの使い分け

シングルトンとstaticクラスは似ているように見えますが、設計上の性質が異なります。

比較項目シングルトンstaticクラス
インスタンス1つだけ存在するインスタンスを持たない
インターフェース実装可能不可
継承設計次第で可能だが通常は避ける不可
DIとの相性比較的よい悪い
モック化工夫すれば可能難しい
主な用途共有サービス、状態を持つサービスユーティリティ関数

単純な便利関数ならstaticクラス、アプリ全体で共有したいサービスならシングルトン、さらにテストやDIを考えるならDIコンテナのAddSingletonを使う、という判断が基本です。

6-3. 継承・インターフェース・DI対応の違い

シングルトンは通常のクラスとして定義できるため、インターフェースを実装できます。

C#
public interface IClock
{
DateTime Now { get; }
}

public sealed class SystemClock : IClock
{
public DateTime Now => DateTime.Now;
}

このようにインターフェースを使えば、テスト時に別の実装へ差し替えやすくなります。

C#
public sealed class FakeClock : IClock
{
public DateTime Now => new DateTime(2026, 1, 1);
}

一方、staticクラスはインターフェースを実装できません。そのため、テスト時に差し替えるのが難しくなります。

ASP.NET Coreでは、インターフェースと実装をDIコンテナに登録し、利用するクラスへコンストラクタインジェクションするのが一般的です。ASP.NET CoreはDIをサポートしており、インターフェースや基底クラスを使って依存関係の実装を抽象化し、サービスコンテナーへ登録し、使用するクラスへ挿入する流れが説明されています。Microsoft Learn

6-4. staticクラスを選ぶべきケース

staticクラスを選ぶべきなのは、状態を持たない処理をまとめたい場合です。

たとえば、次のような処理です。

C#
public static class StringHelper
{
public static bool IsNullOrWhiteSpace(string? value)
{
return string.IsNullOrWhiteSpace(value);
}

public static string ToSafeString(string? value)
{
return value ?? string.Empty;
}
}

このような処理は、インスタンスを持つ必要がありません。むしろ、シングルトンにすると設計が大げさになります。

staticクラスに向いているものは、次のような特徴があります。

  • 状態を持たない

  • 外部リソースに依存しない

  • テストで差し替える必要が少ない

  • 単純な計算や変換処理である

6-5. シングルトンを選ぶべきケース

シングルトンを選ぶべきなのは、インスタンスとして扱いたい理由がある場合です。

たとえば、次のようなケースです。

  • インターフェースを実装したい

  • DIコンテナに登録したい

  • テスト時にモックへ差し替えたい

  • 内部状態や初期化処理を持つ

  • アプリ全体で共有するサービスとして扱いたい

たとえば、キャッシュサービスはインスタンスとして扱うほうが自然です。

C#
public interface ICacheService
{
string? Get(string key);
void Set(string key, string value);
}

このようにインターフェースを定義しておけば、本番ではメモリキャッシュ、テストではフェイクキャッシュに差し替えることができます。

7. C#のDIコンテナでシングルトンを扱う方法

7-1. DIとは何か

DIとはDependency Injectionの略で、日本語では依存性注入と呼ばれます。

簡単に言うと、クラスの中で依存するオブジェクトを直接newするのではなく、外部から渡してもらう設計方法です。

悪い例です。

C#
public class OrderService
{
private readonly Logger _logger = new Logger();

public void CreateOrder()
{
_logger.Info("注文を作成しました。");
}
}

このコードでは、OrderServiceLoggerに強く依存しています。テスト時に別のロガーへ差し替えるのが難しくなります。

DIを使う例です。

C#
public class OrderService
{
private readonly ILogger _logger;

public OrderService(ILogger logger)
{
_logger = logger;
}

public void CreateOrder()
{
_logger.Info("注文を作成しました。");
}
}

依存するオブジェクトをコンストラクタで受け取ることで、クラスの依存関係が明確になり、テストもしやすくなります。

7-2. AddSingletonの基本的な使い方

ASP.NET Coreや.NETのDIでは、サービスをシングルトンとして登録するためにAddSingletonを使います。

C#
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();

このように登録すると、ICacheServiceが要求されたときにMemoryCacheServiceのインスタンスが作られ、その後は同じインスタンスが使い回されます。Microsoftのドキュメントでは、シングルトン有効期間のサービスは初めて要求されたとき、または開発者が実装インスタンスを直接提供したときに作成され、その後の要求では同じインスタンスが使用されると説明されています。Microsoft Learn

利用する側は、コンストラクタでインターフェースを受け取ります。

C#
public class ProductController
{
private readonly ICacheService _cacheService;

public ProductController(ICacheService cacheService)
{
_cacheService = cacheService;
}
}

この形にすると、自作のSingleton.Instanceへ直接アクセスするよりも、依存関係が明確になり、テストしやすくなります。

7-3. AddTransient・AddScoped・AddSingletonの違い

DIのライフタイムには、主にAddTransientAddScopedAddSingletonがあります。

登録方法インスタンスの作られ方主な用途
AddTransient要求されるたびに新しいインスタンスを作る軽量で状態を持たないサービス
AddScoped1つのスコープごとに1つ作るWebリクエスト単位の処理、DbContextなど
AddSingletonアプリ全体で同じインスタンスを使う設定、キャッシュ、共有サービス

MicrosoftのDIライフタイムの説明では、Transientはサービスコンテナーから要求されるたびに作成され、ScopedはWebアプリケーションではクライアント要求ごとに1回作成され、Singletonは後続の要求すべてに同じインスタンスが使用されるとされています。Microsoft Learn

選び方の目安は次のとおりです。

  • 毎回新しく作りたいならAddTransient

  • リクエスト単位で共有したいならAddScoped

  • アプリ全体で共有したいならAddSingleton

7-4. ASP.NET Coreでのシングルトン登録例

ASP.NET Coreでシングルトンサービスを登録する例を見てみましょう。

まず、インターフェースを定義します。

C#
public interface IApplicationCounter
{
int Increment();
}

実装クラスを作ります。

C#
public class ApplicationCounter : IApplicationCounter
{
private int _count;

public int Increment()
{
return Interlocked.Increment(ref _count);
}
}

Program.csで登録します。

C#
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IApplicationCounter, ApplicationCounter>();

var app = builder.Build();

app.MapGet("/count", (IApplicationCounter counter) =>
{
return counter.Increment();
});

app.Run();

この例では、アプリケーション全体で同じApplicationCounterインスタンスが使われます。複数リクエストから同時に呼ばれる可能性があるため、カウント更新にはInterlocked.Incrementを使っています。

DIでシングルトンを登録する場合、シングルトンサービスはスレッドセーフである必要があり、多くの場合ステートレスサービスで使われるとMicrosoftのドキュメントでも説明されています。Microsoft Learn

7-5. DIでシングルトンを使うメリット

DIでシングルトンを使うメリットは、自作のInstance管理を減らせることです。

主なメリットは次のとおりです。

メリット内容
依存関係が明確になるコンストラクタを見れば必要なサービスがわかる
テストしやすいインターフェース経由でモックに差し替えやすい
ライフタイム管理を任せられる生成と破棄をコンテナが管理する
ASP.NET Coreと相性がよい標準機能として統合されている

特にASP.NET Coreでは、自作シングルトンよりもDIコンテナのAddSingletonを使うほうが自然です。コンテナがライフタイムを管理するため、アプリケーション全体の設計も整理しやすくなります。

7-6. DI利用時に避けるべき設計

DIでシングルトンを使うときに避けるべきなのは、シングルトンからScopedサービスを直接依存する設計です。

たとえば、次のような設計は避けるべきです。

C#
public class SingletonService
{
private readonly MyDbContext _dbContext;

public SingletonService(MyDbContext dbContext)
{
_dbContext = dbContext;
}
}

DbContextは一般的にリクエスト単位のScopedとして扱われます。シングルトンがScopedサービスを保持すると、リクエストごとの状態が正しく扱えなくなる可能性があります。Microsoftのドキュメントでも、シングルトンからスコープ付きサービスを直接解決すると、スコープ付きサービスがシングルトンのように動作し、後続の要求で不正な状態が発生する可能性があると説明されています。Microsoft Learn

必要な場合は、シングルトンの中でIServiceScopeFactoryを使って明示的にスコープを作るなど、ライフタイムを意識した設計にしましょう。

8. C#シングルトンの使いどころと具体例

8-1. 設定情報の管理

設定情報は、シングルトンの代表的な利用例です。

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

public static AppConfig Instance => _instance.Value;

public string AppName { get; }
public int MaxRetryCount { get; }

private AppConfig()
{
AppName = "SampleApp";
MaxRetryCount = 3;
}
}

利用例です。

C#
Console.WriteLine(AppConfig.Instance.AppName);
Console.WriteLine(AppConfig.Instance.MaxRetryCount);

ただし、ASP.NET CoreではIOptions<T>や設定システムを使うことが多いため、自作シングルトンで設定を管理する必要は少なくなっています。学習用としてはわかりやすい例ですが、実務ではフレームワーク標準の設定管理も検討しましょう。

8-2. ログ出力クラス

ログ出力もシングルトンの例としてよく登場します。

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

public static SimpleLogger Instance => _instance.Value;

private readonly object _lock = new object();

private SimpleLogger()
{
}

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

利用例です。

C#
SimpleLogger.Instance.Info("処理を開始しました。");

この例では、複数スレッドから同時にログ出力される可能性を考え、lockを使っています。

ただし、実務ではMicrosoft.Extensions.Loggingなどのロギング機能を使うのが一般的です。自作のログシングルトンは、学習用や小規模ツールでは便利ですが、大規模アプリでは拡張性や設定管理の面で不利になることがあります。

8-3. キャッシュ管理

キャッシュ管理は、シングルトンと相性がよいケースがあります。アプリ全体で同じキャッシュを共有したい場合です。

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

public static SimpleCache Instance => _instance.Value;

private readonly Dictionary<string, string> _cache = new Dictionary<string, string>();
private readonly object _lock = new object();

private SimpleCache()
{
}

public void Set(string key, string value)
{
lock (_lock)
{
_cache[key] = value;
}
}

public string? Get(string key)
{
lock (_lock)
{
return _cache.TryGetValue(key, out var value) ? value : null;
}
}
}

利用例です。

C#
SimpleCache.Instance.Set("user:1", "Taro");
var value = SimpleCache.Instance.Get("user:1");

キャッシュは状態を持つため、スレッドセーフ性やメモリ使用量に注意が必要です。特にWebアプリでは、キャッシュが増え続けてメモリを圧迫しないように、有効期限や削除ルールを設ける必要があります。

8-4. アプリ全体で共有するサービス

アプリ全体で共有するサービスも、シングルトンの候補になります。

たとえば、アプリケーションの稼働状態を管理するサービスです。

C#
public interface IApplicationStatus
{
bool IsMaintenanceMode { get; }
void SetMaintenanceMode(bool enabled);
}

public class ApplicationStatus : IApplicationStatus
{
private volatile bool _isMaintenanceMode;

public bool IsMaintenanceMode => _isMaintenanceMode;

public void SetMaintenanceMode(bool enabled)
{
_isMaintenanceMode = enabled;
}
}

DIに登録する場合は次のようにします。

C#
builder.Services.AddSingleton<IApplicationStatus, ApplicationStatus>();

このように、インターフェースを定義してDIに登録すれば、アプリ全体で共有しつつ、テスト時の差し替えもしやすくなります。

8-5. Unityやゲーム開発での利用例

Unityなどのゲーム開発では、ゲーム全体を管理するGameManagerや音声を管理するSoundManagerをシングルトンとして扱う例がよくあります。

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

public static GameManager Instance => _instance.Value;

public int Score { get; private set; }

private GameManager()
{
}

public void AddScore(int point)
{
Score += point;
}
}

利用例です。

C#
GameManager.Instance.AddScore(100);
Console.WriteLine(GameManager.Instance.Score);

ただし、ゲーム開発ではシーン切り替え、オブジェクト破棄、ライフサイクル管理などが関係します。Unityの場合はMonoBehaviourのライフサイクルもあるため、通常のC#シングルトンと同じ感覚で使うと問題が起きることがあります。

ゲーム開発でシングルトンを使う場合も、何でもManager.Instanceに詰め込むのではなく、責務を明確に分けることが大切です。

9. C#シングルトンの注意点とアンチパターン

9-1. グローバル状態になりやすい問題

シングルトンは、どこからでもアクセスできるように設計されることが多いため、グローバル変数のように使われがちです。

C#
UserSession.Instance.UserName = "Taro";

このようなコードがアプリのあちこちにあると、どこで状態が変更されたのか追いにくくなります。

グローバル状態が増えると、次のような問題が起きます。

  • 処理の順番によって結果が変わる

  • バグの原因を追跡しにくい

  • テストごとに状態をリセットする必要がある

  • 並列実行で予期しない競合が起きる

シングルトンを使う場合は、できるだけ状態を持たない設計にするか、状態変更の範囲を明確にしましょう。

9-2. テストしにくくなる理由

シングルトンを直接呼び出すコードは、単体テストが難しくなりやすいです。

C#
public class OrderService
{
public void CreateOrder()
{
Logger.Instance.Info("注文作成");
}
}

このコードでは、OrderServiceLogger.Instanceに直接依存しています。テスト時にログ出力を止めたり、別のロガーに差し替えたりするのが難しくなります。

テストしやすくするには、インターフェースを使い、コンストラクタで依存を渡す設計にします。

C#
public class OrderService
{
private readonly ILogger _logger;

public OrderService(ILogger logger)
{
_logger = logger;
}

public void CreateOrder()
{
_logger.Info("注文作成");
}
}

この形なら、テスト時にFakeLoggerを渡せます。

9-3. 依存関係が見えにくくなる問題

シングルトンを直接参照すると、クラスが何に依存しているのかが見えにくくなります。

C#
public class PaymentService
{
public void Pay()
{
AppConfig.Instance.Load();
Logger.Instance.Info("支払い処理");
CacheManager.Instance.Clear();
}
}

このようにメソッド内で複数のシングルトンを直接呼び出すと、コンストラクタを見ても依存関係がわかりません。コードを読んで初めて依存が見えてくるため、保守性が下がります。

依存関係は、できるだけコンストラクタに明示するのがおすすめです。

C#
public class PaymentService
{
private readonly IAppConfig _config;
private readonly ILogger _logger;
private readonly ICacheManager _cacheManager;

public PaymentService(
IAppConfig config,
ILogger logger,
ICacheManager cacheManager)
{
_config = config;
_logger = logger;
_cacheManager = cacheManager;
}
}

9-4. 乱用すると保守性が下がる理由

シングルトンを乱用すると、アプリケーション全体が密結合になります。

たとえば、何でもApplicationManager.Instanceに追加していくと、巨大な神クラスになりがちです。

C#
ApplicationManager.Instance.UserService.DoSomething();
ApplicationManager.Instance.OrderService.DoSomething();
ApplicationManager.Instance.PaymentService.DoSomething();
ApplicationManager.Instance.CacheService.DoSomething();

このような設計では、責務が曖昧になり、修正の影響範囲が広がります。

シングルトンを使う場合でも、次の原則を守りましょう。

  • 1クラス1責務にする

  • 何でも管理するManagerを作らない

  • 直接参照ではなくDIを検討する

  • 状態を持たせすぎない

  • テストで差し替えられる設計にする

9-5. シングルトンを避けたほうがよいケース

次のようなケースでは、シングルトンを避けたほうがよいです。

避けたほうがよいケース理由
ユーザーごとに状態が異なる全体共有すると別ユーザーの状態が混ざる
リクエストごとに状態が異なるWebアプリではScopedのほうが適切
テストごとに状態を分離したいシングルトンは状態が残りやすい
頻繁に差し替える依存があるDIとインターフェースのほうが向いている
スレッドセーフにできない状態を持つ競合や不整合が起きやすい

特にASP.NET Coreでは、リクエストごとのデータをシングルトンに持たせてはいけません。ユーザー情報、リクエスト情報、DbContextなどは、シングルトンではなくScopedで扱うべき代表例です。

10. C#シングルトンをテストしやすくする設計

10-1. インターフェースを使って依存を分離する

シングルトンをテストしやすくするには、まずインターフェースを定義します。

C#
public interface IDateTimeProvider
{
DateTime Now { get; }
}

本番用の実装です。

C#
public class DateTimeProvider : IDateTimeProvider
{
public DateTime Now => DateTime.Now;
}

テスト用の実装です。

C#
public class FakeDateTimeProvider : IDateTimeProvider
{
public DateTime Now { get; set; } = new DateTime(2026, 1, 1);
}

利用するクラスでは、具体クラスではなくインターフェースに依存します。

C#
public class ReportService
{
private readonly IDateTimeProvider _dateTimeProvider;

public ReportService(IDateTimeProvider dateTimeProvider)
{
_dateTimeProvider = dateTimeProvider;
}

public string CreateReportName()
{
return $"Report_{_dateTimeProvider.Now:yyyyMMdd}";
}
}

これにより、テスト時に現在時刻を固定できます。

10-2. DIコンテナで依存関係を管理する

ASP.NET Coreでは、DIコンテナにシングルトンとして登録できます。

C#
builder.Services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
builder.Services.AddTransient<ReportService>();

ReportServiceIDateTimeProviderをコンストラクタで受け取ります。

C#
public class ReportService
{
private readonly IDateTimeProvider _dateTimeProvider;

public ReportService(IDateTimeProvider dateTimeProvider)
{
_dateTimeProvider = dateTimeProvider;
}
}

この形なら、ReportServiceDateTimeProviderがシングルトンなのか、Transientなのかを意識する必要がありません。ライフタイム管理はDIコンテナに任せられます。

10-3. モック化しやすい設計にする

テストでは、インターフェースを使ってモックやフェイクを渡します。

C#
public class FakeLogger : ILogger
{
public List<string> Messages { get; } = new List<string>();

public void Info(string message)
{
Messages.Add(message);
}
}

テスト対象のクラスです。

C#
public class UserService
{
private readonly ILogger _logger;

public UserService(ILogger logger)
{
_logger = logger;
}

public void CreateUser(string name)
{
_logger.Info($"{name}を作成しました。");
}
}

テスト例です。

C#
var logger = new FakeLogger();
var service = new UserService(logger);

service.CreateUser("Taro");

Console.WriteLine(logger.Messages.Count); // 1

Logger.Instanceを直接呼ぶ設計だと、このような差し替えが難しくなります。C#でシングルトンを使う場合でも、利用側からはインターフェースとして見せる設計を意識しましょう。

10-4. 単体テストで困らないための実装方針

単体テストで困らないためには、次の方針が有効です。

方針理由
Instanceを直接呼ばない依存関係を差し替えにくくなるため
インターフェースを使うモックやフェイクに置き換えやすいため
DIを使うライフタイムと依存関係を管理しやすいため
状態を持たせすぎないテスト間で状態が残りにくくなるため
リセット前提の設計を避けるテスト順序に依存しにくくなるため

シングルトンそのものが悪いわけではありません。問題は、どこからでも直接呼べるグローバル状態として使いすぎることです。テストしやすい設計にするには、依存関係を明示し、差し替え可能にしておくことが重要です。

11. C#シングルトン実装でよくあるエラーと対処法

11-1. 複数インスタンスが生成されてしまう

よくあるミスは、遅延初期化をスレッドセーフにしていないことです。

C#
if (_instance == null)
{
_instance = new Singleton();
}

この実装では、複数スレッドが同時にアクセスした場合に、複数インスタンスが作られる可能性があります。

対処法は、Lazy<T>を使うか、static初期化を使うことです。

C#
private static readonly Lazy<Singleton> _instance =
new Lazy<Singleton>(() => new Singleton());

public static Singleton Instance => _instance.Value;

または、シンプルに次のようにします。

C#
public static Singleton Instance { get; } = new Singleton();

11-2. コンストラクタをpublicにしてしまう

コンストラクタがpublicのままだと、外部から自由にインスタンスを作れてしまいます。

C#
public class Singleton
{
public Singleton()
{
}
}

これではシングルトンになりません。

正しくはprivateにします。

C#
public sealed class Singleton
{
private Singleton()
{
}
}

外部からnewできないようにすることが、シングルトン実装の基本です。

11-3. スレッドセーフではない実装を使ってしまう

シングルトンのインスタンス生成だけでなく、内部状態の変更もスレッドセーフにする必要があります。

危険な例です。

C#
public class Counter
{
public int Count { get; private set; }

public void Increment()
{
Count++;
}
}

Count++は一見単純ですが、読み取り、加算、書き込みの複数ステップで構成されます。複数スレッドから同時に呼ばれると、カウントが正しく増えない可能性があります。

対処法の例です。

C#
public class Counter
{
private int _count;

public int Increment()
{
return Interlocked.Increment(ref _count);
}
}

シングルトンは共有されるため、状態を変更する場合は通常のクラス以上にスレッドセーフ性を意識する必要があります。

11-4. 初期化タイミングが想定と違う

eager initializationでは、実際に使う前にインスタンスが生成されることがあります。

C#
private static readonly Singleton _instance = new Singleton();

一方、Lazy<T>ではValueにアクセスするまで生成されません。

C#
private static readonly Lazy<Singleton> _instance =
new Lazy<Singleton>(() => new Singleton());

初期化タイミングを明確にしたい場合は、次のように考えます。

目的選ぶ実装
シンプルにしたいstatic初期化
初回アクセスまで生成したくないLazy<T>
初期化エラーを起動時に検知したいeager initialization
重い初期化を必要時まで遅らせたいLazy<T>

初期化タイミングの違いは、パフォーマンスだけでなく、例外が発生するタイミングにも影響します。

11-5. DIのライフタイム設定を間違える

DIでは、ライフタイム設定の間違いもよくある問題です。

たとえば、状態を持つサービスをAddSingletonにしてしまうと、全リクエストで状態が共有されてしまいます。

C#
builder.Services.AddSingleton<UserContext>();

ユーザーごとの情報を持つサービスなら、SingletonではなくScopedを検討すべきです。

C#
builder.Services.AddScoped<UserContext>();

また、シングルトンサービスがScopedサービスに依存する設計も避ける必要があります。シングルトンからスコープ付きサービスを直接解決すると、Scopedサービスがシングルトンのように扱われ、不正な状態につながる可能性があります。Microsoft Learn

DIのライフタイムは、次の基準で選ぶと判断しやすくなります。

  • アプリ全体で1つならSingleton

  • リクエストごとに1つならScoped

  • 毎回新しく作るならTransient

12. C#シングルトンに関するよくある質問

12-1. C#でシングルトンは非推奨ですか?

C#でシングルトンが完全に非推奨というわけではありません。設定、キャッシュ、共有サービスなど、アプリ全体で1つのインスタンスを共有したい場面では有効です。

ただし、どこからでも直接アクセスできるグローバル状態として乱用すると、テストしにくく、保守しにくいコードになります。

特にASP.NET Coreでは、自作のSingleton.Instanceよりも、DIコンテナでAddSingleton登録する設計が推奨される場面が多いです。DIコンテナを使えば、依存関係を明示し、ライフタイム管理もコンテナに任せられます。

12-2. シングルトンとstaticはどちらを使うべきですか?

状態を持たない単純な便利関数ならstaticクラスで十分です。

C#
public static class NumberHelper
{
public static bool IsEven(int value) => value % 2 == 0;
}

一方、インターフェースを使いたい、DIで管理したい、テスト時に差し替えたい、内部状態や初期化処理を持つ、といった場合はシングルトンやDIのAddSingletonが向いています。

判断基準は次のとおりです。

やりたいこと選択肢
単純な共通関数をまとめたいstaticクラス
1つのインスタンスを共有したいシングルトン
ASP.NET Coreで共有サービスを使いたいDIのAddSingleton
テストで差し替えたいインターフェース + DI

12-3. Lazy<T>を使えば必ず安全ですか?

Lazy<T>を使えば、インスタンスの遅延初期化は安全にしやすくなります。既定のLazy<T>はスレッドセーフで、複数スレッドからアクセスされた場合でも同じ値を共有するように設計されています。Microsoft Learn

ただし、生成されたオブジェクトの内部処理がすべて安全になるわけではありません。

たとえば、シングルトン内でList<T>Dictionary<TKey, TValue>を更新する場合、複数スレッドから同時に書き込まれると問題が起きる可能性があります。

つまり、Lazy<T>は「生成を安全にする仕組み」であり、「中身の処理をすべて安全にする仕組み」ではありません。状態を変更する場合は、別途スレッドセーフな設計が必要です。

12-4. ASP.NET Coreでは自作シングルトンとDIのどちらがよいですか?

ASP.NET Coreでは、基本的にDIコンテナのAddSingletonを使うほうがおすすめです。

理由は次のとおりです。

  • 依存関係がコンストラクタで明確になる

  • テスト時にモックへ差し替えやすい

  • ライフタイムをコンテナが管理できる

  • フレームワーク標準の設計に沿える

  • ScopedTransientとの使い分けがしやすい

自作シングルトンは、小さなコンソールアプリや学習用途ではわかりやすいですが、ASP.NET CoreのようなDI前提の環境では、AddSingletonのほうが保守しやすい設計になりやすいです。

12-5. シングルトンを使わない代替手段はありますか?

あります。代表的な代替手段は次のとおりです。

代替手段内容
DIのAddSingleton自作せずにコンテナで単一インスタンスを管理する
DIのAddScopedリクエスト単位で共有する
DIのAddTransient必要なたびに新しいインスタンスを作る
staticクラス状態を持たない共通処理をまとめる
ファクトリパターン必要に応じて生成方法を切り替える
Optionsパターン設定情報を型安全に扱う

「どこからでもアクセスしたいからシングルトンにする」のではなく、「どのライフタイムが適切か」「依存関係をどう見せるか」「テストで差し替えられるか」を基準に選ぶことが大切です。

まとめ

C#のシングルトンは、アプリケーション内でインスタンスを1つだけに制限し、同じインスタンスを共有するための設計パターンです。設定情報、ログ出力、キャッシュ管理、共有サービスなどで利用されることがあります。

基本的な実装では、コンストラクタをprivateにして外部からの生成を防ぎ、staticフィールドやInstanceプロパティで唯一のインスタンスを返します。

C#でシングルトンを実装する場合、初心者には次のどちらかがおすすめです。

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

private Singleton()
{
}
}

または、遅延初期化したい場合はLazy<T>を使います。

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

public static Singleton Instance => _instance.Value;

private Singleton()
{
}
}

Lazy<T>は既定でスレッドセーフに扱えるため、C#のシングルトン実装でよく使われます。ただし、生成後のオブジェクト内部の状態変更まで自動で安全になるわけではないため、状態を持つ場合は別途スレッドセーフ性を考える必要があります。

また、ASP.NET Coreでは自作のシングルトンよりも、DIコンテナのAddSingletonを使う設計が多くの場面で適しています。

C#
builder.Services.AddSingleton<IMyService, MyService>();

DIを使うことで、依存関係が明確になり、テストしやすく、ライフタイム管理もしやすくなります。

シングルトンは便利ですが、乱用するとグローバル状態が増え、テストや保守が難しくなります。C#でシングルトンを使うときは、「本当に1つだけでよいのか」「DIで管理したほうがよくないか」「状態を持たせすぎていないか」を確認しながら設計することが大切です。