微軟.NET平臺中類型使用的基本原理
2024-07-10 13:00:00
供稿:網友
 
本文來源于網頁設計愛好者web開發(fā)社區(qū)http://www.html.org.cn收集整理,歡迎訪問。微軟.net平臺中類型使用的基本原理
 
----微軟 .net平臺系列文章之二
 
譯文/趙湘寧
 
 在上一次的討論中,我介紹了許多微軟.net平臺公共語言運行時clr (common language runtime) 中與類型有關的基本概念。其中重點討論了如何從system.object類型中派生出所有別的類型,以及程序員能夠使用的多種強制類型轉換機制(如c#操作符)。最后,我提到了編譯器如何使用名字空間以及公共語言運行時clr是如何忽略名字空間的。
 在本文中,我們將繼續(xù)上次類型基礎的討論。首先從介紹簡單類型開始,然后迅速進入關于引用類型和數值類型的討論。對所有的開發(fā)人員來說,熟練掌握引用類型和數值類型的應用差別尤其重要。在編寫代碼的過程中,如果對這兩種類型使用不當會導致程序bug并引起性能問題。
簡單類型
 某些常用的數據類型,許多編譯器通過簡單的語法就可以對它們進行處理。例如,在c#語言中,你可以使用下列語法來分配一個整型變量:
int a = new int(5);
 但是我敢肯定,你會覺得用這樣的語法來聲明和初始化一個整型變量很笨拙。好在許多編譯器(包括c#編譯器)允許你使用下面的語法來代替:
int a = 5;
 這就使代碼的可讀性更強。不論使用那一種語法,產生的中間語言時一樣的。
凡編譯器直接支持的數據類型稱為簡單數據類型。這些簡單數據類型直接映射到基類庫中存在的類型。例如c#中int類型直接映射到system.int32。所以可以將下列兩行代碼與前面提到的兩行代碼是一樣的:
system.int32 a = new system.int32(5);
system.int32 a = 5;
圖一是c#中簡單數據類型與基類庫中有關類型的對應表(其它語言也會提供類似的簡單數據類型)
引用類型和數數值類型
 當從受管堆(managed heap)中分配對象時,new操作符返回對象的內存地址。通常將這個地址存儲在一個變量當中。這種方式就是引用類型的變量,因為變量不包含實際對象的位,而是引用對象的位。
 在處理引用類型時會有一些性能問題要考慮。首先,內存必須要從受管堆中分配,這樣能強制垃圾回收。其次,引用類型總是通過指針來存取。所以每次引用堆中對象的成員時,為了實現期望的處理,必須要產生和執(zhí)行收回指針的代碼。這反而影響程序的大小和程序執(zhí)行的速度。
 除了引用類型外,實際的對象系統中還有輕量級的數值類型。數值類型對象不能在可回收垃圾的堆中分配,并且表示對象的變量不包含對象的指針,而是變量包含對象本身。因為變量包含著對象,處理對象也就不必考慮指針回收的問題,從而改進了性能。
 圖二中的代碼說明了引用類型和數值類型差別。rectangle類型的聲明使用了結構,而沒有使用更普通的類。在c#中,使用結構聲明的類型是個數值類型,而使用類聲明的是引用類型。其它語言可能用不同的語法來描述數數值類型和引用類型,例如c++中使用_value修飾符。
回顧前面討論簡單類型時提到過的代碼行:
system.int32 a = new system.int32(5); 
 編譯這個語句時,編譯器發(fā)覺system.int32是數值類型并優(yōu)化產生的中間語言(il)代碼,以便使這個“對象”不從堆中分配;而將這個對象放到線程堆棧的局部變量a中。、
可能的情況下,應該使用數值類型而不要使用引用類型,這樣做可以使應用程序的性能更好。尤其是在使用以下數據類型時,你應該將變量聲明為數值類型:
* 簡單數據類型。
* 不需要從其它類型繼承的數據類型。
* 沒有任何從它派生的數據類型。
* 類型對象不會作為方法參數經常性傳遞,這是因為它會導致頻繁的內存拷貝操作,從而損害性能。這一點在下面有關框入和框出的討論中將作更詳細的解釋。
 數值類型的主要優(yōu)點是他們不在受管堆中進行分配。但與引用類型比較,使用數值類型也有幾個局限。以下是對數值類型和引用類型的一個比較。
 數值類型對象有兩種表示法:框出的形式和框入的形式。引用類型對象總是表示為框入形式。
 數值類型從system.valuetype類型中隱含派生。這個類型提供的方法與system.valuetype定義的方法相同。但是,system.valuetype重載equals方法,以便在兩個對象實例字段匹配時返回true。此外,system.valuetype重載gethashcode方法,以便在對象實例字段中使用有這些值參與的算法產生hash 代碼值。當定義自己的數值類型時,強烈推薦你重載并提供外部的equals 和gethashcode方法實現。
 因為使用數值類型作為基類時不能聲明新的數值類型或新的引用類型,數值類型不應有虛函數,不能被抽象,并被隱含式封裝(封裝類型不能被用作新類型的基類)。
 引用類型變量包含堆內存中對象的地址。在缺省情況下,引用類型變量被創(chuàng)建時被初始化為空(null),也就是說這個引用類型變量當前不指向有效對象。試圖使用值為空的引用類型變量會導致nullreferenceexception 異常。與之相對,對于數值類型變量來說,它總是包含潛在類型的值,在缺省情況下,這個數值類型所有成員被初始化零(zero)。當訪問數值類型時就不可能產生nullreferenceexception 異常。
 當你將一個數值類型變量的內容賦值給另一個數值類型變量時,變量值被拷貝。當你將一個引用類型變量的內容賦值給另一個引用類型變量時,只是變量的內存地址被拷貝。
 從以上的討論中可以得出這樣的結論,堆中的單個對象可以涉及兩個以上的引用類型變量。這樣就允許用作用在一個變量上的操作來影響被另一個變量引用的對象。另一方面,每一個數值類型變量都有其自己的對象數據拷貝,而且對其中一個數值類型變量的操作不會影響其它的數值類型變量。
 運行時必須初始化數值類型以及不能調用其缺省構造函數的情形很少見,例如下面的情況下會發(fā)生這種事情,當非受管線程第一次執(zhí)行受管代碼時必須分配和初始化線程本地數值類型。在這種情況下,運行時不能調用類型的構造函數,但仍然保證所有成員被初始化為零或者為空。為此,推薦你不要對數值類型定義無參數的構造函數。實際上,c#編譯器(以及其它編譯器)會認為出錯并不再編譯代碼。這個問題很少見,而且也不會發(fā)生在引用類型上。對于數值類型和引用類型的參數化構造函數沒有這些限制。
 因為框出的數值類型不在堆中分配,只要定義這個類型實例的方法不再是活動的,就可以很瀟灑的為它們分配存儲區(qū)域。也就是說框出的數值類型對象的內存被收回的時候是接收不到通知的。但是,框入的數值類型被當作垃圾收回時會有其finalize方法調用。你絕不能用finalize方法實現一個數值類型。象無參數構造函數一樣,c# 認為這是一個錯誤而不再編譯源代碼。
框入與框出
 在很多種情況下,把數值類型當作引用類型來使用便于問題的處理。假設你想創(chuàng)建一個arraylist對象(它是在system.collections名字空間中定義的類型)來存放一些點(points)。參見圖三。
 代碼中每次循環(huán)point數值類型都被初始化,然后點被存儲在arraylist中。但是想一想,在arraylist中實際存儲的是什么呢?是point結構還是point結構的地址,仰或是別的什么東西?為了得到答案,你必須察看arraylist的add方法看看它的參數被定義成什么類型。在本段代碼中,你可以看到add方法是用以下方式被原型化的:
public virtual void add(object value)
 顯然,add方法的參數是一個對象。而對象總是被看成一個引用類型。但實際上我在代碼中傳遞的是一個p,它是一個point數值類型。這段代碼要運行,必須將point數值類型轉換為真正的堆受管對象,并且必須要能得到對這個對象的引用。
將數值類型轉換為引用類型稱為框入。其內部轉換機制可描述為:
 1、從堆中分配內存,內存大小等于數值類型所占內存加上附加的成為對象的內存開銷,附加開銷包括虛表指針和同步塊指針所需的內存。
 2、數值類型的位被拷貝到新分配的堆內存。
 3、對象的地址被返回。此地址既是當前的引用類型。
 某些語言的編譯器,如c#,自動產生框入數值類型需要的的中間語言代碼(il),但是理解框入轉換的內部機制以便了解代碼量及性能問題是很重要的。
 當add方法被調用時,在堆中為point對象分配內存。駐留在當前point數值類型(p)中的成員被拷貝到新分配的point對象。point對象地址(引用類型)被返回,然后被傳遞到add方法。這個point對象將被保留在堆中直到它被當作垃圾收回。point數值類型變量(p)可以被沖用或者被釋放,因為a rraylist絕不會知道任何關于point數值類型變量的信息。框入使類型得到統一,任 何類型的值基本上都能被作為一個對象來處理。
 與框入相對,框出重新獲得包含在對象中的數值類型(數據字段)的引用,其內部機制可描述為:
 1、clr(common language runtime)首先保證引用類型變量不為空,并且它就是希望數值類型的框入值,如果這兩個條件都不成立,則產生一個invalidcastexception異常。
 2、如果類型確實匹配,則含在對象中的數值類型指針被返回,這個指針所指的數值類型不包含通常與真正的對象關聯的開銷:即虛表指針和同步塊指針。
 注意框入總是創(chuàng)建一個新對象并拷貝框出的的位到這個對象。而框出只是簡單地返回一個框入對象的數據指針:不發(fā)生內存的拷貝。但是通常的情況是:代碼會導致被框出的引用類型所指的數據被拷貝。
下面的代碼示范了框入和框出::
public static void main() {
int32 v = 5; // 創(chuàng)建一個框出的數值類型變量
object o = v; // o 既是v的一個框入版本
v = 123; // 改變框出的值為123
console.writeline(v + ", " + (int32) o); // 顯示 "123, 5"
}
 從上面的代碼中你能想象有多少框入操作發(fā)生嗎?你會驚奇地發(fā)現答案是3!讓我們仔細分析一下代碼以便真正理解所發(fā)生的事情。
 首先創(chuàng)建的是一個int32 框出的的數值類型v,初值為5。接著創(chuàng)建一個對象引用類型o并試圖指向v。但是引用類型總是必須指向堆中的對象,所以c# 要產生相應的中間語言代碼來框入變量v,并將v的框入版本的地址存儲在o中?,F在123是框出的并且引用的數據被拷貝到框出的數值類型v中,它不影響v的框入版本,所以框入版本保持它的值為5。注意這個例子示范了o是如何被框出(返回o中數據的指針),以及o中數據是內存被拷貝到框出的數值類型v。
 現在調用writeline。它需要一個string 對象傳給它,但你又沒有string 對象,而是有三個已知項:一個int32位框出數值類型v,一個串(“,”)以及一個int32引用類型(或者說框入類型)o。它們必須被組合起來構成一個string。
 為了構造string對象,c#編譯器產生調用string對象靜態(tài)concat方法的代碼。concat方法有幾種重載版本。它們實現的功能都一樣,不同的只是參數個數不一樣。如果要用三個已知項來格式化一個串,編譯器將選擇下面的concat方法:
public static string concat(object arg0, object arg1, object arg2);
 第一個參數是arg0,用來傳遞v。但v是框出的值參數,并且arg0是一個對象,所以v必須要被框入并且用arg0來傳遞框入的v的地址。第二個參數是arg1,它是字符串“,”的地址,即一個string對象的地址。最后一個參數是arg2,o(一個對象引用)被強制轉換為int32。它創(chuàng)建一個臨時的int32數值類型,這個數值類型接收當前被o引用的值的框出版本。這個臨時的數值類型必須被再一次用arg2傳遞的內存地址框入。
 一旦concat被調用,它調用每一個指定對象的tostring方法并連結每一個對象的串值。然后從concat返回的string對象被傳遞到writeline,從而顯示最后的結果。
應該指出,如果用以下形式調用writeline,產生的中間代碼(il)會更有效:
console.writeline(v + ", " + o); // 顯示 "123, 5"
 這行代碼與前面的版本是一樣的,只是將o前面“int32”強制轉換去掉了。它之所以更有效是因為o已經是一個對象的引用類型并且其地址被直接傳遞到concat方法。從而即避免了一次框出操作也避免了一次框入操作。
下面是另一個框入和框出的例子:
public static void main() {
int32 v = 5; // 創(chuàng)建一個框出的數值類型變量
object o = v; // o 既是v的框入版本
v = 123; // 改變框出的數值類型為123
console.writeline(v); // 顯示 "123"
v = (int32) o; // 框出 o 到 v
console.writeline(v); // 顯示 "5"
}
 在這段代碼中,你計算了有多少框入操作嗎?答案是一次。之所以只有一次框入操作是因為有一個接收int32類型作為參數的writeline方法。
public static void writeline(int32 value);
 在兩次writeline調用中,變量v(int32框出數值類型)被用值傳遞。writeline可能在內部框入,而你無法控制它。重要的是你已經盡了最大努力并且從代碼中排除了這次框入。
 當你知道所寫的代碼會引起編譯器產生大量的框入代碼,如果轉用手工方法框入數值類型的話,你將會得到更小更快的代碼,如圖四
 c#編譯器自動產生框入和框出代碼。它使得編程更容易,但它對關心性能的程序員隱藏了開銷。與c#語言一樣,其它語言也可能隱藏框入和框出細節(jié)。但某些語言可能強制程序員顯式地編寫框入和框出代碼。例如,c++受管擴展需要程序員顯式地用__box操作符框入數值類型,框出操作是通過使用dynamic_cast.強制轉換框入類型為與其等價框出類型。
 最后要注意:如果一個數值類型不重載由system.valuetype定義的虛擬方法,那么這個方法只能在這個數值類型的框入形式上調用。這是因為只有這個對象的框入形式具有虛表指針。用數值類型直接定義的方法則可以這個值的框入和框出兩個版本調用。
結論
 在本文中討論的概念對于.net開發(fā)人員來說至關重要。你應該真正理解引用類型和數值類型之間的差別。同時還必須理解哪種操作需要框入,以及你所使用的編譯器是否自動框入數值類型(象c#和visual basic)。如果是,你還應該了解編譯器何時進行框入操作以及對代碼有什么影響。對這些概念怎么強調都不過分,任何誤解都會容易導致程序性能下降甚至是難以察覺的bugs。