1. 程式人生 > >c#中的值型別和引用型別 裝箱拆箱 (好文)

c#中的值型別和引用型別 裝箱拆箱 (好文)

 

一,c#中的值型別和引用型別

     眾所周知在c#中有兩種基本型別,它們分別是值型別和引用型別;而每種型別都可以細分為如下型別:

    

  1.  什麼是值型別和引用型別
    • 什麼是值型別:
      • 進一步研究文件,你會發現所有的結構都是抽象型別System.ValueType的直接派生類,而System.ValueType本身又是直接從System.Object派生的。根據定義所知,所有的值型別都必須從System.ValueType派生,所有的列舉都從System.Enum抽象類派生,而後者又從System.ValueType派生。  
      •  所有的值型別都是隱式密封的(sealed),目的是防止其他任何型別從值型別進行派生。       
    • 什麼是引用型別:
      • 在c#中所有的類都是引用型別,包括介面。
  2.  區別和效能
    • 區別:
      • 值型別通常被人們稱為輕量級的型別,因為在大多數情況下,值型別的的例項都分配線上程棧中,因此它不受垃圾回收的控制,緩解了託管堆中的壓力,減少了應用程式的垃圾回收的次數,提高效能。
      • 所有的引用型別的例項都分配在託管堆上,c#中new操作符會返回一個記憶體地址指向當前的物件。所以當你在建立個一個引用型別例項的時候,你必須要考慮以下問題:
        • 記憶體是在託管堆上分配的
        • 在分配每一個物件時都會包含一些額外的成員(型別物件指標,同步塊索引),這些成員必須初始化
        • 物件中的其他位元組總是設為零
        • 在分配物件時,可能會進行一次垃圾回收操作(如果託管堆上的記憶體不夠分配一次物件時)
    • 效能:
      • 在設計一個應用程式時,如果都是應用型別,那麼應用程式的效能將顯著下降,因為這會加大託管堆的壓力,增加垃圾回收的次數。
      • 雖然值型別是一個輕量級的型別,但是如果大量的使用值型別的話,也會有損應用程式的效能(例如下面要講的裝箱和拆箱操作,傳遞例項較大的值型別,或者返回較大的值型別例項)。
      • 由於值型別例項的值是自己本身,而引用型別的例項的值是一個引用,所以如果將一個值型別的變數賦值給另一個值型別的變數,會執行一次逐欄位的複製,將引用型別的變數賦值給另一個引用型別的變數時,只需要複製記憶體地址,所以在對大物件進行賦值時要避免使用值型別。例如下面的程式碼

        複製程式碼

         1  class SomRef
         2     {
         3         public int x;
         4     }
         5     struct SomeVal {
         6         public int x;
         7     }
         8     class Program {
         9         static void ValueTypeDemo() {
        10             SomRef r1 = new SomRef();//在堆上分配
        11             SomeVal v1 = new SomeVal();//在棧上分配
        12             r1.x = 5;//提領指標
        13             v1.x = 5;//在棧上修改
        14             SomRef r2 = r1;//只複製引用(指標)
        15             SomeVal v2 = v1;//在棧上分配並複製成員
        16         }
        17     }

        複製程式碼

  3. 常見誤區
    • 引用型別分配在託管堆上,值型別分配線上程棧上:其實這種說法的前半部分是對的,後半部分是錯的。因為變數的值在它宣告的位置儲存的,所以假如某一個引用型別中有一個值型別的變數, 那麼該變數的值總是和該引用型別的物件的其它資料在一起,也就是分配在堆上。(只有區域性變數(方法內部宣告的變數)和方法的引數在棧上)
    • 結構是輕量級的類:這種錯誤的資訊主要是因為有人認為值型別不應該有方法或者其它有意義的行為-它們應該作為簡單的資料轉移來使用,所以很多人分不清DateTime到底是值型別還是引用型別
    • 物件在c#中預設的是用引用傳遞的其實在呼叫方法的時候,引數值(物件的一個引用)是以傳值得方式傳遞的,如果你想以引用方式傳遞的話,可以使用ref或者out關鍵字。

二,值型別的裝箱和拆箱操作

