c#類的成員初始化順序

iDotNetSpace發表於2009-01-16

C#作為一種純物件導向的話言,為它編寫的整個程式碼裡面到處都離不開物件。一個物件的完整的生命週期是從開始分配空間到初始化,到使用,最後是銷燬,使用的資源被回收。要想真正寫出面高質量的程式碼,我們就得對這期間每一個階段是怎麼樣一個狀態,framework都做了些什麼,我們又能夠做些什麼都要有些瞭解才行。
  一般來說大部分程式設計師對於一個建立好了的物件怎麼使用都是比較清楚的,所以本文也就不想就這一部分做太多的說明,重點就集中開物件的建立和銷燬這兩個階段,這也是程式設計師最容易範錯誤的階斷。本文首先來講一講物件成員的初始化,至於物件的釋放和銷燬,我想放到另外一篇文章裡去講。雖然本文是以C#2005 為例的,但推而廣之,對於其它的基於CLS規範的語言應該也是一樣的。

首先我們來看看引用型別的成員初始化過程

  我們來看一個例子吧 

class Program
{
    
static void Main(string[] args)
    {
         DriveB d 
= new DriveB();
     }
}

class BaseA
{
    
static DisplayClass a = new DisplayClass("基類靜態成員初始化");

     DisplayClass BaseA_c 
= new DisplayClass("基類例項變數BaseA_c初始化");

    
public BaseA()
    {
         Console.WriteLine(
"基類構造方法被呼叫");
     }
}

class DriveB : BaseA
{
    
static DisplayClass DriveB_b = new DisplayClass("繼承類靜態成員DriveB_b初始化");

    
//static BaseA DriveB_a = new BaseA();

     DisplayClass DriveB_c 
= new DisplayClass("繼承類例項變數DriveB_c初始化");

   
public DriveB()
    {
         Console.WriteLine(
"繼承類構造方法被呼叫");
     }
}
class DisplayClass
{
    
public DisplayClass(string diplayString)
    {
         Console.WriteLine(diplayString);
         Console.WriteLine();
     }
}


程式動行的結果是:
繼承類靜態成員DriveB_b初始化
繼承類例項變數DriveB_c初始化
基類靜態成員初始化
基類例項變數BaseA_c初始化
基類構造方法被呼叫
繼承類構造方法被呼叫

得出初始化順序結論: 

1)繼承類靜態成員變數初始化 
2)繼承類例項變數初始化 
3)基類靜態靜態成員變數初始化 
4)基類例項變數初始化 
5)基類構造方法呼叫 
6)繼承類構造方法呼叫。 

  好像結果和JAVA的有點不一樣啊, 有點混亂的感覺,搞不懂M$為什麼要讓初始化按這樣的順序執行,像JAVA那樣嚴格的從基類到派生類多好呀.上例的執行結果說明, 建構函式這麼這個和我們通常思路執行的順序還是有一定的差別.對於例項成員初始化,基本上就是以下步驟執行:
1 類的物件初始化大體順序上例項成員賦值到建構函式
2 成員賦值初始化按照由子類到父類的順序
3 建構函式的初始化按照由父類到子類的順序
從這裡我們有一點需要注意的是,因為成員賦值初始化是從子類到父類的,所以在子類的成員賦值初始化的過程中,不要引用父類定義的成員,因為這個時候父類成員還沒有開始初始化.需要說明一點的是C#在建立物件的第一步分配記憶體完成後會動把所有例項成員變數初始化成變數的預設值,例如整型就是0,引用型別就是null.然後才開始進行成員變數初始化的過程.C#並沒有提供類似於C++建構函式中成員特殊的初始化方式:
public constructor(int a)i_a(a){}
估計是因為分配記憶體和初始化的嚴格分離,以及反射建立物件的需要,而且也不像C++那樣追求的是extreme效率的原因吧;而且就像是以前看到有人說過,再好的語法級別的優化都不能改變寫得爛的程式碼帶來的效率低下.

  我們知道,C#裡面的靜態成員初始化不同於C++的靜態成員初始化.C#裡的靜態成員只會在必要的時候,確切的說是在第一次訪問該類的時候才會進行靜態成員的初始化.這樣做也是有一定道理的,一是減少了記憶體的開銷,再就是加快了程式集啟動的時間,很難想像多一個比較費時的靜態初始化在程式啟動的時候就一一進行,那樣的等待會是比較痛苦的.而且大部分時間我們都只是使用一個程式集裡面很少的一部分類,如果把程式集裡面所有的類不管三七二十一都預先進行初始化的話,對記憶體和時間的浪廢還是比較大的.

  瞭解了靜態成員初始化的時機,就引出了另外一個問題,如果兩個類相互間引用,比如A類的靜態初始化裡引用到了B類,B類的靜態
初始化裡又引用到了A類,這個時候又會出現什麼樣的結果呢,還是用例子還說明吧,請看下面這段程式碼:

using System;
 
