它的性能仍然不如后续的实现。
第四个版本 - 不太懒,不使用锁且线程安全 public sealed class Singleton { private static readonly Singleton instance = new Singleton(); // 显式静态构造函数告诉C#编译器 // 不要将类型标记为BeforeFieldInit static Singleton() { } private Singleton() { } public static Singleton Instance { get { return instance; } } }正如您所看到的,这实际上非常简单——但是为什么它是线程安全的,它有多懒惰?C#中的静态构造函数仅在创建类的实例或引用静态成员时执行,并且每个AppDomain只执行一次。考虑到无论发生什么情况,都需要执行对新构造的类型的检查,这比在前面的示例中添加额外检查要快。然而,还有一些小缺陷:
它并不像其他实现那样懒惰。特别是,如果您有Instance之外的静态成员,那么对这些成员的第一次引用将涉及到创建实例。这将在下一个实现中得到纠正。
如果一个静态构造函数调用另一个静态构造函数,而另一个静态构造函数再次调用第一个构造函数,则会出现复杂情况。查看.NET规范(目前是分区II的第9.5.3节),了解有关类型初始化器的确切性质的更多详细信息——它们不太可能会影响您,但是有必要了解静态构造函数在循环中相互引用的后果。
类型初始化器的懒惰性只有在.NET没有使用名为BeforeFieldInit的特殊标志标记类型时才能得到保证。不幸的是,C#编译器(至少在.NET 1.1运行时中提供)将所有没有静态构造函数的类型(即看起来像构造函数但被标记为静态的块)标记为BeforeFieldInit。我现在有一篇文章,详细介绍了这个问题。另请注意,它会影响性能,如在页面底部所述的那样。
您可以使用此实现(并且只有这一个)的一个快捷方式是将 Instance作为一个公共静态只读变量,并完全删除该属性。这使得基本的框架代码非常小!然而,许多人更愿意拥有一个属性,以防将来需要采取进一步行动,而JIT内联可能会使性能相同。(注意,如果您需要懒惰的,静态构造函数本身仍然是必需的。)
第五版 - 完全懒惰的实例化 public sealed class Singleton { private Singleton() { } public static Singleton Instance { get { return Nested.instance; } } private class Nested { // 显式静态构造告诉C#编译器 // 未标记类型BeforeFieldInit static Nested() { } internal static readonly Singleton instance = new Singleton(); } }在这里,实例化是由对嵌套类的静态成员的第一次引用触发的,该引用只发生在Instance中。这意味着实现是完全懒惰的,但是具有前面实现的所有性能优势。请注意,尽管嵌套类可以访问封闭类的私有成员,但反之则不然,因此需要instance在此处为内部成员。不过,这不会引起任何其他问题,因为类本身是私有的。但是,为了使实例化变得懒惰,代码要稍微复杂一些。
第六版 - 使用.NET 4的Lazy<T>类型如果您使用的是.NET 4(或更高版本),则可以使用System.Lazy 类型使惰性变得非常简单。您需要做的就是将委托传递给调用Singleton构造函数的构造函数——使用lambda表达式最容易做到这一点。
public sealed class Singleton { private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton()); public static Singleton Instance { get { return lazy.Value; } } private Singleton() { } }它很简单,而且性能很好。它还允许您检查是否已使用IsValueCreated 属性创建实例(如果需要的话)。
上面的代码隐式地将LazyThreadSafetyMode.ExecutionAndPublication用作Lazy<Singleton>的线程安全模式。根据您的要求,您可能希望尝试其他模式。
性能与懒惰在许多情况下,您实际上并不需要完全懒惰——除非您的类初始化做了一些特别耗时的事情,或者在其他地方产生了一些副作用,否则最好忽略上面所示的显式静态构造函数。这可以提高性能,因为它允许JIT编译器进行一次检查(例如在方法的开头)以确保类型已经初始化,然后从那时开始设定它。如果在相对紧密的循环中引用单例实例,则会产生(相对)显著的性能差异。您应该决定是否需要完全延迟实例化,并在类中适当地记录此决策。