類型從System.Object派生安全
C#中全部類型都是從System.Object派生的,能夠顯式派生,也能夠隱式派生。System.Object的公共方法以下:數據結構
1 #region 程序集 mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 2 // C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\mscorlib.dll 3 #endregion 4 5 using System.Runtime.ConstrainedExecution; 6 using System.Runtime.InteropServices; 7 using System.Runtime.Versioning; 8 using System.Security; 9 10 namespace System 11 { 12 // 13 // 摘要: 14 // 支持 .NET Framework 類層次結構中的全部類,併爲派生類提供低級別服務。這是 .NET Framework 中全部類的最終基類;它是類型層次結構的根。若要瀏覽此類型的.NET 15 // Framework 源代碼,請參閱 Reference Source。 16 [ClassInterface(ClassInterfaceType.AutoDual)] 17 [ComVisible(true)] 18 public class Object 19 { 20 // 21 // 摘要: 22 // 初始化 System.Object 類的新實例。 23 [NonVersionableAttribute] 24 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] 25 public Object(); 26 27 // 28 // 摘要: 29 // 在垃圾回收將某一對象回收前容許該對象嘗試釋放資源並執行其餘清理操做。 30 [NonVersionableAttribute] 31 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] 32 ~Object(); 33 34 // 35 // 摘要: 36 // 肯定指定的對象實例是否被視爲相等。 37 // 38 // 參數: 39 // objA: 40 // 要比較的第一個對象。 41 // 42 // objB: 43 // 要比較的第二個對象。 44 // 45 // 返回結果: 46 // 若是對象被視爲相等,則爲 true,不然爲 false。若是 objA 和 objB 均爲 null,此方法將返回 true。 47 public static bool Equals(Object objA, Object objB); 48 // 49 // 摘要: 50 // 肯定指定的 System.Object 實例是不是相同的實例。 51 // 52 // 參數: 53 // objA: 54 // 要比較的第一個對象。 55 // 56 // objB: 57 // 要比較的第二個對象。 58 // 59 // 返回結果: 60 // 若是 objA 是與 objB 相同的實例,或若是二者均爲 null,則爲 true,不然爲 false。 61 [NonVersionableAttribute] 62 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] 63 public static bool ReferenceEquals(Object objA, Object objB); 64 // 65 // 摘要: 66 // 肯定指定的對象是否等於當前對象。 67 // 68 // 參數: 69 // obj: 70 // 要與當前對象進行比較的對象。 71 // 72 // 返回結果: 73 // 若是指定的對象等於當前對象,則爲 true,不然爲 false。 74 public virtual bool Equals(Object obj); 75 // 76 // 摘要: 77 // 做爲默認哈希函數。 78 // 79 // 返回結果: 80 // 當前對象的哈希代碼。 81 public virtual int GetHashCode(); 82 // 83 // 摘要: 84 // 獲取當前實例的 System.Type。 85 // 86 // 返回結果: 87 // 當前實例的準確運行時類型。 88 [SecuritySafeCritical] 89 public Type GetType(); 90 // 91 // 摘要: 92 // 返回表示當前對象的字符串。 93 // 94 // 返回結果: 95 // 表示當前對象的字符串。 96 public virtual string ToString(); 97 // 98 // 摘要: 99 // 建立當前 System.Object 的淺表副本。 100 // 101 // 返回結果: 102 // 當前 System.Object 的淺表副本。 103 [SecuritySafeCritical] 104 protected Object MemberwiseClone(); 105 } 106 }
CLR要求全部對象都用new操做符建立。new操做符所作的事情有:ide
●計算類型和全部基類型(一直到System.Object)中定義的全部實例字段所須要的字節數。堆上的每個對象都須要一些額外的開銷成員,包括「類型對象指針」、「同步塊索引」。CLR利用這些成員管理對象。這些額外開銷成員的字節數要記入對象大小。函數
●從託管堆中分配類型要求的字節數,從而分配對象的內存,分配的全部字節都設爲0。性能
●初始化對象的「類型對象指針」和「同步塊索引」成員。優化
●調用類型的實例構造器,傳遞在new中指定的實參。ui
new在執行了這些操做後,返回一個指向新建對象的一個引用或指針。在C#中,沒有與new相對應的delete,而是依靠CLR的GC機制來進行垃圾回收。spa
類型轉換線程
CLR的最重要特性之一就是類型安全。在運行的時候,CLR老是知道對象的類型是什麼。調用GetType能夠知道對象的確切類型。指針
CLR容許將對象轉化爲它的(實際)類型或者基類型。C#不要求任何特殊語法便可將對象轉化爲任何基類型,由於向基類型的轉換被認爲是安全的隱式轉換。然而,將對象轉化成某個派生類型的時候,C#要求開發人員只能進行顯示轉換,由於這種轉換有可能在運行時失敗。下面展現一下向基類型和派生類型的轉換:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Reflection; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace Project_1 9 { 10 internal class Employee : System.Object { 11 } 12 public sealed class Program:System.Object 13 { 14 public static void Main(string[] args) 15 { 16 Object obj = new Employee(); 17 Employee e = (Employee)obj; 18 } 19 } 20 }
下面看看在運行時發生的事情:在運行時,CLR檢查轉型操做,肯定老是轉換爲對象的實際類型或者它的任何基類型。好比下面代碼雖然能夠編譯經過,可是會有異常:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Reflection; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace Project_1 9 { 10 internal class Employee : System.Object { 11 } 12 internal class Manager : Employee { 13 } 14 public sealed class Program:System.Object 15 { 16 public static void PromoteEmployee(Object o) { 17 Employee e = (Employee)o; 18 } 19 20 public static void Main(string[] args) 21 { 22 Manager m = new Manager(); 23 PromoteEmployee(m); 24 DateTime newYears = new DateTime(2019, 2, 20); 25 PromoteEmployee(newYears); 26 27 } 28 } 29 }
Main構造一個Manager對象並將其傳給PromoteEmployee。之因此能夠編譯成功並運行,由於Manager最終從Object派生,而PromoteEmployee傳入的正是一個Object。進入到PromoteEmployee內部以後,PromoteEmployee內部以後,CLR覈實對象o引用的就是一個Employee對象,或者是從Employee派生的一個類型的對象。因爲Manager從Employee派生,因此CLR執行類型轉換,容許PromoteEmployee繼續執行。
PromoteEmployee返回後,Main構造一個DateTime對象並傳給PromoteEmployee。一樣的,Datetime從Object派生,因此編譯器會調用PromoteEmployee的代碼。可是進入PromoteEmployee內部以後,CLR會檢查類型轉換,發現o引用的既不是Employee,也不是Employee的派生類型。所以CLR會禁止轉型,拋出異常System.InvalidCastException。
所以,PromoteEmployee應該將參數類型指定爲Employee,這樣就能夠在編譯的時候報錯了。
C#中進行類型轉換的另外一個方法是is操做符。is檢查對象是否兼容於指定類型,返回Boolean。is操做符永遠不會拋出異常。上面那個程序使用is操做符檢查一下就能夠避免拋出異常了:
public static void PromoteEmployee(Object o) { if (o is Employee) { Employee e = (Employee)o; } }
可是若是這種作,CLR實際上會檢查兩次對象類型。雖然加強了安全性,可是對性能產生了影響。所以,C#提供了as操做符來優化這種狀況:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Reflection; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace Project_1 9 { 10 internal class Employee : System.Object { 11 } 12 internal class Manager : Employee { 13 } 14 public sealed class Program:System.Object 15 { 16 public static void PromoteEmployee(Object o) { 17 Employee e = o as Employee; 18 if (e != null) 19 { 20 System.Console.WriteLine(e.GetType()); 21 } 22 } 23 24 public static void Main(string[] args) 25 { 26 Manager m = new Manager(); 27 PromoteEmployee(m); 28 DateTime newYears = new DateTime(2019, 2, 20); 29 PromoteEmployee(newYears); 30 } 31 } 32 }
在這裏,CLR檢查o是否兼容於Employee類型。若是是,as返回對同一個對象的非null引用;若是不是,返回null。as操做符只檢查一次對象類型,而if只檢查e是否爲null。
as的工做方式與強制類型轉換同樣,只是永遠不拋出異常。若是對象不能轉型,結果就是null。若是須要,請手動拋出異常。
命名空間和程序集
命名空間對相關的類型進行邏輯分組,開發人員能夠經過命名空間定位類型:
public sealed class Program { static void Main(string[] args) { System.IO.FileStream fs = new System.IO.FileStream("text.txt", System.IO.FileMode.Open); System.Text.StringBuilder sb = new System.Text.StringBuilder(); }
這樣子作太繁瑣了,能夠經過using指令簡化代碼:
using System.IO; using System.Text; namespace Program2 { public sealed class Program { static void Main(string[] args) { System.IO.FileStream fs = new FileStream("text.txt", System.IO.FileMode.Open); System.Text.StringBuilder sb = new StringBuilder(); } } }
對於編譯器,命名空間的做用就是爲類型名稱附加以句點分隔的符號,使名稱變得更長,更可能具有惟一性。C#的using指令指示編譯器嘗試爲類型名添加不一樣前綴,直到找到匹配項。可是,CLR並不知道「命名空間」,在訪問類型的時候,CLR須要知道完整的類型名稱。
檢查類型定義的時候,編譯器必須知道要在什麼程序集中檢查。編譯器掃描引用的全部程序集,在其中查找類型定義。一旦找到了正確的程序集,程序集信息和類型信息就嵌入生成的託管模塊的元數據中。爲了獲取程序集信息,必須將定義了被引用類型的程序集傳給編譯器。C#編譯器自動在MSCorLib.dll程序集中查找被引用的類型。MSCorLib.dll程序集包含全部核心Framework類庫類型的定義。
所以有一個潛在問題:有可能有多個類型在不一樣的命名空間同名。這時,只能在開發中爲類型定義提供惟一性的名稱,或者使用完整的引用。
命名空間和程序集不必定相關:同一個命名空間的類型可能在不一樣程序集出現,同一個程序集也可能出如今不一樣的命名空間。在文檔中查找類型的時候,會明確指出命名空間,以及所在程序集。
運行時的相互關係
這部份內容將解釋類型、對象、線程棧和託管堆在運行時的相互關係。此外,還將解釋調用靜態方法、實例方法和虛方法的區別。
上圖展現了已加載CLR的一個Windows進程。該進程可能有多個線程,線程建立時會分配到1MB的棧。棧空間用於向方法傳遞實參,方法內部定義的局部變量也在棧上。棧從高位內存地址向低位內存地址構建。如今線程已經執行了一些代碼,棧頂已經有一些數據了。假定線程執行的代碼要調用M1方法。
方法開始執行的時候,在線程棧上分配局部變量name的內存。
M1調用M2方法,將局部變量name做爲實參傳遞。這就形成name局部變量中的地址被壓入棧。M2方法內部使用參數變量s標識棧位置。此外,調用方法時還會將「返回地址」壓入棧。被調用的方法在結束以後應返回值該位置。
M2方法開始執行的時候,在縣城中爲length和tally分配內存。而後,M2方法內部的代碼開始執行。最終抵達return語句,形成CPU指令指針被設置成棧中的返回地址,M2的棧幀(棧幀,StackFrame,表明當前線程調用棧中的一個方法調用。執行線程的過程當中,進行的每一個方法調用都會在調用棧中建立並壓入一個StackFrame)展開,恢復成下圖的樣子。以後,M1繼續執行M2調用以後的代碼,M1的棧幀將準確反映M1須要的狀態。
最終,M1會返回到他的使用者。這一樣經過將CPU的指令指針設置成返回地址來實現,M1的棧幀展開。以後,調用M1的方法繼續執行M1調用以後的代碼,那個方法的棧幀將準確反映它須要的狀態。
如今,來看這組代碼:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Program3 8 { 9 internal class Employee { 10 public Int32 GetYearsEmployed() { 11 return 10; 12 } 13 public virtual String GetProgressReport() { 14 return "Employee Haha"; 15 } 16 public static Employee Lookup(String name) { 17 Employee Joe = new Employee(); 18 return Joe; 19 } 20 } 21 internal sealed class Manager : Employee { 22 public override string GetProgressReport() 23 { 24 return "Manager Haha"; 25 } 26 } 27 28 class Program 29 { 30 31 static void Main(string[] args) 32 { 33 M3(); 34 } 35 36 private static void M3() 37 { 38 Employee e; 39 Int32 year; 40 e = new Manager(); 41 e = Employee.Lookup("Joe"); 42 year = e.GetYearsEmployed(); 43 System.Console.WriteLine(e.GetProgressReport()); 44 } 45 } 46 }
JIT編譯器將M3的IL代碼換成本機的CPU指令時,會注意到M3內部引用的全部類型。這時CLR要確認定義了這些類型的全部程序集已經加載。而後,利用程序集的元數據,CLR提取與這些類型有關的信息,建立一些數據結構來表示類型自己。下圖展現了爲Employee和Manager類型對象使用的數據結構。
堆上的全部對象都包含兩個額外成員:類型對象指針和同步塊索引。如圖所示,Employee和Manager都有這兩個成員。定義類型時,可在類型內部定義靜態數據字段。爲這些靜態數據字段提供支援的字節在類型對象自身中分配。每一個類型對象最後都包含一個方法表。在方法表中,類型定義的每一個方法都有對應的記錄項。
當CLR確認方法須要的全部類型對象都已建立,M3的代碼已經編譯以後,就容許線程執行M3的本機代碼。M3的序幕代碼執行時必須在線程棧中爲局部變量分配內存,以下圖所示:
CLR自動將全部的局部變量初始化爲null或者0.然而,若是代碼試圖訪問還沒有顯示初始化的局部變量,C#會報告錯誤消息。
而後,M3執行代碼構造了一個Manager對象。這使得在託管堆建立Manager的一個實例