C#銳利體驗-第五講 構造器與析構器)

來源:互聯網
上載者:User
第五講 構造器與析構器

南京郵電學院 李建忠(cornyfield@263.net)

索引


C#銳利體驗

"Hello,World!"程式

C#語言基礎介紹

Microsoft.NET平台基礎構造

類與對象

構造器與析構器

方法

域與屬性

索引器與操作符重載

數組與字串

特徵與映射

COM互操作 非託管編程與異常處理

用C#編織未來--C#編程模型概述

構造器

構造器負責類中成員變數(域)的初始化。C#的類有兩種構造器:執行個體構造器和靜態構造器。執行個體構造器負責初始化類中的執行個體變數,它只有在使用者用new關鍵字為對象分配記憶體時才被調用。而且作為參考型別的類,其執行個體化後的對象必然是分配在託管堆(Managed Heap)上。這裡的託管的意思是指該記憶體受.NET的CLR運行時管理。和C++不同的是,C#中的對象不可以分配在棧中,使用者只聲明對象是不會產生構造器調用的。

執行個體構造器分為預設構造器和非預設構造器。預設構造器是在一個類沒有聲明任何構造器的情況下,編譯器強製為該類添加的一個無參數的構造器,該構造器僅僅調用父類的無參數構造器。預設構造器實際上是C#編譯器為保證每一個類都有至少一個構造器而採取的附加規則。注意這裡的三個要點:

  1. 子類沒有聲明任何構造器;
  2. 編譯器為子類加的預設構造器一定為無參數的構造器;
  3. 父類一定要存在一個無參數的構造器。

看下面例子的輸出:

using System;public class MyClass1{public MyClass1(){Console.WriteLine(“MyClass1Parameterless Contructor!”);}public MyClass1(string param1){Console.WriteLine(“MyClass1Constructor  Parameters : ”+param1);}}public class MyClass2:MyClass1{}public class Test{public static void Main(){MyClass2 myobject1=new MyClass2();}}

編譯器並運行可以得到下面的輸出:

    MyClass1 Parameterless Contructor!

讀者可以去掉MyClass1的無參構造器public MyClass1()看看編譯結果。

構造器在繼承時需要特別的注意,為了保證父類成員變數的正確初始化,子類的任何構造器預設的都必須調用父類的某一構造器,具體調用哪個構造器要看構造器的初始化參數列表。如果沒有初始化參數列表,那麼子類的該構造器就調用父類的無參數構造器;如果有初始化參數列表,那麼子類的該構造器就調用父類對應的參數構造器。看下面例子的輸出:

using System;    public class MyClass1    {    public MyClass1()    {    Console.WriteLine("MyClass1 Parameterless Contructor!");    }    public MyClass1(string param1)    {    Console.WriteLine("MyClass1    Constructor Parameters : "+param1);    }    }    public class MyClass2:MyClass1    {    public MyClass2(string param1):base(param1)    {    Console.WriteLine("MyClass2    Constructor Parameters : "+param1);    }    }    public class Test    {    public static void Main()    {    MyClass2 myobject1=new MyClass2("Hello");    }    }    

編譯器並運行可以得到下面的輸出:

    MyClass1 Constructor Parameters : Hello
    MyClass2 Constructor Parameters : Hello

C#支援變數的聲明初始化。類內的成員變數聲明初始化被編譯器轉換成指派陳述式強加在類的每一個構造器的內部。那麼初始化語句與調用父類構造器的語句的順序是什麼呢?看下面例子的輸出:

using System;        public class MyClass1        {        public MyClass1()        {        Print();        }        public virtual void Print() {}        }        public class MyClass2: MyClass1        {        int x = 1;        int y;        public MyClass2()        {        y = -1;        Print();        }        public override void Print()        {        Console.WriteLine("x = {0}, y = {1}", x, y);        }        }        public class Test        {        static void Main()        {        MyClass2 MyObject1 = new MyClass2();        }        }        

編譯器並運行可以得到下面的輸出:

    x = 1, y = 0
    x = 1, y = -1

容易看到初始化語句在父類構造器調用之前,最後執行的才是本構造器內的語句。也就是說變數初始化的優先權是最高的。

