初期化コストの大きなものや、使うとも限らない機能などは、最初に準備せずとも必要になったら準備できればよい。
このように初期化の契機を遅らせる実装を遅延初期化と言います。
// 大きいクラスと仮定
public class BigData { }
// クラス生成時にBigDataクラスのインスタンスを生成する
public class Sample1
{
public BigData BigData { get; } = new BigData();
}
// プロパティの初回アクセス時にインスタンスを生成する
public class Sample2
{
private BigData bigData = null;
public BigData BigData => bigData ??= new BigData();
}
Sample2では ??=演算子(Null合体割り当て演算子)を使って、bigData がnullの場合は生成することにより、初回以降は生成したbigDataインスタンスを返却しています。
ほとんどの場合はこの実装で良いのですが、問題となるのはマルチスレッドの場合です。
異なるスレッドで同時にアクセスした場合にインスタンスが違ってしまう可能性があります。
これを回避するにはスレッドセーフに実装する必要がありますが、これを簡単に実装する時に使うのがLazyです。
System.Lazy クラス
このクラスを用いることにより、簡単にスレッドセーフに実装できます。
public class Sample3
{
private Lazy<BigData> bigData = new Lazy<BigData>(() => new BigData());
// Value にアクセスされた初回のみコンストラクタで指定したデリゲートが起動して初期化処理が行われる
public BigData BigData => bigData.Value;
}
スレッドセーフの実装はコストがかかります。遅延初期化のみ行いたい場合は、コンストラクタ引数で明示的に指定すればOK
public class Sample4
{
// 引数でFalseを追加設定
private Lazy<BigData> bigData = new Lazy<BigData>(() => new BigData(),false);
public BigData BigData => bigData.Value;
}
public class Sample5
{
// 引数でLazyThreadSafetyModeのNoneを追加設定
private Lazy<BigData> bigData = new Lazy<BigData>(() => new BigData(),System.Threading.LazyThreadSafetyMode.None);
public BigData BigData => bigData.Value;
}
Trueならスレッドセーフ・Falseなら非スレッドセーフです。
次の例ではLazyThreadSafetyModeを指定しています。
LazyThreadSafetyMode列挙体は以下の3つの値を持ちます。
・ExecutionAndPublication デフォルトではこれ。True設定と同じ(スレッドセーフ)。 Double-checked-lockingという方式で排他処理が行われる
・None True設定と同じ(非スレッドセーフ)
・PublicationOnly boolean指定では不可(スレッドセーフ) Race-to-initialize という方式で排他処理が行われる(対象のインスタンス生成が複数回行われる場合がある)
System.Threding.LazyInitializer
排他は実装者側が行う方式ですね。こっちの使用機会はほとんどなさそう。
public class Sample6
{
private BigData bigData = null;
private bool _initialized = false;
private object _lockObj = new object();
public BigData BigData
{
get
{
System.Threading.LazyInitializer.EnsureInitialized
(ref bigData, ref _initialized, ref _lockObj, () => new BigData());
return bigData;
}
}
}