談一談值類型與引用類型和裝箱與拆箱

哇。。。  明天就放假了,很開心,原本每週五晚上都是去看電影的,結果今天要陪老婆去精英學英語, 漫長的三個多小時,還好不是讓我光等着,給了我一臺電腦讓我上上網,因而決定繼續分享一些我所理解的C#知識。先寫理論的東西,等回家了在補實例(沒辦法啊,機子上沒有vs,咋也不能我給人家裝一個不是。)程序員

我寫博客的目的:面試

第一點也是我寫博客最重要的一點,就是經過把本身所理解技術寫下來,以鞏固本身學習的知識(可能不像其餘園友那樣只是單純的爲了和你們分享本身的技術。。。嘿嘿)。由於本身是學數學專業的,去年三月份剛剛接觸編程這麼個東西,知識不像計算機專業的同窗那麼系統,所以也想經過寫博客來記錄本身學習的知識,未來回過頭來翻看。編程

第二點分享我所理解的給你們,從而但願對讀者有必定的幫助。(這一點確定是有的,^_^)。數組

第三點就是也能經過通園友的探討和批評建議中提升本身。因此但願園友還有各路大神們留下寶貴的墨筆,小子在此感激涕零。數據結構

 

言歸正文,前幾天面試(嘿嘿,是咱們技術老大去面,我屬於旁聽的,基本沒我什麼事,不過真的能學到不少東西),碰到一個小夥伴,公司面試題中有一個題目,就是問什麼是值類型與引用類型。小夥伴回答的很完整:ide

C#的值類型包括:結構體(數值類型,bool型,用戶定義的結構體),枚舉,可空類型。

C#的引用類型包括:數組,用戶定義的類、接口、委託,object,字符串。

數組的元素,不論是引用類型仍是值類型,都存儲在託管堆上。

引用類型在棧中存儲一個引用,其實際的存儲位置位於託管堆。爲了方便,本文簡稱引用類型部署在託管推上。

值類型老是分配在它聲明的地方:做爲字段時,跟隨其所屬的變量(實例)存儲;做爲局部變量時,存儲在棧上。

這是我在百度上摘下來的,他回答的和這些差很少,基本都答出來了,而後老大拿着面試題,以爲他這個寫的不錯,就又問了問,說你以爲在何時或者狀況下適合用值類型,什麼狀況下適合用引用類型啊? 結果小夥伴啞口無言。。。函數

額。。。 我以爲這中狀況的小夥伴確定非長多,我也是,當初爲了應付找工做,不少狀況下都是在背面試題。好比什麼是接口,而後上網上一搜:性能

     而後看都有人回答:學習

接着,背誦熟悉了,而後去面試,反正當初的我就是這樣的。。。。。。spa

但是,這樣就真的瞭解接口和抽象類了嗎?

答案是NO,就算你把接口抽象類的概念背的再熟悉,你就這的理解面向接口,面向抽象了嗎?  就算你把繼承、封裝、多態的概念滾瓜爛熟,你就真的瞭解面向對象思想了嗎。。。我以爲,這些概念是爲了讓咱們懂得怎麼去更高的去應用。這才這最主要的 。

 

回到今天的主題上來。

