哇。。。 明天就放假了,很開心,原本每週五晚上都是去看電影的,結果今天要陪老婆去精英學英語, 漫長的三個多小時,還好不是讓我光等着,給了我一臺電腦讓我上上網,因而決定繼續分享一些我所理解的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---值類型實例能夠很大,可是不能做爲參數傳遞。
好了,今天就寫到這吧,說實話寫這篇挺累的,花了我近三個多小時,要不是明天放假。。。。,並且還特孃的沒完,好比裝箱拆箱都沒說,哎有時間下一節的隨筆裏面在說吧。
哈哈明天是俺的生日了,額。。。不 ,是今天。。。 一會洗個澡好好睡一覺。明天得和那幫鐵子出去瘋一天去,並且老婆說有驚喜給我,期待啊。。。
最後,我仍是想說我老師說的,程序員是搞藝術的,他們不是苦逼,而是藝術家。^_^
好啦 睡覺 你們~安啦!