C#基礎之基本類型


本絲花了近半年,終於將《CLR Via C#》這本書看完了(請不要BS本人的看書速度T_T),這確實是一本好書,大大們推薦的果然值得一讀。

雖然很多東西還沒有盡得其要,我常想在自己深刻掌握了某個知識點后再總結分享出來(不知道大家是不是這個心理),但現在我覺得應該在一個人成長的過程中就去做這件事情,所以有了本篇不成文的總結,文中知識點大量來自《CLR Via C#》這本書,在此對作者及翻譯者表示感謝!另文中如有錯誤的地方,歡迎大家指出!

術語解釋:

CLR: 公共語言運行時(Common Language Runtime)

FCL:Framework類庫(Framework Class Library)

IL: 中間語言(Intermediate Language)

類型基礎

 CLR要求所有的類型最終都是從System.Object派生的。這符合在面向對象的語言和設計中,一切皆為對象!

從圖中,我們可以看到,顯式從System.Object派生的類ExplicitlyDerivedFromObject和沒有顯式指定其父類的類ImplicitlyDerivedFromObject,最終生成的IL中間都是一致的。如果我們沒有指定一個類的父類,編譯器會自動為我們的類加上System.Object父類。

基元類型

編譯器直接支持的數據類型稱為基元類型(Primitive type),每種基元類型都對應FCL中的某一類型。如下表格所示每種基元類型與對應的FCL類型:

我們也可以簡單理解成,編譯器會自動在我們的每個源碼文件中,加上以下using指令(using在此的作用是給類型起一個別名):

using int = System.Int32;
using string = System.String;
……

理解了這一點,我們應該能知道在我們的代碼中,寫int還是Int32,用string還是用String本質上是一樣的!從下面兩種不同的寫法生成的IL代碼來看,結果也是符合預計的。

我們還應該知道,C#中的int永遠是代表32位整型,long永遠是64位整型等。這一點和其他編程語言可能是不一致的(比如C/C++可能會根據機器平台決定,比如int在16位機器上可能是16位,而在32位機器上可能是32位。對於C/C++本人早已記憶模糊,這里如果有錯誤,請指出!)。

引用類型、值類型

CLR支持兩種類型:引用類型值類型,它們的區別是在內存分配方式上的差異:引用類型是從托管堆上分配的;值類型是在線程棧上分配的。而CLR的垃圾回收是針對托管堆的,因此值類型不受垃圾回收器的控制。

在FCL中,所有稱為“結構”(struct)的類型都是值類型,所有稱為“類”(class)的類型都是引用類型。所有的Struct都直接派生自抽象類System.ValueType,而System.ValueType直接從System.Object派生。所有的枚舉都直接從System.Enum派生,而后者又派生自System.ValueType,所以枚舉也是值類型。由於CLR的單繼承規則,所以我們在定義值類型時,不能指定基類型,但可以實現接口。同時從下圖生成的IL也可以看出,值類型是隱式密封的(sealed),也就是說也不能從值類型派生。

雖然引用類型與值類型實質只是內存分配上的差異,但這種差異會導致兩種類型在行為表現上有着明顯不同,比如下面的例子:

    struct ValType { public int x;}
    class RefType { public int x;}

    class Program
    {
        static void Main(string[] args)
        {
            ValType v1 = new ValType(); //在棧上分配內存
            RefType r1 = new RefType(); //在堆上分配內存

            v1.x = 2;
            r1.x = 2;
            //執行到這里,內存結構請見圖1

            Console.WriteLine(v1.x);   //2
            Console.WriteLine(r1.x);   //2

            ValType v2 = v1;    //在棧上分配內存(v2),並把v1棧的內容復制到v2
            RefType r2 = r1;    //把r1的堆地址復制給r2

            v2.x = 5;   //只改變v2棧的內容
            r2.x = 5;   //由於r2和r1都引用同一個堆上的對象,改變r2也會改變r1
            //執行到這里,內存結構請見圖2

            Console.WriteLine(v1.x);    //2
            Console.WriteLine(r1.x);    //5 注意這里變成了r2修改后的值
            Console.WriteLine(v2.x);    //5
            Console.WriteLine(r2.x);    //5

            Console.ReadKey();
        }
    }

首先我們定義一個一值類型與一個引用類型,內部都只有一個字段。用new操作符分配內存時,值類型v1的內存分配在了線程棧上,引用類型r1的內存分配在了托管堆上,在程序運行到第一次WriteLine輸出時,看到的結果是一致的。但接下來聲明兩個新的對象並執行賦值時,這里的發生的事明顯不同:雖然賦值操作都是拷貝線程棧上變量的內容,但由於值類型變量v1的棧內容就是ValType類型實例本身,而引用類型r1的棧內容是RefType對象實例在堆上的地址。所以賦值后的結果就是,v1和v2各保存了一份ValType類型實例,而r1和r2保存了同一塊堆內存的地址。所以改變r2對象導致了r1對象的隨同改變。下面是內存示意圖:

 

圖1

 

圖2 

雖然值類型實例不需要垃圾回收,但由於值類型在傳遞時,傳遞的是內容本身,所以並不適合將所一些實例較大的類型定義為值類型。實現上除非滿足以下所有條件,否則不應該將一個類型聲明為值類型。

  • 沒有更改其字段的成員,即該類型是不可變的。(建議所有字段為readonly)
  • 類型不需要從其他任何類型繼承。(值類型不能選擇基類)
  • 類型也不會派生出其他任何類型。(所有的值類型都是隱式密封sealed的)
  • 實例較小(約<=16Byte)或較大但不作為方法實參傳遞,也不從方法返回。

值類型的裝箱與拆箱

將值類型轉換成一個引用類型的過程叫裝箱,整個過程看起來是這樣的:

  1. 在托管堆中分配好內存,分配的內存量=值類型的各個字段所需的內存量+所有堆上對象都有的兩個額外成員(類型對象指針和同步塊索引)所需的內存量。
  2. 值類型的字段復制到新分配的內存。
  3. 返回對象的地址。

拆箱僅是獲取一個指針的過程,該指針指向包含在一個對象中的原始值類型(數據字段)。雖然拆箱比裝箱代價低,但實際在拆箱之后往往緊接着就是賦值操作(內存復制)。顯然裝箱和拆箱/復制會對應用程序的速度與內存消耗上產生不利影響,所以應該了解到這一點,並盡量避免裝箱和拆箱操作。那么什么時候會發生裝箱和拆箱,最直觀的方法就是看生成的IL代碼(IL對應指令是分別是box與unbox),比如下面的例子:

示例中ArrayList的Add方法參數是Object類型,也就是說一個引用類型(在堆上分配的內存),當我們傳遞int類型時,這里便會將int實例裝箱,以返回一個堆上的地址。在將array[0]強制轉型為int時,由於值類型int的對象是在線程棧上分配的,所以這里拆箱並緊接着發生賦值(內存復制)操作。同時為了對比,我加了引用類型的reference,可以看出引用類型是不會發生裝箱與拆箱的。

那么如何避免(或減少)裝箱與拆箱:

  • 盡量使用泛型集合。
  • 盡量將裝箱與拆箱操作移到循環體之外。
  • 定義一個方法如果可接收引用類型或值類型時,盡量不要將參數定義為object,可以考慮通過重載定義多個版本或定義泛型方法。

總結

本文只能算是對自己看書的一點小結,分享出來的目的一是希望如果對某些知識理解有誤,能及時得到大家指正;同時如果您感覺這篇博文有一點小價值,那我的第二個目的也就達到了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM