博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

.Net Framework框架模式(第二篇 单件模式)

Posted on 2008-08-02 12:31  a-peng  阅读(836)  评论(0编辑  收藏  举报
Singleton模式,由于其实现相对简单,所以号称设计模式中最简单的模式.
但是static通常会给你造成一些障碍.不信啊,那你就看看吧,而且还有几个Effective C#条款 :) 希望不会让你失望.
该篇并没有涉及到.Net Framework源码,就算是挂羊头卖狗肉吧.希望延续上篇的高质量.

让我们先来写个简单的SqlHelper,封装SQL数据库的操作.
using System;
using System.Data;
using System.Data.SqlClient;

namespace APeng
{
    
public class SqlHelper
    
{
        
private string m_connString = "Data Source=(local);" +
            
"User ID=sa;Password=password;Initial Catalog=discuz;Pooling=true"
        
//Sql数据库连接字符串                            

         
public SqlDataReader ExecuteReader(CommandType cmdType, string cmdText, params SqlParameter[] cmdParms)        
         
{
            SqlCommand cmd 
= new SqlCommand();
            SqlConnection conn 
= new SqlConnection(m_connString);

            
try
            
{
                PrepareCommand(cmd, conn, cmdType, cmdText, cmdParms);
                SqlDataReader rdr 
= 
cmd.ExecuteReader(CommandBehavior.CloseConnection);
                cmd.Parameters.Clear();
                
return rdr;
            }

            
catch (Exception ex)
            
{
                conn.Close();
                conn.Dispose();
                
throw ex;
            }

        }


        
private void PrepareCommand(SqlCommand cmd, SqlConnection conn, CommandType cmdType, string cmdText, SqlParameter[]  cmdParms)
        
{
            
if (conn.State != ConnectionState.Open)
                conn.Open();

            cmd.Connection 
= conn;
            cmd.CommandText 
= cmdText;
            cmd.CommandType 
= cmdType;

            
if (cmdParms != null)
            
{
                
foreach (SqlParameter parm in cmdParms)
                    cmd.Parameters.Add(parm);
            }

        }

    }

}

这段代码大家应该很熟悉了,接下来让我们来使用它.
using System;
using System.Data;
using System.Data.SqlClient;

namespace APeng
{
    
class Program
    
{
        
static void Main(string[] args)
        
{
            SqlHelper helper 
= new SqlHelper();
            
string cmdText = "select fid,name from dnt_forums";
            
using (SqlDataReader reader = helper.ExecuteReader(CommandType.Text, cmdText, null))
            
{
                
while (reader.Read())
                
{
                    Console.WriteLine(
"编号:" + reader["fid"]);
                    Console.WriteLine(
"名称:" + reader["name"]);
                }

            }

            
            Console.Read();
        }


        
    }

}

程序正常输出: 编号:1 名称:版块1 编号:2 名称:版块2 (很简单,不是嘛.)
接下来我们将要优化这个SqlHelper
) SqlHelper中的private string m_connString = "......"
修改成private static readonly string m_connString = "......" 
这个修改是否有必要呢? (如果没能给我带来什么好处,我为什么要修改呢,所以你得说服我!)

小菜先在A地方实例化一个SqlHelper.
SqlHelper helper1 = new SqlHelper();
那么会有如下构造过程:
1.为数据成员m_connString分配内存空间,此时空间存储数据为null. (执行的是private string m_connString)
 (如果是值类型如int,float,double,空间存储数据为0,如果是引用类型空间存储数据为null,下面还会详细说明)
2.执行数据成员m_connString的初始化语句.(也就是m_connString = "......")
  (那么现在空间存储数据为"......") 
注意:private string m_connString = "......" 这个语句在编译时会被切割成两部份.
语句一:private string m_connString; 语句二: m_connString = "......";

语句二在编译阶段会被放到相应的构造函数最前方,静态数据成员会被放到静态构造函数,非静态数据成员会被放到非静态构造函数.但为了更好的说明对象的构造顺序,小菜将初始化语句独立出来,而且顺序是一致的.
3.执行SqlHelper的构造函数

小菜然后在B地方又实例化一个SqlHelper.
SqlHelper helper2 = new SqlHelper();
那么会有如下构造过程:
1.为数据成员m_connString分配内存空间,此时空间存储数据为null.(执行的是private string m_connString)
2.执行数据成员m_connString的初始化语句.(也就是m_connString = "......")
  (那么现在空间存储数据为"......")
