LUKIYA'S NEVERLAND

春去秋来,花谢花开。

[收集]单例模式(Singleton)深入研究(一):从静态类和静态构造器说起

10/31/2007 05:34:21 PM Views: 738 Pictures: 0

静态类和静态构造器(或叫静态构造函数)是深入理解单例模式的基础知识
一、静态类
C# 2.0 提供了静态类,在1.x中我们要实现静态类需要使用下面的代码。

1.0
public sealed class Class1
{
  private Class1(){}
}


在C# 2.0中我们可以使用static class来更加优雅地解决这个问题。
public static class Class1
{
}


我们反编译一下,会了解其实现机制。

.class public abstract auto ansi sealed beforefieldinit Program
 extends object
{
}


原来编译器将该类声明为 abstract sealed,自然不能被继承被实例化了。
从这段反编译的代码我们可以获知一个很重要的信息:static class机制只是编译时的处理,CLR的
运行时机制并没有因为静态类的出现而发生改变。
但是C#编译器并不允许我们在代码中直接声明一个abstract sealed类型,下面的代码无法通过编译。

public abstract sealed class Class1
{
}


静态类的限制

1. 静态类不能有实例构造器。
2. 静态类不能有任何实例成员。
3. 静态类上不能使用abstract和sealed修饰符。
4. 静态类默认继承自System.Object,不能显式指定任何其他基类。
5. 静态类不能指定任何接口实现。
6. 静态类的成员不能有protected或者protected internal访问保护修饰符。 
以下是MSDN上对静态类的描述:    
当将类声明为static时,以指示它仅包含静态成员。不能使用 new 关键字创建静态类的实例。静态类在加载包含该类的程序或命名空间时由 .NET Framework 公共语言运行库 (CLR) 自动加载。
使用静态类来包含不与特定对象关联的方法。例如,创建一组不操作实例数据并且不与代码中的特定对象关联的方法是很常见的要求。您应该使用静态类来包含那些方法。
静态类的主要功能如下:

它们仅包含静态成员。

它们不能被实例化。

它们是密封的。

它们不能包含实例构造函数
关于静态类的更具体描述,请参考
http://msdn2.microsoft.com/zh-cn/library/79b3xss3(VS.80).aspx
 http://www.codeproject.com/useritems/C__20_static_class.asp
    我们有些时候需要将一堆静态方法放在一个类中,而这个类又没有任何实例成员,比如 System.Web.HttpUtility 和.Net框架中的Math库就是这样的例子,由于没有任何实例成员,继承或者实例化都没有任何意义,因此静态类还是有用的。
 
二、静态构造器(或叫静态构造函数):
静态构造函数,可以在C#中用于初始化类数据,其方式与用于初始化实例数据的实例构造函数一样。静态构造函数与实例构造函数在使用规则上面有一些区别。与实例构造函数不一样,静态构造函数不能重载,所以可用的静态构造函数只有一个默认的无参静态构造函数(也不能添加private和public这样的关键字)。静态构造函数也不能显式的调用,不能在派生类中继承,但是在创建基类类型时可以调用。
C#在使用静态构造函数时的几个原则:
1.静态构造函数在创建类的实例之前调用,因此在所有实例构造函数之前调用。
2.静态构造函数在创建类的第一个实例之前调用。
3.静态构造函数在引用静态字段之前调用。
4.静态构造函数只能用于对静态字段的初始化。              
5.添加static关键字,不能添加访问修饰符,因为静态构造函数都是私有的。        
6.类的静态构造函数在给定应用程序域(AppDomain)中至多执行一次:只有创建类的实例或者引用类的任何静态成员才激发静态构造函数
7.静态构造函数是不可继承的,而且不能被直接调用。            
8.如果类中包含用来开始执行的 Main 方法,则该类的静态构造函数将在调用 Main 方法之前执行。    
9.任何带有初始值设定项的静态字段,则在执行该类的静态构造函数时,先要按照文本顺序执行那些初始值设定项。  


      class Test
    {
        static Test()
        {
            Console.WriteLine("a");
        }

        public Test()
        {
            Console.WriteLine("b");
        }
    }
    class Test1 : Test
    {
        public Test1()
        {
            Console.WriteLine("c");
        }
    }
实例化的时候

    Test t = new Test();  //首先调用Test的静态构造器输出a,然后调用Test的实例构造器输出b
    Test t1 = new Test();//调用Test的实例构造器输出b
    Test1 t2 = new Test1();//调用Test的实例构造器输出b,然后调用Test1的实例构造器输出c
因此最后的输出为:a,b,b,b,c
静态构造函数只调用了一次。
再来看一个示例:
在此示例中,类 Bus 有一个静态构造函数和一个静态成员 Drive()。当调用 Drive() 时,将调用静态构造函数来初始化类。
C#
 复制代码
public class Bus
{
    // Static constructor:
    static Bus()
    {
        System.Console.WriteLine("The static constructor invoked.");
    }

    public static void Drive()
    {
        System.Console.WriteLine("The Drive method invoked.");
    }
}

class TestBus
{
    static void Main()
    {
        Bus.Drive();
    }
}
输出的结果如下:
The static constructor invoked.

