程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> [你必須知道的.NET]第九回:品味類型---值類型與引用類型(中)-規則無邊

[你必須知道的.NET]第九回:品味類型---值類型與引用類型(中)-規則無邊

編輯:關於.NET

本文將介紹以下內容:

類型的基本概念

值類型深入

引用類型深入

值類型與引用類型的比較及應用

1.引言

上回[第八回:品味類型---值類型與引用類型(上)-內存有理]的發布,受到大家的不少關注,我們從內存的角度了解了值類型和引用類型的所以然,留下的任務當然是如何應用類型的不同特點在系統設計、性能優化等方面發揮其作用。因此,本回是對上回有力的補充,同時應朋友的希望,我們盡力從內存調試的角度來著眼一些設計的分析,這樣就有助於對這一主題進行透徹和全面的理解,當然這也是下一回的重點。

從內存角度來討論值類型和引用類型是有理有據的,而從規則的角度來了解值類型和引用類型是無邊無際的。本文旨在從上文呼應的角度,來把這個主題徹底的融會貫通,無邊無跡的應用,還是來自反復無常的實踐,因此對應用我只能說以一個角度來闡釋觀點,但是肯定不可能力求全局。因此,我們從以下幾個角度來完成對值類型與引用類型應用領域的討論。

2.通用規則與比較

通用有規則:

string類型是個特殊的引用類型,它繼承自System.Object肯定是個引用類型,但是在應用表現上又凸現出值類型的特點,那麼究竟是什麼原因呢?例如有如下的一段執行:

簡單的說是由於string的immutable特性,因此每次對string的改變都會在托管堆中產生一個新的string變量,上述string作為參數傳遞時,實際上執行了s=s操作,在托管堆中會產生一個新的空間,並執行數據拷貝,所以才有了類似於按值傳遞的結果。但是根據我們的內存分析可知,string在本質上還是一個引用類型,在參數傳遞時發生的還是按址傳遞,不過由於其特殊的恆定特性,在函數內部新建了一個string對象並完成初始化,但是函數外部取不到這個變化的結果,因此對外表現的特性就類似於按值傳遞。至於string類型的特殊性解釋,我推薦Artech的大作《深入理解string和如何高效地使用string》。

另外,string類型重載了==操作符,在類型比較是比較的是實際的字符串,而不是引用地址,因此有以下的執行結果:

string aString = "123";
string bString = "123";
Console.WriteLine((aString == bString)); //顯示為true,等價於aString.Equals(bString);
string cString = bString;
cString = "456";
Console.WriteLine((bString == cString)); //顯示為false,等價於bString.Equals(cString);

通常可以使用Type.IsValueType來判斷一個變量的類型是否為值類型,典型的操作為: 

public struct MyStructTester
     { }

     public class isValueType_Test
     {
         public static void Main()
         {
             MyStructTester aStruct = new MyStructTester();
             Type type = aStruct.GetType();
             if (type.IsValueType)
             {
                 Console.WriteLine("{0} belongs to value type.", aStruct.ToString());
             }

         }
}

.NET中以操作符ref和out來標識值類型按引用類型方式傳遞,其中區別是:ref在參數傳遞之前必須初始化;而out則在傳遞前不必初始化,且在傳遞時必須顯式賦值。

值類型與引用類型之間的轉換過程稱為裝箱與拆箱,這值得我們以專門的篇幅來討論,因此留待後文詳細討論這一主題。

sizeof()運算符用於獲取值類型的大小,但是不適用於引用類型。

值類型使用new操作符完成初始化,例如:MyStruct aTest = new MyStruct(); 而單純的定義沒有完成初始化動作,此時對成員的引用將不能通過編譯,例如: 

MyStruct aTest;

Console.WriteLine(aTest.X); 

引用類型在性能上欠於值類型主要是因為以下幾個方面:引用類型變量要分配於托管堆上;內存釋放則由GC完成,造成一定的CG堆壓力;同時必須完成對其附加成員的內存分配過程;以及對象訪問問題。因此,.NET系統不能由純粹的引用類型來統治,性能和空間更加優越和易於管理的值類型有其一席之地,這樣我們就不會因為一個簡單的byte類型而進行復雜的內存分配和釋放工作。Richter就稱值類型為“輕量級”類型,簡直恰如其分,處理數據較小的情況時,應該優先考慮值類型。

