程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 基元類型,引用類型和值類型,引用

基元類型,引用類型和值類型,引用

編輯:C#入門知識

基元類型,引用類型和值類型,引用


1.基元類型

有些數據類型我們平常寫代碼經常會用到,例如:int,string等,例如下面我們定義一個整數:

int a =0;

我們也可以用下面的寫法定義:

System.Int32  a = new System.Int32();

以上兩種寫法的結果都是一樣的,為什麼會這樣呢?,大家肯定知道是編譯器做的優化;本人比較喜歡第一種寫法,因為這種語法不僅增強了代碼的可讀性,生成的IL代碼跟System.Int32的IL代碼是完全一樣的。那什麼是基元類型呢?就是編譯器能直接支持的數據類型稱為基元類型(primitive type).基元類型直接映射到.NET框架類庫(FCL)中存在的類型。會不會很難理解?例如:c#中的int直接映射到了System.Int32類型。再看下面的代碼,雖然寫的方式不一樣,但是它們生成的IL代碼是完全一致的。只要是符合公共語言規范(CLS)的類型,不管那種語言都有提供了類似的基元類型。但是,不符合CLS的類型語言就不一定支持。

int a = 0;
System.Int32 a = 0;
int a = new int();
System.Int32 a =new System.Int32();

但是CLR via C#的作者是不推薦這種簡潔的寫法,下面就把他不推薦的理由照抄過來,大家可以作為參考:

1.許多開發人員糾結於是用String還是string。由於c#的string(一個關鍵字)直接映射到System.String(一個FCL類型),所以兩者沒有區別,都可以使用。類似,一些人開發人員說應用程序在32位操作系統上運行,int代表32位整數;在64位系統上運行,int代表64位整數。這個說法完全錯誤。c#的int始終映射到System.Int32,所以不管在什麼操作系統上運行,代表的都是32位整數。如果程序員習慣在代碼中使用Int32,像這樣的誤解就沒有了。

2.c#的long映射到Sytem.Int64,但在其他編程語言中,Long可能映射到Int16或Int32.例如,c++/CLI就將long視為Int32.習慣於用一種語言寫程序的人在看用另一種語言寫的源代碼時,很容易錯誤理解代碼意圖。事實上,大多數語言甚至不將long當作關鍵字,根本不編譯使用了它的代碼。

3.FCL的許多方法都將類型名作為方法名的一部分。例如,BinaryReader類型的方法包括ReadBoolean,ReadInt32,ReadSingle等;而System.Convert類型的方法包括ToBoolean,ToInt32,ToSingle等。以下代碼雖然語法沒問題,但包含float的那一行顯得很別扭,無法一下子判斷該行的正確性:

BinaryReader br = new BinaryReader(...);
float val = br.ReadSingle();    //正確,但感覺別扭
Singleval = br.ReadSingle();    //正確,感覺自然

4.平時只用c#的許多程序員逐漸忘了還可以用其他語言寫面向CLR的代碼,“c#主義”逐漸入侵類庫代碼。例如,Microsoft的FCL幾乎是完全用c#寫的,FCL團隊向庫中引入了像Array的GetLongLength這樣的方法。該方法返回Int64值。這種值在c#確實是long,但在其他語言中不是。另一個例子是System.Linq.Enumerable的LongCount方法。

以上就是作者不推薦基元類型的原因,以至於CLR via c#裡面所描述的類型都是FCL的類型名稱。不過我自己倒覺得使用基元類型看起來比用FCL類型順眼,所以還是自己的習慣吧,至少兩種類型生成的IL代碼都是一樣的,也沒有說用這種性能好,用那種性能不好的說法。在這裡引用了博友的圖片,圖代表了C#的基元類型與對應的FCL類型

//引用類型 class Ref { public int x; } //值類型 struct Val { public int x; } static void Demo() { Ref r1 = new Ref(); //托管堆分配 Val v1 = new Val(); //棧上分配 r1.x = 5; //提領指針 v1.x = 5; //在棧上修改 Console.WriteLine(r1.x); //顯示5 Console.WriteLine(v1.x); //同樣顯示5 Ref r2 = r1; //只復制指針 Val v2 = v1; //在棧上分配並復制成員 r1.x = 8; //r1.x和r2.x都會改變 v1.x = 9; //v1.x會改變,v2.x不會 Console.WriteLine(r1.x); //顯示8 Console.WriteLine(r2.x); //顯示8 Console.WriteLine(v1.x); //顯示9 Console.WriteLine(v2.x); //顯示5 }