1 int i = 5;
2 object o = i;
3 int j = (int)o;
4 Int16 y=(Int16)o;

 

  1.  什麼是裝箱,什麼是拆箱
    • 什麼是裝箱:所謂裝箱就是將值型別轉化為引用型別的過程(例如上面程式碼的第2行),在裝箱時,你需要知道編譯器內部都幹了什麼事:
      • 在託管堆中分配好記憶體,分配的記憶體量是值型別的各個欄位需要的記憶體量加上託管堆上所以物件的兩個額外成員(型別物件指標,同步塊索引)需要的記憶體量
      • 值型別的欄位複製到新分配的堆記憶體中
      • 返回物件的地址,這個地址就是這個物件的引用
    • 什麼是裝箱:將已裝箱的值型別例項(此時它已經是引用型別了)轉化成值型別的過程(例如上面程式碼的第3行),注意:拆箱不是直接將裝箱過程倒過來,拆箱的代價比裝箱要低的多,拆箱其實就是獲取一個指標的過程。一個已裝箱的例項在拆箱時,編譯器在內部都幹了下面這些事:
      • 如果包含了“對已裝箱型別的例項引用”的變數為null時,會丟擲一個NullReferenceException異常。
      • 如果引用指向的物件不是所期待的值型別的一個已裝箱例項,會丟擲一個InvalidCastException異常(例如上面程式碼的第4行)。  
  1.  它們在什麼情況下發生,以及如何避免
  2. 複製程式碼

    1    static void Main(string[] args)
    2         {
    3             int v = 5;
    4             object o = v;
    5             v = 123;
    6             Console.WriteLine(v+","+(int)o);
    7        }

    複製程式碼

          通過上面的分析我們已經知道了,裝箱和拆箱/複製操作會對應用程式的速度和記憶體消耗產生不利的影響(例如消耗記憶體,增加垃圾回收次數,複製操作),所以我們應該注意編譯器在什麼時候會生成程式碼來自動這些操作,並嘗試手寫這些程式碼,儘量避免自動生成程式碼的情況。

    • 你能一眼從上面的程式碼中看出進行了幾次裝箱操作嗎?正取答案是3次。分別進行了哪三次呢,我們來看一下:第一次object o=v;第二次在執行 Console.WriteLine(v+","+(int)o);時將v進行裝箱,然後對o進行拆箱後又裝箱。也就是說裝箱過程總是在我們不經意的時候進行的,所以只有我們充分了解了裝箱的內部機制,才能有效的避免裝箱操作,從而提高應用程式的效能。所以對上面的程式碼進行如下修改可以減少裝箱次數,從而提高效能:

      複製程式碼

      1  static void Main(string[] args)
      2         {
      3             int v = 5;
      4             object o = v;
      5             v = 123;
      6             Console.WriteLine(v.ToString() + "," + ((int)o).ToString());//((int)o).ToString()程式碼本身沒有任何意義,只為演示裝箱和拆箱操作
      7        }

      複製程式碼

    • 下面來討論一下編譯器都會在什麼時候自動生成程式碼來完成這些操作
      • 使用非泛型集合時:比如ArrayList,因為這些集合需要的物件都是object,如果你將一個值型別的物件新增到集合中時會執行一次裝箱操作,當你取值時會執行一次拆箱操作,所以在應用程式中應避免使用這種非泛型的集合。
      • 大家都知道System.Object是所有型別的基類,當你呼叫object型別的非虛方法時會進行裝箱操作(例如GetType方法)。在呼叫object的虛方法時,如果你的值型別沒有重寫虛方法也要進行裝箱操作,所以在定義自己的值型別時,應重寫object內部的虛方法(例如ToString方式)
      • 將值型別轉化為介面型別時也會進行裝箱操作,這是因為介面型別必須包含對堆上的一個物件的引用。

三,泛型的出現(本節只簡單介紹泛型對裝箱和拆箱所起的作用,關於泛型的具體細節請參考下一篇文章)

    •  什麼泛型
      • 泛型是CLR和程式語言提供的一種特殊機制,它在c#2中才被提供出來。
    •  它對避免裝箱有什麼作用?
      • 在使用泛型時需要指定要裝配的型別,這樣可以減少裝箱操作,比如下面的程式碼

        複製程式碼

         1   static void Main(string[] args)
         2         {
         3             ArrayList dateList = new ArrayList { 
         4             DateTime.Now
         5             };
         6 
         7             IList<DateTime> dateT = new List<DateTime> { 
         8             DateTime.Now
         9             };
        10         }

        複製程式碼

        使用ArrayList時,每新增一個時間都會進行一次裝箱操作,而使用List<DateTime>時就不會進行裝箱操作,從而提高應用程式的效能。

    •  C#中常見的泛型集合:

      Queue<T>;

      Stack<T>;

      List<T>;

      Dictionary<Tkey,Tvalue>;

      HashSet<T>;

       

       在使用這些集合之前我們必須要理解每一種集合的工作原理(沒事自己可以實現一下),瞭解每一種集合的適合場合,這樣才能寫出高效的程式碼。

