アプリケーション設計において、「パフォーマンス」と「保守性」を同時に満たすのは簡単ではありません。
特に、初期化コストの高い依存オブジェクトをいつ・どのタイミングで生成するかは、多くのプロジェクトで見落とされがちな設計課題です。
この記事では、.NET 標準クラスである Lazy<T> を用いて、遅延初期化によって設計の柔軟性と実行効率を両立する方法を、基礎から実務視点まで丁寧に解説します。
Lazy<T> が解決する設計上の課題
このセクションでは、遅延初期化がなぜ設計上重要なのか、そして Lazy<T> がどのような課題を解決するのかを整理します。
遅延初期化が設計にもたらすメリット
アプリケーション開発において、「すべての依存オブジェクトを起動時に初期化する」設計は一見シンプルです。
しかし、大規模システムでは以下のような問題を引き起こします。
- 初期化に時間がかかり、起動が遅くなる
- 実行中に一度も使われないオブジェクトが生成される
- メモリや外部リソースを無駄に消費する
Lazy<T> は、実際に必要になるまで初期化しないという選択肢を提供し、これらの問題を設計レベルで回避できる仕組みです。
ダブルチェックロックの限界と Lazy<T> の登場
かつて、スレッドセーフな遅延初期化を実現するために、ダブルチェックロック(Double-Checked Locking)がよく使われていました。
private static ExpensiveService _instance;
private static readonly object _lock = new object();
public static ExpensiveService Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new ExpensiveService();
}
}
}
return _instance;
}
}
このパターンは、.NET のメモリモデルを正しく理解し、volatile などを適切に用いれば理論上は正しく実装可能です。
しかし実務では、実装難易度が高く、可読性が低く、保守時の事故リスクも高いため、現在では推奨されない設計とされています。
そこで登場したのが、シンプルかつ安全に遅延初期化を実現できる Lazy<T> です。
Lazy<T> が選ばれる理由
Lazy<T> が実務で評価されている理由は次の点にあります。
.Valueにアクセスされるまで初期化されない- スレッドセーフな制御を標準で提供
- コードが簡潔で意図が読み取りやすい
これらの特長により、Lazy<T> は安全性・可読性・実装コストのバランスが取れた選択肢となっています。
現場でのユースケース
Lazy<T> は次のような場面で特に効果を発揮します。
- 高コストな初期化(DB接続、外部APIクライアントなど)
- 起動時間の最適化(キャッシュ、診断ツール)
- 条件付き機能(詳細ログ、デバッグ機能)
Lazy<T> の基本構造と使い方
ここでは、Lazy<T> の基本的な使い方と挙動をコード例で確認します。
基本的な遅延初期化の例
Lazy<ExpensiveService> service = new Lazy<ExpensiveService>(() =>
{
Console.WriteLine("★初期化処理が実行されました");
return new ExpensiveService();
});
Console.WriteLine("サービスを呼び出します...");
service.Value.DoSomething();
service.Value.DoSomething();
new Lazy<T>()の時点では初期化されません- 最初の
.Valueアクセス時に一度だけ初期化されます - 以降は同じインスタンスが再利用されます
よくある疑問と注意点
.Valueを使わないと値にはアクセスできません- 初期化は原則一度だけ実行されます
- 初期化関数が
nullを返すことも可能ですが、設計上は慎重に扱う必要があります
IsValueCreated による状態確認
if (!service.IsValueCreated)
{
Console.WriteLine("まだ初期化されていません");
}
IsValueCreated を使うことで、初期化状態を明示的に制御・可視化できます。
値型でも使えるのか
Lazy<T> はジェネリッククラスのため、参照型・値型のどちらでも使用可能です。
通常の Lazy<int> などではボックス化は発生しませんが、object やインターフェース経由で扱う場合はボックス化が発生する可能性があるため注意が必要です。
実務で必須の知識:スレッドセーフ制御
このセクションでは、Lazy<T> のスレッドセーフ機構と選択基準を解説します。
LazyThreadSafetyMode の種類
- ExecutionAndPublication:ロックあり。初期化は1回のみ(標準・推奨)
- PublicationOnly:複数回初期化される可能性あり(冪等処理向け)
- None:ロックなし(非スレッドセーフ、単一スレッド専用)
※ None は非同期ではなく、単にロックを行わない点に注意してください。
実務での判断指針
- 基本はデフォルト設定で十分
- 性能問題が明確になってから
PublicationOnlyを検討 Noneはスクリプトや試作コードに限定
DIコンテナと Lazy<T> の組み合わせ
ここでは、DI と Lazy<T> を組み合わせた実践的な設計を紹介します。
DIによる自動初期化の落とし穴
services.AddScoped<IHeavyService, HeavyService>();
public MyController(IHeavyService service) { }
この場合、実際に使われるかどうかに関係なく HeavyService は生成されます。
Lazy<T> を使った遅延解決
public MyController(Lazy<IHeavyService> service)
{
_service = service;
}
.Value にアクセスされるまで生成されません。ASP.NET Core 標準DIでは追加設定なしで利用可能です。
他のDIコンテナでは挙動が異なる場合があります。
スコープ不整合への注意
- Singleton から Scoped を
Lazy<T>で包むのは危険です - ライフサイクルは必ず一致させてください
まとめ:Lazy<T> を設計に活かすために
このセクションでは、Lazy<T> 活用時のポイントを整理します。
メリットと注意点
- 不要な初期化を排除できる
- スレッドセーフを簡潔に実装できる
- 初期化タイミングが見えにくくなる点には注意が必要
.Value呼び出しが明示的になる
実務チェックリスト
- 初期化コストが高い対象だけに使う
- 初期化ロジックは単純に保つ
- スレッドセーフモードは安易に変えない
- 使用意図をコメントや設計書に残す
最後に
Lazy<T> は万能な仕組みではありません。
しかし、適切な場面で使えば設計とパフォーマンスを同時に改善できる強力な武器です。
「いつ初期化すべきか」を意識できるエンジニアこそ、Lazy<T> を正しく使いこなせるプロフェッショナルと言えるでしょう。


コメント