一.C#中的值類型和引用類型
- 概念
值類型直接存儲其值。
引用類型存儲對值的引用。
說起來有些拗口,其本質是Value
與Reference
的區別,在文檔翻譯過程中也有譯者將Reference
翻譯為參考。兩種類型在內存中的存儲方式有顯著區別。
- 不同的存儲對象
值類型變量存儲的是變量的值,直接儲存在棧內存中。
引用類型變量存儲的是變量所在的內存地址,引用類型變量的實際數據存儲于托管堆,變量本身僅僅是一個指向堆中實際數據的地址,存儲于棧內存中,通常是四個字節。
- 不同的存儲位置
值類型
Value
存儲在線程堆棧中
引用類型
Reference
存儲在托管堆上
內存格局通常劃分為四個區:
全局數據區:存放全局變量,靜態數據,常量
代碼區:存放所有的程序代碼
棧區:存放為運行而分配的局部變量,參數,返回數據,返回地址等
堆區:即自由存儲區
為了理解值類型變量和引用類型變量的內存分配模型,我們應先區分兩種不同的內存區域——線程堆棧Thread Stack
和托管堆Managed Heap
。
每一個正在運行的程序都對應著一個進程Process
,在一個進程內部,可以有一個或多個線程Thread
,每個線程都擁有一塊“自留地”,成為線程堆棧
,大小為1M,用于保存自身的一些數據,如函數中定義的局部變量、函數調用時傳送的參數值等。
現在我們可以解釋第一句話——值類型存儲在線程堆棧中,也就是說所有值類型的變量都是在線程堆棧中分配的。
另一塊內存區域稱為堆Heap
,在.NET這種托管環境下,堆由CLR(Common Language Runtime)管理,所以又稱托管堆Managed Heap
。例如使用new
關鍵字創建類的對象實例時,分配給對象的內存單元就位于托管堆中。
- 不同的類型
這里類型區分的對象是C#中內建的類型Type
和用戶自定義的類型。
C#中的值類型:C#有15個預定義類型,其中13個是值類型,兩個是引用類型(string
和object
)。
由此分類可以得知,struct是輕量級的類
這句話本質上就不成立,兩者的內存模型和行為表現都有區別。
- 不同的表現
1.值類型的表現
int a = 5;
int b = a;
上面這段代碼中我們賦予a
一個常量值5,而賦予b
a的值,這會在內存中兩個不同的地方存儲值20。我們改變a的值,不會影響b的值,這兩個值時獨立存儲的。可以在上述代碼之后改變a的值,輸出b的值進行查看。
2.引用類型的表現
首先創建一個簡單的類,只包含一個int類型的屬性。
class TestRef
{
public int A { get; set; }
}
主方法中與值類型的代碼類型:
public static void Main(string[] args)
{
TestRef testA = new TestRef {A = 20};
TestRef testB = testA; // 將testA賦值給testB
Console.WriteLine("Before:testA中A的值:{0}", testA.A);
Console.WriteLine("Before:testB中A的值:{0}", testB.A);
testB.A = 15; // 改變testB的屬性值
Console.WriteLine("After:testA中A的值:{0}",testA.A);
Console.WriteLine("After:testB中A的值:{0}", testB.A);
Console.ReadKey();
}
運行結果如圖所示:
可以看到testB改變了屬性值之后,testA的屬性值也隨之改變,這是由于這兩個對象只是一個指向堆內存的地址,實際指向的只有一份實際的值。
3.與null的關系
如果變量是引用類型變量,則可以將其值設置為null,表示它不引用任何對象(可以將理解為將指針指向空)。而值類型不能為null,這也是為什么值類型初始化時必須指定初始值或默認值。
-
設計立足點
大多數更復雜的數據類型,包括我們自己聲明的類都是引用類型。它們分配在堆中,其生存期可以跨多個函數調用,可以通過一個或幾個別名來訪問。CLR執行一種精細的算法,來跟蹤哪些引用變量仍是可以訪問的,哪些引用變量已經不能訪問了。CLR會定期刪除不能訪問的對象,把它們占用的內存返回給操作系統。這是通過垃圾收集器實現的。
把基本類型規定為值類型,而把包含許多字段的較大類型(通常在有類的情況下)規定為引用類型,C#設計這種方式的原因是可以得到最佳性能。如果要把自己的類型定義為值類型,就應把它聲明為一個結構。
深拷貝和淺拷貝
深拷貝——源對象與拷貝對象互相獨立,其中任何一個對象的改動都不會對另外一個對象造成影響。
淺拷貝——拷貝對象后,兩個對象并未完全“分離”,改變一個對象實際儲存的內容,則兩個對象同時被改變。
這種差異的產生,即是取決于拷貝子對象時復制內存還是復制指針。深拷貝為子對象重新分配了一段內存空間,并復制其中的內容;淺拷貝僅僅將指針指向原來的子對象。
我們假設有了一個對象orignalObj
,并且對象orignalObj
已經有了一些具體的值,現在我們想創建一個orignalObj
的副本即對象copyObj
,我們希望,操作對象copyObj
的同時不改變對象orignalObj
的值,也就是說對象a和對象b是兩個完全獨立的對象,這即是深拷貝。
當兩個對象指向同一個地址時,如果我們改變其中一個對象的值,另一個對象也被相應的改變,這即是淺拷貝。
- 額外需要注意
(1)String字符串對象是引用對象,但是很特殊,它表現的如值對象一樣,即對它進行賦值,分割,合并,并不是對原有的字符串進行操作,而是返回一個新的字符串對象。但這其實是運算符重載的結果,將string實現為語義遵循一般的、直觀的字符串規則。 String對象被分配在堆上,而不是棧上。
(2)Array數組對象是引用對象,在進行賦值的時候,實際上返回的是源對象的另一份引用而已;因此如果要對數組對象進行真正的復制(深拷貝),那么需要新建一份數組對象,然后將源數組的值逐一拷貝到目的對象中。