CRL支持兩種類型,一種是值類型,一種是引用類型,在FCL中,大部分的類型都是引用類型,引用類型的對象老是從託管堆中分配,C#用new關鍵字來返回對象的內存地址。也就是說,當你要用一個引用類型時,你要考慮到一下幾點。

  1--要從託管對上分配內存

  2--託管堆中的每一個對象都會有一些額外的成員:類型對象指針和同步快索引

    類型對象指針:就是指向對象的類型對象的指針,額。。。 估計這麼解釋確定聽不懂,反正我當時接觸的時候是聽不懂的。其實就是這個意思,咱們知道咱們寫的C#代碼,在編譯的時候都被C#編譯器編譯成一個託管模塊,其中包括CLR頭,PE頭,還有IL代碼 和元數據,IL(中間語言)就是咱們所說的託管代碼,他跑在CRL上面的,當應用程序運行的時候,爲了執行一個方法,首先必須把IL代碼轉換成本地的CPU語言,這時候就用到JIT(即時編譯器),在運行一個方法以前,CLR會檢測這個方法裏面所用到的全部的類型,而後爲這些類型在託管堆中建立一個數據結構,也就是類型對象,好比所你在方法裏面用到了Student類,那麼就會建立Student的類型對象,當你new一個Student的實例的時候,又會在託管堆中建立一個Student對象的實例,這個對象實例裏面包含的類型對象指針就這行Student這個類型對象。咱們說託管對中的每一個對象都有類型對象指針和同步快索引,Student類型對象也不例外,他的類型對象指針指向System.Type類型對象(這個就是祖宗了,就像當與object是全部類型的祖宗同樣),System.Type類型對象也是一個對象,也包含類型對象指針,他指向他本身。  - - - - - -不知道我這樣解釋能明白不。。。

    同步快索引:這個沒咋研究過,不過他在CLR裏面是一個和牛逼的人物,挺多的功能都要經過它實現,等之後有機會再研究。

  3--沒當在託管堆中建立一個對象的時候,都有可能會強制的執行一次垃圾回收。爲啥說可能呢,我以爲應該是這麼回事,垃圾回收器中不是有「代」這個概念嗎,當要在託管堆中建立對象時,發現最低代滿了的時候,就會執行一次垃圾回收。(我猜的啊,不過我以爲           是這樣)

你看啊,弄一個引用類型的對象多麻煩啊,若是全部的類型都用引用類型的話,那麼程序的性能就會降低,若是沒建立一個int對象,都要在託管對中分配內存,這樣性能會受很大的影響,因此CRL又支持一種「輕量級的」類型:值類型。

值類型的實例通常都是在堆棧上分配的,這裏用的是通常狀況啊,由於有時候可能作爲應用類型的對象的字段存放在對象中。如今我們就說這個通常的狀況下。在表明值類型的變量當中,不包含值類型實例的指針,而是包含實例對象自己,這樣就不須要再託管對中分配內存,這樣一來,值類型的使用,即減輕了託管對的壓力,同時又較少了一個應用程序的一個生命週期內,垃圾回收的次數。

 

在FCL中,大多數類型都是引用類型,或者就是通常管引用類型都叫「類」,好比system.Console類 Match類 還有什麼接口啊 事件 委託 數組都是引用類型,而值類型,在FCL中通常都叫結構或者枚舉。例如system.Int32結構,System.DayAndWeek枚舉。

全部的結構都是從System.ValueType派生出來的,全部的枚舉都是從System.Enum類派生出來的,System.Enum又是從System.ValueType派生的。並且全部的值類型都是sealed(密封)的

 

說了這麼多理論性的東西,不少小夥伴都改煩了,好吧,我用代碼展現一下值類型和引用類型在某些方面的不一樣。

1     public struct CalValue  //結構:屬於值類型
2     {
3         public int age;
4     }
6     public class CalRef  //類:輸入引用類型
7     {
8         public int age;
9     }

在Main函數裏:

 1             CalValue val = new CalValue(); //在線程棧上分配 此時val變量包含着CalValue的實例
 2             val.age = 10;//在線程棧上直接修改
 3             CalValue val1 = val;//在線程棧上分配而且複製成員,
 4             val1.age = 20;//在線程棧上修改,此時只修改val1.age 而 val.age不會修改,由於這是兩個徹底獨立的實例
 5             CalRef re = new CalRef();//在對上分配,關鍵字new調用CalRef類的構造函數,返回建立的對象的地址,保存在變量re中。
 6             re.age = 10;//提領指針,找到對象而後修改對象的age字段
 7             CalRef re1 = re;//定義變量re1,而且將re內保存的對象指針複製到re1中,此時,re和re1變量同時包含了指向同一個對象的地址。
