程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#類的成員初始化順序

C#類的成員初始化順序

編輯:關於C#

C#作為一種純面向對象的話言,為它編寫的整個代碼裡面到處都離不開對象。一個對象的完整的生命周期是從開始分配空間到初始化,到使用,最後是銷毀,使用的資源被回收。要想真正寫出面高質量的代碼,我們就得對這期間每一個階段是怎麼樣一個狀態,framework都做了些什麼,我們又能夠做些什麼都要有些了解才行。

一般來說大部分程序員對於一個創建好了的對象怎麼使用都是比較清楚的,所以本文也就不想就這一部分做太多的說明,重點就集中開對象的創建和銷毀這兩個階段,這也是程序員最容易范錯誤的階斷。本文首先來講一講對象成員的初始化,至於對象的釋放和銷毀,我想放到另外一篇文章裡去講。雖然本文是以C#2005 為例的,但推而廣之,對於其它的基於CLS規范的語言應該也是一樣的。

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

我們來看一個例子吧

1class Program
2{
3 static void Main(string[] args)
4 {
5 DriveB d = new DriveB();
6 }
7}
8
9class BaseA
10{
11 static DisplayClass a = new DisplayClass("基類靜態成員初始化");
12
13 DisplayClass BaseA_c = new DisplayClass("基類實例變量BaseA_c初始化");
14
15 public BaseA()
16 {
17 Console.WriteLine("基類構造方法被調用");
18 }
19}
20
21class DriveB : BaseA
22{
23 static DisplayClass DriveB_b = new DisplayClass("繼承類靜態成員DriveB_b初始化");
24
25 //static BaseA DriveB_a = new BaseA();
26
27 DisplayClass DriveB_c = new DisplayClass("繼承類實例變量DriveB_c初始化");
28
29 public DriveB()
30 {
31 Console.WriteLine("繼承類構造方法被調用");
32 }
33}
34class DisplayClass
35{
36 public DisplayClass(string diplayString)
37 {
38 Console.WriteLine(diplayString);
39 Console.WriteLine();
40 }
41}

程序動行的結果是:

繼承類靜態成員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類,這個時候又會出現什麼樣的結果呢,還是用例子還說明吧,請看下面這段代碼:

1using System;
2 class A
3 {
4 public static int X;
5 static A(){
6 X=B.Y+1;
7 }
8 }
9 class B
10 {
11 public static int Y=A.X+1;
12 static B(){}
13 static void Main(){
14 Console.WriteLine("X={0},Y={1}",A.X,B.Y);
15 }

16 }

產生的輸出結果是什麼?

一般來說靜態聲明賦值語句先於靜態構造函數執行,沒有賦值的類成員聲明會被初始化成該類型的默認值,也就是說

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved