一个对象的延迟初始化意味着该对象的创建将会延迟至第一次使用该对象时。延迟初始化主要用于提高性能,避免浪费计算,并减少程序内存要求。以下是最常见的方案:
-
有一个对象的创建开销很大,而程序可能不会使用它。例如,假定您在内存中有一个 Customer 对象,该对象的 Orders 属性包含一个很大的 Order 对象数组,该数组需要数据库连接以进行初始化。如果用户从未要求显示 Orders 或在计算中使用其数据,则没有理由使用系统内存或计算周期来创建它。通过使用 Lazy<Orders> 将 Orders 对象声明为延迟初始化,可以避免在不使用该对象的情况下浪费系统资源。
-
有一个对象的创建开销很大,您想要将创建它的时间延迟到完成其他开销大的操作之后。例如,假定您的程序在启动时加载若干个对象实例,但只有一些对象实例需要立即执行。通过将不必要的对象的初始化延迟到已创建必要的对象之后,可以提高程序的启动性能。
尽管您可以编写自己的代码来执行延迟初始化,但我们推荐使用Lazy<T>,Lazy<T>及其相关的类型还支持线程安全,并提供一致的异常传播策略。
下表列出了 .NET Framework 版本 4 提供的、可在不同方案中启用延迟初始化的类型。
类型 |
说明 |
---|---|
一个包装类,可为任意类库或用户定义的类型提供延迟初始化语义。 | |
类似于 Lazy<T>,只不过它基于本地线程提供延迟初始化语义。每个线程都可以访问自己的唯一值。 | |
为对象的延迟初始化提供高级的 static(Visual Basic 中为 Shared)方法,此方法不需要类开销。 |
若要定义延迟初始化的类型(例如,MyType),请使用 Lazy<MyType>(Visual Basic 中为 Lazy(Of MyType)),如以下示例中所示。如果在 Lazy<T> 构造函数中没有传递委托,则在第一次访问值属性时,将通过使用 ActivatorCreateInstance() 来创建包装类型。如果该类型没有默认的构造函数,则引发运行时异常。
在以下示例中,假定 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>();
此外,还可以在 Lazy<T> 构造函数中传递一个委托,用于在创建时调用包装类的特定构造函数重载,并执行所需的任何其他初始化步骤,如以下示例中所示。
// Initialize by invoking a specific constructor on Order when Value
// property is accessed
Lazy<Orders> _orders = new Lazy<Orders>(() => 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.
}
Lazy<T> 对象始终返回初始化时使用的相同对象或值。因此,Value 属性是只读的。如果 Value 存储引用类型,则不能为它分配新对象。(但是,可以更改其可设置的公共字段和属性的值。)如果 Value 存储一个值类型,则不能修改它的值。但是,可以使用新的参数通过再次调用变量构造函数来创建新的变量。
_orders = new Lazy<Orders>(() => new Orders(10));
在第一次访问 Value 属性之前,新的延迟实例(与早期的延迟实例类似)不会实例化 Orders。
线程安全初始化
一些 Lazy<T> 构造函数重载具有一个名为 isThreadSafe 的布尔参数,该参数用于指定是否将从多个线程访问 Value 属性。如果您打算只从一个线程访问该属性,请传入 false 以获得适度的性能好处。如果您打算从多个线程访问该属性,请传入 true 以指示延迟实例正确处理争用条件(在此条件下,一个线程将在初始化时引发一个异常)。如果使用不带 isThreadSafe 参数的构造函数,则此值默认为 true。
在多线程方案中,要访问 Value 属性的第一个线程将为所有线程上的所有后续访问来初始化该构造函数,并且所有线程都共享相同数据。因此,由哪个线程初始化对象并不重要,争用条件将是良性的。如果要初始化 Value 的第一个线程将导致引发异常,则对所有 Value 的后续访问都将引发相同的异常。不可能出现一个线程引发异常而另一个线程初始化对象的情况。下面的示例演示了同一个 Lazy<int> 实例对于三个不同的线程具有相同的值。
// 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.
*/
如果在每个线程上需要不同的数据,请使用 ThreadLocal<T> 类型,如本主题后面所述。
实现延迟初始化属性
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;
}
}
}
Value 属性是只读的;因此,公开它的属性不具有 set 访问器。如果需要读写属性,则 set 访问器必须为支持字段调用一个构造函数以创建一个新的 Lazy<T> 对象。但是,如果执行此操作,则在下次访问 Value 属性时,将导致初始化新的包装对象,并且性能可能会降低。可能还会影响线程安全,因为所有线程可能不会具有相同的数据视图。因此,在多线程方案中,如果公开延迟初始化的属性的 set 访问器,则可能需要额外的协调。
更多信息参考:http://msdn.microsoft.com/zh-cn/library/dd997286.aspx