值類型的主要優勢是不作為在托管堆上分配。當然,與引用類型相比,值類型也存在自身的一些局限。下面列出了值類型和引用類型的一些區別。

1.值類型有已裝箱和未裝箱2種表示形式,而引用類型總是處於已裝箱形式。

2.值類型總是從System.ValueType中派生。跟System.Object具有相同的方法。但是ValueType重寫了Equals方法,能在兩個對象的字段值完全匹配前提下返回true,此外也重寫了GetHashCode方法。

3.值類型的所有方法都是不能為抽象的,都是隱式密封的(不可以重寫)。

4.將值類型變量賦給另一個值類型變量,會執行逐字段復制。將引用類型的變量賦給另一個引用類型變量只復制內存地址,所以兩個引用類型的變量都是指向堆同一個對象,所以對一個變量執行操作可能影響到另一個變量引用的對象。

5.由於未裝箱的值類型不在堆中分配,一旦定義了該類型的一個實例方法不在活動,為它們分配的存儲就會被釋放,而不是等著進行垃圾回收。

6.引用類型變量包含堆中對象的引用地址。引用類型的變量創建時默認初始化為null,代表當前不指向任何對象,試圖使用null引用類型變量都會拋出NullReferenceException異常。相反,值類型的所有成員都被初始化為0,訪問值類型不可能拋出NullReferenceException異常。CLR已經為值類型添加了可空標識

 

3.裝箱和拆箱

講到引用類型和值類型,必定要講下裝箱和拆箱了,下面開始講述:

值類型比引用類型“輕”,原因是它們不作為對象在托管堆中分配,不被垃圾回收,也不通過指針進行引用。但許多時候需要獲取對值類型實例的引用。例如下面這個栗子,創建一個ArrayList(這裡是為了舉例子而用ArrayList來裝值類型,平常最好不要這樣用,因為FCL已經提供了泛型集合類,List<T>在操作值類型不會進行裝箱和拆箱)來容納一組Point結構,上代碼:

//聲明值類型
struct Point
 {
    public int x, y;
 }

static void Main()
{
    ArrayList arraylist = new ArrayList();
    Point p;            //在棧上分配一個point
    for (int i = 0; i < 10; i++)
    {
        p.x = p.y = i;        //初始化成員
         arraylist.Add(p);        //對值類型裝箱,將引用添加到ArrayList中
     }
    Console.ReadLine();
}

上面的代碼大家很容易就能看出來,每次循環迭代都初始化一個Point的字段,並將該對象存儲在arraylist中。但思考下ArrayList中究竟存儲了什麼?是point結構,還是地址,還是其他的東西?要知道答案,我們來看下ArrayList的Add方法,了解它的參數被定義成什麼。代碼如下

 //
 // 摘要: 
 //     將對象添加到 System.Collections.ArrayList 的結尾處。
 //
 // 參數: 
 //   value:
 //     要添加到 System.Collections.ArrayList 的末尾處的 System.Object。 該值可以為 null。
 //
// 返回結果: 
 //     System.Collections.ArrayList 索引,已在此處添加了 value。
 //
 // 異常: 
 //   System.NotSupportedException:
 //     System.Collections.ArrayList 為只讀。 - 或 – System.Collections.ArrayList 具有固定大小。
 public virtual int Add(object value);

可以看出Add方法獲取的是一個object參數。也就是說,Add獲取對托管堆上的一個對象的引用(或指針)來作為參數。但是point又是值類型的,所以必須轉換成真正的在堆中托管的對象。將值類型轉換成引用類型稱為裝箱。那麼裝箱發生了什麼呢?

1.在托管堆分配內存。分配的內存量是值類型各字段所需的內存量,同時還有兩個額外的成員(類型對象指針和同步塊索引)所需的內存量。

