限定的な初期化

オブジェクトの遅延初期化とは、初めて使用されるまでオブジェクトの作成が延期されることを意味します。 (このトピックでは、遅延初期化遅延インスタンス化を同じ意味の言葉として使っています。)遅延初期化は主に、パフォーマンスの改善、無駄な計算処理の回避、プログラムのメモリ要件の縮小を目的として利用されます。 最も一般的なシナリオ:

  • あるオブジェクトを作成するとコストが高いのに、プログラムでそのオブジェクトが使用されない可能性もある。 たとえば、メモリ内の Customer オブジェクトに Orders プロパティがあるとします。このプロパティには、Order オブジェクトの大きな配列が含まれていますが、これを初期化するにはデータベースに接続する必要があります。 ユーザーが Orders データを表示しないのであれば、あるいは Orders データを計算に使用しないのであれば、システム メモリや計算処理周期を利用してそのデータを作成する理由がありません。 遅延初期化では Lazy<Orders> を利用して Orders オブジェクトを宣言することで、オブジェクトが使用されない場合のシステム リソースの無駄使いを回避できます。

  • あるオブジェクトを作成するとコストが高いとき、他の高額な演算が完了するまで、そのオブジェクトの作成を延期する。 たとえば、プログラムが起動時に複数のオブジェクト インスタンスを読み込むが、すぐに必要になるのはその中の一部のインスタンスだけという場合があります。 必要なオブジェクトが作成されるまで必要にならないオブジェクトの初期化を遅らせることで、プログラムの起動パフォーマンスを改善できます。

初期化を遅延させる独自のコードを記述できますが、代わりに Lazy<T> を使用することが推奨されます。 Lazy<T> とその関連型はスレッドセーフ対応であり、一貫性のある例外反映ポリシーを提供します。

次の表は、.NET Framework バージョン 4 で提供される、さまざまなシナリオの遅延初期化を可能にする型の一覧です。

種類 説明
Lazy<T> あらゆるクラス ライブラリやユーザー定義の型を対象に、遅延初期化セマンティクスを提供するラッパー クラス。
ThreadLocal<T> Lazy<T> に似ていますが、スレッドローカル基準の遅延初期化セマンティクスを提供する点が異なります。 すべてのスレッドがその固有の値にアクセスできます。
LazyInitializer オブジェクトの遅延初期化のために高度な static メソッドを提供します (Visual Basic の場合は Shared)。クラスのオーバーヘッドがありません。

基本的な遅延初期化

MyType のような、遅延初期化型を定義するには、次の例のように、Lazy<MyType> を使用します (Visual Basic の場合は Lazy(Of MyType))。 Lazy<T> コンストラクターにデリゲートが渡されない場合、値プロパティの最初のアクセスで Activator.CreateInstance を使用することで、ラップされた型が作成されます。 型にパラメーターのないコンストラクターがない場合、実行時例外がスローされます。

次の例では、Orders は、データベースから取得された Order オブジェクトの配列を含むクラスであると想定しています。 Customer オブジェクトには Orders のインスタンスが含まれていますが、ユーザーの操作によっては、Orders オブジェクトからのデータは必要ないことがあります。

// Initialize by using default Lazy<T> constructor. The
// Orders array itself is not created yet.
Lazy<Orders> _orders = new Lazy<Orders>();
' Initialize by using default Lazy<T> constructor. The 
'Orders array itself is not created yet.
Dim _orders As Lazy(Of Orders) = New Lazy(Of Orders)()

次の例のように、作成時に特定のコンストラクター オーバーロードを呼び出す Lazy<T> コンストラクターにデリゲートを渡し、他にも初期化手順が必要であればそれを実行することもできます。

// Initialize by invoking a specific constructor on Order when Value
// property is accessed
Lazy<Orders> _orders = new Lazy<Orders>(() => new Orders(100));
' Initialize by invoking a specific constructor on Order 
' when Value property is accessed
Dim _orders As Lazy(Of Orders) = New Lazy(Of Orders)(Function() New Orders(100))

