標籤:ges int 選擇結構 ash system gif 大量 new 指標
一,c#中的實值型別和參考型別
眾所周知在c#中有兩種基本類型,它們分別是實值型別和參考型別;而每種類型都可以細分為如下類型:
- 什麼是實值型別和參考型別
- 什麼是實值型別:
- 進一步研究文檔,你會發現所有的結構都是抽象類別型System.ValueType的直接衍生類別,而System.ValueType本身又是直接從System.Object派生的。根據定義所知,所有的實值型別都必須從System.ValueType派生,所有的枚舉都從System.Enum抽象類別派生,而後者又從System.ValueType派生。
- 所有的實值型別都是隱式密封的(sealed),目的是防止其他任何類型從實值型別進行派生。
- 什麼是參考型別:
- 區別和效能
- 區別:
- 實值型別通常被人們稱為輕量級的類型,因為在大多數情況下,實值型別的的執行個體都分配線上程棧中,因此它不受記憶體回收的控制,緩解了託管堆中的壓力,減少了應用程式的記憶體回收的次數,提高效能。
- 所有的參考型別的執行個體都分配在託管堆上,c#中new操作符會返回一個記憶體位址指向當前的對象。所以當你在建立個一個參考型別執行個體的時候,你必須要考慮以下問題:
- 記憶體是在託管堆上分配的
- 在分配每一個對象時都會包含一些額外的成員(類型對象指標,同步塊索引),這些成員必須初始化
- 對象中的其他位元組總是設為零
- 在指派至時,可能會進行一次記憶體回收操作(如果託管堆上的記憶體不夠分配一次對象時)
- 效能:
- 常見誤區
-
- 參考型別分配在託管堆上,實值型別分配線上程棧上:其實這種說法的前半部分是對的,後半部分是錯的。因為變數的值在它聲明的位置儲存的,所以假如某一個參考型別中有一個實值型別的變數, 那麼該變數的值總是和該參考型別的對象的其它資料在一起,也就是分配在堆上。(只有局部變數(方法內部聲明的變數)和方法的參數在棧上)
- 結構是輕量級的類:這種錯誤的資訊主要是因為有人認為實值型別不應該有方法或者其它有意義的行為-它們應該作為簡單的資料轉移來使用,所以很多人分不清DateTime到底是實值型別還是參考型別。
- 對象在c#中預設的是用過引用傳遞的:其實在調用方法的時候,參數值(對象的一個引用)是以傳值得方式傳遞的,如果你想以引用方式傳遞的話,可以使用ref或者out關鍵字。
二,實值型別的裝箱和拆箱操作
1 int i = 5;2 object o = i;3 int j = (int)o;
4 Int16 y=(Int16)o;
- 什麼是裝箱,什麼是拆箱
- 什麼是裝箱:所謂裝箱就是將實值型別轉化為參考型別的過程(例如上面代碼的第2行),在裝箱時,你需要知道編譯器內部都幹了什麼事:
- 在託管堆中分配好記憶體,分配的記憶體量是實值型別的各個欄位需要的記憶體量加上託管堆上所以對象的兩個額外成員(類型對象指標,同步塊索引)需要的記憶體量
- 實值型別的欄位複製到新分配的堆記憶體中
- 返回對象的地址,這個地址就是這個對象的引用
- 什麼是裝箱:將已裝箱的實值型別執行個體(此時它已經是參考型別了)轉化成實值型別的過程(例如上面代碼的第3行),注意:拆箱不是直接將裝箱過程倒過來,拆箱的代價比裝箱要低的多,拆箱其實就是擷取一個指標的過程。一個已裝箱的執行個體在拆箱時,編譯器在內部都幹了下面這些事:
- 如果包含了“對已裝箱類型的執行個體引用”的變數為null時,會拋出一個NullReferenceException異常。
- 如果引用指向的對象不是所期待的實值型別的一個已裝箱執行個體,會拋出一個InvalidCastException異常(例如上面代碼的第4行)。
- 它們在什麼情況下發生,以及如何避免
1 static void Main(string[] args)2 {3 int v = 5;4 object o = v;5 v = 123;6 Console.WriteLine(v+","+(int)o);7 } 通過上面的分析我們已經知道了,裝箱和拆箱/複製操作會對應用程式的速度和記憶體消耗產生不利的影響(例如消耗記憶體,增加記憶體回收次數,複製操作),所以我們應該注意編譯器在什麼時候會產生代碼來自動這些操作,並嘗試手寫這些代碼,盡量避免自動產生代碼的情況。
你能一眼從上面的代碼中看出進行了幾次裝箱操作嗎?正取答案是3次。分別進行了哪三次呢,我們來看一下:第一次object o=v;第二次在執行 Console.WriteLine(v+","+(int)o);時將v進行裝箱,然後對o進行拆箱後又裝箱。也就是說裝箱過程總是在我們不經意的時候進行的,所以只有我們充分瞭解了裝箱的內部機制,才能有效避免裝箱操作,從而提高應用程式的效能。所以對上面的代碼進行如下修改可以減少裝箱次數,從而提高效能:
1 static void Main(string[] args)2 {3 int v = 5;4 object o = v;5 v = 123;6 Console.WriteLine(v.ToString() + "," + ((int)o).ToString());//((int)o).ToString()代碼本身沒有任何意義,只為示範裝箱和拆箱操作7 }
- 下面來討論一下編譯器都會在什麼時候自動產生代碼來完成這些操作
- 使用非泛型集合時:比如ArrayList,因為這些集合需要的對象都是object,如果你將一個實值型別的對象添加到集合中時會執行一次裝箱操作,當你取值時會執行一次拆箱操作,所以在應用程式中應避免使用這種非泛型的集合。
- 大家都知道System.Object是所有類型的基類,當你調用object類型的非虛方法時會進行裝箱操作(例如GetType方法)。在調用object的虛方法時,如果你的實值型別沒有重寫虛方法也要進行裝箱操作,所以在定義自己的實值型別時,應重寫object內部的虛方法(例如ToString方式)
- 將實值型別轉化為介面類型時也會進行裝箱操作,這是因為介面類型必須包含對堆上的一個對象的引用。
三,泛型的出現(本節只簡單介紹泛型對裝箱和拆箱所起的作用,關於泛型的具體細節請參考下一篇文章)
四,在設計時如何選擇類和結構體
在面試的時候,我們經常被問的一個問題(還有另外一個問題,如何選擇抽象類別和介面,下次我會單獨聊聊這個問題),下面我們來聊聊在設計時應該如何選擇結構體和類
-
- 什麼是結構體
- 結構體是一種特殊的實值型別,所以它擁有實值型別所以的特權(執行個體一般分配線上程棧上)和限制(不能被派生,所以沒有 abstract 和 sealed,未裝箱的執行個體不能進行線程同步的訪問)。
- 什麼情況下選擇結構體,什麼情況下選擇類
- 在大多數的情況下,都應該選擇類,除非滿足以下情況,才考慮選擇結構體:
- 類型具有基元類型的行為
- 類型不需要從其它任何類型繼承
- 類型也不會派生出任何其它類型
- 類型的執行個體較小(約為16位元組或者更小)
- 類型的執行個體較大,但是不作為方法的參數傳遞,也不作為方法的傳回值。
都說程式是一門注重實踐的學科,但是也只有熟悉理解了這些概論的東西,才能在實踐時寫出優秀的代碼,有不對或者不合理的地方歡迎在下面討論;
c#中的參考型別和實值型別