2.值類型的字段復制到新分配的堆內存。

3.返回對象地址。這時值類型就成了引用類型了。

知道了裝箱之後,我們再來看下拆箱是怎麼運行了:

拆箱不是直接將裝箱過程倒過來,它其實就是獲取指針的過程,該指針指向包含在一個對象中的原始值類型,然後再進行字段的復制。所以拆箱的代價比裝箱低得多

那麼已裝箱值類型實例在拆箱時,內部發生下面這些事情:

1.如果包含“對已裝箱值類型實例的引用”的變量為null,拋出NullReferenceException異常

2.如果引用的對象不是所需值類型的已裝箱實例,拋出InvalidCastException異常。

用代碼來看看裝箱和拆箱的例子

static void Main()
 {
            int val = 5;    //創建未裝箱值類型變量
            object obj = val;   //val進行裝箱
            val = 123;          //將val值改為123
            Console.WriteLine(val + "," + (int)obj);    //顯示123,5
 }

大家能從上面代碼看出有多少次裝箱和拆箱嗎?利用ILDasm查看本代碼的IL就能很清楚看出來:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // 代碼大小       47 (0x2f)
  .maxstack  3
  .locals init ([0] int32 val,
           [1] object obj)
  IL_0000:  nop
  //將5加載到val中
  IL_0001:  ldc.i4.5
  IL_0002:  stloc.0
  //將val進行裝箱,將引用指針存儲到obj中
  IL_0003:  ldloc.0
  IL_0004:  box        [mscorlib]System.Int32
  IL_0009:  stloc.1
  //將123加載到val中
  IL_000a:  ldc.i4.s   123
  IL_000c:  stloc.0
  //將val進行裝箱,將指針保留在棧上以進行Concat操作
  IL_000d:  ldloc.0
  IL_000e:  box        [mscorlib]System.Int32
  //將字符串加載到棧上
  IL_0013:  ldstr      ","
  //對obj進行拆箱,獲取一個指針,指向棧上的Int32字段
  IL_0018:  ldloc.1
  IL_0019:  unbox.any  [mscorlib]System.Int32
  IL_001e:  box        [mscorlib]System.Int32
  //調用Concat方法
  IL_0023:  call       string [mscorlib]System.String::Concat(object,
                                                              object,
                                                              object)
  //返回字符串
  IL_0028:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_002d:  nop
  //從main返回,終止引用程序
  IL_002e:  ret
} // end of method Program::Main

上面的IL顯示出三個box和一個unbox,第一次裝箱很明顯就能看出來,但是第二三次裝箱可能有些同學會無法理解,在調用writeline這個方法時返回一個String對象,所以c#編譯器生成代碼來調用String的靜態方法Concat。該方法有幾個重載版本,而在這裡調用了一下版本:

public static string Concat(object arg0, object arg1, object arg2);

所以在代碼中val和轉成Int32的obj都會裝箱然後傳給Concat中。大家有興趣可以把上面的代碼改下,輸入結構代碼變成Console.WriteLine(val + "," + obj); 然後再看看IL代碼,你會發現減少了一次裝箱和拆箱而且代碼的大小減少了10字節左右。所以證明額外的裝箱拆箱會在托管堆分配一個額外的對象,然後還要進行垃圾回收,可見過多的裝箱操作會影響到程序的性能和內存的消耗。所以我們盡可能地在自己的代碼中減少裝箱。

如果知道自己的代碼在編譯器中會反復發生裝箱,那最好是用手動方式對其進行裝箱,例如:

static void Main()
{
            int val=5;
            //會進行3次裝箱
            Console.WriteLine("{0}{1}{2}",val,val,val);

            //手動進行裝箱
            object obj=val;
            //不會發生裝箱
           Console.WriteLine("{0}{1}{2}",obj,obj,obj);
}

以上羅列出了基元類型、引用類型和值類型的區別,最後加了裝箱和拆箱的,文碼並茂,希望能給你帶來比較深刻的印象,雖然不夠深,但願能夠起到拋磚引玉的作用。本文參考了CLR via C#,很推薦大家有空可以閱讀此書。以後我還會在這個系列中多寫些文章,分享給大家。

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