在學習C#的過程中,第一個遇到的就是類型,大多數同學都覺得很簡單,然后一帶而過。但是回過頭來看看,類型中還是有很多需要我們注意的問題。本系列文章是以《CLR via C#》為基礎,再加上一些其他人寫的博客以及我自己的經驗和感悟寫的,希望可以由淺入深,慢慢的把類型需要注意的問題講清講透,使我也在寫博客的過程中學習到更多。
好了,廢話不多說,我們來開始第一節。這一節是類型的最基礎問題,可能里面的點大家都很熟,但是這些又是類型的基礎,需要弄清楚。
一、System.Object
C#中所有的類型都是繼承於System.Object類型的,所以在講其他的內容之前我們先看看System.Object類型的代碼:
// 摘要: // 支持 .NET Framework 類層次結構中的所有類,並為派生類提供低級別服務。這是 .NET Framework 中所有類的最終基類;它是類型層次結構的根。 [Serializable] [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] public class Object { // 摘要: // 初始化 System.Object 類的新實例。 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] public Object(); // 摘要: // 確定指定的 System.Object 是否等於當前的 System.Object。 // // 參數: // obj: // 與當前的 System.Object 進行比較的 System.Object。 // // 返回結果: // 如果指定的 System.Object 等於當前的 System.Object,則為 true;否則為 false。 [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public virtual bool Equals(object obj); // // 摘要: // 確定是否將指定的 System.Object 實例視為相等。 // // 參數: // objA: // 要比較的第一個 System.Object。 // // objB: // 要比較的第二個 System.Object。 // // 返回結果: // 如果認為對象相等,則為 true;否則為 false。 [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public static bool Equals(object objA, object objB); // // 摘要: // 用作特定類型的哈希函數。 // // 返回結果: // 當前 System.Object 的哈希代碼。 [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public virtual int GetHashCode(); // // 摘要: // 獲取當前實例的 System.Type。 // // 返回結果: // System.Type 實例,表示當前實例的確切運行時類型。 [SecuritySafeCritical] public Type GetType(); // // 摘要: // 創建當前 System.Object 的淺表副本。 // // 返回結果: // 當前 System.Object 的淺表副本。 [SecuritySafeCritical] protected object MemberwiseClone(); // // 摘要: // 確定指定的 System.Object 實例是否是相同的實例。 // // 參數: // objA: // 要比較的第一個 System.Object。 // // objB: // 要比較的第二個 System.Object。 // // 返回結果: // 如果 objA 是與 objB 相同的實例,或者如果二者都為空引用,則為 true;否則為 false。 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public static bool ReferenceEquals(object objA, object objB); // // 摘要: // 返回表示當前 System.Object 的 System.String。 // // 返回結果: // System.String,表示當前的 System.Object。 public virtual string ToString(); }
C#中使用new操作符來創建一個對象,如類Animal定義為:
public class Animal { private string _name; public string Name { get; set; } public Animal() { //Do some thing } public Animal(string name) { _name = name; } }
(1)、首先CLR需要知道這個實例化的對象需要多大的內存存儲,所以會計算該類型及其所有基類(一直到System.Object)中定義的所有實例字段需要的字節數。另外因為在堆上存儲的所有對象都需要額外的兩個成員——類型對象指針(type object pointer)以及同步塊索引(sync block index),所以也要把他們的字節數包括在內。另外順便說一句,這額外的兩個成員是由CLR來管理對象使用的。
總之一句話,第一步是計算要分配多大內存給這個對象的。
(2)、從托管堆中分配指定大小的內存。
(3)、初始化對象額外的兩個成員——類型對象指針和同步塊索引
(4)、調用類型的構造函數(也有的地方翻譯為“構造器”,我習慣叫做構造函數,下同),傳入在new的調用中指定的參數。構造函數的調用時一級一級往下的,也就是說要從Object類的構造函數調用開始,一級一級往下調用,每個構造函數都會初始化本類型中定義的實例字段。在本例中,他會先調用System.Object.Object(),然后調用 Animal.Animal(string name)。
tips:我們深入的了解下構造函數的執行順序。所以我們繼續構造一個Dog類,該類繼承與Animal:
public
class Dog : Animal
如果執行Dog dogModel1 = new Dog();,則構造函數的執行順序為:
{ public Dog() { //Do some thing } public Dog(string name) { //_name = name; } public Dog(string name, int age) { //do some thing } }
System.Object.Object(); Animal.Animal(); Dog.Dog();
如果執行Dog dogModel1 = new Dog("Keli");,則構造函數的執行順序為:
System.Object.Object(); Animal.Animal(); Dog.Dog(string name);
如果執行Dog dogModel1 = new Dog("Keli", 15);,則構造函數的執行順序為:
System.Object.Object(); Animal.Animal(); Dog.Dog(string name, int age);
如果Animal只有Animal.Animal(string name)構造函數的話,那么在Dog中的構造函數就會報錯,而且這是在編譯階段就會被發現的錯誤,如下圖:
![]()
如果你想在調用Dog的構造函數Dog(string name)時調用父類Animal的Animal(String name),則需要在構造函數中做如下修改:
public Dog(string name) : base(name) { //_name = name; }
System.Object.Object(); Animal.Animal(string name); Dog.Dog(string name);
同樣,也可以在調用構造函數時先調用該類的另一個構造函數,使用的是this關鍵字:
public Dog(string name, int age) : this(name) { //do some thing }
這種情況下,執行Dog dogModel1 = new Dog("Keli");時,調用的順序是這樣的:
System.Object.Object(); Animal.Animal(string name); Dog.Dog(string name); Dog.Dog(string name, int age);
請不要將構造函數的執行順序刻意的寫成死循環,如:
public Dog(string name) : this(name, 14) { //_name = name; } public Dog(string name, int age) : this(name) { //do some thing }
編譯時不會報錯,而在運行時會報SystemOverFlowException錯誤,報錯信息如下:
![]() |
二、類型轉換
這里的類型轉換並非基元類型的轉換,而是基礎的那種類型轉換。
類型安全性是CLR最重要的特性之一,所以在類型轉換中CLR也需要確認轉換安全才能隱式轉換,而在不確認是否安全的情況下,需要程序員強制轉換。這種情況下一切后果由程序員負責。
其實總結起來很簡單,就是所有對象向自己的基類型轉換是安全的,所以可以直接隱式轉換;而向自己的某個派生類型轉換時,轉換可能失敗,所以需要開發人員顯示轉換。
例如:
Object o = new Animal(); Animal a = (Animal)o; Dog d = (Dog)o;
第二種情況是Object類型轉換為自己的派生類Animal,所以需要強制轉換,但是由於o實際指向的是一個Animal對象,所以這樣的轉換也是安全的,所以這一步不會報錯;
第三中情況是將Object類型轉換為派生類的派生類,也需要強制轉換,但是由於o指向的是一個Animal對象,而Animal類型是Dog類型的父類,不能轉換,所以這一步在運行時會報錯。
C#中的 as 和 is操作符
is操作符是檢查一個對象是否兼容於指定的類型,如果是則返回true,否則返回false。實際上也就是能不能成功轉換與指定的類型。
例如:
bool b1 = o
is Animal;
bool b2 = o is Dog;
上面的例子的b1 = true; b2 = false;
bool b2 = o is Dog;
所以is操作符一般用在判定一個類型是否能安全的轉換成另一個類型的場合,例如:
if (o
is Animal)
{
Animal a2 = (Animal)o;
}
但是這樣寫有一個弊端,那就是在if中會驗證一次o是否可以轉換為Animal類型,在實際轉換中又會轉換一次,而類型轉換時對性能有一定影響的,所以我們可以用as操作符來簡化這段代碼,既安全的轉換了類型,又節省性能。
{
Animal a2 = (Animal)o;
}
as是將一個對象從一種類型轉換為另一種類型,如果成功轉換,則將對象轉為目標類型,否則,as將返回null。as的工作方式和強制類型轉換時一樣的,但是永遠不會拋出異常,因為出現異常會被抑制,結果為null。
這樣,上面的例子就可以轉換為:
上面的代碼僅僅會類型轉換一次,即as,而在if語句中,僅僅是驗證實例 a 是否為null,這個檢查消耗的性能比類型轉換要小得多。
a = o as Animal; if (a != null) { //Do something }
三、命名空間和程序集
1、命名空間(namespace):
命名空間是用於對相關的類型進行邏輯性分組,開發人員使用命名空間來方便的定位一個類型。
比如你電腦上有很多文檔,你要建立一個目錄來邏輯性的吧這些文檔分成很多個類,比如“學習資料”、“電影”等,這種分成的這些類就是相當於我們這里說的命名空間,以后找某個電影文件就可以去“電影”類去找。
當我們在使用某個類型的時候是需要這個類型的全稱的,也就是需要將命名空間帶上,但是C#編譯器為了方便開發人員,應用了using指令來簡化我們的代碼。相信這個大家都懂的,我就不多說了。C#編譯器在編譯時遇到一個類型,會嘗試着在前面加上using中的命名空間來依次匹配。
如果兩個命名空間中有相同名稱的命名類型,此時在使用該類型時就需要將其命名空間帶上來區分。
也可以使用命名空間別名來解決這個問題,例如NameSpace1 和NameSpace2 都有Class1這個類型,當需要使用時可以用如下方法:
using np1 = NameSpace1; using np2 = NameSpace2; np1.Class1 = null; np2.Class1 = null;
還有一種極端情況,就是命名空間名稱和引用的類型名稱完全相同,此時就需要使用外部別名功能,具體的使用可參見msdn:
http://msdn.microsoft.com/zh-cn/library/ms173212.aspx
2、程序集(assembly):
程序集是一個或多個模塊/資源文件的邏輯性分組,他是重用,安全性以及版本控制的最小單元。由於程序集不是本系列討論的重點,暫時僅僅提這么多,可以參考《CLR via C#》中文第三版的P6-P10。
3、命名空間和程序集的關系:
很多童鞋在糾結這個問題,我想說這個真的沒有必要比較,他們幾乎沒有任何關系。一個程序集可以包含幾個命名空間,同時一個命名空間也可以分布在幾個程序集中。
暫且可以這樣理解:剛才講命名空間的時候舉了一個文檔的例子,如果說命名空間是指這些文檔按照邏輯性分出來的類的話,那么程序集就是這些文檔的物理分類,比如C盤、D盤等。
好了,類型基礎就總結這么多,回頭看看大部分都是《CLR via C#》中的內容,權當是這本書的復習吧。最后再次推薦下這本書,寫的很好。