四,在設計時如何選擇類和結構體

在面試的時候,我們經常被問的一個問題(還有另外一個問題,如何選擇抽象類和介面,下次我會單獨聊聊這個問題),下面我們來聊聊在設計時應該如何選擇結構體和類

    •  什麼是結構體
      • 結構體是一種特殊的值型別,所以它擁有值型別所以的特權(例項一般分配線上程棧上)和限制(不能被派生,所以沒有 abstract 和 sealed,未裝箱的例項不能進行執行緒同步的訪問)。
    •  什麼情況下選擇結構體,什麼情況下選擇類
      • 在大多數的情況下,都應該選擇類,除非滿足以下情況,才考慮選擇結構體:
      • 型別具有基元型別的行為
      • 型別不需要從其它任何型別繼承
      • 型別也不會派生出任何其它型別
      • 型別的例項較小(約為16位元組或者更小)
      • 型別的例項較大,但是不作為方法的引數傳遞,也不作為方法的返回值。

都說程式是一門注重實踐的學科,但是也只有熟悉理解了這些概論的東西,才能在實踐時寫出優秀的程式碼,有不對或者不合理的地方歡迎在下面討論;

https://www.cnblogs.com/bakuhert/articles/5878086.html

什麼是值型別,什麼是引用型別

 

概念:值型別直接儲存其值,而引用型別儲存對其值的引用。部署:託管堆上部署了所有引用型別。

 

引用型別:基類為Objcet

值型別:均隱式派生自System.ValueType:

 

值型別:

byte,short,int,long,float,double,decimal,char,bool 和 struct 統稱為值型別。

引用型別:

string 和 class統稱為引用型別。

 

  • 值型別變數聲明後,不管是否已經賦值,編譯器為其分配記憶體。
  • 引用型別當宣告一個類時,只在棧中分配一小片記憶體用於容納一個地址,而此時並沒有為其分配堆上的記憶體空間。當使用 new 建立一個類的例項時,分配堆上的空間,並把堆上空間的地址儲存到棧上分配的小片空間中。
  • 值型別的例項通常是線上程棧上分配的(靜態分配),但是在某些情形下可以儲存在堆中。
  • 引用型別的物件總是在程序堆中分配(動態分配)。

我們來看下面一段程式碼:

輸出結果:

 

值型別在棧內分配空間大小因變數型別而異;

引用型別在棧內的空間大小相同;

 

1. 通用型別系統

C#中,變數是值還是引用僅取決於其資料型別。

C#的基本資料型別都以平臺無關的方式來定義。C#的預定義型別並沒有內置於語言中,而是內置於.NET Framework中。.NET使用通用型別系統(CTS)定義了可以在中間語言(IL)中使用的預定義資料型別,所有面向.NET的語言都最終被編譯為IL,即編譯為基於CTS型別的程式碼。

例如,在C#中宣告一個int變數時,宣告的實際上是CTS中System.Int32的一個例項。這具有重要的意義:

  • 確保IL上的強制型別安全;
  • 實現了不同.NET語言的互操作性;
  • 所有的資料型別都是物件。它們可以有方法,屬性,等。例如:

int i;

i = 1;

string s;

s = i.ToString();

 

以下關係圖(來自MSDN)說明了這幾種型別是如何相關的。注意,型別的例項可以只是值型別或自描述型別,即使這些型別有子類別也是如此。

型別類別:

 

2.值型別

C#的所有值型別均隱式派生自System.ValueType:

結構體:struct(直接派生於System.ValueType);

數值型別:

整型:sbyte(System.SByte的別名),short(System.Int16),int(System.Int32),long(System.Int64),byte(System.Byte),ushort(System.UInt16),uint(System.UInt32),ulong(System.UInt64),char(System.Char);

浮點型:float(System.Single),double(System.Double);

用於財務計算的高精度decimal型:decimal(System.Decimal)。

bool:bool(System.Boolean的別名);

使用者定義的結構體(派生於System.ValueType)。

列舉:enum(派生於System.Enum);

可空型別(派生於System.Nullable<T>泛型結構體,T?實際上是System.Nullable<T>的別名)。

每種值型別均有一個隱式的預設建構函式來初始化該型別的預設值。例如:

int i = new int();

等價於:

Int32 i = new Int32();

等價於:

int i = 0;

等價於:

Int32 i = 0;