我們看到類的構造器的聲明中有public修飾符,那麼當然也可以有protected/private/ internal修飾符。根據修飾符規則,我們如果將一個類的構造器修飾為private,那麼我們在繼承該類的時候,我們將不能對這個private的構造器進行調用,我們是否就不能對它進行繼承了嗎?正是這樣。實際上這樣的類在我們的類內的成員變數都是靜態(static)時,而又不想讓類的使用者對它進行執行個體化,這時必須屏蔽編譯器為我們暗中添加的構造器(編譯器添加的構造器都為public),就很有必要作一個private的執行個體構造器了。protected/internal也有類似的用法。

類的構造器沒有傳回值,這一點是不言自明的。

靜態構造器初始化類中的靜態變數。靜態構造器不象執行個體構造器那樣在繼承中被隱含調用,也不可以被使用者直接調用。掌握靜態構造器的要點是掌握它的執行時間。靜態構造器的執行並不確定(編譯器沒有明確定義)。但有四個準則需要掌握:

  1. 在一個程式的執行過程中,靜態構造器最多隻執行一次。
  2. 靜態構造器在類的靜態成員初始化之後執行。或者講編譯器會將靜態成員初始化語句轉換成指派陳述式放在靜態構造器執行的最開始。
  3. 靜態構造器在任何類的靜態成員被引用之前執行。
  4. 靜態構造器在任何類的執行個體變數被分配之前執行。

看下面例子的輸出:

using System;            class MyClass1            {            static MyClass1()            {            Console.WriteLine("MyClass1 Static Contructor");            }            public static void Method1()            {            Console.WriteLine("MyClass1.Method1");            }            }            class MyClass2            {            static MyClass2()            {            Console.WriteLine("MyClass2 Static Contructor");            }            public static void Method1()            {            Console.WriteLine("MyClass2.Method1");            }            }            class Test            {            static void Main()            {            MyClass1.Method1();            MyClass2.Method1();            }            }            

編譯器並運行可以得到下面的輸出:

    MyClass1 Static Contructor
    MyClass1.Method1
    MyClass2 Static Contructor
    MyClass2.Method1

當然也可能輸出:

    MyClass1 Static Contructor
    MyClass2 Static Contructor
    MyClass1.Method1
    MyClass2.Method1

值得指出的是執行個體構造器內可以引用執行個體變數,也可引用靜態變數。而靜態構造器內能引用靜態變數。這在類與對象的語義下是很容易理解的。
實際上如果我們能夠深刻地把握類的構造器的唯一目的就是保證類內的成員變數能夠得到正確的初始化,我們對各種C#中形形色色的構造器便有會心的理解--它沒有理由不這樣!

析構器

由於.NET平台的自動垃圾收集機制,C#語言中類的析構器不再如傳統C++那麼必要,析構器不再承擔對象成員的記憶體釋放--自動垃圾收集機制保證記憶體的回收。實際上C#中已根本沒有delete操作!析構器只負責回收處理那些非系統的資源,比較典型的如:開啟的檔案,擷取的視窗控制代碼,資料庫連接,網路連接等等需要使用者自己動手釋放的非記憶體資源。我們看下面例子的輸出:

using System;                    class MyClass1                    {                    ~MyClass1()                    {                    Console.WriteLine("MyClass1's destructor");                    }                    }                    class MyClass2: MyClass1                    {                    ~MyClass2()                    {                    Console.WriteLine("MyClass2's destructor");                    }                    }                    public class Test                    {                    public static void Main()                    {                    MyClass2 MyObject = new MyClass2();                    MyObject = null;                    GC.Collect();                    GC.WaitForPendingFinalizers();                    }                    }                    

編譯器並運行可以得到下面的輸出:

    MyClass2's destructor
    MyClass1's destructor

其中程式中最後兩句是保證類的析構器得到調用。GC.Collect()是強迫通用語言運行時進行啟動垃圾收集線程進行回收工作。而GC.WaitForPendingFinalizers()是掛起目前的線程等待整個終止化(Finalizaion)操作的完成。終止化(Finalizaion)操作保證類的析構器被執行,這在下面會詳細說明。

析構器不會被繼承,也就是說類內必須明確的聲明析構器,該類才存在析構器。使用者實現析構器時,編譯器自動添加調用父類的析構器,這在下面的Finalize方法中會詳細說明。析構器由於垃圾收集機制會被在合適的的時候自動調用,使用者不能自己調用析構器。只有執行個體析構器,而沒有靜態析構器。

