(复制粘贴是最容易的编程,但也是最没有价值的编程。复制粘贴就会造成重复,当需求变化或有 Bug 时就需要改多个地方)
把声明的工作放到类的全局变量中完成(private FormToolbox ftb;),这样就可以在方法中判断这个变量是否实例化过了。
21.3 是否实例化是自己的责任
单例类是否实例化过,应该由单例类自己判断。(而不是将判断的代码放到调用类当中)
(如果已经实例化过,则不需要再实例化)实例化其实就是 new 的过程,而如果不对构造方法做改动的话,是不可能阻止他人不去用 new 的,所以我们完全可以直接把这个类的构造方法改成私有(private)。
于是,只要将“工具箱”类的构造方法写成 private 的,那么外部程序就不能用 new 来实例化它了。
对于外部代码,不能用 new 来实例化它,但是我们完全可以再写一个 public 方法,叫做 GetInstance(),这个方法的目的就是返回一个类实例,而此方法中,去做是否有实例化的判断。
如果没有实例化过,由调用 private 的构造方法 new 出这个实例,之所以它可以调用是因为它们在同一个类中,private 方法可以被调用。
参考代码:
public class FormToolbox : Form { private static FormToolbox ftb = null; //声明一个静态的类变量(全局可用) private FormToolbox() //构造方法私有,外部代码不能直接用new来实例化它 { //InitializeComponent(); //这个方法也许只有窗体才有,其他的类只要空着就行 } public static FormToolbox GetInstance() //得到类实例的方法,返回值就是本类对象,注意也是静态的 { if(ftb == null || ftb.IsDisposed) { ftb = new FormToolbox(); //当内部的ftb是null或者被Disposed过,则new它。此时将实例化的对象存在静态的变量ftb中,以后就可以不用实例化而得到它 //可以在这里继续设置ftb的其他属性 } return ftb; } }
(当窗体关闭后,实例并没有变为 null,而只是 Disposed —— if (ftb == null || ftb.IsDisposed))
这样一来,客户端不再考虑是否需要去实例化的问题,而把责任都给了应该负责的类去处理。这就是单例模式。
21.4 单例模式
单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。(因为是全局访问点,所以该方法必须是静态的)
通常,我们可以使用一个全局变量(static)使得一个对象被访问,但它不能防止你实例化多个对象(对象的属性也可能会发生改变)。一个最好的办法就是,让类自身负责保存它的唯一实例。
这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法(即返回该实例的静态方法)。
单例模式的好处:
因为单例模式封装了它的唯一实例,所以它可以严格地控制客户怎样访问它以及何时访问它。简单地说就是对唯一实例的受控访问。
单例与实用类(如Math类)的静态方法类似,实用类通常也会采用私有化的构造方法来避免其有实例。
但它们也有很多的不同,比如:
实用类不保存状态,仅提供一些静态方法或静态属性让你使用,而单例类是有状态的。
实用类不能用于继承多态,而单例虽然实例唯一,却是可以有子类来继承。
实用类只不过是一些方法、属性的集合,而单例却是有着唯一对象的实例。
★21.5 多线程时的单例
另外,还需要注意一些细节,比如说,多线程的程序中,当多个线程同时(注意是同时)访问单例类,调用GetInstance()方法,是会有可能造成创建多个实例的。
解决的方法是给进程加锁来处理:
lock 语句的含义—— lock 可以确保当一个线程位于代码的临界区时,另一个线程不进入临界区。(如果其他线程视图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放)
(当写在了 .NET 网站程序的启动部分时应该就不用考虑这个多线程的问题了,因为它的实例化并不是由线程发起的)
public class Singleton { private static Singleton instance; private static readonly object syncRoot = new object(); //程序运行时创建一个静态只读的进程辅助对象 private Singleton() { } public static Singleton GetInstance() { lock(syncRoot) //在同一个时刻,加了锁的那部分程序只有一个线程可以进入 { if(instance==null) { instance = new Singleton(); } } return instance; } }
由于有了 lock,就保证了多线程环境下的同时访问也不会造成多个实例的生成。(我们并不知道 instance 实例有没有被创建过,所以无法对它加锁,于是创建了一个 syncRoot 并选择对其加锁)
但是,当前这种写法,每次调用 GetInstance 方法时都需要加锁,这种做法是会影响性能的,所以可以对这个类做如下改良:
21.6 双重锁定
public class Singleton { private static Singleton instance; private static readonly object syncRoot = new object(); //程序运行时创建一个静态只读的进程辅助对象 private Singleton() { } public static Singleton GetInstance() { if(instance==null) //先判断实例是否存在,不存在时再加锁处理 { lock (syncRoot) //在同一个时刻,加了锁的那部分程序只有一个线程可以进入 { if (instance == null) { instance = new Singleton(); } } } return instance; } }
现在这样就不用每次都让线程加锁了,而只是在实例未被创建的时候再加锁处理。同时也能保证多线程的安全。这种做法被称为 Double-Check Locking(双重锁定)
这里为什么要两次判断 instance 是否为空呢?
当 instance 为 null 并且同时有两个线程调用 GetInstance()方法时,它们将都可以通过第一重 instance == null 的判断。然后由于 lock 机制,这两个线程只有一个进入,另一个在外排队等候,必须要其中的一个进入并出来后,另一个才能进入。
而此时如果没有了第二重的 instance 是否为 null 的判断,则第一个线程创建了实例,而第二个线程还是可以继续再创建新的实例,这就没有达到单例的目的。
21.7 静态初始化
静态初始化的实现方式更为简单,同时也有着与单例模式类似的特性,但两者仍有一些不同,比如静态初始化只能在静态初始化期间或在类构造函数中分配变量(instance 变量标记为 readonly)
附加:单例模式的继承
父类:
public class Singleton<T> where T : class, new() { private static T _instance; //使用了泛型T,而子类中又用自身进行泛型,所以这里可以直接将 _instance 理解为子类实例(也是子类唯一实例) private static readonly object syslock = new object(); public static T getInstance() { //线程安全锁 if (_instance == null) { lock (syslock) { if (_instance == null) { _instance = new T(); } } } return _instance; } }
子类需要泛型一下:
public class Two : Singleton<Two> { public void Show() { Console.WriteLine("Two Sub class......."); } }
调用:
Two.getInstance().Show();