術語解釋
在閱讀本文之前,你需要了解以下這幾個術語是不同的:值、引用、值類型、引用類型。
注意,上面我說的都是值類型表達式和引用類型表達式,包括局部變量和成員(如字段、屬性、索引器)等。現在,我們來考慮以下問題:
對於上面這些問題,您的答案是什麼呢?
誤區:值類型到底存儲在哪?
在談到值類型和引用類型的區別時,很多初學者常說值類型分配在方法的調用棧(或線程棧)上,引用類型分配在托管堆上,這種說法是錯誤的,至少前半部分是錯誤的。實際上這根本不應該成為值類型和引用類型區別的答案,這是所答非所問。值類型和引用類型的區別在語義層面,與存儲位置無關,並不是值類型和引用類型不同的分配方式導致了它們行為上的差異,而是因為值和*引用*這兩種類型在語義上的差異,才導致了他們不同的分配方式。本文只討論存儲位置,不會深入介紹它們的區別。
有些朋友可能會說,詳細的分配方式應該是這樣的:
具體來講也就是說,當值類型作為引用類型的私有字段時,它將作為引用類型實例的一部分,也分配在托管堆上。而當引用類型作為值類型的成員變量時,棧上將保留該成員的引用,其實際數據還是保存在堆中。
在C# 2出現之前,這樣的說法沒有問題。但C# 2引入了匿名方法和迭代器塊後,以上說法就過於籠統了,它只看到了代碼層面的東西,而沒有看到編譯器層面的東西。值類型實例作為局部變量不都是分配在棧上。這是因為C#代碼中的局部變量,很可能在編譯為IL後就不再是局部變量了。比如,如果匿名方法使用了外部變量(外部方法中聲明的局部變量),或者迭代器塊中聲明了變量,那麼這些變量將被提升為隱藏類的字段,因此也將分配在堆上。
由此可見,雖然MSDN的文檔上也說,“值類型分配在棧上”,但這顯然是不合適的。因為
這樣,關於值類型的存儲位置,正確、完整的說法應該是:對於值類型來說,在微軟桌面CLR的C#實現中,如果值類型的實例是局部變量、Lambda表達式或匿名方法中封閉的臨時變量,且方法體不是迭代器塊,並且JIT不對該值進行寄存,那麼這時該值類型將存儲在棧上。
夠啰嗦吧,其實每一句都必不可少:
之所以會有這樣的誤區,是因為人們總是錯誤地以為類型系統與存儲分配策略有關。然而究竟是存儲在棧上還是堆上,與要存儲的類型沒有任何關系。分配機制的選擇只與存儲所需的生存時間(lifetime)有關。
明確了這些之後,我們可以得出以下結論:
現在我們來看一下實現細節。在微軟CLR對C#的實現中:
這樣就可以很自然地得出:
一旦你摒棄值的類型與存儲有關這個瘋狂的想法,一切就會豁然開朗了。其實,你無需知道這些,除非要編寫不安全代碼或與非托管代碼交互。你盡可以讓編譯器和運行時來管理存儲位置的生存時間,這正是它們所擅長的。
誤區:引用就是地址
下面我們來看一個關於引用的誤區。雖然連《CLR via C#》中都有類似的描述:引用類型的變量保存的是對象的地址,但這是不正確的。引用類型的變量保存的是對象的引用。
引用是一個模糊的概念。指針與引用類似,可以通過跟蹤其位置找到一些數據。但指針更智能,比如可以進行數學運算等。指針也更強大,引用能做的事,指針都能做,反之則不然。指針的缺點是對初學者來說太難理解了,很可能搬石頭砸自己的腳。
指針是通過地址實現的。地址是一個數字,表示對進程的整個虛地址空間的一個偏移量(offset)。正因為地址是數字,所以才能對指針進行數學運算。
有些時候,指針是無法替代的;而大多數時候,又不需要這麼復雜的概念。因此,C#中既包含指針,也包含引用。
C#語言規范中對引用的描述是十分模糊的:引用類型的變量存儲了對某個對象的引用。同樣,對指針的描述也是很模糊的:指針變量存儲了對象的地址。不過,規范中從來沒有說過引用就是地址。因此C#的引用是一個十分模糊的概念。你只能對一個引用進行解引用(dereference),或比較兩個引用是否相等,除此之外不能進行任何操作。
實際上,在後台,對於托管對象的引用,CLR將其實現為GC所擁有的對象的地址。但這是實現細節。C#引用應該實現為只對GC有意義的不透明的句柄,只是這個句柄恰巧為運行時地址。這是實現細節,你既不應該知道,也不應該依賴於此。
所以,你不能說“引用即地址”這樣的話。它並不是必須為地址,實現細節完全有可能改變。而且對初學者來說,你還要解釋什麼是地址,什麼是偏移量。對了解指針的人來說,還會帶來困擾:既然引用和指針都是地址,那麼應該可以將引用轉換為unsafe的指針。但這是不正確的。
綜上所述,如果你不是要向別人解釋C#的內存模型,請不要使用“引用即地址”這種論調。我們應該說:引用是一個小的數據塊,它包含一些信息,CLR可以根據這些信息來找到引用所指向的對象。這很模糊,但卻正確,並且沒有多余的暗示。
結論
你會發現,我們“無意中”從很多書籍和資料中了解到了CLR的實現細節,如果不是要深入研究這些細節,其實是沒有必要知道的。我並不是說這些細節不重要,而是說它們會給我們帶來誤導,讓我們誤以為必須是這樣。
參考資料
The Stack Is An Implementation Detail