class A
{
      
public static int X;
      
static A(){
         X
=B.Y+1;
      }
}
class B
{
      
public static int Y=A.X+1;
      
static B(){}
      
static void Main(){
              Console.WriteLine(
"X={0},Y={1}",A.X,B.Y);
      }
}


產生的輸出結果是什麼?

一般來說靜態宣告賦值語句先於靜態建構函式執行,沒有賦值的類成員宣告會被初始化成該型別的預設值,也就是說
public static int X;
public static int Y=A.X+1;
比各自所在的靜態建構函式先執行,前一句X沒有賦值,預設就是0,後一句的Y在沒有賦值之前也是0,賦值後就是A.X+1的值。
類的靜態初始化包括成員變數的宣告賦值,靜態建構函式的執行。
靜態初始化只有在類第一次被訪問的時候才執行,而且是優先於第一次訪問該類的程式碼執行

因為Main函式在class B中,所以程式先執行的是上面的第二條語句,宣告一個Y,再給Y賦值
在賦值的時候又用到了A類中的X靜態,當第一次訪問A.X的時候,會先呼叫A類的靜態建構函式,這裡執行賦值X=B.Y+1,而重新去訪問B類的成員,因為前面說的靜態初始化只有第一次被訪問的時候會執行,所以再次訪問B類的時候不會重複進行靜態初始化的。這時會因為前一次初始化還未完成,特別是B.Y還沒有賦值完成,所以根據上面說的,B.Y現在處理只是宣告完成的狀態,所以現在B.Y的值就是0,相應的得到的X的值就是1了,在A類的靜態建構函式執行完成的時候,程式會再回到B中Y的賦值語句上來,這時候得到的A.X的值就是1,而Y賦值完成後,此時值就變成了2了
因此最終輸出的結果就是X=1,Y=2

  對於引用型別成員的初始化說了這麼多還是總結一下吧.C#中初始化變數(包括例項成員變數和靜態成員變數)可以採用成員宣告的地方賦值的方式,也可以採用建構函式的方式.我個人在使用例項物件的時候比較推薦採用建構函式的方式,因為建構函式賦值的方式執行的順序是從父類到子類,這種順序避免了子類成員變數的初始化過程引用了未賦值的父類成員變數.而且在建構函式中初始化變數可以採用更多的語句塊,更多的判斷邏輯來初始化,甚至可以加上結構化異常處理try{}catch{}來處理異常資訊,遠比單單一個賦值語句來得靈活.不過對於簡單的內建基本型別(如int,Enum,string等)就無所謂在哪裡進行初始化了.

  以上是引用型別的初始化過程,值型別(這裡主要是指的結構型別)的靜態初始化和引用型別的完全一致.C#的結構型別是有建構函式的(記得C++裡面結構也貌似可以宣告建構函式),而例項成員的初始化因為結構沒有派生的功能,所以在這方面反而比較簡單.但是因為值型別始終是不能為空的,一旦宣告就必須要分配相應的記憶體空間,有了記憶體空間當然是要首先進行初始化的了,這都是為了保證值型別的有效性吧.這個過程是由Framework來完成的,我們自己是沒有辦法寫程式碼來控制.因此Framework自己在初始化呼叫建構函式的時候當然就需要對自己要呼叫的建構函式的引數作個統一的約定,最簡單的就是無參建構函式了.所以在C#的每個結構裡都預設隱含了一個無參的建構函式,程式設計師自己可以過載建構函式,但是不能宣告自己的無參建構函式(這個是被Framework佔用了的).

  有很多剛從C++轉到C#的程式設計師在使用引用型別作為函式的臨時變數的時候還能認識到在使用之前需要new一下建立例項再使用,但是在使用結構作為函式的臨時變數的時候就喜歡宣告後直接拿來使用,問起他們的時候總是說結構是值型別,值型別是存在棧上的,宣告後就直接可以使用了.先不論這句話是不是正確的(關於C#中值型別和引用型別到底存在什麼地方有時間以後一定寫一篇文章專門討論一下).首先按C#程式設計規範值型別同樣是需要進行成員變數的封裝的,很多值型別在宣告後就不能夠改變,而只宣告一個結構體不賦值的話相當於是呼叫的預設的建構函式,而通常這個預設的建構函式對於我們來說是沒有什麼意義的.所以得到的值也是沒有太大的用處,除非你是想用作out引數所實參,真正用到的時候還得另外賦值.所以當你這樣使用結構體的時候,C#編譯器會警告你,這個變數只是宣告瞭沒有賦值(其實是相當於有一個值,但是沒有意義).其實變數使用之前賦值這也是一個很好的習慣,C++裡面雖然直接宣告瞭就可以用,但是一般也會在使用之前先ZeroMemory一下,這其實也是相當於初始化了結構體吧,唯一的區別是不需要重新分配空間.

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-539837/,如需轉載,請註明出處,否則將追究法律責任。

相關文章