《CLR via C#》作者Jeffrey Richter的話來說,“不理解【引用類型】和【值類型】區(qū)別的程序員將會給代碼引入詭異的bug和性能問題(I believe that a developer who misunderstands the difference between reference types and value types will introduce subtle bugs and performance issues into their code.)”。這就要求我們正確理解和使用值類型和引用類型。
C#中數(shù)據(jù)類型為CTS(Common Type System),包括值類型和引用類型;值類型包含【布爾類型】、【字符型】和【枚舉類型】。
枚舉類型(enum)是由一組特定常量構(gòu)成的一組數(shù)據(jù)結(jié)構(gòu),是值類型的一種特殊形式,它從System.Enum繼承而來,并為基礎(chǔ)類型的值提供替代名詞。枚舉類型有名稱、基礎(chǔ)類型和一組字段。基礎(chǔ)類型必須是內(nèi)置的整數(shù)類型,字段是靜態(tài)文本字段,其中的每一個字段都表示常數(shù),同一個值可以分配給多個字段,出現(xiàn)這種情況時,必須將其中某個值標(biāo)記為主要枚舉值,以便進行反射和字符串轉(zhuǎn)換。枚舉聲明可以顯式地聲明 byte、sbyte、short、ushort、int、uint、long 或 ulong 類型作為對應(yīng)的基礎(chǔ)類型,沒有顯式地聲明基礎(chǔ)類型的枚舉聲明意味著所對應(yīng)的基礎(chǔ)類型是int。
用戶可以將基礎(chǔ)類型的值分配給枚舉,反之亦然(運行庫不要求強制轉(zhuǎn)換);也可創(chuàng)建枚舉的實例,并調(diào)用System.Enum的方法以及對枚舉基礎(chǔ)類型定義的任何方法。對于枚舉還有以下附加限制:
1.枚舉不能定義自己的方法
2.枚舉不能實現(xiàn)接口
3.枚舉不能定義屬性或事件
注意點:
1.對于沒有賦值的枚舉類型,聲明的第一個枚舉成員它的默值為0,以后的枚舉成員值是將前一個枚舉成員(按照文本順序)的值加1得到的。
2.允許多個枚舉成員有相同的值。沒有顯示賦值的枚舉成員的值,總是前一個枚舉成員的值加1。
3.使用時注意類型轉(zhuǎn)換。
char代表無符號的16位整數(shù),數(shù)值范圍從0~65535。 char類型的可能值對應(yīng)于統(tǒng)一字符編碼標(biāo)準(Unicode)的字符集。char類型與其他整數(shù)類型相比有以下兩點不同之處:
1.沒有其他類型到char類型的隱式轉(zhuǎn)換。即使是對于sbyte,byte和ushort這樣能完全使用char類型代表其值的類型, sbyte,byte和ushort到char的隱式轉(zhuǎn)換也不存在。
2.char類型的常量必須被寫為字符形式,如果用整數(shù)形式,則必須帶有類型轉(zhuǎn)換前綴。
比如(char)10賦值形式有三種: char chsomechar="A"; char chsomechar="\x0065"; 十六進制 char chsomechar="\u0065 ;
unicode表示法字符型中有下列轉(zhuǎn)義符:
\'用來表示單引號
\"用來表示雙引號
\\ 用來表示反斜杠
\0 表示空字符
\a 用來表示感嘆號
\b 用來表示退格
\f 用來表示換頁
\n 用來表示換行
\r 用來表示回車
\t 用來表示水平tab
\v 用來表示垂直tab
float類型精確到小數(shù)點后面7位。double類型精確到小數(shù)點后面15位或16位。decimal關(guān)鍵字表示128位數(shù)據(jù)類型。同浮點型相比,decimal類型具有更高的精度和更小的范圍,這使它適合于財務(wù)和貨幣計算,精確到小數(shù)點后面28到29位。
1. 通用類型系統(tǒng)
C#中,變量是值還是引用僅取決于其數(shù)據(jù)類型。C#的基本數(shù)據(jù)類型都以平臺無關(guān)的方式來定義。C#的預(yù)定義類型并沒有內(nèi)置于語言中,而是內(nèi)置于.NET Framework中。.NET使用通用類型系統(tǒng)(CTS)定義了可以在中間語言(IL)中使用的預(yù)定義數(shù)據(jù)類型,所有面向.NET的語言都最終被編譯為 IL,即編譯為基于CTS類型的代碼。
例如,在C#中聲明一個int變量時,聲明的實際上是CTS中System.Int32的一個實例。這具有重要的意義:
確保IL上的強制類型安全;
實現(xiàn)了不同.NET語言的互操作性;
所有的數(shù)據(jù)類型都是對象。它們可以有方法,屬性,等。例如:
int i;
i = 1;
string s;
s = i.ToString();
MSDN的這張圖說明了CTS中各個類型是如何相關(guān)的。注意,類型的實例可以只是值類型或自描述類型,即使這些類型有子類別也是如此。
2. 值類型
C#的所有值類型均隱式派生自System.ValueType: 結(jié)構(gòu)體:struct(直接派生于System.ValueType);
數(shù)值類型:
整 型:sbyte(System.SByte的別名),short(System.Int16),int(System.Int32),long (System.Int64),byte(System.Byte),ushort(System.UInt16),uint (System.UInt32),ulong(System.UInt64),char(System.Char);
浮點型:float(System.Single),double(System.Double);
用于財務(wù)計算的高精度decimal型:decimal(System.Decimal)。
bool型:bool(System.Boolean的別名);
用戶定義的結(jié)構(gòu)體(派生于System.ValueType)。
枚舉:enum(派生于System.Enum);
可空類型(派生于System.Nullable泛型結(jié)構(gòu)體,T?實際上是System.Nullable的別名)。
每種值類型均有一個隱式的默認構(gòu)造函數(shù)來初始化該類型的默認值。例如:
int i = new int();
等價于:
Int32 i = new Int32();
等價于:
int i = 0;
等價于:
Int32 i = 0;
使用new運算符時,將調(diào)用特定類型的默認構(gòu)造函數(shù)并對變量賦以默認值。在上例中,默認構(gòu)造函數(shù)將值0賦給了i。MSDN上有完整的默認值表。
所有的值類型都是密封(seal)的,所以無法派生出新的值類型。
值得注意的是,System.ValueType直接派生于System.Object。即System.ValueType本身是一個類類型,而不是值類型。其關(guān)鍵在于ValueType重寫了Equals()方法,從而對值類型按照實例的值來比較,而不是引用地址來比較。
可以用Type.IsValueType屬性來判斷一個類型是否為值類型:
復(fù)制代碼 代碼如下:
TestType testType = new TestType ();
if (testType.GetType().IsValueType)
{
Console.WriteLine("{0} is value type.", testType.ToString());
}
3. 引用類型
C#有以下一些引用類型:
數(shù)組(派生于System.Array)
用戶定義的以下類型:
類:class(派生于System.Object);
接口:interface(接口不是一個“東西”,所以不存在派生于何處的問題。Anders在《C# Programming Language》中說,接口只是表示一種約定[contract]);
委托:delegate(派生于System.Delegate);
object:(System.Object的別名);
字符串:string(System.String的別名)。
可以看出:
引用類型與值類型相同的是,結(jié)構(gòu)體也可以實現(xiàn)接口;
引用類型可以派生出新的類型,而值類型不能;
引用類型可以包含null值,值類型不能(可空類型功能允許將 null 賦給值類型);
引用類型變量的賦值只復(fù)制對對象的引用,而不復(fù)制對象本身。而將一個值類型變量賦給另一個值類型變量時,將復(fù)制包含的值。
對于最后一條,經(jīng)常混淆的是string。我曾經(jīng)在一本書的一個早期版本上看到String變量比string變量效率高;我還經(jīng)常聽說String是引用類型,string是值類型,等等。例如:
string s1 = "Hello, ";
string s2 = "world!";
string s3 = s1 + s2; ? //s3 is "Hello, world!"
這確實看起來像一個值類型的賦值。再如:
string s1 = "a";
string s2 = s1;
s1 = "b"; ? //s2 is still "a"
改變s1的值對s2沒有影響。這更使string看起來像值類型。實際上,這是運算符重載的結(jié)果,當(dāng)s1被改變時,.NET在托管堆上為s1重新分配了內(nèi)存。這樣的目的,是為了將做為引用類型的string實現(xiàn)為通常語義下的字符串。
4. 值類型和引用類型在內(nèi)存中的部署
經(jīng)常聽說,并且經(jīng)常在書上看到:值類型部署在棧上,引用類型部署在托管堆上。實際上并沒有這么簡單。
MSDN上說:托管堆上部署了所有引用類型。這很容易理解。當(dāng)創(chuàng)建一個應(yīng)用類型變量時:
object reference = new object();
關(guān)鍵字new將在托管堆上分配內(nèi)存空間,并返回一個該內(nèi)存空間的地址。左邊的reference位于棧上,是一個引用,存儲著一個內(nèi)存地址;而這個地址指向的內(nèi)存(位于托管堆)里存儲著其內(nèi)容(一個System.Object的實例)。下面為了方便,簡稱引用類型部署在托管推上。
再來看值類型。《C#語言規(guī)范》 上的措辭是“結(jié)構(gòu)體不要求在堆上分配內(nèi)存(However, unlike classes, structs are value types and do not require heap allocation)”而不是“結(jié)構(gòu)體在棧上分配內(nèi)存”。這不免容易讓人感到困惑:值類型究竟部署在什么地方?
5. 正確使用值類型和引用類型
這一部分主要參考《Effective C#》,希望能讓你加深對值類型和引用類型的理解。
C#中,變量是值還是引用僅取決于其數(shù)據(jù)類型。
C#的值類型包括:結(jié)構(gòu)體(數(shù)值類型,bool型,用戶定義的結(jié)構(gòu)體),枚舉,可空類型。
C#的引用類型包括:數(shù)組,用戶定義的類、接口、委托,object,字符串。
數(shù)組的元素,不管是引用類型還是值類型,都存儲在托管堆上。
引用類型在棧中存儲一個引用,其實際的存儲位置位于托管堆。為了方便,本文簡稱引用類型部署在托管推上。
值類型總是分配在它聲明的地方:作為字段時,跟隨其所屬的變量(實例)存儲;作為局部變量時,存儲在棧上。
值類型在內(nèi)存管理方面具有更好的效率,并且不支持多態(tài),適合用作存儲數(shù)據(jù)的載體;引用類型支持多態(tài),適合用于定義應(yīng)用程序的行為。
應(yīng)該盡可能地將值類型實現(xiàn)為具有【常量性】和【原子性】的類型。
應(yīng)該盡可能地確保0為值類型的有效狀態(tài)。
應(yīng)該盡可能地減少裝箱和拆箱。