一:所有類型都是從System.Objcet派生
“運行時”要求每個類型最終都是從system.Object類型派生。換言之,以下兩個類型定義是完全一致的:
//隱式派生自Object
class Employee{
…
}
//顯式派生Object
class Employee: System.Object{
…
}
由於所有類型都是派生自Objcet類型,所以可以保證的是每個類型的每個對象都有一組最基本的方法。Equals(),GetHashCode(),ToString(),GetType()。
CLR(公共語言運行時)要求所有對象都用new操作符來創建。下面代碼演示如何創建一個Employee對象:
Employee e=new Employee(“ConstructorParam1”);
以下是New 操作所做的事情:
1.它計算類型及其所有基類型(直到Objcet)中定義的所有實例字段需要的字符數。堆上每個對象都需要一些額外的成員——即“類型對象指針”和“同步塊索引”。這些成員由CLR用於管理對象。這些額外成員的字節數會計入對象的大小。
2.它從托管堆中分配指定類型要求的字節數,從而分配對象的內存,分配的所有字節數都設為0.
3.它初始化對象的“類型對象指針”和“同步塊索引”。
4.調用類型的實例構造器,向其傳入在對new的調用中指定的任何實際參數(上例就是“ConstructorParam1”)。大多數編輯器都在構造器中自動生成代碼來調用一個基類構造器。每個類型的構造器在調用時,都要負責初始化有這個類型定義的實例字段。最終調用的是System.Objcet的構造器,該構造器只是簡單地返回。不會做任何事情。
new執行了所有這些操作后,會返回指向新建對象一個引用(或指針)。在前面的示例代碼中,這個引用會保存到變量e中。
總結:有關層次結構下的類是在定義自己的構造函數時,會發生什么事情呢。實際上,在創建派生類的實例時,會有多個構造函數起到作用。要實例化的類的構造函數本身不能初始化類,還必須調用基類的構造函數。編譯器首先試圖找到實例化的類的構造函數。上面示例中的構造函數Employee。它首先要找到它的基類的構造函數Sytem.Objcet運行默認的構造函數,誠然。它什么都沒做,它也沒有基類。所以它執行完后,將控制權移交給構造函數Employee。然后該構造函數做它應該做的事情,初始化該類下的各個成員。這樣的流程最后的結果就是派生類的實例可以調用基類下的成員、方法。所以我們也明白,基類的構造函數總是最先調用的。
二:類型轉換
CLR最重要的特性之一就是類型安全性。在運行時,CLR總是知道一個對象是什么類型。調用GetType()。總是知道一個對象確切的類型是什么。由於這個方法是非虛方法,所以一個類型不可能偽裝成另一個類型。例如,Employee類型不可以重寫GetType()。
開發人員經常需要將一個類型轉換為另外一個類型。CLR允許將一個對象轉換為它的類型或者它的任何基類型。C#中,將一個對象轉換為它的任何基類型都被認為是一種安全的隱式轉換。然而,將對象轉換為它的派生類型時,c#要求這樣的轉換只能進行顯示轉換。因為這樣的轉換存在運行時失敗。
總結:1.c#中,將一對象轉換為基類時,不需要任何特殊操作即可完成--隱式轉換過。2.將一對象轉換為它的派生類時,必須顯示轉換。運行時存在失敗。
下面的代碼將演示向基類和派生類型的轉換:
//該類型隱式派生自System.Object
class Employee{
…
}
static void Main(string[] args) { //不需要轉類型,因為new返回一個Employee對象。 Object o = new Employee(); //需要轉類型,因為Employee派生自System.Objcet //這里進行顯示轉換 Employee e = (Employee)o; }
所以該代碼在編譯時不會出錯。在顯示轉換那里。在運行時,CLR核實到o引用的是一個Employee對象。所以該顯示轉換也正好允許執行。
而下面的就會在運行時候報錯了。
static void Main(string[] args) { //不需要轉類型,?因為new返回一個Employee對象。 Object o = new Employee(); //需要轉類型,因為Employee派生自System.Objcet
//這里進行顯示轉換 Object d = new DateTime(); //運行時,報錯 Employee e = (Employee)d; }
所以如果CLR允許這樣的轉型,就無類型安全可言了。
使用C#的is和as操作符來轉型
在C#語言中進行類型轉換的另一種方式是使用is操作符,is檢查一個對象是否兼容於指定的類型,並返回一個bool類型。注意:is操作符永遠不會拋出異常。
Object o = new Employee(); bool b1 = (o is object);//true bool b2 = (o is Employee);//true
如果對象引用的是null,is操作符總是返回false,因為沒有可檢查其類型的對象。
is操作符通常像下面那樣使用:
if (o is Employee) { Employee e = (Employee)o; }
在這段代碼中CLR實際會檢測兩次對象的類型。首先核實o是否兼容於Employee,然后if內部轉型時CLR會再次核實o是否引用一個Employee對象。
這樣無疑會對性能造成一定的影響,這是因為CLR首先必須判斷變量o引用的對象的實際類型。然后,CLR遍歷繼承層次結構。用每個基類型去核對指定的類型。且由於這是一個比較常用的編程模式,所以C#便引入了as關鍵字。目的就是簡化寫法。提升性能。
Employee e = o as Employee; if (e != null) { //... }
所以看到這里似乎明白了一些事情。
總結:as操作符的工作方式和強制類型轉換一樣。只是它不會拋出異常,相反,如果不能轉型,將返回一個NULL。
最后來一個小測試
internal class B { }//基類 internal class D : B { }//派生類
針對每一行代碼都用小勾對注明該代碼是成功、還是編譯時錯誤還是運行時錯誤。下圖所示。

編程語言的基元類型
某些數據類型如此常用,以至於許多編譯器允許代碼以簡化的方式來操作它們。例如。可是使用以下語法來分配一個整數:
Int32 a = new System.Int32();
不過它相對於常用,且比較繁瑣復雜,所以c#允許換用如下的語法:
int b = 0;
這種語法不僅增強了代碼的可讀性,而且生成的IL代碼也是完全一致的。這種編譯器直接支持的數據類型稱為:基元類型。基元類型是直接映射到Framework類庫中(FCL)存在的類型。下面4行代碼都可以正確編譯。並生成相同的IL代碼:
int a = 0;//最方便的語法 System.Int32 b = 0;//方便的語法 int c = new int();//不方便的語法 Int32 d = new System.Int32();//最不方便的語法
當然FCL在c#中對應的基於類型有很多。就不往下舉例了。
一:引用類型和值類型
CLR支持兩種類型:引用類型和值類型。引用類型是從托管堆上分配的,c#的new運算符會返回對象的內存地址——也就是指向該對象數據的內存地址。引用類型有如下特性:
- 內存必須從托管堆上分配。
- 堆上分配的每個對象都有額外的成員,這些成員必須初始化。
- 對象中的其他字節總是設為零。
- 從托管堆上分配一個對象時,可能強制執行一次垃圾收集操作。
如果所有類型都是引用類型,那么應用程序的性能明顯下降。如果每次使用int32類型都進行一次內存分配,性能將會大受影響,畢竟int類型頻率用的非常高。所以。CLR提供了一種名為“值類型”的輕量級。值類型的實例一般在線程棧上分配。在代表值類型的實例變量中,並不包含一個指向實例的指針。相反,變量中包含了實例本身的字段。由於變量已經包含了實例的一個字段,所以為了操作實例中的字段,不再需要提領一個指針。值類型的實例不受垃圾回收的控制。一下代碼演示引類型和值類型的區別:
SomeRef r1 = new SomeRef();//在堆上分配 SomeVal v1 = new SomeVal();//在棧上分配 r1.x = 5; //提領指針 v1.x = 5; //在棧上修改 Console.WriteLine("r1.x的值:" + r1.x); Console.WriteLine("v1.x的值:" + v1.x); Console.WriteLine("-----------"); SomeRef r2 = r1; //復制指針 SomeVal v2 = v1; //復制並重新分配成員 r1.x = 8; v1.x = 8; Console.WriteLine("r1.x的值:" + r1.x);//8 Console.WriteLine("r2.x的值:" + r2.x);//8 Console.WriteLine("v1.x的值:" + v1.x);//8 Console.WriteLine("v2.x的值:" + v2.x);//5 Console.ReadLine();
看看運行效果圖:

在上述代碼中,SomeVal類型是用struct來申明的。而不是常用的class。在C#中,用struct來聲明的類型是值類型,用class來聲明的類型是引用類型。
上述代碼中有這樣的一行:
SomeVal v1=new SomeVal();
因為這一行代碼的寫法,似乎是要在托管堆上分配一個SomeVal的實例。實際上根據前面提到的類型安全性c#編譯器知道SomeVal是一個值類型。所以會在線程棧上分配一個SomeVal的實例,C#還會確保值類型中的所有字段都初始化為零。所以上述代碼還可以這樣寫:
SomeVal v1;
這行代碼在IL代碼中發現也會在線程棧上分配一個實例。並且將字段初始化為零。唯一的區別在於new操作符,C#會認為實例已經完成初始化,一下代碼更能清楚的說明問題:
SomeVal v1=new SomeVal();
int a=v1.x;
//下面不編譯失敗。
SomeVal v1;
int a=v1.x;//使用了未復制的字段x。
設計自己的字段時,要考慮是否應該講一個類型定義為值類型.而不是定義為引用類型。某些時候值類型能提供更好的性能。具體來說,滿足以下條件,可以考慮使用值類型。
- 類型具有基元類型的行為。換言之,這是一個十分簡單的類型,其中沒有成員會修改類型的任何實例字段。若一個類型沒有提供會更改其字段的成員。就是說該類型不可變。對於這樣的類型來說,我們都建議將他標記為readonly。
- 類型不需要從其他任何類型繼承。
- 類型也不會派生出其他任何類型。
- 類型的實例較小(約為16個字節);類型的實例較大的話,不作為方法的實參傳遞,也不從方法返回.
因為類型實例的大小在傳值時候會有性能損失,值類型的實例在傳值時,是對值類型中的實例進行復制.下面看看值類型和引用類型的區別.
- 值類型是從System.ValueType派生的。該類型提供了與System.Object定義相同的方法。然而,System.ValueType重寫了Equals方法,能在兩個對象的字段完全匹配的前提下放回true。除此之外,System.ValueType還重寫了GetHashCode方法,生成哈希嗎的時候,這個重寫的方法所用的算法會將對象的實例字段中的值考慮在內,由於這個默認實現存在性能問題,所以在定義自己的值類型時,應該重寫Equal和GetHashCode方法,並提供他們的顯示實現。
- 由於不能將一個值類型作為基類型定義一個新的類型或者一個新的引用類型,所以不應該在值類型中引入任何新的虛方法。所有方法都不能使抽象的,而且所有方法都隱式的為密封方法。
- 引用類型的變量包含的是堆上的一個對象的地址。默認情況下,在創建一個引用類型的變量時,它被初始化為null,表明引用類型的變量當前不指向一個有效的對象。試圖使用一個null的引用變量,會拋出一個異常,相反,值類型的變量總是包含其基礎類型的一個值,而且值類型的所有成員都初始化為零。由於值類型的變量不是指針,所以在訪問一個值類型時,不可能拋出異常,CLR確實提供了一個特殊的特性,能為值類型添加“可空性”。這個特性稱為可空類型。
- 將一個值類型的變量賦給另一個值類型的變量時,會執行一次逐字段的復制。將引用類型的變量賦給另一個引用類型的變量時候,只是復制內存地址。(所以兩個或多個引用類型的變量能引用堆中的同一對象,對這個對象的操作將會影響別的變量引用的對象,而值類型的則不會。)
- 由於未裝箱的值類型不再堆上分配,所以一旦定義了該類型的一個實例的方法不再處於活動狀態,為他們分配的存儲就會被釋放。這意味着值類型的實例在其內存被回收時,不會通過finalize方法接受到一個通知。
二:值類型的裝箱和拆箱
值類型是比引用類型更“輕型”的一種類型,因為他們不作為對象在托管堆中分配,不會被垃圾回收,也不通過指針來引用。但是在許多情況下,都需要獲取對值類型的一個實例的引用。例如,假定要創建一個ArrayList對象來容納一個Point結構,那么代碼如下:
/// <summary> /// 聲明一個值類型 /// </summary> struct Point { public int x, y; } public sealed class Program { public static void Main() { ArrayList a = new ArrayList(); Point p; for (var i = 0; i < 10; i++) { p.x=p.y=i; a.Add(p);//對值類型的P對象進行裝箱操作,並將引用添加到ArrayList中 } } }
每一次循環迭代都會初始化一個Point,然后被存儲到ArrayList當中,那么ArrayList中到底存儲的是什么呢,是Point結構,還是它的地址。那么先來看看ArrayList對象的Add方法是如何的實現。Add方法的實現如下:
public virtual Int32 Add(Objcet value);
可以看到Add方法需要一個Object類型的參數,換言之,需要獲取托管堆上的一個對象的引用作為參數。但是在此之前Point是一個值類型。所以為了代碼正常的工作,需要對Point類型轉換為一個在堆中托管的對象,而且需要獲取到它的引用。
為了將一個值類型轉換為一個引用類型,需要使用一個名為裝箱的機制。下面探討下對值類型的對象是如何進行裝箱操作的。
1.在托管堆中分配好內存,分配的內存量是值類型的各個字段需要的內存量加上托管堆上的所有對象都存在的兩個額外成員(類型對象指針和同步塊索引)的內存量。
2.值類型的字段復制到新分配的堆內存。
3.返回對象的地址。現在這個地址是對一個對象的引用,值類型現在是一個引用類型。(這里不是很懂,其實我想明白下此刻線程棧上的變化。)
那么緊接其后,看看拆箱是如何進行的。
Point p=(Point)a[0];
現在要獲取ArrayList中的元素0包含的引用,並試圖將其放到一個Point值類型的實例P中,為了做到這一點,包含在已裝箱Point對象中的所有字段都必須復制到值類型的變量P中,后者在線程棧上,CLR份兩步完成這些操作,第一步是獲取到已裝箱的Point對象中的各個Point字段的地址。這個過程稱為拆箱。第二步是將這些字段包含的值從堆中復制到基於棧的值類型的實例中。
拆箱不是直接將裝箱過程倒過來。拆箱的代價比裝箱低得多,拆箱其實就是獲取一個指針的過程,該指針指向包含在一個對象中的原始值類型。事實上,指針指向的是已經裝箱實例中的未裝箱部分。所以和裝箱不同,拆箱不要求在內存中復制任何字節。
三:dynamic動態類型
c#是一種類型安全的編程語言,這意味着所有表達式都解析成某個類型的一個實例,在編譯器生成的代碼過程中,只會執行這個類型來說有效的操作。和非類型安全語言相比,類型安全的語言的優勢在於:程序員會犯很多錯誤能在編譯時檢測到,確保代碼在你嘗試執行它之前是正確的。但是在許多時候,程序仍需要在運行時才知道它的信息。為此,c#編譯器允許將一個表達式的類型標記為dynamic。還可以講一個表達式的結果放到一個變量中,並將變量的類型標記為dynamic,然后使用這個表達式調用一個成員,比如字段、索引器、方法等。下面代碼演示dynamic 的操作:
internal static class DynamicDemo { public static void Main() { for (var i = 0; i < 2; i++) { dynamic arg = (i == 0) ? (dynamic)5 : (dynamic)"A"; dynamic result = Plus(arg); } } private static dynamic Plus(dynamic arg) { return arg + arg;} private static void M(int i) { Console.WriteLine("M(Int32):" + i); } private static void M(string i) { Console.WriteLine("M(String):" + i); } }
那么執行時的結果:
M(Int32):10;
M(String):AA;
在字段、方法參數、返回類型指定為dynamic的前提下,編譯器會將類型轉換為System.Objcet,並在元數據中向字段、方法參數或返回類型應用System.Runtime.CompilerServices.DynamicAttribute的一個實例。為泛型、結構、接口、委托或方法指定泛型類型實參時,也可以使用dynamic。任何表達式都能隱式的轉換為dynamic。因為所有的表達式都是最終都會生成Objcet類型。前面提到過正常情況下,編譯不允許將一個表達式從Objcet隱式轉換為其他類型;必須使用顯示轉換,然而,編譯器允許使用隱式轉換語法將一個表達式從dynamic類型轉換為其他類型:
object o1 = 123;//從int抓換為objcet(裝箱) int i = o1;//Error:無法將類型“objcet”隱式轉換為int類型,是否存在一個顯式轉換? int i2 = (int)o1;//從objcet轉換為int(拆箱) dynamic d1 = 123;//從int類型轉換為dynamic(objcet)(裝箱) int i3 = (int)d1;//拆箱
在大多數情況下, dynamic 類型與 object 類型的行為是一樣的。 但是,不會用編譯器對包含 dynamic 類型表達式的操作進行解析或類型檢查。 編譯器將有關該操作信息打包在一起,並且該信息以后用於計算運行時操作。在此過程中,類型 dynamic 的變量會編譯到類型 object 的變量中。類型 dynamic 只在編譯時存在,在運行時則不存在。
資料《CLR Via c#》
