# 原則18:值類型和引用類型的區別
**By D.S.Qiu**
**尊重他人的勞動,支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
值類型或引用類型?結構體或類?什么時候你需要使用它們?這不是 C++ ,定義的類型為值類型可以當做引用類型使用。這也不是 Java ,所有類都是引用類型(除非你是語言設計者之一)。當你創建類的時候你就需要決定這個類所有實例的行為。在開始的時候就要做好這個重要的選擇。你必須面對這個選擇的后果因為改變之前的選擇會引起一些代碼的破壞。創建類型的時候只是很簡單的選擇 struct 和 class 關鍵字,但是如果改變類型就要花很大功夫去更新客戶代碼。
這不想更喜歡其中的一個那么簡單。正確的選擇取決于你希望怎么樣使用新類型。值類型不會有多態性。它們更適合用于存儲程序操作的數據。引用類型具有多態性應該用來定義程序的行為。考慮新類型期待的職責,并且選擇正確的類型來創建。結構體存儲數據。類定義行為。
.NET 和 C# 會加入值類型和引用類型的區別是因為在 C++ 和 Java 都出現很常見的問題。在 C++ 中,所有參數和返回值都按值傳遞。按值傳遞是非常高效的,但是會有一個問題:部分復制(有時也被稱作對象切割)。如果你在希望是基類的地方使用的是子類,只有子類中基類中的部分會被復制。你因此失去子類的所有信息。即使是調用虛函數也是執行的基類的版本。
Java 語言的針對這個問題處理是或多或少移除值類型。所有用戶定義的類型都是引用類型。在 Java 語言中,所有參數和返回值都是按引用傳遞的。這個策略有著保持一致的優點,但是犧牲了性能。我們來正視這個問題,有些類不需要多態性——它們也不會被那樣設計。Java 程序員為每個變量都需要堆內存的分配和最后的垃圾回收。他們還需要花費更多的時間消耗去解引用每個變量。所有變量都是引用。在 C# 中,你聲明新類型是值類型還是引用類型取決于你使用的是 struct 還是 class 關鍵字。值類型會是小的,輕量的類型。引用類型會出現你類型的繼承結構中。這個部分會檢查類型的不同使用讓你明白值類型和引用類型的所有區別。
首先,這個類型作為方法的返回值:
```
private MyData myData;
public MyData Foo()
{
return myData;
}
// call it:
MyData v = Foo();
TotalSum += v.Value;
```
如果 MyData 是值類型,返回值會被復制存儲為 v 。然而,如果 MyData 是引用類型,會導出把內部的變量的引用。你就破壞了封裝的原則(查看原則26)。
或者,考慮下面的變種:
```
public MyData Foo2()
{
return myData.CreateCopy();
}
// call it:
MyData v = Foo();
TotalSum += v.Value;
```
現在, v 是 myData 的復制。如果是引用類型,在堆內存會創建兩個對象。你不會有暴露內部數據的問題。而是,你在堆內存創建另一個對象。如果 v 是一個局部變量,它會很快變為垃圾并且克隆會強制進行類型檢查。這些都是低效率的。
如果類使用 public 方法和屬性導出數據應該使用值類型。但那不是說所有有 public 的成員的類型都得是值類型。前面的代碼就是假設 MyData 是存儲值。它的職責就是存儲這些值。
但是考慮下面代碼:
```
private MyType myType;
public IMyInterface Foo3()
{
return myType as IMyInterface;
}
// call it:
IMyInterface iMe = Foo3();
iMe.DoWork();
```
myType 仍然從 Foo3 方法中返回。但是這次,不是從返回值中訪問數據,而是訪問對象定義接口的方法。你訪問 MyType 對象不是它存儲的數據而是它的行為。那個行為是通過 IMyInferface 體現的,它可以被多個類實現。對于這里例子, MyType 就需要是引用類型,而不是值類型。 MyType 的職責是圍繞它的行為,而不是它的數據。
這段簡單的代碼開始告訴你的區別:值類型存儲值,引用類型定義的行為。現在看得更深一點,在這些類型存儲在內存和存儲模型上表現的性能。考慮這個類:
```
public class C
{
private MyType a = new MyType();
private MyType b = new MyType();
// Remaining implementation removed.
}
C cThing = new C();
```
多個對象被創建?它們會占多大內存?這還不能下定論。如果 MyType 是值類型,你只有一次內存分配。分配的大小是 MyType 大小的兩倍。然而,如果 MyType 是一個引用類型,你就有三次內存分配,第一個是 C 對象,它是一個8字節(指針占32位),另外兩個是 C 對象包含的 MyType 對象。這個區別是因為值類型會存儲在對象內部,而引用類型不會。每個引用變量持有引用,存儲需要額外的內存分配。
為了證明這一點,考慮下面內存分配:
```
MyType[] arrayOfTypes = new MyType[100];
```
如果 MyType 是值類型,會一次分配100個的 MyType 對象。然而,如果 MyType 是引用類型,只有一次分配發生。數組的每個元素都是 null 。當你初始化數組100個元素,你就已經有101內存分配——101次多一次內存分配。分配大量的引用類型會將對內存碎片化就會變得慢下來。如果你創建的類型是為了存儲數據,那么值類型就是要選擇的方式。
值類型和引用類型是一個很重要的選擇。將值類型改為引用類型會有影響深遠的改變。考慮這個類型:
```
public struct Employee
{
// Properties elided
public string Position
{
get;
set;
}
public decimal CurrentPayAmount
{
get;
set;
}
public void Pay(BankAccount b)
{
b.Balance += CurrentPayAmount;
}
}
```
這個簡單類型只包含一個方法讓你支付被雇傭者。時間久了,系統也會運行很好。如果你決定有不同類型的被雇傭者:銷售人員得到傭金,經理收到獎金。你決定改變 Employee 的類型:
```
public class Employee2
{
// Properties elided
public string Position
{
get;
set;
}
public decimal CurrentPayAmount
{
get;
set;
}
public virtual void Pay(BankAccount b)
{
b.Balance += CurrentPayAmount;
}
}
```
這個會破壞很多已經存在使用你定義 struct 的代碼。返回值類型變為返回引用類型。參數按值傳遞變為按引用傳遞。下面代碼的行為會有巨大的變化:
```
Employee e1 = Employees.Find(e => e.Position == "CEO");
BankAccount CEOBankAccount = new BankAccount();
decimal Bonus = 10000;
e1.CurrentPayAmount += Bonus; // Add one time bonus.
e1.Pay(CEOBankAccount);
```
一次性的獎金變成了永久的加薪。這就是用引用類型替換值類型導致的。編譯器會十分歡快是你的改變生效。 這個 CEO 也會很高興。另一個頭, CFO 就會報錯。你不能想當然把值類型改為引用類型因為實際上:它改變行為了。
這個問題出現是因為 Employee 類不再遵循值類型的規范。定義的被雇傭者除了存儲數據,在這個例子中,它還有其他職責——支付被雇傭者。職責是類類型的范疇。類可以定義多態很容易實現常見的職責,結構體就不能只應該限制存儲數據。
.NET 文檔推薦你考慮類型大小作為決定是否值類型和引用類型的決定因素。實際上,更好的因素是類型的使用。如果類型只是一個結構或者數據的載體最佳的候選是值類型。的確值類型在內存管理更高效:少量的堆內存碎片,更少的垃圾,以及更少尋址。更重要的是,值類型作為方法或屬性的返回值會被復制。暴露內部結構體的引用沒有任何危害。但是注意其他的細節。值類型對支持常見的面向對象技術很有限制。你不能創建值類型的繼承結果。你應該認為所有的值類型都是封閉的。你創建的值類型可以實現接口但是需要裝箱,原則17介紹拆箱操作引起的性能問題(譯者注:這應該是第一步敘述,第一個版中原則17關于封箱和拆箱)。你應該認為值類型作為存儲的容器,而不是面向對象意義上的對象。
你會創建比值類型更多的引用類型。如果下面的問題你都回答是,那你可以創建值類型。以前面雇傭者為例考慮這些問題:
1.這個類是否滿足存儲數據的職責?
2.是否所有屬性 public 接口都只是訪問數據?
3.我是否確定這個類型不會有子類?
4.我是否確定這個類型不會看成多態性?
構建低級的數據存儲類就用值類型。使用引用類型構建你程序的行為。你可以安全的復制數在對象外暴露。你可以享受基于棧和內置值存儲的內存使用優勢,而且你可以利用標準的面向對象計算實現你程序的邏輯。當對類型選擇很疑惑的時候,就使用引用類型。
小結:
總結下:值類型和引用類型的選擇——如果只是數據的存儲,并且所有 public 的接口(屬性和方法)都是只是訪問數據而不是修改數據才使用值類型,其他情況都選擇引用類型。
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/2079672](/blog/2079672)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable<T> 和 IComparer<T> 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數