如果說C#是CLR特邀演員陣容之一,那類型class絕對是C#的演繹/演藝大師、不朽靈魂!它不僅演繹了C#的豪放,也演藝了C#的柔美。時而恢弘、時而細膩。一切類型皆System.Object。這一章將向您解釋類型的生成,類型的演繹轉換及類型設計的必要元素、類型成員的內存分配,當然還有裝箱及拆箱操作。
C#里,所有事物都是被按類划分,一切事物皆對象,任何對象都會對應為某一類。所有被划分出來的類都被歸為一個超級“類”,即System.Object。Object被定義在命名空間System 門下,居住於程序集mscorlib.dll小區。類型Object是一個比較開放的類,成員不多,只公開了四個方法:
public virtual bool Equals(object obj);
確定指定的 System.Object 是否等於當前的 System.Object。
public static bool Equals(object objA, object objB);
判斷兩個給定的對象是否相等,其實就是判斷兩個對象是否包含相同的值。
public virtual int GetHashCode();
返回當前對象的值的哈希碼。在有些集合中判斷兩個對象是否相等的條件就判斷兩個對象的哈希碼是否相同,所以,如果繼承object類的子類中如果重寫了Equals方法,則必須重寫此方法。
public Type GetType();
返回當前實例的運行時類型
public static bool ReferenceEquals(object objA, object objB);
與Equals方法不同,它是判斷兩個對象是否指向同一個對象。
public virtual string ToString();
默認獲取當前對象的帶有命名空間的完全限定名。在子類中調用時,它返回對象的字符串表示。
CLR要求所有的定義類型的祖先(先人板板)必需是System.object。.NET Framework里明確說明所有的類型最終都是從object類型派生。我們所定義的任何類型都是默認從object類派生,而不用明確指定其繼承關系,編譯器會自動識別並做出相應處理。如下我們定義了一個Person 類,通過IL可以看出類型的實際繼承關系,如圖:
(1)類型轉換
既然有了類型繼承,那一定存在多個派生類型同時繼承於同一個基類,並且有可能發生將對象從一個類型轉換到另一個類型情形。CLR是一個堅持類型安全的平台,由於所有類型都是繼承於object 類,也就得到了基類object的GetType()方法。所以調用對象的GetType()方法就可以知道該對象是什么類型。CLR允許將一個對象轉換為它的任何基類型,這種轉換是自然隱式的;也可以將一個對象轉換為它的某一派生類型,但是這種轉換是要求顯示轉換,在轉換過程中,CLR要做類型安全檢查驗證,對於不能向上轉換的,將拋出異常。如下代碼:

public class Person { public string Name { get; set; } } public class Teacher : Person { public int RootID { get; set; } } class Program { static void Main(string[] args) { Teacher teacher = new Teacher(); Person p = teacher; //轉換正常 p.Name = "abc"; teacher = (Teacher)p; //轉換正常 //以下轉換將會拋出異常 Person person = new Person(); teacher = (Teacher)person; } }
(2)類型轉換操作符:as 和 is
C#提供了as和is這兩個操作符方便我們進行類型轉換。
as是將一個對象從一種類型轉換為另一種類型,如果成功轉換,則將對象轉為目標類型,否則,as將返回null。如下:
Teacher teacher1 = new Teacher(); //轉換正常 Person p1 = teacher1 as Person; if (p1 != null) { //其他操作 } Person pTemp = new Person(); //類型檢查不通過,t將為null Teacher t = pTemp as Teacher;
is操作符是判斷一個對象是否是某種類型,如果是則返回true,否則返回false,在我們使用 as轉換前,可以先使用is進行類型確認,如果該對象是目標類型則轉,否則不進行轉換。如下:
Teacher teacherB; if (pTemp is Teacher) { teacherB = pTemp as Teacher; }
由於is和as都會進行對對象進行類型安全檢查,所以上面的代碼段將執行兩次安全驗證。在一個操作過程中執行兩次安全驗證,似乎降低了性能,所以我們可以采用如下方法,只進行一次安全驗證,同樣達到了目的。
Teacher teacher1 = new Teacher(); //轉換正常 Person p1 = teacher1 as Person; if (p1 != null) { //其他操作 }
這段代碼只進行了一次類型安全檢查,從而提高了性能。Is操作符只是看上去更語意更一目了然,可以根據自己的喜好選擇。
在進行某些類型算術運算過程中,很可能導致溢出!比如精度丟失:將int64位數值轉為int32位,我們將在下面會講到這一點。
C#語言支持基元類型。
基元類型:c#編譯器直接支持的數據類型為基元類型。如int、byte、char、string等。這些基元類型與.NET Framework類庫都有一一對應。比如:int對應System.Int32、string對應System.String等。更多的對應關系,可查看 MSDN 。
基元類型的算術運算可能導致溢出,所以要注意。如下:
int a = 2147483647;
int b = a + 1; //-2147483648
很顯然b的值已經超出我們的正常期望,這種溢出是偷偷地發生,如果我們希望這種情況發下,當發生溢出時,我們想捕獲這咱“異常”,可以使用check操作符在運算過程中進行檢查,如下:
int c = checked(a + 1);
當發生溢出時,會拋出OverflowException(算術運算導致溢出)異常。當然我們也可以主觀地不對這種溢出進行安全驗證,可以使用unchecked。如下:
int d = unchecked(a + 1);
同時c#還支持checked和unchecked語句塊,如下:
checked { int e = a + 1; }
unchecked { int f = a + 1; }
另外,CLR支持兩種數據類型:引用類型和值類型。其實CLR只有一種類型那就是引用類型,因為所有的派生類型都是從System.Object類派生,但是由於CLR本身的運行機制要求引用類型分配在托管堆上,既然內存從托管堆分配,每次分配時可能會導致垃圾回收,而垃圾回收會帶來性能損傷。同時由於類型元數據結構的問題,在給對象分配內存時,會有一些額外的數據成員,比如:對象指針、同步塊索引等。這些明顯占據了一部分內存。所以CLR又提出了值類型這一數據類型,事實上值類型也是從System.Object派生而來,如結構類型。而結構類型派生於System.ValueType,System.ValueType又派生於System.Object。所以,凡是派生於System.ValueType的類型都稱為值類型。
值類型
值類型又分為:
簡單類型:Int32、Char、Boolean等,這些類型可以聲明變量、方法等。
結構類型:struct,void等,這些類型聲明常量、字段、方法等。
枚舉類型:enum。聲明一個由一指定常量集合組成的類型。
值類型的實例一般在線程棧上分配而並非托管堆,聲明一個值類型變量后,變量中包含着實例本身的字段值,垃圾回收器不對值類型進行處理,通常在離開它們作用區范圍時,這些實例將自動銷毀。所有用struct聲明的類型都是值類型。當然可以將一個特定的值類型送給引用類型,這就是裝箱,下面會講到。
引用類型
引用類型不存儲實際數據值,只是存儲它們所代表對象的地址引用。當聲明一個引用類型后,它會在線程棧上保存該類型對象在托管堆中的引用地址,同時在托管堆中分配該對象的類型對象指針、同步塊索引,實際數據等。所有class聲明的類型都是引用類型。
由於引用類型的變量保存着堆上的一個對象的地址,所以在定義一個引用類型變量時它默認為null,當使用一個為null的引用類型變量,會拋出空引用的異常。而值類型的變量保存着實際值,所以它永遠不會拋出空引用。
內存分配:
通過上面講解,我們已經知道,值類型是分配在線程棧中而不是在托管堆中,它是不受垃圾回收器管理。Object是所有類型的最終基類,所以按道理它可以接收任何類型的數據,包括值類型,比如:int a=1; object obj=a; obj要想接收a,則必須對值類型a進行類型轉換成object 類型,這個過程就是裝箱。裝箱會有以下操作:
先在托管中分配內存,然后將值類型的字段復制到新分配的內存堆中,最后返回新對象的地址。比如以下方法:
public void Test() { //裝箱 int a = 1; object obj = a; //拆箱 object obj2 = 2; int b = (int)obj2; }
拆箱的過程與裝箱相反,但不並完全是將操步驟相反。拆箱是將已裝箱obj2中的各個字段的值復制到線程棧上,先獲取已裝箱變量中的所有字段地址的引用,然后是將每個地址引用的值復制到線程棧上的值類型的實例中。查看上面方法的IL可以看出真實的操作裝箱、拆箱步驟:
通過以上的描述可以看出,裝箱是要新分配托管堆內存,而這個過程有可能進行垃圾回收,這就帶來了性能問題。所以在我們寫代碼的過程中,盡可能地避免裝箱和拆箱的操作。
每個類型可以定義N個預定義的成員,這些成員可能包含如下這些:
常量指出數據值恆定不變的一個符號,使用const定義,例如:
private const decimal PAI = 3.14M;
字段一個只讀、可讀/寫的數據值。如果是靜態的,它代表類的一狀態數據;如果實例的,它代表對象的狀態數據。例如:
private int count = 100;
實例構造器
類型構造器
方法對類型或對象狀態數據操作的一個過程實現。如果是靜態的,它對類的狀態數據進行操作;如果是非靜態的,它對類對象的狀態數據進行操作。
操作符重載
轉換操作符
屬性:它可以像方法一樣操作類或對象的狀態數據,但看上去卻像字段一樣的書寫方式。如果是靜態的,它對類的狀態數據進行操作;如果是非靜態的,它對對象的狀態數據進行操作。通過IL,我們可以看到它是有方法實現的。如下,我們聲明了一個int型的Age和一個string型的Name:
事件可向N個方法發送通知。如果是靜態的,它可向N個靜態方法發送通知;如果是非靜態的,它可向N個實例方法發送通知。事件常常用一個委托鏈來維護所有已登記的方法。
類型可以在一個類的內部嵌套定義一個類型。
無論是類型還是類型成員,都可以指定其可訪問性。例如我們可以給一個類型的可訪問性指定為public或是internal。如果被指定為internal,則此類只可被在它所在的程序集的其他成員訪問。如果想讓該類型被其他程序集訪問,可使用一個名為System.Runtime.ComiplerServices.InternalsVisibleTo的屬性來達到目的。詳細信息可查詢MSDN。
在C#中,可以使用如下可訪問性定義:public、 private、 protected 和internal。