CLR via C#深解筆記三 - 基元類型、引用類型和值類型 | 類型和成員基礎 | 常量和字段

編程語言的基元類型
 
某些數據類型如此經常使用,以致於許多編譯器容許代碼以簡化的語法來操縱它們。
System.Int32 a = new System.Int32();  // a = 0
a = 1;
 
等價於:
int a = 1;
這種語法不只加強了代碼的可讀性,其生成的IL代碼與使用System.Int32時生成的IL代碼是徹底一致的。
 
編譯器直接支持的數據類型稱爲基元類型(primitive type)。基元類型直接映射到Framework類庫(FCL)中存在的類型。如C#中,int直接映射System.Int32類型。
C#的語言規範稱:「從風格上說,最好是使用關鍵字,而不是使用完整的系統類型名稱。」其實,也許使用FCL類型名稱,避免使用基元類型名稱纔是更好的作法。
 
CLR支持兩種類型:引用類型和值類型。FCL中的大多數類型都是引用類型,可是程序員用得最多的仍是值類型。
 
引用類型
引用類型老是從託管堆上分配的,C#的new 操做符會返回對象的內存地址 - 也就是指向對象數據的內存地址。使用引用類型時候,須注意到一些性能問題,即一下事實:
#1, 內存必須從託管堆上分配
#2, 堆上分配的每一個對象都有一些額外的成員,這些成員必須初始化
#3, 對象中的其餘字節(爲字段而設)老是設爲零
#4, 從託管堆上分配一個對象時,可能強制執行一次垃圾收集操做
 
值類型
爲了儘量的提升性能,提高簡單的、經常使用的類型的性能,CLR提供了名爲「值類型」的輕量級類型。值類型通常在線程棧上分配(由於也會做爲字段,嵌入一個引用類型的對象中)。
在表明值類型實例的一個變量中,並不包含一個指向實例中的指針。相反,值類型的變量中包含了這個實例自己的字段(值),那麼操做實例中的字段,也就再也不須要提領一個指針。值類型的實例不受垃圾回收器的控制。
 
值類型的設計和使用的好處以下:
#1, 緩解了託管堆中的壓力
#2, 減小了應用程序在其內存期內須要進行的垃圾回收次數
 
自定義值類型不可有基類型,可是能夠實現一個或則多個接口。除此以外,全部的值類型都是隱式密封的(sealed),目的是防止將一個值類型做爲其餘任何引用類型或者值類型的基類型。
 
 
設計本身的類型時,仔細考慮是否應該將一個類型定義成值類型,而不是定義成引用類型。某些時候,值類型是能夠提供更好的性能的。
值類型不在堆上分配內存,因此一旦定義了該類型的實例的方法不在處於活動狀態,爲它們分配的存儲就會被釋放,這也意味着類型的實例在其內存被回收時,不會經過Finalize方法接受到一個通知。
 
CLR如何控制類型中的字段的佈局
爲了提升性能,CLR能按照它所選擇的任何方式來排列類型的字段。如,CLR能夠在內存中從新安排字段的順序,從而將對象引用分爲一組,同時正確排列和填充數據字段。然而,在定義一個類型時,針對類型的各個字段,
你能夠指示CLR是嚴格按照本身指定的順序排列,仍是採起CLR本身認爲合適的方式從新排列。System.Runtime.InteropServices.StructLayoutAttribute。這反映在面向CLR的編譯器作的事情。如Microsoft C# 編譯器默認爲引用類型(類)選擇LayoutKind.Auto, 而爲值類型(結構)選擇LayoutKind.Sequential。
 
值類型的裝箱和拆箱
值類型是比引用類型「輕型」的一種類型,由於他們不做爲對象在託管堆中分配,不會被垃圾回收,也不經過指針來引用。
不少狀況下,都須要獲取對值類型的一個實例的引用。若是要獲取對值類型的一個實例的引用,該實例就必須裝箱。ArrayList對象的Add方法 public virtual Int32 Add(Object value);
 
ArrayList.Add須要獲取對託管堆上的一個對象的引用(或指針)來做爲參數。如果須要向ArrayList填充一組Int32數字,那麼Int32數字必須轉換成一個真正的、在託管堆的對象,並且獲取對這個對象的一個引用。
 
