日期: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 构造函数产生正确输入数据所需的任何操作。 在首