了解单例类和静态类之间的区别以及何时在应用程序中使用它。
.NET Core中开发应用程序时,您可能经常需要一个类的单个共享实例。典型的用例是LogManager,StateManager等的实例。您可以使用单例类或静态类来实现此目的。选择哪种选择(单项还是静态)取决于多个因素。本文介绍了单例类和静态类,以及何时应该使用一个类与另一个类。
若要使用本文提供的代码示例,您应该在系统中安装Visual Studio 2019。如果您还没有副本,则可以在此处下载Visual Studio 2019。您可以从此处下载.NET 5.0。
我们将基于以下几点对单例类和静态类进行比较和对比:
依赖注入
内存管理
可扩展性
可测性
在以下各节中,我们将实现两个类-名为StaticLogger的静态类和名为SingletonLogger的单例类。这两个类都提供了Log方法,可用于将数据记录到特定的日志目标。在下面的两个示例中,为了简洁起见,我都省略了必要的代码来记录数据。
静态类不能实例化或扩展。它们是抽象的并隐式密封。要将类声明为静态,应在类声明中使用static关键字将其标记。使用static关键字可以定义静态类和静态成员。
以下代码段说明了一个静态类。
public static class StaticLogger
{
private static readonly object lockObj = new object();
public static void Log(string message)
{
//Write code here to log data.
}
}
通常,静态类用于实现帮助程序或实用程序类。它们通常包含一些可重用的方法和属性的集合。
单例类(单例设计模式的实现)是只能存在单个实例的类。
下面的代码清单说明了单例类的简约实现。静态Instance属性可用于调用单例类的成员。
public sealed class SingletonLogger
{
private static SingletonLogger instance;
private static object lockObj = new Object();
private SingletonLogger () { }
public static SingletonLogger Instance
{
get
{
lock (lockObj)
{
if (instance == null)
instance = new SingletonLogger();
}
return instance;
}
}
public void Log(string message)
{
//Write code here to log data.
}
}
ASP.NET Core 5 MVC内置了对依赖项注入的支持。在ASP.NET Core MVC中工作时,可以在Startup类的ConfigureServices方法中向容器添加服务。然后使用依赖注入将这些服务提供给应用程序中的其他类。然后,您可以利用控制器或其他类中的依赖项注入来使用注入的实例。
假设您有一个名为FileLogger的非静态类,该类实现了一个称为ILogger的接口,则可以使用以下代码段将服务添加到具有单例生存期的容器中。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSingleton();
}
您无需自己编写任何代码即可实现单例模式-ASP.NET Core 5 MVC运行时将负责处理它。
相反,如果您尝试以与上一示例中注入非静态类的实例相同的方式注入静态类,则会收到错误消息。这主要是因为静态类没有任何实例,因此不能用作类型参数。
以下代码段对此进行了说明。
ppublic void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSingleton();
}
编译应用程序时,将出现以下错误。
静态类型不能被用来作为类型参数
您可以使用方法或属性注入在静态类中使用依赖项注入。但是,您不能在静态类中使用构造函数注入,因为静态类的构造函数不能接受任何参数。
静态对象和静态类存储在托管堆的称为高频堆的区域中。仅当应用程序卸载时,才会释放存储在高频堆中的对象。单例类的单个实例是静态的,因此单例类的一个实例存储在高频堆中。因此,当应用程序卸载或终止时,由单例类的实例占用的内存将被释放。
但是,与只能具有静态对象的静态类不同,单例类可以同时具有静态和非静态对象。因此,从内存管理的角度来看,当您使用单例类时,可以利用垃圾回收管理对象。
单例类通常包含一个私有构造函数,并被标记为已密封,以指示它既不能实例化也不能继承。因此,只有在单例类中具有非私有构造函数的情况下,才可以扩展单例类,如下面给出的代码片段所示。
public class SingletonLogger
{
protected SingletonLogger() { }
//Other members
}
现在,您可以扩展单例类,如下所示。
public class LogManager : SingletonLogger
{
//Write your implementation here
}
同样,您不能继承静态类并覆盖其方法。此外,虽然可以在单例类中具有扩展方法,但静态类不能具有扩展方法。
虽然可以克隆单例类的实例,但在静态类中则不可能。您可以在Singleton类中拥有Dispose方法,但不能在静态类中拥有Dispose方法。您无法实例化静态类,因此无法在需要“ this”引用(索引器,方法参数)的任何地方使用它。静态类的任何成员(例如构造函数,字段,属性或事件)都是静态的。每当使用静态类时,您都无法控制何时调用静态构造函数。
可以将Singleton类设计为延迟加载,而只能静态地加载静态类。延迟初始化是一种延迟对象创建的技术,即,可以按需加载类的实例,从而提高应用程序的性能并减少内存需求。
虽然测试单例类很容易,但是对于静态类却不能这么说。模拟静态类非常困难(如果不是不可能的话)。在静态类上执行的测试可能会相互影响,因为这些测试不会在不同的实例上执行。
请注意,与静态类有关的静态方法本身并非不可测试的。可以对不持有状态或不更改状态的静态方法进行单元测试。只要该方法及其依赖性是幂等的,就可以对该方法进行单元测试。当静态方法调用其他方法或被测试的对象调用静态方法时,就会出现问题。
综上所述,静态类是不能包含任何实例的类,只包含静态成员,即与特定实例不相关的成员。静态类表示与任何特定实例都不相关的一组方法的组织单位。
当您只需要一个包含多个实用程序方法的实用程序类时,静态类是一个不错的选择,在这种情况下,您不需要实例。因为您没有此类的任何实例,所以这实现了简单的实现并提高了应用程序的性能。
单例模式可用于设计只需要一个实例的类。典型示例包括用于日志记录,缓存,线程池等的管理器类。当您要管理打印机假脱机程序之类的共享资源时,单例类也是一个不错的选择。为此,您应该有一个实例,以避免对同一资源的请求冲突。