一.C#中的值類型和引用類型
- 概念
值類型直接存儲其值。
引用類型存儲對值的引用骚烧。
說起來有些拗口浸赫,其本質(zhì)是Value
與Reference
的區(qū)別闰围,在文檔翻譯過程中也有譯者將Reference
翻譯為參考。兩種類型在內(nèi)存中的存儲方式有顯著區(qū)別既峡。
- 不同的存儲對象
值類型變量存儲的是變量的值羡榴,直接儲存在棧內(nèi)存中。
引用類型變量存儲的是變量所在的內(nèi)存地址运敢,引用類型變量的實際數(shù)據(jù)存儲于托管堆校仑,變量本身僅僅是一個指向堆中實際數(shù)據(jù)的地址,存儲于棧內(nèi)存中传惠,通常是四個字節(jié)迄沫。
- 不同的存儲位置
值類型
Value
存儲在線程堆棧中
引用類型
Reference
存儲在托管堆上
內(nèi)存格局通常劃分為四個區(qū):
全局數(shù)據(jù)區(qū):存放全局變量,靜態(tài)數(shù)據(jù)涉枫,常量
代碼區(qū):存放所有的程序代碼
棧區(qū):存放為運行而分配的局部變量邢滑,參數(shù),返回數(shù)據(jù)愿汰,返回地址等
堆區(qū):即自由存儲區(qū)
為了理解值類型變量和引用類型變量的內(nèi)存分配模型困后,我們應先區(qū)分兩種不同的內(nèi)存區(qū)域——線程堆棧Thread Stack
和托管堆Managed Heap
。
每一個正在運行的程序都對應著一個進程Process
衬廷,在一個進程內(nèi)部摇予,可以有一個或多個線程Thread
,每個線程都擁有一塊“自留地”吗跋,成為線程堆棧
,大小為1M侧戴,用于保存自身的一些數(shù)據(jù),如函數(shù)中定義的局部變量跌宛、函數(shù)調(diào)用時傳送的參數(shù)值等酗宋。
現(xiàn)在我們可以解釋第一句話——值類型存儲在線程堆棧中,也就是說所有值類型的變量都是在線程堆棧中分配的疆拘。
另一塊內(nèi)存區(qū)域稱為堆Heap
,在.NET這種托管環(huán)境下蜕猫,堆由CLR(Common Language Runtime)管理,所以又稱托管堆Managed Heap
哎迄。例如使用new
關鍵字創(chuàng)建類的對象實例時回右,分配給對象的內(nèi)存單元就位于托管堆中。
- 不同的類型
這里類型區(qū)分的對象是C#中內(nèi)建的類型Type
和用戶自定義的類型漱挚。
C#中的值類型:C#有15個預定義類型,其中13個是值類型,兩個是引用類型(string
和object
)翔烁。
由此分類可以得知,struct是輕量級的類
這句話本質(zhì)上就不成立旨涝,兩者的內(nèi)存模型和行為表現(xiàn)都有區(qū)別蹬屹。
- 不同的表現(xiàn)
1.值類型的表現(xiàn)
int a = 5;
int b = a;
上面這段代碼中我們賦予a
一個常量值5,而賦予b
a的值,這會在內(nèi)存中兩個不同的地方存儲值20哩治。我們改變a的值秃踩,不會影響b的值,這兩個值時獨立存儲的业筏。可以在上述代碼之后改變a的值鸟赫,輸出b的值進行查看蒜胖。
2.引用類型的表現(xiàn)
首先創(chuàng)建一個簡單的類,只包含一個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的屬性值也隨之改變,這是由于這兩個對象只是一個指向堆內(nèi)存的地址岁经,實際指向的只有一份實際的值朋沮。
3.與null的關系
如果變量是引用類型變量,則可以將其值設置為null缀壤,表示它不引用任何對象(可以將理解為將指針指向空)樊拓。而值類型不能為null,這也是為什么值類型初始化時必須指定初始值或默認值塘慕。
-
設計立足點
大多數(shù)更復雜的數(shù)據(jù)類型,包括我們自己聲明的類都是引用類型筋夏。它們分配在堆中,其生存期可以跨多個函數(shù)調(diào)用,可以通過一個或幾個別名來訪問。CLR執(zhí)行一種精細的算法,來跟蹤哪些引用變量仍是可以訪問的,哪些引用變量已經(jīng)不能訪問了图呢。CLR會定期刪除不能訪問的對象,把它們占用的內(nèi)存返回給操作系統(tǒng)条篷。這是通過垃圾收集器實現(xiàn)的。
把基本類型規(guī)定為值類型,而把包含許多字段的較大類型(通常在有類的情況下)規(guī)定為引用類型,C#設計這種方式的原因是可以得到最佳性能蛤织。如果要把自己的類型定義為值類型,就應把它聲明為一個結構赴叹。
深拷貝和淺拷貝
深拷貝——源對象與拷貝對象互相獨立,其中任何一個對象的改動都不會對另外一個對象造成影響指蚜。
淺拷貝——拷貝對象后乞巧,兩個對象并未完全“分離”,改變一個對象實際儲存的內(nèi)容姚炕,則兩個對象同時被改變摊欠。
這種差異的產(chǎn)生,即是取決于拷貝子對象時復制內(nèi)存還是復制指針柱宦。深拷貝為子對象重新分配了一段內(nèi)存空間些椒,并復制其中的內(nèi)容;淺拷貝僅僅將指針指向原來的子對象掸刊。
我們假設有了一個對象orignalObj
免糕,并且對象orignalObj
已經(jīng)有了一些具體的值,現(xiàn)在我們想創(chuàng)建一個orignalObj
的副本即對象copyObj
,我們希望牌芋,操作對象copyObj
的同時不改變對象orignalObj
的值,也就是說對象a和對象b是兩個完全獨立的對象躺屁,這即是深拷貝经宏。
當兩個對象指向同一個地址時犀暑,如果我們改變其中一個對象的值,另一個對象也被相應的改變烁兰,這即是淺拷貝耐亏。
- 額外需要注意
(1)String字符串對象是引用對象沪斟,但是很特殊,它表現(xiàn)的如值對象一樣主之,即對它進行賦值择吊,分割,合并杀餐,并不是對原有的字符串進行操作干发,而是返回一個新的字符串對象史翘。但這其實是運算符重載的結果,將string實現(xiàn)為語義遵循一般的琼讽、直觀的字符串規(guī)則。 String對象被分配在堆上,而不是棧上钻蹬。
(2)Array數(shù)組對象是引用對象,在進行賦值的時候肝匆,實際上返回的是源對象的另一份引用而已;因此如果要對數(shù)組對象進行真正的復制(深拷貝)旗国,那么需要新建一份數(shù)組對象注整,然后將源數(shù)組的值逐一拷貝到目的對象中能曾。