3.执行SqlHelper的构造函数

,有没有搞错啊,怎么一直为m_connString分配内存空间.
而且老是把m_connString空间存储数据置为相同的 "......"
该死的,你就不能聪明点,做一次就够了.(浪费我们宝贵的时间和宝贵的内存资源)

,看来我们得自己动手优化了.怎么优化呢?
等等,小菜刚才说什么来着?修改成private static readonly string m_connString = "......",那它能改变这种状况吗?

小菜先在A地方实例化一个SqlHelper.
SqlHelper helper1 = new SqlHelper();
那么会有如下构造过程:
1.为静态数据成员m_connString分配内存空间,此时空间存储数据为null.

 (如果是值类型如int,float,double,空间存储数据为0,如果是引用类型空间存储数据为null,下面还会详细说明)
2.执行静态数据成员m_connString的初始化语句,也就是上面的m_connString = "......"
  (那么现在空间存储数据为"......") (m_connString="......"该语句编译时会被放到静态构造函数中)
3.执行SqlHelper的构造函数

小菜然后在B地方又实例化一个SqlHelper.
SqlHelper helper2 = new SqlHelper();
那么会有如下构造过程:
1.执行SqlHelper的构造函数

看来真不错,变聪明了,只分配了一次m_connString的内存空间,只初始化了一次m_connString,看来多亏了static.
注意:这里应该引起你的关注.
有一些朋友的代码中时常出现在类的数据成员定义中为值类型成员赋0,为引用类型赋null

public class Person//人类
{
    
private int _age = 0;//年龄
}

 

public class Person//人类
{
    
private Address _address = null;//地址对象
}

这其实是无必要的,和上面new SqlHelper()的构造过程一样.在分配数据成员的内存空间时,便会为值类型成员赋0,为引用类型赋null.如果我们显示的赋值的话,不但没有任何帮助,反而会增加指令的操作,影响效率.
现在小菜通过局部变量来对比,让你的印象更深刻.

public class Class1
{
    
public static void Method1() {
        System.IO.FileStream fs;
//定义一个FileStream
        try {
            fs 
= new FileStream(.);
            
//使用fs
        }
catch (Exception ex) {
            
throw ex;
        }
finally {
            
if (fs != null)
                fs.Close();
        }

    }

}

上面这一小段代码,相信你相当熟悉.但是它会出错,哪出错呢?
if (fs != null)使用了未赋值的变量.如果修改代码为System.IO.FileStream fs = null;则可以.
明白了吗,局部变量可不象数据成员初始化的时候为被置为null或0,俺们得自己来.(Effective C#有介绍过上面的知识点)

其实上面主要的知识点是对象的构造过程
,让我们来复习一下吧.
第一种:
1.当我们调用类里的静态方法时,如果类里面的静态成员还未初始化,那么这个类的所有静态成员依据在类里面出现的次序初始化.
2.为静态成员分配内存空间,此时空间存储数据为0null
3.执行静态成员的初始化语句(也就是赋值语句)

    这里的赋值代码会在编译的时候被移到静态构造函数中执行.(本文前面的SqlHelper构造过程,红字注意中已经提过了)4.执行类的静态构造函数

很明显,这样的话如果我们第二次调用类里的静态方法时,1,2,3,4都不会被执行了.
第二种:
1.当我们对类实例化的时候,如果类里面的静态成员还未初始化,那么这个类的所有静态成员依据在类里面出现的次序初始化.
2.为静态成员分配内存空间,此时空间存储数据为0null
3.执行静态成员的初始化语句(也就是赋值语句)
    这里的赋值代码会在编译的时候被移到静态构造函数中执行.(本文前面的SqlHelper构造过程,红字注意中已经提过了)
4.执行类的静态构造函数
为普通成员分配内存空间,此时空间存储数据为0null
6.执行普通成员的初始化语句(也就是赋值语句)
    这里的赋值代码会在编译的时候被移到构造函数中执行.(本文前面的SqlHelper构造过程,红字注意中已经提过了)
7.  执行类的构造函数

很明显,这样的话如果我们第二次实例化类,1,2,3,4也都不会被执行,只会执行5,6,7

) 将public SqlDataReader ExecuteReader() 修改成 public static SqlDataReader ExecuteReader()
修改不修改关键看什么呢?
如果该方法无需保持或变动跟对象有关的状态,则说明该方法与任何实例无关.所以可设计成static方法,我们的ExecuteReader()满足上面条件,无需操作对象有关状态,而且无需变动跟对象有关的状态.

) public class SqlHelper 修改为 public static class SqlHelper
经过上面的修改后.我们的SqlHelper已经是一个合适工具类,它无需被实例化使用abstract,无需被继承使用sealed
可是没有public abstract sealed class SqlHelper 但有static, 二者是等效的,称为静态类.

