日期:2010-01-31  浏览次数:20485 次

在软件开发中,术语延迟指的是尽可能久地推迟特定的高开销活动的空闲时间。 软件延迟过程中其实也在进行操作,但意味着任何操作仅当需要完成某一特定任务时才会发生。 就这一点而言,延迟是软件开发中的一种重要模式,可以成功地应用于包括设计与实施在内的各种情景中。

  例如,极限编程方法中的一种基本编码实践就被简单地概括为“您不会需要它”,那就是一种明确的延迟要求 - 当且仅当您需要这些功能时,才需要在基本代码中包含它。

  从另一个角度来看,在实施类的过程中,当要从难以访问的源中加载数据时,您也可能需要延迟。 事实上,延迟加载模式解释了这种普遍接受的解决方案,即定义一个类成员,但使其保持为空,直到其他某些客户端代码实际需要其内容时为止。 延迟加载完全适合在对象关系映射 (ORM) 工具(如实体框架和 NHibernate)环境中使用。 ORM 工具用于映射面向对象的环境与关系数据库之间的数据结构。 例如,在这种环境中,延迟加载指的就是仅当某些代码尝试读取 Customer 类上公开的 Orders 集合属性时,
框架才能加载 Customer 的 Orders。

  但是,延迟加载并不限于特定的实施方案(如 ORM 编程)。 而且,延迟加载指的就是在某些数据实际可用之前不获取该数据的实例。 换言之,延迟加载就是要有特殊工厂逻辑,即跟踪必须要创建的内容,最后在实际请求该内容时以静默方式创建该内容。

  在 Microsoft .NET Framework 中,开发人员早就在我们的类中手动实施了所有延迟行为。 在 .NET Framework 4 问世之前,从未有过内置的机制来帮助完成此任务。 在 .NET Framework 4 中,我们可以开始
使用全新的 Lazy<T> 类。

  了解 Lazy<T> 类

  Lazy<T> 是一个特殊的工厂,您可以用来包装给定 T 类型的对象。 Lazy<T> 包装代表一个尚不存在的类实例的实时代理。 使用 Lazy 包装的理由有很多,其中最重要的莫过于可以提高性能。 延迟初始化对象可以避免所有不必要的计算,从而减少内存消耗。 如果加以合理利用,延迟初始化对象也可以成为一种加快应用程序启动的强大工具。 以下代码说明了以延迟方式初始化对象的方法:

var container = new Lazy<DataContainer>();

  在本例中,DataContainer 类表示的是一个引用了其他对象数组的纯数据容器对象。 在刚刚对 Lazy<T> 实例调用完 new 运算符之后,返回的只是一个实时的 Lazy<T> 类实例;无论如何都不会得到指定类型 T 的实例。 如果您需要向其他类的成员传递一个 DataContainer 实例,则必须更改这些成员的签名才能使用 Lazy<DataContainer>,如下所示:

void ProcessData(Lazy<DataContainer> container);

  何时创建 DataContainer 的实际实例,以便程序可以处理其所需的数据? 让我们来看看 Lazy<T> 类的公共编程接口。 该公共接口非常小,因为它只包含两个属性:Value 和 IsValueCreated。 如果存在与 Lazy 类型关联的实例,则属性 Value 就会返回该实例的当前值。 该属性的定义如下:

public T Value 
{ 
 get { ... } 
}

  属性 IsValueCreated 可以返回一个 Boolean 值,表示 Lazy 类型是否已经过实例化。 以下是该属性的源代码中的一段摘录:

public bool IsValueCreated 
{ 
 get 
 { 
  return ((m_boxed != null) && (m_boxed is Boxed<T>)); 
 } 
}

  如果 Lazy<T> 类包含 T 类型的实际实例(如果有),则 m_boxed 成员就是该类的一个内部私有的不稳定成员。 因此,IsValueCreated 只需检查是否存在 T 的实时实例,然后返回一个 Boolean 答案。 如前文所述,m_boxed 成员是私有的并且不稳定(如以下代码段所示):

private volatile object m_boxed;

  在 C# 中,volatile 关键字表示成员可以被并发运行的线程修改。 volatile 关键字用于下面这样的成员:这类成员可以在多线程环境中使用,但无法防止多个可能的并发线程同时对其进行访问(本意是出于性能因素考虑)。 我们稍后再回到 Lazy<T> 的线程方面上来。 目前,可以肯定地说,默认情况下 Lazy<T> 的公共成员和受保护成员是线程安全的。 当有任意代码首次尝试访问 Value 成员时,就会创建类型 T 的实际实例。 对象创建方面的详细信息取决于各种线程属性,这些属性可以通过 Lazy<T> 构造函数来指定。 应该明确的是,线程模式的含义仅当 boxed 值实际上已初始化或首次被访问时才很重要。

  默认情况下,类型 T 的实例是通过调用 Activator.CreateInstance 进行反射获取的。 以下是一个典型的与 Lazy<T> 类型进行交互的简单示例:

var temp = new Lazy<DataContainer>(); 
Console.WriteLine(temp.IsValueCreated); 
Console.WriteLine(temp.Value.SomeValue);

  请注意,在调用 Value 之前,并不一定要对 IsValueCreated 进行检查。 通常情况下,仅当(无论出于何种原因)您需要了解某个值当前是否与 Lazy 类型关联时,才必须查看 IsValueCreated 的值。 您无需检查 IsValueCreated 即可避免发生对 Value 的空引用异常。 以下代码即可保证正常运行:

var temp = new Lazy<DataContainer>(); 
Console.WriteLine(temp.Value.SomeValue);

  Value 属性的 getter 会检查 boxed 值是否已经存在;如果不存在,则会触发逻辑创建一个包装类型实例,并返回该实例。

  实例化过程

  当然,当该 Lazy 类型(上例中的 DataContainer)的构造函数引发异常时,您的代码会负责处理该异常。 所捕获异常属于 TargetInvocationException 类型,该异常是 .NET 反射无法间接创建某类型实例时收到的典型异常。

  Lazy<T> 包装逻辑只能确定是否已创建类型 T 的实例,并不能保证您在访问 T 上的任意公共成员时都不会收到空引用异常。 以下面的代码段为例:

public class DataContainer 
{ 
 public DataContainer() 
 { 
 } 
 
 public IList<String> SomeValues { get; set; } 
}

  现在假设您尝试从客户端程序调用以下代码:

var temp = new Lazy<DataContainer>(); 
Console.WriteLine(temp.Value.SomeValues.Count);

  在这种情况下,您将收到一个异常,这是因为 DataContainer 对象的 SomeValues 属性为空,而非 DataContainer 本身为空。 引发该异常是因为 DataContainer 的构造函数没有正常初始化其所有成员;该错误与 lazy 方法的实施无关。

  Lazy<T> 的 Value 属性为只读属性,即一旦经过初始化,Lazy<T> 对象将始终返回同一个类型 T 实例或同一个值(当 T 为值类型时)。 您无法修改实例,但可以访问该实例可能拥有的所有公共属性。

  以下是配置 Lazy<T> 对象向 T 类型传递临时参数的方法:

temp = new Lazy<DataContainer>(() => new Orders(10));

  其中一个 Lazy<T> 构造函数会接受一个委托,您可以通过该委托指定为 T 构造函数产生正确输入数据所需的任何操作。 在首