明辨值型別與參考型別的使用場合
這個條款討論的是型別設計時的取捨——是將型別設計為結構還是類別。Bill Wagner 先生給出了一個原則:「值型別用於儲存資料,參考型別用於定義行為」。
如何判斷這個原則的適用性,Bill Wagner 也給出了一個方法,那就是首先回答下面幾個問題:
如果所有問題的答案都是 yes,那麼就應該採用值型別。這樣的判斷確實有很好的理由支撐,但是我個人認為「將這 4 個問題回答為 yes」還不足以構成採用值型別的全部理由。因為在很多專案實踐中,我發現值型別帶來的效能問題不可小覷。值型別帶來的效能問題主要有兩個:
關於第 1 條,Bill Wagner 在本條款中提到了「參考型別會給垃圾收集器帶來負擔」這個表面看似正確的判斷。但是由於 box/unbox 的效應,有些情況下,反倒是值型別給垃圾收集器帶來了更多的負擔。比如將一些值型別放到一個集合中,然後又頻繁地對其進行讀寫操作。如果碰到這種情況,我想「放棄結構而採用類別」未嘗不是一種更好的做法。事實上,將一個用作資料儲存的值型別(比如 System.Drawing.Point)加入到一個集合(System.Collections.ArrayList)中是一個太常見不過的操作。不過,C# 2.0 中新引入的泛型技術對 box/unbox 的問題有極大的改善。
關於第 2 條,Scott Meyers 先生在 Effective C++ 的第 22 條「盡量使用 pass-by-reference(傳址),少用 pass-by-value(傳值)」中講得比較清楚。雖然由於 C# 中的結構型別具有預設的深拷貝語意,沒有拷貝建構函式的呼叫。而且結構型別也沒有子類別,因此在某種程度上來講不具有多型性,也就沒有 C++ 物件傳值時可能出現的切割效應。但是值拷貝的成本仍然不小。尤其是在這個值型別比較大的情況下,問題就比較嚴重。實際上,在 .Net 框架的 Design Guidelines for Class Library Developers 文件中,在說明什麼時候應該使用結構型別的時候,其中提到了一項原則(還有其他一些並行原則)——型別實例資料的大小要小於 16 個位元組。該文件主要是從型別的執行效率層面來考慮的,而 Bill Wagner 先生這裡的條款主要是從型別的設計層面來考慮的。
從上述兩條討論來看,我個人傾向於對結構型別採取更為保守的設計策略。而對於類別則可以積極大膽地使用。因為「將結構型別不適當地設計為類別」帶來的不良後果要遠遠小於「將類別不適當地設計為結構型別」所帶來的不良後果。就目前的經驗來看,我甚至認為只有和非受控互操作打交道的情況才是使用結構型別最充足的理由,其他情況都要「三思而後行」。當然,在 C# 2.0 中引入泛型技術之後,box/unbox 將不再是一個沉重的負擔,應付一些非常輕量級的場合,結構型別依然有自己的一席之地。