如何將一個值類型轉換成一個引用類型,要使用一個名爲裝箱(boxing)的機制。那麼,值類型的一個實例進行裝箱操做時在內部發生的事情:
#1, 在託管堆中分配好內存。分配的內存量是值類型的各個字段須要的內存量加上託管堆的全部對象都要的兩個額外成員(類型對象指針和同步塊索引)須要的內存量。
#2, 值類型的字段複製到新分配的對內存。
#3, 返回對象的地址。這個地址正是對這個對象的引用,值類型如今是一個引用類型。
 
注意,已裝箱值類型的生存期超過了未裝箱的值類型的生存期。FCL如今包含一組新的泛型集合類,它們使非泛型的集合類成爲「過期」的東西。例如,泛型集合類進行了大量加強,性能也顯著提高。
最大的一個加強就是泛型集合類容許開發人員在操做值類型的集合時不須要對集合中的項進行裝箱/拆箱處理。除了提升性能上,還得到了編譯時的類型安全性,源代碼也由於強制類型轉換的次數減小而變得更加清晰。
 
裝箱以後,每每還要面臨拆箱的可能。拆箱不是講裝箱倒過來進行,其代價也比裝箱低不少。 如:Int32 i = (Int32)a[0]; 拆箱操做分爲兩步:
#1, 獲取已裝箱的對象中的各個字段的地址,這個過程就是拆箱。
#2, 將這些字段包含的值從堆中複製到基於棧(線程棧)的值類型實例中。
 
