C#のシングルトン:{ get; } = new … の1行と if 文(遅延初期化)はどう違う?
シングルトンそのものが初めての方は、C#のシングルトンとは?「1つだけ」を保証する仕組みをやさしく理解するを先に読むと、この記事が理解しやすくなります。
シングルトンを学ぶと、書き方が2種類出てきて迷うことがあります。
// 書き方A:1行で書く
public static OwnedItemsData Instance { get; } = new OwnedItemsData();
// 書き方B:if 文で書く
private static OwnedItemsData instance;
public static OwnedItemsData Instance
{
get
{
if (instance == null)
{
instance = new OwnedItemsData();
}
return instance;
}
}
どちらも「クラスの中に、インスタンスを1つだけ持つ」という点は同じです。違いはいつ作るかと安全さにあります。この記事では、その違いと使い分けを整理します。
この記事は、【Unity入門】アイテムを拾って持ち物に表示する最小コードで使った OwnedItemsData の書き方を、もう少し詳しく説明したものです。持ち物の仕組み全体の考え方は、【Unity入門】アイテム所持の仕組みを作ろう ― 「データ」と「見た目」を分ける考え方で紹介しています。
この記事で分かること
- 書き方Aと書き方Bの違い
- if 文で書く方法を「遅延初期化」と呼ぶ理由
- どちらを選べばよいかの目安
MonoBehaviourのシングルトンで、if 文の形が必要になる理由Lazy<T>という選択肢
ポイント1:書き方A(1行)は、普通のクラスならこれで十分
普通のクラス(MonoBehaviour ではないクラス)であれば、書き方Aの1行で足ります。
public class OwnedItemsData
{
public static OwnedItemsData Instance { get; } = new OwnedItemsData();
private OwnedItemsData() { }
// 所持品のメモ帳として使うデータをここに書く
}
この書き方には、次の利点があります。
- 短くて、読みやすい
- インスタンスを作る処理は、.NET が1回だけ実行すると保証してくれる
- そのため、複数のスレッドから同時に呼ばれても、2つ作られる心配がない
初学者向けの題材なら、まずこの形を覚えれば十分です。static や new の意味に不安があるときは、なぜstaticをつけるとnewが要らなくなるのか?税込み価格計算で理解するstatic入門と、C#のクラスとインスタンスをメモリ割り当ての図で理解するを先に読むと、理解しやすくなります。
ポイント2:書き方B(if 文)は「使う直前まで作らない」書き方
書き方Bは、Instance を最初に読んだ瞬間まで、インスタンスを作るのを遅らせます。この考え方を遅延初期化(lazy initialization)と呼びます。流れは次のとおりです。
Instanceを初めて読む。このときinstanceはnullnewで作って、instanceに入れる- 2回目以降は、すでに入っている
instanceをそのまま返す
「必要になるまで作らない」ので、作るのに時間がかかる処理があるときに役立ちます。
ポイント3:2つの書き方を比べる
書き方A { get; } = new ... | 書き方B if 文 | |
|---|---|---|
| 作られる時期 | クラスが最初に使われるときまでに1回 | Instance を最初に読んだとき |
| マルチスレッド | .NET が1回だけの実行を保証する | そのままでは、2つ作られる可能性がある(lock が必要) |
| コードの量 | 1行 | 10行前後 |
特に注意したいのが、マルチスレッドの行です。書き方Bで、2つのスレッドが同時に instance == null を判定すると、どちらも new を実行してしまうことがあります。安全にするには lock を足します。
private static OwnedItemsData instance;
private static readonly object lockObject = new object();
public static OwnedItemsData Instance
{
get
{
lock (lockObject)
{
if (instance == null)
{
instance = new OwnedItemsData();
}
return instance;
}
}
}
コードが一気に長くなります。「作るのが軽いなら、書き方Aのほうが安全で簡単」という理由は、ここにあります。
ポイント4:if 文の形が向いている場面
書き方Bが力を発揮するのは、次のような場面です。
- 作るのに時間がかかる、または重い処理(ファイルの読み込みなど)を、使う直前まで遅らせたいとき
- 作るときに、条件や引数が必要なとき
MonoBehaviourのシングルトンを作るとき
ポイント5:MonoBehaviour のシングルトンでは if 文が自然に必要になる
MonoBehaviour を継承したクラスは、new で作れません。Unity では、シーンに置いた GameObject にコンポーネントとして付けて使うためです。そのため、「シーンから探して、なければ GameObject を作る」という処理が必要になり、自然に if 文の形になります。
using UnityEngine;
public class GameManager : MonoBehaviour
{
private static GameManager instance;
public static GameManager Instance
{
get
{
if (instance == null)
{
// シーンの中から探す
instance = FindFirstObjectByType<GameManager>();
if (instance == null)
{
// 見つからなければ、GameObject を作って付ける
var go = new GameObject("GameManager");
instance = go.AddComponent<GameManager>();
}
}
return instance;
}
}
}
MonoBehaviour のシングルトンを学ぶ段階になると、書き方Bの形が必要になる理由が実感できるはずです。シーンをまたいで残したいときは、DontDestroyOnLoadでプレイヤーをシーン間に保持するの記事も参考になります。
ポイント6:Lazy<T> という選択肢
C# には、Lazy<T> という「遅延初期化を安全に書く」ための仕組みがあります。
using System;
public class OwnedItemsData
{
private static readonly Lazy<OwnedItemsData> lazy =
new Lazy<OwnedItemsData>(() => new OwnedItemsData());
public static OwnedItemsData Instance => lazy.Value;
private OwnedItemsData() { }
}
「作るのは遅らせたい。でも、if 文と lock は書きたくない」というときの選択肢です。最初に Instance を読んだときに作られ、スレッドの安全性も .NET が守ってくれます。詳しい仕様は、Microsoft Learn の Lazy<T> クラスで確認できます。
まとめ:どちらを選ぶか
- 普通のクラスで、作るのが軽い → 書き方A(1行)。短くて、安全で、読みやすい
- 作るのが重い、作るときに条件や引数が必要 → 遅延初期化(if 文、または
Lazy<T>) MonoBehaviourのシングルトン → if 文の形(シーンから探して、なければ作る)
所持品のメモ帳のように、作るのが軽く条件もないクラスは、書き方Aの1行で十分です。まずは1行の形で覚えて、必要になったときに if 文の形へ進むのが、無理のない順番です。










ディスカッション
コメント一覧
まだ、コメントがありません