The Drive method invoked.
要记住一点,静态构造器总是在实例构造器之前被调用。(静态类中可以存在静态构造器,但不允许存在实例构造器)
再来看一个例子:
/**************************************************
 *            静 态 构 造 函 数 练 习
 * (1)①②③……为执行顺序
 * (2)输出结果: static A()
 *                 static B()
 *                 X = 1, Y = 2
 ***************************************************/
    class A
    {
        public static int X;
        static A()           //④ 执行完后返回到③
        {
            X = B.Y + 1;
            Console.WriteLine("static A()");
        }
    }
    class B
    {
        public static int Y = A.X + 1;      //③ 调用了A的静态成员,
        //   转到A的静态构造函数---->
        static B()           //② 如果带有初始值设定项的静态字段,
        //   执行该类的静态构造函数时,
        //   先要按照文本顺序执行那些初始值设定项。
        //   转到初始值设定项---->
        {
            Console.WriteLine("static B()");
        }
        static void Main()         //① 程序入口,
        //   如果类中包含用来开始执行的 Main 方法,
        //   该类的静态构造函数将在调用 Main 方法之前执行。
        //   转到B的静态构造函数---->
        {
            Console.WriteLine("X = {0}, Y = {1}", A.X, B.Y);//⑤ 输出结果
            Console.ReadLine();
        }
    }
关于静态构造器更详细的描述,请参考:
http://msdn2.microsoft.com/zh-cn/library/k9x6w0hc(VS.80).aspx
http://www.yaosansi.com/blog/article.asp id=730
 
 三、前面介绍了静态类和静态构造器的基本知识,下面我们来理解一些和单例模式相关的知识
1. BeforeFieldInit标记
静态构造器(static constructors)与类型初始化器(type initializers)的关系
静态构造器的定义:
The C# specification (ECMA 334) states in section 17.11:

The static constructor for a class executes at most once in a given application domain. The execution of a static constructor is triggered by the first of the following events to occur within an application domain:
An instance of the class is created.
Any of the static members of the class are referenced.

也就是说当类被实例化或者类的静态成员被引用的时候静态构造器被调用。在同一个AppDomain中,某个类的静态构造器最多被调用一次。
再来看类型初始化器的定义:
The CLI specification (ECMA 335) states in section 8.9.5:
A type may have a type-initializer method, or not.
A type may be specified as having a relaxed semantic for its type-initializer method (for convenience below, we call this relaxed semantic BeforeFieldInit)
If marked BeforeFieldInit then the type's initializer method is executed at, or sometime before, first access to any static field defined for that type
If not marked BeforeFieldInit then that type's initializer method is executed at (i.e., is triggered by):
first access to any static or instance field of that type, or
first invocation of any static, instance or virtual method of that type

也就是说系统会有一种机制决定什么时候类型初始化器被调用(由系统自身维护的一个BeforeFieldInit来决定)
当BeforeFieldInit被标记时,类型初始化器将在首次访问这个类型的静态字段时或者之前被调用(也就是说类型初始化器被调用的时间不确定,你只能确定它调用的时间<=你首次访问类型的静态字段的时间)
当BeforeFieldInit未被标记时,类型初始化器将在首次访问该类型的成员时被调用(不论是静态成员还是实例成员)。
现在让我们来进入问题的关键:C#的定义中指出当只有在一个类型不具备静态构造器时它的BeforeFieldInit才会被自动标记上。事实上,这是由编译器帮我们完成的,它可能会导致一些我们意想不到的效果。
最后我要再次强调,静态构造器并不等同于类型初始化器。任何类型都有类型初始化器,但不一定有静态构造器
静态构造器是什么我们已经知道了,但类型初始化器到底是什么呢?
类型初始化器实际上负责帮我们初始化静态字段。假设我们定义了这样一条字段,static object o = new object();如果该字段所在的类没有静态构造器,那么o的初始化就是类型初始化器帮我们完成的,反之o的初始化由静态构造器负责。
是不是觉得很难理解呢?那我们就来看一个具体的例子吧:
class Test //没有静态构造器,BeforeFieldInit被标记
{
    static object o = new object();
}


class Test
{
    static object o;

    static Test() //有静态构造器,BeforeFieldInit未被标记
    {
        o = new object();
    }
}
 

你认为上面的两个类是等价的吗? 

实际上以上两个类并不等价。由于第一个类没有静态构造器,因此它的BeforeFieldInit被标记了,而第二个类由于有静态构造器它的BeforeFieldInit没有被标记。因此它们的类型初始化器的调用时间并不相同。具体的调用时间请参考前面的描述。
再来看一个类
class Test
{
    static object o = new object();

    static Test()
    {
    }
}
    这个类和上面的第二个类是等价的
 

    为什么我们需要关注BeforeFieldInit是否被标记呢?以第三个类为例。当我们为Test添加一个空的静态构造器,它的BeforeFieldInit会被设置为未标记。这样我们就可以保证o直到该类型的字段第一次被访问或方法第一次被调用时才会被初始化,这样就可以实现惰性加载了。
   如果我们不添加这样一个空的静态构造器,我们就无法知道o什么时候被初始化(只能确定它在该类型的字段第一次被访问之前)