要瞭解一門程式設計語言,首先就要瞭解它的類型。我們知道,C#一共分為兩大類型:實值型別和參考型別,但實值型別並不單純是我們java中的基礎資料型別 (Elementary Data Type)那麼簡單,有關於是否使用實值型別還是個值得討論的問題:因為裝箱機制。C#的實值型別還能夠自訂方法,甚至能夠實現參考型別的介面類型!這已經超出了我的想象範圍了!
先來點基礎的東西:
基本內容.
文檔是我們學習的好幫手,在C#的文檔中,我們必須注意,凡是參考型別的,名字都是"xx類",凡是實值型別的,就叫"xx結構"或"xx枚舉"。
很多時候,我們的初始化操作的右值是運算式。如果左值是實值型別,那麼它的值就是運算式的值,但如果是參考型別,則是一個引用,並不是該引用指向的對象(學過java,所以對這現象非常熟悉),所以,在C#中,String.Empty的值不是一個Null 字元串,而是對Null 字元串的引用。
聲明一個變數,除了它的類型資訊外,最重要的就是值的儲存地點。變數的值一般是在它聲明時的位置儲存的,而執行個體變數的值儲存在執行個體本身儲存的地方,參考型別和靜態變數則儲存在堆中。
也許我們會以為,實值型別一定在棧(stack,有些地方叫線程棧)上,參考型別一定在堆(heap,有些地方叫託管堆)上,其實這是錯誤的,實值型別也可以在堆上,像是前面講過的,變數的值儲存在它聲明的地方,如果我的實值型別是在一個參考型別中聲明的,那麼該實值型別就是在堆上。方法參數一定是在棧(方法是用棧幀儲存它的資訊)上,但局部變數不一定,它也可以是在堆上(讓我們想想匿名方法,如果它捕獲了一個外部變數,該變數就會作為隱藏委託類型而儲存在堆上)。好了,一個潛在的想法出現了:如果一個參考型別儲存在實值型別中,像是結構中,會怎麼樣呢?參考型別的資料依然儲存在堆中,但它的引用儲存在棧上,因為實值型別也只擁有它的引用而已。
實值型別不能派生出其他類型,因為它是隱式密封(sealed)的,所以它並沒有參考型別執行個體對象開頭的額外資訊(用於標識對象的實際類型和其他資訊),即類型對象指標和同步塊索引。這些額外資訊並不能修改,所以我們永遠也不能修改對象的類型,但我們可以轉換為其他類型。執行強制類型轉換時,我們會擷取一個引用,該引用會檢查它引用的對象本身是否是該目標類型的有效對象,若是,返回該引用並賦給目標類型,否則拋出異常。引用並不清楚它實際引用的對象的類型,所以我們的引用可以指向其他類型。
基礎的東西講完後,就應該講講一些不一樣的東西(雖然大部分很多人都已經非常熟悉了)。細節不討論,只是單單從幾個話題出發並做一下延伸。
話題1:結構是輕量級的類
結構雖然是實值型別,但它可以定義屬性和方法,類的行為它都具備,加上它是實值型別,比起參考型別來說,對記憶體更加友好。所以,就有一種說法:結構是輕量級的類。這種說法見仁見智,實值型別的確是"輕":不需要記憶體回收(不在堆上分配),不會因類型標識而產生開銷(沒有類型對象指標和同步塊索引),而且不需要取值這一步操作(參考型別的欄位一般都是私人的,我們只能通過訪問器getter取得其值)。但參考型別也有它的好處:在執行傳參,賦值,傳回值等類似操作時,只需要賦值4或8位元組(具體得看CLR),因為我們傳遞的只是一個引用(我想說指標,但又覺得不合適,從來就沒有任何一種說法認為引用就等同於指標,雖然CLR的引用的確是一個地址,但CLR強調,它們是引用),而不是複製所有資料。這點在容器那裡非常好用,像是List,如果容量很大,那這個傳參動作就太可怕了。
實值型別也並不總是對記憶體友好,因為隱式的裝箱機制,會使某些情況像是迴圈等,使用實值型別會造成可怕的負擔。C#中的實值型別使用起來並不像java的基礎資料型別 (Elementary Data Type)那麼簡單(java並沒有隱式的裝箱機制,基礎資料型別 (Elementary Data Type)不可能直接轉換為類),除非是以下幾種情況,我們才能放心使用實值型別:
1.實值型別是不可變(immutable)的,即該類型沒有提供任何方法來修改它的欄位。要做到這點,我們必須將實值型別的所有欄位都設定為readonly(唯讀)。這主要針對結構這種封裝其他實值型別的實值型別。
2.類型的執行個體較小(小於16位元組),因為按值傳遞需要複製欄位,但類型執行個體較大以致不可能作為實參傳遞,也不可能作為傳回值的話,也可以考慮使用實值型別。
就算滿足上面的條件,我們也必須考慮到實值型別的缺點(裝箱機制不在列表中,因為這個話題前面已多次提醒了):
1.實值型別繼承自System.ValueType,該類除了提供與System.Object一樣的方法外,還做了一個動作:覆寫了Equals()方法和GetHashCode()方法(實值型別的比較需要考慮到它的欄位,但預設的比較是引用),而預設的實現存在效能問題。我們不能苛求設計者能夠考慮到所有情況,所以,大部分情況都要我們自己覆寫這兩個方法(這個問題不知道是不是從java而來,java也存在這樣的問題)。
2.實值型別可以有自己的方法,但它不能派生也不能繼承(雖然能實現介面),因此它不能含有虛方法,所有方法也是不可覆寫的。
3.參考型別的預設值是null,但我們引用一個null的參考型別時會拋出異常:NullReferenceException,但實值型別預設值是0,並不會拋出異常。CLR為了彌補這點,提供了可空性(nullability)標識---可空類型(nullable)。
4.實值型別之間的相互賦值,會導致欄位的複製,但參考型別只是複製引用。
5.實值型別並不在堆上分配,所以當它被銷毀時,不會通過Finalize方法接到一個通知(這點在有些地方很重要,這時就需要裝箱)。
看了以上的討論,相信對使用實值型別是有點怕怕的:自己是否用錯了呢?程式員是不需要顧慮那麼多的,寫代碼最主要是能夠表達清楚自己的意圖,至於效能這方面,是可以在後期進行重構和最佳化的。
話題2:對象在C#中預設是通過引用傳遞
引用傳遞(pass by reference)的定義非常複雜,百度百科的解釋是這樣的:可以將一個變數通過引用傳遞給函數,這樣該函數就可以修改其參數的值,而引用的解釋就是某一變數的一個別名,對引用的操作與對變數直接操作完全一樣(很多人都說java是按引用傳遞,其實這種說法是不嚴謹的,嚴格意義上是傳遞引用對象地址值的按值傳遞)。如果我們以按引用傳遞的方式傳遞變數,那麼調用的方法可以通過更改其參數值,來改變調用者的變數值。但C#中參考型別變數的值雖然叫引用,但不是對象本身,它更接近於指標。
就算是傳參,有些情況也不能修改它的值:
class Program { public static void Main(String[] args) { String builder = "hello"; Show(builder); Console.WriteLine(builder); } static void Show(String str) { str = "word"; Console.WriteLine(str); } }
在Show()方法裡,修改的只是builder的一個副本。當然,String雖然是參考型別,但它是不可變的。我們來傳一個真正的參考型別:
class Program { public static void Main(String[] args) { People people = new People(); Show(people); Console.WriteLine(people.name); } static void Show(People people) { people.name = "男人"; } } public class People { public String name = "人"; }
這裡我們看到,欄位name改變了。
很困惑吧?為什麼還說C#是按值傳遞呢?C#中,參考型別作為方法參數確實是按"值"傳遞,因為參考型別的值是引用,而該引用是一個地址值,相當於指標(只是相當於,並不等於)。真正的引用傳遞的就是對象本身,因為引用本身就是對象的別名,但C#是不會傳遞對象本身的。
這個問題非常讓人糾結,尤其是CLR採取了引用這個說法,使得我們更加困擾了。
C#的實值型別非常奇怪,我們甚至可以用new來聲明:
int number = new int();number = 5;
這並沒有錯,但剛從java中跳出來的我非常驚訝!
C#編譯器是很聰明的,它知道number是一個實值型別,因為它並沒有類型對象指標,於是在棧上為它分配記憶體,然後確保所有欄位都初始化為0。這樣的動作就算不用new也行:
int number;
但是用new,編譯器就認為該執行個體已經初始化了,而上面的情況如果我們為它賦值就會發生錯誤。所以,聲明一個實值型別最好就是為它進行初始化,哪怕只是預設值。
關於這方面的討論,很多時候我都有心無力,畢竟自己這個初學者要想啃下CLR,難度很大,有什麼不對的地方還請見諒。