那麼析構器是怎麼被自動調用的?這在 .Net記憶體回收機制由一種稱作終止化(Finalizaion)的操作來支援。.Net系統預設的終止化操作不做任何操作,如果使用者需要釋放非受管資源,使用者只要在析構器內實現這樣的操作即可--這也是C#推薦的做法。我們看下面這段代碼:

using System;                        class MyClass1                        {                        ~MyClass1()                        {                        Console.WritleLine("MyClass1 Destructor");                        }                        }                        

而實際上,從產生的中間代碼來看我們可以發現,這些代碼被轉化成了下面的代碼:

using System;                        class MyClass1                        {                        protected override void Finalize()                        {                        try                        {                        Console.WritleLine("My Class1 Destructor");                        }                        finally                        {                        base.Finalize();                        }                        }                        }                        

實際上C#編譯器不允許使用者自己重載或調用Finalize方法--編譯器徹底屏蔽了父類的Finalize方法(由於C#的單根繼承性質,System.Object類是所有類的祖先類,自然每個類都有Finalize方法),好像這樣的方法根本不存在似的。我們看下面的代碼實際上是錯的:

using System;                        class MyClass                        {                        override protected void Finalize() {}// 錯誤                        public void MyMethod()                        {                        this.Finalize();// 錯誤                        }                        }                        

但下面的代碼卻是正確的:

using System;                        class MyClass                        {                        public void Finalize()                        {                        Console.WriteLine("My Class Destructor");                        }                        }                        public class Test                        {                        public static void Main()                        {                        MyClass MyObject=new MyClass();                        MyObject.Finalize();                        }                        }                        

實際上這裡的Finalize方法已經徹底脫離了“終止化操作”的語義,而成為C#語言的一個一般方法了。值得注意的是這也屏蔽了父類System.Object的Finalize方法,所以要格外小心!

終止化操作在.Net運行時裡有很多限制,往往不被推薦實現。當對一個對象實現了終止器(Finalizer)後,運行時便會將這個對象的引用加入一個稱作終止化對象引用集的隊列,作為要求終止化的標誌。當垃圾收集開始時,若一個對象不再被引用但它被加入了終止化對象引用集的隊列,那麼運行時並不立即對此對象進行垃圾收集工作,而是將此對象標誌為要求終止化操作對象。待垃圾收集完成後,終止化線程便會被運行時喚醒執行終止化操作。顯然這之後要從終止化對象引用集的鏈表中將之刪去。而只有到下一次的垃圾收集時,這個對象才開始真正的垃圾收集,該對象的記憶體資源才被真正回收。容易看出來,終止化操作使垃圾收集進行了兩次,這會給系統帶來不小的額外開銷。終止化是通過啟用線程機制來實現的,這有一個安全執行緒的問題。.Net運行時不能保證終止化執行的順序,也就是說如果對象A有一個指向對象B的引用,兩個對象都有終止化操作,但對象A在終止化操作時並不一定有有效對象A引用。.Net運行時不允許使用者在程式運行中直接調用Finalize()方法。如果使用者迫切需要這樣的操作,可以實現IDisposable介面來提供公用的Dispose()方法。需要說明的是提供了Dispose()方法後,依然需要提供Finalize方法的操作,即實現假託的解構函式。因為Dispose()方法並不能保證被調用。所以.Net運行時不推薦對對象進行終止化操作即提供解構函式,只是在有非受管資源如資料庫的串連,檔案的開啟等需要嚴格釋放時,才需要這樣做。

大多數時候,垃圾收集應該交由.Net運行時來控制,但有些時候,可能需要人為地控制一下記憶體回收操作。例如在操作了一次大規模的對象集合後,我們確信不再在這些對象上進行任何的操作了,那我們可以強制記憶體回收立即執行,這通過調用System.GC.Collect() 方法即可實現,但頻繁的收集會顯著地降低系統的效能。還有一種情況,已經將一個對象放到了終止化對象引用集的鏈上了,但如果我們在程式中某些地方已經做了終止化的操作,即明確調用了Dispose()方法,在那之後便可以通過調用System.GC.SupressFinalize()來將對象的引用從終止化對象引用集鏈上摘掉,以忽略終止化操作。終止化操作的系統負擔是很重的。

在深入瞭解了.NET運行時的自動垃圾收集功能後,我們便會領會C#中的析構器為什麼繞了這麼大的彎來實現我們的編程需求,才能把記憶體資源和非記憶體資源的回收做的遊刃有餘--這也正是析構的本原!

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.