C# 類型基礎(下)
前面介紹了基本的類型,接下來我們講講類型的轉換
值類型的兩種表現形式:未裝箱和已裝箱 ,而引用類型總是處於裝箱形式
int count = 10;
object obj = count;
裝箱:值類型轉換為引用類型,C#編譯器可以自動完成裝箱操作
a.在托管堆中分配好內存。內存量 = 值類型字段的內存量 + 類型對象指針 + 同步索引塊
b.將值類型的字段復制到新分配的堆地址中
c.返回對象的地址
int count1 = (int)obj;
拆箱:引用類型轉換為值類型,需要顯式完成
a.獲取obj對象的引用
b.將值從堆中復制到基於棧的值類型實例 coun1中
c.如果obj的引用地址為null,則拋出 NullReferenceException 異常
d.如果obj引用指向的的對象不是int類型的已裝箱的實例,拋出 InvalidCastException 異常
千言萬語,我只想上代碼!
Int32 a = 5 ; object o = a; Int16 b = (Int16) o ;
上面拆箱能否成功?
答案是不能,因為Int32的範圍比Int16大,轉換的時候就拋異常了。
接下來這個例子很有意思,好多人估計都不知所以然。來,做好了,我們繼續開車!
Int32 v = 5 ; object o = v ; v = 123; Console.WriteLine( v + “and “ + (Int32) o);
問題:上面例子發生了多少次裝箱操作 ?
有的人看到代碼就一拍腦袋說:1次,2次......,好吧,這麽說我不怪你,因為即使工作幾年的老司機也不一定能一眼看出來是幾次裝箱。
但是當你覺得不太確定的時候就要去找出答案,撥開迷霧才能看到真相。我們來個簡單粗暴的方法,看IL代碼:
清楚了吧,明白了吧,簡直是一目了然啊,三個box,那就是三次裝箱了,中間還發生過一次拆箱,那就是Int32轉的那一次
那麽為什麽是三次呢?
第一次很明顯,第二次和第三次是發生在 Console.WriteLine 裏面的,我們看到箭頭標註的地方,為什麽會調用了 string.Concat方法呢,首先我們知道這個函數是用來拼接字符串的,那就稍微有點明白了,我們可以看到現在給WriteLine 方法傳的是3個參數,那實際上WriteLine 有沒有三個參數的重載呢?答案是有,但是很遺憾,並不是符合我們給的三個參數類型的。那怎麽辦呢,我們知道編譯器是非常聰明的,它非常確信的知道我們傳入的三個參數中第二個是個字符串,它會默認調用WriteLine 的string重載方法,那這樣的話就要求傳入的是一個完整的string對象,而我們是三個,那就需要把三個參數合成一個,於是乎編譯器很聰明的自動調用了string.Concat 方法,接收三個參數,而Concat方法接收的三個參數都是object的,所以一切都明白了,第一個參數裝箱一次,第三個參數又裝箱一次,所以總共就是三次裝箱。
上面的代碼怎麽能減少裝箱次數?最少用幾次裝箱?各位看官自己想想吧,這個已經很簡單了
分析完上邊的例子,按照慣例我們接著上代碼:
Int32 v = 5 ; object o = v ; v = 123; Console.WriteLine( v ); v = (Int32) o; Console.WriteLine( v );
同樣的問題:上述代碼輸出什麽結果?發生了多少次裝箱 ?
答案會是一樣嗎?自己思考一下吧,如果不確定可以在評論裏說,我會給出分析。
類型轉換
對象類型轉換:
a.將對象轉換為它對應的任何基類型,反之則不行
b.使用as操作符來轉型,強制類型轉換
基元類型轉換:
隱式轉換:編譯器確定轉換“安全”的時候,才允許隱式轉換。對於數值類型,不安全意味著轉換可能會丟失精度或者數量級
int32 a = 5 ;
int64 b = a ;
顯式轉換:顯示指定需要轉換的類型
Byte c = (Byte) a ;
對基元類型執行的許多算術運算符都可能造成溢出,比如下面代碼:
Byte b = 100 ; b = (Byte) (b + 200)
因為Byte的默認長度是255,而相加之後的結果已經超出了最大長度了,但是運行並沒有報錯,這是為什麽呢?答案是編譯器在作怪
大多數的溢出都是悄悄發生的,編譯器並不會報錯,但是大多數情況下都會導致程序行為異常
C# 編譯器允許開發人員決定如何處理溢出,編譯器溢出檢查默認是關閉的,我們可以手動打開檢查溢出的開關
為了處理溢出,我們講講下面這兩個操作符
checked 和 unchecked 操作符
Byte b = 100 ; b = checked((Byte) (b + 200)); // 拋出OverflowException 異常 checked 語句 checked{ //開始一個checked塊 Byte b = 100 ; b =(Byte) (b + 200) ; // 溢出檢查 } // 結束一個checked塊
相信大家已經看得很清楚了,加了checked之後就會拋出異常,而恰巧編譯器又是默認的unchecked。
那麽checked和unchecked 本質上的區別是什麽呢? 據說按慣例我又要上代碼了,來,我們繼續
本質區別就是生成的IL 指令不一樣 ,指令決定了是否檢查溢出
checked:add.ovf unchecked:add
最後我們再來說說創建一個對象的過程究竟發生了什麽事:
創建類型的對象
Person person = new Person();
new 做了什麽事情?
1.計算類型及所有基類型中定義的所有實例字段需要的字節數 (堆上的每個對象都需要一些額外的成員信息:類型對象指針+同步索引塊,這些成員用於CLR管理對象,會計入對象的大小)
2.從托管堆中分配指定類型要求的字節數,從而分配對象的內存,分配的所有字節都為0
3.初始化對象的類型對象指針 和 同步索引塊
4. 調用類型的實例構造器,同時初始化類型的實例字段,最終調用的都是基類的構造器
5.返回指向新建對象的引用地址
垃圾回收器檢查托管堆中是否有不再使用的任何對象就回收
最後留個問題,歡迎一起討論:new 是創建對象,分配內存,如果創建完之後發現多余了,是否可以delete掉對象 ?
其實還想說一句:一直以來.NET程序員備受鄙視,因為微軟麻麻對我們太好了,基本不需要我們做什麽,編譯器和CLR已經替我們做了太多的事情了,導致我們就只會用,只知道怎麽用而不是到為什麽,這對我們的長期發展來說是非常不好的,所以希望大家有時間多看看底層的東西,多看看IL代碼,搞清楚編譯器在中間幹了什麽事情,這是很重要的。
再給大家推薦一本書:《C# Via CLR》講的非常好的一本書,很底層
C# 類型基礎(下)