Math类相信大家都用的很爽吧.比如Math.Abs()取绝对值等方法.
很明显Math也是做为一个工具类,所以在.Net中也被设计成静态类.

注意:有些朋友要说了,SqlHelper可不可以使用单件模式设计.
可以,可是不合适.做为一个工具类,它根本无需被实例化,一次都不要.
有些朋友要说了,讲单件模式讲到哪里去了都不知道,但小菜觉得区分不好static,单件模式是用不好的.滥用误用更是不在话下.

接下来就正式来说单件模式吧! (只允许实例化一次)
1)第一种单件模式

public sealed class Singleton
{
    
private static readonly Singleton _instance = new Singleton();

    
private Singleton()
    
{ }

    
public static Singleton Instance
    
{
        
get
        
{
            
return _instance;
        }

    }


    
public void DoSomething()
    
{
        Console.WriteLine(
"做些事情");
    }

}

接下来,我们来用用测试一下

static void Main(string[] args)
{
    Singleton s1 
= Singleton.Instance;
    Singleton s2 
= Singleton.Instance;  
    s1.DoSomething();
//做些事情     
    s2.DoSomething();//做些事情    
    Console.WriteLine(object.ReferenceEquals(s1, s2));//是否为相同实例,true
}

,不错.大家觉得上面的设计怎么样?
其一: 如果很看注性能的话,或者Singleton很浪费资源的话,使用lazy-init会比较好一点.当需要用时才初始化.
     
但通常上面的代码是够用的.
其二: 由于静态成员的初始化时间很难控制,所以如果是静态引用类型的话,放在静态构造函数中初始化会更加适合.
      而且如果对象的初始化会出现异常的话,也只有放在静态构造函数中才有办法try-catch-finally
     
这里就用到了前面讲到的对象构造顺序.如果不清楚的话,建议拉到前面在看一下,很重要.(Effective C#知识点)
所以代码最好修改为

public sealed class Singleton
{
    
private static readonly Singleton _instance;

    
private Singleton()
    
{ }

    
static Singleton()
    
{
        _instance 
= Singleton();
    }


    
public static Singleton Instance
    
{
        
get
        
{
            
return _instance;
        }

    }


    
public void DoSomething()
    
{
        Console.WriteLine(
"做些事情");
    }

}

如果对性能不是太讲究的话,推荐该做法.而且适合多线程.通常大家的代码中也不见得有多耗资源,不是吗.
)第二种单件模式lazy-init
 1 public sealed class Singleton
 2 {
 3     private static Singleton _instance;
 4 
 5     private Singleton()
 6     { }
 7 
 8     public static Singleton Instance
 9     {
10        get
11        {
12            if (_instance == null)
13            {
14                _instance = new Singleton();
15            }

16            return _instance;
17        }

18    }

19
20    public void DoSomething()
21    {
22        Console.WriteLine("做些事情");
23    }

24}

25

代码也很清晰,但该单件模式只适用于单线程.不适合与多线程,为什么呢?
线程1Singleton.Instance执行到第12if(_instance == null) 成立执行到13行{ 停下
线程2Singleton.Instance执行到第12if(_instance == null) 也同样成立
    
执行第14_instance = new Singleton() 设为obj1 停下
线程1继续执行同样执行第14_instance = new Singleton() 设为obj2
obj1obj2是不同.已经不是单件,是双件了,线程越多,可能多件都有可能.

这样的话,很多朋友马上会相到把 if(_instance==null){/*省略*/}锁上.不就ok.
那就进入第三种单件模式.
)第三种单件模式

