在C#中有兩種類型的資料,一種是實值型別資料,一種是參考型別資料。在編碼的時候區分這兩種類型資料,可以避免一些細小的編碼錯誤。
首先說說什麼類型是實值型別,例如:int、float、bool之類的基礎類型,以及用struct定義的類型,如:DateTime。除此外,如string,數組,以及用class定義的類型等都是參考型別。對於C#來說,很難羅列出所有類型進行一一分別,這需要自己在編碼過程中進行分析總結。為了更好地說明兩種類型之間的區別,借用如下的表格來說明。
|
實值型別 |
參考型別 |
記憶體配置地點 |
分配在棧中 |
分配在堆中 |
效率 |
效率高,不需要地址轉換 |
效率低,需要進行地址轉換 |
記憶體回收 |
使用完後,立即回收 |
使用完後,不是立即回收,等待GC回收 |
賦值操作 |
進行複製,建立一個同值新對象 |
只是對原有對象的引用 |
函數參數與傳回值 |
是對象的複製 |
是原有對象的引用,並不產生新的對象 |
類型擴充 |
不易擴充 |
容易擴充,方便與類型擴充 |
過如上細緻對比,大家對於實值型別和參考型別有個清楚的概念。
不過,無論是對於實值型別還是參考型別來說,對於其作為函數參數或者傳回值的時候,都是容易犯錯誤的地方。
對於實值型別來說,當其作為函數參數的時候,希望在函數中被修改,那麼直接如下操作是不能被修改的。
public void Increment( int i ) { i++; } |
要想在函數中對傳進去的參數做真正的修改,需要藉助於ref這個關鍵字,那麼正確的形式如下。
public void Increment( ref int i ) { i++; } |
也就是說,如果需要在函數中對實值型別參數進行修改,需要用ref或者out進行標識才能真正實現。
那麼參考型別又是如何那?
其實這個問題很容易理解,首先在C#中傳遞方法參數預設是“值拷貝”模式,也就是說對於實值型別(ValueType)變數直接拷貝一份,而對於參考型別則拷貝一個指向同一對象的引用副本傳遞給方法,因此即使不使用ref關鍵字,我們也可以在方法內部改變該引用所指向對象的內部狀態,但是某些時候我們需要在方法內部建立一個新的對象執行個體,並使得原有引用指向這個新的對象。那麼問題就來了,由於現在存在兩個引用,我們改變的只是傳遞到方法的引用副本,而該副本在超出方法範圍後既失去作用,而原有的引用依然指向原有對象。因此我們需要使用ref關鍵字,那麼傳遞給方法的不再是引用副本,而是引用本身。我們就可以改變原有引用對象執行個體了。
public class Data { public int i = 10; } public class Class1 { public static void Test1(Data d) { // 參數d只是一個引用副本,和原引用變數d同時指向同一個對象,因此都可以修改該對象的狀態。 d.i = 100; } public static void Test2(Data d) { // 建立新的Data對象,並將參數d指向它。此時參數d和原有引用d分別指向2個不同的Data對象,因此 // 當超出Test方法作用範圍時,參數d和其引用的對象將失去引用,等待GC回收。 d = new Data(); d.i = 200; } public static void Test3(ref Data d) { // 由於使用ref關鍵字,因此此處的參數d和原變數d為同一引用,並沒有棄置站台,所以建立新的Data // 對象是可行的。 d = new Data(); d.i = 300; } public static void Main(string[] args) { Data d = new Data(); Console.WriteLine(d.i); // 10 Test1(d); Console.WriteLine(d.i); // 100 Test2(d); Console.WriteLine(d.i); // 100 Test3(ref d); Console.WriteLine(d.i); // 300 } } |