8 re1.age = 20;//提領指針,找到對象而後修改對象的age字段,因爲re和re1變量內保存的指針指向同一個對象,因此re.age也會改變爲 20
 9  Console.WriteLine(val.age); 10  Console.WriteLine(val1.age); 11  Console.WriteLine(re.age); 12 Console.WriteLine(re1.age); 顯示結果10 20 20 20

我想你們看了上面代碼裏面的註釋就已經很明白了吧,讓咱們看看它編譯成的IL:

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] valuetype ValueTypeAndObjectType.CalValue val,
        [1] valuetype ValueTypeAndObjectType.CalValue val1,
        [2] class ValueTypeAndObjectType.CalRef re,
        [3] class ValueTypeAndObjectType.CalRef re1)  //表示方法內部定義了四個局部變量,前面的數字是變量的索引
    L_0000: nop //no operation 沒操做,我也不知道爲啥沒操做爲啥還要寫上這麼個東西,正在研究中。。。
------------------------------------------------
   CalValue val = new CalValeu(): L_0001: ldloca.s val //將變量val的地址壓入棧中 L_0003: initobj ValueTypeAndObjectType.CalValue // initobj表示建立一個值類型 並經過val的變量地址保存在val變量中("
並經過val的變量地址保存在val變量中",是我猜測的,但我絕對應該是這樣了,你們能夠作個參考,而後查看相關資料,知道是咋回事的大神還但願您指點一二,再猜測拜上了哈)
--------------------------------------------------
   val.age = 10 L_0009: ldloca.s val //
壓入val變量的地址 L_000b: ldc.i4.10 //將常量 10 壓入棧中
  L_000d: stfld int32 ValueTypeAndObjectType.CalValue::age // 將10保存在 val中的實例的age字段中
--------------------------------------------------
   CalValue val1 = val;
   val1.age = 20; L_0012: ldloc.
0 //
將索引爲0的局部變量裝在到棧中,這裏就是變量val L_0013: stloc.1 // 吧棧中返回的值存放到索引爲1的局部變量中,這裏就是至關於把val變量整個複製了一份,而後賦給val1,由於val是值類型變量,裏面包含了一個值類型實例,因此val1裏面也就有了一個值類型實例 L_0014: ldloca.s val1 //同上 L_0016: ldc.i4.s 20 //同上 L_0018: stfld int32 ValueTypeAndObjectType.CalValue::age // 此時是將20賦給了val1裏面實例的age字段。
--------------------------------------------------
   CalRef re = new CalRef(); L_001d: newobj instance
void ValueTypeAndObjectType.CalRef::.ctor() //newobj表示建立引用類型對象,並返回一個對象的地址 L_0022: stloc.2 //將地址存儲在索引爲2的變量中 :re
-------------------------------------------------- L_0023: ldloc.
2 //將變量re 裝載到棧中 L_0024: ldc.i4.s 10 L_0026: stfld int32 ValueTypeAndObjectType.CalRef::age //將10賦值給對象的age字段
--------------------------------------------------
   Calref re1 = re; L_002b: ldloc.
