单例模式(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<IDatabase,Database>();
在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,但是其背后的私有字段都是静态的。所以不管这个ChiefExecutiveOfficer实例化多少次,其内部引用的都是同一个数据。比如,可以实例化两个对象,但是其内部的内容是一模一样的。
单态模式简单,但是容易引起混乱,所以要想简单,要想实现单例效果,最好还是使用IOC这种依赖注入的框架,让它来帮助我们来管理实例及其生命周期。
以上就是解析C#设计模式之单例模式的详细内容,更多关于c# 单例模式的资料请关注自学编程网其它相关文章!
- 本文固定链接: https://www.zxbcw.cn/post/201175/
- 转载请注明:必须在正文中标注并保留原文链接
- QQ群: PHP高手阵营官方总群(344148542)
- QQ群: Yii2.0开发(304864863)