值類型和引用類型(Value Type & Reference Type)html
.NET使用兩種不一樣的物理內存來存儲數據,數據類型可簡單分爲值類型和引用類型。當聲明一個值類型的變量後,系統會在棧(stack)中分配適當的內存來存儲值類型的數據。而引用類型的變量雖然也利用棧,但棧上的地址是對堆(heap)地址的引用,引用類型的數據都存儲在託管堆上,棧存儲的是引用類型在託管堆上的地址的一個引用。數據庫
值類型之間的相互賦值會產生副本,若是將100賦值給x,那麼當你把x賦值給y時就會發生一次值拷貝。這樣,x和y都有一個相同的值,但兩個變量指向棧上的地址是不同的。也即y是拷貝了x的副本,它們各自有本身的版本。即便你把x傳遞給一個方法,在方法體內部改變這個x,原來的x也並未改變,方法接收的這個x將是原來那個x的一份拷貝。數組
string比較特殊,自己string一旦建立就不能被改變,當爲它賦予一個新字符時,原來那個字符還駐留在內存中,只不過再也不有變量指向原來那個字符在堆上的地址而已。即便更改一個string的大小寫,也不會真的改變這個字符,這隻會在堆上建立一個新字符。爲了下降內存佔用,C#的字符串還有留用機制,假設某個字符串在內存中已經有了地址,那麼假如兩個變量的值都是該字符串,則兩個字符串的引用地址是相等的。但字符串留用機制只針對常量,因此下面第二個測試引用相等性的輸出是false。安全
將值類型變量傳遞給方法,方法會創建值類型變量的拷貝,將引用類型變量傳遞給方法,方法會創建引用類型變量的地址的拷貝。也即值類型參數在方法中是獨立的,與外部的那個變量沒有關係,在方法內部改變這個變量不會影響外部那個變量,而引用類型由於傳遞的是引用地址,因此在方法中改變該變量會同時改變外部的那個變量。ide
將一個數組賦值給另外一個數組時,是將前一個數組的引用傳遞給另外一個數組,但傳遞一個數組給某個方法時,發生的是值拷貝。函數
從佔用內存空間上考慮,值類型的釋放明顯快於引用類型,由於存儲在棧中的數據一旦離開做用域(塊)就會被馬上銷燬而不用等待垃圾收集器來完成銷燬的工做,試考慮在一個方法中定義了一個值類型的數據,一旦方法執行結束,該值會被馬上清除,又假設在方法中定義一個引用類型x,方法中調用了另外一個方法,假設另外一個方法也調用了其它方法而且每個方法都引用了x,那麼x是不可能立刻被銷燬的,由於引用類型不創建拷貝,堆上的數據被改變,那麼引用這個地址的變量都會被改變。從執行效率上考慮,當拷貝發生時,引用類型比值類型更高效,執行效率更快,由於它不須要副本,只須要拷貝一個堆地址的引用。值類型卻須要在棧上分配內存空間存儲副本數據。針對不一樣的狀況應採起不一樣的方式處理這個問題。性能
null能夠賦值給引用類型的變量,它表明的含義是未指向堆上的任何地址。若是x="",則x指向了""在堆上存儲的地址,因此null!=""。學習
值類型不能夠被賦值爲null,但數據庫表的值類型卻能夠爲null,從表裏查詢的數據若是是null則沒辦法賦值給C#的值類型,爲了解決這個問題,從2.0開始可使用Nullable<T>來表示一個能夠爲空的值類型。可使用可空修飾符?來代表任意類型是能夠被賦值爲null的。測試
void是在聲明方法時使用,代表該方法沒有任何返回類型。大數據
直接寫出的一個值就是字面量(值)。如12345六、"aa"。但string x="aa"則不是。
值類型分爲枚舉(Enum)和結構(Struct)。
內置的值類型就是struct類型,從小到大依次爲:sbyte<short<int<long<float<double<decimal
字符將被自動轉換爲其對應的UTF-16編碼,一個英文字符對應的UTF-16編碼佔一個字節,一箇中文字符對應的UTF-16編碼佔兩個字節。char類型的字符不能使用雙引號,只能使用單引號括起來。
全部值類型或自定義的結構類型都派生自System.ValueType類。
注意
C#編譯器默認整數類型的字面量是int類型,若是int存儲不了該值則默認是long類型,浮點數則被默認是double類型。即便你把1賦值給一個byte類型的變量,該值也是Int32類型。
當你用一個byte類型的變量存儲整數值時,該值被默認爲是int,但實際存儲的只有8個位。若是該變量參與數學運算,則它的值又會被當作int,在C#中整數類型都是以int或long進行計算,C#並無爲byte等類型重寫任何數學運算符。另外,整數類型相除若是預期結果有小數,小數不會保留,計算這樣的結果應使用浮點數類型。
能夠爲值類型的字面量指定後綴以轉換該值被C#默認爲的類型,可用的後綴(不區分大小寫)有:m、d、f、u、l、ul,分別表示:decimal、double、float、uint、long、ulong。
小轉大就是隱式轉換。也即隱式轉換老是發生於位數小的類型轉位數大的類型。
大轉小就是強制轉換,也即位數大的類型轉位數小的類型可能會丟失精度(sbyte轉int沒問題,int轉flota沒問題,倒過來則是錯誤的),編譯器會及時提示錯誤。需考慮強轉。
計算機以數字的二進制形式進行存儲。當聲明瞭一個值類型的變量時,系統會根據它可存儲的bit位數來進行內存分配(劃分)。下圖是cpu的棧位,0-7是8個位,8個位=1個字節。內存編號是存儲數據的地址,變量標識符(變量名)指向了數據存儲的物理地址(內存編號)。
數值的存儲規則
1.該數的二進制數的位佔不滿內存劃分的位時,系統會把0放置在該二進制數以前進行填充直到佔滿爲止。
根據你聲明的值類型的可存儲最大位數,cpu自動爲其劃分對應位數的棧,下圖塗色區域是可存儲8位的棧,其它以此類推。
如今假設咱們要聲明一個int類型的變量來存儲3。
數字3的二進制數是11,該數只佔2個bit位,2個位佔不滿int所聲明的32個位,因此11前面會被填充30個0:
0000 0000 0000 0000 0000 0000 0000 0011(看下圖)
補0時從內存編號的高位開始補起,下圖中能夠看到是從10000003的區域開始補0:
999999999的二進制數是1110 1110 0110 1011 0010 0111 1111 11,該數只佔30個位,30個位佔不滿int所聲明的32個位,因此該數前面會被填充2個0:
0011 1011 1001 1010 1100 1001 1111 1111(看下圖,3的二進制數已佔滿從內存編號10000000開始到10000003的區域,因此999999999的二進制數將劃分在後面,灰色部分)
補0(補碼)時從內存編號的高位開始補起,下圖中能夠看到是從10000007的區域開始補0:
2.負數的存儲是把當前數字的絕對值的滿位後的二進制數按位取反再+1的形式來表示,流程是:1.取絕對值。2.轉化爲二進制數。3.不滿位數則以0補位。4.按位取反:1變0,0變1。5.用結果數+1。6.用結果數逢二進一。按位取反稱爲反碼,+1稱爲補碼。
假設如今要用short存儲數字-1000,絕對值1000的二進制數是1111 1010 00,該數只有10位,前面要補6個0獲得0000 0011 1110 1000,每一個位取相反數(1的相反數是0)獲得:1111 1100 0001 0111,1111 1100 0001 0111+1=1111 1100 0001 0112,逢二進一獲得:1111 1100 0001 1000,首位的1會被計算機識別爲負號,負號佔了1個位。以下圖:
寫個程序檢測一下:
引用類型分爲三種
1.類(Class)
2.接口(Interface)
3.委託(Delegate)
計算機以數字的二進制形式進行存儲。當聲明瞭一個引用類型的變量時,系統會在棧上默認爲其劃分32個位用於存儲該標識符對該對象地址的引用。這個分配內存的流程以下:
在Main中聲明瞭一個Animal類型的變量時,系統在棧上份內存並把每一個位所有都刷成0,如圖:
接着你new一個對象
此時系統會掃描該對象的成員,上面咱們在該對象的類裏定義了一個32位的ID和一個16位的NameCode ,計算後獲得48個位,系統就在堆上面爲其劃分48個位用來存儲該對象。
從30000001開始先分配32個位,接着分配16個位。完成後,須要把對象在堆上的起始地址(內存編號)30000001轉換爲二進制數,這個二進制數會被填充到棧上,棧就完成了對堆的引用。30000001轉換爲二進制數獲得:
1110 0100 1110 0001 1100 0000 1 (4*6=24位,不夠32位,因此在其高位補足7個0)獲得:
0000 0001 1100 1001 1100 0011 1000 0001 (恰好32位)
這個數字會被填充到剛纔被刷成0的棧上,這樣,棧就完成了對實例對象的引用,30000001的二進制數就成爲了指向Animal對象的真正地址。30000001的二進制數填充到棧後如圖:
這樣animal這個變量在棧上的數據就被刷成了一個內存上的物理地址,這個地址指向了該變量所對應的對象的真正數據。
如今假設你要把animal賦值給另外一個變量,如圖:
此時,系統會把animal在棧上的內存編號所引用的地址(30000001的二進制表示)copy、填充到animal2在棧上的內存編號所佔用的位,假設此時10000004到10000007已經被其它數據佔滿,那麼這個copy會在10000008處開始填充,如圖:
若是類型裏有引用類型的成員,好比一個string類型的成員,那麼系統一樣會在堆上(而非棧上)爲該成員變量分配32個位用來存儲能指向它數據的地址,而後在堆上另闢一塊區域去存儲它的值。
方法在執行時,系統會在棧的高(內存編號從高到低,從下往上爲函數劃份內存)位上爲該方法分配一個stack frame的空間。分配完成後stack frame以下圖,棧幀用於存儲函數做用域並執行。做用域中爲方法的變量分配棧空間,一條規則是這樣的:在哪一個方法中聲明哪些變量,那麼那些變量就由那個方法負責爲它們劃份內存。因此在如下在A方法的做用域中聲明瞭兩個byte類型的變量x和y,因此在棧幀包含的棧上爲x和y劃分了兩個字節,A方法還接收了兩個參數,由於參數是byte類型,因此還會發生值拷貝,這樣,A方法還須要爲i和z劃份內存,Main方法中聲明瞭i和z,因此在Main方法的棧幀上會爲i和z劃份內存,Main方法還接收一個string類型的args參數,因此還須要爲args在堆上劃份內存,再把堆地址填充到Main的棧幀所包含的棧上。
以上的Main方法是caller,因此調用的A方法的兩個參數i和z的內存分配就劃歸給Main管理。看圖:
棧溢出(stack overflow)就是由於運行時,方法的stack frame空間是由高位向低位劃份內存,若是方法有返回值,return後函數終止,它區塊內的全部變量就會當即被銷燬,但若是一直沒有rentun,好比無限遞歸,這會形成一直向上劃份內存,直到低位的區塊被完全佔滿,最終就會致使棧溢出。
最後咱們須要知道,程序運行時,不管是值類型抑或引用類型,當程序執行到聲明這些變量的代碼的時候,就會爲其劃分對應的內存,而後將每個位都刷成0,直到賦值後纔會有數據。這就是爲何當聲明一個變量卻不使用它時,編譯器會提示你還未使用過該變量,由於當程序運行起來後未使用的變量會浪費內存資源。
咱們能夠把棧當作小盒子,把堆當作大箱子。裝箱拆箱是指不一樣數據類型之間的轉換。
由於值類型較小而引用類型較大,把小數據裝進大箱子是裝得下的,因此裝箱是屬於隱式進行。把大數據裝進小盒子不必定裝得下,對象裝進小盒子就有可能拆掉大箱子後都裝不下,因此拆箱是屬於顯示或強制進行,系統不會自動爲你轉換,這須要你本身手動顯示或強制轉換,裝箱拆箱須要一個裝或拆的過程,因此大量裝和拆就會形成性能損耗。
假設有一個Animal類型的變量,若是直接把這個變量賦值給另外一個Animal變量,這個行爲不叫拷貝,應叫作賦值,這會使兩個Animal變量指向同一個堆上的地址。如今假設Animal有一個int類型的ID字段和一個Person類型的person字段,當拷貝Animal對象時,你有兩個選擇:
1.只拷貝Animal對象的ID,只拷貝Animal對象的person指向的堆地址,此爲淺拷貝。
2.拷貝Animal對象的ID,拷貝Animal對象的person的數據,此爲深拷貝。
你可使用Object的MemberwiseClone方法建立對某對象的淺拷貝。MemberwiseClone是一個受保護的方法,只能在Object的派生類的類代碼塊中使用,該方法返回一個淺拷貝的對象,該對象只拷貝了源對象的值類型的成員,而引用類型的成員則只有一個堆引用地址。
從上面代碼的結果可知,改變p2的成員department,則p的department也會跟着被改變。由於p2的department修改了堆上的值,而深度拷貝能夠解決這個問題。
轉換
對於值類型來講,小轉大,大能存儲小,因此隱式轉換就能夠完成。 大轉小,小不能存儲大,不被容許,因此必須顯示甚至強制轉換。對於引用類型來講,子類轉父類/基類,子派生自父類/基類,因此隱式轉換就能夠完成。父類/基類轉小,父類/基類並不從子類派生,因此不存在轉換問題。
隱式轉換:直接賦值
相同類型之間,小轉大,可隱式轉換。隱式轉換就是編譯器自動進行轉換,不須要你親自動手。
顯示轉換:(類型)變量
相同類型之間,大轉小,精度丟失,編譯器會提示錯誤,此時須要你親自動手顯示轉換。
強制轉換:Convert.Toxxx( )方法 | Parse()方法
不一樣類型之間進行轉換時才須要強轉,編譯器沒法推測轉換結果,這種轉換若是出錯只能在運行時拋出異常。也即強制轉換就是告訴編譯器不要插手個人邏輯,我對個人行爲負責。一般狀況都是須要將一個object類型轉換爲其它類型時使用強轉。
安全強制轉換:TryParse()方法 | as操做符
這是最保險的方法,TryPrase方法是值類型的方法,它接受兩個參數,一個是被轉換的操做數,另外一個是out類型的操做數。該方法測試操做數是否可被轉換,並返回一個bool值,若是結果爲真,就把轉換結果給out變量,爲假則不。as操做符是引用類型的操做符,它測試當前操做數的類型是否能夠轉換爲目標類型,若是不能則返回null,該操做符不會由於轉換失敗拋出異常。
非轉換:toString()
任何類型都繼承了Object類,它提供了toString()方法,既然是任何類型,則結構類型一樣可使用toString(),但null由於沒有指向堆上的地址,因此爲null的變量使用該方法會拋錯。
自定義顯示轉換:關鍵字explicit
自定義隱式轉換:關鍵implicit