zoukankan      html  css  js  c++  java
  • 浅谈静态字段与静态构造函数之间的初始化关系以及执行顺序

    偶然看到一道面试题,题目如下:
    view plaincopy to clipboardprint?
     1 public class A
     2     {
     3         public static int X;
     4         static A()
     5         {
     6             X = B.Y + 1;
     7         }
     8     }
     9     public class B
    10     {
    11         public static int Y = A.X + 1;
    12         static B()
    13         { }
    14     }
    15     class Program
    16     {
    17         static void Main(string[] args)
    18         {
    19             Console.WriteLine("X={0},Y={1}", A.X, B.Y);
    20             Console.ReadLine();
    21         }
    22     }
     
     要求写出结果。
        当然,因为我不是在面试的情况下遇到这题,所以直接在电脑上运行了,结果是:。回过头来想想,当执行Console.WriteLine("X={0},Y={1}", A.X, B.Y)一句之时,求取A.X的顺序在前,因此程序首先进入到A的静态构造函数中去计算X的值,而X的值依赖于B的静态成员Y,所以程序应该跳转到B中去求取Y的值,而Y=A.X+1,此时不会再一次进入A中去求取X的值,而是使用整型变量的默认值0,这样计算出来Y的值等于1,返回到A的构造函数中继续计算,得到X的结果为2,符合运行之后得到的结果。为证明我的想法,我在程序中设置断点并单步执行,执行的顺序和我想象一致。
        至此,如我一般的菜鸟认为已经没什么问题了。正巧碰到一朋友,此人乃一高手,我将程序发与他,并告知自己所想。朋友看后,很快给我发会一份代码并让我猜测结果,代码如下:
    view plaincopy to clipboardprint?

     1 public class A
     2     {
     3         public static int X;
     4         static A()
     5         {
     6             X = B.Y + 1;
     7         }
     8     }
     9
    10
    11     public class B
    12     {
    13         public static int Y = A.X+1;
    14         B()
    15         { }
    16     }
    17
    18
    19     class Program
    20     {
    21         static void Main(string[] args)
    22         {
    23             Console.WriteLine("X={0},Y={1}", A.X, B.Y);
    24             Console.ReadLine();
    25         }
    26     }
    27
    28
     
     我大致一看,似乎还是我发过去的程序,一问之下,原来B的构造函数不再是静态了。B的构造函数体内没有任何代码,我想当然的认为结果还是和原来一样,但运行之后让我大吃一惊,结果居然成了。一个没有任何代码的空构造函数,只是由静态改为非静态,居然会得到完全不同的结果,这真是让我意想不到。看来,在程序的执行过程之上,有一些东西是我所不了解的。还好我懂得“谦受益,满招损”的道理,于是向朋友求教。在朋友的一步步指导之下,终于明白了其中的缘由,整理记录以备遗忘之时查询,也希望如我一般的菜鸟能够从中多少受益。
        先考虑最简单的情况,只有Main函数所在的那一个类。我们都知道,对于控制台应用程序,Main函数是入口函数,那么,在程序进入到Main函数之前都发生了些什么呢?我们通过代码来看一看:

    view plaincopy to clipboardprint?
    1 class Program
     2     {
     3         public static int num1;
     4         public static int num2 = 1;
     5         public static int num3;
     6         static void Main(string[] args)
     7         {
     8             Console.WriteLine(num2);
     9             Console.WriteLine(A.num4);
    10             Console.ReadLine();
    11         }
    12         static Program()
    13         {
    14             Console.WriteLine(num1);
    15             num3++;
    16             Console.WriteLine(num3);
    17         }
    18     }

     我在第3,7,13行都设置了断点。在进入到Main函数之前,编译器会先检查所有的静态字段并给予默认值。我们运行程序,可以看到执行到 num2=1的时候进入到断点。此时num1,num2,num3都被赋予了默认的值0,单步执行,进入到静态构造函数,接下来是主函数。
        我们接着考虑有两个类的情况,还是先看代码:

    view plaincopy to clipboardprint?

    1 class Program
     2     {
     3         public static int num1;
     4         public static int num2 = 1;
     5         public static int num3;
     6         static void Main(string[] args)
     7         {
     8             Console.WriteLine(num2);
     9             Console.WriteLine(A.num4);//注意在这里引用到了类A的静态成员
    10             Console.ReadLine();
    11         }
    12         static Program()
    13         {
    14             Console.WriteLine(num1);
    15             num3++;
    16             Console.WriteLine(num3);
    17         }
    18     }
    19
    20     class A
    21     {
    22         public static int num4 = 1;
    23         A()//注意这里是非静态的构造函数
    24         {
    25         }
    26     }
     
    通过单步执行,可以看到首先是Program类的静态字段被赋值,然后进入到Program类的静态构造函数,执行完构造函数,程序并不是进入Main函数,而是先进入到类A,对静态字段赋值,然后才会进入到Main函数之中。至此,我们似乎可以得出结论,在进入到Main函数之前,类的静态字段会先被赋值,并且Main函数所在类的静态字段会先于类A被赋值。那么,再增加一个类会怎样呢?我们看代码:

    view plaincopy to clipboardprint?
    1 class Program
     2     {
     3         public static int num1;
     4         public static int num2 = 1;
     5         public static int num3;
     6         static void Main(string[] args)
     7         {
     8             Console.WriteLine(num2);
     9             Console.WriteLine(A.num4);//注意这里只引用了类A的成员
    10             Console.ReadLine();
    11         }
    12         static Program()
    13         {
    14             Console.WriteLine(num1);
    15             num3++;
    16             Console.WriteLine(num3);
    17         }
    18     }
    19
    20     class A
    21     {
    22         public static int num4 = 1;
    23         A()
    24         {
    25         }
    26     }
    27
    28     class B
    29     {
    30         public static int num5 = 1;
    31         B(){}//注意这里是非静态的构造函数
    32     }
     
    通过单步执行,我们可以看到在执行了Program的静态构造函数之后,进入到类A里边对A的静态成员进行赋值,然后直接进入到Main函数,类B中的代码并没有得到执行的机会。从编译器对代码进行优化的角度来看,这很合理,类B并不需要执行,因此单步执行也无法进入到类B中。通过在Main函数中增加 Console.WriteLine(B.num5),我们可以通过单步执行进入到类B中;通过在Main函数中调节引用A.num4和B.num5的顺序,我们能够看到先引用哪个类的成员,在单步执行中就会先进入到哪个类中。据此我们可以得出结论:在被引用到类的静态成员按引用的先后顺序初始化之后,程序才进入到Main函数中。
         在此之前并没有什么让人疑惑之处。接下来我们把上边的代码稍微修改下:
    view plaincopy to clipboardprint?
    class Program  
        {  
            public static int num1;  
            public static int num2 = 1;  
            public static int num3;  
            static void Main(string[] args)  
            {  
                Console.WriteLine(num2);  
                Console.WriteLine(B.num5);//这里引用了类B的静态成员  
                Console.ReadLine();  
            }  
            static Program()  
            {  
                Console.WriteLine(num1);  
                num3++;  
                Console.WriteLine(num3);  
            }  
        }  
     
        class A  
        {  
            public static int num4 = 1;  
            A()//注意这里是非静态的构造函数  
            {  
            }  
        }  
     
        class B  
        {  
            public static int num5 = A.num4+1;//类B中引用了类A的静态成员  
            B(){}//注意这里是非静态的构造函数  
        } 


    我们根据先前的代码执行过程知道在进入Main函数之前会对引用到的类的静态成员进行初始化。现在我们看到,Main函数引用到的是类B的成员。根据经验,我们可能会认为,程序会先执行到类B里边,在对B的静态成员求值的时候再进入到类A里边。事情是否真的是这样呢?目前只考虑了非静态的构造函数,如果是静态的构造函数,又会得出怎样的结果呢?有心人可以自己进行实际的测试,我也会在下篇给出自己测试所得到的结果以及自己的一点看法。

    在上篇中留下了一个问题,想来有心的读者已经自行解决问题并且得出了自己的结论。事实上,程序并不是象通常的函数调用,进进入主调函数,然后进入被调函数。在这里,通过单步执行,可以看到程序先进入到类A中进行静态成员的初始化,然后进入到类B中进行B的静态成员的赋值,最后才进入Main函数。我们可以猜测,编译器根据Main函数中所用到的类的先后顺序对类的静态成员进行初始化。在我们的例子中,编译器检查到引用了类B的静态成员,接着继续检查该成员又用到了类A的静态成员,经过优化,编译器先初始化了类A的静态成员,然后是类B,然后回到主函数继续来执行,这符合我们看到的单步执行的顺序。
        然而,在有构造函数的情况下,事情又非如此。还是刚才的例子,我们把类A的构造函数改为静态,代码如下:

    1.    class  A   
    2.      {   
    3.           public   static   int  num4 = 1;   
    4.           static  A() //注意这里是静态的构造函数   
    5.           {   
    6.           }   
    7.       }   
    8.      
    9.         class  B   
    10.       {   
    11.           public   static   int  num5 = A.num4+1; //类B中引用了类A的静态成员   
    12.           B(){} //注意这里是非静态的构造函数   
    13.      }  

    通过单步执行,我们可以看到,程序先进入到了类B,在对B的静态成员求值的时候才进入了类A,最后进入Main函数。现在,我们再做一点改动,把类B的构造函数也改为静态的:

    1.    class  Program   
    2.       {   
    3.            public   static   int  num1;   
    4.            public   static   int  num2 = 1;   
    5.            public   static   int  num3;   
    6.            static   void  Main( string [] args)   
    7.           {   
    8.               Console.WriteLine(num2);   
    9.               Console.WriteLine(B.num5); //这里引用了类B的静态成员   
    10.              Console.ReadLine();   
    11.          }   
    12.           static  Program()   
    13.          {   
    14.              Console.WriteLine(num1);   
    15.              num3++;   
    16.              Console.WriteLine(num3);   
    17.          }   
    18.      }   
    19.    
    20.       class  A   
    21.      {   
    22.           public   static   int  num4 = 1;   
    23.           static  A() //注意这里是静态的构造函数   
    24.          {   
    25.          }   
    26.      }   
    27.     
    28.       class  B   
    29.      {   
    30.           public   static   int  num5 = A.num4+1; //类B中引用了类A的静态成员   
    31.           static  B(){} //注意这里改为了静态的构造函数   
    32.      }  

    通过单步执行,我们可以看到会先进入到Main函数中,然后进入到类B,然后是类A。
        现在,我们可以做一个最终猜测了:编译器在编译的时候,会事先分析所需要的静态字段,如果这些静态字段所在的类有静态的构造函数,则忽略字段的初始化,否则先进行静态字段的初始化。对类的静态成员初始化的顺序取决于在Main函数中的引用顺序,先引用到的先进行初始化,但如果类的静态成员的初始化依赖于其它类的静态成员,则会先初始化被依赖类的静态成员。而带有静态构造函数的类的静态字段,只有在引用到的时候才进行初始化。
        回过头来考虑最初令人有些迷惑的代码,可以发现已经不再难以理解。第一段代码中A和B都是具有静态构造函数的类,所以是从Main函数中引用到类A的时候开始进入到类A进行初始化,在初始化的过程中又用到了B的静态成员,然后进入到B中。对于第二段代码,由于B是非静态的构造函数,所以会在主函数执行前被初始化,同样由于初始化的过程中用到了类A的静态成员,然后跳转到类A。这样就造成了代码一和代码二运行之后得到不同的结果。

        附:文中所用代码仅为示例之用,并不符合良好代码的规范和要求。尤其使用未经赋值的字段是一个非常不好的编程习惯,请阅读此文的人予以注意。

  • 相关阅读:
    Session丢失问题解决方案
    SQL 将txt文件导入sql数据库
    Ajax的应用实例解析
    ViewState与Session
    .net动态创建DataTable
    TSQL查询进阶数据集之间的运算
    【朝夕Net社区技术专刊】Core3.1 WebApi集群实战专题WebApi环境搭建运行发布部署篇
    经典SQL语句大全
    “存储过程”的定义及优点
    搜索关键词
  • 原文地址:https://www.cnblogs.com/cpcpc/p/2123135.html
Copyright © 2011-2022 走看看