簡單地說,若是獲取對值類型的一個實例的引用,該實例就必須裝箱。如上的ArrayList.Add 方法,將一個值類型的實例傳給須要獲取一個引用類型的方法,就會發生這種狀況。
前面提到,未裝箱的值類型是比引用類型更「輕型」的類型。這要歸結於一下兩個緣由:
#1, 它們不在託管堆上分配。
#2, 它們沒有堆上的每一個對象都要的額外成員,也就是一個「類型對象指針」和一個「同步塊索引」。
因此,因爲未裝箱的值類型沒有同步塊索引,就不能使用System.Threading.Monitor類型的各類方法(或者C#的lock語句)讓多個線程同步對這個實例的訪問。
 
關於值類型的幾點說明:
#1, 值類型能夠重寫Equals, GetHashCode或者ToString的虛方法,CLR能夠非虛地調用該方法,由於值類型是隱式密封的(即不存在多態性),沒有任何類型可以從它們派生。
#2, 此外,用於調用方法的值類型實例不會被裝箱。可是,若是你重寫的虛方法要調用方法在基類中的實現,那麼在調用基類的實現時,值類型實例就會裝箱,以便經過this指針將對一個堆對象的引用傳給基方法。
#3, 值類型調用一個非虛的、繼承的方法時(好比GetType或MemberwiseClone),不管如何都要對值類型進行裝箱。這是由於這些方法是由System.Object定義的,因此這些方法指望this實參是指向堆上一個對象的指針。
#4, 將值類型的一個未裝箱實例轉型爲類型的某個接口時,要求對實例進行裝箱。這是由於接口變量必須包含對堆上的一個對象的引用。
 
感言:
任何.NET Framework開發人員只有在切實理解了這些概念以後,才能保證本身開發程序的長期成功。由於只有深入理解了以後,才能更快、更輕鬆地構建高效率的應用程序。
 
重要提示:
在值類型中定義的成員不該該修改類型的任何實例字段。也就是說,值類型應該是不可變(immutable)的。事實上,我建議將值類型的字段都標記爲readonly。這樣一來,若是不慎寫了一個方法企圖更改一個字段,編譯就沒法經過。
由於假如一個方法企圖修改值類型的實例字段,由於裝箱的變化,調用這個方法就會產生非預期的行爲。構造好一個值類型以後,若是不去調用任何會修改其狀態的方法(或者若是根本不存在這樣的方法),就不用再爲何時候會發生裝箱和拆箱/字段複製而擔憂。若是一個值類型是不可變的,只需簡單地複製相同的狀態就能夠了(不用擔憂任何方法會修改這些狀態),代碼的任何行爲都將在你的掌握之中。
也許,你在看到值類型的這些細微末節時遠離自定義值類型,或者你歷來就沒用過自定義值類型。可是,FCL的核心值類型(Byte, Int32, ... 以及全部的enums 都是「不可變」的);而且瞭解並記住這些可能問題,當代碼真正出現這些問題的時候,也就會心中有數。
 
 
對象相等性和同一性
對於Object的Equals方法的默認實現來講,它實現的實際是同一性(identity),而非相等性(equality)。
 
對象哈希碼
一種算法,讓同一個類的對象按照本身不一樣的特徵儘可能的有不一樣的哈希碼,但不表示不一樣的對象哈希碼徹底不一樣。」哈希碼就是對象的身份證。「
 
dynamic
dynamic表達式實際上是和System.Object同樣的類型。編譯器假定你在表達式上進行的任何操做都是合法的,因此不會生成任何警告或者錯誤。但若是試圖在運行時執行無效的操做,就會拋出異常。
 
 
類型和成員基礎
 
類型的各類成員
常量、字段、實例構造器、類型構造器、方法、操做符重載、轉換操做符、屬性、事件、類型
 
類型的可見性
public: 不只對它的定義程序集中的全部代碼可見,還對其餘程序集中的代碼可見。
internal: 類型僅對定義程序集中的全部代碼可見,對其餘程序集中的代碼不可見。
 
若定義類型時,若是不顯式指定類型的可見性,C#編譯器默認將類型的可見性設爲internal。
 
友元程序集
咱們但願TeamA程序集能有一個辦法將其工具類型定義爲internal, 同時仍然容許團隊TeamB訪問這些類型。
CLR和C#經過友元程序集(friend assembly)來提供這方面的支持。若是但願在一個程序集中包含代碼,對另外一個程序集中的內部類型執行單元測試,友元程序集功能也能派上用場。
 
成員的可訪問性
CLR本身定義了一組可訪問性修飾符,但每種編程語言在向成員應用可訪問性時,都選擇了本身一組術語以及相應的語法。如,CLR使用Assembly來代表成員對同一程序集內的全部代碼可見,而C#對應的術語是internal。
C#: private, protected, internal, protected internal, public
一個派生類型重寫在它的基類型中定義的一個成員時,C#編譯器要求原始成員和重寫成員具備相同的可訪問性。也就是說,若是基類中的成員是protected的,派生類中的重寫成員必須也是protected的。可是,這只是C#語言自己的一個限制,而不是CLR的。從一個基類派生時,CLR容許放寬成員的可訪問性限制,但不容許收緊。之因此不容許在派生類中將對一個基類方法的訪問變得更嚴格,是由於CLR承諾派生類老是能夠轉型爲基類,並獲取對基類方法的訪問權。若是容許在派生類中對重寫方法進行更嚴格的訪問限制,CLR的承諾就沒法兌現了。
 
分部類、結構和接口
這個功能徹底是由C#編譯器提供的,CLR對於分部類、結構和接口是一無所知的。partial 這個關鍵字告訴C#編譯器,一個類,結構或者接口的定義,其源代碼可能要分散到一個或者多個源代碼文件中。
 
CLR如何調用虛方法、屬性和事件
方法表明在類型或者類型的實例上執行某些操做的代碼。在類型上執行操做,稱爲靜態方法;在類型的實例上執行操做,稱爲非靜態方法。任何方法都有一個名稱、一個簽名和一個返回值(能夠是void)。
 
合理使用類型的可見性和成員的可訪問性
使用.Net Framework時,應用程序極可能是使用多個公司聲場的多個程序集所定義的類型構成的。這意味着開發人員對所用的組件(程序集)以及其中定義的類型幾乎沒有什麼控制權。開發人員一般沒法訪問源代碼(甚至不知道組件是用什麼編程語言建立的),並且不一樣組件的版本發佈通常都基於不一樣的時間表。除此以外,因爲多態和受保護(protected)成員,基類開發人員必須信任派生類開發人員所寫的代碼。固然,派生類的開發人員也必須信任從基類繼承的代碼。設計組件和類型時,應該慎重考慮這些問題。具體地說,就是要着重討論如何正確設置類型的可見性和成員的可訪問性,以便取得最好的結果。
 
在定義一個新類型時,編譯器應該默認生成密封類,使它不能做爲基類使用。可是包括C#編譯器在內的許多編譯器都默認生成非密封類,固然容許開發人員使用sealed顯式地將新類型標記爲密封。
密封類之因此比非密封類更好,有如下三方面緣由:
#1, 版本控制:類開始是密封的,未來能夠不破壞兼容性的前提下更改成非密封的。反之,否則。
#2, 性能:類是密封的,就確定不會有派生類,調用方法時,就不需判斷是哪一個類型定義了要調用的方法,即不須在運行時查找對象的類型,而直接採用非虛的方式調用虛方法。
#3, 安全性和可預測性:派生類既可重寫基類的虛方法,也可直接帶哦用這個虛方法在基類中的實現。一旦將某個方法、屬性或事件設爲virtual,基類就會喪失對它的行爲和狀態的部分控制權。
 
下面是定義類時會遵循的一些原則:
 
 
擴展閱讀:
 
每一個應用程序都要使用這樣或者那樣的資源,好比文件、內存緩衝區、屏幕空間、網絡鏈接、數據庫資源等。事實上,在面向對象的環境中,每一個類型都表明可供程序使用的一種資源。
要使用這些資源,必須爲表明資源的類型分配內存。
 
訪問一個資源所需的具體步驟以下:
#1,調用IL指令newobj, 爲表明資源的類型分配內存。C#中使用new操做符,編譯器就會自動生成該指令。
#2,初始化內存,設置資源的初始狀態,使資源可用。類型的實例構造器負責設置該初始狀態。
#3,訪問類型的成員(可根據須要反覆)來使用資源。
#4,摧毀資源的狀態以進行清理。
#5,釋放內存。垃圾回收將獨自負責這一步。
 
須要注意的是,值類型(含全部枚舉類型)、集合類型、String、Attribute、Delegate和Exception 所表明的資源無需執行特殊的清理操做。如,只要銷燬對象的內存中維護的字符數組,一個String資源就會被徹底清理。
CLR要求全部的資源都從託管堆(managed heap)分配。應用程序不須要的對象會被自動清除。那麼「託管堆又是如何知道應用程序再也不用一個對象?」
 
進程初始化時,CLR要保留一塊連續的地址空間,這個地址空間最初並無對象的物理內存空間。這個地址空間就是託管堆。託管堆還維護着一個指針,我把它稱爲NextObjPtr。指向下一個對象在堆中的分配位置。剛開始時候,NextObjPtr設爲保留地址空間的基地址。
 
IL指令newobj用於建立一個對象。許多語言都提供了一個new操做符,它致使編譯器在方法的IL代碼中生成一個newobj指令。newobj指令將致使CLR執行以下步驟:
#1,計算類型(極其全部基類型)的字段須要的字節數。
#2,加上字段的開銷所需的字節數。每一個對象都有兩個開銷字段:一個是類型對象指針,和一個同步塊索引。
#3,CLR檢查保留區域是否可以提供分配對象所需的字節數,若有必要就提交存儲(commit storage)。若是託管堆有足夠的可用空間,對象會被放入。對象是在NextObjPtr指針指向的地址放入的,而且爲它分配的字節會被清零。接着,調用類型的實例構造器(爲this參數傳遞NextObjPtr), IL指令newobj(或者C# new 操做符)將返回對象的地址。就在地址返回以前,NextObjPtr指針的值會加上對象佔據的字節數,這樣會獲得一個新值,它就指向下一個對象放入托管堆時的地址。
 
 
做爲對比,讓咱們看一下C語言運行時堆如何分配內存,它爲對象分配內存須要遍歷一個由數據結構組成的鏈表,一旦發現一個足夠大的塊,那個塊就會被拆分,同時修改鏈表節點中的指針,以確保鏈表的完整性。
對於託管堆,分配對象只需在一個指針上加一個值 - 這顯然要快得多。事實上,從託管堆中分配對象的速度幾乎能夠與從線程棧分配內存媲美!
另外,大多數堆(C運行時堆)都是在他們找到可用空間的地方分配對象。因此,若是連續建立幾個對象,這些對象極有可能被分散,中間相隔MB的地址空間。但在託管堆中,連續分配的對象能夠確保它們在內存中是連續的。
託管堆彷佛在實現的簡單性和速度方面遠遠優於普通的堆,如C運行時堆。而託管堆之因此有這些好處,是由於它作了一個至關大膽的假設 - 地址空間和存儲是無限的。而這個假設顯然是不成立的,也就是說託管堆必須經過某種機制來容許它作這樣的假設。這個機制就是垃圾回收器。
相關文章
相關標籤/搜索