延迟初始化
一个对象的延迟初始化意味着该对象的创建将会延迟至第一次使用该对象时。(在本主题中,术语“延迟初始化”和“延迟实例化”是同义词。)延迟初始化主要用于提高性能,避免浪费计算,并减少程序内存要求。 以下是最常见的方案:
-
有一个对象的创建开销很大,而程序可能不会使用它。 例如,假定您在内存中有一个 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> 构造函数中没有传递委托,则在第一次访问值属性时,将通过使用 Activator.CreateInstance 来创建包装类型。 如果该类型没有默认的构造函数,则引发运行时异常。
在以下示例中,假定 School 是一个类,该类包含从数据库检索的 School 对象数据。 Student 对象包含一个 School 实例,但根据用户操作,可能不需要来自 School 对象的数据。
直接上Demo代码:
public class School { public string Name { get; set; } public string Address { get; set; } public School(string name, string address) { this.Name = name; this.Address = address; } }
public class Student { public string Number { get; set; } public string Name { get; set; } /// <summary> /// 学校对象,使用Lazy延迟加载School对象,当调用的时候才会初始化值 /// </summary> public Lazy<School> School = new Lazy<School>(() => new School("重庆工商大学", "重庆市南岸区")); public Student(string number, string name) { this.Name = name; this.Number = number; } }
class Program { static void Main(string[] args) { Student student = new Student("1001", "zhangfj");//声明并初始化Student对象 //首次,判断student对象的School属性值是否初始化 if (!student.School.IsValueCreated) { Console.WriteLine("School isn't Init"); } else { Console.WriteLine("School is Init"); } //使用student对象的School属性值 Console.WriteLine(student.School.Value.Name); //再次,判断student对象的School属性值是否初始化 if (!student.School.IsValueCreated) { Console.WriteLine("School isn't Init"); } else { Console.WriteLine("School is Init"); } Console.Read(); } }
默认情况下,Lazy<T> 对象是线程安全的。 这意味着如果构造函数未指定线程安全性的类型,它创建的 Lazy<T> 对象都是线程安全的。 在多线程方案中,要访问线程安全的 Lazy<T> 对象的 Value 属性的第一个线程将为所有线程上的所有后续访问初始化该对象,并且所有线程都共享相同数据。 因此,由哪个线程初始化对象并不重要,争用条件将是良性的。
若要通过使用延迟初始化来实现一个公共属性,请将该属性的支持字段定义为 Lazy<T>,并从该属性的 get 访问器中返回 Value 属性。
首先,修改Student的代码,使用延迟初始化实现School属性
public class Student { private Lazy<School> _school; public string Number { get; set; } public string Name { get; set; } /// <summary> /// 学校属性。当调用当前对象的School属性时候,才会初始化值。此处应用延迟初始化,对系统性能方面的提升很明显。 /// </summary> public School School { get { return this._school.Value; } } public Student(string number, string name) { this.Name = name; this.Number = number; //此处可以根据构造函数的参数,查询当前学生的学校信息等(实际开发中类似) this._school = new Lazy<School>(() => new School("重庆工商大学", "重庆市南岸区")); } }
class Program { static void Main(string[] args) { Student student = new Student("1001", "zhangfj");//声明并初始化Student对象 //首次,判断student对象的School属性值是否初始化 //if (!student.School.IsValueCreated) //{ // Console.WriteLine("School isn't Init"); //} //else //{ // Console.WriteLine("School is Init"); //} //使用student对象的School属性值 Console.WriteLine(student.School.Name); //再次,判断student对象的School属性值是否初始化 //if (!student.School.IsValueCreated) //{ // Console.WriteLine("School isn't Init"); //} //else //{ // Console.WriteLine("School is Init"); //} Console.Read(); } }
在某些多线程方案中,可能要为每个线程提供它自己的私有数据。 此类数据称为“线程本地数据”。 在 .NET Framework 3.5 和更低版本中,可以将 ThreadStatic 特性应用于静态变量以使其成为线程本地变量。 但是,使用 ThreadStatic 特性会导致细小的错误。 例如,即使基本的初始化语句也将导致该变量只在访问它的第一个线程上进行初始化,如以下示例中所示。
[ThreadStatic] static int counter = 1;
在所有其他线程上,该变量将通过使用默认值(零)来进行初始化。 在 .NET Framework 4 中,作为一种替代方法,可以使用 System.Threading.ThreadLocal<T> 类型创建基于实例的线程本地变量,此变量可通过您提供的 Action<T> 委托在所有线程上进行初始化。 在以下示例中,所有访问 counter 的线程都会将其起始值看作 1。
class Program { static void Main(string[] args) { // 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 */ Console.Read(); }