遅延オブジェクトの作成後、遅延変数の Value プロパティが初めてアクセスされるまで、Orders のインスタンスは作成されません。 最初のアクセスで、ラップされた型が作成され、返され、今後のアクセスのために保存されます。

// We need to create the array only if displayOrders is true
if (displayOrders == true)
{
    DisplayOrders(_orders.Value.OrderData);
}
else
{
    // Don't waste resources getting order data.
}
' We need to create the array only if _displayOrders is true
If _displayOrders = True Then
    DisplayOrders(_orders.Value.OrderData)
Else
    ' Don't waste resources getting order data.
End If

Lazy<T> オブジェクトは常に、初期化に利用されたものと同じオブジェクトまたは値を返します。 そのため、Value プロパティは読み取り専用です。 Value が参照型を保存する場合、それに新しいオブジェクトを割り当てることはできません。 (ただし、その設定可能なパブリックのフィールドとプロパティを変更できます。)Value が値型を保存する場合、その値を変更することはできません。 それでも、変数コンストラクターを呼び出し、新しい変数を作成することはできます。新しい引数を利用します。

_orders = new Lazy<Orders>(() => new Orders(10));
_orders = New Lazy(Of Orders)(Function() New Orders(10))

先に見たような新しい遅延インスタンスの場合、その Value が初めてアクセスされるまで Orders はインスタンス化されません。

スレッドセーフな初期化

既定では、Lazy<T> オブジェクトはスレッド セーフです。 つまり、コンストラクターがスレッドセーフの種類を指定しない場合、スレッドセーフな Lazy<T> オブジェクトが作成されます。 マルチスレッド シナリオの場合、スレッドセーフな Lazy<T> オブジェクトの Value プロパティにアクセスした最初のスレッドが、すべてのスレッドのすべての後続アクセスで、オブジェクトを初期化します。すべてのスレッドが同じデータを共有します。 そのため、どのスレッドがオブジェクトを初期化するかは問題ではありません。競合状態に害はありません。

Note

例外キャッシュを利用すれば、エラー状態にもこの一貫性が及びます。 詳細については、次のセクションの「遅延オブジェクトの例外」を参照してください。

次の例では、同じ Lazy<int> インスタンスで、3 つの別個のスレッドに対して同じ値が与えられています。

// Initialize the integer to the managed thread id of the
// first thread that accesses the Value property.
Lazy<int> number = new Lazy<int>(() => Thread.CurrentThread.ManagedThreadId);

Thread t1 = new Thread(() => Console.WriteLine("number on t1 = {0} ThreadID = {1}",
                                        number.Value, Thread.CurrentThread.ManagedThreadId));
t1.Start();

Thread t2 = new Thread(() => Console.WriteLine("number on t2 = {0} ThreadID = {1}",
                                        number.Value, Thread.CurrentThread.ManagedThreadId));
t2.Start();

Thread t3 = new Thread(() => Console.WriteLine("number on t3 = {0} ThreadID = {1}", number.Value,
                                        Thread.CurrentThread.ManagedThreadId));
t3.Start();

// Ensure that thread IDs are not recycled if the
// first thread completes before the last one starts.
t1.Join();
t2.Join();
t3.Join();

/* Sample Output:
    number on t1 = 11 ThreadID = 11
    number on t3 = 11 ThreadID = 13
    number on t2 = 11 ThreadID = 12
    Press any key to exit.
*/
' Initialize the integer to the managed thread id of the 
' first thread that accesses the Value property.
Dim number As Lazy(Of Integer) = New Lazy(Of Integer)(Function()
                                                          Return Thread.CurrentThread.ManagedThreadId
                                                      End Function)

