本章要討論的是.net的各類類型。這章開始,我想摒棄之前的抄書模式,嘗試本身閱讀後先行總結,而後再寫博客。編程
基元類型ide
所謂基元類型,指的是編譯器直接支持的數據類型。基元類型直接映射到Framework類庫中存在的類型。下面四行代碼能夠生成徹底相同的IL:性能
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Program1 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 int a = 0; 14 Int32 b = 0; 15 int c = new int(); 16 Int32 d = new Int32(); 17 } 18 } 19 }
再看他們的IL代碼:this
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 10 (0xa) .maxstack 1 .locals init (int32 V_0, int32 V_1, int32 V_2, int32 V_3) IL_0000: nop IL_0001: ldc.i4.0 IL_0002: stloc.0 IL_0003: ldc.i4.0 IL_0004: stloc.1 IL_0005: ldc.i4.0 IL_0006: stloc.2 IL_0007: ldc.i4.0 IL_0008: stloc.3 IL_0009: ret } // end of method Program::Main
由此可知,這四個寫法是徹底等價的。spa
在本書中,堅持使用FCL名稱,主要有如下緣由:.net
1.不少人糾結於使用string仍是System.String,其實這二者沒有區別。相似的,還有int和Int32:C#的int永遠映射到Int32.C#的long固定映射到Int64.線程
2.FLC的許多方法都將類型名做爲方法名的一部分。設計
3.方便些其餘面向CLR的代碼(代碼風格一致)。指針
在高精度基元類型隱式轉換到低精度基元類型的時候,每每會進行截斷處理(區別於向上取整)。code
C#自帶checked操做符來在特定的區域控制溢出檢查:
Byte b = 100; b = checked((Byte)(b + 200));
會拋出異常:
還可使用checked語句:
1 static void Main(string[] args) 2 { 3 checked { 4 Byte b = 100; 5 b = (Byte)(b + 200); 6 } 7 8 }
結果是同樣的。若是使用了checked語句塊,還能夠將+=應用於Byte:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 checked { 6 Byte b = 100; 7 b += 200; 8 } 9 10 } 11 }
在平常編程時,給予諸位以下建議:
1.儘可能使用有符號數值類型Int32之類而不是UInt32,這樣編譯器會檢查更多的上溢下溢。此外,類庫中的不少方法的返回值都是有符號的,這樣子能夠減小強制類型轉換。以及,無符號數值類型不符合CLS。
2.若是代碼可能發生溢出,請放到checked語句塊中。
3.將容許溢出的代碼放到unchecked中。
4.對於沒有使用checked和unchecked的代碼,溢出默認會拋出異常,
引用類型和值類型
首先,要認清楚四個事實:
1.內存必須從託管堆中分配;
2.堆上的每個對象都有額外成員,這些成員必須初始化;
3.對象的其餘字節老是爲零;
4.從託管堆分配對象時,可能強制執行一次GC。
所以,使用引用類型而非值類型的時候,性能會降低。在設計本身的類型時,要考慮是否應該定義成值類型而不是引用類型。除非知足如下所有條件,不然不該該聲明爲值類型:
1.類型具備基元類型的行爲,是不可變類型(沒有提供會更改其字段的成員);
2.不須要從其餘任何類型繼承;
3.沒有派生類型;
4.類型實例較小(小於等於16字節);
5.實例類型較大,但不做爲方法傳遞實參,也不從方法返回。
列出值類型和引用類型的一些區別:
1.值類型有兩種形式:已裝箱和未裝箱。引用類型老是處於已裝箱;
2.值類型從ValueType派生
#region 程序集 mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 // C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\mscorlib.dll #endregion using System.Runtime.InteropServices; using System.Security; namespace System { // // 摘要: // 提供值類型的基類。 [ComVisible(true)] public abstract class ValueType { // // 摘要: // 初始化 System.ValueType 類的新實例。 protected ValueType(); // // 摘要: // 指示此實例與指定對象是否相等。 // // 參數: // obj: // 要與當前實例進行比較的對象。 // // 返回結果: // 若是 obj 和該實例具備相同的類型並表示相同的值,則爲 true;不然爲 false。 [SecuritySafeCritical] public override bool Equals(object obj); // // 摘要: // 返回此實例的哈希代碼。 // // 返回結果: // 一個 32 位有符號整數,它是該實例的哈希代碼。 [SecuritySafeCritical] public override int GetHashCode(); // // 摘要: // 返回該實例的徹底限定類型名。 // // 返回結果: // 包含徹底限定類型名的 System.String。 public override string ToString(); } }
而ValueType繼承自System.Object;
3.不能在值類型中加入虛方法,全部的方法都不能抽象,不可重寫;
4.引用類型包含了堆中對象的地址。引用類型變量在建立的時候默認初始化爲NULL,而值類型老是0。null引用類型會拋出異常。值類型能夠添加可空標識;
5.值類型複製是徹底拷貝,而引用類型只拷貝地址;
6.修改引用類型,會致使其引用也受到影響;
7.由於值類型是沒有被裝箱的,因此一旦一個實例再也不活動,爲它分配的存儲就會被釋放,而不是等待GC。
拆箱和裝箱
這部分是這一章的重中之重之重中之重中重。
不少時候,要獲取值類型的實例引用。這也是「何時會進行裝箱」的答案。
先例舉一個簡單的例子:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Collections; 7 8 namespace Program4 9 { 10 class Program 11 { 12 internal struct Point { 13 private Int32 m_x, m_y; 14 public Point(Int32 x, Int32 y) { 15 m_x = x; 16 m_y = y; 17 } 18 public void Change(Int32 x, Int32 y) 19 { 20 m_x = x; 21 m_y = y; 22 } 23 public override String ToString() 24 { 25 return String.Format("{0}, {1}", m_x.ToString(), m_y.ToString()); 26 } 27 } 28 static void Main(string[] args) 29 { 30 ArrayList a = new ArrayList(); 31 Point p = new Point(0, 0); 32 for (Int32 i = 0; i < 5; i++) { 33 p.Change(i, i); 34 a.Add(p); 35 } 36 } 37 } 38 }
本例中的Add方法原型以下 :
// // 摘要: // 將對象添加到 System.Collections.ArrayList 的結尾處。 // // 參數: // value: // 要添加到 System.Collections.ArrayList 末尾的 System.Object。該值能夠爲 null。 // // 返回結果: // value 已添加的 System.Collections.ArrayList 索引。 // // 異常: // T:System.NotSupportedException: // The System.Collections.ArrayList is read-only.-or- The System.Collections.ArrayList // has a fixed size. public virtual int Add(object value);
能夠看出來,Add要獲取的是一個Object,是一個引用類型,可是Point p是一個值類型。爲了使代碼正確工做,須要將p轉換成在堆中託管的對象,以獲取對該對象的引用。
這時,就要使用裝箱機制。對值類型裝箱時發生了以下事情:
1.在託管堆中分配內存。除了值類型各字段所需的內存量,還須要爲類型對象指針和同步塊索引分配內存空間;
2.值類型的字段拷貝到新分配的內存;
3.返回對象地址。
能夠看一下IL代碼:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 代碼大小 59 (0x3b) .maxstack 3 .locals init (class [mscorlib]System.Collections.ArrayList V_0, valuetype Program4.Program/Point V_1, int32 V_2, bool V_3) IL_0000: nop IL_0001: newobj instance void [mscorlib]System.Collections.ArrayList::.ctor() IL_0006: stloc.0 IL_0007: ldloca.s V_1 IL_0009: ldc.i4.0 IL_000a: ldc.i4.0 IL_000b: call instance void Program4.Program/Point::.ctor(int32, int32) IL_0010: nop IL_0011: ldc.i4.0 IL_0012: stloc.2 IL_0013: br.s IL_0032 IL_0015: nop IL_0016: ldloca.s V_1 IL_0018: ldloc.2 IL_0019: ldloc.2 IL_001a: call instance void Program4.Program/Point::Change(int32, int32) IL_001f: nop IL_0020: ldloc.0 IL_0021: ldloc.1 IL_0022: box Program4.Program/Point IL_0027: callvirt instance int32 [mscorlib]System.Collections.ArrayList::Add(object) IL_002c: pop IL_002d: nop IL_002e: ldloc.2 IL_002f: ldc.i4.1 IL_0030: add IL_0031: stloc.2 IL_0032: ldloc.2 IL_0033: ldc.i4.5 IL_0034: clt IL_0036: stloc.3 IL_0037: ldloc.3 IL_0038: brtrue.s IL_0015 IL_003a: ret } // end of method Program::Main
會發現其中有裝箱操做。
這裏稍微擴展一個,關於for循環在IL中的知識:在IL中,for循環經過兩個指令:br.s(無條件地將控制轉移到目標指令)和brtrue.s(若是 value 爲 true、非空或非零,則將控制轉移到目標指令)兩個指令實現循環,clt(比較兩個值。若是第一個值小於第二個值,則將整數值 1 (int32) 推送到計算堆棧上;反之,將 0 (int32) 推送到計算堆棧上)來控制是否繼續循環的那個值。
下面來看拆箱。假定咱們要獲取ArrayList的第一個元素:
Point p2 = (Point)a[0];
它獲取了ArrayList的元素0包含的引用,試圖將其放到Point值類型的實例p中。爲此,已裝箱Point對象中的全部字段都必須複製到值類型變量p2中。爲此,已裝箱Point對象中的全部字段都必須複製到值類型變量p2中,後者在線程棧上。CLR分兩步完成複製:第一步獲取已裝箱Point對象中哥哥Point字段的地址,這個過程被稱爲拆箱。第二步就是將字段包含的值從堆複製到基於棧的值類型實例中。
拆箱不是將裝箱的過程倒過來。拆箱只是獲取指針的過程,該指針指向包含在一個對象中的原始值類型。指針指的是已裝箱實例中的未裝箱部分。
已裝箱值類型的實例在拆箱時,會發生下面的事情:
1.若是包含「對已裝箱值類實例的引用」的變量爲null,拋出異常;
2.若是引用的對象不是所需值類型以裝箱的實例,拋出異常。
第二條的具體狀況舉例:
Int32 x = 5; Object o = x; Int16 y = (Int16)o;
正確的寫法應該是:
Int32 x = 5; Object o = x; Int16 y = (Int16)(Int32)o;
再來看一個例子:
Int32 x = 5; Object o = x; x = 123; Console.WriteLine(x + "," + (Int32)o);
請問在這裏總共執行了多少次裝箱?
答案是3次。
第一次裝箱發生在Object o = x,第二次是WriteLine的x(在WriteLine須要一個String對象,而String是個引用類型。爲了將Int32轉換成String,須要進行一次裝箱操做),第三次是在o進行了一次拆箱操做後,爲了獲取String,又進行了一次裝箱。
能夠用下面的寫法來避免第二次拆箱和第三次裝箱:
Console.WriteLine(x + "," + o);
還能夠避免第一次的裝箱操做:
Console.WriteLine(x.ToString + "," + o);
雖然未裝箱對象沒有類型對象指針,但仍可調用由類型繼承或重寫的虛方法。若是值類型重寫了虛方法,那麼CLR能夠非虛的調用該方法,由於值類型隱式密封,不會有類型派生,並且調用虛方法的值類型沒有封裝。然而。若是重寫的虛方法要調用在基類中的實現的時候,值類型就會裝箱,以便經過一個this指針將對一個堆對象的引用傳給基方法。將值類型的未裝箱實例轉型爲類型的某個接口時要對實例進行裝箱,這是由於接口變量必須包含對堆對象的引用。能夠看下面的代碼,結合其IL:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Collections; 7 8 namespace Program4 9 { 10 class Program 11 { 12 internal struct Point 13 { 14 private Int32 m_x, m_y; 15 public Point(Int32 x, Int32 y) 16 { 17 m_x = x; 18 m_y = y; 19 } 20 public void Change(Int32 x, Int32 y) 21 { 22 m_x = x; 23 m_y = y; 24 } 25 public override String ToString() 26 { 27 return String.Format("{0}, {1}", m_x.ToString(), m_y.ToString()); 28 } 29 } 30 static void Main(string[] args) 31 { 32 Point p = new Point(0, 0); 33 Console.WriteLine(p); 34 p.Change(1, 2); 35 Console.WriteLine(p); 36 object o = p; 37 Console.WriteLine(o); 38 ((Point)o).Change(3, 3); 39 Console.WriteLine(o); 40 } 41 } 42 }
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 84 (0x54) .maxstack 3 .locals init (valuetype Program4.Program/Point V_0, object V_1, valuetype Program4.Program/Point V_2) IL_0000: nop IL_0001: ldloca.s V_0 IL_0003: ldc.i4.0 IL_0004: ldc.i4.0 IL_0005: call instance void Program4.Program/Point::.ctor(int32, int32) IL_000a: nop IL_000b: ldloc.0 IL_000c: box Program4.Program/Point IL_0011: call void [mscorlib]System.Console::WriteLine(object) IL_0016: nop IL_0017: ldloca.s V_0 IL_0019: ldc.i4.1 IL_001a: ldc.i4.2 IL_001b: call instance void Program4.Program/Point::Change(int32, int32) IL_0020: nop IL_0021: ldloc.0 IL_0022: box Program4.Program/Point IL_0027: call void [mscorlib]System.Console::WriteLine(object) IL_002c: nop IL_002d: ldloc.0 IL_002e: box Program4.Program/Point IL_0033: stloc.1 IL_0034: ldloc.1 IL_0035: call void [mscorlib]System.Console::WriteLine(object) IL_003a: nop IL_003b: ldloc.1 IL_003c: unbox.any Program4.Program/Point IL_0041: stloc.2 IL_0042: ldloca.s V_2 IL_0044: ldc.i4.3 IL_0045: ldc.i4.3 IL_0046: call instance void Program4.Program/Point::Change(int32, int32) IL_004b: nop IL_004c: ldloc.1 IL_004d: call void [mscorlib]System.Console::WriteLine(object) IL_0052: nop IL_0053: ret } // end of method Program::Main
對象哈希碼
FLC的設計者認爲,若是能將對象的任何實例放到哈希表集合中,能帶來不少好處。爲此,System.Object提供了虛方法GetHashCode,能獲取任意對象的Int32的哈希碼。因此,若是重寫了Equals方法,必定要重寫GetHashCode方法。