2 L_002c: stloc.3 //這裏是關鍵,在這裏能夠看出,知識把re變量整個賦值一下,而後給re1,而沒有將堆中的對象也同時複製。由於re裏面包含類對象的指針,因此re1裏面也包含了一個相同的指針(由於是複製嘛),這兩個指針指向同一個對象。(看到了IL代碼,我才更加理解只是複製了引用是怎麼回事)
-------------------------------------------------- L_002d: ldloc.
3 L_002e: ldc.i4.s 20 L_0030: stfld int32 ValueTypeAndObjectType.CalRef::age L_0035: ldloca.s val L_0037: ldfld int32 ValueTypeAndObjectType.CalValue::age L_003c: call void [mscorlib]System.Console::WriteLine(int32) L_0041: nop L_0042: ldloca.s val1 L_0044: ldfld int32 ValueTypeAndObjectType.CalValue::age L_0049: call void [mscorlib]System.Console::WriteLine(int32) L_004e: nop L_004f: ldloc.2 L_0050: ldfld int32 ValueTypeAndObjectType.CalRef::age L_0055: call void [mscorlib]System.Console::WriteLine(int32) L_005a: nop L_005b: ldloc.3 L_005c: ldfld int32 ValueTypeAndObjectType.CalRef::age L_0061: call void [mscorlib]System.Console::WriteLine(int32) L_0066: nop L_0067: ret }

我相信你們看了IL代碼以後是否是就更加理解這個問題了。

 

這裏我總結一下值類型好引用類型的區別:

1---從表示形式來看:值類型有兩種,一種是未裝箱形式,一種是已裝箱形式。而引用類型都是已裝箱的形式。

2---從實例化的角度上看,值類型實例化,不須要在託管對中非配內存。他與他的變量一塊兒保存在堆棧上。而引用該類型實例化時候要在託管堆中分配內存,而後要先初始化對象的兩個額外成員,而後經過關鍵字new給調用構造函數,建立對象並返回對象的地址保存在

變量中。這時變量只保存了對象的地址。

3---從性能方面看,值類型要優與引用類型,由於值類型不須要在堆上分配內存,因此也涉及不到垃圾回收。這樣既緩解了託管堆的壓力,又減小了應用程序一個生命週期內的垃圾回收次數。 這裏說一下,對於值類型,一旦定義了他的實例的方法不在處於活動狀態,那

麼爲這些實例分配的內存就會釋放。

4---從賦值角度上看(額。。。我也不知道這個角度該叫啥,暫且就這麼叫着吧),因爲值類型的實例是直接保存在變量中的,而引用類型的實例保存在託管堆中,因此變量賦值的時候,值類型會執行一次逐字段的賦值,而引用類型只是賦值地址。

5---因爲第4條,多個引用類型的變量能夠引用託管堆中同一個對象,所以其中一個變量執行的操做會影響到其餘變量的對象。而值類型則不會。

 

值了行雖然好,單不是何時都能用的,下面列出一些值類型的條件,最好只有如下條件都知足的時候,再用值類型:

1---類型很是簡單,其中沒有成員能修改任何其實力字段,事實上,許多值類型,建議把他的全部子都設置爲只讀的(readonly)

2---類型須要從其餘任何類型繼承

3---類型也不會派生出任何其餘類型 (由於值類型適合作數據的載體,不支持多態,這種事是類乾的事,類用來定義應用程序的行爲) 

4---若是值類型的實例要被做爲參數傳遞的話,那麼這個值類型的實例應該較小,由於默認的狀況下,實參是以值傳遞的形式傳遞的,這會形成對實參進行復制。(大小約爲16字節或者更小)---這是Jeffery Richter說的(在編程這個領域裏是個人偶像啊 嘿嘿)。對了這

裏只是說默認的狀況下,由於有時候會由於ref或者out而改變。

5---值類型實例能夠很大,可是不能做爲參數傳遞。

 

好了,今天就寫到這吧,說實話寫這篇挺累的,花了我近三個多小時,要不是明天放假。。。。,並且還特孃的沒完,好比裝箱拆箱都沒說,哎有時間下一節的隨筆裏面在說吧。

哈哈明天是俺的生日了,額。。。不 ,是今天。。。   一會洗個澡好好睡一覺。明天得和那幫鐵子出去瘋一天去,並且老婆說有驚喜給我,期待啊。。。

最後,我仍是想說我老師說的,程序員是搞藝術的,他們不是苦逼,而是藝術家。^_^

好啦 睡覺 你們~安啦!

相關文章
相關標籤/搜索