C#单例设计模式学习
首先我们来用一句话解释下单例模式是什么,它就是一种只会把对象实例化一次的设计模式,也就是说,在程序的运行过程中,只会运行一次这个类的构造函数。那到底为什么要用单例呢,我们先来看下面这个类的构造函数。
public FirstSingle()
{
Thread.Sleep(2000);
}
这个构造函数非常简单,但是也非常耗费时间。如果我们需要实例化100个这样的对象,那就至少需要200s,不管是开发人员还是客户都会觉得这样太耗费时间了,但是如果在实例这200个对象的过程中,只执行一次构造函数的话,那就非常快了。没错,单例模式就是为了解决这个问题而横空出世的。接下来我会给大家分享几种实现单例模式的方法。
1.1 双层if+lock实现单例
我们先直接上代码,大家觉得有疑惑没关系,我会给大家一点一点地讲解。
private FirstSingle()
{
Thread.Sleep(2000);
}
首先将构造函数改成私有的方法,这样它就不会被外界随意调用了。
//使用静态变量
private static FirstSingle _FirstSingle;
private static readonly Object lockObject = new object();
//使用双层if+lock
public static FirstSingle CreateSingle()
{
if (_FirstSingle == null)
{
//减小Lock的作用范围
lock (lockObject)
{
//防止刚开始有多个线程进入创建线程的代码
//在第一个进入实例化对象后释放锁 在等待的第二个会进入
//所以需要再次判断对象是否已被初始化
if (_FirstSingle == null)
_FirstSingle = new FirstSingle();
}
}
return _FirstSingle;
}
我们首先定义了一个_FirstSingle,让它作为返回对象的出口。然后我们定义了一个Object类型的变量lockObject ,因为我们需要用它作为锁的对象,所以它必须是一个静态并且私有的变量。
在进入CreateSingle()方法后,会首先判断_FirstSingle是否为空,如果不为空就说明已经被实例化了,直接返回就行,这样就避免了多次调用构造函数。如果发现_FirstSingle为空,就需要通过调用构造函数来给它赋值。
但是这里就存在一个问题,可能存在多个进程同时要来获取对象,例如下面这样
Thread thread = new Thread(() =>
{
//FirstSingle single = FirstSingle.CreateSingle();
FirstSingle single = FirstSingle.CreateSecond();
single.show();
});
thread.Start();
如果我们不使用lock(),那么可能在刚开始时就有多个进程同时去调用构造函数,所以我们必须使用lock()来避免多次调用构造函数。
可能有人疑惑为什么需要用两个if呢。第一个if是用来当_FirstSingle已经被赋值后直接返回的,第二个if的作用也是为了防止多次被初始化。假设在程序刚开始运行时,_FirstSingle没有被赋值,所以此时有多个进程进入了第一层if中,启动一个进程获取了lock并且进去给_FirstSingle赋值了。在它释放lock后下一个进程会进入lock,实际上此时_FirstSingle已经被上一个进程赋值了,此时若不再判断一次,_FirstSingle仍然会被赋值。
1.2 使用静态构造函数来实现单例
private static FirstSingle _FirstSingle;
private FirstSingle()
{
Thread.Sleep(2000);
}
//该静态构造函数只会被调用一次
static FirstSingle()
{
_FirstSingle = new FirstSingle();
}
public static FirstSingle CreateSecond()
{
return _FirstSingle;
}
这种方法比上面的实现步骤要简单些,只要是系统确保的静态构造函数只会被运行一次,所以就实现了单例。
但是此时也有一个问题(不要笑),这样虽然能使确保构造函数只被执行一次,但是如果我们要实例化的200个对象的参数值都不同,那还是要通过执行多次构造函数来给不同的对象参数赋值,由此便引出了接下来的内容:原型模式。
2.1 通过浅克隆实现原型模式
原型模式就是为了解决单例存在的问题而解决的。它本质就是通过单例实例化出一个对象,此时会调用构造函数给各个参数赋值。接下来如果还需要实例化一个对象,就会先将刚才创建的对象作为原型复制一份,再修改参数的值。这样就只执行了一遍构造函数。
我们有一下的几个类
[Serializable] //添加可序列化的特性
public class Student
{
public string id { get; set; }
public string name { get; set; }
public Course course { get; set; }
}
[Serializable]
public class Course
{
public string CourseName { get; set; }
}
我们继续在Student类中添加方法和属性
private static Student _Student = null;
//系统自动调用且至调用一次私有构造函数
//私有方法 用于构造
private Student()
{
Thread.Sleep(2000);
long res = 0;
for (int i = 0; i < 10000; i++)
{
res += i;
}
Console.WriteLine("{0}被构造", this.GetType().Name);
}
static Student()
{
_Student = new Student()
{
id = "1",
name = "lc",
course = new Course() { CourseName = "" }
};
}
public static Student CreateInstance(string id, string name, string courseName)
{
//浅克隆
//赋值一份构造好的对象出来
Student stu = (Student)_Student.MemberwiseClone();
stu.id = id;
//在修改了string事,是重新创建一个string,并修改原有的变量指向这个新的地址
//挤string中的=为new 以为string的内容是不可修改的
stu.name = name;
//对于非string的引用型变量 只能修改引用值
stu.course = new Course() { CourseName = courseName };
return stu;
}
public void Study()
{
Console.WriteLine("{0}正在学习{1}", this.name, this.course.CourseName);
}
可以看到以上代码与单例模式的区别主要是CreateInstance()方法的区别,它并不是直接返回_Student,而是先克隆一份,然后根据传入的参数来给对象的参数赋值。
细心的同学就会发现,在修改CourseName 时,并不是直接给stu.course.CourseName 赋值,这是因为参数course是一个引用类型的变量,保存的是地址而不是具体内容,如果直接修改,那么所有对象的CourseName 都会被修改。
那有人可能要问了,string也是引用类型的变零,那为什么可以直接修改呢。其实string的“=”就相当于使用了“new ”,因为string的值是不可修改的,所以当通过“=”给string赋值时,其实是新创建了一个string对象。
2.2 通过深度克隆实现原型模式
在浅克隆中,对引用型参数的处理不是很方便,那就可以用深克隆了。在这种模式下,其实就是讲原型对象序列化成字符串,然后再将字符串反序列化成指定的对象。这样我们在没有使用构造方法的情况下得到了一个完全独立的对象,随便修改值引用类型变量都不会影响到其他对象。
先上用于序列化和反序列化的代码
//将对象序列化和反序列化实现深克隆
public class SerializeHelper
{
public static string Serializable(Object target)
{
using (MemoryStream stream = new MemoryStream())
{
new BinaryFormatter().Serialize(stream, target);
return Convert.ToBase64String(stream.ToArray());
}
}
public static T Derilizable<T>(string target)
{
byte[] targetArray = Convert.FromBase64String(target);
using (MemoryStream stream = new MemoryStream(targetArray))
{
return (T)(new BinaryFormatter().Deserialize(stream));
}
}
public static T DeepClone<T>(T t)
{
return Derilizable<T>(Serializable(t));
}
}
此时需要注意的是要给被序列化的对象添加可 序列化的属性[Serializable]
接下来修改CreateInstance
public static Student CreateInstance(string id, string name, string courseName)
{
//深度克隆
Student stu = SerializeHelper.DeepClone<Student>(_Student);
stu.id = id;
stu.name = name;
stu.course.CourseName = courseName;
return stu;
}
最后我们来讨论下什么场景下用原型模式呢?就是当对象的克隆比创建省时间的时候。