Q1: 什么是基元類型?C#中有哪些基元類型?
A1: 編譯器能直接支持的數據類型稱為基元類型,基元類型直接映射到FCL中存在的類型,比如C#中int映射到System.Int32類型。
C#中的基元類型:
我們可以定義這樣定義一個字符串:
String str = "abc";
也可以這樣定義一個字符串:
string str = "abc";
它們生成的IL是完全相同的,string可以被C#編譯器直接映射到FCL中的System.String類型,換言之,C#編譯器自動假定所有的源代碼中都添加了這個命令:
using string = System.String;
類似的int和Int32, double和Double也都是一樣的。
Q2: 什么是值類型,什么是引用類型,它們的區別是什么?
A3: 派生自System.ValueType的類型稱為值類型,值類型可以實現一個或多個接口,但不能作為其他類型的基類。值類型的內存空間分配在線程棧中,在代表值類型的一個實例的變量中,並不包含指針,而是包含了實例本身的字段,值類型的實例不受垃圾回收器的控制。引用類型的內存空間分配在托管堆中,它的實例由垃圾回收器進行回收,在線程棧中存儲的實例變量只包含一個指向托管堆中特定位置的一個指針。
Q3: 分析以下代碼。

1 namespace Test 2 { 3 4 // 引用類型 5 class SomeRef 6 { 7 public Int32 x; 8 } 9 10 // 值類型 11 struct SomeVal 12 { 13 public Int32 x; 14 } 15 16 class Program 17 { 18 static void Main(string[] args) 19 { 20 SomeRef r1 = new SomeRef(); // 在托管堆上分配 21 SomeVal v1 = new SomeVal(); // 在線程棧上分配 22 r1.x = 5; // x儲存在托管堆中,將其值設為5,線程棧中存儲着一個r1變量指向托管堆中的SomeRef實例 23 v1.x = 5; // 在線程棧中將變量值改為5; 24 Console.WriteLine(r1.x); // 打印出5 25 Console.WriteLine(v1.x); // 打印出5 26 27 SomeRef r2 = r1; // 在線程棧中分配一個新的變量r2,同樣指向r1指向的托管堆中的實例 28 SomeVal v2 = v1; // 在線程棧中分配一個新的變量v2, 將v1的值復制到v2中 29 r1.x = 8; // 修改存儲在托管堆中的x的值為8 30 v1.x = 9; // 修改線程棧中v1.x的值為9 31 32 Console.WriteLine(r1.x); // 打印出8 33 Console.WriteLine(r2.x); // r2指向和r1的同一個實例,故也打印出8 34 Console.WriteLine(v1.x); // 打印出9 35 Console.WriteLine(v2.x); // v2.x在線程棧中的值並沒有改變,故打印出5 36 } 37 } 38 }
A3:
Q4: 何時使用struct, 何時使用class?
A4: 同時滿足以下三個條件:1)類型中沒有成員會會修改類型的實例字段;2)類型不需要從其它任何類型繼承;3)類型不會派生出其他任何類型;並滿足以下兩個條件中的一個:1)類型的實例小於16字節;2)類型的實例大於16字節,但不作為方法的實參傳遞,也不作為方法的返回值。此時,可以把這個類型定義為struct, 否則定義為class.
Q5: 定義一個值類型應該注意什么?
A5: 1)由於System.ValueType重寫了Equals方法和GetHashCode方法,在定義自己的值類型時,也要重寫這兩個方法並提供它們的顯式實現;2)所有方法都不能是虛方法。
Q6: SomeVal為一個struct,包含一個實例字段x, 以下代碼有何不同?
1 SomeVal v = new SomeVal(); 2 SomeVal v;
A6: 兩行代碼都會在線程棧上分配內存空間,唯一的不同在於C#會認為new操作符對v進行了初始化。如果用第一行代碼定義v, 打印v.x時會打印出默認值0, 如果使用第二行代碼定義v, 打印v.x時會出錯。
Q7: 什么是裝箱和拆箱,裝箱和拆箱的過程是什么?
A7: 值類型轉換為引用類型會發生裝箱。裝箱的時候,首先會在托管堆中分配內存空間,這包括值類型各個字段需要的內存空間加上托管堆上所有對象都有的兩個成員(類型對象指針和同步塊索引);然后會將值類型的字段復制到托管堆上;最后返回對象的地址。引用類型轉換為值類型會發生拆箱,拆箱操作並不是裝箱操作的逆操作,拆箱其實是獲取一個指針的過程,該指針指向已裝箱實例的未裝箱部分,緊接着會發生一次復制操作。
Q8: 分析以下代碼,判斷三次打印各發生了多少次裝箱和拆箱。
1 static void Main(string[] args) 2 { 3 Int32 v = 5; 4 object o = v; 5 v = 123; 6 Console.WriteLine(v + "," + (Int32)o); // 打印一 7 Console.WriteLine(v + "," + o); // 打印二 8 Console.WriteLine(v.ToString() + "," + o); // 打印三 9 }
A8: 第一次打印發生了三次裝箱(Int32類型的v轉換為Object類型的o, Int32類型的v轉換為字符串,o被轉換為Int32發生拆箱,再被轉換為字符串,發生裝箱);第二次打印發生兩次裝箱(Int32類型的v轉換為Object類型的o, Int32類型的v轉換為字符串);第三次打印只發生了一次裝箱(Int32類型的v轉換為Object類型的o)。
Q9: 以下代碼是否正確?
1 static void Main(string[] args) 2 { 3 Int32 x = 5; 4 object o = x; 5 Int16 y = (Int16)o; 6 }
A9: 不對。當一個對象進行拆箱操作時,只能轉換為它原先未裝箱時的值類型,故應該為:
Int16 y = (Int16)(Int32)o;