Dim t1 As New Thread(Sub()
                         Console.WriteLine("number on t1 = {0} threadID = {1}",
                                           number.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t1.Start()

Dim t2 As New Thread(Sub()
                         Console.WriteLine("number on t2 = {0} threadID = {1}",
                                           number.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t2.Start()

Dim t3 As New Thread(Sub()
                         Console.WriteLine("number on t3 = {0} threadID = {1}",
                                           number.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t3.Start()

' Ensure that thread IDs are not recycled if the 
' first thread completes before the last one starts.
t1.Join()
t2.Join()
t3.Join()

' Sample Output:
'       number on t1 = 11 ThreadID = 11
'       number on t3 = 11 ThreadID = 13
'       number on t2 = 11 ThreadID = 12
'       Press any key to exit.

各スレッドで別個のデータが必要であれば、このトピックの後半で説明するように、ThreadLocal<T> 型を使用してください。

一部の Lazy<T> コンストラクターには、isThreadSafe という名前のブール値パラメーターがあります。これを利用し、複数のスレッドから Value プロパティにアクセスするかどうかを指定します。 1 つだけのスレッドからこのプロパティにアクセスする場合、適度なパフォーマンス上の利点が得られるように、false を渡します。 複数のスレッドからプロパティにアクセスする場合、true を渡し、初期化時、1 つのスレッドが例外をスローするような競合状態を適切に処理するように Lazy<T> インスタンスに指示します。

Lazy<T> の一部のコンストラクターには、mode という名前の LazyThreadSafetyMode パラメーターが含まれています。 このコンストラクターでは、スレッド セーフ モードが 1 つ増えます。 次の表は、スレッド セーフを指定するコンストラクター パラメーターの影響を Lazy<T> オブジェクトのスレッド セーフが受けるしくみをまとめたものです。 各コンストラクターに含まれるこのようなパラメーターは多くても 1 つです。

オブジェクトのスレッド セーフ LazyThreadSafetyModemode パラメーター ブール値 isThreadSafe パラメーター スレッド セーフ パラメーターなし
完全スレッドセーフ。一度に 1 つだけのスレッドが値の初期化を試行します。 ExecutionAndPublication true はい。
スレッド セーフではない。 None false 適用不可。
完全スレッドセーフ。値の初期化に際し、スレッドの競合が起こります。 PublicationOnly 適用不可。 適用不可。

表に示されているように、mode パラメーターに LazyThreadSafetyMode.ExecutionAndPublication を指定することは、isThreadSafe パラメーターに true を指定することと同じであり、LazyThreadSafetyMode.None を指定することは false を指定することと同じです。

ExecutionPublication が表す内容の詳細については、LazyThreadSafetyMode に関する記事を参照してください。

LazyThreadSafetyMode.PublicationOnly を指定すると、Lazy<T> インスタンスの初期化を複数のスレッドが試行できます。 1 つだけのスレッドがこの競合を征します。他のスレッドはすべて、競合を征したスレッドにより初期化された値を受け取ります。 初期化中、あるスレッドで例外がスローされた場合、そのスレッドは、競合を征したスレッドが設定した値を受け取ることがありません。 例外はキャッシュされません。Value プロパティへの後続のアクセス試行で初期化が行われます。 これは、他のモードでの例外の扱いとは異なります。それについては、次のセクションで説明します。 詳細については、LazyThreadSafetyMode 列挙型のページをご覧ください。

遅延オブジェクトの例外

前述のように、Lazy<T> オブジェクトは常に、初期化に使用されたものと同じオブジェクトまたは値を返します。そのため、Value プロパティは読み取り専用です。 例外キャッシュを有効にすると、この不変性は例外動作にも及びます。 ある遅延初期化オブジェクトで例外キャッシュを有効にしている場合、Value プロパティが初めてアクセスされたときにその初期化メソッドから例外がスローされると、Value プロパティに対する後続のすべてのアクセス試行で同じ例外がスローされます。 言い換えると、マルチスレッドのシナリオであっても、ラップされた型のコンストラクターが再び呼び出されることはありません。 そのため、Lazy<T> オブジェクトがあるアクセスで例外をスローし、後続のアクセスで値を返すということはありません。

初期化メソッド (valueFactory パラメーター) を取る System.Lazy<T> コンストラクターを使用すると、例外キャッシュが有効になります。たとえば、Lazy(T)(Func(T)) コンストラクターの使用時に有効になります。 そのコンストラクターは LazyThreadSafetyMode 値 (mode パラメーター) も取る場合、LazyThreadSafetyMode.ExecutionAndPublication または LazyThreadSafetyMode.None を指定します。 初期化メソッドを指定すると、この 2 つのモードで例外キャッシュが有効になります。 初期化メソッドは非常に単純にすることができます。 たとえば、T にパラメーターのないコンストラクターを呼び出すことができます。C# の場合は new Lazy<Contents>(() => new Contents(), mode)、Visual Basic の場合は New Lazy(Of Contents)(Function() New Contents()) です。 初期化メソッドを指定しない System.Lazy<T> コンストラクターを使用する場合、T についてパラメーターなしのコンストラクターからスローされる例外はキャッシュされません。 詳細については、LazyThreadSafetyMode 列挙型のページをご覧ください。

Note

isThreadSafe コンストラクター パラメーターを false に設定するか、mode コンストラクター パラメーターを LazyThreadSafetyMode.None に設定して Lazy<T> オブジェクトを作成した場合、1 つのスレッドから Lazy<T> オブジェクトにアクセスするか、独自の同期を提供する必要があります。 これは、例外キャッシュを含む、オブジェクトのあらゆる側面に適用されます。

前のセクションで説明したように、Lazy<T> オブジェクトの作成に LazyThreadSafetyMode.PublicationOnly を指定した場合、例外の処理が異なります。 PublicationOnly の場合、Lazy<T> インスタンスの初期化にあたり、複数のスレッドが競合できます。 その場合、例外はキャッシュされません。Value プロパティに対するアクセス試行は初期化完了まで続行されます。

次の表は、Lazy<T> コンストラクターが例外キャッシュを制御するしくみをまとめたものです。

コンストラクター スレッド セーフ モード 初期化メソッドを使用します 例外がキャッシュされます
Lazy(T)() (ExecutionAndPublication) いいえ いいえ
Lazy(T)(Func(T)) (ExecutionAndPublication) はい はい
Lazy(T)(Boolean) True (ExecutionAndPublication) または false (None) いいえ いいえ
Lazy(T)(Func(T), Boolean) True (ExecutionAndPublication) または false (None) はい はい
Lazy(T)(LazyThreadSafetyMode) ユーザー指定 いいえ いいえ
Lazy(T)(Func(T), LazyThreadSafetyMode) ユーザー指定 はい ユーザーが PublicationOnly を指定する場合は、いいえ。指定しない場合は、はい。

遅延初期化プロパティを実装する

遅延初期化を利用してパブリック プロパティを実装するには、プロパティのバッキング フィールドを Lazy<T> として定義し、プロパティの get アクセサーから Value プロパティを返します。

class Customer
{
    private Lazy<Orders> _orders;
    public string CustomerID {get; private set;}
    public Customer(string id)
    {
        CustomerID = id;
        _orders = new Lazy<Orders>(() =>
        {
            // You can specify any additional
            // initialization steps here.
            return new Orders(this.CustomerID);
        });
    }

    public Orders MyOrders
    {
        get
        {
            // Orders is created on first access here.
            return _orders.Value;
        }
    }
}
Class Customer
    Private _orders As Lazy(Of Orders)
    Public Shared CustomerID As String
    Public Sub New(ByVal id As String)
        CustomerID = id
        _orders = New Lazy(Of Orders)(Function()
                                          ' You can specify additional 
                                          ' initialization steps here
                                          Return New Orders(CustomerID)
                                      End Function)

    End Sub
    Public ReadOnly Property MyOrders As Orders

        Get
            Return _orders.Value
        End Get

    End Property

End Class

Value プロパティは読み取り専用です。そのため、それを公開するプロパティには set アクセサーがありません。 読み取り/書き込みプロパティに Lazy<T> オブジェクトのバッキングが必要な場合、set アクセサーは新しい Lazy<T> オブジェクトを作成し、それをバッキング ストアに割り当てる必要があります。 set アクセサーは、set アクセサーに渡された新しいプロパティ値を返すラムダ式を作成し、そのラムダ式を新しい Lazy<T> オブジェクトのコンストラクターに渡す必要があります。 Value プロパティの次のアクセスで新しい Lazy<T> が初期化されます。そのため、その Value プロパティは、プロパティに割り当てられた新しい値を返します。 このような入り組んだしくみになるのは、Lazy<T> にビルドされるマルチスレッド保護を保存するためです。 そうでなければ、プロパティ アクセサーは Value プロパティにより返された最初の値をキャッシュし、その値だけを変更することになり、自分のスレッド セーフ コードを記述しなければならなくなります。 Lazy<T> オブジェクトがバッキングする読み取り/書き込みプロパティで追加の初期化が必要になるため、パフォーマンスは許容できないものになる可能性があります。 さらに、シナリオによっては、セッターとゲッターの競合状態を回避するために、追加の調整が必要になる場合があります。

スレッドローカルの遅延初期化

一部のマルチスレッド シナリオでは、各スレッドに独自のプライベート データを与えると効果的になります。 そのようなデータはスレッドローカル データと呼ばれています。 .NET Framework バージョン 3.5 以前では、ThreadStatic 属性を静的変数に適用し、スレッドローカルにすることができました。 ただし、ThreadStatic 属性を使用すると、検出が困難なエラーが発生することがあります。 たとえば、次の例のように、基本的な初期化ステートメントであっても、それにアクセスする最初のスレッドでのみ、変数が初期化されます。

[ThreadStatic]
static int counter = 1;
<ThreadStatic()>
Shared counter As Integer

その他すべてのスレッドでは、その既定値 (ゼロ) を利用して変数は初期化されます。 .NET Framework バージョン 4 における代替として、System.Threading.ThreadLocal<T> 型を使用し、インスタンスベースで、指定した Action<T> デリゲートによりすべてのスレッドで初期化されるスレッドローカル変数を作成できます。 次の例では、counter にアクセスするすべてのスレッドでその開始値が 1 になります。

ThreadLocal<int> betterCounter = new ThreadLocal<int>(() => 1);
Dim betterCounter As ThreadLocal(Of Integer) = New ThreadLocal(Of Integer)(Function() 1)

ThreadLocal<T> は、Lazy<T> とほとんど同じ方法でそのオブジェクトをラップしますが、本質的な違いとして次があります。

  • 各スレッドは、他のスレッドからアクセスできない独自のプライベート データを利用してスレッドローカル変数を初期化します。

  • ThreadLocal<T>.Value プロパティは編集可能なプロパティです。何回でも変更できます。 これは例外伝達に影響を与えることがあります。たとえば、ある get 操作で例外が発生したが、次の操作で値を初期化できたということがあります。

  • 初期化デリゲートが指定されない場合、ThreadLocal<T> は、型の初期値を利用し、そのラップされた型を初期化します。 この点では、ThreadLocal<T>ThreadStaticAttribute 属性と一致しています。

次の例は、ThreadLocal<int> インスタンスにアクセスするすべてのスレッドがデータの独自のコピーを取得する様子を示しています。

// Initialize the integer to the managed thread id on a per-thread basis.
ThreadLocal<int> threadLocalNumber = new ThreadLocal<int>(() => Thread.CurrentThread.ManagedThreadId);
Thread t4 = new Thread(() => Console.WriteLine("threadLocalNumber on t4 = {0} ThreadID = {1}",
                                    threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId));
t4.Start();

Thread t5 = new Thread(() => Console.WriteLine("threadLocalNumber on t5 = {0} ThreadID = {1}",
                                    threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId));
t5.Start();

Thread t6 = new Thread(() => Console.WriteLine("threadLocalNumber on t6 = {0} ThreadID = {1}",
                                    threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId));
t6.Start();

// Ensure that thread IDs are not recycled if the
// first thread completes before the last one starts.
t4.Join();
t5.Join();
t6.Join();

/* Sample Output:
   threadLocalNumber on t4 = 14 ThreadID = 14
   threadLocalNumber on t5 = 15 ThreadID = 15
   threadLocalNumber on t6 = 16 ThreadID = 16
*/
' Initialize the integer to the managed thread id on a per-thread basis.
Dim threadLocalNumber As New ThreadLocal(Of Integer)(Function() Thread.CurrentThread.ManagedThreadId)
Dim t4 As New Thread(Sub()
                         Console.WriteLine("number on t4 = {0} threadID = {1}",
                                           threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t4.Start()

Dim t5 As New Thread(Sub()
                         Console.WriteLine("number on t5 = {0} threadID = {1}",
                                           threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t5.Start()

Dim t6 As New Thread(Sub()
                         Console.WriteLine("number on t6 = {0} threadID = {1}",
                                           threadLocalNumber.Value, Thread.CurrentThread.ManagedThreadId)
                     End Sub)
t6.Start()

' Ensure that thread IDs are not recycled if the 
' first thread completes before the last one starts.
t4.Join()
t5.Join()
t6.Join()

'Sample(Output)
'      threadLocalNumber on t4 = 14 ThreadID = 14 
'      threadLocalNumber on t5 = 15 ThreadID = 15
'      threadLocalNumber on t6 = 16 ThreadID = 16 

Parallel.For と ForEach のスレッドローカル変数

Parallel.For メソッドまたは Parallel.ForEach メソッドを利用してデータ ソースを並列で繰り返すとき、スレッドローカル データのサポートが組み込まれたオーバーロードを利用できます。 そのようなメソッドでは、ローカル デリゲートを利用してデータの作成、アクセス、クリーンアップを行うことで、スレッドローカルになります。 詳細については、「方法:スレッド ローカル変数を使用する Parallel.For ループを記述する」および「方法:パーティション ローカル変数を使用する Parallel.ForEach ループを記述する」をご覧ください。

オーバーヘッドが低いシナリオで遅延初期化を使用する

大量のオブジェクトを遅延初期化しなければならないシナリオでは、Lazy<T> で各オブジェクトをラッピングするとき、あまりにも多くのメモリまたは計算処理リソースが必要になるのか判断することがあります。 あるいは、遅延初期化の公開方法について厳格な要件が与えられることがあります。 そのような場合、System.Threading.LazyInitializer クラスの static (Visual Basic の場合、Shared) メソッドを利用し、Lazy<T> のインスタンスでラッピングせずに、各オブジェクトを遅延初期化できます。

次の例では、1 つの Lazy<T> オブジェクトで Orders オブジェクト全体をラッピングする代わりに、必要な場合にのみ、個々の Order オブジェクトを遅延初期化したと想定しています。

// Assume that _orders contains null values, and
// we only need to initialize them if displayOrderInfo is true
if (displayOrderInfo == true)
{
    for (int i = 0; i < _orders.Length; i++)
    {
        // Lazily initialize the orders without wrapping them in a Lazy<T>
        LazyInitializer.EnsureInitialized(ref _orders[i], () =>
        {
            // Returns the value that will be placed in the ref parameter.
            return GetOrderForIndex(i);
        });
    }
}
' Assume that _orders contains null values, and
' we only need to initialize them if displayOrderInfo is true
If displayOrderInfo = True Then


    For i As Integer = 0 To _orders.Length
        ' Lazily initialize the orders without wrapping them in a Lazy(Of T)
        LazyInitializer.EnsureInitialized(_orders(i), Function()
                                                          ' Returns the value that will be placed in the ref parameter.
                                                          Return GetOrderForIndex(i)
                                                      End Function)
    Next
End If

この例では、ループの繰り返しの旅に初期化プロシージャが呼び出されていることに注意してください。 マルチスレッド シナリオでは、初期化プロシージャを呼び出した最初のスレッドの値をすべてのスレッドで確認できます。 後続のスレッドも初期化プロシージャを呼び出しますが、その結果は利用されません。 この種類の潜在的競合状態が許容できない場合、ブール値引数と同期オブジェクトを取る LazyInitializer.EnsureInitialized のオーバーロードを使用します。

関連項目