public sealed class Singleton
{
    
private static Singleton _instance;
    
private static object _lockHelper = new object();

    
private Singleton()
    
{ }

    
public static Singleton Instance
    
{
        
get
        
{
            
lock (_lockHelper)
            
{
                
if (_instance == null)
                
{
                    _instance 
= new Singleton();
                }

            }

            
return _instance;
        }

    }


    
public void DoSomething()
    
{
        Console.WriteLine(
"做些事情");
    }

}

看来该单件模式支持多线程,但看来并不是太聪明.
如果_instance已经被初始化,然而每次线程进入还是需要同步,很明显性能下降太多.
我们只需要第一次初始化的时候同步,之后不要同步是最好的.效率也高.
看来double-check即双检查,会大大提高效率.
那就进入第四种单件模式.

)第四种单件模式

public sealed class Singleton
{
    
private static Singleton _instance;
    
private static object _lockHelper = new object();

    
private Singleton()
    
{ }

    
public static Singleton Instance
    
{
        
get
        
{
            
if (_instance == null)
            
{
                
lock (_lockHelper)
                
{
                    
if (_instance == null)
                    
{
                        _instance 
= new Singleton();
                    }

                }

            }

            
return _instance;
        }

    }


    
public void DoSomething()
    
{
        Console.WriteLine(
"做些事情");
    }

}

记得看到过有人这么问.为什么要双检查呢.为什么不改成如下代码?

 1 public static Singleton Instance
 2 {
 3     get
 4     {
 5         if(_instance == null)
 6         {
 7             lock(_lockHelper)
 8             {
 9                 _instance = new Singleton();
10            }
 
11        }

12        return _instance
13    }

14}

15

那我们来分析一下.
线程1执行到第8行{ 停下来.
线程2执行到第7行 lock(_lockHelper),因为线程1lock里面,被阻塞,停下来.
线程1继续执行第9行 _instance = new Singleton(),初始化一个_instance设为obj1,退出lock
线程2进入lock里面,也初始化一个_instance设为obj2,退出lock
现在又是双件了.所以为什么要叫double-check双检查,也是来源与此,双if嘛.

这个单件模式被用的最多.但它就真的那么完美无缺吗?
因为上面的代码被编译,编译器由于考虑时间和空间的问题,会对代码进行优化,指令的顺序可能也会被改变.
所以在多线程中可能还是会出状况,虽然这种概率很低,但要是有解决方法为什么不用呢?
那就来看第五种单件模式

)第五种单件模式

public sealed class Singleton
{
    
private static volatile Singleton _instance;
    
private static object _lockHelper = new object();

    
private Singleton()
    
{ }

    
public static Singleton Instance
    
{
        
get
        
{
            
if (_instance == null)
            
{
                
lock (_lockHelper)
                
{
                    
if (_instance == null)
                    
{
                        _instance 
= new Singleton();
                    }

                }

            }

            
return _instance;
        }

    }


    
public void DoSomething()
    
{
        Console.WriteLine(
"做些事情");
    }

}

小菜并没有做多少事,只是把private static Singleton _instance;
修改为private static volatile Singleton _instance;
volatile关键字有这么大魔力?
编译器保证对此_instance的读写操作都不会被优化.
这一种也是推荐的作法.

接下来我们来讲个集万千宠爱与一生的单件模式
)第六个单件模式

public sealed class Singleton
{
    
private Singleton()
    
{ }

    
public static Singleton Instance
    
{
        
get
        
{
            
return Nested.instance;
        }

    }


    
public void DoSomething()
    
{
        Console.WriteLine(
"做些事情");
    }


    
//嵌套类
    private class Nested
    
{
        
internal static readonly Singleton instance;

        
private Nested()
        
{ }

        
static Nested()
        
{
            instance 
= new Singleton();
        }

    }

}

return Nested.instance保证了原子性.
第一次执行它时
1.Netsted的静态数据成员Singleton instance分配内存空间,存储空间的值为null
2.执行instance的静态初始语句,由于没有所以跳过
3.执行静态构造函数,执行 instance = new Singleton() 初始化instance
4.返回instance对象

很明显用到的知识还是前面的对象构造顺序,可见有多重要.
这一种也是强烈推荐的,不过有些朋友认为它不易理解,因人而异吧,三种推荐应该有一种你喜欢的. :)

到这里单件模式的多种实现都介绍完了.
小菜得出了四字真言: 不容易啊.