C# 中的单例设计模式:第二部分






2.74/5 (13投票s)
本文将讨论延迟初始化、lazy 关键字、为什么将单例类设为密封类以及单例类与静态类之间的区别。
- 下载 PDFArticle.zip - 646.5 KB
- 下载 5.SingletonVsStatic.zip - 135.7 KB
- 下载 4.SingletonWithLazyInitialization.zip - 129.4 KB
- 下载 3.SingletonWithEagerLoading.zip - 129 KB
目录
引言
在上一篇关于学习单例模式的文章中,我们讨论了什么是单例设计模式,何时需要它以及它解决了哪些问题。我们还讨论了如何逐步创建一个基本的单例类,以及如何借助锁机制使其线程安全,以及如何借助双重检查锁机制提高性能。在本文中,我们将讨论延迟初始化、lazy 关键字、为什么将单例类设为密封类以及单例类与静态类之间的区别。在开始之前,我强烈建议您阅读我的上一篇文章。
延迟初始化
在上一篇文章中,我们讨论了延迟初始化。延迟初始化在性能上非常有效。在按需创建对象的情况下,例如只有在访问时才创建类的对象,延迟初始化效果很好。它有助于应用程序更快地加载,因为它不依赖于应用程序启动时创建实例。也可能存在需要急切初始化的情况。急切初始化与延迟初始化相反(或者可以说是非延迟初始化)。让我们看看如何使我们的单例类支持急切初始化。您可以获取我们在上一篇文章中开发的源代码并开始使用。它也随文章一起提供。
急切初始化实现
急切加载基本上是初始化将来要访问的对象并将其保留在内存中,而不是按需初始化,以便我们可以在需要时使用这些对象。
打开 Visual Studio,打开单例的最后一个解决方案并打开 Singleton
类。
- 将 null 支持字段初始化更改为新的
Singleton
类初始化,如下所示,并将支持字段设为只读。 - 由于我们已将支持字段设为只读,因此我们无法在
SingleInstance
属性中再次实例化它,因此只需按原样返回支持字段,并删除我们在上一版单例中实现的双重检查锁。 - 也删除我们之前声明的锁定变量。现在运行应用程序并检查输出。请注意,我们仍然并行调用了两个方法。
我们看到只创建了一个实例。我们的类在删除锁变量后仍然是线程安全的,这并不奇怪,因为 CLR 内部负责变量初始化,从而避免我们在急切加载初始化中陷入死锁情况。
以下是完整的代码。
using static System.Console;
namespace Singleton
{
class class Singleton
{
static int instanceCounter = 0;
private static readonly Singleton singleInstance = new Singleton(); //private static Singleton singleInstance = null;
private Singleton()
{
instanceCounter++;
WriteLine("Instances created " + instanceCounter );
}
public static Singleton SingleInstance
{
get
{
return singleInstance;
}
}
public void LogMessage(string message)
{
WriteLine("Message " + message);
}
}
}
使用 Lazy 关键字实现延迟初始化
我们可以使用 lazy 关键字修改我们的类以进行延迟初始化。让我们看看如何做到这一点。
- 首先通过以下代码将支持字段初始化更改为 Lazy 初始化
private static readonly Lazy<Singleton> singleInstance = new Lazy<Singleton>(()=>new Singleton());
这就是我们通过将委托传递给创建实例,即
() => new Singleton()
来延迟初始化对象的方式。 - 现在在属性中,实例不能直接返回,但我们返回
singleInstance.Value
,因为现在singleInstance.Value
是 Singleton 类类型,而不是实例。完整的类代码如下。
using System; using static System.Console; namespace Singleton { sealed class Singleton { static int instanceCounter = 0; private static readonly Lazy<Singleton> singleInstance = new Lazy<Singleton>(()=>new Singleton()); //private static Singleton singleInstance = null; private Singleton() { instanceCounter++; WriteLine("Instances created " + instanceCounter ); } public static Singleton SingleInstance { get { return singleInstance.Value; } } public void LogMessage(string message) { WriteLine("Message " + message); } } }
- 运行应用程序并检查输出。
我们得到了相同的输出,因为 lazy 关键字只创建了一个单例类对象,并且它们默认是线程安全的。这就是为什么在并行调用访问单例实例的方法时没有出现任何错误的原因。
单例类中 Sealed 关键字的重要性
在上一篇关于单例的文章开头,我提到我们应该在单例类之前使用 sealed 关键字。这是因为我们不希望我们的单例类被继承。密封类帮助我们限制类继承,但在单例类中,它做得更多,因为单例类无论是否使用 sealed 关键字,继承都是受限制的,因为单例类的所有构造函数都是私有的,这永远不会允许单例类被继承。例如,创建一个派生类并尝试使您的单例成为基类。将单例设为公共公共类 Singleton,并在 Visual Studio 中添加一个新类,并将其命名为 DerivedClass
,现在将 Singleton
类设为这个新添加类的基类。我们立即看到以下错误。
它说 Singleton
类由于其保护级别而无法访问。那么,既然我们的目的已经解决了,为什么我们仍然需要 sealed 关键字呢?在嵌套类的情况下,我们需要 sealed 关键字。例如,考虑这样一种情况:这个派生类是单例类的嵌套类,并且也从单例类继承。根据面向对象编程,这完全是可能的。所以我们的实现就变成了这样
using System;
using static System.Console;
namespace Singleton
{
public class Singleton
{
static int instanceCounter = 0;
private static readonly Lazy<Singleton> singleInstance = new Lazy<Singleton>(()=>new Singleton()); //private static Singleton singleInstance = null;
private Singleton()
{
instanceCounter++;
WriteLine("Instances created " + instanceCounter );
}
public static Singleton SingleInstance
{
get
{
return singleInstance.Value;
}
}
public void LogMessage(string message)
{
WriteLine("Message " + message);
}
public class DerivedClass : Singleton
{
}
}
}
现在尝试从 Program.cs 的 Main
方法访问此派生类。
由于 DerivedClass
是一个嵌套类,我们可以通过 Singleton
类调用它,并且由于它继承自 Singleton
类,它也可以访问 LogMessage()
方法。现在运行应用程序并检查输出。
我们清楚地看到,当我们运行应用程序时,创建了两个 Singleton
类的实例。第一个实例是由 parallelInvoke 调用 Log 方法创建的,第二个实例是由 Derived
类构造函数创建的,因为它在创建派生类实例时隐式调用了基类构造函数。因此,无论是否为嵌套类,我们都必须在单例类中限制继承。由于私有构造函数在嵌套类的情况下无法帮助我们限制继承,因此我们应该使用 sealed 关键字来限制继承。现在在 Singleton
类上应用 sealed 关键字并检查其嵌套派生类。
派生类
现在,当我们在基单例类上应用了 sealed 关键字后,派生类会提示它不能继承自密封类,无论派生类是否嵌套。因此,我们永远不会有两个单例类的实例,因为它永远不会被派生。
using System;
using static System.Console;
namespace Singleton
{
public sealed class Singleton
{
static int instanceCounter = 0;
private static readonly Lazy<Singleton> singleInstance = new Lazy<Singleton>(()=>new Singleton()); //private static Singleton singleInstance = null;
private Singleton()
{
instanceCounter++;
WriteLine("Instances created " + instanceCounter );
}
public static Singleton SingleInstance
{
get
{
return singleInstance.Value;
}
}
public void LogMessage(string message)
{
WriteLine("Message " + message);
}
}
}
单例类 vs 静态类
我们不会详细讨论什么是静态类,而是更侧重于单例类和静态类之间的区别。以下是静态类和单例模式之间逐点列出的区别。
- 单例是一种对象创建设计模式,其设计遵循某些准则,以确保它只返回其类的一个对象,而 static 是 C# 中的一个保留关键字,用于在类或方法之前使其成为静态。
- 单例类或设计模式可以包含静态成员和非静态成员,而如果一个类被标记为静态,它只能包含静态成员。例如,如果一个类是静态的,它应该只包含静态方法、静态属性和静态变量。
- 单例方法可以作为参数传递给其他方法或对象,但静态成员不能作为引用传递。
- 单例对象可以以支持处置的方式创建;这意味着它们可以被处置。
- 单例对象存储在堆上,而静态对象存储在栈上。
- 单例对象可以被克隆。
- 单例模式促进代码重用和代码共享,并且还可以实现接口。单例可以从其他类继承,促进继承并更好地控制对象状态,而静态类不能继承其实例成员。
- 单例类可以被设计成能够利用延迟初始化或异步初始化,并且直接被 CLR 考虑,而静态类在第一次加载时首先被初始化。
- 静态类不能包含实例构造函数,因此不能被实例化,而单例类有私有实例构造函数。
静态类实现
让我们做一些实际的实现来更详细地理解。打开我们创建的解决方案或新解决方案,并添加一个控制台应用程序类型的新项目,并将其命名为 StaticClasses
。
现在假设我们的项目有一个需求,即创建一个实用程序类,用于存储各种形状的面积。例如,让我们考虑该类包含计算圆形、正方形、矩形和三角形面积的公式。我们确信计算几何形状面积的公式始终保持不变,并且基于我们传递的输入参数。例如,矩形的面积是矩形的长度乘以矩形的宽度,而正方形的面积是边长的平方。由于面积始终保持不变,我们可以借助静态类和静态方法实现此功能。因此,在 StaticClasses
控制台应用程序中创建一个名为 Areas 的类并使其为静态。
现在让我们创建静态方法来计算圆形、矩形、正方形和三角形的面积。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Math;
namespace StaticClasses
{
public static class Areas
{
/// <summary>
/// Area of Circle is πr2.
/// value of pi is 3.14159 and r is the radius of the circle.
/// </summary>
/// <returns></returns>
public static double AreaOfCircle(double radius)
{
return PI * (radius * radius);
}
/// <summary>
/// Area of Square is side2.
/// Side * Side
/// </summary>
/// <returns></returns>
public static double AreaOfSquare(double side)
{
return side * side;
}
/// <summary>
/// Area of Rectangle is L*W.
/// L is the length of one side and W is the width of one side
/// </summary>
/// <returns></returns>
public static double AreaOfRectangle(double length, double width)
{
return length * width;
}
/// <summary>
/// Area of Traingle is b*h/2.
/// b is base and h is height of traingle
/// </summary>
/// <returns></returns>
public static double AreaOfTraingle(double baseOfTraingle, double heightOfTraingle)
{
return (baseOfTraingle * heightOfTraingle)/2;
}
}
}
由于我们的类是静态的,它应该只包含静态方法、变量和属性,因此我们定义了静态方法来计算矩形、正方形、圆形和三角形的面积,这些方法接受输入参数。并且由于公式始终保持不变,因此根据公式进行一些计算并将结果返回给调用者。请注意,我使用了静态 Math 类中的 PI,因为它是一个静态属性,并且 PI 的值始终保持不变,我们可以直接使用它来计算圆的面积。请注意,我在 usings 中定义了静态类,这是我们可以在 usings 中定义静态类的新方法,然后我们可以在类中直接使用它们的成员,而无需静态类名。我在主方法中对 System.Console
类也做了同样的事情。
在主方法中,我们现在通过以下方式将参数传递给方法来调用方法。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
namespace StaticClasses
{
class Program
{
static void Main(string[] args)
{
double radiusOfCircle = 10.8;
double lengthOfRectangle = 5.5;
double widthOfRectangle = 2.3;
double sideOfSquare = 4.0;
double heightOfTriangle = 9.0;
double baseOfTriangle = 5.0;
WriteLine("Area of Rectangle with length {0} and width {1} is {2}", lengthOfRectangle, widthOfRectangle, Areas.AreaOfRectangle(lengthOfRectangle, widthOfRectangle));
WriteLine("Area of Square with side {0} is {1}", sideOfSquare, Areas.AreaOfSquare(sideOfSquare));
WriteLine("Area of Circle with radius {0} is {1}", radiusOfCircle, Areas.AreaOfCircle(radiusOfCircle));
WriteLine("Area of Triangle with height {0} and base {1} is {2}", heightOfTriangle, baseOfTriangle, Areas.AreaOfTraingle(heightOfTriangle, baseOfTriangle));
ReadLine();
}
}
}
在上面提到的主方法中,我们通过类名 Areas 直接引用静态方法来调用它们。请注意,我们无需先创建对象即可调用静态方法,它们可以直接通过类名调用。当我们运行应用程序时,我们得到以下输出
所以,我们可以看到我们可以使用静态类和静态方法作为辅助类来完成简单的任务并获得结果。
静态与单例的选择
现在我们知道如何实现静态类以及如何实现单例设计模式并使类成为单例。问题是,何时使用静态,何时使用单例。我们可以说,单例在设计上给我们提供了更大的灵活性和更多的控制,我们可以根据需求和要求进行更改。因此,静态或单例的使用完全取决于它们的优缺点。在实际场景中,单例可以用于日志记录、处理数据库连接、打印假脱机等。请记住,单例是一个架构概念,而静态是 .NET 框架预先提供的。在需要对象处置的情况下使用静态存在潜在风险,因为静态成员在应用程序结束之前不会被处置,并且始终保留在内存中。此外,静态变量在某种程度上是全局的,它们在不同的应用程序之间共享,在 Web 应用程序或 ASP.NET 应用程序中使用它可能存在风险,因为无论用户会话如何,变量都将共享给所有用户,因为这些变量将保留在应用程序域中。我们已经讨论了很多关于静态和单例的选择,以便在开发时做出选择。我希望这些要点能帮助您决定何时使用什么。
结论
在本文中,我们通过实际示例学习了什么是延迟初始化和急切初始化。我们还学习了为什么有必要使用 sealed 关键字将单例类设为密封类。我们学习了静态类和单例类之间的区别,以及何时使用静态类和何时使用单例类。