C#類的成員初始化順序

來源:互聯網
上載者:User

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一下,這其實也是相當於初始化了結構體吧,唯一的區別是不需要重新分配空間.

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.