值類型都繼承自System.ValueType,而System.ValueType又繼承自System.Object,其主要區別是ValueType重寫了Equals方法,實現對值類型按照實例值比較而不是引用地址來比較,具體為:

char a = 'c';
char b = 'c';
Console.WriteLine((a.Equals(b))); //會返回true; 

基元類型,是指編譯器直接支持的類型,其概念其實是針對具體編程語言而言的,例如C#或者VB.NET,通常對應用.NET Framework定義的內置值類型。這是概念上的界限,不可混淆。例如:int對應於System.Int32,float對應於System.Single。

比較出真知:

值類型繼承自ValueType(注意:而System.ValueType又繼承自System.Object);而引用類型繼承自System.Object。

值類型變量包含其實例數據,每個變量保存了其本身的數據拷貝(副本),因此在默認情況下,值類型的參數傳遞不會影響參數本身;而引用類型變量保存了其數據的引用地址,因此以引用方式進行參數傳遞時會影響到參數本身,因為兩個變量會引用了內存中的同一塊地址。

值類型有兩種表示:裝箱與拆箱;引用類型只有裝箱一種形式。我會在下節以專門的篇幅來深入討論這個話題。

典型的值類型為:struct,enum以及大量的內置值類型;而能稱為類的都可以說是引用類型。struct和class主要的區別可以參見我的拙作《第四回:後來居上:class和struct》來詳細了解,也是對值類型和引用類型在應用方面的有力補充。

值類型的內存不由GC(垃圾回收,Gabage Collection)控制,作用域結束時,值類型會自行釋放,減少了托管堆的壓力,因此具有性能上的優勢。例如,通常struct比class更高效;而引用類型的內存回收,由GC來完成,微軟甚至建議用戶最好不要自行釋放內存。

值類型是密封的(sealed),因此值類型不能作為其他任何類型的基類,但是可以單繼承或者多繼承接口;而引用類型一般都有繼承性。

值類型不具有多態性;而引用類型有多態性。

值類型變量不可為null值,值類型都會自行初始化為0值;而引用類型變量默認情況下,創建為null值,表示沒有指向任何托管堆的引用地址。對值為null的引用類型的任何操作,都會拋出NullReferenceException異常。

值類型有兩種狀態:裝箱和未裝箱,運行庫提供了所有值類型的已裝箱形式;而引用類型通常只有一種形式:裝箱。

3.對症下藥-應用場合與注意事項

現在,在內存機制了解和通用規則熟悉的基礎上,我們就可以很好的總結出值類型和引用類型在系統設計時,如何作出選擇?當然我們的重點是告訴你,如何去選擇使用值類型,因為引用類型才是.NET的主體,不必花太多的關照就可以贏得市場。

3.1 值類型的應用場合

MSDN中建議以類型的大小作為選擇值類型或者引用類型的決定性因素。數據較小的場合,最好考慮以值類型來實現可以改善系統性能;

結構簡單,不必多態的情況下,值類型是較好的選擇;

類型的性質不表現出行為時,不必以類來實現,那麼用以存儲數據為主要目的的情況下,值類型是優先的選擇;

參數傳遞時,值類型默認情況下傳遞的是實例數據,而不是內存地址,因此數據傳遞情況下的選擇,取決於函數內部的實現邏輯。值類型可以有高效的內存支持,並且在不暴露內部結構的情況下返回實例數據的副本,從安全性上可以考慮值類型,但是過多的值傳遞也會損傷性能的優化,應適當選擇;

值類型沒有繼承性,如果類型的選擇沒有子類繼承的必要,優先考慮值類型;

在可能會引起裝箱與拆箱操作的集合或者隊列中,值類型不是很好的選擇,因為會引起對值類型的裝箱操作,導致額外內存的分配,例如在Hashtable。關於這點我將在後續的主題中重點討論。

3.2 引用類型的應用場合

可以簡單的說,引用類型是.NET世界的全值殺手,我們可以說.NET世界就是由類構成的,類是面向對象的基本概念,也是程序框架的基本要素,因此靈活的數據封裝特性使得引用類型成為主流;

