다음을 통해 공유


Lazy Initialization

Microsoft Silverlight will reach end of support after October 2021. Learn more.

Lazy initialization of an object means that its creation is deferred until it is first used. (In this topic, the terms lazy initialization and lazy instantiation are synonymous.) Lazy initialization is primarily used to improve performance, avoid wasteful computation, and reduce program memory requirements. These are the most common scenarios:

  • When you have an object that is expensive to create, and the application might not use it. For example, assume that you have in memory a Customer object that has an Orders property that returns an Orders object. Initializing the Orders object might require the creation of a large array of Order objects, and perhaps a database connection. If the user never accesses the Orders property, there is no reason to use system memory or computing cycles to create the Orders object. By using Lazy<Orders> to declare the Orders object for lazy initialization, you can avoid wasting system resources when the object is not used.

  • When you have an object that is expensive to create, and you want to defer its creation until after other expensive operations have been completed. For example, assume that your application loads several object instances when it starts, but only some of them are required immediately. You can improve the startup performance of the application by deferring initialization of the objects that are not required until the required objects have been created.

Although you can write your own code to perform lazy initialization, we recommend that you use the System.Lazy<T> class instead. Lazy<T> supports thread safety and provides a consistent exception propagation policy.

Basic Lazy Initialization

To define a lazy-initialized type (for example, MyType), use Lazy<MyType> (Lazy(Of MyType) in Visual Basic), as shown in the following example. If no delegate is passed in the Lazy<T> constructor, the wrapped type is created by using the Activator.CreateInstance method when the Value property is first accessed. If the type does not have a default constructor, a run-time exception is thrown.

In the following example, assume that Orders is a class that contains an array of Order objects retrieved from a database. A Customer object contains an instance of Orders, but depending on user actions, the data from the Orders object might not be required.

' 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)()
// Initialize by using default Lazy<T> constructor. The 
// Orders array itself is not created yet.
Lazy<Orders> _orders = new Lazy<Orders>();

You can also pass a delegate in the Lazy<T> constructor that invokes a specific constructor overload on the wrapped type at creation time, and performs any other initialization steps that are required, as shown in the following example.

' 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))
// Initialize by invoking a specific constructor on Order when Value
// property is accessed
Lazy<Orders> _orders = new Lazy<Orders>(() => new Orders(100));

After the Lazy object is created, no instance of Orders is created until the Value property of the Lazy<T> instance is accessed for the first time. On first access, the wrapped type is created and returned, and stored for any future access.

' 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
// 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.
}

A Lazy<T> object always returns the same object or value that it was initialized with. Therefore, the Value property is read-only. However, if the object that the Value property stores is a reference type, you can change the value of the object's settable public fields and properties.

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

This new Lazy<T> instance, like the earlier one, does not instantiate Orders until its Value property is first accessed.

Thread-Safe Initialization

By default, Lazy<T> objects are thread-safe. That is, if the constructor does not specify the kind of thread safety, the Lazy<T> objects it creates are thread-safe. In multithreaded scenarios, the first thread to access the Value property of a thread-safe Lazy<T> object initializes it for all subsequent accesses on all threads, and all threads share the same data. Therefore, it does not matter which thread initializes the object, and race conditions are benign.

NoteNote:

You can extend this consistency to error conditions by using exception caching. For more information, see the next section, Exceptions in Lazy Objects.

The following example shows that the same Lazy<int> instance has the same value for three separate threads.

' 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.
// 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.
*/

Some Lazy<T> constructors have a Boolean parameter named isThreadSafe that is used to specify whether the Value property will be accessed from multiple threads. If you intend to access the property from just one thread, pass in false to obtain a modest performance benefit. If you intend to access the property from multiple threads, pass in true to instruct the Lazy<T> instance to correctly handle race conditions in which one thread throws an exception at initialization time.

Some Lazy<T> constructors have a LazyThreadSafetyMode parameter named mode. These constructors provide an additional thread safety mode. The following table shows how the thread safety of a Lazy<T> object is affected by constructor parameters that specify thread safety. Each constructor has at most one such parameter.

Thread safety of the object

LazyThreadSafetyModemode parameter

Boolean isThreadSafe parameter

No thread safety parameters

Fully thread-safe; only one thread at a time tries to initialize the value.

ExecutionAndPublication

true

Yes.

Not thread-safe.

None

false

Not applicable.

Fully thread-safe; threads race to initialize the value.

PublicationOnly

Not applicable.

Not applicable.

As the table shows, specifying LazyThreadSafetyMode.ExecutionAndPublication for the mode parameter is the same as specifying true for the isThreadSafe parameter, and specifying LazyThreadSafetyMode.None is the same as specifying false.

