星星博客 »  > 

解析C#设计模式之单例模式

单例模式(Singleton),故名思议就是说在整个应用程序中,某一对象的实例只应该存在一个。比如,一个类加载数据库中的数据到内存中以提供只读数据,这就很适合使用单例模式,因为没有必要在内存中加载多份相同的数据,另外,有些情况下不允许内存中存在多分份相同的数据,比如数据过大,内存容不下两份相同数据等等。

约定单例模式(Singleton by Convention)

这种方式有点“Too simple, Sometimes naïve”,他就是提示使用者,我是单例,不要重复初始化我,比如:
public class Database
{
  /// <summary>
  /// 警告,这是单例,不要初始化多次,否则,后果自负.
  /// </summary>
  public Database() {}
};

一种情况是,根本不会注意到这个提示,其次是在很多时候,这些初始化是偷偷摸摸无意中发生的,比如通过反射,通过工厂产生(Activator.CreateInstance),通过注入等等,虽然有一个“约定大于配置”,但是这里不使用。

单例模式最常见的想法是提供一个全局的,静态的对象。

public static class Globals
{
  public static Database Database = new Database();
}

这种方式并没有很安全。这个并没有阻止用户在其他地方new Database,并且,用户可能并不知道有一个Globals类,里面有个Database单例。

经典的实现方式

唯一的方式阻止用户实例化对象是将构造函数变成私有的,并且提供方法或者属性返回唯一的内部对象。

public class Database
{
  private Database() { ... }
  public static Database Instance { get; } = new Database();
}

现在将构造函数设置为了私有,当然设为私有依旧可以通过反射继续调用,但是这毕竟需要额外操作,这已经可以阻止大部分用户直接实例化了。通过将实例定义为static静态, 使得其生命周期延长至应用程序运行期间。

延迟初始化

以上方法线程安全,但是因为是静态属性,在类的所有实例创建之前,或者任何静态成员访问之前就会初始化,并且在每个AppDomain里都只会初始化一次。

如何实现延迟初始化,即将单例对象的构造推迟到应用程序首次请求该对象进行时,如果应用程序永远不请求对象,则对象永远不会构造。在之前,可以使用double check方式。其实要实现正确的double check还是有些问题需要注意,比如上面这个例子,第一版可能这么写,用一个锁。

public class Database
{
  private static Database db;
  private static object olock = new object();
  private Database()
  { }
 
  public static Database GetInstance()
  {
    lock (olock)
    {
      if (db == null)
      {
        db = new Database();
      }
      return db;
    }
  }
}

这虽然线程安全,但是每次访问GetInstance,不论对象是否已经创建,都需要获取然后释放锁,比较消耗资源,所以,在外面再加一层判断。

public class Database
{
  private static Database db;
  private static object olock = new object();
  private Database()
  { }
 
  public static Database GetInstance()
  {
    if (db == null)
    {
      lock (olock)
      {
        if (db == null)
        {
          db = new Database();
        }
      }
    }
    return db;
  }
}

在访问对象之前判断是否已经初始化,如果初始化直接返回,这样就避免了一次对锁的访问。But,这里仍然存在问题。假设Database初始化起来耗时,当线程A获得锁正在对db进行初始化的时候,线程B在最外层判断db是否为空,这个时候,线程A正在初始化db,有可能只初始化了部分,这个时候db就可能不为空,直接返回了没有完全初始化完全的对象,这可能导致线程B崩溃。

解决方式是,将对象存储到临时变量中,然后以原子写的方式存储到db中,如下

public class Database
{
  private static Database db;
  private static object olock = new object();
  private Database()
  { }
 
  public static Database GetInstance()
  {
    if (db == null)
    {
      lock (olock)
      {
        if (db == null)
        {
          var temp = new Database();
          Volatile.Write(ref db, temp);
        }
      }
    }
    return db;
  }
}

非常繁琐,虽然实现了延迟初始化,但是跟开头的静态字段对比,复杂太多,而且一不小心就会写错。虽然可以简化为:

public static Database GetInstance()
{
  if (db == null)
  {
    var temp = new Database();
    Interlocked.CompareExchange(ref db, temp, null);
  }
  return db;
}

该方法看起来没有用锁,temp对象有可能会被初始化两次,但是在将temp写入到db的时候,Interlock.CompareExchange会保证只会有1个对象正确被写入到db,没有被写入的temp对象会被垃圾回收,这种方式速度比上述的double check要快。但仍然需要学习成本。幸好,C#提供了Lazy方法:

private static Lazy<Database> db = new Lazy<Database>(() => new Database(), true);
public static Database GetInstance() => db.Value;

简单且完美。

依赖注入与单例模式

前面的单例模式其实是一种代码侵入的做法,就是要想一个原本没有实现单例的代码要实现单例,需要修改代码实现,并且这个代码还容易出错。有些人认为单例模式的唯一正确的做法就是在IOC依赖注入中,这样不需要修改源代码,通过依赖注入框架来实现依赖注入,在统一的入口,统一的管理生命周期,在ASP.NET Core MVC中,在Startup的ConfigureServices代码中:

services.AddSingleton<Database>();

或者加入需要用IDatabase的地方,要用Database单例的话:

services.AddSingleton<IDatabaseDatabase>();

在ASP.NET Core MVC的后续代码中,只要用到IDatabase的地方,就会用Database的单例来实现,不需要我们在Database内做任何修改。在使用的时候,只需要引用IServiceProvider接口里的GetService方法,IServiceProvider是由ASP.NET Core MVC的IOC框架直接提供,不需要特别处理:

public XXXController(IServiceProvider serviceProvider)
{
   var db = serviceProvider.GetService<IDatabase>();
}

单态模式(Monostate)

单态模式是单例模式的一个变种,它是一个普通的类,但是其行为和表现就像单例模式。

比如我们在对一个公司的人员结构进行建模,一个典型的公司一般只会有一个CEO,
public class ChiefExecutiveOfficer
{
  private static string name;
  private static int age;
 
  public string Name
  {
    get => name;
    set => name = value;
  }
 
  public int Age
  {
    get => age;
    set => age = value;
  }
}

这个类的属性有get,set,但是其背后的私有字段c#教程都是静态的。所以不管这个ChiefExecutiveOfficer实例化多少次,其内部引用的都是同一个数据。比如,可以实例化两个对象,但是其内部的内容是一模一样的。

单态模式简单,但是容易引起混乱,所以要想简单,要想实现单例效果

相关文章