1. 程式人生 > >C# 類型基礎(下)

C# 類型基礎(下)

合成 托管 相加 返回 長度 參數類型 一個 con 重載

前面介紹了基本的類型,接下來我們講講類型的轉換

值類型的兩種表現形式:未裝箱和已裝箱 ,而引用類型總是處於裝箱形式

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# 類型基礎(下)