Specifying LazyThreadSafetyMode.PublicationOnly allows multiple threads to attempt to initialize the Lazy<T> instance. Only one thread can win this race, and all the other threads receive the value that was initialized by the successful thread. If an exception is thrown on a thread during initialization, that thread does not receive the value set by the successful thread. Exceptions are not cached, so a subsequent attempt to access the Value property can result in successful initialization. This differs from the way exceptions are treated in other modes, which is described in the following section. For more information, see the LazyThreadSafetyMode enumeration.

Exceptions in Lazy Objects

As stated earlier, a Lazy<T> object always returns the same object or value that it was initialized with, and therefore the Value property is read-only. If you enable exception caching, this immutability also extends to exception behavior. If a lazy-initialized object has exception caching enabled and throws an exception from its initialization method when the Value property is first accessed, that same exception is thrown on every subsequent attempt to access the Value property. In other words, the constructor of the wrapped type is never re-invoked, even in multithreaded scenarios. Therefore, the Lazy<T> object cannot throw an exception on one access and return a value on a subsequent access.

Exception caching is enabled when you use any System.Lazy<T> constructor that takes an initialization method (valueFactory parameter); for example, it is enabled when you use the Lazy(T)(Func(T)) constructor. If the constructor also takes a LazyThreadSafetyMode value (mode parameter), specify LazyThreadSafetyMode.None or LazyThreadSafetyMode.ExecutionAndPublication. Specifying an initialization method enables exception caching for these two modes. The initialization method can be very simple. For example, it might call the default constructor for T: new Lazy<Contents>(() => new Contents(), mode) in C#, or New Lazy(Of Contents)(Function() New Contents()) in Visual Basic. If you use a System.Lazy<T> constructor that does not specify an initialization method, exceptions that are thrown by the default constructor for T are not cached. For more information, see the LazyThreadSafetyMode enumeration.

NoteNote:

If you create a Lazy<T> object with the isThreadSafe constructor parameter set to false or the mode constructor parameter set to LazyThreadSafetyMode.None, you must access the Lazy<T> object from a single thread or provide your own synchronization. This applies to all aspects of the object, including exception caching.

As noted in the previous section, Lazy<T> objects created by specifying LazyThreadSafetyMode.PublicationOnly treat exceptions differently. With PublicationOnly, multiple threads can compete to initialize the Lazy<T> instance. In this case, exceptions are not cached, and attempts to access the Value property can continue until initialization is successful.

The following table summarizes the way the Lazy<T> constructors control exception caching.

Constructor

Thread safety mode

Uses initialization method

Exceptions are cached

Lazy(T)()

(ExecutionAndPublication)

No

No

Lazy(T)(Func(T))

(ExecutionAndPublication)

Yes

Yes

Lazy(T)(Boolean)

True (ExecutionAndPublication) or false (None)

No

No

Lazy(T)(Func(T), Boolean)

True (ExecutionAndPublication) or false (None)

Yes

Yes

Lazy(T)(LazyThreadSafetyMode)

User-specified

No

No

Lazy(T)(Func(T), LazyThreadSafetyMode)

User-specified

Yes

No if user specifies PublicationOnly; otherwise, yes.

Implementing a Lazy-Initialized Property

To implement a public property by using lazy initialization, define the backing field of the property as a Lazy<T> object, and return the Value property from the get accessor of the property.

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
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 additonal 
            // initialization steps here.
            return new Orders(this.CustomerID);
        });
    }

    public Orders MyOrders
    {
        get
        {
            // Orders is created on first access here.
            return _orders.Value;
        }
    }
}

The Value property is read-only; therefore, the property that exposes it has no set accessor. If you require a read/write property backed by a Lazy<T> object, the set accessor must create a new Lazy<T> object and assign it to the backing store. The set accessor must create a lambda expression that returns the new property value that was passed to the set accessor, and pass that lambda expression to the constructor for the new Lazy<T> object. The next access of the Value property will cause initialization of the new Lazy<T>, and its Value property will thereafter return the new value that was assigned to the property. The reason for this convoluted arrangement is to preserve the multithreading protections built into Lazy<T>. Otherwise, the property accessors would have to cache the first value returned by the Value property and only modify the cached value, and you would have to write your own thread-safe code to do that. Because of the additional initializations required by a read/write property backed by a Lazy<T> object, the performance might not be acceptable. Furthermore, depending on the specific scenario, additional coordination might be required to avoid race conditions between setters and getters.

See Also

Reference

Concepts