學習Jeffrey Richter的CLR via C#已經有兩個月的時間,每天早上起床看一個小時,目前也就看到了14章。發現看到後面前面的內容基本上就差不多忘記了,所以打算寫個類似讀書筆記的東西,記下干貨方便自己日後查閱。可能有人不知道CLR via C#這本書名是什麼意思,根據我這幾個月的理解,應該是通過C#了解CLR的意思。
值類型和引用類型
言歸正傳,CLR支持兩種類型,值類型和引用類型。下圖是MSDN對值類型和引用類型的經典描述
需要注意的是,引用類型都是從托管堆上分配內存的,使用New關鍵詞返回創建的內存地址。使用引用是,需要考慮一下事實:
引用類型的內存必須從托管堆上分配
堆上分配的每個對象都有一些額外的成員【額外成員包括一些同步索引塊、對象指針等必須的東西】,這些成員必須初始化
對象中的其他字節總是零
從托管堆上分配一個對象實例時,可能強制執行一次垃圾回收
每次使用引用類型都會從托管堆上分配一些額外參數,還會導致GC的負擔過重,這些肯定會影響程序的性能。CLR還提供一種輕型、高效的數據結構:值類型。值類型通常分配在線程棧上【如果值類型嵌入在引用類型裡,則也分配在托管堆上】,變量的本身就包含實例的字段,所以值類型不需要指針,線程結束就會被隨即釋放,所以不會給GC造成壓力。看個簡單的例子:
public static void Go() {
SomeRef r1 = new SomeRef(); // Allocated in heap
SomeVal v1 = new SomeVal(); // Allocated on stack
r1.x = 5; // Pointer dereference
v1.x = 5; // Changed on stack
Console.WriteLine(r1.x); // Displays "5"
Console.WriteLine(v1.x); // Also displays "5"
// The left side of Figure 5-2 reflects the situation
// after the lines above have executed.
SomeRef r2 = r1; // Copies reference (pointer) only
SomeVal v2 = v1; // Allocate on stack & copies members
r1.x = 8; // Changes r1.x and r2.x
v1.x = 9; // Changes v1.x, not v2.x
Console.WriteLine(r1.x); // Displays "8"
Console.WriteLine(r2.x); // Displays "8"
Console.WriteLine(v1.x); // Displays "9"
Console.WriteLine(v2.x); // Displays "5"
// The right side of Figure 5-2 reflects the situation
// after ALL the lines above have executed.
}
值類型那麼牛逼是吧,那麼是類型的適用條件是什麼呢?通常情況下必須同時滿足一下情況才可以將一個類型聲明為值類型:
類型不需要從其他類型中繼承
類型不需要被其他類型繼承
類型的實例字段不會給成員更改【建議將值類型的字段都設置為readonly】
滿足以上三個條件的同時還必須滿足一下任意一個條件:
類型的實例比較小。16字節或這一下
類型的實例比較大,但是不作為方法的實參傳遞,也不從方法返回。
值類型簡單高效,但也有缺點or局限:
值類型對象的兩種表示形式:未裝箱和裝箱。對值類型的裝箱操作是非常消耗性能的。
【引用類型總是處於裝箱的形式,所以裝箱針對值類型來說的,後面詳細介紹裝箱、拆箱】
值類型不能被其他類型(包括值類型和引用類型)繼承。所以值類型裡面不可以有新的虛方法,方法不能是抽象的,都必須是密閉的。
值類型總是有默認值,不能為null
將一個值類型的變量賦給另一個值類型的變量是,會執行一次逐個字段的賦值。而引用類型的變量賦給另一個引用類型的變量時,只復制內存地址
基於上一條,值類型的變量值不會受到另一個值類型變量的影響。應用類型如果同時指向同一個托管堆,那麼任何一個引用變量的改變都會影響其他的變量。
線程棧上的值類型,實例不可達時便會給自動釋放,所以不會收到Finalize的通知。
值類型的裝箱和拆箱
前面說了,裝箱和拆箱都是針對值類型而言的,因為引用類型本身就處於“裝箱”狀態。將一個值類型轉換成引用類型,這個就是裝箱操作 。
裝箱的步驟:
在托管堆中分配好內存。【這裡分配的內存總理是值類型各個字段需要的內存量加上托管堆上對象都有的額外成員(類型對象指針和同步索引塊)需要的內存量】
值類型的字段復制到新分配的托管堆內存中
返回現分配的托管堆上對象的地址。值類型現在是一個引用類型了。
裝箱轉換允許將value-type 隱式轉換為reference-type。存在下列裝箱轉換:
· 從任何value-type 到object 類型。
· 從任何value-type 到System.ValueType 類型。
· 從任何non-nullable-value-type 到value-type 實現的任何interface-type。
· 從任何nullable-type 到由nullable-type 的基礎類型實現的任何interface-type。
· 從任何enum-type 到System.Enum 類型。
· 從任何具有基礎enum-type 的nullable-type 到System.Enum 類型。
過程很復雜,C#編譯器會自動生成IL去完成這些操作,只需要知道原理就OK了。裝箱現在懂了,那麼拆箱是什麼個情況呢?大多數初學者都會想當然的認為拆箱是裝箱的逆向,其實不然。拆箱操作其實只有一步,就是獲取托管堆上要拆箱的對象的地址。緊接著將該對象的各個字段的值復制到線程棧的實例中。顯然拆箱的代價比裝箱低得多。
取消裝箱轉換允許將reference-type 顯式轉換為value-type。存在以下拆箱轉換:
· 從object 類型到任何value-type。
· 從System.ValueType 類型到任何value-type。
· 從任何interface-type 到實現了該interface-type 的任何non-nullable-value-type。
· 從任何interface-type 到其基礎類型實現了該interface-type 的任何nullable-type。
· 從System.Enum 類型到任何enum-type。
· 從 System.Enum 類型到任何具有基礎enum-type 的nullable-type。
看了裝箱拆箱的例子:
public static void Main6() {
Int32 v = 5;
Object o = v;
v = 123;
Console.WriteLine(v + ", " + (Int32)o);
}
猜猜一共發生過幾次裝箱操作,答案是three!如果你可以詳細的解釋以上代碼的裝箱操作,那麼你對裝箱拆箱操作就很熟了。
作者 BangQ