5.1基元類型
編譯器(C#)直接支持的任何數據類型都稱為基元類型(primitive type),基元類型直接映射到FCL中存 在的類型。可以認為 using string = System.String;自動產生。
FCL中的類型在C#中都有相應的基元類型,但是在CLS中不一定有,如Sbyte,UInt16等等。
C#允許在“安全”的時候隱式轉型——不會發生數據丟失,Int32可以轉為Int64,但是反過來要顯示 轉換,顯示轉換時C#對結果進行截斷處理。
unchecked和check控制基元類型操作
C#每個運算符都有2套IL指令,如+對應Add和Add.ovf,前者不執行溢出檢查,後者要檢查並拋出 System.OverflowException異常。
溢出檢查默認是關閉的,即自動對應Add這樣的指令而不是Add.ovf。
控制C#溢出的方法:
1.使用 /check+編譯器開關
2.使用操作符checked和unchecked:
int b = 32767; // Max short value //b = checked((short)(b + 32767)); throw System.OverflowException b = (short)checked(b + 32767); //return -2
這裡,被注釋掉的語句肯定會檢查到溢出,運行期抱錯;而第二句是在Int32中檢查,所以不會溢出。 注意這兩條語句只是為了說明check什麼時候發揮作用,是兩條不同語義的語句,而不是一條語句的正誤 兩種寫法。
3.使用 checked和unchecked語句,達到與check操作符相同的效果:
int b = 32767; // Max short value checked { b = b + 32767; } return (short)b;
System.Decimal類型在C#中是基元,但在CLR中不是,所以check對其無效。
5.2 引用類型和值類型
引用類型從托管堆上分配內存,值類型從一個線程堆棧分配。
值類型不需要指針,值類型實例不受垃圾收集器的制約
struct和enum是值類型,其余都是引用類型。這裡,Int32,Boolean,Decimal,TimeSpan都是結構。
struct都派生自System.ValueType,後者是從System.Object派生的。enum都派生自System.Enum,後 者是從System.ValueType派生的。
值類型都是sealed的,可以實現接口。
new操作符對值類型的影響:C#確保值類型的所有字段都被初始化為0,如果使用new,則C#會認為實例 已經被初始化;反之也成立。
SomeVal v1 = new SomeVal(); Int32 a1 = v1.x; //已經初始化為0 SomeVal v2; Int32 a2 = v2.x; //編譯器報錯,未初始化
使用值類型而不是引用類型的情況:
1.類型具有一個基元類型的行為:不可變類型,其成員字段不會改變
2.類型不需要從任何類型繼承
3.類型是sealed的
4.類型大小:或者類型實例較小(<16k);或者類型實例較大,但不作為參數和返回值使用
值類型有已裝箱和未裝箱兩種形式;引用類型總是已裝箱形式。
System.ValueType重寫了Equals()方法和GetHashCode()方法;自定義值類型也要重寫這兩個方法。
引用類型可以為null;值類型總是包含其基礎類型的一個值(起碼初始化為0),CLR為值類型提供相應 的nullable。
copy值類型變量會逐字段復制,從而損害性能,copy引用類型只復制內存地址。
值類型的Finalize()方法是無效的,不會在垃圾自動回收後執行——就是說不會被垃圾收集。
CLR控制類型字段的布局:System.Runtime.InteropServices.StructLayoutAttribute屬性, LayoutKind.Auto為自動排列(默認),CLR會選擇它認為最好的排列方式;LayoutKind.Sequential會按照 我們定義的字段順序排列;LayoutKind.Explicit按照偏移量在內存中顯示排列字段。
[System.Runtime.InteropServices.StructLayout(LayoutKind.Auto)] struct SomeVal { public Int32 x; public Byte b; }
Explicit排列,一般用於COM互操作
[StructLayout(LayoutKind.Explicit)] struct SomeVal { [FieldOffset(0)] public Int32 x; [FieldOffset(0)] public Byte b; }
5.3 值類型的裝箱和拆箱
boxing機制:
1.從托管堆分配內存,包括值類型各個字段的內存,以及兩個額外成員的內存:類型對象指針和同步 塊索引。
2.將值類型的字段復制到新分配的堆內存。
3.返回對象的地址。
——這樣一來,已裝箱對象的生存期 超過了 未裝箱的值類型生存期。後者可以重用,而前者一直到 垃圾收集才回收。
unboxing機制:
1.獲取已裝箱對象的各個字段的地址。
2.將這些字段包含的值從堆中復制到基於堆棧的值類型實例中。
——這裡,引用變量如果為null,對其拆箱時拋出NullRefernceException異常;拆箱時如果不能正確 轉型,則拋出InvalidCastException異常。
裝箱之前是什麼類型,拆箱時也要轉成該類型,轉成其基類或子類都不行,所以以下語句要這麼寫:
Int32 x = 5; Object o = x; Int16 y = (Int16)(Int32)o;
拆箱操作返回的是一個已裝箱對象的未裝箱部分的地址。
大多數方法進行重載是為了減少值類型的裝箱次數,例如Console.WriteLine提供多種類型參數的重載 ,從而即使是Console.WriteLine(3);也不會裝箱。注意,也許WriteLine會在內部對3進行裝箱,但無法 加以控制,也就默認為不裝箱了。我們所要做的,就是盡可能的手動消除裝箱操作。
可以為自己的類定義泛型方法,這樣類型參數就可以為值類型,從而不用裝箱。
最差情況下,也要手動控制裝箱,減少裝箱次數,如下:
Int32 v = 5; Console.WriteLine("{0}, {1}, {2}", v, v, v); //要裝箱3 次 Object o = v; //手動裝箱 Console.WriteLine("{0}, {1}, {2}", o, o, o); //僅裝箱1 次
由於未裝箱的值類型沒有同步塊索引,所以不能使用System.Threading.Monitor的各種方法,也不能 使用lock語句。
值類型可以使用System.Object的虛方法Equals,GetHashCode,和ToString,由於System.ValueType 重寫了這些虛方法,而且希望參數使用未裝箱類型。即使是我們自己重寫了這些虛方法,也是不需要裝箱 的——CLR以非虛的方式直接調用這些虛方法,因為值類型不可能被派生。
值類型可以使用System.Object的非虛方法GetType和MemberwiseClone,要求對值類型進行裝箱
值類型可以繼承接口,並且該值類型實例可以轉型為這個接口,這時候要求對該實例進行裝箱
5.4使用接口改變已裝箱值類型
interface IChangeBoxedPoint { void Change(int x); } struct Point : IChangeBoxedPoint { int x; public Point(int x) { this.x = x; } public void Change(int x) { this.x = x; } public override string ToString() { return x.ToString(); } class Program { static void Main(string[] args) { Point p = new Point(1); Object obj = p; ((Point)obj).Change(3); Console.WriteLine(obj); //輸出1,因為change(3)的對 象是一個臨時對象,並不是obj ((IChangeBoxedPoint)p).Change(4); Console.WriteLine(p); //輸出1,因為change(4)的 對象是一個臨時的裝箱對象,並不是對p操作 ((IChangeBoxedPoint)obj).Change(5); Console.WriteLine(obj); //輸出5,因為change(5) 的對象是(IChangeBoxedPoint)obj裝箱對象,於是使用接口方法,修改引用對象obj } } }
5.5 對象相等性和身份標識
相等性:equality
同一性:identity
System.Object的Equal方法實現的是同一性,這是目前Equal的實現方式,也就是說,這兩個指向同一 個對象的引用是同一個對象:
public class Object { public virtual Boolean Equals(Object obj) { if (this == obj) return true; //兩個引用,指向同一個對象 return false; } }
但現實中我們需要判斷相等性,也就是說,可能是具有相同類型與成員的兩個對象,所以我們要重寫 Equal方法:
public class Object { public virtual Boolean Equals(Object obj) { if (obj == null) return false; //先判斷對象不為null if (this.GetType() != obj.GetType()) return false; //再比較對象類型 //接下來比較所有字段,因為System.Object下沒有字段,所以不用比較,值類 型則比較引用的值 return true; } }
如果重寫了Equal方法,就又不能測試同一性了,於是Object提供了靜態方法ReferenceEquals()來檢 測同一性,實現代碼同重寫前的Equal()。
檢測同一性不應使用C#運算符==,因為==可能是重載的,除非將兩個對象都轉型為Object。
System.ValueType重寫了Equals方法,檢測相等性,使用反射技術——所以自定義值類型時,還是要 重寫這個Equal方法來提高性能,不能調用base.Equals()。
重寫Equals方法的同時,還需要:
讓類型實現System.IEquatable<T>接口的Equals方法。
運算符重載==和!=
如果還需要排序功能,那額外做的事情就多了:要實現System.IComparable的CompareTo方法和 System.IComparable<T>的CompareTo方法,以及重載所有比較運算符<,>,<=, >=
5.6 對象哈希碼
重寫Equals方法的同時,要重寫GetHashCode方法,否則編譯器會有警告。
——因為System.Collection.HashTable和Generic.Directory的實現中,要求Equal的兩個對象要具有 相同的哈希碼。
HashTable/Directory原理:添加一個key/value時,先獲取該鍵值對的HashCode;查找時,也是查找 這個HashCode然後定位。於是一旦修改key/value,就再也找不到這個鍵值對,所以修改的做法是,先移 除原鍵值對,在添加新的鍵值對。
不要使用Object.GetHashCode方法來獲取某個對象的唯一性。FCL提供了特殊的方法來做這件事:
using System.Runtime.CompilerServices; RuntimeHelpers.GetHashCode(Object o)
這個GetHashCode方法是靜態的,並不是對System.Object的GetHashCode方法重寫。
System.ValueType實現的GetHashCode方法使用的是反射技術。