引用型別和值型別都繼承自System.Object類。不同的是,幾乎所有的引用型別都直接從System.Object繼承,而值型別則繼承其子類,即 直接繼承System.ValueType。System.ValueType直接派生於System.Object。即System.ValueType本身是一個類型別,而不是值型別。其關鍵在於ValueType重寫了Equals()方法,從而對值型別按照例項的值來比較,而不是引用地址來比較。

可以用Type.IsValueType屬性來判斷一個型別是否為值型別:

TestType testType = new TestType ();

if (testTypetype.GetType().IsValueType)

{

     Console.WriteLine("{0} is value type.", testType.ToString());

}

 

3.引用型別

C#有以下一些引用型別:

陣列(派生於System.Array)

使用者用定義的以下型別:

類:class(派生於System.Object);

介面:interface(介面不是一個“東西”,所以不存在派生於何處的問題。Anders在《C# Programming Language》中說,介面只是表示一種約定[contract]);

委託:delegate(派生於System.Delegate)。

object(System.Object的別名);

字串:string(System.String的別名)。

可以看出:

引用型別與值型別相同的是,結構體也可以實現介面;

引用型別可以派生出新的型別,而值型別不能;

引用型別可以包含null值,值型別不能(可空型別功能允許將 null 賦給值型別);

引用型別變數的賦值只複製對物件的引用,而不復制物件本身。而將一個值型別變數賦給另一個值型別變數時,將複製包含的值。

對於最後一條,經常混淆的是string。我曾經在一本書的一個早期版本上看到String變數比string變數效率高;我還經常聽說String是引用型別,string是值型別,等等。例如:

string s1 = "Hello, ";

string s2 = "world!";

string s3 = s1 + s2;//s3 is "Hello, world!"

 

這確實看起來像一個值型別的賦值。再如:

string s1 = "a";

string s2 = s1;

s1 = "b";//s2 is still "a"

 

改變s1的值對s2沒有影響。這更使string看起來像值型別。實際上,這是運算子過載的結果,當s1被改變時,.NET在託管堆上為s1重新分配了記憶體。這樣的目的,是為了將做為引用型別的string實現為通常語義下的字串。

 

4. 值型別和引用型別在記憶體中的部署

經常聽說,並且經常在書上看到:值型別部署在棧上,引用型別部署在託管堆上。實際上並沒有這麼簡單。

MSDN上說:託管堆上部署了所有引用型別。這很容易理解。當建立一個應用型別變數時:

object reference = new object();

 

關鍵字new將在託管堆上分配記憶體空間,並返回一個該記憶體空間的地址。左邊的reference位於棧上,是一個引用,儲存著一個記憶體地址;而這個地址指向的記憶體(位於託管堆)裡儲存著其內容(一個System.Object的例項)。下面為了方便,簡稱引用型別部署在託管推上。

再來看值型別。《C#語言規範》上的措辭是“結構體不要求在堆上分配記憶體(However, unlike classes, structs are value types and do not require heap allocation)”而不是“結構體在棧上分配記憶體”。這不免容易讓人感到困惑:值型別究竟部署在什麼地方?

4.1陣列

考慮陣列:

int[] reference = new int[100];

根據定義,陣列都是引用型別,所以int陣列當然是引用型別(即reference.GetType().IsValueType為false)。

而int陣列的元素都是int,根據定義,int是值型別(即reference[i].GetType().IsValueType為true)。那麼引用型別陣列中的值型別元素究竟位於棧還是堆?

如果用WinDbg去看reference[i]在記憶體中的具體位置,就會發現它們並不在棧上,而是在託管堆上。

實際上,對於陣列:

TestType[] testTypes = new TestType[100];

如果TestType是值型別,則會一次在託管堆上為100個值型別的元素分配儲存空間,並自動初始化這100個元素,將這100個元素儲存到這塊記憶體裡。

如果TestType是引用型別,則會先在託管堆為testTypes分配一次空間,並且這時不會自動初始化任何元素(即testTypes[i]均為null)。等到以後有程式碼初始化某個元素的時候,這個引用型別元素的儲存空間才會被分配在託管堆上。

 

4.2型別巢狀

 

引用型別部署在託管堆上;

值型別總是分配在它宣告的地方:作為欄位時,跟隨其所屬的變數(例項)儲存;作為區域性變數時,儲存在棧上。

從上下文看,mc是一個區域性變數,所以部署在託管堆上,並被棧上的一個引用所持有;

值型別欄位_value1屬於引用型別例項mc的一部分,所以跟隨引用型別例項mc部署在託管堆上(有點類似於陣列的情形);

value2是值型別區域性變數,所以部署在棧上。

而對於值型別例項,即MyStruct

根據上下文,值型別例項ms本身是一個區域性變數而不是欄位,所以位於棧上;

其引用型別欄位_object1不存在跟隨的問題,必然部署在託管堆上,並被一個引用所持有(該引用是ms的一部分,位於棧);

其引用型別區域性變數_object2顯然部署在託管堆上,並被一個位於棧的引用所持有。

所以,簡單地說“值型別儲存在棧上,引用型別儲存在託管堆上”是不對的。必須具體情況具體分析

 

 

C#中,我們用struct/class來宣告一個型別為值型別/引用型別。考慮下面的例子:

SomeType[] oneTypes = new SomeType[100];

如 果SomeType是值型別,則只需要一次分配,大小為SomeType的100倍。而如果SomeType是引用型別,剛開始需要100次分配,分配後 陣列的各元素值為null,然後再初始化100個元素,結果總共需要進行101次分配。這將消耗更多的時間,造成更多的記憶體碎片。所以,如果型別的職責主 要是儲存資料,值型別比較合適。

一般來說,值型別(不支援多型)適合儲存供 C#應用程式操作的資料,而引用型別(支援多型)應該用於定義應用程式的行為。通常我們建立的引用型別總是多於值型別。如果滿足下面情況,那麼我們就應該建立為值型別:

該型別的主要職責用於資料儲存。

該型別的共有介面完全由一些資料成員存取屬性定義。

該型別永遠不可能有子類。

該型別不具有多型行為。

 

5. 辨明值型別和引用型別的使用場合

在C#中,我們用struct/class來宣告一個型別為值型別/引用型別。考慮下面的例子:

SomeType[] oneTypes = new SomeType[100];

如 果SomeType是值型別,則只需要一次分配,大小為SomeType的100倍。而如果SomeType是引用型別,剛開始需要100次分配,分配後 陣列的各元素值為null,然後再初始化100個元素,結果總共需要進行101次分配。這將消耗更多的時間,造成更多的記憶體碎片。所以,如果型別的職責主 要是儲存資料,值型別比較合適。

一般來說,值型別(不支援多型)適合儲存供 C#應用程式操作的資料,而引用型別(支援多型)應該用於定義應用程式的行為。通常我們建立的引用型別總是多於值型別。如果滿足下面情況,那麼我們就應該建立為值型別:

該型別的主要職責用於資料儲存。

該型別的共有介面完全由一些資料成員存取屬性定義。

該型別永遠不可能有子類。

該型別不具有多型行為。

 

 

值型別和引用型別的區別(小結)

相同點:

引用型別可以實現介面,值型別當中的結構體也可以實現介面;

引用型別和值型別都繼承自System.Object類。

 

1)範圍方面

C#的值型別包括:結構體(數值型別、bool型、使用者定義的結構體),列舉,可空型別。

C#的引用型別包括:陣列,使用者定義的類、介面、委託,object,字串。

2)記憶體分配方面:

陣列的元素不管是引用型別還是值型別,都儲存在託管堆上。

引用型別在棧中儲存一個引用,其實際的儲存位置位於託管堆。簡稱引用型別部署在託管推上。而值型別總是分配在它宣告的地方:作為欄位時,跟隨其所屬的變數(實 例)儲存;作為區域性變數時,儲存在棧上。(棧的記憶體是自動釋放的,堆記憶體是.NET中會由GC來自動釋放)

3)適用場合

值型別在記憶體管理方面具有更好的效率,並且不支援多型,適合用做儲存資料的載體;引用型別支援多型,適合用於定義應用程式的行為。

引用型別可以派生出新的型別,而值型別不能,因為所有的值型別都是密封(seal)的;

引用型別可以包含null值,值型別不能(可空型別功能允許將 null 賦給值型別,如   int? a = null;  );

引用型別變數的賦值只複製對物件的引用,而不復制物件本身。而將一個值型別變數賦給另一個值型別變數時,將複製包含的值。

 

值得注意的是,引用型別和值型別都繼承自System.Object類。不同的是,幾乎所有的引用型別都直接從System.Object繼承,而值型別則繼承其子類,即 直接繼承System.ValueType。即System.ValueType本身是一個類型別,而不是值型別。其關鍵在於ValueType重寫了Equals()方法,從而對值型別按照例項的值來比較,而不是引用地址來比較。

內容參考來自文章:

值型別和引用型別,棧和堆的含義

通用型別系統概述

C#詳解值型別和引用型別區別