引用類型適用於結構復雜,有繼承、有多態,突出行為的場合;

參數傳遞情況也是考慮的必要因素;

4.再論類型判等

類型的比較通常有Equals()、ReferenceEquals()和==/!=三種常見的方法,其中核心的方法是Equals。我們知道Equals是System.Object提供的虛方法,用於比較兩個對象是否指向相同的引用地址,.NET Framework的很多類型都實現了對Equals方法的重寫,例如值類型的“始祖”System.ValueType就重載了Equal方法,以實現對實例數據的判等。因此,類型的判等也要從重寫或者重載Equals等不同的情況具體分析,對值類型和引用類型判等,這三個方法各有區別,應多加注意。

4.1 值類型判等

Equals,System.ValueType重載了System.Object的Equals方法,用於實現對實例數據的判等。

ReferenceEquals,對值類型應用ReferenceEquals將永遠返回false。

==,未重載的==的值類型,將比較兩個值是否“按位”相等。

4.2 引用類型判等

Equals,主要有兩種方法,如下  

public virtual bool Equals(object obj);
public static bool Equals(object objA, object objB);

一種是虛方法,默認為引用地址比較;而靜態方法,如果objA是與objB相同的實例,或者如果兩者均為空引用,或者如果objA.Equals(objB)返回true,則為true;否則為false。.NET的大部分類都重寫了Equals方法,因此判等的返回值要根據具體的重寫情況決定。

ReferenceEquals,靜態方法,只能用於引用類型,用於比較兩個實例對象是否指向同一引用地址。

==,默認為引用地址比較,通常進行實現了==的重載,未重載==的引用類型將比較兩個對象是否引用地址,等同於引用類型的Equals方法。因此,很多的.NET類實現了對==操作符的重載,例如System.String的==操作符就是比較兩個字符串是否相同。而==和equals方法的主要區別,在於多態表現上,==是被重載,而Equals是重寫。

有必要在自定義的類型中,實現對Equals和==的重寫或者重載,以提高性能和針對性分析。

5.再論類型轉換

類型轉換是引起系統異常一個重要的因素之一,因此在有必要在這個主題裡做以簡單的總結,我們不力求照顧全面,但是追去提綱挈領。常見的類型轉換包括:

隱式轉換:由低級類型項高級類型的轉換過程。主要包括:值類型的隱式轉換,主要是數值類型等基本類型的隱式轉換;引用類型的隱式轉換,主要是派生類向基類的轉換;值類型和引用類型的隱士轉換,主要指裝箱和拆箱轉換。

顯示轉換:也叫強制類型轉換。但是轉換過程不能保證數據的完整性,可能引起一定的精度損失或者引起不可知的異常發生。轉換的格式為,

例如:int a = (int)(b + 2.02);

值類型與引用類型的裝箱與拆箱是.NET中最重要的類型轉換,不恰當的轉換操作會引起性能的極大損耗,因此我們將以專門的主題來討論。

以is和as操作符進行類型的安全轉換,詳見本人拙作《第一回:恩怨情仇:is和as》。

System.Convert類定義了完成基本類型轉換的便捷實現。

除了string以外的其他類型都有Parse方法,用於將字符串類型轉換為對應的基本類型;

使用explicit或者implicit進行用戶自定義類型轉換,主要給用戶提高自定義的類型轉換實現方式,以實現更有目的的轉換操作,轉換格式為,

例如:

public Student
{
     //…

     static public explicite opertator Student(string name, int age)
     {
         return new Student(name, age);
     }

     //…
}

其中,所有的轉換都必須是static的。

6.結論

現在,我們從幾個角度延伸了上回對值類型和引用類型的分析,正如本文開頭所言,對類型的把握還有很多可以挖掘的要點,但是以偏求全的辦法我認為還是可取的,尤其是在技術探求的過程中,力求面面俱到的做法並不是好事。以上的幾個角度,我認為是對值類型和引用類型把握的必經之路,否則在實際的系統開發中常常會在細小的地方栽跟頭,摸不著頭腦。

品味類型,我們以應用為要點撬開值類型和引用類型的規矩與方圓。

品味類型,我們將以示例為導航,開動一個層面的深入分析,下回《第十回:品味類型---值類型與引用類型(下)-應用征途》我們再見。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved