做者簡介:王濤 微軟C# MVP,高級軟件工程師,機械工程碩士,主要研究方向爲.NET底層架構和企業級系統應用。現就任於某軟件公司負責架構設計、軟件開發和項目管理方面的工做。做者對.NET基礎架構和CLR底層運行機制有濃厚的研究興趣和造詣,熟悉ASP.NET、XML、SQL Server相關技術,對面向對象、設計模式和軟件架構有必定的研究與實踐經驗。
第1部分 淵源——.NET與面向對象 第1章 OO大智慧
1.1 對象的旅行 2 1.1 對象的旅行 3 本節將介紹如下內容: 4 — 面向對象的基本概念 5 — .NET基本概念評述 6 — 通用類型系統 7 1.1.1 引言 8 提起面向對象,每一個程序設計者都有本身的理解,有的深刻肌理,有的劍走偏鋒。可是不管所長,幾個基本的概念總會獲得你們的重視,它們是:類、對象、繼承、封裝和多態。很對,差很少就是這些元素構成了面向對象設計開發的基本邏輯,成爲數以千萬計程序設計者不懈努力去深刻理解和實踐的根本。而實際上,理解面向對象一個重要的方法就是以實際的生活來類比對象世界,對象世界的邏輯和咱們生活的邏輯造成對比的時候,這種體驗將會更有親切感,深刻程度天然也就不一樣以往。 9 本節就從對象這一最基本元素開始,進行一次深度的對象旅行,把.NET面向對象世界中的主角來一次遍歷式曝光。把對象的世界和人類的世界進行一些深度類比,以人類的角度戲說對象,同時也以對象的邏輯反思人類。究竟這種旅程,會有什麼樣的洞悉,且看本文的演義。 10 對象和人,兩個世界,同樣情懷。 11 1.1.2 出生 12 對象就像個體的人,生而入世,死而離世。 13 咱們的故事就從對象之生開始吧。首先,看看一個對象是如何出生的: 14 Person aPerson = new Person("小王", 27); 15 那麼一我的又是如何出生呢?每一個嬰兒隨着一聲啼哭來到這個世界,鼻子是鼻子、嘴巴是嘴巴,已經成爲一個活生生的獨立的個體。而母親的懷胎十月是人在母體內的成長過程,母親爲胎兒提供了全部的營養和溫馨的環境,這個過程就是一次實實在在的生物化構造。一樣的道理,對象的出生,也是一次完整的構造過程:首先會在內存中分配必定的存儲空間;而後初始化其附加成員,就像給人取個
具備標識做用的姓名同樣;最後,再調用構造函數執行初始化,這樣一個對象實體就完成了其出生的過程,例如上例中咱們爲aPerson對象初始化了姓名和年齡。 16 正如人出生之時,一身赤裸沒有任何的附加品,其他的一切將隨需而生,生不帶來就是這個意思。對象的出生也只是完成了對必要字段的初始化操做,其餘數據要經過後面的操做來完成。例如對屬性賦值,經過方法獲取必要的信息等。 17 1.1.3 旅程 18 嬰兒一出世,由it成爲he or she,就意味着今後融入了複雜的社會關係,經歷一次在人類倫理與社會規則的雙重標準中生活,開始了爲人的旅程。同理,對象也同樣。 19 做爲個體的人,首先是有類型之分的,農民、工人、學者、公務員等,所造成的社會規則就是農民在田間務農,工人在工廠生產,學者探討知識,公務員管理國家。 20 對象也同樣是有類型的,例如整型、字符型等等。固然,分類的標準不一樣,產生的類別也就不一樣。可是常見的分類就是值類型和引用類型兩種。其依據是對象在運行時在內存中的位置,值類型位於線程的堆棧,而引用類型位於託管堆。正如農民能夠進城務工,工人也能夠回鄉務農,值類型和引用類型的角色也會發生轉變,這個過程在面向對象中稱爲裝箱與拆箱。這一點卻是與剛剛的例子很貼切,農民進城,工人回鄉,不都得把行李裝進箱子裏折騰嘛。 21 做爲人,咱們都是有屬性的,例如你的名字、年齡、籍貫等,用來描述你的狀態信息,同時每一個人也用不一樣的行爲來操做本身的屬性,實現了與外界的交互。對象的字段、屬性就是咱們本身的標籤,而方法就是操做這些標籤的行爲。人的名字來自於長輩,是每一個人在出生之時構造的,這和對象產生時給字段賦值同樣。可是每一個人都有隨時改名的權力,這種操做名稱的行爲,咱們稱之爲方法。在面向對象中,能夠像這樣來完成: 22 aPerson.ChangeName("Apple Boy"); 23 因此,對象的旅行過程,在某種程度上就是外界經過方法與對象交互,從而達到改變對象狀態信息的過程,這也和人的生存之道暗合。 24 人與人之間經過語言交流。人一出生,就必然和這個世界的其餘人進行溝通,造成種種相互的關係,融入這個完整的社會羣體。在對象的世界裏,你得絕對相信對象之間也是相互關聯的,不一樣的對象之間發生着不一樣的交互性操做,那麼對象的交互是經過什麼方式呢?對象的交互方式被記錄在一本稱爲「設計模式」的魔法書中,當你不解以什麼樣的方式創建對象與對象之間的關係時,學習前人的經驗,每每是最好的選擇。 25 下面,咱們簡要地分析一下對象到底旅行在什麼樣的世界裏?
26 對象的生存環境是CLR,而人的生存環境是社會。CLR提供了對象賴以生存的託管環境,制定一系列的規則,稱之爲語法,例如類型、繼承、多態、垃圾回收等,在對象世界裏創建了真正的法制秩序;而社會提供了人行走江湖的秩序,例如法律、規範、道德等,幫助咱們制約個體,維護社會。 27 人類社會就是系統架構,也是分層的。上層建築表明政治和思想,經過社會契約和法律規範爲經濟基礎服務,在對象世界中,這被稱爲接口。面向接口的編程就是以接口方式來抽象變化,從而造成體系。正如人類以法律手段來維繫社會體系的運做和秩序同樣。 28 因而可知,對象的旅行就是這樣一個過程,在必定的約定與規則下,經過方法進行彼此的交互操做,從而達到改變自己狀態的目的。從最簡單的方式理解實際狀況,這些體會與人的旅程如此接近,給咱們的啓示更加感同身受。 29 1.1.4 插曲 30 接下來,咱們以與人類世界的諸多類似之處,來進一步闡釋對象世界的幾個最熟悉的概念。 31 關於繼承。人的社會中,繼承通常發生在有血緣關係的族羣中。最直接的例子通常是,兒子繼承父親,包括姓氏、基因、財產和一切能夠遺留的東西。但並不表明能夠繼承全部,由於父親隱私的那一部分屬於父親獨有,不可繼承。固然,也多是繼承於族羣的其餘人,視實情而定。而在面向對象中,繼承無處不在,子類繼承父類,以訪問權限來實現不一樣的控制規則,稱爲訪問級別,如表1-1所示。 32 表1-1 訪問修改符
訪問修飾符
訪問權限
pubic
對訪問成員沒有限制,屬於最高級別訪問權限
protected
訪問包含類或者從包含類派生的類
internal
訪問僅限於程序集
protected internal
訪問僅限於從包含類派生的當前程序集或類型。也就是同一個程序集的對象,或者該類及其子類能夠訪問
private
訪問僅限於包含類型 33 這些規則能夠以公司的體制來舉例說明,將公司職權的層級與面向對象的訪問權限層級作類比,應該是這樣: 34 — public,具備最高的訪問權限,就像是公司的董事會具備最高的決策權與管理權,所以public開放性最大,無論是否同一個程序集或者無論是否繼承,均可以訪問。 35 — protected,相似於公司業務部門經理的職責,具備對本部門的直接管轄權,在面向對象中就體現爲子類繼承這種縱向關係的訪問約定,也就是隻要繼承了該類,則其對象就有訪問父類的權限,而無論這兩個具備繼承關係的類是否在同一個程序集中。 36 — internal,具備類比意義的就是internal相似於公司的職能部門的職責,無論是否具備上下級的隸屬關係,人力資源部都能管轄全部其餘部門的員工考勤。這是一種橫向的職責關係,在面向對象中用來
表示同一程序集的訪問權限,只要是隸屬於同一程序集,對象便可訪問其屬性,而無論是否存在隸屬關係。 37 — protected internal,能夠看作是protected internal的並集,就像公司中掌管職能部門的副總經理,從橫向到縱向都有管理權。 38 — private,具備最低的訪問權限,就像公司的通常員工,管好本身就好了。所以,對應於面向對象的開放性最小。 39 另外,對象中繼承的目的是提升軟件複用,而人類中的繼承,不也是現實中的複用嗎? 40 而關於多態,人的世界中,咱們經常在不一樣的環境中表現爲不一樣的角色,而且遵照不一樣的規則。例如在學校咱們是學生,回到家裏是兒女,而在車上又是乘客,同一我的在不一樣的狀況下,表明了不一樣的身份,在家裏你能夠撒嬌可是在學校你不能夠,在學校你能夠打球但在車上你不能夠。因此這種身份的不一樣,帶來的是規則的差別。在面向對象中,咱們該如何表達這種複雜的人類社會學呢? 41 interface IPerson 42 { 43 string Name 44 { 45 get; 46 set; 47 } 48 Int32 Age 49 { 50 get; 51 set; 52 } 53 void DoWork(); 54 } 55 class PersonAtHome : IPerson 56 { 57 } 58 class PersonAtSchool : IPerson 59 { 60 } 61 class PersonOnBus : IPerson 62 {
63 } 64 顯然,咱們讓不一樣角色的Person繼承同一個接口:IPerson。而後將不一樣的實現交給不一樣角色的人自行負責,不一樣的是PersonAtHome在實現時多是CanBeSpoil(),而PersonOnBus多是BuyTicket()。不一樣的角色實現不一樣的規則,也就是接口協定。在使用上的規則是這個樣子: 65 IPerson aPerson = new PersonAtHome(); 66 aPerson.DoWork(); 67 另外一個角色又是這個樣子: 68 IPerson bPerson = new PersonOnBus(); 69 bPerson.DoWork(); 70 由此帶來的好處是顯而易見的,咱們以IPerson表明了不一樣角色的人,在不一樣的狀況下實現了不一樣的操做,而把決定權交給系統自行處理。這就是多態的魅力,其樂無窮中,帶來的是面向對象中最爲重要的特性體驗。記住,很重要的一點是,DoWork在不一樣的實現類中體現爲同一命名,不一樣的只是實現的內部邏輯。 71 這和咱們的規則多麼一致呀! 72 固然,有必要補充的是對象中的多態主要包括如下兩種狀況: 73 — 接口實現多態,就像上例所示。 74 — 抽象類實現多態,就是以抽象類來實現。 75 其細節咱們將在1.4節「多態的藝術」中加以詳細討論。 76 因而可知,以咱們本身的角度來闡釋技術問題,有時候會有意想不到的收穫,不然你將被淹沒在諸如「爲何以這種方式來實現複用」的叫喊中不能自拔。換一個角度,眼界與思路都會更加開闊。 77 1.1.5 消亡 78 對象和人,有生必然有死。在對象的世界裏,它的生命是由GC控制的,而在人的世界裏咱們把GC稱爲天然規律。進入死循環的對象,是違反規則的,必然沒法逃脫被Kill的命運,就如同沒有長生不死的人同樣。 79 在這一部分,咱們首先觀察對象之死,以此反思和體味人類入世的哲學,二者相比較,也會給咱們更多關於本身的啓示。對象的生命週期由GC控制,其規則大概是這樣:GC管理全部的託管堆對象,當內存回收執行時,GC檢查託管堆中再也不被使用的對象,並執行內存回收操做。不被應用程序使用的對象,指的是對象沒有任何引用。關於如何回收、回收的時刻,以及遍歷可回收對象的算法,是較爲複雜的問題,咱們將在5.3節「垃圾回收」中進行深度探討。不過,這個回收的過程,一樣使咱們感慨。大天然就是那個看不見的GC,造物而又終將萬物回收,沒法改變。咱們所能作到的是,將生命的週期拓寬、延長、書寫得更加精彩。
80 1.1.6 結論 81 程序世界其實和人類世界有不少類似的地方,本節就以這種類比的方式來詮釋這兩個世界的主角:對象和人。以演化推動的手法來描述面向對象程序世界的主角對象由生而死的全過程,好似複雜的人生。而其實,人也能夠是簡單的。這是一種相互的較量,也是一種相互的借鑑。
對象建立始末(上)
本文將介紹如下內容:
對象的建立過程
內存分配分析
內存佈局研究
1. 引言
瞭解.NET的內存管理機制,首先應該從內存分配開始,也就是對象的建立環節。對象的建立,是個複雜的過程,主要包括內存分配和初始化兩個環節。例如,對象的建立過程能夠表示爲: FileStream fs = new FileStream(@"C:"temp.txt", FileMode.Create);
經過new關鍵字操做,即完成了對FileStream類型對象的建立過程,這一看似簡單的操做背後,卻經歷着至關複雜的過程和周折。
本篇全文,正是對這一操做背後過程的詳細討論,從中瞭解.NET的內存分配是如何實現的?
2. 內存分配
關於內存的分配,首先應該瞭解分配在哪裏的問題。CLR管理內存的區域,主要有三塊,分別爲:
線程的堆棧,用於分配值類型實例。堆棧主要由操做系統管理,而不受垃圾收集器的控制,當值類型實例所在方法結束時,其存儲單位自動釋放。棧的執行效率高,但存儲容量有限。
GC堆,用於分配小對象實例。若是引用類型對象的實例大小小於85000字節,實例將被分配在GC堆上,當有內存分配或者回收時,垃圾收集器可能會對GC堆進行壓縮,詳情見後文講述。
LOH(Large Object Heap)堆,用於分配大對象實例。若是引用類型對象的實例大小不小於85000字節時,該實例將被分配到LOH堆上,而LOH堆不會被壓縮,並且只在徹底GC回收時被回收。
本文討論的重點是.NET的內存分配機制,所以下文將不加說明的以GC堆上的分配爲例來展開。關於值類型和引用類型的論述,請參見[第八回:品味類型---值類型與引用類型(上)-內存有理]。
瞭解了內存分配的區域,接着咱們看看有哪些操做將致使對象建立和內存分配的發生,關於實例建立有多個IL指令解析,主要包括:
newobj,用於建立引用類型對象。
ldstr,用於建立string類型對象。
newarr,用於分配新的數組對象。
box,在值類型轉換爲引用類型對象時,將值類型字段拷貝到託管堆上發生的內存分配。
在上述論述的基礎上,下面從堆棧的內存分配和託管堆的內存分配兩個方面來分別論述.NET的內存分配機制。
2.1 堆棧的內存分配機制
對於值類型來講,通常建立在線程的堆棧上。但並不是全部的值類型都建立在線程的堆棧上,例如做爲類的字段時,值類型做爲實例成員的一部分也被建立在託管堆上;裝箱發生時,值類型字段也會拷貝在託管堆上。
對於分配在堆棧上的局部變量來講,操做系統維護着一個堆棧指針來指向下一個自由空間的地址,而且堆棧的內存地址是由高位到低位向下填充。如下例而言: public static void Main() { int x = 100; char c = 'A'; }
假設線程棧的初始化地址爲50000,所以堆棧指針首先指向50000地址空間。代碼由入口函數Main開始執行,首先進入做用域的是整型局部變量x,它將在棧上分配4Byte的內存空間,所以堆棧指針向下移動4個字節,則值100將保存在49997~50000單位,而堆棧指針表示的下一個自由空間地址爲
49996,如圖所示:
接着進入下一行代碼,將爲字符型變量c分配2Byte的內存空間,堆棧指針向下移動2個字節至49994單位,值’A’會保存在49995~49996單位,地址的分配如圖:
最後,執行到Main方法的右括號,方法體執行結束,變量x和c的做用域也隨之結束,須要刪除變量x和c在堆棧內存中的值,其釋放過程和分配過程恰好相反:首先刪除c的內存,堆棧指針向上遞增2個字節,而後刪除x的內存,堆棧指針繼續向上遞增4個字節,程序執行結
束,此時的內存情況爲: 其餘較複雜的分配過程,可能在做用域和分配大小上有所不一樣,可是基本過程大同小異。棧上的內存分配,效率較高,可是內存容量不大,同時變量的生存週期隨着方法的結束而消亡。
未完待續:託管堆的內存分配機制和必要的補充說明,近期發佈,敬請關注。
第十九回:對象建立始末(下)
本文將介紹如下內容:
對象的建立過程
內存分配分析
內存佈局研究
接上回[第十八回:對象建立始末(上)],繼續對對象建立話題的討論>>>
2.2 託管堆的內存分配機制
引用類型的實例分配於託管堆上,而線程棧倒是對象生命週期開始的地方。對32位處理器來講,應用程序完成進程初始化後,CLR將在進程的可用地址空間上分配一塊保留的地址空間,它是進程(每一個進程可以使用4GB)中可用地址空間上的一塊內存區域,但並不對應於任何物理內存,這塊地址空間便是託管堆。
託管堆又根據存儲信息的不一樣劃分爲多個區域,其中最重要的是垃圾回收堆(GC Heap)和加載堆(Loader Heap),GC Heap用於存儲對象實例,受GC管理;Loader Heap又分爲High-Frequency Heap、Low-Frequency Heap和Stub Heap,不一樣的堆上又存儲不一樣的信息。Loader Heap最重要的信息就是元數據相關的信息,也就是Type對象,每一個Type在Loader Heap上體現爲一個Method Table(方法表),而Method Table中則記錄了存儲的元數據信息,例如基類型、靜態字段、實現的接口、全部的方法等等。Loader Heap不受GC控制,其生命週期爲從建立到AppDomain卸載。
在進入實際的內存分配分析以前,有必要對幾個基本概念作以交代,以便更好的在接下來的分析中展開討論。
TypeHandle,類型句柄,指向對應實例的方法表,每一個對象建立時都包含該附加成員,而且佔用4個字節的內存空間。咱們知道,每一個類型都對應於一個方法表,方法表建立於編譯時,主要包含了類型的特徵信息、實現的接口數目、方法表的slot數目等。
SyncBlockIndex,用於線程同步,每一個對象建立時也包含該附加成員,它指向一塊被稱爲Synchronization Block的內存塊,用於管理對象同步,一樣佔用4個字節的內存空間。
NextObjPtr,由託管堆維護的一個指針,用於標識下一個新建對象分配時在託管堆中所處的位置。CLR初始化時,NextObjPtr位於託管堆的基地址。
所以,咱們對引用類型分配過程應該有個基本的瞭解,因爲本篇示例中FileStream類型的繼承關係相對複雜,在此本文實現一個相對簡單的類型來作說明: //@ 2007 Anytao.com //http://www.anytao.com public class UserInfo { private Int32 age = -1;
private char level = 'A'; } public class User { private Int32 id; private UserInfo user; } public class VIPUser : User { public bool isVip; public bool IsVipUser() { return isVip; } public static void Main() { VIPUser aUser; aUser = new VIPUser(); aUser.isVip = true; Console.WriteLine(aUser.IsVipUser()); } }
將上述實例的執行過程,反編譯爲IL語言可知:new關鍵字被編譯爲newobj指令來完成對象建立工做,進而調用類型的構造器來完成其初始化操做,在此咱們詳細的描述其執行的具體過程:
首先,將聲明一個引用類型變量aUser: VIPUser aUser;
它僅是一個引用(指針),保存在線程的堆棧上,佔用4Byte的內存空間,將用於保存VIPUser對象的有效地址,其執行過程正是上文描述的在線程棧上的分配過程。此時aUser未指向任何有效的實例,所以被自行初始化爲null,試圖對aUser的任何操做將拋出NullReferenceException異常。
接着,經過new操做執行對象建立: aUser = new VIPUser();
如上文所言,該操做對應於執行newobj指令,其執行過程又可細分爲如下幾步:
(a)CLR按照其繼承層次進行搜索,計算類型及其全部父類的字段,該搜索將一直遞歸到System.Object類型,並返回字節總數,以本例而言類型VIPUser須要的字節總數爲15Byte,具體計算爲:VIPUser類型自己字段isVip(bool型)爲1Byte;父類User類型的字段id(Int32型)爲4Byte,字段user保存了指向UserInfo型的引用,所以佔4Byte,而同時還要爲UserInfo分配6Byte字節的內存。
實例對象所佔的字節總數還要加上對象附加成員所需的字節總數,其中附加成員包括TypeHandle和SyncBlockIndex,共計8字節(在32位CPU平臺下)。所以,須要在託管堆上分配的字節總數爲23字節,而堆上的內存塊老是按照4Byte的倍數進行分配,所以本例中將分配24字節的地址空間。
(c)CLR在當前AppDomain對應的託管堆上搜索,找到一個未使用的20字節的連續空間,併爲其分配該內存地址。事實上,GC使用了很是高效的算法來知足該請求,NextObjPtr指針只須要向前推動20個字節,並清零原NextObjPtr指針和當前NextObjPtr指針之間的字節,而後返回原NextObjPtr指針地址便可,該地址正是新建立對象的託管堆地址,也就是aUser引用指向的實例地址。而此時的NextObjPtr仍指向下一個新建對象的位置。注意,棧的分配是向低地址擴展,而堆的分配是向高地址擴展。
另外,實例字段的存儲是有順序的,由上到下依次排列,父類在前子類在後,詳細的分析請參見[第十五回:繼承本質論]。
在上述操做時,若是試圖分配所需空間而發現內存不足時,GC將啓動垃圾收集操做來回收垃圾對象所佔的內存,咱們將之後對此作詳細的分析。
最後,調用對象構造器,進行對象初始化操做,完成建立過程。該構造過程,又可細分爲如下幾個環節:
(a)構造VIPUser類型的Type對象,主要包括靜態字段、方法表、實現的接口等,並將其分配在上文提到託管堆的Loader Heap上。
(b)初始化aUser的兩個附加成員:TypeHandle和SyncBlockIndex。將TypeHandle指針指向Loader Heap上的MethodTable,CLR將根據TypeHandle來定位具體的Type;將SyncBlockIndex指針指向Synchronization Block的內存塊,用於在多線程環境下對實例對象的同步操做。
(c)調用VIPUser的構造器,進行實例字段的初始化。實例初始化時,會首先向上遞歸執行父類初始化,直到完成System.Object類型的初始化,而後再返回執行子類的初始化,直到執行VIPUser類爲止。以本例而言,初始化過程爲首先執行System.Object類,再執行User類,最後纔是VIPUser類。最終,newobj分配的託管堆的內存地址,被傳遞給VIPUser的this參數,並將其引用傳給棧上聲明的aUser。
上述過程,基本完成了一個引用類型建立、內存分配和初始化的整個流程,然而該過程只能看做是一個簡化的描述,實際的執行過程更加複雜,涉及到一系列細化的過程和操做。對象建立並初始化以後,內存的佈局,能夠表示爲: 由上文的分析可知,在託管堆中增長新的實例對象,只是將NextObjPtr指針增長一
定的數值,再次新增的對象將分配在當前NextObjPtr指向的內存空間,所以在託管堆棧中,連續分配的對象在內存中必定是連續的,這種分配機制很是高效。
2.3 必要的補充
有了對象建立的基本流程概念,下面的幾個問題時常引發你們的思考,在此本文一併作以探索:
值類型中的引用類型字段和引用類型中的值類型字段,其分配狀況又是如何?
這一思考實際上是一個問題的兩個方面:對於值類型嵌套引用類型的狀況,引用類型變量做爲值類型的成員變量,在堆棧上保存該成員的引用,而實際的引用類型仍然保存在GC堆上;對於引用類型嵌套值類型的狀況,則該值類型字段將做爲引用類型實例的一部分保存在GC堆上。在[ 第八回:品味類型---值類型與引用類型(上)-內存有理]一文對這種嵌套結構,有較詳細的分析。對於值類型,你只要記着它老是分配在聲明它的地方。
方法保存在Loader Heap的MethodTable中,那麼方法調用時又是怎麼樣的過程?
如上文所言,MethodTable中包含了類型的元數據信息,類在加載時會在Loader Heap上建立這些信息,一個類型在內存中對應一份MethodTable,其中包含了全部的方法、靜態字段和實現的接口信息等。對象實例的TypeHandle在實例建立時,將指向MethodTable開始位置的偏移處(默認偏移12Byte),經過對象實例調用某個方法時,CLR根據TypeHandle能夠找到對應的MethodTable,進而能夠定位到具體的方法,再經過JIT Compiler將IL指令編譯爲本地CPU指令,該指令將保存在一個動態內存中,而後在該內存地址上執行該方法,同時該CPU指令被保存起來用於下一次的執行。
在MethodTable中,包含一個Method Slot Table,稱爲方法槽表,該表是一個基於方法實現的線性鏈表,並按照如下順序排列:繼承的虛方法,引入的虛方法,實例方法和靜態方法。方法表在建立時,將按照繼承層次向上搜索父類,直到System.Object類型,若是子類覆寫了父類方法,則將會以子類方法覆蓋父類虛方法。關於方法表的建立過程,能夠參考[第十五回:繼承本質論]中的描述。
靜態字段的內存分配和釋放,又有何不一樣?
靜態字段也保存在方法表中,位於方法表的槽數組後,其生命週期爲從建立到AppDomain卸載。所以一個類型不管建立多少個對象,其靜態字段在內存中也只有一份。靜態字段只能由靜態構造函數進行初始化,靜態構造函數確保在類型任何對象建立前,或者在任何靜態字段或方法被引用前執行,其詳細的執行順序請參考相關討論。
3. 結論
對象建立過程的瞭解,是從底層接觸CLR運行機制的入口,也是認識.NET自動內存管理的關鍵。經過本文的詳細論述,關於對象的建立、內存分配、初始化過程和方法調用等技術都會創建一個相對全面的理解,同時也清楚的把握了線程棧和託管堆的執行機制。
對象老是有生有滅,本文簡述其生,這是個偉大的開始。
1.2 什麼是繼承 本節將介紹如下內容: — 什麼是繼承? — 繼承的實現本質 — 繼承的分類與規則 — 繼承與聚合 — 繼承的侷限 1.2.1 引言 繼承,一個熟悉而容易產生誤解的話題。這是大部分人對繼承最直觀的感覺。說它熟悉,是由於做爲面向對象的三大要素之一的繼承,每一個技術研究者都會在職業生涯中不斷地重複關於繼承的話題;說它容易產生誤解,是由於它老是和封裝、多態交織在一塊兒,造成複雜的局面。以繼
承爲例,如何理清多層繼承的機制,如何瞭解實現繼承與接口繼承的異同,如何體會繼承與多態的關係,彷佛都不是件簡單的事情。 本節但願將繼承中最爲頭疼,最爲複雜的問題通通拿出來曬一曬,以防時間久了,不知不覺在使用者那裏發黴生蟲。 本節不會花太多筆墨作系統性的論述,若有須要請參考其餘技術專著上更詳細的分析。咱們將從關於繼承的熱點出發,逐個擊破,最後總結規律,指望用這種方式實現對繼承全面的瞭解,讓你掌握什麼纔是繼承。 1.2.2 基礎爲上 正如引言所述,繼承是個容易產生誤解的技術話題。那麼,對於繼承,就應該着手從這些容易誤解與引發爭論的話題來尋找關於全面認識和了解繼承的答案。一點一滴擺出來,最後再對分析的要點作概括,造成一種系統化認識。這是一種探索問題的方式,用於剖析繼承這一話題真是再恰當不過了。 不過,解密以前,咱們仍是按照技術分析的慣例,從基本出發,以簡潔的方式來快速瞭解關於繼承最基本的概念。首先,認識一張比較簡單的動物分類圖(圖1-1),以便引入咱們對繼承概念的介紹。
圖1-1 繼承關係圖 從圖1-1中,咱們能夠得到的信息包括: — 動物繼承關係是以必定的分類規則進行的,將相同屬性和特徵的動物及其類別抽象爲一類,類別與類別之間的關係反映爲對類似或者對不類似的某種抽象關係,例如鳥類通常都能飛,而魚類通常都生活在水中。 — 位於繼承圖下層的類別繼承了上層全部類別的特性,造成一種IS-A的關係,例如咱們能夠說,人類IS-A哺乳類、人類IS-A脊椎類。可是這種關係是單向的,因此咱們不能說鳥類IS-A雞。 — 動物繼承圖自上而下是一種逐層具體化過程,而自下而上是一種逐層抽象化過程,這種抽象化關係反映爲上下層之間的繼承關係。例如,最高層的動物具備最廣泛的特徵,而最低層的人則具備較具體的特徵。 — 下層類型只能從上層類型中的某一個類別繼承,例如鯨類的上層只能是哺乳類一種,所以是一種單繼承形式。
— 這種繼承關係中,層與層的特性是向下傳遞的,例如鳥類具備脊椎類的特徵,鶴類也具備脊椎類的特徵,而全部的類都具備動物的特徵,所以說動物是這個層次關係的根。 咱們將這種現實世界的對象抽象化,就造成了面向對象世界的繼承機制。所以,關於繼承,咱們能夠定義爲: 繼承,就是面向對象中類與類之間的一種關係。繼承的類稱爲子類、派生類,而被繼承類稱爲父類、基類或超類。經過繼承,使得子類具備父類的屬性和方法,同時子類也能夠經過加入新的屬性和方法或者修改父類的屬性和方法創建新的類層次。 繼承機制體現了面向對象技術中的複用性、擴展性和安全性。爲面向對象軟件開發與模塊化軟件架構提供了最基本的技術基礎。 在.NET中,繼承按照其實現方式的不一樣,通常分類以下。 — 實現繼承:派生類繼承了基類的全部屬性和方法,而且只能有一個基類,在.NET中System.Object是全部類型的最終基類,這種繼承方式稱爲實現繼承。 — 接口繼承:派生類繼承了接口的方法簽名。不一樣於實現繼承的是,接口繼承容許多繼承,同時派生類只繼承了方法簽名而沒有方法實現,具體的實現必須在派生類中完成。所以,確切地說,這種繼承方式應該稱爲接口實現。 CLR支持實現單繼承和接口多繼承。本節重點關注對象的實現繼承,關於接口繼承,咱們將在1.5節「玩轉接口」中作詳細論述。另外,值得關注的是繼承的可見性問題,.NET經過訪問權限來實現不一樣的控制規則,這些訪問修飾符主要包括:public、protected、internal和private。 下面,咱們就以動物繼承狀況爲例,實現一個最簡單的繼承實例,如圖1-2所示。
圖1-2 動物系統UML 在這個繼承體系中,咱們實現了一個簡單的三層繼承層次,Animal類是全部類型的基類,在此將其構造爲抽象類,抽象了全部類型的廣泛特徵行爲:Eat方法和ShowType方法,其中ShowType方法爲虛函數,其具體實如今子類Chicken和Eagle中給出。這種在子類中實現虛函數的方式,稱爲方法的動態綁定,是實現面向對象另外一特性:多態的基本機制。另外,Eagle類實現了接口繼承,使得Eagle實例能夠實現Fly這一特性,接口繼承的優勢是顯而易見的:經過IFlyable接口,實現了對象與行爲的分離,這樣咱們無需擔憂由於繼承不當而使Chicken有Fly的能力,保護了系統的完整性。 從圖1-2所示的UML圖中可知,經過繼承咱們垂手可得地實現了代碼的複用和擴展,同時經過重載(overload)、覆寫(override)、接口實現等方式實現了封裝變化,隱藏私有信息等面向對象的基本規則。經過繼承,輕易地實現了子類對父類共性的繼承,例如,Animal類中實現了方法Eat(),那麼它的全部子類就都具備了Eat()特性。同時,子類也能夠實現對基類的擴展和改寫,主要有兩種方式:一是經過在子類中添加新方法,例如Bird類中就添加了新方法ShowColor用於現實鳥類的毛色;二是經過對父類方法的從新改寫,在.NET中稱爲覆寫,例如Eagle類中的ShowColor()方法。 1.2.3 繼承本質論
瞭解了關於繼承的基本概念,咱們迴歸本質,從編譯器運行的角度來揭示.NET繼承中的運行本源,來發現子類對象如何實現對父類成員與方法的繼承,以簡單的示例揭示繼承的實質,來闡述繼承機制是如何被執行的。 public abstract class Animal { public abstract void ShowType(); public void Eat() { Console.WriteLine("Animal always eat."); } } public class Bird: Animal { private string type = "Bird"; public override void ShowType() { Console.WriteLine("Type is {0}", type); } private string color; public string Color { get { return color; } set { color = value; } } } public class Chicken : Bird { private string type = "Chicken"; public override void ShowType() { Console.WriteLine("Type is {0}", type);
} public void ShowColor() { Console.WriteLine("Color is {0}", Color); } } 而後,在測試類中建立各個類對象,因爲Animal爲抽象類,咱們只建立Bird對象和Chicken對象。 public class TestInheritance { public static void Main() { Bird bird = new Bird(); Chicken chicken = new Chicken(); } } 下面咱們從編譯角度對這一簡單的繼承示例進行深刻分析,從而瞭解.NET內部是如何實現咱們強調的繼承機制的。 (1)咱們簡要地分析一下對象的建立過程: Bird bird = new Bird(); Bird bird建立的是一個Bird類型的引用,而new Bird()完成的是建立Bird對象,分配內存空間和初始化操做,而後將這個對象引用賦給bird變量,也就是創建bird變量與Bird對象的關聯。 (2)咱們從繼承的角度來分析CLR在運行時如何執行對象的建立過程,由於繼承的本質正體現於對象的建立過程當中。 在此咱們以Chicken對象的建立爲例,首先是字段,對象一經建立,會首先找到其父類Bird,併爲其字段分配存儲空間,而Bird也會繼續找到其父類Animal,爲其分配存儲空間,依次類推直到遞歸結束,也就是完成System.Object內存分配爲止。咱們能夠在編譯器中用單步執行的方法來大體瞭解其分配的過程和順序,所以,對象的建立過程是按照順序完成了對整個父類及其自己字
段的內存建立,而且字段的存儲順序是由上到下排列,最高層類的字段排在最前面。其緣由是若是父類和子類出現了同名字段,則在子類對象建立時,編譯器會自動認爲這是兩個不一樣的字段而加以區別。 而後,是方法表的建立,必須明確的一點是方法表的建立是類第一次加載到AppDomain時完成的,在對象建立時只是將其附加成員TypeHandle指向方法列表在Loader Heap上的地址,將對象與其動態方法列表相關聯起來,所以方法表是先於對象而存在的。相似於字段的建立過程,方法表的建立也是父類在先子類在後,緣由是顯而易見的,類Chicken生成方法列表時,首先將Bird的全部方法複製一份,而後和Chicken自己的方法列表作對比,若是有覆寫的虛方法則以子類方法覆蓋同名的父類方法,同時添加子類的新方法,從而建立完成Chicken的方法列表。這種建立過程也是逐層遞歸到Object類,而且方法列表中也是按照順序排列的,父類在前子類在後,其緣由和字段大同小異,留待讀者本身體味。不言而喻,任何類型方法表中,開始的4個方法老是繼承自System.Object類型的虛方法,它們是:ToString、Equals、GetHashCode和Finalize,詳見8.1節「萬物歸宗:System.Object」所述。 結合咱們的分析過程,如今將對象建立的過程以圖例來揭示其在內存中的分配情形,如圖1-3所示。 圖1-3 對象建立內存歸納 從咱們的分析和上面的對象建立過程當中,咱們應對繼承的本質有了如下更明確的認識:
— 繼承是可傳遞的,子類是對父類的擴展,必須繼承父類方法,同時能夠添加新方法。 — 子類能夠調用父類方法和字段,而父類不能調用子類方法和字段。 — 虛方法如何實現覆寫操做,使得父類指針能夠指向子類對象成員。 — 子類不光繼承父類的公有成員,同時繼承了父類的私有成員,只是在子類中不被訪問。 — new關鍵字在虛方法繼承中的阻斷做用。 你是否已經找到了理解繼承、理解動態編譯的不二法門? 經過上面的講述與分析,咱們基本上對.NET在編譯期的實現原理有了大體的瞭解,可是還有如下的問題,可能會引發疑惑,那就是: Bird bird2 = new Chicken(); 這種狀況下,bird2.ShowType應該返回什麼值呢?而bird2.type又該是什麼值呢?有兩個原則,是.NET專門用於解決這一問題的。 — 關注對象原則:調用子類仍是父類的方法,取決於建立的對象是子類對象仍是父類對象,而不是它的引用類型。例如Bird bird2 = new Chicken()時,咱們關注的是其建立對象爲Chicken類型,所以子類將繼承父類的字段和方法,或者覆寫父類的虛方法,而不用關注bird2的引用類型是否爲Bird。引用類型的區別決定了不一樣的對象在方法表中不一樣的訪問權限。 注意 根據關注對象原則,下面的兩種狀況又該如何區別呢? Bird bird2 = new Chicken(); Chicken chicken = new Chicken(); 根據上文的分析,bird2對象和chicken對象在內存佈局上是同樣的,差異就在於其引用指針的類型不一樣:bird2爲Bird類型指針,而chicken爲Chicken類型指針。以方法調用爲例,不一樣的類型指針在虛擬方法表中有不一樣的附加信息做爲標誌來區別其訪問的地址區域,稱爲offset。不一樣類型的指針只能在其特定地址區域內執行,子類覆蓋父類時會保證其訪問地址區域的一致性,從而解決了不一樣的類型訪問具備不一樣的訪問權限問題。
— 執行就近原則:對於同名字段或者方法,編譯器是按照其順序查找來引用的,也就是首先訪問離它建立最近的字段或者方法,例如上例中的bird2,是Bird類型,所以會首先訪問Bird_type(注意編譯器是不會從新命名的,在此是爲區分起見),若是type類型設爲public,則在此將返回「Bird」值。這也就是爲何在對象建立時必須將字段按順序排列,而父類要先於子類編譯的緣由了。 思考 1.上面咱們分析到bird2.type的值是「Bird」,那麼bird2.ShowType()會顯示什麼值呢?答案是「Type is Chicken」,根據上面的分析,想一想到底爲何? 2.關於new關鍵字在虛方法動態調用中的阻斷做用,也有了更明確的理論基礎。在子類方法中,若是標記new關鍵字,則意味着隱藏基類實現,其實就是建立了與父類同名的另外一個方法,在編譯中這兩個方法處於動態方法表的不一樣地址位置,父類方法排在前面,子類方法排在後面。 1.2.4 密境追蹤 經過對繼承的基本內容的討論和本質揭示,是時候將咱們的眼光轉移到繼承應用中的熱點問題了,主要是從面向對象的角度對繼承進行討論,就像追蹤繼承中的密境,在迷失的森林中尋找出口。 1.實現繼承與接口繼承 實現繼承一般狀況下表現爲對抽象類的繼承,而其與接口繼承在規則上有如下幾點概括: — 抽象類適合於有族層概念的類間關係,而接口最適合爲不一樣的類提供通用功能。 — 接口着重於CAN-DO關係類型,而抽象類則偏重於IS-A式的關係。 — 接口多定義對象的行爲;抽象類多定義對象的屬性。 — 若是預計會出現版本問題,能夠建立「抽象類」。例如,建立了狗(Dog)、雞(Chicken)和鴨(Duck),那麼應該考慮抽象出動物(Animal)來應對之後可能出現馬和牛的事情。而向接口中添加新成員則會強制要求修改全部派生類,並從新編譯,因此版本式的問題最好以抽象類來實現。
— 由於值類型是密封的,因此只能實現接口,而不能繼承類。 關於實現繼承與接口繼承的更詳細的討論與規則,請參見7.4節「面向抽象編程:接口和抽象類」。
2.聚合仍是繼承,這是個問題。 類與類的關係,一般有如下幾種狀況,咱們分別以兩個簡單類Class1和Class2的UML圖來表示以下。 (1)繼承 如圖1-4所示,Class2繼承自Class1,任何對基類Class1的更改都有可能影響到子類Class2,繼承關係的耦合度較高。 (2)聚合 如圖1-5所示。 圖1-4 繼承關係 圖1-5 聚合關係 聚合分爲三種類型,依次爲無、共享和複合,其耦合度逐級遞增。無聚合類型關係,類的雙方彼此不受影響;共享型關係,Class2不須要對Class1負責;而複合型關係,Class1會受控於Class2的更改,所以耦合度更高。總之,聚合關係是一種HAS-A式的關係,耦合度沒有繼承關係高。 (3)依賴 依賴關係代表,若是Class2被修改,則Class1會受到影響,如圖1-6所示。
圖1-6 依賴關係 經過上述三類關係的比較,咱們知道類與類之間的關係,一般以耦合度來描述,也就是表示類與類之間的依賴關係程度。沒有耦合關係的系統是根本不存在的,由於類與類、模塊與模塊、系統與系統之間或多或少要發生相互交互,設計應力求將類與類之間的耦合關係降到最低。而面向對象的基本原則之一就是實現低耦合、高內聚的耦合關係,在2.1節「OO原則綜述」中所述的合成/聚合複用原則正是對這一思想的直接體現。 顯然,將耦合的概念應用到繼承機制上,一般狀況下子類都會對父類產生緊密的耦合,對基類的修改每每會對子類產生一系列的不良反應。繼承之毒瘤主要體如今: — 繼承可能形成子類的無限膨脹,不利於類體系的維護和安全。 — 繼承的子類對象肯定於編譯期,沒法知足須要運行期才肯定的狀況,而類聚合很好地解決了這一問題。 — 隨着繼承層次的複雜化和子類的多樣化,不可避免地會出現對父類的無效繼承或者有害繼承。子類部分的繼承父類的方法或者屬性,更能適應實際的設計需求。 那麼,經過上面的分析,咱們深知繼承機制在知足更加柔性的需求方面有一些弊端,從而可能形成系統設計的漏洞與失衡。解決問題的辦法固然是多種多樣的,根據不一樣的需求進行不一樣的設計變動,例如將對象與行爲分離抽象出接口實現來避免大基類設計,以聚合代替繼承實現更柔性的子類需求等等。 面向對象的基本原則 多聚合,少繼承。 低耦合,高內聚。 聚合與繼承一般體如今設計模式的偉大思想中,在此以Adapter模式的兩種方式爲例來比較繼承和聚合的適應場合與柔性較量。首先對Adapter模式進行簡單的介紹。Adapter模式主要用於
將一個類的接口轉換爲另一個接口,一般狀況下在改變原有體系的條件下應對新的需求變化,經過引入新的適配器類來完成對既存體系的擴展和改造。Adapter模式就其實現方式主要包括: — 類的Adapter模式。經過引入新的類型來繼承原有類型,同時實現新加入的接口方法。其缺點是耦合度高,須要引入過多的新類型。 — 對象的Adapter模式。經過聚合而非繼承的方式來實現對原有系統的擴展,鬆散耦合,較少的新類型。 下面,咱們回到動物體系中,爲鳥兒加上鳴叫ToTweet這一行爲,爲天然界點綴更多美麗的聲音。固然不一樣的鳥叫聲是不一樣的,雞鳴鷹嘶,各有各的範兒。所以,在Bird類的子類都應該對ToTweet有不一樣的實現。如今咱們的要求是在不破壞原有設計的基礎上來爲Bird實現ITweetable接口,理所固然,以Adapter模式來實現這一需求,經過類的Adapter模式和對象的Adapter模式兩種方式來感覺其差異。 首先是類的Adpater模式,其設計UML圖表示爲圖1-7。 圖1-7 類的Adapter模式 在這一新設計體系中,兩個新類型ChickenAdapter和EagleAdapter就是類的Adapter模式中新添加的類,它們分別繼承自原有的類,從而保留原有類型特性與行爲,並實現添加ITweetable接口的新行爲ToTweet()。咱們沒有破壞原有的Bird體系,同時添加了新的行爲,這是繼承的魔力在Adapter模式中的應用。咱們在客戶端應用新的類型來爲Chicken調用新的方法,如圖1-8所見,原有繼承體系中的方法和新的方法對對象ca都是可見的。
圖1-8 ToTweet方法的智能感知 咱們輕鬆地完成了這一難題,是否該輕鬆一下?不。事實上還早着呢,要知道天然界裏的鳥兒們都有美麗的歌喉,咱們只爲Chicken和Eagle配上了鳴叫的行爲,那其餘成千上萬的鳥兒們都有意見了。怎麼辦呢?以目前的實現方式咱們不得不爲每一個繼承自Bird類的子類提供相應的適配類,這樣太累了,有沒有更好的方式呢? 答案是固然有,這就是對象的Adapter模式。類的Adapter模式以繼承方式來實現,而對象的Adapter模式則以聚合的方式來完成,詳情如圖1-9所示。 圖1-9 對象的Adapter模式 具體的實現細節爲: interface ITweetable { void ToTweet(); } public class BirdAdapter : ITweetable { private Bird _bird; public BirdAdapter(Bird bird) { _bird = bird; }
public void ShowType() { _bird.ShowType(); } ……部分省略…… public void ToTweet() { //爲不一樣的子類實現不一樣的ToTweet行爲 } } 客戶端調用爲: public class TestInheritance { public static void Main() { BirdAdapter ba = new BirdAdapter(new Chicken()); ba.ShowType(); ba.ToTweet(); } } 如今能夠鬆口氣了,咱們以聚合的方式按照對象的Adapter模式思路來解決爲Bird類及其子類加入ToTweet()行爲的操做,在沒有添加過多新類型的基礎上十分輕鬆地解決了這一問題。看起來一切都很完美,新的BirdAdapter類與Bird類型之間只有鬆散的耦合關係而不是緊耦合。 至此,咱們以一個幾乎完整的動物體系類設計,基本完成了對繼承與組合問題的探討,系統設計是一個複雜、兼顧、重構的過程,無論是繼承仍是聚合,都是系統設計過程當中必不可少的技術基礎,採起什麼樣的方式來實現徹底取決於具體的需求狀況。根據面向對象多組合、少繼承的原則,對象的Adapter模式更能體現鬆散的耦合關係,應用更靈活。 1.2.5 規則制勝 根據本節的全部討論,行文至此,咱們頗有必要對繼承進行概括總結,將繼承概念中的重點內容和重點規則作系統地梳理,對咱們來講這些規則條款是掌握繼承的金科玉律,主要包括: — 密封類不能夠被繼承。 — 繼承關係中,咱們更多的是關注其共性而不是特性,由於共性是層次複用的基礎,而特性是系統擴展的基點。
— 實現單繼承,接口多繼承。 — 從宏觀來看,繼承多關注於共通性;而多態多着眼於差別性。 — 繼承的層次應該有所控制,不然類型之間的關係維護會消耗更多的精力。 — 面向對象原則:多組合,少繼承;低耦合,高內聚。 1.2.6 結論 在.NET中,若是建立一個類,則該類老是在繼承。這緣於.NET的面向對象特性,全部的類型都最終繼承自共同的根System.Object類。可見,繼承是.NET運行機制的基礎技術之一,一切皆爲對象,一切皆於繼承。對於什麼是繼承這個話題,但願每一個人能從中尋求本身的答案,理解繼承、關注封裝、品味多態、玩轉接口是理解面向對象的起點,也但願本節是這一旅程的起點。
第十五回:繼承本質論
本文將介紹如下內容:
什麼是繼承?
繼承的實現本質
1. 引言
關於繼承,你是否駕熟就輕,關於繼承,你是否瞭如指掌。
本文不討論繼承的基本概念,咱們迴歸本質,從編譯器運行的角度來揭示.NET繼承中的運行本源,來發現子類對象是如何實現了對父類成員與方法的繼承,以最爲簡陋的示例來揭示繼承的實質,闡述繼承機制是如何被執行的,這對於更好的理解繼承,是必要且必然的。
2. 分析
下面首先以一個簡單的動物繼承體系爲例,來進行說明: public abstract class Animal {
public abstract void ShowType(); public void Eat() { Console.WriteLine("Animal always eat."); } } public class Bird: Animal { private string type = "Bird"; public override void ShowType() { Console.WriteLine("Type is {0}", type); } private string color; public string Color { get { return color; } set { color = value; } } } public class Chicken : Bird {
private string type = "Chicken"; public override void ShowType() { Console.WriteLine("Type is {0}", type); } public void ShowColor() { Console.WriteLine("Color is {0}", Color); } }
而後,在測試類中建立各個類對象,因爲Animal爲抽象類,咱們只建立Bird對象和Chicken對象。 public class TestInheritance { public static void Main() { Bird bird = new Bird(); Chicken chicken = new Chicken(); } }
下面咱們從編譯角度對這一簡單的繼承示例進行深刻分析,從而瞭解.NET內部是如何實現咱們強調的繼承機制。
(1)咱們簡要的分析一下對象的建立過程: Bird animal = new Bird();
Bird bird建立的是一個Bird類型的引用,而new Bird()完成的是建立Bird對象,分配內存空間和初始化操做,而後將這個對象賦給bird引用,也就是創建bird引用與Bird對象的關聯。
(2)咱們從繼承的角度來分析在編譯器編譯期是如何執行對象的建立過程,由於繼承的本質就體現於對象的建立過程。
在此咱們以Chicken對象的建立爲例,首先是字段,對象一經建立,會首先找到其父類Bird,併爲其字段分配存儲空間,而Bird也會繼續找到其父類Animal,爲其分配存儲空間,依次類推直到遞歸結束,也就是完成System.Object內存分配爲止。咱們能夠在編譯器中單步執行的方法來大體瞭解其分配的過程和順序,所以,對象的建立過程是按照順序完成了對整個父類及其自己字段的內存建立,而且字段的存儲順序是由上到下排列,object類的字段排在最前面,其緣由是若是父類和子類出現了同名字段,則在子類對象建立時,編譯器會自動認爲這是兩個不一樣的字段而加以區別。
而後,是方法表的建立,必須明確的一點是方法表的建立是類第一次加載到CLR時完成的,在對象建立時只是將其附加成員TypeHandle指向方法列表在Loader Heap上的地址,將對象與其動態方法列表相關聯起來,所以方法表是先於對象而存在的。相似於字段的建立過程,方法表的建立也是父類在先子類在後,緣由是顯而易見的,類Chicken生成方法列表時,首先將Bird的全部方法拷貝一份,而後和Chicken自己的方法列表作以對比,若是有覆寫的虛方法則以子類方法覆蓋同名的父類方法,同時添加子類的新方法,從而建立完成Chicken的方法列表。這種建立過程也是逐層遞歸到Object類,而且方法列表中也是按照順序排列的,父類在前子類在後,其緣由和字段大同小異,留待讀者本身體味。
結合咱們的分析過程,如今將對象建立的過程以簡單的圖例來揭示其在內存中的分配情形,以下:
從咱們的分析,和上面的對象建立過程可見,對繼承的本質咱們有了更明確的認識,對於如下的問題就有了清晰明白的答案:
繼承是可傳遞的,子類是對父類的擴展,必須繼承父類方法,同時能夠添加新方法。
子類能夠調用父類方法和字段,而父類不能調用子類方法和字段。
虛方法如何實現覆寫操做,使得父類指針能夠指向子類對象成員。
new關鍵字在虛方法繼承中的阻斷做用。
你是否已經找到了理解繼承、理解動態編譯的不二法門。
3. 思考
經過上面的講述與分析,咱們基本上對.NET在編譯期的實現原理有了大體的瞭解,可是還有如下的問題,必定會引發必定的疑惑,那就是: Bird bird2 = new Chicken();
這種狀況下,bird2.ShowType應該返回什麼值呢?而bird2.type有該是什麼值呢?有兩個原則,是.NET專門用於解決這一問題的:
關注對象原則:調用子類仍是父類的方法,取決於建立的對象是子類對象仍是父類對象,而不是它的引用類型。例如Bird bird2 = new Chicken()時,咱們關注的是其建立對象爲Chicken類型,所以子類將繼承父類的字段和方法,或者覆寫父類的虛方法,而不用關注bird2的引用類型是否爲Bird。引用類型不一樣的區別決定了不一樣的對象在方法表中不一樣的訪問權限。
注意
根據關注對象原則,那麼下面的兩種狀況又該如何區別呢? Bird bird2 = new Chicken(); Chicken chicken = new Chicken();
根據咱們上文的分析,bird2對象和chicken對象在內存佈局上是同樣的,差異就在於其引用指針的類型不一樣:bird2爲Bird類型指針,而chicken爲Chicken類型指針。以方法調用爲例,不一樣的類型指針在虛擬方法表中有不一樣的附加信息做爲標誌來區別其訪問的地址區域,稱爲offset。不一樣類型的指針只能在其特定地址區域內進行執行,子類覆蓋父類時會保證其訪問地址區域的一致性,從而解決了不一樣的類型訪問具備不一樣的訪問權限問題。
執行就近原則:對於同名字段或者方法,編譯器是按照其順序查找來引用的,也就是首先訪問離它建立最近的字段或者方法,例如上例中的bird2,是Bird類型,所以會首先訪問Bird_type(注意編譯器是不會從新命名的,在此是爲區分起見),若是type類型設爲public,則在此將返回「Bird」值。這也就是爲何在對象建立時必須將字段按順序排列,而父類要先於子類編譯的緣由了。
思考
1. 上面咱們分析到bird2.type的值是「Bird」,那麼bird2.ShowType()會顯示什麼值呢?答案是「Type is Chicken」,根據本文上面的分析,想一想到底爲何?
2. 關於new關鍵字在虛方法動態調用中的阻斷做用,也有了更明確的理論基礎。在子類方法中,若是標記new關鍵字,則意味着隱藏基類實現,其實就是建立了與父類同名的另外一個方法,在編譯中這兩個方法處於動態方法表的不一樣地址位置,父類方法排在前面,子類方法排在後面。
4. 結論
在.NET中,若是建立一個類,則該類老是在繼承。這緣於.NET的面向對象特性,全部的類型都最終繼承自共同的根System.Object類。可見,繼承是.NET運行機制的基礎技術之一,一切皆爲對象,一切皆於繼承。本文從基礎出發,深刻本質探索本源,分析疑難比較鑑別。對於什麼是繼承這個話題,但願每一個人能從中尋求本身的答案,理解繼承、關注封裝、玩轉多態是理解面向對象的起點,但願本文是這一旅程的起點。
[祝福] 僅以此篇獻給個人老師們:湯文海老師,陳樺老師。
1.3 封裝的祕密 本節將介紹如下內容: — 面向對象的封裝特性 — 字段賞析 — 屬性賞析 1.3.1 引言 在面向對象三要素中,封裝特性爲程序設計提供了系統與系統、模塊與模塊、類與類之間交互的實現手段。封裝爲軟件設計與開發帶來史無前例的革命,成爲構成面向對象技術最爲重要的基礎之一。在.NET中,一切看起來都已經被包裝在.NET Framework這一複雜的網絡中,提供給最終開發人員的是成千上萬的類型、方法和接口,而Framework內部一切已經作好了封裝。例如,若是你想對文件進行必要的操做,那麼使用System.IO.File基本就可以知足多變的需求,由於.NET Framwork已經把對文件的重要操做都封裝在System.IO.File等一些基本類中,用戶不須要關心具體的實現。
1.3.2 讓ATM告訴你,什麼是封裝 那麼,封裝到底是什麼? 首先,咱們考察一個常見的生活實例來進行說明,例如每當發工資的日子小王都來到ATM機前,用工資卡取走一筆錢爲女友買禮物,從這個很帥的動做,能夠得出如下的結論: — 小王和ATM機之間,以銀行卡進行交互。要取錢,請交卡。 — 小王並不知道ATM機將錢放在什麼地方,取款機如何計算錢款,又如何經過銀行卡返回小王所要數目的錢。對小王來講,ATM就是一個黑匣子,只能等着取錢;而對銀行來講,ATM機就像銀行本身的一份子,是安全、可靠、健壯的員工。 — 小王要想取到本身的錢,必須遵照ATM機的對外約定。他的任何違反約定的行爲都被視爲不軌,例如欲以磚頭砸開取錢,用公交卡冒名取錢,盜卡取錢都將面臨法律風險,因此小王只能安分守己地過着月光族的日子。 那麼小王和ATM機的故事,能給咱們什麼樣的啓示?對應上面的3條結論,咱們的分析以下: — 小王以工資卡和ATM機交互信息,ATM機的入卡口就是ATM機提供的對外接口,磚頭是塞不進去的,公交卡放進去也沒有用。 — ATM機在內部完成身份驗證、餘額查詢、計算取款等各項服務,具體的操做對用戶小王是不可見的,對銀行來講這種封閉的操做帶來了安全性和可靠性保障。 — 小王和ATM機之間遵照了銀行規定、國家法律這樣的協約。這些協約和法律,就掛在ATM機旁邊的牆上。 結合前面的示例,再來分析封裝吧。具體來講,封裝隱藏了類內部的具體實現細節,對外則提供統一訪問接口,來操做內部數據成員。這樣實現的好處是實現了UI分離,程序員不須要知道類內部的具體實現,只需按照接口協議進行控制便可。同時對類內部來講,封裝保證了類內部成員的安全性和可靠性。在上例中,ATM機能夠看作封裝了各類取款操做的類,取款、驗證的操做對類ATM來講,都在內部完成。而ATM類還提供了與小王交互的統一接口,並以文檔形式——法律法規,規定了接口的規範與協定來保證服務的正常運行。以面向對象的語言來表達,相似於下面的樣子:
namespace InsideDotNet.OOThink.Encapsulation { /// <summary> /// ATM類 /// </summary> public class ATM { #region 定義私有方法,隱藏具體實現 private Client GetUser(string userID) {} private bool IsValidUser(Client user) {} private int GetCash(int money) {} #endregion #region 定義公有方法,提供對外接口 public void CashProcess(string userID, int money) { Client tmpUser = GetUser(userID); if (IsValidUser(tmpUser)) { GetCash(money); } else { Console.Write("你不是合法用戶,是否是想被髮配南極?"); } } #endregion } /// <summary> /// 用戶類 /// </summary>
public class Client { } } 在.NET應用中,Framework封裝了你能想到的各類常見的操做,就像微軟提供給咱們一個又一個功能不一樣的ATM機同樣,而程序員手中籌碼就是根據.NET規範進行開發,是否能取出本身的錢,要看你的卡是否合法。 那麼,若是你是銀行的主管,又該如何設計本身的ATM呢?該以什麼樣的技術來保證本身的ATM在內部隱藏實現,對外提供接口呢? 1.3.3 祕密何處:字段、屬性和方法 字段、屬性和方法,是面向對象的基本概念之一,其基本的概念介紹不是本書的範疇,任何一本關於語言和麪向對象的著做中都有相關的詳細解釋。本書關注的是在類設計之初應該基於什麼樣的思路,來實現類的功能要求與交互要求?每一個設計者,是以什麼角度來完成對類架構的設計與規劃呢?在我看來,下面的問題是應該首先被列入討論的選項: — 類的功能是什麼? — 哪些是字段,哪些是屬性,哪些是方法? — 對外提供的公有方法有哪些,對內隱藏的私有變量有哪些? — 類與類之間的關係是繼承仍是聚合? 這些看似簡單的問題,卻每每是困擾咱們進行有效設計的關鍵因素,一般系統需求描述的核心名詞,能夠抽象爲類,而對這些名詞驅動的動做,能夠對應地抽象爲方法。固然,具體的設計思路要根據具體的需求狀況,在總體架構目標的基礎上進行有效的篩選、剝離和抽象。取捨之間,彰顯OO智慧與設計模式的魅力。
那麼,瞭解這些選項與原則,咱們就不難理解關於字段、屬性和方法的實現思路了,這些規則能夠從對字段、屬性和方法的探索中找到痕跡,而後從反方向來完善咱們對於如何設計的思考與理解。 1.字段 字段(field)一般定義爲private,表示類的狀態信息。CLR支持只讀和讀寫字段。值得注意的是,大部分狀況下字段都是可讀可寫的,只讀字段只能在構造函數中被賦值,其餘方法不能改變只讀字段。常見的字段定義爲: public class Client { private string name; //用戶姓名 private int age; //用戶年齡 private string password; //用戶密碼 } 若是以public表示類的狀態信息,則咱們就能夠以類實例訪問和改變這些字段內容,例如: public static void Main() { Client xiaoWang = new Client(); xiaoWang.name = "Xiao Wang"; xiaoWang.age = 27; xiaoWang.password = "123456" } 這樣看起來並無帶來什麼問題,Client實例經過操做公有字段很容易達到存取狀態信息的目的,然而封裝原則告訴咱們:類的字段信息最好以私有方式提供給類的外部,而不是以公有方式來實現,不然不適當的操做將形成沒必要要的錯誤方式,破壞對象的狀態信息,數據安全性和可靠性沒法保證。例如: xiaoWang.age = 1000; xiaoWang.password = "5&@@Ld;afk99";
顯然,小王的年齡不多是1000歲,他是人不是怪物;小王的密碼也不多是「@&;」這些特殊符號,由於ATM機上根本沒有這樣的按鍵,並且密碼必須是6位。因此對字段公有化的操做,會引發對數據安全性與可靠性的破壞,封裝的第一個原則就是:將字段定義爲private。 那麼,如上文所言,將字段設置爲private後,對對象狀態信息的控制又該如何實現呢?小王的狀態信息必須以另外的方式提供給類外部訪問或者改變。同時咱們也指望除了實現對數據的訪問,最好能加入必定的操做,達到數據控制的目的。所以,面向對象引入了另外一個重量級的概念:屬性。 2.屬性 屬性(property)一般定義爲public,表示類的對外成員。屬性具備可讀、可寫屬性,經過get和set訪問器來實現其讀寫控制。例如上文中Client類的字段,咱們能夠相應地封裝其爲屬性: public class Client { private string name; //用戶姓名 public string Name { get { return name; } set { name = value == null ? String.Empty : value; } } private int age; //用戶年齡 public int Age { get { return age; } set { if ((value > 0) && (value < 150)) {
age = value; } else { throw new ArgumentOutOfRangeException ("年齡信息不正確。"); } } } } 當咱們再次以 xiaoWang.Age = 1000; 這樣的方式來實現對小王的年齡進行寫控制時,天然會彈出異常提示,從而達到了保護數據完整性的目的。 那麼,屬性的get和set訪問器怎麼實現對對象屬性的讀寫控制呢?咱們打開ILDASM工具查看client類反編譯後的狀況時,會發現如圖1-10所示的情形。 圖1-10 Client類的IL結構
由圖1-10可見,IL中不存在get和set方法,而是分別出現了get_Age、set_Age這樣的方法,打開其中的任意方法分析會發現,編譯器的執行邏輯是:若是發現一個屬性,而且查看該屬性中實現了get仍是set,就對應地生成get_屬性名、set_屬性名兩個方法。所以,咱們能夠說,屬性的實質其實就是在編譯時分別將get和set訪問器實現爲對外方法,從而達到控制屬性的目的,而對屬性的讀寫行爲伴隨的實際是一個相應方法的調用,它以一種簡單的形式實現了方法。 因此咱們也能夠定義本身的get和set訪問器,例如: public string get_Password() { return password; } public string set_Password(string value) { if (value.Length < 6) password = value; } 事實上,這種實現方法正是Java語言所採用的機制,而這樣的方式顯然沒有實現get和set訪問器來得輕便,並且對屬性的操做也帶來多餘的麻煩,因此咱們推薦的仍是下面的方式: public string Password { get { return password; } set { if (value.Length < 6) password = value; } } 另外,get和set對屬性的讀寫控制,是經過實現get和set的組合來實現的,若是屬性爲只讀,則只實現get訪問器便可;若是屬性爲可寫,則實現set訪問器便可。
經過對公共屬性的訪問來實現對類狀態信息的讀寫控制,主要有兩點好處:一是避免了對數據安全的訪問限制,包含內部數據的可靠性;二是避免了類擴展或者修改帶來的變量連鎖反應。 至於修改變量帶來的連鎖反應,表如今對類的狀態信息的需求信息發生變化時,如何來減小代碼重構基礎上,實現最小的損失和最大的補救。例如,若是對client的用戶姓名由原來的簡單name來標識,換成以firstName和secondName來實現,若是不是屬性封裝了字段而帶來的隱藏內部細節的特色,那麼咱們在代碼中就要拼命地替換原來xiaoWang.name這樣的實現了。例如: private string firstName; private string secondName; public string Name { get { return firstName + secondName; } } 這樣帶來的好處是,咱們只須要更改屬性定義中的實現細節,而原來程序xiaoWang.name這樣的實現就不須要作任何修改便可適應新的需求。你看,這就是封裝的強大力量使然。 還有一種含參屬性,在C#中稱爲索引器(indexer),對CLR來講並無含不含參數的區別,它只是負責將相應的訪問器實現爲對應的方法,不一樣的是含參屬性中加入了對參數的處理過程罷了。
3.方法 方法(method)封裝了類的行爲,提供了類的對外表現。用於將封裝的內部細節以公有方法提供對外接口,從而實現與外部的交互與響應。例如,從上面屬性的分析咱們可知,實際上對屬性的讀寫就是經過方法來實現的。所以,對外交互的方法,一般實現爲public。 固然不是全部的方法都被實現爲public,不然類內部的實現豈不是所有暴露在外。必須對對外的行爲與內部操做行爲加以區分。所以,一般將在內部的操做所有以private方式來實現,而將須要與外部交互的方法實現爲public,這樣既保證了對內部數據的隱藏與保護,又實現了類的對外交互。例如在ATM類中,對錢的計算、用戶驗證這些方法涉及銀行的關鍵數據與安全數據的保護問題,必須以private方法來實現,以隱藏對用戶不透明的操做,而只提供返回錢款這一public方法接口便可。在封裝原則中,有效地保護內部數據和有效地暴露外部行爲同樣關鍵。 那麼這個過程應該如何來實施呢?仍是回到ATM類的實例中,咱們首先關注兩個方法:IsValidUser()和CashProcess(),其中IsValidUser()用於驗證用戶的合法性,而CashProcess()用於提供用戶操做接口。顯然,驗證用戶是銀行自己的事情,外部用戶無權訪問,它主要用於在內部進行驗證處理操做,例如CashProcess()中就以IsValidUser()做爲方法的進入條件,所以很容易知道IsValidUser()被實現爲private。而CashProcess()用於和外部客戶進行交互操做,這正是咱們反覆強調的外部接口方法,顯然應該實現爲public。其餘的方法GetUser()、GetCash()也是從這一主線出發來肯定其對外封裝權限的,天然就能找到合理的定位。從這個過程當中咱們發現,誰爲公有、誰爲私有,取決於需求和設計雙重因素,在職責單一原則下爲類型設計方法,應該普遍考慮的是類自己的功能性,從開發者與設計者兩個角度出發,分清訪問權限就會水到渠成。 1.3.4 封裝的意義 經過對字段、屬性與方法在封裝性這一點上的分析,咱們能夠更加明確地瞭解到封裝特性做爲面向對象的三大特性之一,表現出來的無與倫比的重要性與必要性,對於深刻地理解系統設計與類設計提供了絕好的切入點。 下面,咱們針對上文的分析進行小結,以便更好地理解咱們對於封裝所提出的思考,主要包括:
(1)字段一般定義爲private,屬性一般實現爲public,而方法在內部實現爲private,對外部實現爲public,從而保證對內部數據的可靠性讀寫控制,保護了數據的安全和可靠,同時又提供了與外部接口的有效交互。這是類得以有效封裝的基礎機制。 (2)一般狀況下的理解正如咱們上面提到的規則,可是具體的操做還要根據實際的設計需求而定,例若有些時候將屬性實現爲private,也將方法實現爲private是更好的選擇。例如在ATM類中,可能須要提供計數器來記錄更新或者選擇的次數,而該次數對用戶而言是沒必要要的狀態信息,所以只需在ATM類內部實現爲private便可;同理,類型中的某些方法是對內部數據的操做,所以也以private方式來提供,從而達到數據安全的目的。 (3)從內存和數據持久性角度上來看,有一個很重要但經常被忽視的事實是,封裝屬性提供了數據持久化的有效手段。由於,對象的屬性和對象同樣在內存期間是常駐的,只要對象不被垃圾回收,其屬性值也將一直存在,而且記錄最近一次對其更改的數據。 (4)在面向對象中,封裝的意義還遠不止類設計層面對字段、屬性和方法的控制,更重要的是其廣義層面。咱們理解的封裝,應該是以實現UI分離爲目的的軟件設計方法,一個系統或者軟件開發以後,從維護和升級的目的考慮,必定要保證對外接口部分的絕對穩定。無論系統內部的功能性實現如何多變,保證接口穩定是保證軟件兼容、穩定、健壯的根本。因此OO智慧中的封裝性旨在保證: — 隱藏系統實現的細節,保證系統的安全性和可靠性。 — 提供穩定不變的對外接口。所以,系統中相對穩定部分常被抽象爲接口。 — 封裝保證了代碼模塊化,提升了軟件的複用和功能分離。 1.3.5 封裝規則 如今,咱們對封裝特性的規則作一個總結,這些規則就是在日常的實踐中提煉與完善出的良藥,咱們在進行實際的開發和設計工做時,應儘可能遵照規則,而不是盲目地尋求方法。 — 儘量地調用類的訪問器,而不是成員,即便在類的內部。其目的在咱們的示例中已有說明,例如Client類中的Name屬性就能夠避免因爲需求變化帶來的代碼更改問題。
— 內部私有部分能夠任意更改,可是必定要在保證外部接口穩定的前提下。 — 將對字段的讀寫控制實現爲屬性,而不是方法,不然舍近而求遠,非明智之選。 — 類封裝是由訪問權限來保證的,對內實現爲private,對外實現爲public。再結合繼承特性,還要對protected,internal有較深的理解,詳細的狀況參見1.1節「對象的旅行」。 — 封裝的精華是封裝變化。張逸在《軟件設計精要與模式》一書中指出,封裝變化是面向對象思想的核心,他提到開發者應從設計角度和使用角度兩方面來分析封裝。所以,咱們將系統中變化頻繁的部分封裝爲獨立的部分,這種隔離選擇有利於充分的軟件複用和系統柔性。 1.3.6 結論 封裝是什麼?橫掃全文,咱們的結論是:封裝就是一個包裝,將包裝的內外分爲兩個空間,對內實現數據私有,對外實現方法調用,保證了數據的完整性和安全性。 咱們從封裝的意義談起,而後逐層深刻到對字段、屬性和方法在定義和實現上的規則,這是一次自上而下的探求方式,也是一次反其道而行的揭密旅程。關於封裝,遠不是本節所能全面展示的話題,關於封裝的技巧和更多深刻的探求,來自於面向對象,來自於設計模式,也來自於軟件工程。所以,要想全面而準確地認識封裝,除了本節打下的基礎以外,不斷的在實際學習中完善和總結是不可缺乏的,這在.NET學習中也是相當重要的。
1.4 多態的藝術 本節將介紹如下內容: — 什麼是多態? — 動態綁定 — 品味多態和麪向對象 1.4.1 引言
翻開大部頭的《韋氏大詞典》,關於多態(Polymorphisn)的定義爲:能夠呈現不一樣形式的能力或狀態。這一術語來源於生物系統,意指同族生物具備的相同特徵。而在.NET中,多態指同一操做做用於不一樣的實例,產生不一樣運行結果的機制。繼承、封裝和多態構成面向對象三要素,成就了面向對象編程模式的基礎技術機制。 在本節,咱們以入情入理的小故事爲線索,來展開一次關於多態的按部就班之旅,在故事的情節中思考多態和麪向對象的藝術品質。 1.4.2 問題的拋出 故事開始。 小王的爺爺,開始着迷於電腦這個新鮮玩意兒了,可是老人家面對陌生的屏幕卻老是摸不着頭腦,各類各樣的文件和資料眼花繚亂,老人家殊不知道如何打開,這可急壞了身爲光榮程序員的小王。爲了讓爺爺享受高科技帶來的便捷與震撼,小王決定本身開發一個萬能程序,用來一鍵式打開常見的計算機資料,例如文檔、圖片和影音文件等,只需安裝一個程序就能夠免了其餘應用文件的管理,而且使用方便,就暫且稱之爲萬能加載器(FileLoader)吧。 既然是個獨立的應用系統,小王就分析了萬能加載器應有的幾個功能點,小結以下: — 自動加載各類資料,一站式搜索系統常見資料。 — 可以打開常見文檔類資料,例如txt文件、Word文件、PDF文件、Visio文件等。 — 可以打開常見圖片資料,例如jpg格式文件、gif格式文件、png格式文件等。 — 可以打開常見音頻資料和視頻資料,例如avi文件、mp3文件等。 — 支持簡單可用的類型擴展接口,易於實現更多文件類型的加載。 這可真是一個不小的挑戰,小王決定利用業餘時間逐步地來實現這一偉大的構想,就當成是送給爺爺60歲的壽禮。有了一個使人興奮的念頭,小王怎麼都睡不着,半夜按捺不住爬起來,構思了一個基本的系統流程框架,如圖1-11所示。
圖1-11 萬能加載器系統框架圖 1.4.3 最初的實現 說幹就幹,小王按照構思的系統框架,首先構思了可能打開的最經常使用的文件,並將其設計爲一個枚舉,這樣就能夠統一來管理文件的類型了,實現以下: //可支持文件類型,以文件擴展名劃分 enum FileType { doc, //Word文檔 pdf, //PDF文檔 txt, //文本文檔 ppt, //Powerpoint文檔 jpg, //jpg格式圖片 gif, //gif格式圖片 mp3, //mp3音頻文件 avi //avi視頻文件 } 看着這個初步設想的文件類型枚舉,小王暗暗以爲真很多,若是再增長一些經常使用的文件類型,這個枚舉還真是氣魄不小呀。 有了要支持的文件類型,小王首先想到的就是實現一個文件類,來表明不一樣類型的文件資料,具體以下: class Files
{ private FileType fileType; public FileType FileType { get { return fileType; } } } 接着小王按照既定思路構建了一個打開文件的管理類,爲每種文件實現其具體的打開方式,例如: class FileManager { //打開Word文檔 public void OpenDocFile() { Console.WriteLine("Alibaba, Open the Word file."); } //打開PDF文檔 public void OpenPdfFile() { Console.WriteLine("Alibaba, Open the PDF File."); } //打開Jpg文檔 public void OpenJpgFile() { Console.WriteLine("Alibaba, Open the Jpg File."); } //打開MP3文檔 public void OpenMp3File() { Console.WriteLine("Alibaba, Open the MP3 File."); }
} 哎呀,這個長長的單子還在繼續往下寫:OpenJpgFile、OpenGifFile、OpenMp3File、OpenAviFile……不知到何時。 上一步着實讓小王寸步難行,下一步的實現更讓小王瀕臨崩潰了,在系統調用端,小王實現的文件加載器是被這樣實現的: class FileClient { public static void Main() { //首先啓動文件管理器 FileManager fm = new FileManager(); //看到一堆一堆的電腦資料 IList<Files> files = new List<Files>(); //當前的萬能加載器該如何完成工做呢? foreach (Files file in files) { switch(file.FileType) { case FileType.doc: fm.OpenDocFile(); break; case FileType.pdf: fm.OpenPdfFile(); break; case FileType.jpg: fm.OpenJpgFile(); break; case FileType.mp3: fm.OpenMp3File(); break;
//……部分省略…… } } } } 完成了文件打開的調用端,一切都好像上了軌道,小王的萬能文檔器也有了基本的架子,剩下再根據實際需求作些調整便可。小王興沖沖地將本身的做品拿給爺爺試手,卻發現爺爺正在想打開一段rm格式的京劇聽聽。可是小王的系統尚未支持這一文件格式,沒辦法只好回去繼續修改了。 等到要添加支持新類型的時候,拿着半成品的小王,忽然發現本身的系統好像很難再插進一腳,除了添加新的文件支持類型,修改打開文件操做代碼,還得在管理類中添加新的支持代碼,最後在客戶端還要修改相應的操做。小王發現添加新的文件類型,好像把原來的系統整個作了一次大裝修,那麼下次爺爺那裏有了新需求呢,號稱萬能加載器的做品,應該怎麼應付下一次的需求變化呢?這真是噩夢,氣喘吁吁的小王,忍不住回頭看了看一天的做品,才發現本身好像掉進了深淵,沒法回頭。敢於探索的小王通過一番深刻的分析發現了當前設計的幾個重要問題,主要包括: — 須要深度調整客戶端,爲系統維護帶來麻煩,何況咱們應該儘可能保持客戶端的相對穩定。 — Word、PDF、MP3等,都是能夠實現的獨立對象,整個系統除了有文檔管理類,幾乎沒有面向對象的影子,所有是面向結構和過程的開發方式。 — 在實現打開文件程序時,小王發現其實OpenDocFile方法、OpenPDFFile方法以及OpenTxtFile方法有不少可複用的代碼,而OpenJpgFile方法和OpenGifFile方法也有不少重複構造的地方。 — 因爲系統之間沒有分割、沒有規劃,整個系統就像一堆亂麻,幾乎不可能完成任何簡單的擴展和維護。 — 任何修改都會將整個系統洗禮一次,修改遍及全系統的整個代碼,而且所有從新編譯才行。 — 需求變動是結構化設計的大敵,沒法輕鬆完成起碼的系統擴展和變動,例如在打開這一操做以外,若是實現刪除、重命名等其餘操做,對當前的系統來講將是致命的打擊。在發生需求多變的今天,必須實現可以靈活擴展和簡單變動的設計構思,面向對象是靈活設計的有效手段之一。
1.4.4 多態,救命的稻草 看着經不起考驗的系統,通過了短時間的鬱悶和摸索,小王終於找到了阿里巴巴念動芝麻之門打開的魔咒,這就是:多態。 沒錯!就是多態,就是面向對象。這是小王痛定思痛後,發出的由衷感慨。小王再接再礪,顛覆了原來的構思,一個新的設計框架應運而生,如圖1-12。 結合新的框架,比較以前的蹩腳設計,小王提出了新系統的新氣象,主要包括如下幾個修改: — 將Word、PDF、TXT、JPG、AVI等業務實體抽象爲對象,並在每一個相應的對象內部來處理本對象類型的文件打開工做,這樣各個類型之間的交互操做就被分離出來,這樣很好地體現了職責單一原則的目標。 — 將各個對象的屬性和行爲相分離,將文件打開這一行爲封裝爲接口,再由其餘類來實現這一接口,有利於系統的擴展同時減小了類與類的依賴。 圖1-12 萬能加載器系統設計 — 將類似的類抽象出公共基類,在基類中實現具備共性的特徵,並由子類繼承父類的特徵,例如Word、PDF、TXT的基類能夠抽象爲DocLoader;而JPG和GIF的基類能夠抽象爲ImageLoader,這種實現體現的是面向對象的開放封閉原則:對擴展開放,對修改關閉。若是有新的類型須要擴展,則只需繼承合適的基類成員,實現新類型的特徵代碼便可。
— 實現可柔性擴展的接口機制,可以更加簡單的實現增長新的文件類型加載程序,也可以很好的擴展打開文件以外的其餘操做,例如刪除、重命名等修改操做。 — 實如今不須要調整原系統,或者不多調整原系統的狀況下,進行功能擴展和優化,甚至是無需編譯的插件式系統。 下面是具體的實現,首先是通用的接口定義: interface IFileOpen { void Open(); } 接着定義全部文件類型的公共基類,由於公共的文件基類是不能夠實例化的,在此處理爲抽象類實現會更好,詳細爲: abstract class Files: IFileOpen { private FileType fileType = FileType.doc; public FileType FileType { get { return fileType; } } public abstract void Open(); } 基類Files實現了IFileOpen接口,不過在此仍然定義方法爲抽象方法。除了文件打開抽象方法,還能夠實現其餘的通用文件處理操做,例如文件刪除Delete、文件重命名ReName和獲取文件路徑等。有了文件類型的公共基類,是時候實現其派生類了。通過必定的分析和設計,小王沒有立刻提供具體的資料類型類,而是對派生類型作了歸檔,初步實現文件類型、圖片類型和媒體類型三個大類,將具體的文件類型進一步作了抽象: abstract class DocFile: Files { public int GetPageCount() { //計算文檔頁數
} } abstract class ImageFile : Files { public void ZoomIn() { //放大比例 } public void ZoomOut() { //縮小比例 } } 終因而實現具體資料類的時候了,在此以Word類型爲例來講明具體的實現: class WORDFile : DocFile { public override void Open() { Console.WriteLine("Open the WORD file."); } } 其餘類型的實現相似於此,不一樣之處在於不一樣的類型有不一樣Open實現規則,以應對不一樣資料的打開操做。 小王根據架構的設計,同時提供了一個資料管理類來進行資料的統一管理: class LoadManager { private IList<Files> files = new List<Files>(); public IList<Files> Files { get { return files; } } public void LoadFiles(Files file) { files.Add(file);
} //打開全部資料 public void OpenAllFiles() { foreach(IFileOpen file in files) { file.Open(); } } //打開單個資料 public void OpenFile(IFileOpen file) { file.Open(); } //獲取文件類型 public FileType GetFileType(string fileName) { //根據指定路徑文件返回文件類型 FileInfo fi = new FileInfo(fileName); return (FileType)Enum.Parse(typeof(FileType), fi.Extension); } } 最後,小王實現了簡單的客戶端,並根據所需進行文件的加載: class FileClient { public static void Main() { //首先啓動文件加載器 LoadManager lm = new LoadManager(); //添加要處理的文件 lm.LoadFiles(new WORDFile()); lm.LoadFiles(new PDFFile()); lm.LoadFiles(new JPGFile()); lm.LoadFiles(new AVIFile()); foreach (Files file in lm.Files)
{ if (file is 爺爺選擇的) //僞代碼 { lm.OpenFile(file); } } } } 固然,如今的FileLoader客戶端還有不少要完善的工做要作,例如關於文件加載的類型,徹底能夠定義在配置文件中,並經過抽象工廠模式和反射於運行期動態獲取,以免耦合在客戶端。不過基本的文件處理部分已經可以知足小王的預期。 1.4.5 隨需而變的業務 爺爺機子上的資料又增長了新的視頻文件MPEG,原來的AVI文件都太大了。但是這回根本就沒有難倒小王的萬能加載器。在電腦前輕鬆地折騰30分鐘後,萬能加載器就能夠適應新的需求,圖1-13所示的是修改的框架設計。 按照這個新的設計,小王對系統只需作以下的簡單調整,首先是增長處理MPEG文件的類型MPEGFile,並讓它繼承自MediaFile,實現具體的Open方法便可。 class MPEGFile : MediaFile { public override void Open() { Console.WriteLine("Open the MPEG file."); } }
圖1-13 萬能加載器架構設計調整 接着就是添加處理新文件的加載操做,以下: lm.LoadFiles(new MPEGFile()); OK。添加新類型的操做就此完成,在沒有對原系統進行修改的繼承上,只需加入簡單的類型和操做便可完成原來看似複雜的操做,結果證實新架構經得起考驗,爺爺也爲小王豎起了大拇指。事實證實,只要有更合理的設計與架構,在基於面向對象和.NET框架的基礎上,徹底能夠實現相似於插件的可擴展系統,而且無需編譯便可更新擴展。 這一切是如何神奇般地實現了呢?回顧從設計到實現的各個環節,小王深知這都是源於多態機制的神奇力量,那麼究竟什麼是多態,.NET中如何來實現多態呢? 1.4.6 多態的類型、本質和規則 從小王一系列大刀闊斧的改革中,咱們不難發現是多態、是面向對象技術成就了FileLoader的強大與靈活。回過頭來,結合FileLoader系統的實現分析,咱們也能夠從技術的角度來進一步探討關於多態的話題。
1.多態的分類 多態有多種分類的方式,Luca Cardelli在《On Understanding Types, Data Abstraction, and Polymorphism》中將多態分爲四類:強制的、重載的、參數的和包含的。本節能夠理解爲包含的多態,從面向對象的角度來看,根據其實現的方式咱們能夠進一步分爲基類繼承式多態和接口實現式多態。 (1)基類繼承式多態 基類繼承多態的關鍵是繼承體系的設計與實現,在FileLoader系統中File類做爲全部資料類型的基類,而後根據需求進行逐層設計,咱們從架構設計圖中能夠清楚地瞭解繼承體系關係。在客戶端調用時,多態是以這種方式體現的: Files myFile = new WORDFile(); myFile.Open(); myFile是一個父類Files變量,保持了指向子類WORDFile實例的引用,而後調用一個虛方法Open,而具體的調用則決定於運行時而非編譯時。從設計模式角度看,基類繼承式多態體現了一種IS-A方式,例如WORDFile IS-A Files就體如今這種繼承關係中。 (2)接口實現式多態 多態並不是僅僅體如今基於基類繼承的機制中,接口的應用一樣能體現多態的特性。區別於基類的繼承方式,這種多態經過實現接口的方法約定造成繼承體系,具備更高的靈活性。從設計模式的角度來看,接口實現式多態體現了一種CAN-DO關係。一樣,在萬能加載器的客戶端調用時,也能夠是這樣的實現方式: IFileOpen myFile = new WORDFile(); myFile.Open(); 固然,不少時候這兩種方式都是混合應用的,就像本節的FileLoader系統的實現方式。 2.多態的運行機制 從技術實現角度來看,是.NET的動態綁定機制成就了面向對象的多態特性。那麼什麼是動態綁定,.NET又是如何實現動態綁定呢?這就是本節關於多態的運行機制所要探討的問題。
動態綁定,又叫晚期綁定,是區別與靜態綁定而言的。靜態綁定在編譯期就能夠肯定關聯,通常是以方法重載來實現的;而動態綁定則在運行期經過檢查虛擬方法表來肯定動態關聯覆寫的方法,通常以繼承和虛方法來實現。在.NET中,虛方法以virtual關鍵字來標記,在子類中覆寫的虛方法則以override關鍵字標記。從設計角度考量,一般將子類中共有的但卻容易變化的特徵抽取爲虛函數在父類中定義,而在子類中經過覆寫來從新實現其操做。 注意 嚴格來說,.NET中並不存在靜態綁定。全部的.NET源文件都首先被編譯爲IL代碼和元數據,在方法執行時,IL代碼才被JIT編譯器即時轉換爲本地CPU指令。JIT編譯發生於運行時,所以也就不存在徹底在編譯期創建的關聯關係,靜態綁定的概念也就無從談起。本文此處僅是參照C++等傳統語言的綁定概念,讀者應區別其本質。 關於.NET經過什麼方式來實現虛函數的動態綁定機制,詳細狀況請參閱本章2.2節「什麼是繼承」的詳細描述。在此,咱們提取萬能加載器FileLoader中的部分代碼,來深刻分析經過虛方法進行動態綁定的通常過程: abstract class Files: IFileOpen { public abstract void Open(); public void Delete() { //實現對文件的刪除處理 } } abstract class DocFile: Files { public int GetPageCount() { //計算文檔頁數 } }
class WORDFile : DocFile { public override void Open() { Console.WriteLine("Open the WORD file."); } } 在繼承體系的實現基礎上,接着是客戶端的實現部分: Files myFile = new WORDFile(); myFile.Open(); 針對上述示例,具體的調用過程,能夠小結爲: 編譯器首先檢查myFile的聲明類型爲Files,而後查看myFile調用方法是否被實現爲虛方法。若是不是虛方法,則直接執行便可;若是是虛方法,則會檢查實現類型WORDFile是否重寫該方法Open,若是重寫則調用WORDFile類中覆寫的方法,例如本例中就將執行WORDFile類中覆寫過的方法;若是沒有重寫,則向上遞歸遍歷其父類,查找是否覆寫該方法,直到找到第一個覆寫方法調用才結束。 3.多態的規則和意義 — 多態提供了對同一類對象的差別化處理方式,實現了對變化和共性的有效封裝和繼承,體現了「一個接口,多種方法」的思想,使方法抽象機制成爲可能。 — 在.NET中,默認狀況下方法是非虛的,以C#爲例必須顯式地經過virtual或者abstract標記爲虛方法或者抽象方法,以便在子類中覆寫父類方法。 — 在面向對象的基本要素中,多態和繼承、多態和重載存在緊密的聯繫,正如前文所述多態的基礎就是創建有效的繼承體系,所以繼承和重載是多態的實現基礎。 1.4.7 結論
在爺爺大壽之際,小王終於完成了送給爺爺的生日禮物:萬能加載器。看到爺爺輕鬆地玩着電腦,小王笑開了花,原來幸福是面向對象的。 在本節中,花了大量的筆墨來詮釋設計架構和麪向對象,或多或少有些喧賓奪主。然而,深刻地瞭解多態及其應用,正是體如今設計模式、軟件架構和麪向對象的思想中;另外一方面,也正是多態、繼承和封裝從技術角度成就了面向對象和設計模式,因此深刻的理解多態就離不開大肆渲染以消化設計,這正是多態帶來的藝術之美。
1.5 玩轉接口 本節將介紹如下內容: — 什麼是接口 — 接口映射本質 — 面向接口編程 — 典型的.NET接口 1.5.1 引言 接口,是面向對象設計中的重要元素,也是打開設計模式精要之門的鑰匙。玩轉接口,就意味着緊握這把鑰匙,打開面向對象的抽象之門,成全設計原則、成就設計模式,實現集優雅和靈活於一身的代碼藝術。 本節,從接口由來說起,經過概念闡述、面向接口編程的分析以及.NET框架中的典型接口實例,勾畫一個理解接口的框架藍圖,經過這一藍圖將會了解玩轉接口的學習曲線。 1.5.2 什麼是接口 所謂接口,就是契約,用於規定一種規則由你們遵照。因此,.NET中不少的接口都以able爲命名後綴,例如INullable、ICloneable、IEnumerable、IComparable等,意指可以爲空、可以克隆、可以枚舉、可以對比,其實正是對契約的一種遵照寓意,只有實現了ICloneable接口的類型,才
容許其實例對象被拷貝。以社會契約而言,只有司機,纔可以駕駛,人們必須遵照這種約定,無照駕駛將被視爲犯罪而不被容許,這是社會契約的表現。由此來理解接口,纔是對面向接口編程及其精髓的把握,例如: interface IDriveable { void Drive(); } 面向接口編程就意味着,在自定義類中想要有駕駛這種特性,就必須遵照這種契約,所以必須讓自定義類實現IDriveable接口,從而才使其具備了「合法」的駕駛能力。例如: public class BusDriver : IDriveable { public void Drive() { Console.WriteLine("有經驗的司機能夠駕駛公共汽車。"); } } 沒有實現IDriveable接口的類型,則不被容許具備Drive這一行爲特性,因此接口是一組行爲規範。例如要使用foreach語句迭代,其前提是操做類型必須實現IEnumerable接口,這也是一種契約。 實現接口還意味着,一樣的方法對不一樣的對象表現爲不一樣的行爲。若是使司機具備駕駛拖拉機的能力,也必須實現IDriveable接口,並提供不一樣的行爲方式,例如: public class TractorDriver: IDriveable { public void Drive() { Console.WriteLine("拖拉機司機駕駛拖拉機。"); } }
在面向對象世界裏,接口是實現抽象機制的重要手段,經過接口實現能夠部分的彌補繼承和多態在縱向關係上的不足,具體的討論能夠參見1.4節「多態的藝術」和7.4節「面向抽象編程:接口和抽象類」。接口在抽象機制上,表現爲基於接口的多態性,例如: public static void Main() { IList<IDriveable> drivers = new List<IDriveable>(); drivers.Add(new BusDriver()); drivers.Add(new CarDriver()); drivers.Add(new TractorDriver()); foreach (IDriveable driver in drivers) { driver.Drive(); } } 經過接口實現,同一個對象能夠有不一樣的身份,這種設計的思想與實現,普遍存在於.NET框架類庫中,正是這種基於接口的設計成就了面向對象思想中不少了不得的設計模式。 1.5.3 .NET中的接口 1.接口多繼承 在.NET中,CLR支持單實現繼承和多接口繼承。這意味着同一個對象能夠表明多個不一樣的身份,以DateTime爲例,其定義爲: public struct DateTime : IComparable, IFormattable, IConvertible, ISerializable, IComparable<DateTime>, IEquatable<DateTime> 所以,能夠經過DateTime實例表明多個身份,不一樣的身份具備不一樣的行爲,例如: public static void Main() { DateTime dt = DateTime.Today; int result = ((IComparable)dt).CompareTo(DateTime.MaxValue);
DateTime dt2 = ((IConvertible)dt).ToDateTime(new System.Globalization.DateTimeFormatInfo()); } 2.接口的本質 從概念上理解了接口,還應進一步從本質上揭示其映射機制,在.NET中基於接口的多態到底是如何被實現的呢?這是值得思考的話題,根據下面的示例,及其IL分析,咱們對此進行必定的探討: interface IMyInterface { void MyMethod(); } 該定義在Reflector中的IL爲: .class private interface abstract auto ansi IMyInterface { .method public hidebysig newslot abstract virtual instance void MyMethod() cil managed { } } 根據IL分析可知,IMyInterface接口本質上仍然被標記爲.class,同時提供了abstract virtual方法MyMethod,所以接口其實本質上能夠看做是一個定義了抽象方法的類,該類僅提供了方法的定義,而沒有方法的實現,其功能由接口的實現類來完成,例如: class MyClass : IMyInterface { void IMyInterface.MyMethod() { } } 其對應的IL代碼爲:
.class private auto ansi beforefieldinit MyClass extends [mscorlib]System.Object implements InsideDotNet.OOThink.Interface.IMyInterface { .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .method private hidebysig newslot virtual final instance void InsideDotNet.OOThink.Interface.IMyInterface.MyMethod() cil managed { .override InsideDotNet.OOThink.Interface.IMyInterface::MyMethod } } 因而可知,實現了接口的類方法在IL標記爲override,表示覆寫了接口方法實現,所以接口的抽象機制仍然是多態來完成的。接口在本質上,仍舊是一個不能實例化的類,可是又區別於通常意義上的類,例如不能實例化、容許多繼承、能夠做用於值類型等。 那麼在CLR內部,接口的方法分派是如何被完成的呢?在託管堆中CLR維護着一個接口虛表來完成方法分派,該表基於方法表內的接口圖信息建立,主要保存了接口實現的索引記錄。以IMyInterface爲例,在MyClass第一次加載時,CLR檢查到MyClass實現了IMyInterface的MyMethod方法,則會在接口虛表中建立一條記錄信息,用於保存MyClass方法表中實現了MyMethod方法的引用地址,其餘實現了IMyInterface的類型都會在接口虛表中建立相應的記錄。所以,接口的方法調用是基於接口虛表進行的。 3.由string所想到的:框架類庫的典型接口 在.NET框架類庫中,存在大量的接口,以典型的System.String類型爲例,就可知接口在FCL設計中的重要性: public sealed class String : IComparable, ICloneable, IConvertible, Icomparable <string>, IEnumerable<char>, IEnumerable, IEquatable<string> 其中IComparable<string>、IEnumerable<char>和IEquatable<string>爲泛型接口,具體的討論能夠參見10.3節「深刻泛型」。 表1.2對幾個典型的接口進行簡要的分析,以便在FCL的探索中不會感受陌生,同時也有助於熟悉框架類庫。 表1-2 FCL的典型接口
接口名稱
接口定義
功能說明
IComparable
public interface IComparable
{
int CompareTo(object obj);
}
提供了方法CompareTo,用於對單個對象進行比較,實現IComparable接口的類須要自行提供排序比較函數。值類型比較會引發裝箱與拆箱操做,IComparable<T>是它的泛型版本
IComparer
public interface IComparer
{
int Compare(object x, object y);
}
定義了爲集合元素排序的方法Compare,支持排序比較,所以實現IComparer接口的類型不須要自行實現排序操做。IComparer接口一樣存在裝箱與拆箱問題,IComparer<T>是其泛型版本
IConvertible
public interface IConvertible
{
TypeCode GetTypeCode();
bool ToBoolean(IFormatProvider provider);
byte ToByte(IFormatProvider provider);
char ToChar(IFormatProvider provider);
int ToInt32(IFormatProvider provider);
string ToString(IFormatProvider provider);
object ToType(Type conversionType, IFormatProvider provider);
//部分省略
}
提供了將類型的實例值轉換爲CLR標準類型的多個方法,在.NET中,類Convert提供了公開的IConvertible方法,經常使用於類型的轉換
ICloneable
public interface ICloneable
{
object Clone();
}
支持對象克隆,既能夠實現淺拷貝,也能夠實現深複製
IEnumerable
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
公開枚舉數,支持foreach語句,方法GetEnumerator用於返回IEnumerator枚舉,IEnumerable<T>是它的泛型版本 續表
接口名稱
接口定義
功能說明
IEnumerator
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
是全部非泛型集合的枚舉數基接口,可用於支持非泛型集合的迭代,IEnumerator<T>是它的泛型版本
IFormattable
public interface IFormattable
{
string ToString(string format, IFormatProvider formatProvider);
}
提供了將對象的值轉化爲字符串的形式
ISerializable
public interface ISerializable
{
[SecurityPermission(SecurityAction. LinkDemand, Flags = SecurityPermissionFlag. SerializationFormatter)]
void GetObjectData(SerializationInfo info, StreamingContext context);
}
實現自定義序列化和反序列化控制方式,方法GetObjectData用於將對象進行序列化的數據存入SerializationInfo對象
IDisposable
public interface IDisposable
{
void Dispose();
}
對於非託管資源的釋放,.NET提供了兩種模式:一種是終止化操做方式,一種是Dispose模式。實現Dispose模式的類型,必須實現IDisposable接口,用於顯示的釋放非託管資源
關於框架類庫的接口討論,在本書的各個部分均有所涉及,例如關於集合的若干接口IList、ICollection、IDictionary等在7.9節「集合通論」中有詳細的討論,在本書的學習過程當中將會逐漸有所收穫,在此僅作簡要介紹。 1.5.4 面向接口的編程 設計模式的師祖GoF,有句名言:Program to an interface, not an implementation,表示對接口編程而不要對實現編程,更通俗的說法是對抽象編程而不要對具體編程。關於面向對象和設計原則,將始終強調對抽象編程的重要性,這源於抽象表明了系統中相對穩定並又可以經過多態特性對其擴展,這很好地符合了高內聚、低耦合的設計思想。 下面,就以著名的Petshop 4.0中一個簡單的面向對象設計片斷爲例,來詮釋面向接口編程的奧祕。 在Petshop 4.0的數據訪問層設計上,微軟設計師將較爲基礎的增刪改查操做封裝爲接口,由具體的實體操做類來實現。抽象出的單獨接口模塊,使得對於數據的操做和業務邏輯對象相分離。借鑑這種設計思路實現一個簡單的用戶操做數據訪問層,其設計如圖1-14所示。 圖1-14 基於Petshop的數據訪問層設計 從上述設計可見,經過接口將增刪改查封裝起來,再由具體的MySQLUser、AccessUser和XMLUser來實現,Helper類則提供了操做數據的通用方法。基於接口的數據訪問層和具體的數據操做
實現完全隔離,對數據的操做規則的變動不會影響實體類對象的行爲,體現了職責分離的設計原則,而這種機制是經過接口來完成的。 同時,可以以IUser接口來統一處理用戶操做,例如在具體的實例建立時,能夠藉助反射機制,經過依賴注入來設計實現: public sealed class DataAccessFactory { private static readonly string assemblyPath = ConfigurationManager.AppSettings ["AssemblyPath"]; private static readonly string accessPath = ConfigurationManager.AppSettings ["AccessPath"]; public static IUser CreateUser() { string className = accessPath + ".User"; return (IUser)Assembly.Load(assemblyPath).CreateInstance(className); } } 你看,經過抽象能夠將未知的對象表現出來,經過讀取配置文件的相關信息能夠很容易建立具體的對象,當有新的類型增長時不須要對原來的系統作任何修改只要在配置文件中增長相應的類型全路徑便可。這種方式體現了面向接口編程的另外一個好處:對修改封閉而對擴展開放。 正是基於這種設計才造成了數據訪問層、業務邏輯層和表現層三層架構的良好設計。而數據訪問層是實現這一架構的基礎,在業務邏輯層,將只有實體對象的相互操做,而沒必要關心具體的數據庫操做實現,甚至看不到任何SQL語句執行的痕跡,例如: public class BLL { private static readonly IUser user = DataAccessFactory.CreateUser(); private static User userInfo = new User(); public static void HandleUserInfo(string ID) { userInfo = user.GetUser(ID); //對userInfo實體對象進行操做
} } 另外,按照接口隔離原則,接口應該被實現爲具備單一功能的多個小接口,而不是具備多個功能的大接口。經過多個接口的不一樣組合,客戶端按需實現不一樣的接口,從而避免出現接口污染的問題。 1.5.5 接口之規則 關於接口的規則,能夠有如下的概括: — 接口隔離原則強調接口應該被實現爲具備單一功能的小接口,而不要實現爲具備多個功能的胖接口,類對於類的依賴應創建在最小的接口之上。 — 接口支持多繼承,既能夠做用於值類型,也能夠做用於引用類型。 — 禁止爲已經發布的接口,添加新的成員,這意味着你必須從新修改全部實現了該接口的類型,在實際的應用中,這每每是不可能完成的事情。 — 接口不能被實例化,沒有構造函數,接口成員被隱式聲明爲public。 — 接口能夠做用於值類型和引用類型,而且支持多繼承。 1.5.6 結論 一般而言,良好的設計必然是面向抽象的,接口是實現這一思想的完美手段之一。經過面向接口編程,保證了系統的職責清晰分離,實體與實體之間保持相對合適的耦合度,尤爲是高層模塊再也不依賴於底層模塊,而依賴於比較穩定的抽象,使得底層的更改不會波及到高層,實現了良好的設計架構。 透徹地瞭解接口,認識對接口編程,體會面向對象的設計原則,是培養一個良好設計習慣的開端。關於接口,是否玩的過癮,就看如何體會本節強調的在概念上的契約,在設計上的抽象。
第2部分 本質——.NET深刻淺出
第3章 一切從IL開始
從Hello, world開始認識IL
本文將介紹如下內容:
IL代碼分析方法
Hello, world歷史
.NET學習方法論
1. 引言
1988年Brian W. Kernighan和Dennis M. Ritchie合著了軟件史上的經典鉅著《The C programming Language》,我推薦全部的程序人都有機會重溫這本歷史上的經典之做。從那時起,Hello, world示例就做爲了幾乎全部實踐型程序設計書籍的開篇代碼,一直延續至今,除了表達對巨人與歷史的尊重,本文也以Hello, world示例做爲咱們扣開IL語言的起點,開始咱們按部就班的IL認識之旅。
2. 從Hello, world開始
首先,固然是展現咱們的Hello, world代碼,開始一段有益的分享。 using System; using System.Data; public class HelloWorld { public static void Main() { Console.WriteLine("Hello, world."); } }
這段代碼執行了最簡單的過程,向陌生的世界打了一個招呼,那麼運行在高級語言背後真相又是什麼呢,下面開始咱們基於上述示例的IL代碼分析。
3. IL體驗中心
對編譯後的可執行文件HelloWorld.exe應用ILDasm.exe反編譯工具,還原HelloWorld的爲文本MSIL編碼,至於其工做原理咱們指望在系列的後續文章中作以交代,咱們查看其截圖爲:
由上圖可知,編譯後的IL結構中,包含了MANIFEST和HelloWorld類,其中MANIFEST是個附加信息列表,主要包含了程序集的一些屬性,例如程序集名稱、版本號、哈希算法、程序集模塊等,以及對外部引用程序集的引用項;而HelloWorld類則是咱們下面介紹的主角。
3.1 MANIFEST清單分析
打開MANIFEST清單,咱們能夠看到 從這段IL代碼中,咱們的分析以下:
.assembly指令用於定義編譯目標或者加載外部庫。在IL清單中可見,.assembly extern mscorlib表示外部加載了外部核心庫mscorlib,而.assembly HelloWorld則表示了定義的編譯目標。值得注意的是,.assembly將只顯示程序中實際應用到的程序集列表,而對於加入using引用的程序集,若是並未在程序中引用,則編譯器會忽略多加載的程序集,例如System.Data將被忽略,這樣就有效避免了過分加載引發的代碼膨脹。
咱們知道mscorlib.dll程序集定義managed code依賴的核心數據類型,屬於必須加載項。 例如接下來要分析的.ctor指令表示構造函數,從代碼中咱們知道沒有爲HelloWord類提供任何顯示的構造函數,所以能夠確定其繼承自基類System.Object,而這個System.Object就包含在mscorlib程序集中。
在外部指令中還會指明瞭引用版本(.ver);應用程序實際公鑰標記(.publickeytoken),公鑰Token是SHA1哈希碼的低8位字節的反序(以下圖所示),用於惟一的肯定程序集;還包括其餘信息如語言文化等。
HelloWorld程序集中包括了.hash algorithm指令,表示實現安全性所使用的哈希算法,系統缺省爲0x00008004,代表爲SHA1算法;.ver則表示了HelloWorld程序集的版本號;
程序集由模塊組成, .module爲程序集指令,代表定義的模塊的元數據,以指定當前模塊。
其餘的指令還有:imagebase爲影像基地址;.file alignment爲文件對齊數值;.subsystem爲鏈接系統類型,0x0003表示從控制檯運行;.corflags爲設置運行庫頭文件標誌,默認爲1;這些指令不是咱們研究的重點,詳細的信息請參考MSDN相關信息。
3.2 HelloWorld類分析
首先是HelloWorld類,代碼爲: .class public auto ansi beforefieldinit HelloWorld extends [mscorlib]System.Object { } // end of class HelloWorld
.class代表了HelloWorld是一個public類,該類繼承自外部程序集mscorlib的System.Object類。
public爲訪問控制權限,這點很容易理解。
auto代表程序加載時內存的佈局是由CLR決定的,而不是程序自己
ansi屬性則爲了在沒有被管理和被管理代碼間實現無縫轉換。沒有被管理的代碼,指的是沒有運行在CLR運行庫之上的代碼,例如原來的C,C++代碼等。
beforefieldinit屬性爲HelloWorld提供了一個附加信息,用於標記運行庫能夠在任什麼時候候執行類型構造函數方法,只要該方法在第一次訪問其靜態字段以前執行便可。若是沒有beforefieldinit則運行庫必須在某個精確時間執行類型構造函數方法,從而影響性能優化,詳細的狀況能夠參與MSDN相關內容。
而後是.ctor方法,代碼爲: .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // 代碼大小 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret } // end of method HelloWorld::.ctor
cil managed 說明方法體中爲IL代碼,指示編譯器編譯爲託管代碼。
.maxstack代表執行構造函數.ctor期間的評估堆棧(Evaluation Stack)可容納數據項的最大個數。關於評估堆棧,其用於保存方法所需變量的值,並在方法執行結束時清空,或者存儲一個返回值。
IL_0000,是一個標記代碼行開頭,通常來講,IL_以前的部分爲變量的聲明和初始化。
ldarg.0 表示裝載第一個成員參數,在實例方法中指的是當前實例的引用,該引用將用於在基類構造函數中調用。
call指令通常用於調用靜態方法,由於靜態方法是在編譯期指定的,而在此調用的是構造函數.ctor()也是在編譯期指定的;而另外一個指令callvirt則表示調用實例方法,它的調用過程有異於call,函數的調用是在運行時肯定的,首先會檢查被調用函數是否爲虛函數,若是不是就直接調用,若是是則向下檢查子類是否有重寫,若是有就調用重寫實現,若是沒有還調用原來的函數,依次類推直到找到最新的重寫實現。
ret表示執行完畢,返回。
最後是Main方法,代碼爲: .method public hidebysig static void Main() cil managed { .entrypoint // 代碼大小 11 (0xb) .maxstack 8 IL_0000: ldstr "Hello, world." IL_0005: call void [mscorlib]System.Console::WriteLine(string) IL_000a: ret } // end of method HelloWorld::Main
.entrypoint指令代表了CLR加載程序HelloWorld.exe時,是首先從.entrypoint方法開始執行的,也就是代表Main方法將做爲程序的入口函數。每一個託管程序必須有而且只有一個入口點。這區別於將Main函數做爲程序入口標誌。
ldstr指令表示將字符串壓棧,"Hello, world."字符串將被移到stack頂部。CLR經過從元數據表中得到文字常量來構造string對象,值得注意的是,在此構造string對象並未出如今《第五回:深刻淺出關鍵字---把new說透》中提到的newobj指令,對於這一點的解釋咱們將在下一回中作簡要分析。
hidebysig屬性用於表示若是當前類做爲父類時,類中的方法不會被子類繼承,所以HelloWorld子類中不會看到Main方法。
接下來的一點補充:
關於註釋,IL代碼中的註釋和C#等高級語言的註釋相同,其實編譯器在編譯IL代碼時已經將全部的註釋去掉,因此任何對程序的註釋在IL代碼中是看不見的。
3.3 迴歸簡潔
去粗取精,咱們的IL代碼能夠簡化,下面的代碼是基於上面的分析,並去處不重要的信息,以更簡潔的方式來展示的HelloWorld版IL代碼,詳細的分析就以註釋來展開吧。
4. 結論
結束本文,咱們從一個點的角度和IL來了一次接觸,除了瞭解幾個重要的指令含義,更重要的是已經走進了IL的世界。經過一站式的掃描HelloWorld的IL編碼,咱們還不足以從全局來了解IL,不過第一次的親密接觸至少讓咱們太陌生,並且隨着系列文章的深刻咱們將逐漸創建起這種認知,從而提升咱們掌握瞭解.NET底層的有效工具。本系列也將在後續的文章中,逐漸創建起這種使用工具的方法,敬請關注。
3.2教你認識IL代碼---從基礎到工具
本文將介紹如下內容:
IL代碼分析方法
IL命令解析 .NET學習方法論
1. 引言
自從『你必須知道.NET』系列開篇以來,受到你們不少的關注和支持,給予了anytao巨大的鼓勵和動力。俱往昔,我發現不少的園友都把目光和焦點注意在如何理解IL代碼這個問題上。對我來講,這真是個莫大的好消息,由於很明顯咱們的思路慢慢的從應用向底層發生着轉變,技巧性的東西是一個方面的積累,底層的探索在我認爲也是必不可少的修煉。若是咱們選擇了來關注這項修煉,那麼咱們就應該選擇如何來着手這項修煉,首先關注anytao的『你必須知道的.NET』系列能夠給你提供一個捷徑,少花一些功夫;其次對大師級的做品也應有更深刻的瞭解,如《Applied Microsoft .NET Framework Programming》、《.NET本質論》;再次,就是像我同樣從博客園和MSDN的知識庫中不斷的成長。呵呵,除了給本身作了個廣告以外,我認爲無論是何種途徑,瞭解和認識IL代碼,對於咱們更深入的理解.NET和.NET應用之上的本質絕對有不同的收穫,這也就是本文研究和分享的理由。
那麼,咱們要了解IL代碼,就要知道了解IL的好處,時間對每一個程序設計師來講都是寶貴的,你必須清楚本身投資的價值再決定投入的資本。對於.NET程序員來講,IL代碼意味着:
通用的語言基礎是.NET運行的基礎,當咱們對程序運行的結果有異議的時候,如何透過本質看表面,須要咱們從本質入手來探索,這時IL是你必須知道的基礎;
元數據和IL語言是CLR的基礎,瞭解必要的中間語言是深刻認識CLR的捷徑;
大量的事例分析是以IL來揭密的,所以瞭解IL是讀懂他人代碼的必備基礎,能夠給本身更多收穫。
很明顯這些優越性足以誘惑咱們花時間和精力涉獵其中。然而,瞭解了IL的好處,並不意味着咱們應該過度的來關注IL,有人甚至能夠洋洋灑灑的寫一堆IL代碼來實現一個簡單Hello world程序,可是正如咱們知道的那樣,程序設計已經走過了幾十年的發展,若是純粹的陶醉在歷史中,除了腦子很差,沒有其餘的解釋。否則看見任何代碼都以IL的角度來分析,又將走進另外一個誤區,咱們的宗旨是追求但不過度。
所以,有了上述了應該瞭解的理由和不該該過度的基線,在擺正心態的前提下,本文開始以做者認爲的方式來展開對IL代碼的認識,做者指望經過本文的闡述與分析使得大
家都能對IL有個概觀之解,並在平時的項目實踐中使用這種方法經過了解本身的代碼來了解.NET。我想,這種方法應該是值得提倡和發揮的最佳實踐,不知你信不信呢?呵呵。
2. 使用工具
俗話說,工欲善其事,必先利其器。IL的器主要就是ILadsm.exe和reflector.exe,這兩個工具都是瞭解IL的基礎,其原理都是經過反射機制來查看IL代碼。
ILadsm.exe
打開.NET Framework SKD 命令提示行,輸入ildasm回車便可打開,如圖所示:
上圖是咱們熟悉的《第十三回:從Hello, world開始認識IL》中的示例,其中的樹形符號表明的意思,能夠從MSDN的一張經典幫助示例來解釋,以下圖所示:
(圖表來源:MSDN)
reflector.exe【下載】
Reflector是Lutz Roeder開發的一個讓人興奮的反編譯利器,目前的版本是Version 5.0.35.0,能夠支持.NET3.0,其功能也至關強大,在使用上也較ILDASM更加靈活,如圖所示:
Reflector能夠方便的反編譯爲IL、C#、VB、Delphi等多種語言,是深刻了解IL的最佳利器。
在本文中咱們以最簡單的ILadsm.exe爲說明工具。
3. 分析結構
分析IL結構,就參閱《第十三回:從Hello, world開始認識IL》 ,已經有了大體的介紹,在此不須要進行過多的筆墨,實際上IL的自己的結構也不是很複雜,瞭解了大體的體系便可。
4. 解析經常使用命令
咱們在瞭解了IL文件結構的基礎上,經過學習經常使用的IL命令,就能夠基本上對IL達到了瞭解不過度的標準,所以對IL經常使用命令的分析就是本文的重點和要點。咱們經過對經常使用命令的解釋、示例與分析,逐步瞭解你陌生的語言世界原來也很簡單。
IL指令集包括了基礎指令集和對象模型指令集大概有近200多個,對咱們來講消化這麼多的陌生指令顯然不是明智的辦法,就行高級語言的關鍵字同樣,咱們只取其一瓢獨飲,抓大放小的革命傳統一樣是有效的學習辦法,詳細的指令集解釋請下載[MSIL指令速查手冊]。
4.1 newobj和initobj
newobj和intiobj指令就像兩個兄弟,經常讓咱們迷惑在其然而不知其因此然,雖然認識可是不怎麼清楚,這種感受很鬱悶,下面就讓咱們看看他們的究竟:
代碼引入
指令說明
深刻分析
從上面的代碼中,咱們能夠得出哪些值得推敲的結論呢? MSDN給出的解釋是:newobj用於分配和初始化對象;而initobj用於初始化值類型。
那麼newobj又是如何分配內存,完成對象初始化;而initobj又如何完成對值類型的初始化呢?
顯然,關於newobj指令,在《第五回:深刻淺出關鍵字---把NEW說透》中,已經有了必定的介紹,簡單說來關於newobj咱們有以下結論:
從託管堆分配指定類型所須要的所有內存空間。
在調用執行構造函數初始化以前,首先初始化對象附加成員:一個是指向該類型方法表的指針;一個是SyncBlockIndex,用於進行線程同步。全部的對象都包含這兩個附加成員,用於管理對象。
最後纔是調用構造函數ctor,進行初始化操做。並返回新建對象的引用地址。
而initobj的做用又能夠小結爲:
構造新的值類型,完成值類型初始化。值得關注的是,這種構造不須要調用值類型的構造函數。具體的執行過程呢?以上例來講,initobj MyStruct的執行結果是,將MyStruct中的引用類型初時化爲null,而基元類型則置爲0。
所以,值類型的初始化能夠是: //initobj方式初始化值類型 initobj Anytao.net.My_Must_net.IL.MyStruct
同時,也能夠直接顯示調用構造函數來完成初始化,具體爲 MyStruct ms = new MyStruct(123);
對應於IL則是對構造函數cto的調用。 //調用構造函數方式初始化值類型 call instance void Anytao.net.My_Must_net.IL.MyStruct::.ctor(int32)
Initobj還用於完成設定對指定存儲單元的指針置空(null)。這一操做雖不常見,可是應該引發注意。
因而可知,newobj和initobj,都具備完成實例初始化的功能,可是針對的類型不一樣,執行的過程有異。其區別主要包括:
newobj用於分配和初始化對象;而initobj用於初始化值類型。所以,能夠說,newobj在堆中分配內存,並完成初始化;而initobj則是對棧上已經分配好的內存,進行初始化便可,所以值類型在編譯期已經在棧上分配好了內存。
newobj在初始化過程當中會調用構造函數;而initobj不會調用構造函數,而是直接對實例置空。
newobj有內存分配的過程;而initobj則只完成數據初始化操做。
關於對象的建立,還有其餘的狀況值得注意,例如:
Newarr指令用來建立一維從零起始的數組;而多維或非從零起始的一維數組,則仍由newobj指令建立。
String類型的建立由ldstr指令來完成,具體的討論咱們在下文來展開。
4.2 call、callvirt和calli
call、callvirt和calli指令用於完成方法調用,這些正是咱們在IL中再熟悉不過的幾個朋友。那麼,一樣是做爲方法調用,這幾位又有何區別呢?咱們首先對其作以歸納性的描述,再來經過代碼與實例,進入深刻分析層面。
call使用靜態調度,也就是根據引用類型的靜態類型來調度方法。
callvirt使用虛擬調度,也就是根據引用類型的動態類型來調度方法;
calli又稱間接調用,是經過函數指針來執行方法調用;對應的直接調用固然就是前面的:call和callvirt。
然而,雖然有以上的通用性結論,可是對於call和callvirt不可一律而論。call在某種狀況下能夠調用虛方法,而callvirt也能夠調用非虛方法。具體的分析咱們在之後的文章中來展開,暫不作過多分析。
5. 結論
本文從幾個重點的IL指令開始,力求經過對比性的分析和深刻來逐步揭開IL的神祕與迷惑,正如咱們在開始強調的那樣,本文只是個開始也許也是個階段,對IL的探求正如我本身的腳步同樣,也在繼續着,爲的是在.NET的技術世界可以有更多的領悟。做者指望經過不斷的努力逐漸和你們一塊兒從IL世界探求.NET世界,在之後的討論中咱們間或的繼續這個主題的不斷成長。
第4章 品味類型
4.1品味類型---從通用類型系統開始
本文將介紹如下內容:
.NET 基礎架構概念
類型基礎
通用類型系統
CLI、CTS、CLS的關係簡述
1. 引言
本文不是連環畫,之因此在開篇以圖形的形式來展現本文主題,其實就是想更加特別的強調這幾個概念的重要性和關注度,同時但願從剖析其關係和聯繫的角度來說述.NET Framework背後的故事。由於,在做者看來想要深刻的瞭解.NET,必須首先從瞭解類型開始,由於CLR技術就是基於類型而展開的。而瞭解類型則有必要把焦點放在.NET類型體系的公共基礎架構上,這就是:通用類型系統(Common Type System, CTS)。
我之因此將最基本的內容以獨立的章節來大加筆墨,除了爲後面幾篇關於對類型這一話題深刻討論作以鋪墊以外,更重要的是從論壇上、博客間,我發現有不少同行對.NET Framework基礎架構的幾個重要體系的理解有所誤差,所以頗有必要補上這一課,必備咱們在深刻探索知識的過程當中,可以遊刃有餘。
2. 基本概念
仍是老套路,首先引入MSDN對通用類型系統的定義,通用類型系統定義瞭如何在運行庫中聲明、使用和管理類型,同時也是運行庫支持跨語言集成的一個重要組成部分。通用類型系統執行如下功能:
創建一個支持跨語言集成、類型安全和高性能代碼執行的框架。
提供一個支持完整實現多種編程語言的面向對象的模型。
定義各語言必須遵照的規則,有助於確保用不一樣語言編寫的對象可以交互做用。
那麼咱們如何來理解呢?
仍是一個現實的場景來引入討論吧。小王之前是個VB迷,寫了一堆的VB.NET代碼,如今他變心了,就投靠C#的陣營,由於流行嘛。因此固然就想在當前的基於C#開發的項目中,應用原來VB.NET現成的東西,省點事兒:-)。那麼CLR是如何來實現類型的轉換的,例如Dim i as Single變量i,編譯器會自動的實現將i由Single到float的映射,固然其緣由是全部的.NET編譯器都是基於CLS實現的。具體的過程爲:CTS定義了在MSIL中使用的預約義數據類型,.NET語言最終都要編譯爲IL代碼,也就是全部的類型最終都要基於這些預約義的類型,例如應用ILDasm.exe分析可知,VB.NET中Single類型映射爲IL類型就是float32,而C#中float類型也映射爲float32,由此就能夠創建起VB.NET和C#的類型關係,爲互操做打下基礎。 .method public hidebysig static void Main(string[] args) cil managed { .entrypoint // 代碼大小 15 (0xf) .maxstack 1 .locals init (float32 V_0) IL_0000: nop IL_0001: ldc.r4 1. IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: call void [mscorlib]System.Console::WriteLine(float32) IL_000d: nop IL_000e: ret } // end of method BaseCts::Main
過去,因爲各個語言在類型定義方面的不一致,形成跨語言編程實現的難度,基於這一問題,.NET中引入CTS來解決各個編程語言類型不一致的問題,類型機制使得多語言的代碼能夠無縫集成。所以CTS也成爲.NET跨語言編程的基礎規範,爲多語言的互操做提供了便捷之道。能夠簡單的說,基於.NET的語言共同使用一個類型系統,這就是CTS。
進一步的探討通用類型系統的內容,咱們知道CTS支持兩種基本的類型,每種類型又能夠細分出其下級子類,能夠如下圖來表示:
.NET提供了豐富的類型層次結構,從上圖中也能夠看出該層次結構是基於單繼承層次實現的,反映了.NET面向對象原則中實現單繼承、接口多繼承的特色。關於值類型和引用類型,是以後要探討的重點內容,也是『品味類型』子系列的重中之重,在此不做進一步探討,可是上面的這張圖有必要清楚的印在心中,由於沒有什麼比這個更基礎的了。
3. 位置與關係
位置強調的是CTS在.NET技術框架中的位置和做用,做者指望以這種方式來天然的引出.NET技術架構的其餘基本內容,從而在各個技術要點的層次中,來說明白各個技術要點的些細聯繫,從大局的角度來對其有個基本的把握。我想,這樣也能夠更好的理解CTS自己,由於技術歷來都不是孤立存在的。
.NET技術能夠以規範和實現兩部分來劃分,而咱們常常強調和提起的.NET Framwork,主要包括公共語言運行時(Common Language Runtime, CLR)和.NET框架類庫(Framework Class Library, FCL),實際上是對.NET規範的實現。而另一部分:規範,咱們稱之爲公共語言架構(Common Language Infrastructure, CLI),主要包括通用類型系統(CTS),公共語言規範(Common Language Specification, CLS)和通用中間語言(Common Intermediate Language, CIL)。咱們以圖的形式來看看CTS在.NET技術陣營中的位置,再來簡要的介紹新登場的各個明星。
CLI,.NET技術規範,已經獲得ECMA(歐洲計算機制造商協會)組織的批准實現了標註化。
CTS,本文主題,此不冗述。
CLS,定義了CTS的子集,開發基於CTS的編譯器,則必須遵照CLS規則,由本文開頭的圖中就能夠看出CLS是面向.NET的開發語言必須支持的最小集合。
CIL,是一種基於堆棧的語言,是任何.NET語言編譯產生的中間代碼,咱們能夠理解爲IL就是CLR的彙編語言。IL定義了一套與處理器無關的虛擬指令集,與CLR/CTS的規則進行映射,執行IL都會翻譯爲本地機器語言來執行。常見的指令有:add, box, call, newobj, unbox。另外,IL很相似於Java世界裏的字節碼(Bytecode),固然也徹底不是一回事,最主要的區別是IL是即時編譯(Just in time, JIT)方式,而Bytecode是解釋性編譯,顯然效率上更勝一躊。
.NET Framework,能夠說是CLI在windows平臺的實現,運行與windows平臺之上。
CLR,.NET框架核心,也是本系列的核心。相似於Java世界的JVM,主要的功能是:管理代碼執行,提供CTS和基礎性服務。對CLR的探討,將伴隨着這個系列的成長來慢慢展開,在此就很少說了。
FCL,提供了一整套的標準類型,以命名空間組織成樹狀形式,樹的根是System。對程序設計人員來講,學習和熟悉FCL是突破設計水平的必經之路,由於其中數以萬計的類幫助咱們完成了程序設計絕大部分的基礎性工做,重要的是咱們要知道如何去使用。
可見,這些基本內容相互聯繫,以簡單的筆墨來澄清其概念、聯繫和功能,顯然還不夠力度。然而在此咱們以拋磚引玉的方式來引入對這些知識的探求,目的是給一個入口,今後來進行更深刻的探索是每一個設計人員的成長的關鍵,就像對FCL的認識,須要實踐,須要時間,須要心思。
4. 通用規則
.NET中,全部的類型都繼承自System.Object類。
類型轉換,一般有is和as兩種方式,具體的探討能夠參考個人另外一拙做《第一回:恩怨情仇:is和as》。另外,還有另外的幾個類型轉換的方式:(typename)valuename,是通用方法;Convert類提供了靈活的類型轉換封裝;Parse方法,適用於向數字類型的轉換。
能夠給類型建立別名,例如,using mynet = Anytao.net.MyClass,其好處是當須要有兩個命名空間的同名類型時,能夠清楚的作以區別,例如: using AClass = Anytao.net.MyClass; using BClass = Anytao.com.MyClass;
其實,咱們經常使用的int、char、string對應的是System.Int3二、System.Char、System.String的別名。
一個對象得到類型的辦法是:obj.GetType()。
Typeof操做符,則常在反射時,得到自定義類型的Type對象,從而獲取關於該類型的方法、屬性等。
能夠使用 CLSCompliantAttribute 將程序集、模塊、類型和成員標記爲符合 CLS 或不符合 CLS。
IL中使用/checked+開關來進行基元類型的溢出檢查,在C#中實現這一功能的是checked和unchecked操做符。
命名空間是從功能角度對類型的劃分,是一組類型在邏輯上的集合。
5. 結論
類型的話題,是個老掉牙的囫圇覺,但也是個永不言退的革命黨。在實際的程序設計中,咱們常常要吃這一虧。由於,不少異常的產生,不少性能的損耗,不少冗餘的設計都和類型解下不解之緣,因此清晰、清楚的瞭解類型,沒有什麼不能夠。重要的是,咱們以什麼角度來了解和化解,內功的修煉仍是要從內力開始。本系列不求一應俱全,但求以更新鮮、更全面的角度,清楚、乾淨、深刻的把某個問題說透,此足尹。
品味類型,就從CTS開始了。
4.2 品味類型——品味類型---值類型與引用類型(上)-內存有理
本文將介紹如下內容:
類型的基本概念
值類型深刻
引用類型深刻
值類型與引用類型的比較及應用
1. 引言
買了新本本,忙了好幾天系統,終於開始了對值類型和引用類型作個全面的講述了,本系列開篇之時就是由於想寫這個主題,纔有了寫個系列的想法。因此對值類型和引用類型的分析,是我最想成文的一篇,其緣由是過去的學習過程當中我就是從這個主題開始,喜歡以IL語言來分析執行,也喜愛從底層的過程來深刻了解。這對我來講,彷佛是一件找到了有效提升的方法,因此想寫的衝動就沒有停過,旨在以有效的方式來分享所得。同時,我也認爲,對值類型和引用類型的把握,是理解語言基礎環節的關鍵主題,有必要花力氣來了解和深刻。
2. 一切從內存開始
2.1 基本概念
從上回《第七回:品味類型---從通用類型系統開始》咱們知道,CLR支持兩種基本類型:值類型和引用類型。所以,仍是把MSDN這張經典視圖拿出來作個鋪墊。
值類型(Value Type),值類型實例一般分配在線程的堆棧(stack)上,而且不包含任何指向實例數據的指針,由於變量自己就包含了其實例數據。其在MSDN的定義爲值類型直接包含它們的數據,值類型的實例要麼在堆棧上,要麼內聯在結構中。咱們由上圖可知,值類型主要包括簡單類型、結構體類型和枚舉類型等。一般聲明爲如下類型:int、char、float、long、bool、double、struct、enum、short、byte、decimal、sbyte、uint、ulong、ushort等時,該變量即爲值類型。
引用類型(Reference Type),引用類型實例分配在託管堆(managed heap)上,變量保存了實例數據的內存引用。其在MSDN中的定義爲引用類型存儲對值的內存地址的引用,位於堆上。咱們由上圖可知,引用類型能夠是自描述類型、指針類型或接口類型。而自描述類型進一步細分紅數組和類類型。類類型是則能夠是用戶定義的類、裝箱的值類型和委託。一般聲明爲如下類型:class、interface、delegate、object、string以及其餘的自定義引用類型時,該變量即爲引用類型。
下面簡單的列出咱們類型的進一步細分,數據來自MSDN,爲的是給咱們的概念中有清晰的類型概念,這是最基礎也是最必須的內容。
2.2 內存深刻
2.2.1. 內存機制
那麼.NET的內存分配機制如何呢?
數據在內存中的分配位置,取決於該變量的數據類型。由上可知,值類型一般分配在線程的堆棧上,而引用類型一般分配在託管堆上,由GC來控制其回收。例如,如今有MyStruct和MyClass分別表明一個結構體和一個類,以下: using System; public class Test { static void Main() { //定義值類型和引用類型,並完成初始化 MyStruct myStruct = new MyStruct();
MyClass myClass = new MyClass(); //定義另外一個值類型和引用類型, //以便了解其內存區別 MyStruct myStruct2 = new MyStruct(); myStruct2 = myStruct; MyClass myClass2 = new MyClass(); myClass2 = myClass; } }
在上述的過程當中,咱們分別定義了值類型變量myStruct和引用類型變量myClass,並使用new操做符完成內存分配和初始化操做,此處new的區別能夠詳見《第五回:深刻淺出關鍵字---把new說透》 的論述,在此不作進一步描述。而咱們在此強調的是myStruct和myClass兩個變量在內存分配方面的區別,仍是以一個簡明的圖來展現一下:
咱們知道,每一個變量或者程序都有其堆棧,不一樣的變量不能共有同一個堆棧地址,所以myStruct和myStruct2在堆棧中必定佔用了不一樣的堆棧地址,儘管通過了變量的傳遞,實際的內存仍是分配在不一樣的地址上,若是咱們再對myStruct2變量改變時,顯然不會影響到myStruct的數據。從圖中咱們還能夠顯而易見的看出,myStruct在堆棧中包含其實例數據,而myClass在堆棧中只是保存了其實例數據的引用地址,實際的數據保存在託管堆中。所以,就有可能不一樣的變量保存了同一地址的數據引用,當數據從一個引用類型變量傳遞到另外一個相同類型的引用類型變量時,傳遞的是其引用地址
而不是實際的數據,所以一個變量的改變會影響另外一個變量的值。從上面的分析就能夠明白的知道這樣一個簡單的道理:值類型和引用類型在內存中的分配區別是決定其應用不一樣的根本緣由,由此咱們就能夠很容易的解釋爲何參數傳遞時,按值傳遞不會改變形參值,而按址傳遞會改變行參的值,道理正在於此。
對於內存分配的更詳細位置,能夠描述以下:
值類型變量作爲局部變量時,該實例將被建立在堆棧上;而若是值類型變量做爲類型的成員變量時,它將做爲類型實例數據的一部分,同該類型的其餘字段都保存在託管堆上,這點咱們將在接下來的嵌套結構部分來詳細說明。
引用類型變量數據保存在託管堆上,可是根據實例的大小有所區別,以下:若是實例的大小小於85000Byte時,則該實例將建立在GC堆上;而當實例大小大於等於85000byte時,則該實例建立在LOH(Large Object Heap)堆上。
更詳細的分析,我推薦《類型實例的建立位置、託管對象在託管堆上的結構》。
2.2.2. 嵌套結構
嵌套結構就是在值類型中嵌套定義了引用類型,或者在引用類型變量中嵌套定義了值類型,相信園子中關於這一話題的論述和關注都不是不少。所以咱們頗有必要發揮一下,在此就順藤摸瓜,從上文對.NET的內存機制着手來理解會水到渠成。
引用類型嵌套值類型
值類型若是嵌套在引用類型時,也就是值類型在內聯的結構中時,其內存分配是什麼樣子呢? 其實很簡單,例如類的私有字段若是爲值類型,那它做爲引用類型實例的一部分,也分配在託管堆上。例如: public class NestedValueinRef { //aInt作爲引用類型的一部分將分配在託管堆上 private int aInt; public NestedValueinRef { //aChar則分配在該段代碼的線程棧上 char achar = 'a';
} }
其內存分配圖能夠表示爲:
值類型嵌套引用類型
引用類型嵌套在值類型時,內存的分配狀況爲:該引用類型將做爲值類型的成員變量,堆棧上將保存該成員的引用,而成員的實際數據仍是保存在託管堆中。例如: public struct NestedRefinValue { public MyClass myClass; public NestedRefinValue { myClass.X = 1; myClass.Y = 2; } }
其內存分配圖能夠表示爲:
2.2.3. 一個簡單的討論
經過上面的分析,若是咱們如今有以下的執行時:
AType[] myType = new AType[10];
試問:若是AType是值類型,則分配了多少內存;而若是AType是引用類型時,又分配了多少內存?
咱們的分析以下:根據CRL的內存機制,咱們知道若是ATpye爲Int32類型,則表示其元素是值類型,而數組自己爲引用類型,myType將保存指向託管堆中的一塊大小爲4×10byte的內存地址,而且將全部的元素賦值爲0;而若是AType爲自定義的引用類型,則會只作一次內存分配,在線程的堆棧建立了一個指向託管堆的引用,而全部的元素被設置爲null值,表示爲空。 未完,下回即將發佈。。。
參考文獻
(USA)Jeffrey Richter, Applied Microsoft .NET Framework Programming
(USA)David Chappell, Understanding .NET
廣而告之
本文有些長,所以分兩回來展開。咱們已經分析了類型的內存機制,接下來就該着重於類型的實際應用領域了,所以在下回中咱們會從[通用規則與區別]、[實例分析]、[應用場合]、[類型比較]等幾個方面來着重展開,但願給你們以幫助,對於表達有謬或者理解有誤的地方還望不吝賜教,本人將不勝感激。
To be continue soon ...
溫故知新
品味類型---值類型與引用類型(中)-規則無邊
接上回[第八回:品味類型---值類型與引用類型(上)-內存有理]的探討,繼續咱們關注值類型和引用類型的話題。
本文將介紹如下內容:
類型的基本概念
值類型深刻
引用類型深刻
值類型與引用類型的比較及應用
1. 引言
上回[第八回:品味類型---值類型與引用類型(上)-內存有理]的發佈,受到你們的很多關注,咱們從內存的角度瞭解了值類型和引用類型的因此然,留下的任務固然是如何應用類型的不一樣特色在系統設計、性能優化等方面發揮其做用。所以,本回是對上回有力的補充,同時應朋友的但願,咱們盡力從內存調試的角度來着眼一些設計的分析,這樣就有助於對這一主題進行透徹和全面的理解,固然這也是下一回的重點。
從內存角度來討論值類型和引用類型是有理有據的, 而從規則的角度來了解值類型和引用類型是一望無際的。本文旨在從上文呼應的角度,來把這個主題完全的融會貫通,無邊無跡的應用,仍是來自反覆無常的實踐,所以對應用我只能說以一個角度來闡釋觀點,可是確定不可能力求全局。所以,咱們從如下幾個角度來完成對值類型與引用類型應用領域的討論。
2. 通用規則與比較
通用有規則:
string類型是個特殊的引用類型,它繼承自System.Object確定是個引用類型,可是在應用表現上又凸現出值類型的特色,那麼到底是什麼緣由呢?例若有以下的一段執行:
簡單的說是因爲string的immutable特性,所以每次對string的改變都會在託管堆中產生一個新的string變量,上述string做爲參數傳遞時,實際上執行了s=s操做,在託管堆中會產生一個新的空間,並執行數據拷貝,因此纔有了相似於按值傳遞的結果。可是根據咱們的內存分析可知,string在本質上仍是一個引用類型,在參數傳遞時發生的仍是按址傳遞,不過因爲其特殊的恆定特性,在函數內部新建了一個string對象並完成初始化,可是函數外部取不到這個變化的結果,所以對外表現的特性就相似於按值傳遞。至於string類型的特殊性解釋,我推薦Artech的大做《深刻理解string和如何高效地使用string》。
另外,string類型重載了==操做符,在類型比較是比較的是實際的字符串,而不是引用地址,所以有如下的執行結果: string aString = "123"; string bString = "123"; Console.WriteLine((aString == bString)); //顯示爲true,等價於aString.Equals(bString); string cString = bString; cString = "456"; Console.WriteLine((bString == cString)); //顯示爲false,等價於bString.Equals(cString);
一般能夠使用Type.IsValueType來判斷一個變量的類型是否爲值類型,典型的操做爲: public struct MyStructTester { } public class isValueType_Test { public static void Main() { MyStructTester aStruct = new MyStructTester(); Type type = aStruct.GetType(); if (type.IsValueType) { Console.WriteLine("{0} belongs to value type.", aStruct.ToString()); } } }
.NET中以操做符ref和out來標識值類型按引用類型方式傳遞,其中區別是:ref在參數傳遞以前必須初始化;而out則在傳遞前沒必要初始化,且在傳遞時必須顯式賦值。
值類型與引用類型之間的轉換過程稱爲裝箱與拆箱,這值得咱們以專門的篇幅來討論,所以留待後文詳細討論這一主題。
sizeof()運算符用於獲取值類型的大小,可是不適用於引用類型。
值類型使用new操做符完成初始化,例如:MyStruct aTest = new MyStruct(); 而單純的定義沒有完成初始化動做,此時對成員的引用將不能經過編譯,例如: MyStruct aTest; Console.WriteLine(aTest.X);
引用類型在性能上欠於值類型主要是由於如下幾個方面:引用類型變量要分配於託管堆上;內存釋放則由GC完成,形成必定的CG堆壓力;同時必須完成對其附加成員的內存分配過程;以及對象訪問問題。所以,.NET系統不能由純粹的引用類型來統治,性能和空間更加優越和易於管理的值類型有其一席之地,這樣咱們就不會由於一個簡單的byte類型而進行復雜的內存分配和釋放工做。Richter就稱值類型爲「輕量級」類型,簡直恰如其分,處理數據較小的狀況時,應該優先考慮值類型。
值類型都繼承自System.ValueType,而System.ValueType又繼承自System.Object,其主要區別是ValueType重寫了Equals方法,實現對值類型按照實例值比較而不是引用地址來比較,具體爲: char a = 'c'; char b = 'c'; Console.WriteLine((a.Equals(b))); //會返回true;
基元類型,是指編譯器直接支持的類型,其概念實際上是針對具體編程語言而言的,例如C#或者VB.NET,一般對應用.NET Framework定義的內置值類型。這是概念上的界限,不可混淆。例如:int對應於System.Int32,float對應於System.Single。
比較出真知:
值類型繼承自ValueType(注意:而System.ValueType又繼承自System.Object);而引用類型繼承自System.Object。
值類型變量包含其實例數據,每一個變量保存了其自己的數據拷貝(副本),所以在默認狀況下,值類型的參數傳遞不會影響參數自己;而引用類型變量保存了其數據的引用地址,所以以引用方式進行參數傳遞時會影響到參數自己,由於兩個變量會引用了內存中的同一塊地址。
值類型有兩種表示:裝箱與拆箱;引用類型只有裝箱一種形式。我會在下節以專門的篇幅來深刻討論這個話題。
典型的值類型爲:struct,enum以及大量的內置值類型;而能稱爲類的均可以說是引用類型。 struct和class主要的區別能夠參見個人拙做《第四回:後來居上:class和struct》來詳細瞭解,也是對值類型和引用類型在應用方面的有力補充。
值類型的內存不禁GC(垃圾回收,Gabage Collection)控制,做用域結束時,值類型會自行釋放,減小了託管堆的壓力,所以具備性能上的優點。例如,一般struct比class更高效;而引用類型的內存回收,由GC來完成,微軟甚至建議用戶最好不要自行釋放內存。
值類型是密封的(sealed),所以值類型不能做爲其餘任何類型的基類,可是能夠單繼承或者多繼承接口;而引用類型通常都有繼承性。
值類型不具備多態性;而引用類型有多態性。
值類型變量不可爲null值,值類型都會自行初始化爲0值;而引用類型變量默認狀況下,建立爲null值,表示沒有指向任何託管堆的引用地址。對值爲null的引用類型的任何操做,都會拋出NullReferenceException異常。
值類型有兩種狀態:裝箱和未裝箱,運行庫提供了全部值類型的已裝箱形式;而引用類型一般只有一種形式:裝箱。
3. 對症下藥-應用場合與注意事項
如今,在內存機制瞭解和通用規則熟悉的基礎上,咱們就能夠很好的總結出值類型和引用類型在系統設計時,如何做出選擇?固然咱們的重點是告訴你,如何去選擇使用值類型,由於引用類型纔是.NET的主體,沒必要花太多的關照就能夠贏得市場。
3.1 值類型的應用場合
MSDN中建議以類型的大小做爲選擇值類型或者引用類型的決定性因素。數據較小的場合,最好考慮以值類型來實現能夠改善系統性能;
結構簡單,沒必要多態的狀況下,值類型是較好的選擇;
類型的性質不表現出行爲時,沒必要以類來實現,那麼用以存儲數據爲主要目的的狀況下,值類型是優先的選擇;
參數傳遞時,值類型默認狀況下傳遞的是實例數據,而不是內存地址,所以數據傳遞狀況下的選擇,取決於函數內部的實現邏輯。值類型能夠有高效的內存支持,而且在不暴露內部結構的狀況下返回
實例數據的副本,從安全性上能夠考慮值類型,可是過多的值傳遞也會損傷性能的優化,應適當選擇;
值類型沒有繼承性,若是類型的選擇沒有子類繼承的必要,優先考慮值類型;
在可能會引發裝箱與拆箱操做的集合或者隊列中,值類型不是很好的選擇,由於會引發對值類型的裝箱操做,致使額外內存的分配,例如在Hashtable。關於這點我將在後續的主題中重點討論。
3.2 引用類型的應用場合
能夠簡單的說,引用類型是.NET世界的全值殺手,咱們能夠說.NET世界就是由類構成的,類是面向對象的基本概念,也是程序框架的基本要素,所以靈活的數據封裝特性使得引用類型成爲主流;
引用類型適用於結構複雜,有繼承、有多態,突出行爲的場合;
參數傳遞狀況也是考慮的必要因素;
4. 再論類型判等
類型的比較一般有Equals()、ReferenceEquals()和==/!=三種常見的方法,其中核心的方法是Equals。咱們知道Equals是System.Object提供的虛方法,用於比較兩個對象是否指向相同的引用地址,.NET Framework的不少類型都實現了對Equals方法的重寫,例如值類型的「始祖」System.ValueType就重載了Equal方法,以實現對實例數據的判等。所以,類型的判等也要從重寫或者重載Equals等不一樣的狀況具體分析,對值類型和引用類型判等,這三個方法各有區別,應多加註意。
4.1 值類型判等
Equals,System.ValueType重載了System.Object的Equals方法,用於實現對實例數據的判等。
ReferenceEquals,對值類型應用ReferenceEquals將永遠返回false。
==,未重載的==的值類型,將比較兩個值是否「按位」相等。
4.2 引用類型判等
Equals,主要有兩種方法,以下 public virtual bool Equals(object obj); public static bool Equals(object objA, object objB);
一種是虛方法,默認爲引用地址比較;而靜態方法,若是objA是與objB相同的實例,或者若是二者均爲空引用,或者若是objA.Equals(objB)返回true,則爲true;不然爲false。.NET的大部分類都重寫了Equals方法,所以判等的返回值要根據具體的重寫狀況決定。
ReferenceEquals,靜態方法,只能用於引用類型,用於比較兩個實例對象是否指向同一引用地址。
==,默認爲引用地址比較,一般進行實現了==的重載,未重載==的引用類型將比較兩個對象是否引用地址,等同於引用類型的Equals方法。所以,不少的.NET類實現了對==操做符的重載,例如System.String的==操做符就是比較兩個字符串是否相同。而==和equals方法的主要區別,在於多態表現上,==是被重載,而Equals是重寫。
有必要在自定義的類型中,實現對Equals和==的重寫或者重載,以提升性能和針對性分析。
5. 再論類型轉換
類型轉換是引發系統異常一個重要的因素之一,所以在有必要在這個主題裏作以簡單的總結,咱們不力求照顧全面,可是追去提綱挈領。常見的類型轉換包括:
隱式轉換:由低級類型項高級類型的轉換過程。主要包括:值類型的隱式轉換,主要是數值類型等基本類型的隱式轉換;引用類型的隱式轉換,主要是派生類向基類的轉換;值類型和引用類型的隱士轉換,主要指裝箱和拆箱轉換。
顯示轉換:也叫強制類型轉換。可是轉換過程不能保證數據的完整性,可能引發必定的精度損失或者引發不可知的異常發生。轉換的格式爲, (type)(變量、表達式)
例如:int a = (int)(b + 2.02);
值類型與引用類型的裝箱與拆箱是.NET中最重要的類型轉換,不恰當的轉換操做會引發性能的極大損耗,所以咱們將以專門的主題來討論。
以is和as操做符進行類型的安全轉換,詳見本人拙做《第一回:恩怨情仇:is和as》。
System.Convert類定義了完成基本類型轉換的便捷實現。
除了string之外的其餘類型都有Parse方法,用於將字符串類型轉換爲對應的基本類型;
使用explicit或者implicit進行用戶自定義類型轉換,主要給用戶提升自定義的類型轉換實現方式,以實現更有目的的轉換操做,轉換格式爲,
static 訪問修飾操做符 轉換修飾操做符 operator 類型(參數列表);
例如: public Student { // static public explicite opertator Student(string name, int age) { return new Student(name, age); } // }
其中,全部的轉換都必須是static的。
6. 結論
如今,咱們從幾個角度延伸了上回對值類型和引用類型的分析,正如本文開頭所言,對類型的把握還有不少能夠挖掘的要點,可是以偏求全的辦法我認爲仍是可取的,尤爲是在技術探求的過程當中,力求面面俱到的作法並非好事。以上的幾個角度,我認爲是對值類型和引用類型把握的必經之路,不然在實際的系統開發中經常會在細小的地方栽跟頭,摸不着頭腦。
品味類型,咱們以應用爲要點撬開值類型和引用類型的規矩與方圓。
品味類型,咱們將以示例爲導航,開動一個層面的深刻分析,下回《第十回:品味類型---值類型與引用類型(下)-應用征途》咱們再見。
參考文獻
(USA)Jeffrey Richter, Applied Microsoft .NET Framework Programming
(USA)David Chappell, Understanding .NET
品味類型---值類型與引用類型(下)-應用征途
本文將介紹如下內容:
類型的基本概念
值類型深刻
引用類型深刻
值類型與引用類型的比較及應用
[下載]:[類型示例代碼]
1. 引言
值類型與引用類型的話題通過了兩個回合([第八回:品味類型---值類型與引用類型(上)-內存有理]和[第九回:品味類型---值類型與引用類型(中)-規則無邊])的討論和切磋,咱們就基本的理解層面來講已經差很少了,可是對這一部分的進一步把握和更深入的理解還要繼續和深化,由於我本身就在兩篇發佈之際,我就獲得裝配腦殼兄的不倦指導,以後又查閱了不少的資料發現類型在.NET或者說語言基礎中何其重要的內涵和深度,所以關於這個話題的討論尚未中止,之後我將繼續分享本身的所得與所感。
不過做爲一個階段,本文將值類型和引用類型的討論從應用示例角度來進一步作以延伸,能夠看做是對前兩回的補充性探討。咱們從類型定義、實例建立、參數傳遞、類型判等、垃圾回收等幾個方面來簡要的對上兩回的內容作以剖析,並以必定的IL語言和內存機制來講明,指望進一步加深咱們的理解和分析。
2. 以代碼剖析
下面,咱們以一個經典的值類型和引用類型對比的示例來剖析,其區別和實質。在剖析的過程當中,咱們主要以執行分析(主要是代碼註釋)、內存分析(主要是圖例說明)和IL分析(主要是IL代碼簡析)三個方面來逐知識點解析,最後再作以總結描述,這樣就能夠有更深的理解。
2.1 類型定義
定義簡單的值類型MyStruct和引用類型MyClass,在後面的示例中將逐漸完善,完整的代碼能夠點擊下載[類型示例代碼]。咱們的討論如今開始,
代碼演示
// 01 定義值類型 public struct MyStruct { private int _myNo; public int MyNo { get { return _myNo; } set { _myNo = value; } } public MyStruct(int myNo) { _myNo = myNo; } public void ShowNo() { Console.WriteLine(_myNo); } }
// 02 定義引用類型 public class MyClass { private int _myNo; public int MyNo { get { return _myNo; } set { _myNo = value; } } public MyClass()
{ _myNo = 0; } public MyClass(int myNo) { _myNo = myNo; } public void ShowNo() { Console.WriteLine(_myNo); } }
IL分析
分析IL代碼可知,靜態方法.ctor用來表示實現構造方法的定義,其中該段IL代碼表示將0賦給字段_myNo。
2.2 建立實例、初始化及賦值
接下來,咱們完成實例建立和初始化,和簡單的賦值操做,而後在內存和IL分析中發現其實質。
代碼演示 建立實例、初始化及賦值
內存實況
首先是值類型和引用類型的定義,這是一切面向對象的開始,
而後是初始化過程,
簡單的賦值和拷貝,是最基本的內存操做,不妨看看,
2.3 參數傳遞
代碼演示
參數傳遞
沒必要多說,就是一個簡要闡釋,對於參數的傳遞做者將計劃以更多的筆墨來在後面的系列中作以澄清和深刻。
2.4 類型轉換
類型轉換的演示,包括不少個方面,在此咱們只以自定義類型轉換爲例來作以說明,更詳細的類型轉換能夠參考[第九回:品味類型---值類型與引用類型(中)-規則無邊]的[再論類型轉換部分]。
代碼演示
首先是值類型的自定義類型轉換,
public struct MyStruct { // 01.2 自定義類型轉:整形->MyStruct型 static public explicit operator MyStruct(int myNo)
{ return new MyStruct(myNo); } }
而後是引用類型的自定義類型轉換,
public class MyClass { // 02.2 自定義類型轉換:MyClass->string型 static public implicit operator string(MyClass mc) { return mc.ToString(); } public override string ToString() { return _myNo.ToString(); } }
最後,咱們對自定義的類型作以測試, public static void Main(string[] args) { #region 03. 類型轉換 MyStruct MyNum; int i = 100; MyNum = (MyStruct)i; Console.WriteLine("整形顯式轉換爲MyStruct型---"); Console.WriteLine(i); MyClass MyCls = new MyClass(200); string str = MyCls;
Console.WriteLine("MyClass型隱式轉換爲string型---"); Console.WriteLine(str); #endregion }
2.5 類型判等
類型判等主要包括:ReferenceEquals()、Equals()虛方法和靜態方法、==操做符等方面,同時注意在值類型和引用類型判等時的不一樣之處,能夠參考[第九回:品味類型---值類型與引用類型(中)-規則無邊]的[4. 再論類型判等]的簡述。
代碼演示
// 01 定義值類型 public struct MyStruct {
// 01.1 值類型的類型判等 public override bool Equals(object obj) { return base.Equals(obj); }
}
public class MyClass {
// 02.1 引用類型的類型判等 public override bool Equals(object obj) { return base.Equals(obj); }
}
public static void Main(string[] args)
{
#region 05 類型判等 Console.WriteLine("類型判等---"); // 05.1 ReferenceEquals判等 //值類型老是返回false,通過兩次裝箱的myStruct不可能指向同一地址 Console.WriteLine(ReferenceEquals(myStruct, myStruct)); //同一引用類型對象,將指向一樣的內存地址 Console.WriteLine(ReferenceEquals(myClass, myClass)); //RefenceEquals認爲null等於null,所以返回true Console.WriteLine(ReferenceEquals(null, null));
// 05.2 Equals判等 //重載的值類型判等方法,成員大小不一樣 Console.WriteLine(myStruct.Equals(myStruct2)) ;
//重載的引用類型判等方法,指向引用相同 Console.WriteLine(myClass.Equals(myClass2));
#endregion
}
2.6 垃圾回收
首先,垃圾回收機制,絕對不是三言兩語就能交代清楚,分析明白的。所以,本示例只是從最簡單的說明出發,對垃圾回收機制作以簡單的分析,目的是善始善終的交代實例由建立到消亡的全過程。
代碼演示
public static void Main(string[] args) {
#region 06 垃圾回收的簡單闡釋 //實例定義及初始化 MyClass mc1 = new MyClass(); //聲明但不實體化 MyClass mc2; //拷貝引用,mc2和mc1指向同一託管地址 mc2 = mc1;
//定義另外一實例,並完成初始化 MyClass mc3 = new MyClass(); //引用拷貝,mc一、mc2指向了新的託管地址 //那麼原來的地址成爲GC回收的對象,在 mc1 = mc3; mc2 = mc3; #endregion
}
內存實況
GC執行時,會遍歷全部的託管堆對象,按照必定的遞歸遍歷算法找出全部的可達對象和不可訪問對象,顯然本示例中的託管堆A對象沒有被任何引用訪問,屬於不可訪問對象,將被列入執行垃圾收集的目標。對象由newobj指令產生,到被GC回收是一個複雜的過程,咱們指望在系列的後期對此作以深刻淺出的理解。
2.7 總結陳述
這些示例主要從從基礎的方向入手來剖析前前兩回中的探討,不求可以全面而深邃,但求可以一點而及面的展開,技術的魅力正在於變幻無窮,技術追求者的力求倒是從變化中尋求不變,否則咱們實質太累了,我想這就是好方法,本系列但願的就是提供一個入口,打開一個方法。示例的詳細分析能夠下載[類型示例代碼],簡單的分析但願能帶來絲絲愜意。
3. 結論
值類型和引用類型,要說的,要作的,還有不少。此篇只是一個階段,更多的深刻和探討我相信還在繼續,同時普遍的關注技術力量的成長,是每一個人應該進取的空間和道路。
品味類型,爲應用之路開闢技術基礎。
品味類型,繼續探討還會更多精彩。
4.3 參數之惑——參數之惑---傳遞的藝術(上)
本文將介紹如下內容:
按值傳遞與按引用傳遞深論
ref和out比較
參數應用淺析
1. 引言
接上回《第九回:品味類型---值類型與引用類型(中)-規則無邊》中,對值類型和引用類型的討論,其中關於string類型的參數傳遞示例和解釋,引發園友的關注和討論,可謂一石激起千層浪。受教於裝配腦殼的深切指正,對這一律念有了至關進一步的瞭解,事實證實是我錯了,在此向朋友們致歉,同時很是感謝你們的參與,尤爲是裝配腦殼的不倦相告。
所以,本文就以更爲清晰的角度,把我理解有誤的雷區做作以深刻的討論與分析,但願經過個人一點點努力和探討至少對以下幾個問題能有清晰的概念:
什麼是按值傳遞?什麼是按引用傳遞?
按引用傳遞和按引用類型參數傳遞的區別?
ref與out在按引用傳遞中的比較與應用如何?
param修飾符在參數傳遞中的做用是什麼?
2. 參數基礎論
簡單的來講,參數實現了不一樣方法間的數據傳遞,也就是信息交換。Thinking in Java的做者有過一句名言:一切皆爲對象。在.NET語言中也是如此,一切數據都最終抽象於類中封裝,所以參數通常用於方法間的數據傳遞。例如典型的Main入口函數就有一個string數組參數,args是函數命令行參數。一般參數按照調用方式能夠分爲:形參和實參。形參就是被調用方法的參數,而實參就是調用方法的參數。例如: using System; public class Arguments { public static void Main(string [] args) { string myString = "This is your argument."; //myString是實際參數 ShowString(myString); } private void ShowString(string astr) { Console.WriteLine(astr); } }
由上例能夠得出如下幾個關於參數的基本語法:
形參和實參必須類型、個數與順序對應匹配;
參數能夠爲空;
解析Main(string [] args),Main函數的參數能夠爲空,也能夠爲string數組類,其做用是接受命令行參數,例如在命令行下運行程序時,args提供了輸入命令行參數的入口。
另外,值得一提的是,雖然CLR支持參數默認值,可是C#中卻不能設置參數默認值,這一點讓我很鬱悶,不知爲什麼?不過能夠經過重載來變相實現,具體以下:
static void JudgeKind(string name, string kind) { Console.WriteLine("{0} is a {1}", name, kind); } static void JudgeKind(string name) { //僞代碼 if(name is person) { Console.WriteLine(name, "People"); } }
這種方法能夠擴展,能夠實現更多個默認參數實現,不過,說實話有些畫蛇添足,不夠靈活,不爽不爽。
3. 傳遞的基礎
接下來,咱們接上面的示例討論,重點將參數傳遞的基礎作以交代,以便對參數之惑有一個從簡入繁的演化過程。咱們以基本概念的形式來一一列出這些基本概念,先混個臉兒熟,關於形參、實參、參數默認值的概念就很少作交代,參數傳遞是本文的核心內容,將在後文以大量的筆墨來闡述。因此接下來的概念,咱們就作以簡單的引入不花大量的精力來討論,主要包括:
3.1 泛型類型參數
泛型類型參數,能夠是靜態的,例如MyGeneric<int>;也能夠是動態的,此時它其實就是一個佔位符,例如MyGeneric<T>中的T能夠是任何類型的變量,在運行期動態替換爲相應的類型參數。泛型類型參數通常也以T開頭來命名。
3.2 可變數目參數
通常來講參數個數都是固定的,定義爲集羣類型的參數能夠實現可變數目參數的目的,可是.NET提供了更靈活的機制來實現可變數目參數,這就是使用param修飾符。可變數目參數的好處就是在某些狀況下能夠方便的提供對於參數個數不肯定狀況的實現,例如計算任意數字的加權和,鏈接任意字符串爲一個字符串等。咱們以一個簡單的示例來展開對這個問題的論述,爲:
在此基礎上,咱們將使用param關鍵字實現可變數目參數的規則和使用作以小結爲:
param關鍵字的實質是:param是定製特性ParamArrayAttribute的縮寫(關於定製特性的詳細論述請參見第三回:歷史糾葛:特性和屬性),該特性用於指示編譯器的執行過程大概能夠簡化爲:編譯器檢查到方法調用時,首先調用不包含ParamArrayAttribute特性的方法,若是存在這種方法就施行調用,若是不存在才調用包含ParamArrayAttribute特性的方法,同時應用方法中的元素來填充一個數組,同時將該數組做爲參數傳入調用的方法體。總之就是param就是提示編譯器實現對參數進行數組封裝,將可變數目的控制由編譯器來完成,咱們能夠很方便的從上述示例中獲得啓示。例如:
static void ShowAgeSum(string team, params int[] ages){...}
實質上是這樣子:
static void ShowAgeSum(string team, [ParamArrayAttribute] int[] ages){...}
param修飾的參數必須爲一維數組,事實上一般就是以羣集方式來實現多個或者任意多個參數的控制的,因此數組是最簡單的選擇;
param修飾的參數數組,但是是任何類型。所以,若是須要接受任何類型的參數時,只要設置數組類型爲object便可;
param必須在參數列表的最後一個,而且只能使用一次。
4. 深刻討論,傳遞的藝術
默認狀況下,CRL中的方法都是按值傳遞的,可是在具體狀況會根據傳遞的參數狀況的不一樣而有不一樣的表現,咱們在深刻討論傳遞藝術的要求下,就是將不一樣的傳遞狀況和不一樣的表現狀況作以小結,從中剝離出參數傳遞複雜表現以內的實質所在。從而爲開篇的幾個問題給出清晰的答案。
4.1 值類型參數的按值傳遞
首先,參數傳遞根據參數類型分爲按值傳遞和按引用傳遞,默認狀況下都是按值傳遞的。按值傳遞主要包括值類型參數的按值傳遞和引用類型參數的按值傳遞。值類型實例傳遞的是該值類型實例的一個拷貝,所以被調用方法操做的是屬於本身自己的實例拷貝,所以不影響原來調用方法中的實例值。以例爲證: // FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/01 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net { class Args {
public static void Main() { int a = 10; Add(a); Console.WriteLine(a); } private static void Add(int i) { i = i + 10; Console.WriteLine(i); } } }
參數之惑---傳遞的藝術(下) 本文將介紹如下內容:
按值傳遞與按引用傳遞深論
ref和out比較
參數應用淺析
接上篇繼續,『第十一回:參數之惑---傳遞的藝術(上)』 4.2 引用類型參數的按值傳遞
當傳遞的參數爲引用類型時,傳遞和操做的是指向對象的引用,這意味着方法操做能夠改變原來的對象,可是值得思考的是該引用或者說指針自己仍是按值傳遞的。所以,咱們在此必須清楚的瞭解如下兩個最根本的問題:
引用類型參數的按值傳遞和按引用傳遞的區別?
string類型做爲特殊的引用類型,在按值傳遞時表現的特殊性又如何解釋?
首先,咱們從基本的理解入手來了解引用類型參數按值傳遞的本質所在,簡單的說對象做爲參數傳遞時,執行的是對對象地址的拷貝,操做的是該拷貝地址。這在本質上和值類型參數按值傳遞是相同的,都是按值傳遞。不一樣的是值類型的「值」爲類型實例,而引用類型的「值」爲引用地址。所以,若是參數爲引用類型時,在調用方代碼中,能夠改變引用的指向, 從而使得原對象的指向發生改變,如例所示: 引用類型參數的按值傳遞 // FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/01 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net { class Args { public static void Main() { ArgsByRef abf = new ArgsByRef(); AddRef(abf); Console.WriteLine(abf.i); } private static void AddRef(ArgsByRef abf) { abf.i = 20; Console.WriteLine(abf.i); } }
class ArgsByRef { public int i = 10; } }
所以,咱們進一步能夠總結爲:按值傳遞的實質的是傳遞值,不一樣的是這個值在值類型和引用類型的表現是不一樣的:參數爲值類型時,「值」爲實例自己,所以傳遞的是實例拷貝,不會對原來的實例產生影響;參數爲引用類型時,「值」爲對象引用,所以傳遞的是引用地址拷貝,會改變原來對象的引用指向,這是兩者在統一律念上的表現區別,理解了本質也就抓住了根源。關於值類型和引用類型的概念能夠參考《第八回:品味類型---值類型與引用類型(上)-內存有理》《第九回:品味類型---值類型與引用類型(中)-規則無邊》《第十回:品味類型---值類型與引用類型(下)-應用征途》,相信能夠經過對系列中的值類型與引用類型的3篇的理解,加深對參數傳遞之惑的昭雪。
瞭解了引用類型參數按值傳遞的實質,咱們有必要再引入另外一個參數傳遞的概念,那就是:按引用傳遞,一般稱爲引用參數。這兩者的本質區別能夠小結爲:
引用類型參數的按值傳遞,傳遞的是參數自己的值,也就是上面提到的對象的引用;
按引用傳遞,傳遞的不是參數自己的值,而是參數的地址。若是參數爲值類型,則傳遞的是該值類型的地址;若是參數爲引用類型,則傳遞的是對象引用的地址。
關於引用參數的詳細概念,咱們立刻就展開來討論,不過仍是先分析一下string類型的特殊性,究竟特殊在哪裏?
關於string的討論,在本人拙做《第九回:品味類型---值類型與引用類型(中)-規則無邊》已經有了討論,也就是開篇陳述的本文成文的歷史,因此在上述分析的基礎上,我認爲應該更能對第九回的問題,作以更正。
string自己爲引用類型,所以從本文的分析中可知,對於形如
static void ShowInfo(string aStr){...}
的傳遞形式,能夠清楚的知道這是按值傳遞,也就是本文總結的引用類型參數的按值傳遞。所以,傳遞的是aStr對象的值,也就是aStr引用指針。接下來咱們看看下面的示例來分析,爲何string類型在傳遞時表現出特殊性及其產生的緣由?
// FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/05 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net { class how2str { static void Main() { string str = "Old String"; ChangeStr(str); Console.WriteLine(str); } static void ChangeStr(string aStr) { aStr = "Changing String"; Console.WriteLine(aStr); } } }
下面對上述示例的執行過程簡要分析一下:首先,string str = "Old String"產生了一個新的string對象,如圖表示:
而後執行ChangeStr(aStr),也就是進行引用類型參數的按值傳遞,咱們強調說這裏傳遞的是引用類型的引用值,也就是地址指針;而後調用ChangeStr方法,過程aStr = "Changing String"完成了如下的操做,先在新的一個地址生成一個string對象,該新對象的值爲"Changing String",引用地址爲0x06賦給參數aStr,所以會改變aStr的指向,可是並無改變原來方法外str的引用地址,執行過程能夠表示爲:
所以執行結果就可想而知,咱們從分析過程就能夠發現string做爲引用類型,在按值傳遞過程當中和其餘引用類型是同樣的。若是須要完成ChangeStr()調用後,改變原來str的值,就必須使用ref或者out修飾符,按照按引用傳遞的方式來進行就能夠了,屆時aStr = "Changing String"改變的是str
的引用,也就改變了str的指向,具體的分析但願你們經過接下來的按引用傳遞的揭密以後,能夠自行分析。
4.3 按引用傳遞之ref和out
無論是值類型仍是引用類型,按引用傳遞必須以ref或者out關鍵字來修飾,其規則是:
方法定義和方法調用必須同時顯示的使用ref或者out,不然將致使編譯錯誤;
CRL容許經過out或者ref參數來重載方法,例如: // FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/03 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net._11_Args { class TestRefAndOut { static void ShowInfo(string str) { Console.WriteLine(str); } static void ShowInfo(ref string str) { Console.WriteLine(str); } } }
固然,按引用傳遞時,無論參數是值類型仍是引用類型,在本質上也是相同的,這就是:ref和out關鍵字將告訴編譯器,方法傳遞的是參數地址,而不是參數自己。理解了這一點也就抓住了按引用傳遞的本質,所以根據這一本質結論咱們能夠得出如下更明白的說法,這就是:
無論參數自己是值類型仍是引用類型,按引用傳遞時,傳遞的是參數的地址,也就是實例的指針。
若是參數是值類型,則按引用傳遞時,傳遞的是值類型變量的引用,所以在效果上相似於引用類型參數的按值傳遞方式,其實質能夠分析爲:值類型的按引用傳遞方式,實現的是對值類型參數實例的直接操做,方法調用方爲該實例分配內存,而被調用方法操做該內存,也就是值類型的地址;而引用類型參數的按值傳遞方式,實現的是對引用類型的「值」引用指針的操做。例如: // FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/06 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net { class TestArgs { static void Main(string[] args) { int i = 100; string str = "One"; ChangeByValue(ref i); ChangeByRef(ref str); Console.WriteLine(i); Console.WriteLine(str); } static void ChangeByValue(ref int iVlaue) { iVlaue = 200; } static void ChangeByRef(ref string sValue) { sValue = "One more."; }
} }
若是參數是引用類型,則按引用傳遞時,傳遞的是引用的引用而不是引用自己,相似於指針的指針概念。示例只需將上述string傳遞示例中的ChangeStr加上ref修飾便可。
下面咱們再進一步對ref和out的區別作以交代,就基本闡述清楚了按引用傳遞的精要所在,能夠總結爲:
相同點:從CRL角度來講,ref和out都是指示編譯器傳遞實例指針,在表現行爲上是相同的。最能證實的示例是,CRL容許經過ref和out來實現方法重載,可是又不容許經過區分ref和out來實現方法重載,所以從編譯角度來看,無論是ref仍是out,編譯以後的代碼是徹底相同的。例如: // FileName : Anytao.net.My_Must_net // Description : The .NET what you should know of arguments. // Release : 2007/07/03 1.0 // Copyright : (C)2007 Anytao.com http://www.anytao.com using System; namespace Anytao.net.My_Must_net._11_Args { class TestRefAndOut { static void ShowInfo(string str) { Console.WriteLine(str); } static void ShowInfo(ref string str) { Console.WriteLine(str); } static void ShowInfo(out string str) { str = "Hello, anytao."; Console.WriteLine(str);
} } }
編譯器將提示: 「ShowInfo」不能定義僅在 ref 和 out 上有差異的重載方法。
不一樣點:使用的機制不一樣。ref要求傳遞以前的參數必須首先顯示初始化,而out不須要。也就是說,使用ref的參數必須是一個實際的對象,而不能指向null;而使用out的參數能夠接受指向null的對象,而後在調用方法內部必須完成對象的實體化。
5. 結論
完成了對值類型與引用類型的論述,在這些知識積累的基礎上,本文指望經過深刻的論述來進一步的分享參數傳遞的藝術,解開層層疑惑的面紗。從探討問題的角度來講,參數傳遞的種種誤區其實根植與對值類型和引用類型的本質理解上,所以完成了對類型問題的探討再進入參數傳遞的迷宮,咱們纔會更加遊刃有餘。我想,這種探討問題的方式,也正是咱們追逐問題的方式,深刻進入.NET的高級殿堂是繞不開這一選擇的。
參考文獻
(USA)Jeffrey Richter, Applied Microsoft .NET Framework Programming
(USA)David Chappell, Understanding .NET
第5章 內存天下
5.1 內存管理概要 5.1.1 引言 說起內存管理,始終是C++程序員最爲頭疼的問題,而這一切在.NET託管平臺下將變得容易,對象的建立、生存期管理及資源回收都由CLR負責,大大解放了開發者的精力,能夠將更多的腦細胞投入到業務邏輯的實現上。 那麼,使得這一切如此輕鬆的技術,又來自哪裏?答案是.NET自動內存管理(Automatic Memory Management)。CLR引入垃圾收集器(GC,Garbage Collection)來負責執行
內存的清理工做,GC經過對託管堆的管理,能有效解決C++程序中相似於內存泄漏、訪問不可達對象等問題。然而,必須明確的是垃圾回收並不能解決全部資源的清理,對於非託管資源,例如:數據庫連接、文件句柄、COM對象等,仍然須要開發者自行清理,.NET又是如何處理呢? 總結起來,.NET的自動內存管理,主要包括如下幾個方面: l 對象建立時的內存分配。 l 垃圾回收。 l 非託管資源釋放。 本節,首先對這幾個方面做以簡單的介紹,而詳細的論述在本章的其餘部分逐一展開。 5.1.2 內存管理概觀要論 本書在1.1節「對象的旅行」一節,從宏觀的角度對對象生命週期作了一番調侃,而宏觀以外對象的整個週期又是如何呢?下面,首先從一個典型的示例開始,之內存管理的角度對對象的生命週期作以梳理: class MemoryProcess { public static void Main() { //建立對象,分配內存,並初始化 FileStream fs = new FileStream(@"C:\temp.txt", FileMode.Create); try { //對象成員的操做和應用 byte[] txts = new UTF8Encoding(true).GetBytes("Hello, world."); fs.Write(txts, 0, txts.Length); }
finally { //執行資源清理 if (fs != null) fs.Close(); } } } 上述示例完成了一個簡單的文件寫入操做,咱們要關注的是FileStream類型對象從建立到消亡的整個過程,針對上述示例總結起來各個階段主要包括: l 對象的建立及內存分配。 經過new關鍵字執行對象建立並分配內存,對應於IL中的newobj指令,除了這種建立方式,.NET還提供了其餘的對象建立方式與內存分配,在本章5.2節「對象建立始末」中,將對.NET的內存分配及管理做以詳細的討論與分析。 l 對象初始化。 經過調用構造函數,完成對象成員的初始化,在本例FileStream對象的初始化過程當中,必然發生對文件句柄的初始化操做,以便執行讀寫文件等應用。.NET提供了15個不一樣的FileStream構造函數來完成對不一樣狀況下的初始化處理,詳細的分析見本章5.2節「對象建立始末」。 l 對象的應用和操做。 完成了內存分配和資源的初始化操做,就能夠使用這些資源進行必定的操做和應用,例如本例中fs.Write經過調用文件句柄進行文件寫入操做。 l 資源清理。 應用完成後,必須對對象訪問的資源進行清理,本例中經過Close方法來釋放文件句柄,關於非託管資源的釋放及其清理方式,詳見描述可參見5.3節「垃圾回收」。 l 垃圾回收。
在.NET中,內存資源的釋放由GC負責,這是.NET技術中最閃亮的技術之一。CLR徹底代替開發人員管理內存,從分配到回收都有相應的機制來完成,原來熟悉的free和delete命令早已不復存在,在本章5.3節「垃圾回收」中,將對垃圾回收機制做以詳細的討論與分析。 5.1.3 結論 雖然,CLR已經不須要開發者作太多的事情了,可是適度的探索能夠幫助咱們實現更好的駕馭,避免不少沒必要要的錯誤。本章的重點正是關於內存管理,對象建立、垃圾回收及性能優化等.NET核心問題的探討。本節能夠看做一個起點,在接下來的各篇中咱們將逐一領略.NET自動內存管理的各個方面。
5.2 對象建立始末 5.2.1 引言 瞭解.NET的內存管理機制,首先應該從內存分配開始,也就是對象的建立環節。對象的建立,是個複雜的過程,主要包括內存分配和初始化兩個環節。在本章開篇的示例中,對象的建立過程爲: FileStream fs = new FileStream(@"C:\temp.txt", FileMode.Create); 經過new關鍵字操做,即完成了對FileStream類型對象的建立過程,這一看似簡單的操做背後,卻經歷着至關複雜的過程和波折。 本篇全文,正是對這一操做背後過程的詳細討論,從中瞭解.NET的內存分配是如何實現的。 5.2.2 內存分配 關於內存的分配,首先應該瞭解分配在哪裏的問題。CLR管理內存的區域,主要有三塊,分別爲:
l 線程的堆棧,用於分配值類型實例。堆棧主要由操做系統管理,而不受垃圾收集器的控制,當值類型實例所在方法結束時,其存儲單位自動釋放。棧的執行效率高,但存儲容量有限。 l GC堆,用於分配小對象實例。若是引用類型對象的實例大小小於85000字節,實例將被分配在GC堆上,當有內存分配或者回收時,垃圾收集器可能會對GC堆進行壓縮,詳見後文講述。 l LOH(Large Object Heap)堆,用於分配大對象實例。若是引用類型對象的實例大小不小於85000字節時,該實例將被分配到LOH堆上,而LOH堆不會被壓縮,並且只在徹底GC回收時被回收。這種設計方案是對垃圾回收性能的優化考慮。 本節討論的重點是.NET的內存分配機制,所以下文將不加說明的以GC堆上的分配爲例來展開。關於值類型和引用類型的論述,請參見本書4.2節「品味類型——值類型與引用類型」。 瞭解了內存分配的區域,接着咱們看看有哪些操做將致使對象建立和內存分配的發生,在本書3.4節「經典指令解析之實例建立」一節中,詳細描述了關於實例建立的多個IL指令解析,主要包括: l newobj,用於建立引用類型對象。 l ldstr,用於建立string類型對象。 l newarr,用於分配新的數組對象。 l box,在值類型轉換爲引用類型對象時,將值類型字段拷貝到託管堆上發生的內存分配。 在上述論述的基礎上,咱們將從堆棧的內存分配和託管堆的內存分配兩個方面來分別論述.NET的內存分配機制。 1.堆棧的內存分配機制 對於值類型來講,通常建立在線程的堆棧上。但並不是全部的值類型都建立在線程的堆棧上,例如做爲類的字段時,值類型做爲實例成員的一部分也被建立在託管堆上;裝箱發生時,值類型字段也會拷貝在託管堆上。 對於分配在堆棧上的局部變量來講,操做系統維護着一個堆棧指針來指向下一個自由空間的地址,而且堆棧的內存地址是由高位到低位向下填充,也就表示入棧時棧頂向低地址擴展,出棧時,棧頂向高地址回退。如下例而言: public void MyCall() {
int x = 100; char c = 'A'; } 當程序執行至MyCall方法時,假設此時線程棧的初始地址爲50000,所以堆棧指針開始指向50000地址空間。方法調用時,首先入棧的是返回地址,也就是方法執行以後的下一條可執行語句的地址,用於方法返回以後程序繼續執行,如圖5-1所示。
圖5-1 棧上的內存分配 而後是整型局部變量x,它將在棧上分配4Byte的內存空間,所以堆棧指針繼續向下移動4個字節,並將值100保存在相應的地址空間,同時堆棧指針指向下一個自由空間,如圖5-2所示。 圖5-2 棧上的內存分配 接着是字符型變量c,在堆棧上分配2Byte的內存空間,所以堆棧指針向下移動2個字節,值‘A’會保存在新分配的棧上空間,內存的分配如圖5-3所示。 圖5-3 棧上的內存分配
最後,MyCall方法開始執行,直到方法體執行結束,執行結果被返回,棧上的存儲單元也被自行釋放。其釋放過程和分配過程恰好相反:首先刪除c的內存,堆棧指針向上遞增2個字節,而後刪除x的內存,堆棧指針繼續向上遞增4個字節,最終的內存情況如圖5-4所示,程序又將回到棧上最初的方法調用地址,繼續向下執行。 圖5-4 棧上的內存分配 其實,實際的分配狀況是個很是複雜的分配過程,同時還包括方法參數,堆引用等多種情形的發生,可是本例演示的簡單過程基本闡釋了棧上分配的操做方式和過程。經過內置於處理器的特殊指令,棧上的內存分配,效率較高,可是內存容量不大,同時棧上變量的生存週期由系統自行管理。 注意 上述執行過程,只是一個簡單的模擬狀況,實際上在方法調用時都會在棧中建立一個活動記錄(包含參數、返回值地址和局部變量),並分配相應的內存空間,這種分配是一次性完成的。方法執行結束返回時,活動記錄清空,內存被一次性解除。而數據的壓棧和出棧是有順序的,棧內是先進先出(FILO)的形式。具體而言:首先入棧的是返回地址;而後是參數,通常以由右向左的順序入棧;最後是局部變量,依次入棧。方法執行以後,出棧的順序正好相反,首先是局部變量,再是參數,最後是那個地址指針。
2.託管堆的內存分配機制 引用類型的實例分配於託管堆上,而線程棧倒是對象生命週期開始的地方。對32位處理器來講,應用程序完成進程初始化後,CLR將在進程的可用地址空間上分配一塊保留的地址空間,它是進程(每一個進程可以使用4GB)中可用地址空間上的一塊內存區域,但並不對應於任何物理內存,這塊地址空間便是託管堆。 託管堆又根據存儲信息的不一樣劃分爲多個區域,其中最重要的是垃圾回收堆(GC Heap)和加載堆(Loader Heap),GC Heap用於存儲對象實例,受GC管理;Loader Heap用於存儲類型系統,又分爲High-Frequency Heap、Low-Frequency Heap和Stub Heap,不一樣的堆上存儲不一樣的信息。Loader Heap最重要的信息就是元數據相關的信息,也就是Type對象,每一個Type在Loader Heap上體現爲一個Method Table(方法表),而Method Table中則記錄了存儲的元數據信息,例如基類型、靜態字段、實現的接口、全部的方法等等。Loader Heap不受GC控制,其生命週期爲從建立到AppDomain卸載。 在進入實際的內存分配分析以前,有必要對幾個基本概念作個交代,以便更好地在接下來的分析中展開討論。 TypeHandle,類型句柄,指向對應實例的方法表,每一個對象建立時都包含該附加成員,而且佔用4個字節的內存空間。咱們知道,每一個類型都對應於一個方法表,方法表建立於編譯時,主要包含了類型的特徵信息、實現的接口數目、方法表的slot數目等。 SyncBlockIndex,用於線程同步,每一個對象建立時也包含該附加成員,它指向一塊被稱爲Synchronization Block的內存塊,用於管理對象同步,一樣佔用4個字節的內存空間。 NextObjPtr,由託管堆維護的一個指針,用於標識下一個新建對象分配時在託管堆中所處的位置。CLR初始化時,NextObjPtr位於託管堆的基地址。 所以,咱們對引用類型分配過程應該有個基本的瞭解,因爲本篇示例中FileStream類型的繼承關係相對複雜,在此本節實現一個相對簡單的類型來作說明: public class UserInfo { private Int32 age = -1; private char level = 'A';
} public class User { private Int32 id; private UserInfo user; } public class VIPUser : User { public bool isVip; public bool IsVipUser() { return isVip; } public static void Main() { VIPUser aUser; aUser = new VIPUser(); aUser.isVip = true; Console.WriteLine(aUser.IsVipUser()); } } 將上述實例的執行過程,反編譯爲IL語言可知:new關鍵字被編譯爲newobj指令來完成對象建立工做,進而調用類型的構造器來完成其初始化操做,在此咱們詳細的描述其執行的具體過程。 首先,將聲明一個引用類型變量aUser: VIPUser aUser; 它僅是一個引用(指針),保存在線程的堆棧上,佔用4Byte的內存空間,將用於保存VIPUser對象的有效地址,其執行過程正是上文描述的在線程棧上的分配過程。此時aUser未指向任何有效的實例,所以被自行初始化爲null,試圖對aUser的任何操做將拋出NullReferenceException異常。
接着,經過new操做執行對象建立: aUser = new VIPUser(); 如上文所言,該操做對應於執行newobj指令,其執行過程又可細分爲如下幾步: (a)CLR按照其繼承層次進行搜索,計算類型及其全部父類的字段,該搜索將一直遞歸到System.Object類型,並返回字節總數,以本例而言類型VIPUser須要的字節總數爲15Byte,具體計算爲:VIPUser類型自己字段isVip(bool型)爲1Byte;父類User類型的字段id(Int32型)爲4Byte,字段user保存了指向UserInfo型的引用,所以佔4Byte,而同時還要爲UserInfo分配6Byte字節的內存。 (b)實例對象所佔的字節總數還要加上對象附加成員所需的字節總數,其中附加成員包括TypeHandle和SyncBlockIndex,共計8字節(在32位CPU平臺下)。所以,須要在託管堆上分配的字節總數爲23字節,而堆上的內存塊老是按照4Byte的倍數進行分配,所以本例中將分配24字節的地址空間。 (c)CLR在當前AppDomain對應的託管堆上搜索,找到一個未使用的24字節的連續空間,併爲其分配該內存地址。事實上,GC使用了很是高效的算法來知足該請求,NextObjPtr指針只須要向前推動24個字節,並清零原NextObjPtr指針和當前NextObjPtr指針之間的字節,而後返回原NextObjPtr指針地址便可,該地址正是新建立對象的託管堆地址,也就是aUser引用指向的實例地址。而此時的NextObjPtr仍指向下一個新建對象的位置。注意,棧的分配是向低地址擴展,而堆的分配是向高地址擴展。 另外,實例字段的存儲是有順序的,由上到下依次排列,父類在前子類在後,詳細的分析請參見1.2節「什麼是繼承」。 在上述操做時,若是試圖分配所需空間而發現內存不足時,GC將啓動垃圾收集操做來回收垃圾對象所佔的內存,咱們將在下一節對此作詳細的分析。 最後,調用對象構造器,進行對象初始化操做,完成建立過程。該構造過程,又可細分爲如下幾個環節: (a)構造VIPUser類型的Type對象,主要包括靜態字段、方法描述、實現的接口等,並將其分配在上文提到託管堆的Loader Heap上。
(b)初始化aUser的兩個附加成員:TypeHandle和SyncBlockIndex。將TypeHandle指針指向Loader Heap上的MethodTable,CLR將根據TypeHandle來定位具體的Type;將SyncBlockIndex指針指向Synchronization Block的內存塊,用於在多線程環境下對實例對象的同步操做。 (c)調用VIPUser的構造器,進行實例字段的初始化。實例初始化時,會首先向上遞歸執行父類初始化,直到完成System.Object類型的初始化,而後再返回執行子類的初始化,直到執行VIPUser類爲止。以本例而言,初始化過程首先執行System.Object類,再執行User類,最後纔是VIPUser類。最終,newobj分配的託管堆的內存地址,被傳遞給VIPUser的this參數,並將其引用傳給棧上聲明的aUser。 關於構造函數的執行順序,本書在7.8節「動靜之間:靜態和非靜態」一節有較爲詳細的論述。 上述過程,基本完成了一個引用類型建立、內存分配和初始化的整個流程,然而該過程只能看做是一個簡化的描述,實際的執行過程更加複雜,涉及一系列細化的過程和操做。對象建立並初始化以後,內存的佈局,能夠表示爲圖5-5。 圖5-5 堆上的內存分配 由上面的分析可知,在託管堆中增長新的實例對象,只是將NextObjPtr指針增長必定的數值,再次新增的對象將分配在當前NextObjPtr指向的內存空間,所以在託管堆棧中,連續分配的對象在內存中必定是連續的,這種分配機制很是高效。 3.必要的補充 有了對象建立的基本流程概念,下面的幾個問題時常引發你們的思考,在此本節一併作以探索:
l 值類型中的引用類型字段和引用類型中的值類型字段,其分配狀況又是如何? 這一思考實際上是一個問題的兩個方面:對於值類型嵌套引用類型的狀況,引用類型變量做爲值類型的成員變量,在堆棧上保存該成員的引用,而實際的引用類型仍然保存在GC堆上;對於引用類型嵌套值類型的狀況,則該值類型字段將做爲引用類型實例的一部分保存在GC堆上。本書在4.2節「品味類型——值類型與引用類型」一節對這種嵌套結構,有較詳細的分析。 l 方法保存在Loader Heap的MethodTable中,那麼方法調用時又是怎樣的過程呢? 如上所言,MethodTable中包含了類型的元數據信息,類在加載時會在Loader Heap上建立這些信息,一個類型在內存中對應一份MethodTable,其中包含了全部的方法、靜態字段和實現的接口信息等。對象實例的TypeHandle在實例建立時,將指向MethodTable開始位置的偏移處(默認偏移12Byte)。經過對象實例調用某個方法時,CLR根據TypeHandle能夠找到對應的MethodTable,進而能夠定位到具體的方法,再經過JIT Compiler將IL指令編譯爲本地CPU指令,該指令將保存在一個動態內存中,而後在該內存地址上執行該方法,同時該CPU指令被保存起來用於下一次的執行。 在MethodTable中,包含一個Method Slot Table,稱爲方法槽表,該表是一個基於方法實現的線性鏈表,並按照如下順序排列:繼承的虛方法、引入的虛方法、實例方法和靜態方法。方法表在建立時,將按照繼承層次向上搜索父類,直到System.Object類型,若是子類覆寫了父類方法,則將會以子類方法覆蓋父類虛方法。關於方法表的建立過程,能夠參考2.2節「什麼是繼承」中的描述。 l 靜態字段的內存分配和釋放,又有何不一樣? 靜態字段也保存在方法表中,位於方法表的槽數組後,其生命週期爲從建立到AppDomain卸載。所以一個類型不管建立多少個對象,其靜態字段在內存中也只有一份。靜態字段只能由靜態構造函數進行初始化,靜態構造函數確保在任何對象建立前,或者在任何靜態字段或方法被引用前執行,其詳細的執行順序在7.8節「動靜之間:靜態和非靜態」有所討論。 5.2.3 結論
對象建立過程的瞭解,是從底層接觸CLR運行機制的入口,也是認識.NET自動內存管理的關鍵。經過本節的詳細論述,關於對象的建立、內存分配、初始化過程和方法調用等技術都會創建一個相對全面的理解,同時也清楚地把握了線程棧和託管堆的執行機制。 對象老是有生有滅,本節簡述其生,下一節討論其亡。繼續本章對自動內存管理技術的認識,下一個重要的內容就是:垃圾回收機制。
5.3 垃圾回收 本節將介紹如下內容: — .NET垃圾回收機制 — 非託管資源的清理 5.3.1 引言 .NET自動內存管理將開發人員從內存錯誤的泥潭中解放出來,這一切都歸功於垃圾回收(GC,Garbage Collection)機制。 經過對對象建立全過程的講述,咱們理解了CLR執行對象內存分配的基本面貌。一個分配了內存空間和完成初始化的對象實例,就是一個CLR世界中的新生命體,其生命週期大概能夠歸納爲:對象在系統中進行必定的操做和應用,到必定階段它將不被系統中任何對象引用或操做,則表示該對象不會再被使用。所以,對象符合了能夠銷燬的條件,而CLR可能不會立刻執行銷燬操做,而是在適當的時間執行該對象的內存銷燬。一旦被執行銷燬,對象及其成員將不可在運行時使用,最後由垃圾收集器釋放其內存資源,完成一個對象由生而滅的全過程。 因而可知,在.NET中自動內存管理是由垃圾回收器來執行的,GC自動完成對託管堆的全權管理,然而一股腦將全部事情交給GC,並不是萬全保障。基於性能與安全的考慮,頗有必要對GC的工做機理、執行過程,以及對非託管資源的清理作一個討論。 5.3.2 垃圾回收
顧名思義,垃圾回收就是清理內存中的垃圾,所以瞭解垃圾回收機制就應從如下幾個方面着手: l 什麼樣的對象被GC認爲是垃圾呢? l 如何回收? l 什麼時候回收? l 回收以後,又執行哪些操做? 清楚地回答上述幾個問題,也就基本瞭解.NET的垃圾回收機制。下面本節就逐一揭開這幾個問題的答案。 l 什麼樣的對象被GC認爲是垃圾呢? 簡單地說,一個對象成爲「垃圾」就表示該對象不被任何其餘對象所引用。所以,GC必須採用必定的算法在託管堆中遍歷全部對象,最終造成一個可達對象圖,而不可達的對象將成爲被釋放的垃圾對象等待收集。 l 如何回收? 每一個應用程序有一組根(指針),根指向託管堆中的存儲位置,由JIT編譯器和CLR運行時維護根指針列表,主要包括全局變量、靜態變量、局部變量和寄存器指針等。下面以一個簡單的示例來講明,GC執行垃圾收集的具體過程。 class A { private B objB; public A(B o) { objB = o; } ~A() { Console.WriteLine("Destory A."); }
} class B { private C objC; public B(C o) { objC = o; } ~B() { Console.WriteLine("Destory B."); } } class C { ~C() { Console.WriteLine("Destory C."); } } public class Test_GCRun { public static void Main() { A a = new A(new B(new C())); //強制執行垃圾回收 GC.Collect(0); GC.WaitForPendingFinalizers(); } }
在上述執行中,當建立類型A的對象a時,在託管堆中將新建類型B的實例(假設表示爲objB)和類型C的實例(假設表示爲objC),而且這幾個對象之間保存着必定的聯繫。而局部變量a則至關於一個應用程序的根,假設其在託管堆中對應的實例表示爲objA,則當前的引用關係能夠表示爲圖5-6。 圖5-6 垃圾收集執行前的託管堆 垃圾收集器正是經過根指針列表來得到託管堆中的對象圖,其中定義了應用程序根引用的託管堆中的對象,當垃圾收集器啓動時,它假設全部對象都是可回收的垃圾,並開始遍歷全部的根,將根引用的對象標記爲可達對象添加到可達對象圖中,在遍歷過程當中,若是根引用的對象還引用着其餘對象,則該對象也被添加到可達對象圖中,依次類推,垃圾收集器經過根列表的遞歸遍歷,將能找到全部可達對象,並造成一個可達對象圖。同時那些不可達對象則被認爲是可回收對象,垃圾收集器接着運行垃圾收集進程來釋放垃圾對象的內存空間。一般,將這種收集算法稱爲:標記和清除收集算法。 在上例中,a能夠看出是應用程序的一個根,它在託管堆中對應的對象objA就是一個可達對象,而對象objA依次關聯的objB、objC都是可達對象,被添加到可達對象圖中。當Main方法運行結束時,a再也不被引用,則其再也不是一個根,此時經過GC.Collect強制啓動垃圾收集器,a對應的objA,以及相關聯的objB和objC將成爲不可達對象,咱們從執行結果中能夠看出類型A、B、C的析構方法被分別調用,由此能夠分析垃圾回收執行了對objA、objB、objC實例的內存回收。 l 什麼時候回收? 垃圾收集器週期性的執行內存清理工做,通常在如下狀況出現時垃圾收集器將會啓動: (1)內存不足溢出時,更確切地應該說是第0代對象充滿時。 (2)調用GC.Collect方法強制執行垃圾回收。
(3)Windows報告內存不足時,CLR將強制執行垃圾回收。 (4)CLR卸載AppDomain時,GC將對全部代齡的對象執行垃圾回收。 (5)其餘狀況,例如物理內存不足,超出短時間存活代的內存段門限,運行主機拒絕分配內存等等。 做爲開發人員,咱們無需實現任何代碼來管理應用程序中各個對象的生命週期,CLR知道什麼時候去執行垃圾收集工做來知足應用程序的內存需求。當上述狀況發生時,GC將着手進行內存清理,當內存釋放以前GC會首先檢查終止化鏈表中是否有記錄來決定在釋放內存以前執行非託管資源的清理工做,而後才執行內存釋放。 同時,微軟強烈建議不要經過GC.Collect方法來強制執行垃圾收集,由於那會妨礙GC自己的工做方式,經過Collect會使對象代齡不斷提高,擾亂應用程序的內存使用。只有在明確知道有大量對象中止引用時,才考慮使用GC.Collect方法來調用收集器。 l 回收以後,又執行哪些操做? GC在垃圾回收以後,堆上將出現多個被收集對象的「空洞」,爲避免託管堆的內存碎片,會從新分配內存,壓縮託管堆,此時GC能夠看出是一個緊縮收集器,其具體操做爲:GC找到一塊較大的連續區域,而後將未被回收的對象轉移到這塊連續區域,同時還要對這些對象重定位,修改應用程序的根以及發生引用的對象指針,來更新複製後的對象位置。所以,勢必影響GC回收的系統性能,而CLR垃圾收集器使用了Generation的概念來提高性能,還有其餘一些優化策略,如併發收集、大對象策略等,來減小垃圾收集對性能的影響。例如,上例中執行後的託管堆的內存情況能夠表示爲圖5-7。
圖5-7 垃圾收集執行後的託管堆 CLR提供了兩種收集器:工做站垃圾收集器(Workstation GC,包含在mscorwks.dll)和服務器垃圾收集器(Server GC,包含在mscorsvr.dll),分別爲不一樣的處理機而設計,默認狀況爲工做站收集器。工做站收集器主要應用於單處理器系統,工做站收集器儘量地經過減小垃圾回收過程當中程序的暫停次數來提升性能;服務器收集器,專爲具備多處理器的服務器系統而設計,採用並行算法,每一個CPU都具備一個GC線程。在CLR加載到進程時,能夠經過CorBindToRuntimeEx()函數來選擇執行哪一種收集器,選擇合適的收集器也是有效、高效管理的關鍵。 關於代齡(Generation) 接下來對文中屢次提到的代齡概念作以解釋,來理解GC在性能優化方面的策略機制。 垃圾收集器將託管堆中的對象分爲三代,分別爲:0、1和2。在CLR初始化時,會選擇爲三代設置不一樣的闕值容量,通常分配爲:第0代大約256KB,第1代2MB,第2代10MB,可表示爲如圖5-8所示。顯然,容量越大效率越低,而GC收集器會自動調節其闕值容量來提高執行效率,第0代對象的回收效率確定是最高的。 圖5-8 代齡的闕值容量 在CLR初始化後,首先被添加到託管堆中的對象都被定爲第0代,如圖5-9所示。當有垃圾回收執行時,未被回收的對象代齡將提高一級,變成第1代對象,然後新建的對象仍爲第0代對象。也就是說,代齡越小,表示對象越新,一般狀況下其生命週期也最短,所以垃圾收集器老是首先收集第0代的不可達對象內存。
隨着對象的不斷建立,垃圾收集再次啓動時則只會檢查0代對象,並回收0代垃圾對象。而1代對象因爲未達到預約的1代容量闕值,則不會進行垃圾回收操做,從而有效的提升了垃圾收集的效率,這就是代齡機制在垃圾回收中的性能優化做用。
圖5-9 初次執行垃圾回收 那麼,垃圾收集器在什麼狀況下,才執行對第1代對象的收集呢?答案是僅當第0代對象釋放的內存不足以建立新的對象,同時1代對象的體積也超出了容量闕值時,垃圾收集器將同時對0代和1代對象進行垃圾回收。回收以後,未被回收的1代對象升級爲2代對象,未被回收的0代對象升級爲1代對象,然後新建的對象仍爲第0代對象,如圖5-10所示。垃圾收集正是對上述過程的不斷重複,利用分代機制提升執行效率。 圖5-10 執行1代對象垃圾回收 經過GC.Collect方法能夠指定對從第0代到指定代的對象進行回收,經過GC. MaxGeneration來獲取框架版本支持的代齡的最大有效值。 規則小結 關於垃圾回收,對其有如下幾點小結: l CLR提供了一種分代式、標記清除型GC,利用標記清除算法來對不一樣代齡的對象進行垃圾收集和內存緊縮,保證了運算效率和執行優化。
l 一個對象沒有被其餘任何對象引用,則該對象被認爲是能夠回收的對象。 l 最好不要經過調用GC.Collect來強制執行垃圾收集。 l 垃圾對象並不是當即被執行內存清理,GC能夠在任什麼時候候執行垃圾收集。 l 對「胖」對象考慮使用弱引用,以提升性能,詳見5.4節「性能優化的多方探討」。 5.3.3 非託管資源清理 對於大部分的類型來講,只存在內存資源的分配與回收問題,所以CLR的處理已經可以知足這種需求,然而還有部分的類型不可避免的涉及訪問其餘非託管資源。常見的非託管資源包括數據庫連接、文件句柄、網絡連接、互斥體、COM對象、套接字、位圖和GDI+對象等。 GC全權負責了對託管堆的內存管理,而內存以外的資源,又該由誰打理?在.NET中,非託管資源的清理,主要有兩種方式:Finalize方法和Dispose方法,這兩種方法提供了在垃圾收集執行前進行資源清理的方法。Finalize方式,又稱爲終止化操做,其大體的原理爲:經過對自定義類型實現一個Finalize方法來釋放非託管資源,而終止化操做在對象的內存回收以前經過調用Finalize方法來釋放資源;Dispose模式,指的是在類中實現IDisposable接口,該接口中的Dispose方法定義了顯式釋放由對象引用的全部非託管資源。所以,Dispose方法提供了更加精確的控制方式,在使用上更加的靈活。 1.終止化操做 對C++程序員來講,提起資源釋放,會首先想到析構器。不過,在.NET世界裏,沒落的析構器已經被終結器取而代之,.NET在語法上選擇了相似的實現策略,例如你能夠有以下定義: class GCApp: Object { ~GCApp() { //執行資源清理 } }
將上述代碼編譯爲IL: .method family hidebysig virtual instance void Finalize() cil managed { // 代碼大小 14 (0xe) .maxstack 1 .try { IL_0000: nop IL_0001: nop IL_0002: leave.s IL_000c } // end .try finally { IL_0004: ldarg.0 IL_0005: call instance void [mscorlib]System.Object::Finalize() IL_000a: nop IL_000b: endfinally } // end handler IL_000c: nop IL_000d: ret } // end of method GCApp::Finalize 可見,編譯器將~GCApp方法編譯爲託管模塊元數據中一個Finalize方法,因爲示例自己沒有實現任何資源清理代碼,上述Finalize方法只是簡單調用了Object.Finalize方法。能夠經過重寫基類的Finalize方法實現資源清理操做,注意:自.NET 2.0起,C#編譯器認爲Finalize方法是一個特殊的方法,對其調用或重寫必須使用析構函數語法來實現,不能夠經過顯式非覆寫Finalize方法來實現。所以在自定義類型中重寫Finalize方法將等效於: protected override void Finalize() { try
{ //執行自定義資源清理操做 } finally { base.Finalize(); } } 因而可知,在繼承鏈中全部實例將遞歸調用base.Finalize方法,也就是意味調用終結器釋放資源時,將釋放全部的資源,包括父類對象引用的資源。所以,在C#中,也無需調用或重寫Object.Finalize方法,事實上顯示的重寫會引起編譯時錯誤,只需實現虛構函數便可。 在具體操做上,終結器的工做原理是這樣的:在Systm.Object中,Finalize方法被實現爲一個受保護的虛方法,GC要求任何須要釋放非託管資源的類型都要重寫該方法,若是一個類型及其父類均未重寫Systm.Object的Finalize方法,則GC認爲該類型及其父類不須要執行終止化操做,當對象變成不可達對象時,將不會執行任何資源清理操做;而若是隻有父類重寫了Finalize方法,則父類會執行終止化操做。所以,對於在類中重寫了Finalize的方法(在C#中實現析構函數),當GC啓動時,對於斷定爲可回收的垃圾對象,GC會自動執行其Finalize方法來清理非託管資源。例如一般狀況下,對於Window資源的釋放,是經過調用Win32API的CloseHandle函數來實現關閉打開的對象句柄。 對於重寫了Finalize方法的類型來講,能夠經過GC. SuppressFinalize來免除終結。 對於Finalize方式來講,存在以下幾個弊端,所以通常狀況下在自定義類型中應避免重寫Finalize方法,這些弊端主要包括: l 終止化操做的時間沒法控制,執行順序也不能保證。所以,在資源清理上不夠靈活,也可能因爲執行順序的不肯定而訪問已經執行了清理的對象。 l Finalize方法會極大地損傷性能,GC使用一個終止化隊列的內部結構來跟蹤具備Finalize方法的對象。當重寫了Finalize方法的類型在建立時,要將其指針添加到該終止化隊列中,由此對性能產生影響;另外,垃圾回收時調用Finalize方法將同時清理全部的資源,包括其父類對象的資源,也是影響性能的一個因素。
l 重寫了Finalize方法的類型對象,其引用類型對象的代齡將被提高,從而帶來內存壓力。 l Finalize方法在某些狀況下可能不被執行,例如可能某個終結器被無限期的阻止,則其餘終結器得不到調用。所以,應該確保重寫的Finalize方法儘快被執行。 基於以上緣由,應該避免重寫Finalize方法,而實現Dispose模式來完成對非託管資源的清理操做,具體實現見下文描述。 對於Finalize方法,有如下規則值得總結: l 在C#中沒法顯示的重寫Finalize方法,只能經過析構函數語法形式來實現。 l struct中不容許定義析構函數,只有class中才能夠,而且只能有一個。 l Finalize方法不能被繼承或重載。 l 析構函數不能加任何修飾符,不能帶參數,也不能被顯示調用,惟一的例外是在子類重寫時,經過base調用父類Finalize方法,並且這種方式也被隱式封裝在析構函數中。 l 執行垃圾回收以前系統會自動執行終止化操做。 l Finalize方法中,能夠實現使得被清理對象復活的機制,不過這種操做至關危險,並且沒有什麼實際意義,僅做參考,不推薦使用: public class ReLife { ~ReLife() { //對象從新被一個根引用 Test_ReLife.Instance = this; //從新將對象添加到終止化隊列 GC.ReRegisterForFinalize(this); } public void ShowInfo() { Console.WriteLine("對象又復活了。"); }
} public class Test_ReLife { public static ReLife Instance; public static void Main() { Instance = new ReLife(); Instance = null; GC.Collect(); GC.WaitForPendingFinalizers(); //對象又復活了 Instance.ShowInfo(); } } 2.Dispose模式 另外一種非託管資源的清理方式是Dispose模式,其原理是定義的類型必須實現System.IDisposable接口,該接口中定義了一個公有無參的Dispose方法,用戶能夠在該方法中實現對非託管資源的清理操做。在此,咱們實現一個典型的Dispose模式: class MyDispose : IDisposable { //定義一個訪問外部資源的句柄 private IntPtr _handle; //標記Dispose是否被調用 private bool disposed = false; //實現IDisposable接口 public void Dispose() { Dispose(true); //阻止GC調用Finalize方法 GC.SuppressFinalize(this);
} //實現一個處理資源清理的具體方法 protected virtual void Dispose(bool disposing) { if (! disposed) { if (disposing) { //清理託管資源 } //清理非託管資源 if (_handle != IntPtr.Zero) { //執行資源清理,在此爲關閉對象句柄 CloseHandle(_handle); _handle = IntPtr.Zero; } } disposed = true; } public void Close() { //在內部調用Dispose來實現 Dispose(); } } 在上述實現Dispose模式的典型操做中,有幾點說明: l Dispose方法中,應該使用GC. SuppressFinalize防止GC調用Finalize方法,由於顯式調用Dispose顯然是較佳選擇。 l 公有Dispose方法不能實現爲虛方法,以禁止在派生類中重寫。
l 在該模式中,公有Dispose方法經過調用重載虛方法Dispose(bool disposing)方法來實現,具體的資源清理操做實現於虛方法中。兩種策略的區別是:disposing參數爲真時,Dispose方法由用戶代碼調用,可釋放託管或者非託管資源;disposing參數爲假時,Dispose方法由Finalize調用,而且只能釋放非託管資源。 l disposed字段,保證了兩次調用Dispose方法不會拋出異常,值得推薦。 l 派生類中實現Dispose模式,應該重寫基類的受保護Dispose方法,而且經過base調用基類的Dispose方法,以確保釋放繼承鏈上全部對象的引用資源,在整個繼承層次中傳播Dispose模式。 protected override void Dispose(bool disposing) { if (!disposed) { try { //子類資源清理 //...... disposed = true; } finally { base.Dispose(disposing); } } } l 另外,基於編程習慣的考慮,通常在實現Dispose方法時,會附加實現一個Close方法來達到一樣的資源清理目的,而Close內部其實也是經過調用Dispose來實現的。 3.最佳策略 最佳的資源清理策略,應該是同時實現Finalize方式和Dispose方式。一方面,Dispose方法能夠克服Finalize方法在性能上的諸多弊端;另外一方面,Finalize方法又可以確保沒有顯式調用Dispo
se方法時,也自行回收使用的全部資源。事實上,.NET框架類庫的不少類型正是同時實現了這兩種方式,例如FileStream等。所以,任何重寫了Finalize方法的類型都應實現Dispose方法,來實現更加靈活的資源清理控制。 所以,咱們模擬一個簡化版的文件處理類FileDealer,其中涉及對文件句柄的訪問,以此來講明在自定義類型中對非託管資源的清理操做,在此同時應用Finalize方法和Dispose方法來實現: class FileDealer: IDisposable { //定義一個訪問文件資源的Win32句柄 private IntPtr fileHandle; //定義引用的託管資源 private ManagedRes managedRes; //定義構造器,初始化託管資源和非託管資源 public FileDealer(IntPtr handle, ManagedRes res) { fileHandle = handle; managedRes = res; } //實現終結器,定義Finalize ~FileDealer() { if(fileHandle != IntPtr.Zero) { Dispose(false); } } //實現IDisposable接口 public void Dispose() { Dispose(true); //阻止GC調用Finalize方法
GC.SuppressFinalize(this); } //實現一個處理資源清理的具體方法 protected virtual void Dispose(bool disposing) { if (disposing) { //清理託管資源 managedRes.Dispose(); } //執行資源清理,在此爲關閉對象句柄 if (fileHandle != IntPtr.Zero) { CloseHandle(fileHandle); fileHandle = IntPtr.Zero; } } public void Close() { //在內部調用Dispose來實現 Dispose(); } //實現對文件句柄的其餘應用方法 public void Write() { } public void Read() { } //引入外部Win32API [DllImport("Kernel32")] private extern static Boolean CloseHandle(IntPtr handle); } 注意,本例只是一個簡單化的演示,並不是專門的設計文件操做類型。在.NET框架中的FileStream類中,文件句柄被封裝到一個SafeFileHandle的類中實現,該類間接繼承於SafeHandle抽象類。
其中SafeHandle類型是一個對操做系統句柄的包裝類,實現了對本地資源的封裝,所以對於大部分的資源訪問應用來講,以SafeHandle的派生類做爲操做系統資源的訪問方式,是安全而可信的,例如FileStream中的SafeFileHandle類,就是對文件句柄的有效包裝。 4.using語句 using語句簡化了資源清理代碼實現,而且可以確保Dispose方法獲得調用,所以值得推薦。凡是實現了Dispose模式的類型,都可以using語句來定義其引用範圍。關於using語句的詳細描述,請參考6.3節「using的多重身份」,在此咱們將演示引用using語句實現對上述FileDealer類的訪問: public static void Main() { using(FileDealer fd = new FileDealer(new IntPtr(), new ManagedRes())) { fd.Read(); } } 上述執行,等效於實現了一個try/finally塊,並將資源清理代碼置於finally塊中: public static void Main() { FileDealer fd = null; try { fd = new FileDealer(new IntPtr(), new ManagedRes()); fd.Read(); } finally { if(fd != null) fd.Dispose(); }
} 5.規則所在 對於Finalize方法和Dispose方法,有以下的規則,留做參考: l 對於非託管資源的清理,Finalize由GC自行調用,而Dispose由開發者強制執行調用。 l 儘可能避免使用Finalize方式來清理資源,必須實現Finalize時,也應一併實現Dispose方法,來提供顯式調用的控制權限。 l 經過GC. SuppressFinalize能夠免除終結。 l 垃圾回收時,執行終結器的準確時間是不肯定的,除非顯式的調用Dispose或者Close方法。 l 強烈建議不要重寫Finalize方法,同時強烈建議在任何有非託管資源訪問的類中同時實現終止化操做和Dispose模式。 l Finalize方法和Dispose方法,只能清理非託管資源,釋放內存的工做仍由GC負責。 l 對象使用完畢應該當即釋放其資源,最好顯式調用Dispose方法來實現。 5.3.4 結論 .NET自動內存管理,是CLR提供的最爲重要的基礎服務之一。經過本節對垃圾回收和非託管資源的管理分析,能夠基本瞭解CLR對系統資源管理回收方面的操做本質。對於開發人員來講,GC全權負責了對內存的管理、監控與回收,咱們應將更多的努力關注於非託管資源的清理方式的理解和應用上,以提高系統資源管理的性能和安全。
5.4 性能優化的多方探討 本節將介紹如下內容: — .NET性能優化的策略探討 — 多種性能優化分析 5.4.1 引言
什麼纔算良好的軟件產品?業務流程、用戶體驗、安全性還有性能,一個都不能少。所以,良好的系統性能,是用戶評價產品的重要指標之一。交易所裏數以萬億計的數據要想保證全球股市交易的暢通無阻,穩定運行和高效的性能缺一不可。而小型系統的性能,一樣會受到關注,由於誰也不想訪問一個蝸牛般的軟件系統。 所以,性能是系統設計的重要因素,然而影響系統性能的要素又是多種多樣,例如硬件環境、數據庫設計以及軟件設計等等。本節將關注集中在.NET中最多見的性能殺手,並以條款的方式來一一展示,某些多是規則,某些多是習慣,而某些多是語法。 本節在分析了.NET自動內存管理機制的基礎上,來總結.NET開發中值得關注的性能策略,並以這些策略做爲選擇的依據和平衡的槓桿。同時,本節的優化條款主要針對.NET基礎展開,而不針對專門的應用環節,例如網站性能優化、數據庫優化等。 孰優孰劣,比較應用中自有體現。 5.4.2 性能條款 ¡ Item1:推薦以Dispose模式來代替Finalize方式。 在本章中關於非託管資源的清理,主要有終止化操做和Dispose模式兩種,其中Finalize方式存在執行時間不肯定,運行順序不肯定,同時對垃圾回收的性能有極大的損傷。所以強烈建議以Dispose模式來代替Finalize方式,在帶來性能提高的同時,實現了更加靈活的控制權。 對於兩者的詳細比較,請參見5.3節「垃圾回收」的討論。 ¡ Item2:選擇合適的垃圾收集器:工做站GC和服務期GC。 .NET CLR實現了兩種垃圾收集器,不一樣的垃圾收集器應用不一樣的算法,分別爲不一樣的處理機而設計:工做站GC主要應用於單處理器系統,而服務器收集器專爲多處理器的服務器系統設計,默認狀況爲工做站收集器。所以,在多處理器系統中若是使用工做站收
集器,將大大下降系統的性能,沒法適應高吞吐量的並行操做模式,爲不一樣主機選擇合適的垃圾收集器是有效提升性能的關鍵之一。 ¡ Item3:在適當的狀況下對對象實現弱引用。 爲對象實現弱引用,是有效提升性能的手段之一。弱引用是對象引用的一種「中間態」,實現了對象既能夠經過GC回收其內存,又可被應用程序訪問的機制。這種看似矛盾的解釋,的確對胖對象的內存性能帶來提高,由於胖對象須要大量的內存來建立,弱引用機制保證了胖對象在內存不足時GC能夠回收,而不影響內存使用,在沒有被GC回收前又能夠再次引用該對象,從而達到空間與時間的雙重節約。 在.NET中,WeakReference類用於表示弱引用,經過其Target屬性來表示要追蹤的對象,經過其值賦給變量來建立目標對象的強引用,例如: public void WeakRef() { MyClass mc = new MyClass(); //建立弱引用 WeakReference wr = new WeakReference(mc); //移除強引用 mc = null; if (wr.IsAlive) { //弱引用轉換爲強引用,對象能夠再次使用 mc = wr.Target as MyClass; } else { //對象已經被回收,從新建立 mc = new MyClass(); } }
關於弱引用的相關討論,參見5.3節「垃圾回收」。 ¡ Item4:儘量以using來執行資源清理。 以using語句來執行實現了Dispose模式的對象,是較好的資源清理選擇,簡潔優雅的代碼實現,同時可以保證自動執行Dispose方法來銷燬非託管資源,在本章已作詳細討論,所以值得推薦。 ¡ Item5:推薦使用泛型集合來代替非泛型集合。 泛型實現了一種類型安全的算法重用,其最直接的應用正是在集合類中的性能與安全的良好體現,所以咱們建議以泛型集合來代替非泛型集合,以List<T>和ArrayList爲例來作以說明: public static void Main() { //List<T>性能測試 List<Int32> list = new List<Int32>(); for (Int32 i = 0; i < 10000; i++) //未發生裝箱 list.Add(i); //ArrayList性能測試 ArrayList al = new ArrayList(); for (Int32 j = 0; j < 10000; j++) //發生裝箱 al.Add(j); } 上述示例,僅僅給出了泛型集合和非泛型集合在裝箱操做上引發的差異,一樣的拆箱操做也伴隨了這兩種不一樣集合的取值操做。同時,大量的裝箱操做會帶來頻繁的垃圾回收,類型轉換時的安全檢查,都不一樣程度的影響着性能,而這些弊端在泛型集合中蕩然無存。
必須明確的是,泛型集合並不能徹底代替非泛型集合的應用,.NET框架類庫中有大量的集合類用以完成不一樣的集合操做,例如ArrayList中包含的不少靜態方法是List<T>所沒有的,而這些方法又能爲集合操做帶來許多便利。所以,恰當地作出選擇是很是重要的。 注意,這種性能差異對值類型的影響較大,而引用類型不存在裝箱與拆箱問題,所以性能影響不是很明顯。關於集合和泛型的討論,詳見7.9節「集合通論」和第10章「接觸泛型」中的討論。 ¡ Item6:初始化時最好爲集合對象指定大小。 長度動態增長的集合類,例如ArrayList、Queue的等。能夠無需指定其容量,集合自己可以根據需求自動增長集合大小,爲程序設計帶來方便。然而,過度依賴這種特性並不是好的選擇,由於集合動態增長的過程是一個內存從新分配和集合元素複製的過程,對性能形成必定的影響,因此有必要在集合初始化時指定一個適當的容量。例如: public static void Main() { ArrayList al = new ArrayList(2); al.Add("One"); al.Add("Two"); //容量動態增長一倍 al.Add("Three"); Console.WriteLine(al.Capacity); } ¡ Item7:特定類型的Array性能優於ArrayList。 ArrayList只接受Object類型的元素,向ArrayList添加其餘值類型元素會發生裝箱與拆箱操做,所以在性能上使用Array更具優點,固然object類型的數組除外。不過,ArrayList更容易操做和使用,因此這種選擇一樣存在權衡與比較。 ¡ Item8:字符串駐留機制,是CLR爲String類型實現的特殊設計。 String類型無疑是程序設計中使用最頻繁、應用最普遍的基元類型,所以CLR在設計上爲了提高String類型性能考慮,實現了一種稱爲「字符串駐留」的機制,從而實現了相同字符串可能共享內存空間。同時,字符串駐留是進程級的,垃圾回收不能釋放CLR內部
哈希表維護的字符串對象,只有進程結束時才釋放。這些機制均爲String類型的性能提高和內存優化提供了良好的基礎。 關於String類型及其字符串駐留機制的理解,詳見8.3「如此特殊:大話string」。 ¡ Item9:合理使用System.String和System.Text.StringBuilder。 在簡單的字符串操做中使用String,在複雜的字符串操做中使用StringBuilder。簡單地說,StringBuilder對象的建立代價較大,在字符串鏈接目標較少的狀況下,應優先使用String類型;而在有大量字符串鏈接操做的狀況下,應優先考慮StringBuilder。 同時,StringBuilder在使用上,最好指定合適的容量值,不然因爲默認容量的不足而頻繁進行內存分配的操做會影響系統性能。 關於String和StringBuilder的性能比較,詳見8.3「如此特殊:大話string」的討論。 ¡ Item10:儘可能在子類中重寫ToString方法。 ToString方法是System.Object提供的一個公有的虛方法,.NET中任何類型均可繼承System.Object類型提供的實現方法,默認爲返回類型全路徑名稱。在自定義類或結構中重寫ToString方法,除了能夠有效控制輸出結果,還能在必定程度上減小裝箱操做的發生。 public struct User { public string Name; public Int32 Age; //避免方法調用時的裝箱 public override string ToString() { return "Name: " + Name + ", Age:" + Age.ToString(); } } 關於ToString方法的討論,能夠參考8.1節「萬物歸宗:System.Object」。 ¡ Item11:其餘推薦的字符串操做。 字符串比較,經常習慣的作法是: public bool StringCompare(string str1, string str2)
{ return str1 == str2; } 而較好的實現應該是: public int StringCompare(string str1, string str2) { return String.Compare(str1, str2); } 兩者的差異是:前者調用String.Equals方法操做,然後者調用String. Compare方法來實現。String.Equals方法實質是在內部調用一個EqualsHelper輔助方法來實施比較,內部處理相對複雜。所以,建議使用String.Compare方式進行比較,尤爲是非大小寫敏感字符串的比較,在性能上更加有效。 相似的操做包含字符串判空的操做,推薦的用法以Length屬性來判斷,例如: public bool IsEmpty(string str) { return str.Length == 0; } ¡ Item12:for和foreach的選擇。 推薦選擇foreach來處理可枚舉集合的循環結構,緣由以下: l .NET 2.0之後編譯器對foreach進行了很大程度的改善,在性能上foreach和for實際差異不大。 l foreach語句可以迭代多維數組,可以自動檢測數組的上下限。 l foreach語句可以自動適應不一樣的類型轉換。 l foreach語句代碼更簡潔、優雅,可讀性更強。 public static void Main() { ArrayList al = new ArrayList(3); al.Add(100); al.Add("Hello, world."); al.Add(new char[] { 'A', 'B', 'C' }); foreach (object o in al) Console.WriteLine(o.ToString());
for (Int32 i = 0; i < al.Count; i++) Console.WriteLine(al[i].ToString()); } ¡ Item13:以多線程處理應對系統設計。 毫無疑問,多線程技術是輕鬆應對多任務處理的最強大技術,一方面可以適應用戶的響應,一方面能在後臺完成相應的數據處理,這是典型的多線程應用。在.NET中,基於託管環境的多個線程能夠在一個或多個應用程序域中運行,而應用多個線程來處理不一樣的任務也形成必定的線程同步問題,同時過多的線程有時由於佔用大量的處理器時間而影響性能。 推薦在多線程編程中使用線程池,.NET提供了System.Threading.ThreadPool類來提供對線程池的封裝,一個進程對應一個ThreadPool,能夠被多個AppDomain共享,可以完成異步I/O操做、發送工做項、處理計時器等操做,.NET內部不少異步方法都使用ThreadPool來完成。在此作以簡單的演示: class ThreadHandle { public static void Main() { ThreadHandle th = new ThreadHandle(); //將方法排入線程池隊列執行 ThreadPool.QueueUserWorkItem(new WaitCallback(th.MyProcOne), "線程1"); Thread.Sleep(1000); ThreadPool.QueueUserWorkItem(new WaitCallback(th.MyProcTwo), "線程2"); //實現阻塞主線程 Console.Read(); } //在不一樣的線程執行不一樣的回調操做 public void MyProcOne(object stateInfo) { Console.WriteLine(stateInfo.ToString()); Console.WriteLine("起牀了。"); } public void MyProcTwo(object stateInfo) { Console.WriteLine(stateInfo.ToString()); Console.WriteLine("刷牙了。"); } }
然而,多線程編程將使代碼控制相對複雜化,不當的線程同步可能形成對共享資源的訪問衝突等待,在實際的應用中應該引發足夠的重視。 ¡ Item14:儘量少地拋出異常,禁止將異常處理放在循環內。 異常的發生必然形成系統流程的中斷,同時過多的異常處理也會對性能形成影響,應該儘可能用邏輯流程控制來代替異常處理。對於例行發生的事件,能夠經過編程檢查方式來判斷其狀況,而不是一併交給異常處理,例如: Console.WriteLine(obj == null ? String.Empty : obj.ToString()); 不只簡潔,並且性能表現更好,優於以異常方式的處理: try { Console.WriteLine(obj.ToString()); } catch (NullReferenceException ex) { Console.WriteLine(ex.Message); } 固然,大部分狀況下以異常機制來解決異常信息是值得確定的,可以保證系統安全穩定的面對不可意料的錯誤問題。例如不可預計的溢出操做、索引越界、訪問已關閉資源等操做,則應以異常機制來處理。 關於異常機制及其性能的討論話題,詳見8.6節「直面異常」的分析。 ¡ Item15:捕獲異常時,catch塊中儘可能指定具體的異常篩選器,多個catch塊應該保證異常由特殊到通常的排列順序。 指定具體的異常,能夠節約CLR搜索異常的時間;而CLR是按照自上而下的順序搜索異常,所以將特定程度較高的排在前面,而將特定程度較低的排在後面,不然將致使編譯錯誤。 ¡ Item16:struct和class的性能比較。 基於性能的考慮,在特殊狀況下,以struct來實現對輕量數據的封裝是較好的選擇。這是由於,struct是值類型,數據分配於線程的堆棧上,所以具備較好的性能表現。在本章中,已經對值類型對象和引用類型對象的分配進行了詳細討論,由此能夠看出在線程棧上進行內存分配具備較高的執行效率。
固然,絕大部分狀況下,class都具備不可代替的地位,在面向對象程序世界裏更是如此。關於strcut和class的比較,詳見7.2節「後來居上:class和struct」。 ¡ Item17:以is/as模式進行類型兼容性檢查。 以is和as操做符能夠用於判斷對象類型的兼容性,以is來實現類型判斷,以as實現安全的類型轉換,是值得推薦的方法。這樣可以避免沒必要要的異常拋出,從而實現一種安全、靈活的轉換控制。例如: public static void Main() { MyClass mc = new MyClass(); if (mc is MyClass) { Console.WriteLine("mc is a MyClass object."); } object o = new object(); MyClass mc2 = o as MyClass; if (mc2 != null) { //對轉換類型對象執行操做 } } 詳細的論述,請參見7.5「恩怨情仇:is和as」。 ¡ Item18:const和static readonly的權衡。 const是編譯時常量,readonly是運行時常量,因此const高效,readonly靈活。在實際的應用中,推薦以static readonly來代替const,以解決const可能引發的程序集引用不一致問題,還有帶來的較多靈活性控制。 關於const和readonly的討論,詳細參見7.1節「什麼纔是不變:const和readonly」。 ¡ Item19:儘可能避免不當的裝箱和拆箱,選擇合適的代替方案。 經過本節多個條款的性能討論,咱們不難發現不少狀況下影響性能的正是裝箱和拆箱,例如非泛型集合操做,類型轉換等,所以選擇合適的替代方案是頗有必要的。能夠使用泛型集合來代替非泛型集合,能夠實現多個重載方法以接受不一樣類型的參數來減小裝箱,能夠在子類中重寫ToString方法來避免裝箱等等。 關於裝箱和拆箱的詳細討論,參見4.4節「皆有可能——裝箱與拆箱」的深刻分析。 ¡ Item20:儘可能使用一維零基數組。
CLR對一維零基數組使用了特殊的IL操做指令newarr,在訪問數組時不須要經過索引減去偏移量來完成,並且JIT也只需執行一次範圍檢查,能夠大大提高訪問性能。在各類數組中其性能最好、訪問效率最高,所以值得推薦。 關於一維零基數組的討論,參加3.4節「經典指令解析之實例建立」的分析。 ¡ Item21:以FxCop工具,檢查你的代碼。 FxCop是微軟開發的一個針對.NET託管環境的代碼分析工具,如圖5-11所示,能夠幫助咱們檢查分析現存託管程序在設計、本地化、命名規範、性能和安全性幾個方面是否規範。
圖5-11 FxCop代碼分析工具 尤爲是在性能的檢查方面,FxCop能給咱們不少有益的啓示,最重要的是FxCop簡單易用,並且免費,在改善軟件質量,重構既有代碼時,FxCop是個不錯的選擇工具。 5.4.3 結論 性能條款就是系統開發過程當中的槓桿,在平衡功能與性能之間作出恰當的選擇,本節的21條選擇策略僅從最廣泛意義的選擇角度進行了分析,這些條款應該做爲開發人員軟件設計的參照座標,並應用於實際的代碼編寫中。 通讀全部條款,你可能會發現本節在必定程度上對本書不少內容作了一次梳理,箇中條款以簡單的方式呈現,滲透了大師們對於.NET開發的智慧和經驗,做者有幸做爲一個概括梳理的後輩,從中受益不淺。
第3部分 格局——.NET面面俱到 第6章 深刻淺出——關鍵字的祕密
6.1 把new說透
本文將介紹如下內容:
面向對象基本概念
new關鍵字深刻淺出
對象建立的內存管理
1. 引言
園子裏好像沒有或者不多把new關鍵字拿出來講的,那我就佔個先機吧,呵呵。那麼,咱們到底有必要將一個關鍵字拿出來長篇大論嗎?看來是個問題。回答的關鍵是:你真的理解了new嗎?若是是,那請不要浪費時間,若是不是,那請繼續本文的循序之旅。
下面幾個 問題能夠大概的考察你對new的掌握,開篇以前,但願你們作個檢驗,若是經過了,直接關掉本頁便可。若是沒有經過,但願本文的闡述能幫你找出答案。
new一個class對象和new一個struct或者enum有什麼不一樣?
new在.NET中有幾個用途,除了建立對象實例,還能作什麼?
new運算符,能夠重載嗎?
範型中,new有什麼做用?
new一個繼承下來的方法和override一個繼承方法有何區別?
int i和int i = new int()有什麼不一樣?
2. 基本概念
通常說來,new關鍵字在.NET中用於如下幾個場合,這是MSDN的典型解釋:
做爲運算符, 用於建立對象和調用構造函數。
本文的重點內容,本文在下一節來重點考慮。
做爲修飾符,用於向基類成員隱藏繼承成員。
做爲修飾符,基本的規則能夠總結爲:實現派生類中隱藏方法,則基類方法必須定義爲virtual;new做爲修飾符,實現隱藏基類成員時,不可和override共存,緣由是這二者語義相斥:new用於實現建立一個新成員,同時隱藏基類的同名成員;而override用於實現對基類成員的擴展。
另外,若是在子類中隱藏了基類的數據成員,那麼對基類原數據成員的訪問,能夠經過base修飾符來完成。
例如: new做爲修飾符
做爲約束,用於在泛型聲明中約束可能用做類型參數的參數的類型。
MSDN中的定義是:new 約束指定泛型類聲明中的任何類型參數都必須有公共的無參數構造函數。當泛型類建立類型的新實例時,將此約束應用於類型參數。
注意:new做爲約束和其餘約束共存時,必須在最後指定。
其定義方式爲: class Genericer<T> where T : new() { public T GetItem() { return new T(); } }
實現方式爲:
class MyCls { private string _name; public string Name { get { return _name; } set { _name = value; }
} public MyCls() { _name = "Emma"; } }
class MyGenericTester { public static void Main(string[] args) { Genericer<MyCls> MyGen = new Genericer<MyCls>(); Console.WriteLine(MyGen.GetItem().Name); } }
使用new實現多態。 這不是我熟悉的話題,詳細的內容能夠參見 《多態與 new [C#]》,這裏有較詳細的論述。
3. 深刻淺出
做爲修飾符和約束的狀況,不是很難理解的話題,正如咱們看到本文開篇提出的問題,也大多集中在new做爲運算符的狀況,所以咱們研究的重點就是揭開new做爲運算符的前世此生。
Jeffrey Richter在其著做中,極力推薦讀者使用ILDASM工具查看IL語言細節,從而提升對.NET的深刻探究,在我認爲這真是一條不錯的建議,也給了本身不少提升的空間挖掘。所以,如下是本人的一點建議,我將在後續的系列中,關於學習方法論的討論中深刻探討,這裏只是順便小議,但願有益於你們。 1 不斷的學習代碼; 2 常常看看IL語言的運行細節,對於提供.NET的認識很是有效。
文歸正題,new運算符用於返回一個引用,指向系統分配的託管堆的內存地址。所以,在此咱們以Reflector工具,來了解如下new操做符執行的背後,隱藏着什麼玄機。
首先咱們實現一段最簡單的代碼,而後分析其元數據的實現細節,來探求new在建立對象時到作了什麼?
new做爲運算符
使用Reflector工具反編譯產生的IL代碼以下爲: IL元數據分析
從而能夠得出如下結論:
new一個class時,new完成了如下兩個方面的內容:一是調用newobj命令來爲實例在託管堆中分配內存;二是調用構造函數來實現對象初始化。
new一個struct時,new運算符用於調用其帶構造函數,完成實例的初始化。
new一個int時,new運算符用於初始化其值爲0。
另外必須清楚,值類型和引用類型在分配內存時是不一樣的,值類型分配於線程的堆棧(stack)上,並變量自己就保存其實值,所以也不受GC的控制,;而引用類型變量,包含了指向託管堆的引用,內存分配於託管堆(managed heap)上,內存收集由GC完成。
另外還有如下規則要多加註意:
new運算符不可重載。
new分配內存失敗,將引起OutOfMemoryException異常。
對於基本類型來講,使用new操做符來進行初始化的好處是,某些構造函數能夠完成更優越的初始化操做,而避免了不高明的選擇,例如: string str = new string('*', 100); string str = new string(new char[] {'a', 'b', 'c'});
而不是 string str = "***************************************";
4. 結論
我能說的就這麼多了,至於透了沒透,做者的能量也就這麼多了。但願園子的大牛們常來扔塊磚頭,對我也是一種莫大的促進。可是做爲基本的原理和應用,我想對大部分的需求是知足了。但願這種力求深刻淺出的介紹,能給你分享new關鍵字和其本質的前因後果能有所幫助。
言歸正傳,開篇的幾個題目,不知讀者是否有了各自的答案,咱們不妨暢所欲言,作更深刻的討論,以便揭開其真實的面紗。 參考文獻 (USA)Stanley B.Lippman, C# Primer (USA)David Chappell Understanding .NET
廣而告之 [預告] 另外鑑於前幾個主題的討論中,無論是類型、關鍵字等都涉及到引用類型和值類型的話題,我將於近期發表相關內容的探討,同時還有其餘的關鍵字值得研究,這是本系列近期動向,給本身作個廣告。祝各位愉快。 [聲明] 本文的關鍵字new指的是C#中的關鍵字概念,並不是通常意義上的.NET CRL範疇,之因此將這個主題加入本系列,是基於在.NET體系下開發的咱們,何言能逃得過基本語言的只是要點。因此大可沒必要追究什麼是.NET,什麼是C#的話題,但願你們理清概念,有的放肆。 6.2 base和this 本文將介紹如下內容:
面向對象基本概念
base關鍵字深刻淺出
this關鍵字深刻淺出
1. 引言
new關鍵字引發了你們的很多關注,尤爲感謝Anders Liu的補充,讓我感受博客園賦予的交流平臺真的無所不在。因此,咱們就有必要繼續這個話題,把我認爲最值得關注的關鍵字開展下去,本文的重點是訪問關鍵字(Access Keywords):base和this。雖然訪問關鍵字不是很難理解的話題,咱們仍是有能夠深刻討論的地方來理清思路。仍是老辦法,個人問題先列出來,您是否作好了準備。
是否能夠在靜態方法中使用base和this,爲何?
base經常使用於哪些方面?this經常使用於哪些方面?
能夠base訪問基類的一切成員嗎?
若是有三層或者更多繼承,那麼最下級派生類的base指向那一層呢?例如.NET體系中,若是以base訪問,則應該是直接父類實例呢,仍是最高層類實例呢?
以base和this應用於構造函數時,繼承類對象實例化的執行順序如何?
2. 基本概念
base和this在C#中被歸於訪問關鍵字,顧名思義,就是用於實現繼承機制的訪問操做,來知足對對象成員的訪問,從而爲多態機制提供更加靈活的處理方式。
2.1 base關鍵字
其用於在派生類中實現對基類公有或者受保護成員的訪問,可是隻侷限在構造函數、實例方法和實例屬性訪問器中,MSDN中小結的具體功能包括:
調用基類上已被其餘方法重寫的方法。
指定建立派生類實例時應調用的基類構造函數。
2.2 this關鍵字
其用於引用類的當前實例,也包括繼承而來的方法,一般能夠隱藏this,MSDN中的小結功能主要包括:
限定被類似的名稱隱藏的成員
將對象做爲參數傳遞到其餘方法
聲明索引器
3. 深刻淺出
3.1 示例爲上
下面以一個小示例來綜合的說明,base和this在訪問操做中的應用,從而對其有個概要了解,更詳細的規則和深刻咱們接着闡述。本示例沒有徹底的設計概念,主要用來闡述base和this關鍵字的使用要點和難點闡述,具體的以下: base和this示例
3.2 示例說明
上面的示例基本包括了base和this使用的全部基本功能演示,具體的說明能夠從註釋中獲得解釋,下面的說明是對註釋的進一步闡述和補充,來講明在應用方面的幾個要點:
base經常使用於,在派生類對象初始化時和基類進行通訊。
base能夠訪問基類的公有成員和受保護成員,私有成員是不可訪問的。
this指代類對象自己,用於訪問本類的全部常量、字段、屬性和方法成員,並且無論訪問元素是任何訪問級別。由於,this僅僅侷限於對象內部,對象外部是沒法看到的,這就是this的基本思想。另外,靜態成員不是對象的一部分,所以不能在靜態方法中引用this。
在多層繼承中,base能夠指向的父類的方法有兩種狀況:一是有重載存在的狀況下,base將指向直接繼承的父類成員的方法,例如Audi類中的ShowResult方法中,使用base訪問的將是Car.ShowResult()方法,而不能訪問Vehicle.ShowResult()方法;而是沒有重載存在的狀況下,base能夠指向任何上級父類的公有或者受保護方法,例如Audi類中,能夠使用base訪問基類Vehicle.Run()方法。這些咱們能夠使用ILDasm.exe,從IL代碼中獲得答案。 .method public hidebysig virtual instance void ShowResult() cil managed { // 代碼大小 27 (0x1b) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 //base調用父類成員 IL_0002: call instance void Anytao.net.My_Must_net.Car::ShowResult() IL_0007: nop
IL_0008: ldarg.0 //base調用父類成員,由於沒有實現Car.Run(),因此指向更高級父類 IL_0009: call instance void Anytao.net.My_Must_net.Vehicle::Run() IL_000e: nop IL_000f: ldstr "It's audi's result." IL_0014: call void [mscorlib]System.Console::WriteLine(string) IL_0019: nop IL_001a: ret } // end of method Audi::ShowResult
3.3 深刻剖析
若是有三次或者更多繼承,那麼最下級派生類的base指向那一層呢?例如.NET體系中,若是以base訪問,則應該是直接父類實例呢,仍是最高層類實例呢?
首先咱們有必要了解類建立過程當中的實例化順序,才能進一步瞭解base機制的詳細執行過程。通常來講,實例化過程首先要先實例化其基類,而且依此類推,一直到實例化System.Object爲止。所以,類實例化,老是從調用System.Object.Object()開始。所以示例中的類Audi的實例化過程大概能夠小結爲如下順序執行,詳細能夠參考示例代碼分析。
執行System.Object.Object();
執行Vehicle.Vehicle(string name, int speed);
執行Car.Car();
執行Car.Car(string name, int speed);
執行Audi.Audi();
執行Audi.Audi(string name, int speed)。
咱們在充分了解其實例化順序的基礎上就能夠順利的把握base和this在做用於構造函數時的執行狀況,並進一步瞭解其基本功能細節。
下面更重要的分析則是,以ILDASM.exe工具爲基礎來分析IL反編譯代碼,以便更深層次的瞭解執行在base和this背後的應用實質,只有這樣咱們才能說對技術有了基本的剖析。
Main方法的執行狀況爲:
IL分析base和this執行 所以,對重寫父類方法,最終指向了最高級父類的方法成員。 4. 通用規則
儘可能少用或者不用base和this。除了決議子類的名稱衝突和在一個構造函數中調用其餘的構造函數以外,base和this的使用容易引發沒必要要的結果。
在靜態成員中使用base和this都是不容許的。緣由是,base和this訪問的都是類的實例,也就是對象,而靜態成員只能由類來訪問,不能由對象來訪問。
base是爲了實現多態而設計的。
使用this或base關鍵字只能指定一個構造函數,也就是說不可同時將this和base做用在一個構造函數上。
簡單的來講,base用於在派生類中訪問重寫的基類成員;而this用於訪問本類的成員,固然也包括繼承而來公有和保護成員。
除了base,訪問基類成員的另一種方式是:顯示的類型轉換來實現。只是該方法不能爲靜態方法。
5. 結論 base和this關鍵字,不是特別難於理解的內容,本文之因此將其做爲系列的主題,除了對其應用規則作以小結以外,更重要的是在關注其執行細節的基礎上,對語言背景創建更清晰的把握和分析,這些纔是學習和技術應用的根本所在,也是.NET技術框架中本質訴求。對學習者來講,只有從本質上來把握概念,才能在變化非凡的應用中,一眼找到答案。 言歸正傳,開篇的幾個題目,不知讀者是否有了各自的答案,咱們不妨暢所欲言,作更深刻的討論,以便揭開其真實的面紗。
參考文獻
(USA)Stanley B.Lippman, C# Primer
(USA)David Chappell, Understanding .NET
(Cnblog)Bear-Study-Hard,C#學習筆記(二):構造函數的執行序列
廣而告之
[預告]
另外鑑於前幾個主題的討論中,無論是類型、關鍵字等都涉及到引用類型和值類型的話題,我將於近期發表相關內容的探討,主要包括3個方面的內容,這是本系列近期動向,給本身作個廣告。祝各位愉快。
[聲明]
本文的關鍵字指的是C#中的關鍵字概念,並不是通常意義上的.NET CRL範疇,之因此將這個主題加入本系列,是基於在.NET體系下開發的咱們,何言能逃得過基本語言的只是要點。因此大可沒必要追究什麼是.NET,什麼是C#的話題,但願你們理清概念,有的放肆。
6.3深刻淺出關鍵字---using全接觸
本文將介紹如下內容:
using指令的多種用法
using語句在Dispose模式中的應用
1. 引言
在.NET你們庭中,有很多的關鍵字承擔了多種角色,例如new關鍵字就身兼數職,除了可以建立對象,在繼承體系中隱藏基類成員,還在泛型聲明中約束可能用做類型參數的參數,在[第五回:深刻淺出關鍵字---把new說透]咱們對此都有詳細的論述。本文,將把目光轉移到另一個身兼數職的明星關鍵字,這就是using關鍵字,在詳細討論using的多重身份的基礎上來了解.NET在語言機制上的簡便與深邃。
那麼,using的多重身份都體如今哪些方面呢,咱們先一睹爲快吧:
引入命名空間
建立別名
強制資源清理
下面,本文將從這幾個角度來闡述using的多彩應用。
2. 引入命名空間
using做爲引入命名空間指令的用法規則爲: using Namespace;
在.NET程序中,最多見的代碼莫過於在程序文件的開頭引入System命名空間,其緣由在於System命名空間中封裝了不少最基本最經常使用的操做,下面的代碼對咱們來講最爲熟悉不過: using System;
這樣,咱們在程序中就能夠直接使用命名空間中的類型,而沒必要指定詳細的類型名稱。using指令能夠訪問嵌套命名空間。
關於:命名空間
命名空間是.NET程序在邏輯上的組織結構,而並不是實際的物理結構,是一種避免類名衝突的方法,用於將不一樣的數據類型組合劃分的方式。例如,在.NET中不少的基本類型都位於System命名空間,數據操做類型位於System.Data命名空間,
誤區:
using相似於Java語言的import指令,都是引入命名空間(Java中稱做包)這種邏輯結構;而不一樣於C語言中的#include指令,用於引入實際的類庫,
using引入命名空間,並不等於編譯器編譯時加載該命名空間所在的程序集,程序集的加載決定於程序中對該程序集是否存在調用操做,若是代碼中不存在任何調用
操做則編譯器將不會加載using引入命名空間所在程序集。所以,在源文件開頭,引入多個命名空間,並不是加載多個程序集,不會形成「過分引用」的弊端。
3. 建立別名
using爲命名空間建立別名的用法規則爲: using alias = namespace | type;
其中namespace表示建立命名空間的別名;而type表示建立類型別名。例如,在.NET Office應用中,經常會引入Microsoft.Office.Interop.Word.dll程序集,在引入命名空間時爲了不繁瑣的類型輸入,咱們一般爲其建立別名以下: using MSWord = Microsoft.Office.Interop.Word;
這樣,就能夠在程序中以MSWord來代替Microsoft.Office.Interop.Word前綴,若是要建立Application對象,則能夠是這樣, private static MSWord.Application ooo = new MSWord.Application();
一樣,也能夠建立類型的別名,用法爲: using MyConsole = System.Console; class UsingEx { public static void Main() { MyConsole.WriteLine("應用了類的別名。"); } }
而建立別名的另外一個重要的緣由在於同一cs文件中引入的不一樣命名空間中包括了相同名稱的類型,爲了不出現名稱衝突能夠經過設定別名來解決,例如: namespace Boyspace { public class Player { public static void Play() { System.Console.WriteLine("Boys play football."); } } } namespace Girlspace { public class Player
{ public static void Play() { System.Console.WriteLine("Girls play violin."); } } }
以using建立別名,有效的解決了這種可能的命名衝突,儘管咱們能夠經過類型全名稱來加以區分,可是這顯然不是最佳的解決方案,using使得這一問題迎刃而解,不費絲毫功夫,同時在編碼規範上看來也更加的符合編碼要求。
4. 強制資源清理
4.1 由來
要理解清楚使用using語句強制清理資源,就首先從瞭解Dispose模式提及,而要了解Dispose模式,則應首先了解.NET的垃圾回收機制。這些顯然不是本文所能完成的宏論,咱們只須要首先明確的是.NET提供了Dispose模式來實現顯式釋放和關閉對象的能力。
Dispose模式
Dispose模式是.NET提供的一種顯式清理對象資源的約定方式,用於在.NET 中釋放對象封裝的非託管資源。由於非託管資源不受GC控制,對象必須調用本身的Dispose()方法來釋放,這就是所謂的Dispose模式。從概念角度來看,Dispose模式就是一種強制資源清理所要遵照的約定;從實現角度來看,Dispose模式就是讓要一個類型實現IDisposable接口,從而使得該類型提供一個公有的Dispose方法。
本文再也不討論如何讓一個類型實現Dispose模式來提供顯示清理非託管資源的方式,而將注意集中在如何以using語句來簡便的應用這種實現了Dispose模式的類型的資源清理方式。咱們在內存管理與垃圾回收章節將有詳細的討論。
using語句提供了強制清理對象資源的便捷操做方式,容許指定什麼時候釋放對象的資源,其典型應用爲: using (Font f = new Font("Verdana", 12, FontStyle.Regular)) { //執行文本繪製操做 Graphics g = e.Graphics; Rectangle rect = new Rectangle(10, 10, 200, 200); g.DrawString("Try finally dispose font.", f, Brushes.Black, rect); }//運行結束,釋放f對象資源
在上述典型應用中,using語句在結束時會自動調用欲被清除對象的Dispose()方法。所以,該Font對象必須實現IDispose接口,才能使用using語句強制對象清理資源。咱們查看其類型定義可知: public sealed class Font : MarshalByRefObject, ICloneable, ISerializable, IDisposable
Font類型的確實現了IDisposeable接口,也就具備了顯示回收資源的能力。然而,咱們並未從上述代碼中,看出任何使用Dispose方法的蛛絲馬跡,這正式using語句帶來的簡便之處,其實質究竟怎樣呢?
4.2 實質
要想了解using語句的執行本質,瞭解編譯器在背後作了哪些手腳,就必須迴歸到IL代碼中來揭密才行:
.method public hidebysig static void Main() cil managed { .entrypoint // 代碼大小 40 (0x28) .maxstack 4 .locals init ([0] class [System.Drawing]System.Drawing.Font f, [1] bool CS$4$0000) IL_0000: nop IL_0001: ldstr "Verdana" IL_0006: ldc.r4 12. IL_000b: ldc.i4.0 IL_000c: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string,float32, valuetype [System.Drawing]System.Drawing.FontStyle) IL_0011: stloc.0 .try { ……部分省略…… } // end .try finally { ……部分省略…… IL_001f: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0024: nop IL_0025: endfinally } // end handler IL_0026: nop IL_0027: ret } // end of method UsingDispose::Main
顯然,編譯器在自動將using生成爲try-finally語句,並在finally塊中調用對象的Dispose方法,來清理資源。
在.NET規範中,微軟建議開放人員在調用一個類型的Dispose()或者Close()方法時,將其放在異常處理的finally塊中。根據上面的分析咱們可知,using語句正是隱式的調用了類型的Dispose方法,所以如下的代碼和上面的示例是徹底等效的: Font f2 = new Font("Arial", 10, FontStyle.Bold); try { //執行文本繪製操做 Graphics g = new Graphics(); Rectangle rect = new Rectangle(10, 10, 200, 200); g.DrawString("Try finally dispose font.", f2, Brushes.Black, rect); } finally { if (f2 != null)
((IDisposable)f2).Dispose(); }
4.3 規則
using只能用於實現了IDisposable接口的類型,禁止爲不支持IDisposable接口的類型使用using語句,不然會出現編譯時錯誤;
using語句適用於清理單個非託管資源的狀況,而多個非託管對象的清理最好以try-finnaly來實現,由於嵌套的using語句可能存在隱藏的Bug。內層using塊引起異常時,將不能釋放外層using塊的對象資源。
using語句支持初始化多個變量,但前提是這些變量的類型必須相同,例如: using(Pen p1 = new Pen(Brushes.Black), p2 = new Pen(Brushes.Blue)) { // }
不然,編譯將不可經過。不過,仍是有變通的辦法來解決這一問題,緣由就是應用using語句的類型必然實現了IDisposable接口,那麼就能夠如下面的方式來完成初始化操做, using (IDisposable font = new Font("Verdana", 12, FontStyle.Regular), pen = new Pen(Brushes.Black)) { float size = (font as Font).Size; Brush brush = (pen as Pen).Brush; }
另外一種辦法就是以使用try-finally來完成,無論初始化的對象類型是否一致。
Dispose方法用於清理對象封裝的非託管資源,而不是釋放對象的內存,對象的內存依然由垃圾回收器控制。
程序在達到using語句末尾時退出using塊,而若是到達語句末尾以前引入異常則有可能提早退出。
using中初始化的對象,能夠在using語句以前聲明,例如: Font f3 = new Font("Verdana", 9, FontStyle.Regular); using (f3) { //執行文本繪製操做 }
5. 結論
一個簡單的關鍵字,多種不一樣的應用場合。本文從比較全面的角度,詮釋了using關鍵字在.NET中的多種用法,值得指出的是這種用法並不是實現於.NET的全部高級語言,本文的狀況主要侷限在C#中。
第7章 巔峯對決——走出誤區
7.2 後來居上:class和struct 本文將介紹如下內容:
• 面向對象基本概念
• 類和結構體簡介
• 引用類型和值類型區別
1. 引言
提起class和struct,咱們首先的感受是語法幾乎相同,待遇卻翻天覆地。歷史將接力棒由面向過程編程傳到面向對象編程,class和struct也揹負着各自的命運前行。在我認爲,struct英雄遲暮,class天下獨行,最本質的區別是class是引用類型,而struct是值類型,它們在內存中的分配狀況有所區別。由此產生的一系列差別性,本文將作以全面討論。
2. 基本概念
2.1. 什麼是class?
class(類)是面向對象編程的基本概念,是一種自定義數據結構類型,一般包含字段、屬性、方法、屬性、構造函數、索引器、操做符等。由於是基本的概念,因此沒必要在此詳細描述,讀者能夠查詢相關概念瞭解。咱們重點強調的是.NET中,全部的類都最終繼承自System.Object類,所以是一種引用類型,也就是說,new一個類的實例時,對象保存了該實例實際數據的引用地址,而對象的值保存在託管堆(managed heap)中。
2.2. 什麼是struct?
struct(結構)是一種值類型,用於將一組相關的信息變量組織爲一個單一的變量實體 。全部的結構都繼承自System.ValueType類,所以是一種值類型,也就是說,struct實例分配在線程的堆棧(stack)上,它自己存儲了值,而不包含指向該值的指針。因此在使用struct時,咱們能夠將其看成int、char這樣的基本類型類對待。
3. 相同點,不一樣點
相同點:語法相似。
不一樣點:
class是引用類型,繼承自System.Object類;struct是值類型,繼承自System.ValueType類,所以不具多態性。可是注意,System.ValueType是個引用類型。
從職能觀點來看,class表現爲行爲;而struct經常使用於存儲數據。
class支持繼承,能夠繼承自類和接口;而struct沒有繼承性,struct不能從class繼承,也不能做爲class的基類,但struct支持接口繼承(記得嗎,《第二回:對抽象編程:接口和抽象類》也作過討論)
class能夠聲明無參構造函數,能夠聲明析構函數;而struct只能聲明帶參數構造函數,且不能聲明析構函數。所以,struct沒有自定義的默認無參構造函數,默認無參構造器只是簡單地把全部值初始化爲它們的0等價值
實例化時,class要使用new關鍵字;而struct能夠不使用new關鍵字,若是不以new來實例化struct,則其全部的字段將處於未分配狀態,直到全部字段完成初始化,不然引用未賦值的字段會致使編譯錯誤。
class能夠實抽象類(abstract),能夠聲明抽象函數;而struct爲抽象,也不能聲明抽象函數。
class能夠聲明protected成員、virtual成員、sealed成員和override成員;而struct不能夠,可是值得注意的是,struct能夠重載System.Object的3個虛方法,Equals()、ToString()和GetHashTable()。
class的對象複製分爲淺拷貝和深拷貝(該主題咱們在本系列之後的主題中將重點講述,本文不做詳述),必須通過特別的方法來完成複製;而struct建立的對象複製簡單,能夠直接以等號鏈接便可。
class實例由垃圾回收機制來保證內存的回收處理;而struct變量使用完後當即自動解除內存分配。
做爲參數傳遞時,class變量是以按址方式傳遞;而struct變量是以按值方式傳遞的。
咱們能夠簡單的理解,class是一個能夠動的機器,有行爲,有多態,有繼承;而struct就是個零件箱,組合了不一樣結構的零件。其實,class和struct最本質的區別就在於class是引用類型,內存分配於託管堆;而struct是值類型,內存分配於線程的堆棧上。由此差別,致使了上述全部的不一樣點,因此只有深入的理解內存分配的相關內容,才能更好的駕馭。本系列將再之後的內容中,將引用類型和值類型作以深刻的比較和探討,敬請關注。固然正如本文標題描述的同樣,使用class基本能夠替代struct的任何場合,class後來居上。雖然在某些方面struct有性能方面的優點,可是在面向對象編程裏,基本是class橫行的天下。
那麼,有人難免會提出,既然class幾乎能夠徹底替代struct來實現全部的功能,那麼struct還有存在的必要嗎?答案是,至少在如下狀況下,鑑於性能上的考慮,咱們應該考慮使用struct來代替class:
實現一個主要用於存儲數據的結構時,能夠考慮struct。
struct變量佔有堆棧的空間,所以只適用於數據量相對小的場合。
結構數組具備更高的效率。
提供某些和非託管代碼通訊的兼容性。
全部這些是struct有一席之地的理由,固然也許還有其餘的更多說法,只是我不知道罷了:-)
4. 經典示例
4.1 小菜一碟
下面以示例爲說明,來闡述本文的基本規則,詳細見註釋內容。 (1)定義接口 interface IPerson { void GetSex(); }
(2)定義類 public class Person { public Person() { } public Person(string name, int age) { _name = name; _age = age; }
private string _name; public string Name { get { return _name; } set { _name = value; } } private int _age; public int Age { get { return _age; } set { _age = value; } } }
(3)定義結構 //能夠繼承自接口,不可繼承類或結構 struct Family: IPerson { public string name; public int age; public bool sex; public string country; public Person person; //不能夠包含顯式的無參構造函數和析構函數 public Family(string name, int age, bool sex, string country, Person person) { this.name = name; this.age = age; this.sex = sex; this.country = country; this.person = person; }
//不能夠實現protected、virtual、sealed和override成員 public void GetSex() { if (sex) Console.WriteLine(person.Name + " is a boy."); else Console.WriteLine(person.Name + " is a girl."); } public void ShowPerson() { Console.WriteLine("This is {0} from {1}", new Person(name, 22).Name, country); } //能夠重載ToString虛方法 public override string ToString() { return String.Format("{0} is {1}, {2} from {3}", person.Name, age, sex ? "Boy" : "Girl", country); } }
(4)測試結構和類
猜猜運行結果如何,能夠順便檢查檢查對這個概念的認識。
4.2 .NET研究
在.NET 框架中,System.Drawing命名空間中的有些元素,如System.Drawing.Point就是實現爲struct,而不是class。其緣由也正在於以上介紹的各方面的權衡,你們能夠就此研究研究,能夠體會更多。另外,還有以struct實現的System.Guid。
5. 結論
對基本概念的把握,是咱們進行技術深刻探索的必經之路,本系列的主旨也是可以從基本框架中,提供給你們一個通向高級技術的必修課程。本文關於class和struct的討論就是如此,在.NET框架中,關於class和struct的討論將涉及到對引用類型和值類型的認識,而且進一步將觸角伸向變量內存分配這一高級主題,因此咱們有必要來了解其運行機制,把握區別和應用場合,以便在日常的系統設計中把握好對這一律念層次的把握。
另外,請你們就如下問題進行討論,但願可以更加清晰本文的拓展:
struct還主要應用在哪些方面?
C++和C#中,關於struct的應用又有所不一樣,這些不一樣又有哪些區別?
7.3 歷史糾葛:特性和屬性
本文將介紹如下內容:
• 定製特性的基本概念和用法
• 屬性與特性的區別比較
• 反射的簡單介紹
1. 引言
attribute是.NET框架引入的有一技術亮點,所以咱們有必要花點時間來了解本文的內容,走進一個發現attribute登堂入室的入口。由於.NET Framework中使用了大量的定製特性來完成代碼約定,[Serializable]、[Flags]、[DllImport]、[AttributeUsage]這些的構造,相信咱們都見過吧,那麼你是否瞭解其背後的技術。
提起特性,因爲高級語言發展的歷史緣由,難免讓人想起另外一個耳熟能詳的名字:屬性。特性和屬性,每每給初學者或者從C++轉移到C#的人混淆的概念衝擊。那麼,什麼是屬性,什麼是特性,兩者的概念和區別,用法與示例,將在本文作以歸納性的總結和比較,但願給你的理解帶來收穫。另外本文的主題以特性的介紹爲主,屬性的論述重點突出在兩者的比較上,關於屬性的更多論述將在另外一篇主題中詳細討論,敬請關注。
2. 概念引入
2.1. 什麼是特性?
MADN的定義爲:公共語言運行時容許添加相似關鍵字的描述聲明,叫作attributes, 它對程序中的元素進行標註,如類型、字段、方法和屬性等。Attributes和Microsoft .NET Framework文件的元數據保存在一塊兒,能夠用來向運行時描述你的代碼,或者在程序運行的時候影響應用程序的行爲。
咱們簡單的總結爲:定製特性attribute,本質上是一個類,其爲目標元素提供關聯附加信息,並在運行期以反射的方式來獲取附加信息。具體的特性實現方法,在接下來的討論中繼續深刻。
2.2. 什麼是屬性?
屬性是面向對象編程的基本概念,提供了對私有字段的訪問封裝,在C#中以get和set訪問器方法實現對可讀可寫屬性的操做,提供了安全和靈活的數據訪問封裝。關於屬性的概念,不是本文的重點,並且相信大部分的技術人員應該對屬性有清晰的概念。如下是簡單的屬性示例:
public class MyProperty { //定義字段 private string _name; private int _age; //定義屬性,實現對_name字段的封裝 public string Name { get { return (_name == null) ? string.Empty : _name; } set { _name = value; } } //定義屬性,實現對_age字段的封裝 //加入對字段的範圍控制 public int Age { get { return _age; } set { if ((value > 0) && (value < 150)) {
_age = value; } else { throw new Exception("Not a real age"); } } } } public class MyTest { public static void Main(string[] args) { MyProperty myProperty = new MyProperty(); //觸發set訪問器 myProperty.Name = "Anytao"; //觸發get訪問器 Console.WriteLine(myProperty.Name); myProperty.Age = 66; Console.WriteLine(myProperty.Age.ToString()); Console.ReadLine(); } }
2.3. 區別與比較
經過對概念的澄清和歷史的回溯,咱們知道特性和屬性只是在名稱上有過糾葛,在MSDN上關於attribute的中文解釋甚至仍是屬性,可是我贊成更一般的稱呼:特性。在功能上和應用上,兩者其實沒有太多模糊的概念交叉,所以也沒有必要來比較其應用的異同點。本文則以特性的概念爲重點,來討論其應用的場合和規則。
我理解的定製特性,就是爲目標元素,能夠是數據集、模塊、類、屬性、方法、甚至函數參數等加入附加信息,相似於註釋,可是能夠在運行期以反射的方式得到。定製特性主要應用在序列化、編譯器指令、設計模式等方面。
3. 通用規則
定製特性能夠應用的目標元素能夠爲:程序集(assembly)、模塊(module)、類型(type)、屬性(property)、事件(event)、字段(field)、方法(method)、參數(param)、返回值(return),應該全了。
定製特性以[,]形式展示,放在緊挨着的元素上,多個特性能夠應用於同一元素,特性間以逗號隔開,如下表達規則有效:[AttributeUsage][ Flags]、[AttributeUsage, Flags]、[Flags, AttibuteUsageAttribute]、[AttributeUsage(), FlagesAttribute()]
attibute實例,是在編譯期進行初始化,而不是運行期。
C#容許以指定的前綴來表示特性所應用的目標元素,建議這樣來處理,由於顯式處理能夠消除可能帶來的二義性。例如: using System; namespace Anytao.net { [assembly: MyAttribute(1)] //應用於程序集 [moduel: MyAttribute(2)] //應用於模塊 pubic class Attribute_how2do { // } }
定製特性類型,必須直接或者間接的繼承自System.Attribute類,並且該類型必須有公有構造函數來建立其實例。
全部自定義的特性名稱都應該有個Attribute後綴,這是習慣性約定。
定製特性也能夠應用在其餘定製特性上,這點也很好理解,由於定製特性自己也是一個類,遵照類的公有規則。例如不少時候咱們的自定義定製特性會應用AttributeUsageAttribute特性,來控制如何應用新定義的特性。
[AttributeUsageAttribute(AttributeTarget.All), AllowMultiple = true, Inherited = true] class MyNewAttribute: System.Attribute { // }
定製特性不會影響應用元素的任何功能,只是約定了該元素具備的特質。
全部非抽象特性必須具備public訪問限制。
特性經常使用於編譯器指令,突破#define, #undefine, #if, #endif的限制,並且更加靈活。
定製特性經常使用於在運行期得到代碼註釋信息,以附加信息來優化調試。
定製特性能夠應用在某些設計模式中,如工廠模式。
定製特性還經常使用於位標記,非託管函數標記、方法廢棄標記等其餘方面。
4. 特性的應用
4.1. 經常使用特性
經常使用特性,也就是.NET已經提供的固有特性,事實上在.NET框架中已經提供了豐富的固有特性由咱們發揮,如下精選出我認爲最經常使用、最典型的固有特性作以簡單討論,固然這只是個人一家之言,亦不足道。我想了解特性,仍是從這裏作爲起點,從.NET提供的經典開始,或許是一種求知的捷徑,但願能給你們以啓示。
AttributeUsage
AttributeUsage特性用於控制如何應用自定義特性到目標元素。關於AttributeTargets、AllowMultiple、Inherited、ValidOn,請參閱示例說明和其餘文檔。咱們已經作了至關的介紹和示例說明,咱們仍是在實踐中本身體會更多吧。
Flags
以Flags特性來將枚舉數值看做位標記,而非單獨的數值,例如:
enum Animal { Dog = 0x0001, Cat = 0x0002, Duck = 0x0004, Chicken = 0x0008 }
所以,如下實現就至關輕鬆, Animal animals = Animal.Dog | Animal.Cat; Console.WriteLine(animals.ToString());
請猜想結果是什麼,答案是:"Dog, Cat"。若是沒有Flags特別,這裏的結果將是"3"。關於位標記,也將在本系列的後續章回中有所交代,在此只作以探討止步。
DllImport
DllImport特性,可讓咱們調用非託管代碼,因此咱們能夠使用DllImport特性引入對Win32 API函數的調用,對於習慣了非託管代碼的程序員來講,這一特性無疑是救命的稻草。 using System; using System.Runtime.InteropServices; namespace Anytao.net { class MainClass { [DllImport("User32.dll")] public static extern int MessageBox(int hParent, string msg, string caption, int type); static int Main() { return MessageBox(0, "How to use attribute in .NET", "Anytao_net", 0); } } }
Serializable
Serializable特性代表了應用的元素能夠被序列化(serializated),序列化和反序列化是另外一個能夠深刻討論的話題,在此咱們只是提出概念,深刻的研究有待以專門的主題來呈現,限於篇幅,此不贅述。
Conditional
Conditional特性,用於條件編譯,在調試時使用。注意:Conditional不可應用於數據成員和屬性。
還有其餘的重要特性,包括:Description、DefaultValue、Category、ReadOnly、BrowerAble等,有時間能夠深刻研究。
4.2. 自定義特性
既然attribute,本質上就是一個類,那麼咱們就能夠自定義更特定的attribute來知足個性化要求,只要遵照上述的12條規則,實現一個自定義特性實際上是很容易的,典型的實現方法爲:
定義特性
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true)] public class TestAttribute : System.Attribute { public TestAttribute(string message) { Console.WriteLine(message); } public void RunTest() { Console.WriteLine("TestAttribute here."); } }
應用目標元素 [Test("Error Here.")] public void CannotRun() { // }
獲取元素附加信息
若是沒有什麼機制來在運行期來獲取Attribute的附加信息,那麼attribute就沒有什麼存在的意義。所以,.NET中以反射機制來實如今運行期獲取attribute信息,實現方法以下:
public static void Main() { Tester t = new Tester(); t.CannotRun(); Type tp = typeof(Tester); MethodInfo mInfo = tp.GetMethod("CannotRun"); TestAttribute myAtt = (TestAttribute)Attribute.GetCustomAttribute(mInfo, typeof(TestAttribute)); myAtt.RunTest(); }
5. 經典示例
5.1 小菜一碟
啥也不說了,看註釋吧。
using System; using System.Reflection; //應用反射技術得到特性信息 namespace Anytao.net { //定製特性也能夠應用在其餘定製特性上, //應用AttributeUsage,來控制如何應用新定義的特性 [AttributeUsageAttribute(AttributeTargets.All, //可應用任何元素 AllowMultiple = true, //容許應用屢次 Inherited = false)] //不繼承到派生類 //特性也是一個類, //必須繼承自System.Attribute類, //命名規範爲:"類名"+Attribute。 public class MyselfAttribute : System.Attribute { //定義字段 private string _name; private int _age; private string _memo; //必須定義其構造函數,若是不定義有編譯器提供無參默認構造函數 public MyselfAttribute() { } public MyselfAttribute(string name, int age) { _name = name; _age = age; } //定義屬性 //顯然特性和屬性不是一回事兒 public string Name { get { return _name == null ? string.Empty : _name; } }
public int Age { get { return _age; } } public string Memo { get { return _memo; } set { _memo = value; } } //定義方法 public void ShowName() { Console.WriteLine("Hello, {0}", _name == null ? "world." : _name); } } //應用自定義特性 //能夠以Myself或者MyselfAttribute做爲特性名 //能夠給屬性Memo賦值 [Myself("Emma", 25, Memo = "Emma is my good girl.")] public class Mytest { public void SayHello() { Console.WriteLine("Hello, my.net world."); } } public class Myrun { public static void Main(string[] args) { //如何以反射肯定特性信息
Type tp = typeof(Mytest); MemberInfo info = tp; MyselfAttribute myAttribute = (MyselfAttribute)Attribute.GetCustomAttribute(info, typeof(MyselfAttribute)); if (myAttribute != null) { //嘿嘿,在運行時查看註釋內容,是否是很爽 Console.WriteLine("Name: {0}", myAttribute.Name); Console.WriteLine("Age: {0}", myAttribute.Age); Console.WriteLine("Memo of {0} is {1}", myAttribute.Name, myAttribute.Memo); myAttribute.ShowName(); } //多點反射 object obj = Activator.CreateInstance(typeof(Mytest)); MethodInfo mi = tp.GetMethod("SayHello"); mi.Invoke(obj, null); Console.ReadLine(); } } }
啥也別想了,本身作一下試試。
5.2 他山之石
MSDN認爲,特性 (Attribute) 描述如何將數據序列化,指定用於強制安全性的特性,並限制實時 (JIT) 編譯器的優化,從而使代碼易於調試。屬性 (Attribute) 還能夠記錄文件名或代碼做者,或在窗體開發階段控制控件和成員的可見性。
dudu Boss收藏的系列文章《Attribute在.net編程中的應用》,給你應用方面的啓示會不少,值得研究。
亞歷山大同志 的系列文章《手把手教你寫ORM(六)》中,也有很好的詮釋。
idior的文章《Remoting基本原理及其擴展機制》也有收穫,所以補充。
6. 結論
Attribute是.NET引入的一大特點技術,但在博客園中討論的不是不少,因此拿出本身的體會來分享,但願就這一技術要點進行一番登堂入室的引導。更深層次的應用,例如序列化、程序安全性、設計模式多方面均可以挖掘出閃耀的金子,這就是.NET在技術領域帶來的百變魅力吧。但願你們暢所欲言,來完善和補充做者在這方面的不全面和認知上的不深刻,那將是做者最大的鼓勵和動力。
7.4:對抽象編程:接口和抽象類
本文將介紹如下內容:
• 面向對象思想:多態
• 接口
• 抽象類
1. 引言
在我以前的一篇post《抽象類和接口的誰是誰非》中,和同事管偉的討論,獲得不少朋友的關注,由於是不成體系的論道,因此給你們瞭解形成不便,同時關於這個主題的系統性理論,我認爲也有必要作以總結,所以纔有了本篇的新鮮出爐。同時,我將把上貼中的問題順便也在此作以交代。
2. 概念引入
什麼是接口?
接口是包含一組虛方法的抽象類型,其中每一種方法都有其名稱、參數和返回值。接口方法不能包含任何實現,CLR容許接口能夠包含事件、屬性、索引器、靜態方法、靜態字段、靜態構造函數以及常數。可是注意:C#中不能包含任何靜態成員。一個類能夠實現多個接口,當一個類繼承某個接口時,它不只要實現該接口定義的全部方法,還要實現該接口從其餘接口中繼承的全部方法。
定義方法爲: public interface System.IComparable { int CompareTo(object o); }
public class TestCls: IComparable { public TestCls() { } private int _value; public int Value { get { return _value; } set { _value = value; } } public int CompareTo(object o) { //使用as模式進行轉型判斷 TestCls aCls = o as TestCls; if (aCls != null) { //實現抽象方法 return _value.CompareTo(aCls._value); } } }
什麼是抽象類?
抽象類提供多個派生類共享基類的公共定義,它既能夠提供抽象方法,也能夠提供非抽象方法。抽象類不能實例化,必須經過繼承由派生類實現其抽象方法,所以對抽象類不能使用new關鍵字,也不能被密封。若是派生類沒有實現全部的抽象方法,則該派生類也必須聲明爲抽象類。另外,實現抽象方法由overriding方法來實現。
定義方法爲:
/// <summary> /// 定義抽象類 /// </summary> abstract public class Animal { //定義靜態字段 protected int _id; //定義屬性 public abstract int Id { get; set; } //定義方法 public abstract void Eat(); //定義索引器 public string this[int i] { get; set; } } /// <summary> /// 實現抽象類 /// </summary> public class Dog: Animal { public override int Id { get {return _id;} set {_id = value;}
} public override void Eat() { Console.Write("Dog Eats.") } }
3. 相同點和不一樣點
3.1 相同點
都不能被直接實例化,均可以經過繼承實現其抽象方法。
都是面向抽象編程的技術基礎,實現了諸多的設計模式。
3.2 不一樣點
接口支持多繼承;抽象類不能實現多繼承。
接口只能定義抽象規則;抽象類既能夠定義規則,還可能提供已實現的成員。
接口是一組行爲規範;抽象類是一個不徹底的類,着重族的概念。
接口能夠用於支持回調;抽象類不能實現回調,由於繼承不支持。
接口只包含方法、屬性、索引器、事件的簽名,但不能定義字段和包含實現的方法;抽象類能夠定義字段、屬性、包含有實現的方法。
接口能夠做用於值類型和引用類型;抽象類只能做用於引用類型。例如,Struct就能夠繼承接口,而不能繼承類。
經過相同與不一樣的比較,咱們只能說接口和抽象類,各有所長,但無優略。在實際的編程實踐中,咱們要視具體狀況來酌情量才,可是如下的經驗和積累,或許能給你們一些啓示,除了個人一些積累以外,不少都來源於經典,我相信經得起考驗。因此在規則與場合中,咱們學習這些經典,最重要的是學以至用,固然我將以一家之言博你們之笑,看官請繼續。
3.3 規則與場合
請記住,面向對象思想的一個最重要的原則就是:面向接口編程。
藉助接口和抽象類,23個設計模式中的不少思想被巧妙的實現了,我認爲其精髓簡單說來就是:面向抽象編程。
抽象類應主要用於關係密切的對象,而接口最適合爲不相關的類提供通用功能。
接口着重於CAN-DO關係類型,而抽象類則偏重於IS-A式的關係;
接口多定義對象的行爲;抽象類多定義對象的屬性;
接口定義能夠使用public、protected、internal 和private修飾符,可是幾乎全部的接口都定義爲public,緣由就沒必要多說了。
「接口不變」,是應該考慮的重要因素。因此,在由接口增長擴展時,應該增長新的接口,而不能更改現有接口。
儘可能將接口設計成功能單一的功能塊,以.NET Framework爲例,IDisposable、IDisposable、IComparable、IEquatable、IEnumerable等都只包含一個公共方法。
接口名稱前面的大寫字母「I」是一個約定,正如字段名如下劃線開頭同樣,請堅持這些原則。
在接口中,全部的方法都默認爲public。
若是預計會出現版本問題,能夠建立「抽象類」。例如,建立了狗(Dog)、雞(Chicken)和鴨(Duck),那麼應該考慮抽象出動物(Animal)來應對之後可能出現風馬牛的事情。而向接口中添加新成員則會強制要求修改全部派生類,並從新編譯,因此版本式的問題最好以抽象類來實現。
從抽象類派生的非抽象類必須包括繼承的全部抽象方法和抽象訪問器的實實現。
對抽象類不能使用new關鍵字,也不能被密封,緣由是抽象類不能被實例化。
在抽象方法聲明中不能使用 static 或 virtual 修飾符。
以上的規則,我就厚顏無恥的暫定爲T14條吧,寫的這麼累,就當一時的獎賞吧。你們也能夠互通有無,我將及時修訂。
4. 經典示例
4.1 絕對經典
.NET Framework是學習的最好資源,有意識的研究FCL是每一個.NET程序員的必修課,關於接口和抽象類在FCL中的使用,我有如下的建議:
FCL對集合類使用了基於接口的設計,因此請關注System.Collections中關於接口的設計實現;
FCL對數據流相關類使用了基於抽象類的設計,因此請關注System.IO.Stream類的抽象類設計機制。
4.2 別樣小菜
下面的實例,由於是個人理解,所以給經典打上「相對」的記號,至於何時晉升爲「絕對」,就看我在.NET追求的路上,是否可以一如既往的如此執着,所以我將把相對重構到絕對爲止(呵呵)。 本示例沒有闡述抽象類和接口在設計模式中的應用,由於那將是另外一篇有討論價值的文本,本文着眼與概念和原則的把握,可是真正的應用來自於具體的需求規範。
設計結構如圖所示:
1. 定義抽象類 public abstract class Animal { protected string _name; //聲明抽象屬性 public abstract string Name { get; } //聲明抽象方法 public abstract void Show(); //實現通常方法 public void MakeVoice() { Console.WriteLine("All animals can make voice!"); } }
2. 定義接口 public interface IAction { //定義公共方法標籤 void Move(); }
3. 實現抽象類和接口 public class Duck : Animal, IAction { public Duck(string name) {
_name = name; } //重載抽象方法 public override void Show() { Console.WriteLine(_name + " is showing for you."); } //重載抽象屬性 public override string Name { get { return _name;} } //實現接口方法 public void Move() { Console.WriteLine("Duck also can swim."); } } public class Dog : Animal, IAction { public Dog(string name) { _name = name; } public override void Show() { Console.WriteLine(_name + " is showing for you."); } public override string Name
{ get { return _name; } } public void Move() { Console.WriteLine(_name + " also can run."); } }
4. 客戶端實現 public class TestAnmial { public static void Main(string [] args) { Animal duck = new Duck("Duck"); duck.MakeVoice(); duck.Show(); Animal dog = new Dog("Dog"); dog.MakeVoice(); dog.Show(); IAction dogAction = new Dog("A big dog"); dogAction.Move(); } }
5. 他山之石
正所謂真理是你們看出來的,因此將園子裏有創新性的觀點潛列於此,一是感謝你們的共享,二是完善一家之言的不足,但願可以將領域造成知識,受用於我,受用於衆。
dunai認爲:抽象類是提取具體類的公因式,而接口是爲了將一些不相關的類「雜湊」成一個共同的羣體。至於他們在各個語言中的句法,語言細節並非我關心的重點。
樺山澗的收藏也很不錯。
Artech認爲:所代碼共用和可擴展性考慮,儘可能使用Abstract Class。固然接口在其餘方面的優點,我認爲也不可忽視。
shenfx認爲:當在差別較大的對象間尋求功能上的共性時,使用接口;當在共性較多的對象間尋求功能上的差別時,使用抽象基類。
最後,MSDN的建議是:
若是預計要建立組件的多個版本,則建立抽象類。抽象類提供簡單易行的方法來控制組件版本。經過更新基類,全部繼承類都隨更改自動更新。另外一方面,接口一旦建立就不能更改。若是須要接口的新版本,必須建立一個全新的接口。
若是建立的功能將在大範圍的全異對象間使用,則使用接口。抽象類應主要用於關係密切的對象,而接口最適合爲不相關的類提供通用功能。
若是要設計小而簡練的功能塊,則使用接口。若是要設計大的功能單元,則使用抽象類。
若是要在組件的全部實現間提供通用的已實現功能,則使用抽象類。抽象類容許部分實現類,而接口不包含任何成員的實現。
6. 結論
接口和抽象類,是論壇上、課堂間討論最多的話題之一,之因此將這個老話題拿出來再議,是由於從個人體會來講,深入的理解這兩個面向對象的基本內容,對於盤活面向對象的抽象化編程思想相當重要。本文基本概況了接口和抽象類的概念、異同和使用規則,從學習的觀點來看,我認爲這些總結已經足以表達其核心。可是,對於面向對象和軟件設計的深刻理解,仍是創建在不斷實踐的基礎上,Scott說本身天天堅持一個小時用來寫Demo,那麼咱們是否是更應該勤於鍵盤呢。對於接口和抽象類,請多用而知其然,多想而知其奧吧。
7.5:恩怨情仇:is和as
本文將介紹如下內容:
• 類型轉換
• is/as操做符小議
1. 引言 類型安全是.NET設計之初重點考慮的內容之一,對於程序設計者來講,徹底把握系統數據的類型安全,常常是力不從心的問題。如今,這一切已經在微軟大牛們的設計框架中爲你解決了。在.NET中,一切類型都必須集成自System.Object類型,所以咱們能夠很容易的得到對象的準確類型,方法是:GetType()方法。那麼.NET中的類型轉換,應該考慮的地方有那些呢?
2. 概念引入
類型轉換包括顯示轉換和隱式轉換,在.NET中類型轉換的基本規則以下:
任何類型均可以安全的轉換爲其基類類型,能夠由隱式轉換來完成;
任何類型轉換爲其派生類型時,必須進行顯示轉換,轉換的規則是:(類型名)對象名;
使用GetType能夠取得任何對象的精確類型;
基本類型能夠使用Covert類實現類型轉換;
除了string之外的其餘類型都有Parse方法,用於將字符串類型轉換爲對應的基本類型;
值類型和引用類型的轉換機制稱爲裝箱(boxing)和拆箱(unboxing)。
3. 原理與示例說明
淺談了類型轉換的幾個廣泛關注的方面,該將主要精力放在is、as操做符的恩怨情仇上了。類型轉換將是個較大的話題,留於適當的時機討論。 is/as操做符,是C#中用於類型轉換的,提供了對類型兼容性的判斷,從而使得類型轉換控制在安全的範疇,提供了靈活的類型轉換控制。 is的規則以下:
檢查對象類型的兼容性,並返回結果,true或者false;
不會拋出異常;
若是對象爲null,則返回值永遠爲false。
其典型用法爲: 1object o = new object(); 2 3class A 4 5{ 6 7} 8 9if (o is A) //執行第一次類型兼容檢查 10 11{ 12 13 A a = (A) o; //執行第二次類型兼容檢查 14 15} 16 17
as的規則以下:
檢查對象類型的兼容性,並返回結果,若是不兼容就返回null;
不會拋出異常;
若是結果判斷爲空,則強制執行類型轉換將拋出NullReferenceException異常。
其典型用法爲: 1object o = new object(); 2 3class B 4 5{ 6 7} 8
9B b = o as B; //執行一次類型兼容檢查 10 11if (b != null) 12 13{ 14 15 MessageBox.Show("b is B's instance."); 16 17} 18 19
4. 結論
縱上比較,is/as操做符,提供了更加靈活的類型轉型方式,可是as操做符在執行效率上更勝一籌,咱們在實際的編程中應該體會其異同,酌情量才。
7.6:貌合神離:覆寫和重載
本文將介紹如下內容:
什麼是覆寫,什麼是重載
覆寫與重載的區別
覆寫與重載在多態特性中的應用
1. 引言
覆寫(override)與重載(overload),是成就.NET面向對象多態特性的基本技術之一,兩個貌似類似而實則否則的概念,經常帶給咱們不少的誤解,所以有必要以專題來討論清楚其區別,而更重要的是關注其在多態中的應用。
在系列中,咱們前後都有關於這一話題的點滴論述,本文以專題的形式再次作以深度討論,相關的內容請對前文作以參考。
2. 認識覆寫和重載
從一個示例開始來認識什麼是覆寫,什麼是重載? abstract class Base { //定義虛方法 public virtual void MyFunc() { } //參數列表不一樣,virtual不足以區分 public virtual void MyFunc(string str) { } //參數列表不一樣,返回值不一樣 public bool MyFunc(string str, int id) { Console.WriteLine("AAA"); return true; } //參數列表不一樣表現爲個數不一樣,或者相同位置的參數類型不一樣 public bool MyFunc(int id, string str) { Console.WriteLine("BBB"); return false; } //泛型重載,容許參數列表相同 public bool MyFunc<T>(string str, int id) { return true;
} //定義抽象方法 public abstract void Func(); } class Derived: Base { //阻隔父類成員 public new void MyFunc() { } //覆寫基類成員 public override void MyFunc(string str) { //在子類中訪問父類成員 base.MyFunc(str); } //覆寫基類抽象方法 public override void Func() { //實現覆寫方法 } }
2.1 覆寫基礎篇
覆寫,又稱重寫,就是在子類中重複定義父類方法,提供不一樣實現,存在於有繼承關係的父子關係。當子類重寫父類的虛函數後,父類對象就能夠根據根據賦予它的不一樣子類指針動態的調用子類的方法。從示例的分析,總結覆寫的基本特徵包括:
在.NET中只有以virtual和abstract標記的虛方法和抽象方法才能被直接覆寫。
覆寫以關鍵字override標記,強調繼承關係中對基類方法的重寫。
覆寫方法要求具備相同的方法簽名,包括:相同的方法名、相同的參數列表和相同的返回值類型。 概念:虛方法 虛方法就是以virtual關鍵字修飾並在一個或多個派生類中實現的方法,子類重寫的虛方法則以override關鍵字標記。虛方法調用,是在運行時肯定根據其調用對象的類型來肯定調用適當的覆寫方法。.NET默認是非虛方法,若是一個方法被virtual標記,則不可再被static、abstrcat和override修飾。 概念:抽象方法 抽象方法就是以abstract關鍵字修飾的方法,抽象方法能夠看做是沒有實現體的虛方法,而且必須在派生類中被覆寫,若是一個類包括抽象方法,則該類就是一個抽象類。所以,抽象方法其實隱含爲虛方法,只是在聲明和調用語法上有所不一樣。abstract和virtual一塊兒使用是錯誤的。
2.2 重載基礎篇
重載,就是在同一個類中存在多個同名的方法,而這些方法的參數列表和返回值類型不一樣。值得注意的是,重載的概念並不是面向對象編程的範疇,從編譯器角度理解,不一樣的參數列表、不一樣的返回值類型,就意味着不一樣的方法名。也就是說,方法的地址,在編譯期就已經肯定,是這一種靜態綁定。從示例中,咱們總結重載的基本特徵包括:
重載存在於同一個類中。
重載方法要求具備相同的方法名,不一樣的參數列表,返回值類型能夠相同也能夠不一樣(經過operator implicit 能夠實現必定程度的返回值重載,不過不值得推薦)。
.NET 2.0引入泛型技術,使得相同的參數列表、相同的返回值類型的狀況也能夠構成重載。
3. 在多態中的應用
多態性,簡單的說就是「一個接口,多個方法」,具體表現爲相同的方法簽名表明不一樣的方法實現,同一操做做用於不一樣的對象,產生不一樣的執行結果。在.NET中,覆寫實現了運行時的多態性,而重載實現了編譯時的多態性。
運行時的多態性,又稱爲動態聯編,經過虛方法的動態調度,在運行時根據實際的調用實例類型決定調用的方法實現,從而產生不一樣的執行結果。 class Base {
public virtual void MyFunc(string str) { Console.WriteLine("{0} in Base", str); } } class Derived: Base { //覆寫基類成員 public override void MyFunc(string str) { Console.WriteLine("{0} in Derived", str); } public static void Main() { Base B = new Base(); B.MyFunc("Hello"); Derived A = new Derived(); B = A; B.MyFunc("Morning"); } }
從結果中可知,對象B兩次執行B.MyFunc調用了不一樣的方法,第一次調用基類方法MyFunc,而第二次調用了派生類方法MyFunc。在執行過程當中,對象B前後指向了不一樣的類的實例,從而動態調用了不一樣的實例方法,顯然這一執行操做並不是肯定於編譯時,而是在運行時根據對象B執行的不一樣類型來肯定的。咱們在此不分析虛擬方法的動態調度機制,而只關注經過虛方法覆寫而實現的多態特性,詳細的實現機制請參考本系列的其它內容。
編譯時的多態性,又稱爲靜態聯編,通常包括方法重載和運算符重載。對於非虛方法來講,在編譯時經過方法的參數列表和返回值類型決定不一樣操做,實現編譯時的多態性。例如,在實際的開發過程當中,.NET開發工具Visual Studio的智能感知功能就很好的爲方法重載提供了很好的交互手段,例如:
從智能感知中可知方法MyFunc在派生類Derived中有三次重載,調用哪一種方法由程序開發者根據其參數、返回值的不一樣而決定。因而可知,方法重載是一種編譯時的多態,對象A調用哪一種方法在編譯時就已經肯定。
4. 比較,仍是規則
若是基訪問引用的是一個抽象方法,則將致使編譯錯誤。 abstract class Base { public abstract void Func(); } class Derived: Base { //覆寫基類抽象方法 public override void Func() { base.Func(); } }
虛方法不能是靜態的、密封的。
覆寫實現的多態肯定於運行時,所以更加的靈活和抽象;重載實現的多態肯定於編譯時,所以更加的簡單和高效。兩者各有特色與應用,不可替代。
在下表中,將覆寫與重載作以總結性的對比,主要包括:
規則
覆寫(override)
重載(overload)
存在位置
存在於有繼承關係的不一樣類中
存在於同一個類中
調用機制
運行時肯定
編譯時肯定
方法名
必須相同
必須相同
參數列表
必須相同
必須不一樣
返回值類型
必須相同
能夠不相同
泛型方法
能夠覆寫
能夠重載
注:參數列表相同表示參數的個數相同,而且相同位置的參數類型也相同。
5. 結論
深刻的理解覆寫和重載,是對多態特性和麪向對象機制的有力補充,本文從基本概念到應用領域將兩個概念進行一一梳理,經過對比整理區別,還覆寫和重載以更全面的認知角度,同時也更能從側面深刻的瞭解運行時多態與編譯時多態的不一樣狀況。
再談重載與覆寫 昨天我在新手區發了一篇《重載仍是覆寫?》的隨筆,後來我發現我犯了一個嚴重的錯誤,沒有具體說明是.NET 1.1仍是2.0,在.NET2.0中因爲泛型的出現,對重載和覆寫有時候就不能按照1.1下那幾個特徵去要求。 1.重載(Overload) 在.NET1.1下,咱們定義重載:類中定義的方法可能有不一樣的版本,它具備以下的特徵:
I. 方法名必須相同 II. 參數列表必須不相同,與參數列表的順序無關 III. 返回值類型能夠不相同 示意代碼: public class MyClass { public void Write(string _str) { // } public void Write(string _str, string _target) { // } public bool Write(string _str, string _target, bool _flag) { // } } 在.NET2.0下,因爲泛型的出現,咱們就不能再用這三個特徵來判斷重載,以下面的兩個方法,它們具備相同的方法名,相同的參數列表,相同的返回值,可是它們卻能夠構成重載: public class MyClass { public void Write<T>(string _str) { // } public void Write(string _str)
{ // } } 再看下面這兩個方法,它們不能構成重載,由於若是T,U若是實例化時傳入相同的類型,則這兩個方法就具備相同的簽名: public class MyClass8<T,U> { public T MyMothed(T a, U b) { return a; } public U MyMothed(U a, T b) { return b; } } 可是當咱們再添加另一個方法後,這個類卻能夠編譯經過: public class MyClass8<T,U> { public T MyMothed(T a, U b) { return a; } public U MyMothed(U a, T b) { return b; } public int MyMothed(int a, int b)
{ return a + b; } } 經過調用能夠發現,優先匹配的方法是通常方法,而非泛型方法。總之,構成重載的第二個特徵參數列表必須不一樣,實際上是讓方法具備不一樣的簽名,調用程序能夠區分,在有泛型時要特別注意,而第一點和第三點仍然適用。 2.覆寫(Override) 在.NET1.1下,對覆寫咱們的定義是:子類中爲知足本身的須要來重複定義某個方法的不一樣實現,它具備以下特徵: I. 經過使用關鍵字Override來覆寫 II. 只有虛方法和抽象方法直接能夠被覆寫 III. 相同的方法名 IV. 相同的參數列表 V. 相同的返回值類型 示意代碼: public abstract class BaseClass { public abstract void Write(string _str); } public class SubClass : BaseClass { public override void Write(string _str) { // } }
在.NET2.0中,泛型方法的覆寫,除了要遵照以上幾點外,還應該注意: 在重寫定義了泛型參數的虛擬方法時,子類方法必須從新定義該方法特定的泛型參數: public class MyBaseClass { public virtual void MyMothed<T>(T t) { // } } public class MySubClass : MyBaseClass { public override void MyMothed<T>(T t)//從新定義泛型參數T { // } } 在重寫定義了泛型參數的虛擬方法時,子類實現不能重複在基類方法級別出現的約束: public class MyBaseClass { public virtual void MyMothed<T>(T t) where T : new() { // } } public class MySubClass:MyBaseClass { public override void MyMothed<T>(T t)//不能重複任何約束 { //
} }
第8章 原本面目——框架詮釋
8.1 萬物歸宗:System.Object 本節將介紹如下內容: — System.Object類型解析 — Object類型的經常使用方法及其應用 8.1.1 引言 正如標題所示,System.Object是全部類型的基類,任何類型都直接或間接繼承自System.Object類。沒有指定基類的類型都默認繼承於System.Object,從而具備Object的基本特性,這些特性主要包括: l 經過GetType方法,獲取對象類型信息。 l 經過Equals、ReferenceEquals和==,實現對象判等。 l 經過ToString方法,獲取對象字符串信息 ,默認返回對象類型全名。 l 經過MemberwiseClone方法,實現對象實例的淺拷貝。 l 經過GetHashCode方法,獲取對象的值的散列碼。 l 經過Finalize方法,在垃圾回收時進行資源清理。 接下來,咱們和這些公共特性一一過招,來了解其做用和意義,深刻其功能和應用。 8.1.2 初識
有了對Object類型的初步認識,咱們使用Reflector工具加載mscorlib程序集來反編譯Sytem.Object的實現狀況,首先不關注具體的實現細節,將注意力放在基本的類型定義上: public class Object { //構造函數 public Object() { } public virtual int GetHashCode() { } //獲取對象類型信息 public System.Type GetType() { } //虛方法,返回對象的字符串表示方式 public virtual string ToString() { } //幾種對象判等方法 public virtual bool Equals(object obj) { } public static bool Equals(object objA, object objB) { } public static bool ReferenceEquals(object objA, object objB) { } //執行對象的淺拷貝 protected object MemberwiseClone() { } //析構函數 protected virtual void Finalize() { } } 從反編譯代碼中可知,System.Object主要包括了4個公用方法和2個受保護方法,其具體的應用和實如今後文表述。 8.1.3 分解 下面,咱們選擇Object的幾個主要的方法來分析其實現,以便從總體上把握對Object的認知。 1.ToString解析 ToString是一個虛方法,用於返回對象的字符串表示,在Object類型的實現能夠表示爲: public virtual string ToString()
{ return this.GetType().FullName.ToString(); } 可見,默認狀況下,對象調用ToString方法將返回類型全名稱,也就是命名空間加類型名全稱。在一般的狀況下,ToString方法提供了在子類中從新覆寫基類方法而獲取對象當前值的字符串信息的合理途徑。例如,下面的類型MyLocation將經過ToString方法來獲取其座標信息: class MyLocation { private int x = 0; private int y = 0; public override string ToString() { return String.Format("The location is ({0}, {1}).", x, y); } } 而.NET框架中的不少類型也實現了對ToString方法的覆寫,例如Boolean類型經過覆寫ToString來返回真或者假特徵: public override string ToString() { if (!this) { return "False"; } return "True"; } ToString方法,能夠在調試期快速獲取對象信息,可是Object類型中實現的ToString方法仍是具備一些侷限性,例如在格式化、語言文化方面Object.ToString方法就沒有更多的選擇。解決的辦法就是實現IFormattable接口,其定義爲: public interface IFormattable
{ string ToString(string format, System.IFormatProvider formatProvider); } 其中,參數format代表要格式化的方式,而參數formatProvider則提供了特定語言文化的信息。事實上,.NET基本類型都實現了IFormattable接口,以實現更靈活的字符串信息選擇。以DateTime類型的ToString方法爲例,其實現細節可表示爲: public struct DateTime : IFormattable { public string ToString(string format, IFormatProvider provider) { return DateTimeFormat.Format(this, format, DateTimeFormatInfo.GetInstance(provider)); } } 咱們能夠經過控制format參數和provider參數來實現特定的字符串信息返回,例如要想獲取當前線程的區域性長格式日期時,能夠如下面的方式實現: DateTime dt = DateTime.Now; string time = dt.ToString("D", DateTimeFormatInfo.CurrentInfo); 而想要獲取固定區域性短格式日期時,則以另外的設定來實現: DateTime dt = DateTime.Now; string time = dt.ToString("d", DateTimeFormatInfo.InvariantInfo); 關於ToString方法,還應指出的是System.String類型中並無實現IFormattable接口,System.String.ToString方法用來返回當前對象的一個引用,也就是this。 2.GetType解析 GetType方法爲非虛的,用於在運行時經過查詢對象元數據來獲取對象的運行時類型。由於子類沒法經過覆寫GetType而篡改類型信息,從而保證類型安全。例如在下面的示例中: class MyType
{ } class Test_GetType { public static void Main() { MyType mt = new MyType(); //使用Object.GetType返回Type實例 Type tp = mt.GetType(); //返回類型全名稱 Console.WriteLine(tp.ToString()); //僅返回類型名 Console.WriteLine(tp.Name.ToString()); } } //執行結果 //InsideDotNet.Framework.Object.MyType //MyType GetType返回的是一個System.Type或其派生類的實例。而該實例對象能夠經過反射獲取類型的元數據信息。從能夠提供所屬類型的不少信息,例如字段、屬性和方法等,例如: class MyType { private int number = 0; private string name = null; public static void ShowType(string type, string info) { Console.WriteLine("This type is MyType."); } private void ShowNumber() { Console.WriteLine(number.ToString());
} } class Test_GetType { public static void Main() { MyType mt = new MyType(); //根據Type實例查找類型成員 foreach (MemberInfo info in tp.GetMembers()) { Console.WriteLine("The member is {0}, {1}", info.Name, info.DeclaringType); } //根據Type實例查找類型方法 foreach (MethodInfo mi in tp.GetMethods()) { Console.WriteLine("The method is {0}", mi.ToString()); //查找方法參數信息 ParameterInfo[] pis = mi.GetParameters(); foreach (ParameterInfo pi in pis) { Console.WriteLine("{0}'s member is {1}", mi.ToString(), pi.ToString()); } } } } 經過反射機制,就能夠根據GetType方法返回的Type對象在運行期枚舉出元數據表中定義的全部類型的信息,並根據System.Reflection空間中的方法獲取類型的信息,包括:字段、屬性、方法、參數、事件等,例如上例中就是根據System.Reflection中定義的相關方法來完成獲取對象信息的處理過程。在晚期綁定的應用場合中,這種處理尤其常見。
.NET中,用於在運行期獲取類型Type實例的方法並不是只有Object.GetType方法,Type.GetType靜態方法和typeof運算符也能完成一樣的操做,不過在應用上有些區別,主要是: l Type.GetType是非強類型方法;而typeof運算符支持強類型。 Type tp = Type.GetType("InsideDotNet.Framework.Object.MyType"); Type tp = typeof(InsideDotNet.Framework.Object.MyType); l Type.GetType支持運行時跨程序集反射,以解決動態引用;而typeof只能支持靜態引用。 Assembly ass = Assembly.LoadFrom(@"C:\Anytao.Utility.exe"); Type tpd = ass.GetType("Anytao.Utility.Message.AnyMsg"); Console.WriteLine(tpd.ToString()); 注意:Type.GetType必須使用徹底限定名,以免模塊依賴或循環引用問題。 另外,對於在運行期獲取Type實例的方法,還可參考如下幾種常見的方式,主要包括: l 利用System.Reflection.Assembly的非靜態方法GetType或GetTypes。 l 利用System.Reflection.Module的非靜態方法GetType或GetTypes。 經過Assembly或Module實例來獲取Type實例,也是程序設計中常見的技巧之一。 3.其餘 l Equals靜態方法、虛方法和ReferenceEquals方法用於對象判等,詳細的應用請參考8.2節「規則而定:對象判等」。 l GetHashCode方法,用於在類型中提供哈希值,以應用於哈希算法或哈希表,不過值得注意的是對Equals方法和GetHashCode方法的覆寫要保持統一,由於兩個對象的值相等,其哈希碼也應該相等,不然僅覆寫Equals而不改變GetHashCode,會致使編譯器拋出警告信息。 l Memberwise方法,用於在對象克隆時實現對象的淺拷貝,詳細應用請參考7.7節「有深有淺的克隆:淺拷貝和深拷貝」。 l Finalize方法,用於在垃圾回收時實現資源清理,詳細應用請參考5.3節「垃圾回收」。 8.1.4 意義
l 實現自上而下的單根繼承。 l System.Object是一切類型的最終基類,也就意味着.NET的任何變量都是System.Object的實例,這種機制提供了不一樣類型之間進行交互通訊的可能。也賦予了全部.NET基本類型的最小化功能方法,例如ToString方法、GetHashCode方法和Equals方法等。 8.1.5 結論 經過本節的論述,咱們基本瞭解了System.Object類型的設計思路和實現細節,從框架設計的角度來看,咱們應該瞭解和學習System.Object在設計與實現上的可取之道,一方面.NET框架提供了最小功能特徵在子類中繼承,另外一方面則分別將不一樣的特徵方法實現爲不一樣的訪問級別和虛方法,這些思路和技巧正是值得咱們借鑑和深思的精華所在。
8.2 規則而定:對象判等 本節將介紹如下內容: — 四種判等方法解析 — 實現自定義Equals方法 — 判等規則 8.2.1 引言 瞭解.NET的對象判等,有必要從瞭解幾個相關的基本概念開始: l 值相等。表示比較的兩個對象的數據成員按內存位分別相等,即兩個對象類型相同,而且具備相等和相同的字段。 l 引用相等。表示兩個引用指向同一對象實例,也就是同一內存地址。所以,能夠由引用相等推出其值相等,反之則否則。 關於對象的判等,涉及了對相等這一律唸的理解。其實這是一個典型的數學論題,因此數學上的等價原則也一樣適用於對象判等時的規則,主要是:
l 自反性,就是a==a老是爲true。 l 對稱性,就是若是a==b成立,則b==a也成立。 l 傳遞性,就是若是a==b,b==c成立,則a==c也成立。 瞭解了對象判斷的類型和原則,接下來就認識一下System.Object類中實現的幾個對象判等方法,它們是: l public virtual bool Equals(object obj)虛方法,比較對象實例是否相等。 l public static bool Equals(object objA,object objB)靜態方法,比較對象實例是否相等。 l public static bool ReferenceEquals(object objA,object objB)靜態方法,比較兩個引用是否指向同一個對象。 同時在.NET中,還有一個「==」操做符提供了更簡潔的語義來表達對象的判等,因此.NET的對象判等方法就包括了這四種類型,下面一一展開介紹。 8.2.2 本質分析 1.Equals靜態方法 Equals靜態方法實現了對兩個對象的相等性判別,其在System.Object類型中實現過程能夠表示爲: public static bool Equals(object objA, object objB) { if (objA == objB) { return true; } if ((objA != null) && (objB != null)) { return objA.Equals(objB); }
return false; } 對以上過程,能夠小結爲:首先比較兩個類型是否爲同一實例,若是是則返回true;不然將進一步判斷兩個對象是否都爲null,若是是則返回true;若是不是則返回objA對象的Equals虛方法的執行結果。因此,Equals靜態方法的執行結果,依次取決於三個條件: l 是否爲同一實例。 l 是否都爲null。 l 第一個參數的Equals實現。 所以,一般狀況下Equals靜態方法的執行結果經常受到判等對象的影響,例若有下面的測試過程: class MyClassA { public override bool Equals(object obj) { return true; } } class MyClassB { public override bool Equals(object obj) { return false; } } class Test_Equals { public static void Main() { MyClassA objA = new MyClassA();
MyClassB objB = new MyClassB(); Console.WriteLine(Equals(objA, objB)); Console.WriteLine(Equals(objB, objA)); } } //執行結果 True False 由執行結果可知,靜態Equals的執行取決於==操做符和Equals虛方法這兩個因素。所以,決議靜態Equals方法的執行,就要在自定義類型中覆寫Equals方法和重載==操做符。 還應注意到,.NET提供了Equals靜態方法能夠解決兩個值爲null對象的判等問題,而使用objA.Equals(object objB)來判斷兩個null對象會拋出NullReferenceException異常,例如: public static void Main() { object o = null; o.Equals(null); } 2.ReferenceEquals靜態方法 ReferenceEquals方法爲靜態方法,所以不能在繼承類中重寫該方法,因此只能使用System.Object的實現代碼,具體爲: public static bool ReferenceEquals(object objA, object objB) { return (objA == objB); } 可見,ReferenceEquals方法用於判斷兩個引用是否指向同一個對象,也就是前文強調的引用相等。所以以ReferenceEquals方法比較同一個類型的兩個對象實例將返回fasle,而.NET認爲null等於null,所以下面的實例就能很容易理解得出的結果: public static void Main()
{ MyClass mc1 = new MyClass(); MyClass mc2 = new MyClass(); //mc1和mc3指向同一對象實例 MyClass mc3 = mc1; //顯示:False Console.WriteLine(ReferenceEquals(mc1, mc2)); //顯示:True Console.WriteLine(ReferenceEquals(mc1, mc3)); //顯示:True Console.WriteLine(ReferenceEquals(null, null)); //顯示:False Console.WriteLine(ReferenceEquals(mc1, null)); } 所以,ReferenceEquals方法,只能用於比較兩個引用類型,而以ReferenceEquals方法比較值類型,必然伴隨着裝箱操做的執行,分配在不一樣地址的兩個裝箱的實例對象,確定返回false結果,關於裝箱詳見4.4節「皆有可能——裝箱與拆箱」。例如: public static void Main() { Console.WriteLine(ReferenceEquals(1, 1)); } //執行結果:False 另外,應該關注.NET某些特殊類型的「意外」規則,例以下面的實現將突破常規,除了深入地瞭解ReferenceEquals的實現規則,也應理解某些特殊狀況背後的祕密: public static void Main() { string strA = "ABCDEF"; string strB = "ABCDEF"; Console.WriteLine(ReferenceEquals(strA, strB));
} //執行結果:True 從結果分析可知兩次建立的string類型實例不只內容相同,並且分享共同的內存空間,事實上的確如此,這緣於System.String類型的字符串駐留機制,詳細的討論見8.3節「爲何特殊:string類型解析」,在此咱們必須明確ReferenceEquals判斷引用相等的實質是無可置疑的。 3.Equals虛方法 Equals虛方法用於比較兩個類型實例是否相等,也就是判斷兩個對象是否具備相同的「值」,在System.Object中其實現代碼,能夠表示爲: public virtual bool Equals(object obj) { return InternalEquals(this, obj); } 其中InternalEquals爲一個靜態外部引用方法,其實現的操做能夠表示成: if (this == obj) return true; else return false; 可見,默認狀況下,Equals方法和ReferenceEquals方法是同樣的,Object類中的Equals虛方法僅僅提供了最簡單的比較策略:若是兩個引用指向同一個對象,則返回true;不然將返回false,也就是判斷是否引用相等。然而這種方法並未達到Equals比較兩個對象值相等的目標,所以System.Object將這個任務交給其派生類型去從新實現,能夠說Equals的比較結果取決於類的建立者是如何實現的,而非統一性約定。 事實上,.NET框架類庫中有不少的引用類型實現了Equals方法用於比較值相等,例如比較兩個System.String類型對象是否相等,確定關注其內容是否相等,判斷的是值相等語義: public static void Main() { string str1 = "acb";
string str2 = "acb"; Console.WriteLine(str1 == str2); } 4.==操做符 在.NET中,默認狀況下,操做符「==」在值類型狀況下表示是否值相等,由值類型的根類System.ValueType提供了實現;而在引用類型狀況下表示是否引用相等,而「!=」操做符與「==」語義相似。固然也有例外,System.String類型則以「==」來處理值相等。所以,對於自定義值類型,若是重載Equals方法,則應該保持和「==」在語義上的一致,以返回值相等結果;而對於引用類型,若是以覆寫來處理值相等規則時,則不該該再重載「==」運行符號,由於保持其缺省語義爲判斷引用相等纔是恰當的處理規則。 Equals虛方法與==操做符的主要區別在於多態表現:Equals經過虛方法覆寫來實現,而==操做符則是經過運算符重載來實現,覆寫和重載的區別請參考1.4節「多態的藝術」。 8.2.3 覆寫Equals方法 通過對四種不一樣類型判等方法的討論,咱們不難發現無論是Equals靜態方法、Equals虛方法抑或==操做符的執行結果,均可能受到覆寫Equals方法的影響。所以研究對象判等就必須將注意力集中在自定義類型中如何實現Equals方法,以及實現怎樣的Equals方法。由於,不一樣的類型,對於「相等」的理解會有所誤差,你甚至能夠在自定義類型中實現一個老是相等的類型,例如: class AlwaysEquals { public override bool Equals(object obj) { return true; } } 所以,Euqls方法的執行結果取決於自定義類型的具體實現規則,而.NET又爲何提供這種機制來實現對象判等策略呢?首先,對象判等決定於需求,沒有必要爲全部.NET類型完成邏輯判等,
System.Object基類也沒法提供知足各類需求的判等方法;其次,對象判等包括值判等和引用判等兩個方面,不一樣的類型對判等的處理又有所不一樣,經過多態機制在派生類中處理各自的判等實現顯然是更加明智與可取的選擇。 接下來,咱們開始研究如何經過覆寫Equals方法實現對象的判等。覆寫Equals每每並不是易事,要綜合考慮到對值類型字段和引用類型字段的分別判等處理,同時還要兼顧父類覆寫所帶來的影響。不適當的覆寫會引起意想不到的問題,因此必須遵循三個等價原則:自反、傳遞和對稱,這是實現Equals的通用契約。那麼又如何爲自定義類型實現Equals方法呢? 最好的參考資源固然來自於.NET框架類庫的實現,事實上,關於Equals的覆寫在.NET中已經有不少的基本類型完成了這一實現。從值類型和引用類型兩個角度來看: l 對於值類型,基類System.ValueType經過反射機制覆寫了Equals方法來比較兩個對象的值相等,可是這種方式並不高效,更明智的辦法是在自定義值類型時有針對性的覆寫Equals方法,來提供更靈活、高效的處理機制。 l 對於引用類型,覆寫Equals方法意味着要改變System.Object類型提供的引用相等語義。那麼,覆寫Equals要根據類型自己的特色來實現,在.NET框架類庫中就有不少典型的引用類型實現了值相等語義。例如System.String類型的兩個變量相等意味着其包含了相等的內容,System.Version類型的兩個變量相等也意味着其Version信息的各個指標分別相等。 所以對Equals方法的覆寫主要包括對值類型的覆寫和對引用類型的覆寫,同時也要區別基類是否已經有過覆寫和未曾覆寫兩種狀況,並以等價原則爲前提,進行判斷。在此,咱們僅提供較爲標準的實現方法,具體的實現取決於不一樣的類型定義和語義需求。 class EqualsEx { //定義值類型成員ms private MyStruct ms; //定義引用類型成員mc private MyClass mc; public override bool Equals(object obj) { //爲null,則必不相等
if (obj == null) return false; //引用判等爲真,則兩者一定相等 if (ReferenceEquals(this, obj)) return true; //類型判斷 EqualsEx objEx = obj as EqualsEx; if (objEx == null) return false; //最後是成員判斷,分值類型成員和引用類型成員 //一般能夠提供強類型的判等方法來單獨處理對各個成員的判等 return EqualsHelper(this, objEx); } private static bool EqualsHelper(EqualsEx objA, EqualsEx objB) { //值類型成員判斷 if (!objA.ms.Equals(objA.ms)) return false; //引用類型成員判斷 if (!Equals(objA.mc, objB.mc)) return false; //最後,才能夠斷定兩個對象是相等的 return true; } } 上述示例只是從標準化的角度來闡釋Equals覆寫的簡單實現,而實際應用時又會有所不一樣,然而總結起來實現Equals方法咱們應該着力於如下幾點:首先,檢測obj是否爲null,若是是則必然不相等;而後,以ReferenceEquals來判等是否引用相等,這種辦法比較高效,由於引用相等便可以推出值相等;而後,再進行類型判斷,不一樣類型的對象必定不相等;最後,也是最複雜的一個過程,即對對象的各個成員進行比較,引用類型進行恆定性判斷,值類型進行恆等性判斷。在本例中咱們將成員判斷封裝爲一個專門的處理方法EqualsHelper,以隔離對類成員的判斷實現,主要有如下幾個好處: l 符合Extract Method原則,以隔離相對變化的操做。 l 提供了強類型版本的Equals實現,對於值類型成員來講還能夠避免沒必要要的裝箱操做。
l 爲==操做符提供了重載實現的安全版本。 在.NET框架中,System.String類型的Equals覆寫方法就提供了EqualsHelper方法來實現。 8.2.4 與GetHashCode方法同步 GetHashCode方法,用於獲取對象的哈希值,以應用於哈希算法、加密和校驗等操做中。相同的對象必然具備相同的哈希值,所以GetHashCode的行爲依賴於Equals方法進行判斷,在覆寫Equals方法時,也必須覆寫GetHashCode,以同步兩者在語義上的統一。例如: public class Person { //每一個人有惟一的身份證號,所以能夠做爲Person的標識碼 private string id = null; private string name = null; //以id做爲哈希碼是可靠的, 而name則有可能相同 public override int GetHashCode() { return id.GetHashCode(); } public override bool Equals(object obj) { if(ReferenceEquals(this, obj)) return true; Person person = obj as Person; if(person == null) return false; //Equals也以用戶身份證號做爲判等依據 if(this.id == person.id) return true; return false; } }
兩者的關係能夠表達爲:若是x.Equals(y)爲true成立,則必有x.GetHashCode() == y.GetHashCode()成立。若是覆寫了Equals而沒有實現GetHashCode,C#編譯器會給出沒有覆寫GetHashCode的警告。 8.2.5 規則 l 值相等仍是引用相等決定於具體的需求,Equals方法的覆寫實現也決定於類型想要實現的判等邏輯。 l 幾個判等方法相互引用,因此對某個方法的覆寫可能會影響其餘方法的執行結果。 l 若是覆寫了Equals虛方法,則必須從新實現GetHashCode方法,使兩者保持同步。 l 禁止從Equals方法或者「==」操做符拋出異常,應該在Equals內部首先避免null引用異常,要麼相等要麼不等。 l ReferenceEquals方法主要用於判別兩個對象的惟一性,比較兩個值類型則必定返回false。 l ReferenceEquals方法比較兩個System.String類型的惟一性時,要注意String類型的特殊性:字符串駐留。 l 實現ICompare接口的類型必須從新實現Equals方法。 l 值類型最好從新實現Equals方法和重載==操做符,由於默認狀況下實現的是引用相等。 8.2.6 結論 四種判等方法,各有用途又相互關聯。這是CLR提供給咱們關於對象等值性和惟一性的執行機制。分,咱們以不一樣角度來了解其本質;合,咱們以規則來闡釋其關聯。在本質和關聯之上,充分體會.NET這種抽象而又靈活的判等機制,留下更多的思考來認識這種精妙的設計。
8.3 如此特殊:大話String 本節將介紹如下內容: — String類型解析
— 字符串恆定與字符串駐留 — StringBuilder應用與對比 8.3.1 引言 String類型很特殊,算是.NET你們庭中少有的異類,它是如此的不同凡響,使咱們沒法忽視它的存在。本節就是這樣一篇關於String類型及其特殊性討論的話題,經過逐層解析來解密System.String類型。 那麼,String究竟特殊在哪裏? l 建立特殊性:String對象不以newobj指令建立,而是ldstr指令建立。在實現機制上,CLR給了特殊照顧來優化其性能。 l String類型是.NET中不變模式的經典應用,在CLR內部由特定的控制器來專門處理String對象。 l 應用上,String類型表現爲值類型語義;內存上,String類型實現爲引用類型,存儲在託管堆中。 l 兩次建立內容相同的String對象能夠指向相同的內存地址。 l String類型被實現爲密封類,不可在子類中繼承。 l String類型是跨應用程序域的,能夠在不一樣的應用程序域中訪問同一String對象。 然而,將String類型認清看透並不是易事,根據上面的特殊問題,咱們給出具體的答案,爲String類型的各個難點解惑,最後再給出應用的常見方法和典型操做。 8.3.2 字符串建立 string類型是C#基元類型,對應於FCL中的System.String類型,是.NET中使用最頻繁,應用最普遍的基本類型之一。其建立與實例化過程很是簡單,在操做方式上相似與其餘基元類型int、char等,例如: string mystr = "Hello";
分析IL可知,CLR使用ldstr指令從元數據中獲取文本常量來加載字符串,而以典型的new方式來建立: String mystr2 = new String("Hello"); 會致使編譯錯誤。由於System.String只提供了數個接受Char*、Char[]類型的構造函數,例如: Char[] cs = {'a', 'b', 'c'}; String strArr = new String(cs); 在.NET中不多使用構造器方式來建立string對象,更多的方式仍是以加載字符常量的方式來完成,關於String類型的建立,咱們在3.4節「經典指令解析之實例建立」中已有詳細的本質分析,詳細狀況請參閱。 8.3.3 字符串恆定性 字符串恆定性(Immutability),是指字符串一經建立,就不可改變。這是String對象最爲重要的特性之一,是CLR高度集成String以提升其性能的考慮。具體而言,字符串一旦建立,就會在託管堆上分配一塊連續的內存空間,咱們對其的任何改變都不會影響到原String對象,而是從新建立出新的String對象,例如: public static void Main() { string str = "This is a test about immutablitity of string type."; Console.WriteLine(str.Insert(0, "Hi, ").Substring(19).ToUpper()); Console.WriteLine(str); } 在上例中,咱們對str對象完成一系列的修改:增長、取子串和大寫格式改變等操做,從結果輸出上來看str依然保持原來的值不變。而Insert、Substring和ToUpper方法都會建立出新的臨時字符串,而這些新對象不被其餘代碼所引用,所以成爲下次垃圾回收的目標,從而形成了性能上的損失。
之因此特殊化處理String具備恆定性的特色,源於CLR對其的處理機制:String類型是不變模式在.NET中的典型應用,String對象從應用角度體現了值類型語義,而從內存角度實現爲引用類型存儲,位於託管堆。 對象恆定性,爲程序設計帶來了極大的好處,主要包括爲: l 保證對String對象的任意操做不會改變原字符串。 l 恆定性還意味着操做字符串不會出現線程同步問題。 l 恆定性必定程度上,成就了字符串駐留。 對象恆定性,還意味着String類型必須爲密封類,例如String類型的定義爲: public sealed class String : IComparable, ICloneable, IConvertible, Icomparable <string>, IEnumerable<char>, IEnumerable, IEquatable<string> 若是能夠在子類中繼承String類型,則必然有可能破壞CLR對String類型的特殊處理機制,也會破壞String類型的恆定性。 8.3.4 字符串駐留 關於字符串駐留,咱們以一個簡單的示例開始: class StringInterning { public static void Main() { string strA = "abcdef"; string strB = "abcdef"; Console.WriteLine(ReferenceEquals(strA, strB)); string strC = "abc"; string strD = strC + "def"; Console.WriteLine(ReferenceEquals(strA, strD)); strD = String.Intern(strD); Console.WriteLine(ReferenceEquals(strA, strD));
} } //執行結果: //True //False //True 上述示例,會給咱們三個意外,也是關於執行結果的意外:首先,strA和strB爲兩個不一樣的String對象,按照通常的分析兩次建立的不一樣對象,CLR將爲其在託管堆分配不一樣的內存塊,而ReferenceEquals方法用於判斷兩個引用是否指向同一對象實例,從結果來看strA和strB顯然指向了同一內存地址;其次,strD和strA在內容上也是同樣的,然而其ReferenceEquals方法返回的結果爲False,顯然strA和strD並無指向相同的內存塊;最後,以靜態方法Intern操做strD後,兩者又指向了相同的對象,ReferenceEquals方法又返回True。 要想解釋以上疑惑,只有請字符串駐留(String Interning)登場了。下面咱們經過對字符串駐留技術的分析,來一步一步解開上述示例的種種疑惑。 緣起 String類型區別於其餘類型的最大特色是其恆定性。對字符串的任何操做,包括字符串比較,字符串連接,字符串格式化等會建立新的字符串,從而伴隨着性能與內存的雙重損耗。而String類型自己又是.NET中使用最頻繁、應用最普遍的基本類型,所以CLR有必要有針對性的對其性能問題,採起特殊的解決辦法。 事實上,CLR以字符串駐留機制來解決這一問題:對於相同的字符串,CLR不會爲其分別分配內存空間,而是共享同一內存。所以,有兩個問題顯得尤其重要: l 一方面,CLR必須提供特殊的處理結構,來維護對相同字符串共享內存的機制。 l 另外一方面,CLR必須經過查找來添加新構造的字符串對象到其特定結構中。 的確如此,CLR內部維護了一個哈希表(Hash Table)來管理其建立的大部分string對象。其中,Key爲string自己,而Value爲分配給對應的string的內存地址。咱們以一個簡單的圖例(圖8-1)來講明這一問題。
圖8-1 string的內存概況 細節 咱們一步一步分析上述示例的執行過程,而後才能從整體上對字符串駐留機制有所瞭解。 string strA = "abcdef"; CLR初始化時,會建立一個空哈希表,當JIT編譯方法時,會首先在哈希表中查找每個字符串常量,顯然第一次它不會找到任何「abcdef」常量,所以會在託管堆中建立一個新的string對象strA,並在哈希表中建立一個Key-Value對,將「abcdef」串賦給Key,而將strA對象的引用賦給Value,也就是說Value內保持了指向「abcdef」字符串在託管堆中的引用地址。這樣就完成了第一次字符串的建立過程。 string strB = "abcdef"; 程序接着運行,JIT根據「abcdef」在哈希表中逐個查找,結果找到了該字符串,因此JIT不會執行任何操做,只是把找到的Key-Value對的Value值賦給strB對象。由此可知,strA和strB具備相同的內存引用,因此ReferenceEquals方法固然返回true。 string strC = "abc"; string strD = strC + "def"; 接着,JIT以相似的過程來向哈希表中添加了「abc」字符串,並將引用返回給strC對象;可是strD對象的建立過程又有所區別,由於strD是動態生成的字符串,這樣的字符串是不會被添加到哈希表中維護的,所以以ReferenceEquals來比較strA和strD會返回false。
對於動態生成的字符串,由於沒有添加到CLR內部維護的哈希表而使字符串駐留機制失效。可是,當咱們須要高效的比較兩個字符串是否相等時,能夠手工啓用字符串駐留機制,這就是調用String類型的兩個靜態方法,它們是: public static string Intern(string str); public static string IsInterned(string str); 兩者的處理機制都是在哈希表中查找是否存在str參數字符串,若是找到就返回已存在的String對象的引用,不然Intern方法將該str字符串添加到哈希表中,並返回引用;而IsInterned方法則不會向哈希表中添加字符串,而只是返回null。例如, strD = String.Intern(strD); Console.WriteLine(ReferenceEquals(strA, strD)); 咱們就很容易解釋上述代碼的執行結果了。 補充 綜上所述,當一個引用字符串的方法被編譯時,全部的字符串常量都會被以這種方式添加到該哈希表中,可是動態生成的字符串並未執行字符串駐留機制。值得注意的是,下面的代碼執行結果又會有所不一樣: public static void Main() { string strA = "abcdef"; string strC = "abc"; string strD = strC + "def"; Console.WriteLine(ReferenceEquals(strA, strD)); string strE = "abc" + "def"; Console.WriteLine(ReferenceEquals(strA, strE)); } 由結果可知,strA和strD指向不一樣的對象;而strA與strE指向相同的對象。咱們將上述代碼翻譯爲IL代碼: IL_0001: ldstr "abcdef"
IL_0006: stloc.0 IL_0007: ldstr "abc" IL_000c: stloc.1 IL_000d: ldloc.1 IL_000e: ldstr "def" IL_0013: call string [mscorlib]System.String::Concat(string, string) ……部分省略…… IL_0026: ldstr "abcdef" IL_002b: stloc.3 由IL分析可知,動態生成字符串時,CLR調用了System::Concat來執行字符串連接;而直接賦值strE = 「abc」 + 「def」的操做,編譯器會自動將其鏈接爲一個文本常量加載,所以會添加到內部哈希表中,這也是爲何最後strA和strE指向同一對象的緣由了。 最後,須要特別指出的是:字符串駐留是進程級的,能夠跨應用程序域(AppDomain)而存在。垃圾回收不能釋放哈希表中引用的字符串對象,只有進程結束這些對象纔會被釋放。所以,String類型的特殊性還表如今同一個字符串對象能夠在不一樣的應用程序域中被訪問,從而突破了AppDomain的隔離機制,其緣由仍是源於字符串的恆定性,由於是不可變的,因此根本沒有必要再隔離。 8.3.5 字符串操做典籍 本節從幾個相對孤立的角度來描述String類型,包括了不一樣操做、經常使用方法和典型問題幾個方面。 1.字符串類型與其餘基元類型的轉換 String類型能夠與其餘基本類型直接進行轉換,在此以System.Double類型與System.String類型的轉換爲例,來簡要說明兩者轉換的幾個簡單的方法及其區別。 Double類型轉換爲String類型: Double num = 123.456;
string str = num.ToString(); Double類型覆寫了ToString方法用於返回對象的值。 String類型轉換爲Double類型,有多種方法可供選擇: string str = "123.456"; Double num= 0.0; num = Double.Parse(str); Double.TryParse(str, out num); num = Convert.ToDouble(str); 這三種方法的區別主要是對異常的處理機制上:若是轉換失敗,則Parse方法總會拋出異常,主要包括ArgumentNullException、OverflowException、FormatException等;TryParse則不會拋出任何異常,而返回false標誌解析失敗;Convert方法在str爲null時不會拋出異常,而是返回0。 其餘的基元類型,例如Int3二、Char、Byte、Boolean、Single等均提供了上述方法實現與String類型進行必定程度的轉換,同時對於特定的格式化轉換能夠參考上述方法的各個重載版本,限於篇幅,此不贅述。 2.轉義字符和字面字符串 l 使用轉義字符來實現特定格式字符串 對於在C++等語言中熟悉的轉義字符串,在.NET中一樣適用,例如C#語言提供了相應的實現版本: string strName = "Name:\n\t\"小雨\""; 上述示例實現了回車和Tab空格操做,併爲「小雨」添加了雙引號。 l 在文件和目錄路徑、數據庫鏈接字符串和正則表達式中普遍應用的字面字符串(verbatim string),爲C#提供了聲明字符串的特殊方式,用於將引號之間的全部字符視爲字符串的一部分,例如: string strPath = @"C:\Program Files \Mynet.exe"; 上述代碼,徹底等效於:
string strPath = "C:\\Program Files \\Mynet.exe"; 而如下代碼則致使被提示「沒法識別的轉義序列」的編譯錯誤: string strPath = "C:\Program Files \Mynet.exe"; 顯然,以@實現的字面字符串更具可讀性,克服了轉義字符串帶來的閱讀障礙。 3.關於string和System.String string與System.String經常使不少初學者感到困惑。實際上,string和System.String編譯爲IL代碼時,會生成徹底相同的代碼。那麼關於string和System.String咱們應該瞭解的是其概念上的細微差異。 l string爲C#語言的基元類型,相似於int、char和long等其餘C#基元類型,基元類型簡化了語言代碼,帶來簡便的可讀性,不一樣高級語言對同一基元類型的標識符可能有所不一樣。 l System.String是框架類庫(FCL)的基本類型,string和System.String有直接的映射關係。 l 從IL角度來看,string和System.String之間沒有任何不一樣。一樣的狀況,還存在於其餘的基元類型,例如:int和System.Int32,long和System.Int64,float和System.Single,以及object和System.Object等。 4.String類型參數的傳遞問題 有一個足以引發關注的問題是,String類型做爲參數傳遞時,以按值傳遞和按引用傳遞時所表現的不一樣: class StringArgument { public static void Main() { string strA = "String A"; string strB = "String B"; //參數爲String類型的按值傳遞(strA)和按引用傳遞(strB) ChangeString(strA, ref strB); Console.WriteLine(strA);
Console.WriteLine(strB); } private static void ChangeString(string stra, ref string strb) { stra = "Changing String A"; strb = "Changing String B"; } } //執行結果 //String A //Changing String B String做爲典型的引用類型,其做爲參數傳遞也表明了典型的引用類型按值傳遞和按引用傳遞的區別,能夠小結爲: l 默認狀況爲按值傳遞,strA參數所示,傳遞strA的值,也就是指向「String A」的引用; l ref標識了按引用傳遞,strB參數所示,傳遞的是原引用的引用,也就是傳遞一個到strB自己的引用,這區別於到「String B」的引用這個概念,兩者不是相同的概念。 所以,默認狀況下,string類型也是按值傳遞的,只是這個「值」是指向字符串實例的引用而已,關於參數傳遞的詳細描述請參考4.3節「參數之惑---傳遞的藝術」。 5.其餘經常使用方法 表8-1對System.String的經常使用方法作以簡單說明,而不以示例展開,這些方法普遍的應用在日常的字符串處理操做中,所以有必要作以說明。 表8-1 System.String類型的經常使用方法
經常使用方法
方法說明
ToString
ToString方法是System.Object提供的虛方法,用於返回對象的字符串表達形式,能夠獲取格式化或者帶有語言文化信息的實例信息
SubString
用於獲取子字符串,FCL提供了兩個重載版本,能夠指定起始位置和長度
Split
返回包含此實例中由指定Char或者String元素隔開的子字符串的 String 數組
StartsWith、EndsWith
StartsWith用於判斷字符串是否以指定內容開始;而EndsWith用於判斷字符串是否以指定內容結尾
ToUpper、ToLower
ToUpper用於返回實例的大寫版本;而ToLower用於返回實例的小寫版本
IndexOf、LastIndexOf
IndexOf用於返回匹配項的第一個的索引位置;LastIndexOf用於返回匹配項的最後一個索引位置
Insert、Remove
Insert用於向指定位置插入指定的字符串;Remove用於從實例中刪除指定個數的字符串
Trim、TrimStart、TrimEnd
Trim方法用於從實例開始和末尾位置,移除指定字符的全部匹配項;TrimStart用於從實例開始位置,移除指定字符的全部匹配項;TrimEnd用於從實例
結束位置,移除指定字符的全部匹配項
Copy、CopyTo
Copy爲靜態方法,CopyTo爲實例方法,都是用於拷貝實例內容給新的String對象。其中CopyTo方法能夠指定起始位置,拷貝個數等信息
Compare、CompareOrdinal、CompareTo
Compare爲靜態方法,用於返回兩個字符串間的排序狀況,而且容許指定語言文化信息;CompareOrdinal爲靜態方法,按照字符串中的碼值比較字符集,並返回比較結果,爲0表示結果相等,爲負表示第一個字符串小,爲正表示第一個字符串大;而CompareTo是實例方法,用於返回兩個字符串的排序,不容許指定語言文化信息,由於該方法老是使用當前線程相關聯的語言文化信息
Concat、Join
均爲靜態方法。Concat用於鏈接一個或者多個字符串;Join用於以指定分隔符來串聯String數組的各個元素,並返回新的String實例
Format
靜態方法。用於格式化String對象爲指定的格式或語言文化信息 8.3.6 補充的禮物:StringBuilder String對象是恆定不變的,而System.Text.StringBuilder對象表示的字符串是可變的。StringBuilder是.NET提供的動態建立String對象的高效方式,以克服String對象恆定性帶來的性能影響,克服了對String對象進行屢次修改帶來的建立大量String對象的問題。所以,咱們首先將兩者的執行性能作以簡單的比較: public static void Main() { #region 性能比較 Stopwatch sw = Stopwatch.StartNew(); //String性能測試 string str = ""; for (int i = 0; i < 10000; i++) str += i.ToString(); sw.Stop(); Console.WriteLine(sw.ElapsedMilliseconds); //StringBuilder性能測試 sw.Reset(); sw.Start(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) sb.Append(i.ToString()); sw.Stop(); Console.WriteLine(sw.ElapsedMilliseconds); #endregion
} //執行結果 //422 //3 建立一樣的字符串過程,執行結果有百倍之多的性能差異,並且這種差異會隨着累加次數的增長而增長。所以,基於性能的考慮,咱們應該儘量使用StringBuilder來動態建立字符串,而後以ToString方法將其轉換爲String對象應用。StringBuilder內部有一個指向Char數值的字段,StringBuilder正是經過操做該字符數組而實現高效的處理機制。 1.構造StringBuilder StringBuilder對象的實例化沒有什麼特殊可言,與其餘對象實例化同樣,典型的構造方式爲: StringBuilder sb = new StringBuilder("Hello, word.", 20); 其中,第二個參數表示容量,也就是StringBuilder所維護的字符數組的長度,默認爲16,能夠設定其爲合適的長度來避免沒必要要的垃圾回收;還有一個概念爲最大容量,表示字符串所能容納字符的最大個數,默認爲Int32.MaxValue,對象建立時一經設定就不可更改;字符串長度表示當前StringBuilder對象的字符數組長度,能夠使用Length屬性來獲取和設定當前的StringBuilder長度。 2.StringBuilder的經常使用方法 (1)ToString方法 返回一個StringBuilder中字符數組字段的String,由於沒必要拷貝字符數組,因此執行效率很高,是最經常使用的方法之一。不過,值得注意的是,在調用了StringBuilder的ToString方法以後,都會致使StringBuilder從新分配和建立新的字符數組,由於ToString方法返回的String必須是恆定的。 (2)Append/AppendFormat方法 用於將文本或者對象字符串添加到當前StringBuilder字符數組中,例如: StringBuilder sbs = new StringBuilder("Hello, "); sbs.Append("Word."); Console.WriteLine(sbs);
//執行結果 //Hello, Word. 而AppendFormat方法進一步實現了IFormattable接口,可接受IFormatProvider類型參數來實現可格式化的字符串信息,例如: StringBuilder formatStr = new StringBuilder("The price is "); formatStr.AppendFormat("{0:C}", 22); formatStr.AppendFormat("\r\nThe Date is {0:D}", DateTime.Now.Date); Console.WriteLine(formatStr); (3)Insert方法 用於將文本或字符串對象添加到指定位置,例如: StringBuilder mysb = new StringBuilder("My name XiaoWang"); mysb.Insert(8, "is "); Console.WriteLine(mysb); //執行結果 //My name is XiaoWang (4)Replace方法 Replace方法是一種重要的字符串操做方法,用來將字符串數組中的一個字符或字符串替換爲另一個字符或字符串,例如: StringBuilder sb = new StringBuilder("I love game."); sb.Replace("game", ".NET"); Console.WriteLine(sb); //執行結果 //I love .NET. 限於篇幅,咱們再也不列舉其餘方法,例如Remove、Equals、AppendLine等,留於讀者本身來探索StringBuilder帶來的快捷操做。 3.再論性能 StringBuilder有諸多的好處,是否能夠代替String呢?基於這個問題咱們有以下的對比性分析:
l String是恆定的;而StringBuilder是可變的。 l 對於簡單的字符串鏈接操做,在性能上StringBuilder不必定老是優於String。由於StringBuilder對象的建立代價較大,在字符串鏈接目標較少的狀況下,過分濫用StringBuilder會致使性能的浪費而非節約。只有大量的或者沒法預知次數的字符串操做,才考慮以StringBuilder來實現。事實上,本節開始的示例若是將鏈接次數設置爲一百次之內,就根本看不出兩者的性能差異。 l String類型的「+」鏈接操做,其實是重載操做符「+」調用String.Concat來操做,而編譯器則會優化這種鏈接操做的處理,編譯器根據其傳入參數的個數,一次性分配相應的內存,並依次拷入相應的字符串。 l StringBuilder在使用上,最好指定合適的容量值,不然因爲默認容量不足而頻繁的進行內存分配操做,是不妥的實現方法。 l 一般狀況下,進行簡單字符串鏈接時,應該優先考慮使用String.Concat和String.Join等操做來完成字符串的鏈接,可是應該留意String.Concat可能存在的裝箱操做。 8.3.7 結論 最後,回答爲何特殊? String類型是全部系統中使用最頻繁的類型,以至於CLR必須考慮爲其實現特定的實現方式,例如System.Object基類就提供了ToString虛方法,一切.NET類型均可以使用ToString方法來獲取對象的字符串表達。所以,String類型緊密地集成於CLR,CLR能夠直接訪問String類型的內存佈局,以一系列解決方案來優化其執行。
8.4 簡易不簡單:認識枚舉 本節將介紹如下內容: — 枚舉類型全解 — 位標記應用 — 枚舉應用規則
8.4.1 引言 在哪裏能夠看到枚舉?打開每一個文件的屬性,咱們會看到只讀、隱藏的選項;操做一個文件時,你能夠採用只讀、可寫、追加等模式;設置系統級別時,你可能會選擇緊急、普通和不緊急來定義。這些各式各樣的信息中,一個共同的特色是信息的狀態分類相對穩定,在.NET中能夠選擇以類的靜態字段來表達這種簡單的分類結構,可是更明智的選擇顯然是:枚舉。 事實上,在.NET中有大量的枚舉來表達這種簡單而穩定的結構,FCL中對文件屬性的定義爲System.IO.FileAttributes枚舉,對字體風格的定義爲System.Drawing.FontStyle枚舉,對文化類型定義爲System.Globlization.CultureType枚舉。除了良好的可讀性、易於維護、強類型的優勢以外,性能的考慮也佔了一席之地。 關於枚舉,在本節會給出詳細而全面的理解,認識枚舉,從一點一滴開始。 8.4.2 枚舉類型解析 1.類型本質 全部枚舉類型都隱式並且只能隱式地繼承自System.Enum類型,System.Enum類型是繼承自System.ValueType類型惟一不爲值類型的引用類型。該類型的定義爲: public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible 從該定義中,咱們能夠得出如下結論: l System.Enum類型是引用類型,而且是一個抽象類。 l System.Enum類型繼承自System.ValueType類型,而ValueType類型是一切值類型的根類,可是顯然System.Enum並不是值類型,這是ValueType惟一的特例。 l System.Enum類型實現了IComparable、IFormattable和IConvertible接口,所以枚舉類型能夠與這三個接口實現類型轉換。 .NET之因此在ValueType之下實現一個Enum類型,主要是實現對枚舉類型公共成員與公共方法的抽象,任何枚舉類型都自動繼承了Enum中實現的方法。關於枚舉類型與Enum類型的關
系,能夠表述爲:枚舉類型是值類型,分配於線程的堆棧上,自動繼承於Enum類型,可是自己不能被繼承;Enum類型是引用類型,分配於託管堆上,Enum類型自己不是枚舉類型,可是提供了操做枚舉類型的共用方法。 下面咱們根據一個枚舉的定義和操做來分析其IL,以從中獲取關於枚舉的更多認識: enum LogLevel { Trace, Debug, Information, Warnning, Error, Fatal } 將上述枚舉定義用Reflector工具翻譯爲IL代碼,對應爲: .class private auto ansi sealed LogLevel extends [mscorlib]System.Enum { .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Debug = int32(1) .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Error = int32(4) .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Fatal = int32(5) .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Information = int32(2) .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Trace = int32(0) .field public specialname rtspecialname int32 value__ .field public static literal valuetype InsideDotNet.Framework.EnumEx.LogLevel Warnning = int32(3) }
從上述IL代碼中,LogLevel枚舉類型的確繼承自System.Enum類型,而且編譯器自動爲各個成員映射一個常數值,默認從0開始,逐個加1。所以,在本質上枚舉就是一個常數集合,各個成員常量至關於類的靜態字段。 而後,咱們對該枚舉類型進行簡單的操做,以瞭解其運行時信息,例如: public static void Main() { LogLevel logger = LogLevel.Information; Console.WriteLine("The log level is {0}.", logger); } 該過程實例化了一個枚舉變量,並將它輸出到控制檯,對應的IL爲: .method public hidebysig static void Main() cil managed { .entrypoint .maxstack 2 .locals init ( [0] valuetype InsideDotNet.Framework.EnumEx.LogLevel logger) L_0000: nop L_0001: ldc.i4.2 L_0002: stloc.0 L_0003: ldstr "The log level is {0}." L_0008: ldloc.0 L_0009: box InsideDotNet.Framework.EnumEx.LogLevel L_000e: call void [mscorlib]System.Console::WriteLine(string, object) L_0013: nop L_0014: ret } 分析IL可知,首先將2賦值給logger,而後執行裝箱操做(L_0009),再調用WriteLine方法將結果輸出到控制檯。
2.枚舉規則 討論了枚舉的本質,咱們再回過頭來,看看枚舉類型的定義及其規則,例以下面的枚舉定義略有不一樣: enum Week: int { Sun = 7, Mon = 1, Tue, Wed, Thur, Fri, Sat, Weekend = Sun } 根據以上定義,咱們瞭解關於枚舉的種種規則,這些規則是定義枚舉和操做枚舉的基本綱領,主要包括: l 枚舉定義時能夠聲明其基礎類型,例如本例Week枚舉的基礎類型指明爲int型,默認狀況時即爲int。經過指定類型限定了枚舉成員的取值範圍,而被指定爲枚舉聲明類型的只能是除char外的8種整數類型:byte、sbyte、short、ushort、int、uint、long和ulong,聲明其餘的類型將致使編譯錯誤,例如Int1六、Int64。 l 枚舉成員是枚舉類型的命名常量,任意兩個枚舉常量不能具備一樣的名稱符號,可是能夠具備相同的關聯值。 l 枚舉成員會顯式或者隱式與整數值相關聯,默認狀況下,第一個元素對應的隱式值爲0,而後各個成員依次遞增1。還能夠經過顯式強制指定,例如Sun爲7,Mon爲1,而Tue則爲2,而且成員Weekend和Sun則關聯了相同的枚舉值。 l 枚舉成員能夠自由引用其餘成員的設定值,可是必定注意避免循環定義,不然將引起編譯錯誤,例如: enum MusicType
{ Blue, Jazz = Pop, Pop } 編譯器將沒法確知成員Jazz和Pop的設定值到底爲多少。 l 枚舉是一種特殊的值類型,不能定義任何的屬性、方法和事件,枚舉類型的屬性、方法和事件都繼承自System.Enum類型。 l 枚舉類型是值類型,能夠直接經過賦值進行實例化,例如: Week myweek = Week.Mon; 也能夠以new關鍵字來實例化,例如: Week myweek = new Week(); 值得注意的是,此時myweek並不等於Week枚舉類型中定義的第一個成員的Sun的關聯值7,而是等效於字面值爲0的成員項。若是枚舉成員不存在0值常數,則myweek將默認設定爲0,能夠從下面代碼來驗證這一規則: enum WithZero { First = 1, Zero = 0 } enum WithNonZero { First = 1, Second } class EnumMethod { public static void Main() { WithZero wz = new WithZero(); Console.WriteLine(wz.ToString("G")); WithNonZero wnz = new WithNonZero(); Console.WriteLine(wnz.ToString("G")); } } //執行結果
//Zero //0 所以,以new關鍵字來實例化枚舉類型,並不是好的選擇,一般狀況下咱們應該避免這種操做方式。 l 枚舉能夠進行自增自減操做,例如: Week day = (Week)3; day++; Console.WriteLine(day.ToString()); 經過自增運算,上述代碼輸出結果將爲:Fri。 8.4.3 枚舉種種 1.類型轉換 (1)與整型轉換 由於枚舉類型本質上是整數類型的集合,所以能夠與整數類型進行相互的類型轉換,可是這種轉換必須是顯式的。 //枚舉轉換爲整數 int i = (int)Week.Sun; //將整數轉換爲枚舉 Week day = (Week)3; 另外,Enum還實現了Parse方法來間接完成整數類型向枚舉類型的轉換,例如: //或使用Parse方法進行轉換 Week day = (Week)Enum.Parse(typeof(Week), "2"); (2)與字符串的映射 枚舉與String類型的轉換,實際上是枚舉成員與字符串表達式的相互映射,這種映射主要經過Enum類型的兩個方法來完成:
l ToString實例方法,將枚舉類型映射爲字符串表達形式。能夠經過指定格式化標誌來輸出枚舉成員的特定格式,例如「G」表示返回普通格式、「X」表示返回16進制格式,而本例中的「D」則表示返回十進制格式。 l Parse靜態方法,將整數或者符號名稱字符串轉換爲等效的枚舉類型,轉換不成功則拋出ArgumentException異常,例如: Week myday = (Week)Enum.Parse(typeof(Week), "Mon", true); Console.WriteLine(myday); 所以,Parse以前最好應用IsDefined方法進行有效性判斷。對於關聯相同整數值的枚舉成員,Parse方法將返回第一個關聯的枚舉類型,例如: Week theDay = (Week)Enum.Parse(typeof(Week), "7"); Console.WriteLine(theDay.ToString()); //執行結果 //Sun (3)不一樣枚舉的相互轉換 不一樣的枚舉類型之間能夠進行相互轉換,這種轉換的基礎是枚舉成員本質爲整數類型的集合,所以其過程至關於將一種枚舉轉換爲值,而後再將該值映射到另外一枚舉的成員。 MusicType mtToday = MusicType.Jazz; Week today = (Week)mtToday; (4)與其它引用類型轉換 除了能夠顯式的與8種整數類型進行轉換以外,枚舉類型是典型的值類型,能夠向上轉換爲父級類和實現的接口類型,而這種轉換實質發生了裝箱操做。小結枚舉可裝箱的類型主要包括:System.Object、System.ValueType、System.Enum、System.IComparable、System.IFormattable和System.IConvertible。例如: IConvertible iConvert = (IConvertible)MusicType.Jazz; Int32 x = iConvert.ToInt32(CultureInfo.CurrentCulture); Console.WriteLine(x);
1.經常使用方法 System.Enum類型爲枚舉類型提供了幾個值得研究的方法,這些方法是操做和使用枚舉的利器,因爲System.Enum是抽象類,Enum方法大都是靜態方法,在此僅舉幾個簡單的例子點到爲止。 以GetNames和GetValues方法分別獲取枚舉中符號名稱數組和全部符號的數組,例如: //由GetName獲取枚舉常數名稱的數組 foreach (string item in Enum.GetNames(typeof(Week))) { Console.WriteLine(item.ToString()); } //由GetValues獲取枚舉常數值的數組 foreach (Week item in Enum.GetValues(typeof(Week))) { Console.WriteLine("{0} : {1}", item.ToString("D"), item.ToString()); } 應用GetValues方法或GetNames方法,能夠很容易將枚舉類型與數據顯式控件綁定來顯式枚舉成員,例如: ListBox lb = new ListBox(); lb.DataSource = Enum.GetValues(typeof(Week)); this.Controls.Add(lb); 以IsDefined方法來判斷符號或者整數存在於枚舉中,以防止在類型轉換時的越界狀況出現。 if(Enum.IsDefined(typeof(Week), "Fri")) { Console.WriteLine("Today is {0}.", Week.Fri.ToString("G")); } 以GetUnderlyingType靜態方法,返回枚舉實例的聲明類型,例如: Console.WriteLine(Enum.GetUnderlyingType(typeof(Week)));
8.4.4 位枚舉 位標記集合是一種由組合出現的元素造成的列表,一般設計爲以「位或」運算組合新值;枚舉類型則一般表達一種語義相對獨立的數值集合。而以枚舉類型來實現位標記集合是最爲完美的組合,簡稱爲位枚舉。在.NET中,須要對枚舉常量進行位運算時,一般以System.FlagsAttribute特性來標記枚舉類型,例如: [Flags] enum ColorStyle { None = 0x00, Red = 0x01, Orange = 0x02, Yellow = 0x04, Greeen = 0x08, Blue = 0x10, Indigotic = 0x20, Purple = 0x40, All = Red | Orange | Yellow | Greeen | Blue | Indigotic | Purple } FlagsAttribute特性的做用是將枚舉成員處理爲位標記,而不是孤立的常數,例如: public static void Main() { ColorStyle mycs = ColorStyle.Red | ColorStyle.Yellow | ColorStyle.Blue; Console.WriteLine(mycs.ToString()); } 在上例中,mycs實例的對應數值爲21(十六進制0x15),而覆寫的ToString方法在ColorStyle枚舉中找不到對應的符號。而FlagsAttribute特性的做用是將枚舉常數當作一組位標記來操做,從而影響ToString、Parse和Format方法的執行行爲。在ColorStyle定義中0x15顯然由0x0一、0x04和0x10組合而成,示例的結果將返回:Red, Yellow, Blue,而非21,緣由正在於此。
位枚舉首先是一個枚舉類型,所以具備通常枚舉類型應有的全部特性和方法,例如繼承於Enum類型,實現了ToString、Parse、GetValues等方法。可是因爲位枚舉的特殊性質,所以應用於某些方法時,應該留意其處理方式的不一樣之處。這些區別主要包括: l Enum.IsDefined方法不能應對位枚舉成員,正如前文所言位枚舉區別與普通枚舉的重要表現是:位枚舉不具有排他性,成員之間能夠經過位運算進行組合。而IsDefined方法只能應對已定義的成員判斷,而沒法處理組合而成的位枚舉,所以結果將老是返回false。例如: Enum.IsDefined(typeof(ColorStyle), 0x15) Enum.IsDefined(typeof(ColorStyle), "Red, Yellow, Blue") MSDN中給出瞭解決位枚舉成員是否認義的判斷方法:就是將該數值與枚舉成員進行「位與」運算,結果不爲0則表示該變量中包含該枚舉成員,例如: if ((mycs & ColorStyle.Red) != 0) Console.WriteLine(ColorStyle.Red + " is in ColorStyle"); l Flags特性影響ToString、Parse和Format方法的執行過程和結果。 l 若是不使用FlagsAttribute特性來標記位枚舉,也能夠在ToString方法中傳入「F」格式來得到一樣的結果,以「D」、「G」等標記來格式化處理,也能得到相應的輸出格式。 l 在位枚舉中,應該顯式的爲每一個枚舉成員賦予有效的數值,而且以2的冪次方爲單位定義枚舉常量,這樣能保證明現枚舉常量的各個標誌不會重疊。固然你也能夠指定其它的整數值,可是應該注意指定0值做爲成員常數值時,「位與」運算將老是返回false。 8.4.5 規則與意義 l 枚舉類型使代碼更具可讀性,理解清晰,易於維護。在Visual Stuido 2008等編譯工具中,良好的智能感知爲咱們進行程序設計提供了更方便的代碼機制。同時,若是枚舉符號和對應的整數值發生變化,只需修改枚舉定義便可,而沒必要在漫長的代碼中進行修改。 l 枚舉類型是強類型的,從而保證了系統安全性。而以類的靜態字段實現的相似替代模型,不具備枚舉的簡單性和類型安全性。例如: public static void Main()
{ LogLevel log = LogLevel.Information; GetCurrentLog(log); } private static void GetCurrentLog(LogLevel level) { Console.WriteLine(level.ToString()); } 試圖爲GetCurrentLog方法傳遞整數或者其餘類型參數將致使編譯錯誤,枚舉類型保證了類型的安全性。 l 枚舉類型的默認值爲0,所以,一般給枚舉成員包含0值是有意義的,以免0值遊離於預約義集合,致使枚舉變量保持非預約義值是沒有意義的。另外,位枚舉中與0值成員進行「位與」運算將永遠返回false,所以不能將0值枚舉成員做爲「位與」運算的測試標誌。 l 枚舉的聲明類型,必須是基於編譯器的基元類型,而不能是對應的FCL類型,不然將致使編譯錯誤。 8.4.6 結論 枚舉類型在BCL中佔有一席之地,說明了.NET框架對枚舉類型的應用是普遍的。本節力圖從枚舉的各個方面創建對枚舉的全面認知,經過枚舉定義、枚舉方法和枚舉應用幾個角度來闡釋一個看似簡單的概念,對枚舉的理解與探索更進了一步。
8.5 一脈相承:委託、匿名方法和Lambda表達式 本節將介紹如下內容: — 委託 — 事件
— 匿名方法 — Lambda表達式 8.5.1 引言 委託,實現了類型安全的回調方法。在.NET中回調無處不在,因此委託也無處不在,事件模型創建在委託機制上,Lambda表達式本質上就是一種匿名委託。本節中將完成一次關於委託的旅行,全面闡述委託及其核心話題,逐一梳理委託、委託鏈、事件、匿名方法和Lambda表達式。 8.5.2 解密委託 1.委託的定義 瞭解委託,從其定義開始,一般一個委託被聲明爲: public delegate void CalculateDelegate(Int32 x, Int32 y); 關鍵字delegate用於聲明一個委託類型CalculateDelegate,能夠對其添加訪問修飾符,默認其返回值類型爲void,接受兩個Int32型參數x和y,可是委託並不等同與方法,而是一個引用類型,相似於C++中的函數指針,稍後在委託本質裏將對此有所交代。 下面的示例將介紹如何經過委託來實現一個計算器模擬程序,在此基礎上來了解關於委託的定義、建立和應用: class DelegateEx { //聲明一個委託 public delegate void CalculateDelegate(Int32 x, Int32 y); //建立與委託關聯的方法,兩者具備相同的返回值類型和參數列表 public static void Add(Int32 x, Int32 y) { Console.WriteLine(x + y); }
//定義委託類型變量 private static CalculateDelegate myDelegate; public static void Main() { //進行委託綁定 myDelegate = new CalculateDelegate(Add); //回調Add方法 myDelegate(100, 200); } } 上述示例,在類DelegateEx內部聲明瞭一個CalculateDelegate委託類型,它具備和關聯方法Add徹底相同的返回值類型和參數列表,不然將致使編譯時錯誤。將方法Add傳遞給CalculateDelegate構造器,也就是將方法Add指派給CalculateDelegate委託,並將該引用賦給myDelegate變量,也就表示myDeleage變量保存了指向Add方法的引用,以此實現對Add的回調。 因而可知,委託表示了對其回調方法的簽名,能夠將方法看成參數進行傳遞,並根據傳入的方法來動態的改變方法調用。只要爲委託提供相同簽名的方法,就能夠與委託綁定,例如: public static void Subtract(Int32 x, Int32 y) { Console.WriteLine(x - y); } 一樣,能夠將方法Subtract分配給委託,經過參數傳遞實現方法回調,例如: public static void Main() { //進行委託綁定 myDelegate = new CalculateDelegate(Subtract); myDelegate(100, 200); }
2.多播委託和委託鏈 在上述委託實現中,Add方法和Subtract能夠綁定於同一個委託類型myDelegate,由此能夠很容易想到將多個方法綁定到一個委託變量,在調用一個方法時,能夠依次執行其綁定的全部方法,這種技術稱爲多播委託。在.NET中提供了至關簡潔的語法來建立委託鏈,以+=和-=操做符分別進行綁定和解除綁定的操做,多個方法綁定到一個委託變量就造成一個委託鏈,對其調用時,將會依次調用全部綁定的回調方法。例如: public static void Main() { myDelegate = new CalculateDelegate(Add); myDelegate += new CalculateDelegate(Subtract); myDelegate += new CalculateDelegate(Multiply); myDelegate(100, 200); } 上述執行將在控制檯依次輸出300、-100和20000三個結果,可見多播委託按照委託鏈順序調用全部綁定的方法,一樣以-=操做能夠解除委託鏈上的綁定,例如: myDelegate -= new CalculateDelegate(Add); myDelegate(100, 200); 結果將只有-100和20000被輸出,可見經過-=操做解除了Add方法。 事實上,+=和-=操做分別調用了Deleagate.Combine和Deleagate.Remove方法,由對應的IL可知: .method public hidebysig static void Main() cil managed { .entrypoint // 代碼大小 151 (0x97) .maxstack 4 IL_0000: nop IL_0001: ldnull IL_0002: ldftn void InsideDotNet.NewFeature.CSharp3.DelegateEx::Add(int32, int32) //部分省略…… IL_0023: call class [mscorlib]System.Delegate [mscorlib]System.Delegate:: Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) //部分省略……
IL_0043: call class [mscorlib]System.Delegate [mscorlib]System.Delegate:: Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) //部分省略…… IL_0075: call class [mscorlib]System.Delegate [mscorlib]System.Delegate:: Remove(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) //部分省略…… IL_0095: nop IL_0096: ret } // end of method DelegateEx::Main 因此,上述操做實際等效於: public static void Main() { myDelegate = (CalculateDelegate)Delegate.Combine(new CalculateDelegate(Add), new CalculateDelegate(Subtract), new CalculateDelegate(Multiply)); myDelegate(100, 200); myDelegate = (CalculateDelegate)Delegate.Remove(myDelegate, new CalculateDelegate(Add)); myDelegate(100, 200); } 另外,多播委託返回值通常爲void,委託類型爲非void類型時,多播委託將返回最後一個調用的方法的執行結果,因此在實際的應用中不被推薦。 3.委託的本質 委託在本質上仍然是一個類,如此簡潔的語法正是由於CLR和編譯器在後臺完成了一系列操做,將上述CalculateDelegate委託編譯爲IL,你將會看得更加明白如圖8-2所示。 圖8-2 CalculateDelegate的IL分析
因此,委託本質上仍舊是一個類,該類繼承自System.MulticastDelegate類,該類維護一個帶有連接的委託列表,在調用多播委託時,將按照委託列表的委託順序而調用的。還包括一個接受兩個參數的構造函數和3個重要方法:BeginInvoke、EndInvoke和Invoke。 首先來了解CalculateDelegate的構造函數,它包括了兩個參數:第一個參數表示一個對象引用,它指向了當前委託調用回調函數的實例,在本例中即指向一個DelegateEx對象;第二個參數標識了回調方法,也就是Add方法。所以,在建立一個委託類型實例時,將會爲其初始化一個指向對象的引用和一個標識回調方法的整數,這是由編譯器完成的。那麼一個回調方法是如何被執行的,繼續以IL代碼來分析委託的調用,便可顯露端倪(在此僅分析委託關聯Add方法時的狀況): .method public hidebysig static void Main() cil managed { .entrypoint // 代碼大小 37 (0x25) .maxstack 8 IL_0000: nop IL_0001: ldnull IL_0002: ldftn void InsideDotNet.NewFeature.CSharp3.DelegateEx::Add(int32, int32) IL_0008: newobj instance void InsideDotNet.NewFeature.CSharp3.DelegateEx/ CalculateDelegate::.ctor(object, native int) IL_000d: stsfld class InsideDotNet.NewFeature.CSharp3.DelegateEx/ CalculateDelegate InsideDotNet.NewFeature.CSharp3.DelegateEx::myDelegate IL_0012: ldsfld class InsideDotNet.NewFeature.CSharp3.DelegateEx/Calculate Delegate InsideDotNet.NewFeature.CSharp3.DelegateEx::myDelegate IL_0017: ldc.i4.s 100 IL_0019: ldc.i4 0xc8 IL_001e: callvirt instance void InsideDotNet.NewFeature.CSharp3.DelegateEx/ CalculateDelegate::Invoke(int32, int32) IL_0023: nop
IL_0024: ret } // end of method DelegateEx::Main 在IL代碼中可見,首先調用CalculateDelegate的構造函數來建立一個myDelegate實例,而後經過CalculateDelegate::Invoke執行回調方法調用,可見真正執行調用的是Invoke方法。所以,你也能夠經過Invoke在代碼中顯示調用,例如: myDelegate.Invoke(100, 200); 其執行過程和隱式調用是同樣的,注意在.NET 1.0中C#編譯器是不容許顯示調用的,之後的版本中修正了這一限制。 另外,Invoke方法直接對當前線程調用回調方法,在異步編程環境中,除了Invoke方法,也會生成BeginInvoke和EndInvoke方法來完成必定的工做。這也就是委託類中另外兩個方法的做用。 8.5.3 委託和事件 .NET的事件模型創建在委託機制之上,透徹的瞭解了委託才能明白的分析事件。能夠說,事件是對委託的封裝,從委託的示例中可知,在客戶端能夠隨意對委託進行操做,必定程度上破壞了面向的對象的封裝機制,所以事件實現了對委託的封裝。 下面,經過將委託的示例進行改造,來完成一個事件的定義過程: public class Calculator { //定義一個CalculateEventArgs, //用於存放事件引起時向處理程序傳遞的狀態信息 public class CalculateEventArgs: EventArgs { public readonly Int32 x, y; public CalculateEventArgs(Int32 x, Int32 y) { this.x = x; this.y = y;
} } //聲明事件委託 public delegate void CalculateEventHandler(object sender,CalculateEventArgs e); //定義事件成員,提供外部綁定 public event CalculateEventHandler MyCalculate; //提供受保護的虛方法,能夠由子類覆寫來拒絕監視 protected virtual void OnCalculate(CalculateEventArgs e) { if (MyCalculate != null) { MyCalculate(this, e); } } //進行計算,調用該方法表示有新的計算髮生 public void Calculate(Int32 x, Int32 y) { CalculateEventArgs e = new CalculateEventArgs(x, y); //通知全部的事件的註冊者 OnCalculate(e); } } 示例中,對計算器模擬程序作了簡要的修改,從兩者的對比中能夠體會事件的完整定義過程,主要包括: l 定義一個內部事件參數類型,用於存放事件引起時向事件處理程序傳遞的狀態信息,EventArgs是事件數據類的基類。 l 聲明事件委託,主要包括兩個參數:一個表示事件發送者對象,一個表示事件參數類對象。 l 定義事件成員。
l 定義負責通知事件引起的方法,它被實現爲protected virtual方法,目的是能夠在派生類中覆寫該方法來拒絕監視事件。 l 定義一個觸發事件的方法,例如Calculate被調用時,表示有新的計算髮生。 一個事件的完整程序就這樣定義好了。而後,還須要定義一個事件觸發程序,用來監聽事件: //定義事件觸發者 public class CalculatorManager { //定義消息通知方法 public void Add(object sender, Calculator.CalculateEventArgs e) { Console.WriteLine(e.x + e.y); } public void Substract(object sender, Calculator.CalculateEventArgs e) { Console.WriteLine(e.x - e.y); } } 最後,實現一個事件的處理程序: public class Test_Calculator { public static void Main() { Calculator calculator = new Calculator(); //事件觸發者 CalculatorManager cm = new CalculatorManager(); //事件綁定 calculator.MyCalculate += cm.Add; calculator.Calculate(100, 200); calculator.MyCalculate += cm.Substract;
calculator.Calculate(100, 200); //事件註銷 calculator.MyCalculate -= cm.Add; calculator.Calculate(100, 200); } } 若是對設計模式有所瞭解,上述實現過程實質是Observer模式在委託中的應用,在.NET中對Observer模式的應用嚴格的遵照了相關的規範。在Windows Form程序開發中,對一個Button的Click就對應了事件的響應,例如: this.button1.Click += new System.EventHandler(this.button1_Click); 用於將button1_Click方法綁定到button1的Click事件上,當有按鈕被按下時,將會觸發執行button1_Click方法: private void button1_Click(object sender, EventArgs e) { } 8.5.4 匿名方法 匿名方法之內聯方式放入委託對象的使用位置,而避免建立一個委託來關聯回調方法,也就是由委託調用了匿名的方法,將方法代碼和委託實例直接關聯,在語法上有簡潔和直觀的好處。例如以匿名方法來綁定Click事件將變得很是簡單: button1.Click += delegate { MessageBox.Show("Hello world."); }; 所以,有必要以匿名方法來實現本節開始的委託示例,瞭解其實現過程和底層實質,例如: class AnonymousMethodEx { delegate void CalculateDelegate(Int32 x, Int32 y); public static void Main() { //匿名方法 CalculateDelegate mySubstractDelegate = delegate(Int32 x, Int32 y)
{ Console.WriteLine(x - y); }; CalculateDelegate myAddDelegate = delegate(Int32 x, Int32 y) { Console.WriteLine( x + y); }; mySubstractDelegate(100, 200); } } 事實上,匿名方法和委託在IL層是等效的,編譯器爲匿名方法增長了兩個靜態成員和靜態方法,如圖8-3所示。 圖8-3 匿名方法的IL分析 由編譯器生成的兩個靜態成員和靜態方法,輔助實現了委託調用同樣的語法結構,這正是匿名方法在底層的真相。 8.5.5 Lambda表達式 Lambda表達式是Functional Programming的核心概念,如今C# 3.0中也引入了Lambda表達式來實現更加簡潔的語法,而且爲LINQ提供了語法基礎,這些將在本書第12章有所交代。再次應用Lambda表達式來實現相同的過程,其代碼爲: class LambdaExpressionEx { delegate void CalculateDelegate(Int32 x, Int32 y); public static void Main() { CalculateDelegate myDelegate = (x, y) => Console.WriteLine(x - y); myDelegate(100, 200); }
} 分析Lambda表達式的IL代碼,可知編譯器一樣自動生成了相應的靜態成員和靜態方法,Lambda表達式在本質上仍然是一個委託。帶來這一切便利的是編譯器,在此對IL上的細節再也不作進一步分析。 8.5.6 規則 l 委託實現了面向對象的,類型安全的方法回調機制。 l 以Delegate做爲委託類型的後綴,以EventHandle做爲事件委託的後綴,是規範的命名規則。 l 多播委託返回值通常爲void,不推薦在多播委託中返回非void的類型。 l 匿名方法和Lambda表達式提供了更爲簡潔的語法表現,而這些新的特性主要是基於編譯器而實現的,在IL上並無本質的變化。 l .NET的事件是Observer模式在委託中的應用,而且基於.NET規範而實現,體現了更好的耦合性和靈活性。 8.5.7 結論 從委託到Lambda表達式的逐層演化,咱們能夠看到.NET在語言上的不斷進化和發展,也正是這些進步促成了技術的向前發展,使得.NET在語言上更加地兼容和優化。對於技術開發人員而言,這種進步也正是咱們所指望的。 然而,從根本上了解委託、認識委託纔是一切的基礎,不然語法上的進化只能使得理解更加迷惑。本節的討論,意在爲理解這些內容提供基礎,創建一個較爲全面的概念。
8.6 直面異常 本節將介紹如下內容: — .NET異常機制 — .NET常見的異常類型
— 自定義異常 8.6.1 引言 內存耗盡、索引越界、訪問已關閉資源、堆棧溢出、除零運算等一個個擺在你面前的時候,你想到的是什麼呢?固然是,異常。 在系統容錯和程序規範方面,異常機制是不可或缺的重要因素和手段。當挑戰來臨的時候,良好的系統設計一定有良好的異常處理機制來保證程序的健壯性和容錯機制。然而對異常的理解每每存在或多或少的誤解,例如: l 異常就是程序錯誤,以錯誤代碼返回錯誤信息就足夠了。 l 在系統中異常越多越能保證容錯性,儘量多的使用try/catch塊來處理程序執行。 l 使用.NET自定義Exception就能捕獲全部的異常信息,不須要特定異常的處理塊。 l 將異常類做爲方法參數或者返回值。 l 在自定義異常中經過覆寫ToString方法報告異常信息,對這種操做不能掉以輕心,由於某些安全敏感信息有泄漏的可能。 但願讀者在從本節的脈絡上了解異常的基本狀況和通用規則,將更多的探索留於實踐中的體察和品味。 8.6.2 爲什麼而拋? 關於異常,最多見的誤解可能莫過於對其可用性的理解。對於異常的處理,基本有兩種方式來完成:一種是異常形式,一種是返回值形式。然而,無論是傳統Win32 API下習慣的32位錯誤代碼,仍是COM編程中的HRESULT返回值,異常機制所具備的優點都不可替代,主要表現爲: l 不少時候,返回值方式具備固有的侷限性,例如在構造函數中就沒法有效的應用返回值來返回錯誤信息,只有異常才能提供全面的解決方案來應對。
l 提供更豐富的異常信息,便於交互和調試,而傳統的錯誤代碼不能有效提供更多的異常信息和調試指示,在程序理解和維護方面異常機制更具優點。 l 有效實現異常回滾,而且能夠根據不一樣的異常,回滾不一樣的操做,有效實現了對系統穩定性與可靠性的控制。例如,下例實現了一個典型的事務回滾操做: public void ExcuteSql(string conString, string cmdString) { SqlConnection con = new SqlConnection(conString); try { con.Open(); SqlTransaction tran = con.BeginTransaction(); SqlCommand cmd = new SqlCommand(cmdString, con); try { cmd.ExecuteNonQuery(); tran.Commit(); } catch (SqlException ex) { Console.WriteLine(ex.Message); //實現事務回滾 tran.Rollback(); throw new Exception("SQL Error!", ex); } } catch(Exception e) { throw (e); } finally { con.Close(); } } l 很好地與面嚮對象語言集成,在.NET中異常機制已經很好地與高級語言集成在一塊兒,以異常System.Exception類創建起的體系結構已經可以輕鬆應付各類異常信息,而且能夠經過面向對象機制定義本身的特定異常處理類,實現更加特性化的異常信息。 l 錯誤處理更加局部化,錯誤代碼更集中地放在一塊兒,加強了代碼的理解和維護,例如資源清理的工做徹底交由finally子句來執行,沒必要花費過多的精力去留意其維護。
l 錯誤代碼返回的信息內容有限而難於理解,一連串數字顯然不及豐富的文字信息說明問題,同時也不利於快速地定位和修改須要調試的代碼。 l 異常機制能有效應對未處理的異常信息,咱們不可能輕易地忽略任何異常;而返回值方式不可能深刻到異常可能發生的各個角落,不經意的遺漏就會形成系統的不穩定,何況這種維護方式顯然會讓系統開發人員精疲力竭。 l 異常機制提供了實現自定義異常的可能,有利於實現異常的擴展和特點定製。 綜上所述,異常機制是處理系統異常信息的最好機制與選擇,Jeffrey Richter在《Microsoft .NET框架程序設計》一書中給出了異常本質的最好定義,那就是: 異常是對程序接口隱含假設的一種違反。 然而關於異常的焦慮經常突出在其性能對系統形成的壓力上,由於返回值方式的性能毋庸置疑更具「先天」的優點。那麼異常的性能問題,咱們又該如何理解呢? 本質上,CLR會爲每一個可執行文件建立一個異常信息表,在該表中每一個方法都有一個關聯的異常處理信息數組,數組的每一項描述一個受保護的代碼塊、相關聯的異常篩選器(後文介紹)和異常處理程序等。在沒有異常發生時,異常信息表在處理時間和內存上的損失幾乎能夠忽略,只有異常發生時這種損失才值得考慮。例如: class TestException { //測試異常處理的性能 public int TestWithException(int a, int b) { try { return a / b; } catch { return -1; }
} //測試非異常處理的性能 public int TestNoExceptioin(int a, int b) { return a / b; } } 上述代碼對應的IL更能說明其性能差異,首先是有異常處理的方法: .method public hidebysig instance int32 TestWithException(int32 a, int32 b) cil managed { // 代碼大小 17 (0x11) .maxstack 2 .locals init ([0] int32 CS$1$0000) IL_0000: nop .try { IL_0001: nop IL_0002: ldarg.1 IL_0003: ldarg.2 IL_0004: div IL_0005: stloc.0 IL_0006: leave.s IL_000e } // end .try catch [mscorlib]System.Object { IL_0008: pop IL_0009: nop IL_000a: ldc.i4.m1 IL_000b: stloc.0 IL_000c: leave.s IL_000e
} // end handler IL_000e: nop IL_000f: ldloc.0 IL_0010: ret } // end of method TestException::TestWithException 代碼大小爲17個字節,在不發生異常的狀況下,數據在IL_0006出棧後以leave.s指令退出try受保護區域,並繼續執行IL_000e後面的操做:壓棧並返回。 而後是不使用異常的情形: .method public hidebysig instance int32 TestNoExceptioin(int32 a, int32 b) cil managed { // 代碼大小 9 (0x9) .maxstack 2 .locals init ([0] int32 CS$1$0000) IL_0000: nop IL_0001: ldarg.1 IL_0002: ldarg.2 IL_0003: div IL_0004: stloc.0 IL_0005: br.s IL_0007 IL_0007: ldloc.0 IL_0008: ret } // end of method TestException::TestNoExceptioin 代碼大小爲9字節,沒有特別處理跳出受保護區域的操做。 因而可知,兩種方式在內存的消化上差異很小,只有8個字節。而實際運行的時間差異也微不足道,因此沒有異常引起的狀況下,異常處理的性能損失是很小的;然而,有異常發生的狀況下,必須認可異常處理將佔用大量的系統資源和執行時間,所以建議儘量的以處理流程來規避異常處理。
8.6.3 從try/catch/finally提及:解析異常機制 理解.NET的異常處理機制,以try/catch/finally塊的應用爲起點,是最好的切入口,例如: class BasicException { public static void Main() { int a = 1; int b = b; GetResultToText(a, 0); } public static void GetResultToText(int a, int b) { StreamWriter sw = null; try { sw = File.AppendText(@"E:\temp.txt"); int c = a / b; //將運算結果輸出到文本 sw.WriteLine(c.ToString()); Console.WriteLine(c.ToString()); } catch (DivideByZeroException) { //實現從DivideByZeroException恢復的代碼 //並從新給出異常提示信息 throw new DivideByZeroException ("除數不能爲零!"); } catch (FileNotFoundException ex) { //實現從IOException恢復的代碼
//並再次引起異常信息 throw(ex); } catch (Exception ex) { //實現從任何與CLS兼容的異常恢復的代碼 //並從新拋出 throw; } catch { //實現任何異常恢復的代碼,不管是否與CLS兼容 //並從新拋出 throw; } finally { sw.Flush(); sw.Close(); } //未有異常拋出,或者catch捕獲而未拋出異常, //或catch塊從新拋出別的異常,此處才被執行 Console.WriteLine("執行結束。"); } } 1.try分析 try子句中一般包含可能致使異常的執行代碼,而try塊一般執行到引起異常或成功執行完成爲止。它不能單獨存在,不然將致使編譯錯誤,必須和零到多個catch子句或者finally子句配合使用。其中,catch子句包含各類異常的響應代碼,而finally子句則包含資源清理代碼。
2.catch分析 catch子句包含了異常出現時的響應代碼,其執行規則是:一個try子句能夠關聯零個或多個catch子句,CLR按照自上而下的順序搜索catch塊。catch子句包含的表達式,該表達式稱爲異常篩選器,用於識別try塊引起的異常。若是篩選器識別該異常,則會執行該catch子句內的響應代碼;若是篩選器不接受該異常,則CLR將沿着調用堆棧向更高一層搜索,直到找到識別的篩選器爲止,若是找不到則將致使一個未處理異常。無論是否執行catch子句,CLR最終都會執行finally子句的資源清理代碼。所以編譯器要求將特定程度較高的異常放在前面(如DivideByZeroException類),而將特定程度不高的異常放在後面(如示例中最下面的catch子句能夠響應任何異常),依此類推,其餘catch子句按照System.Exception的繼承層次依次由底層向高層羅列,不然將致使編譯錯誤。 catch子句的執行代碼一般會執行從異常恢復的代碼,在執行末尾能夠經過throw關鍵字再次引起由catch捕獲的異常,並添加相應的信息通知調用端更多的信息內容;或者程序實現爲線程從捕獲異常的catch子句退出,而後執行finally子句和finally子句後的代碼,固然前提是兩者存在的狀況下。 關於:異常篩選器 異常篩選器,用於表示用戶可預料、可恢復的異常類,全部的異常類必須是System.Exception類型或其派生類,System.Excetpion類型是一切異常類型的基類,其餘異常類例如DivideByZeroException、FileNotFoundException是派生類,從而造成一個有繼承層次的異常類體系,越具體的異常類越位於層次的底層。 若是try子句未拋出異常,則CLR將不會執行任何catch子句的響應代碼,而直接轉向finally子句執行直到結束。 值得注意的是,finally塊以後的代碼段不老是被執行,由於在引起異常而且沒有被捕獲的狀況下,將不會執行該代碼。所以,對於必須執行的處理環節,必須放在finally子句中。 3.finally分析 異常發生時,程序將轉交給異常處理程序,意味着那些老是但願被執行的代碼可能不被執行,例如文件關閉、數據庫鏈接關閉等資源清理工做,例如本例的StreamWriter對象。異常機制提供
了finally子句來解決這一問題:不管異常是否發生,finally子句老是執行。所以,finally子句不老是存在,只有須要進行資源清理操做時,纔有必要提供finally子句來保證清理操做老是被執行,不然沒有必要提供「多餘」的finally子句。 finally在CLR按照調用堆棧執行完catch子句的全部代碼時執行。一個try塊只能對應一個finally塊,而且若是存在catch塊,則finally塊必須放在全部的catch塊以後。若是存在finally子句,則finally子句執行結束後,CLR會繼續執行finally子句以後的代碼。 根據示例咱們對try、catch和finally子句分別作了分析,而後對其應用規則作以小結,主要包括: l catch子句能夠帶異常篩選器,也能夠不帶任何參數。若是不存在任何表達式,則代表該catch子句能夠捕獲任何異常類型,包括兼容CLS的異常或者不兼容的異常。 l catch子句按照篩選器的繼承層次進行順序羅列,若是將具體的異常類放在執行順序的末尾將致使編譯器異常。而對於繼承層次同級的異常類,則能夠隨意安排catch子句的前後順序,例如DivideByZeroException類和FileNotFoundException類處於System.Exception繼承層次的同一層次,所以其對應的catch子句之間能夠隨意安排前後順序。 l 異常篩選器,能夠指定一個異常變量,該變量將指向拋出的異常類對象,該對象記錄了相關的異常信息,能夠在catch子句內獲取該信息。 l finally子句內,也能夠拋出異常,可是應該儘可能避免這種操做。 l CLR若是沒有搜索到合適的異常篩選器,則說明程序發生了未預期的異常,CLR將拋出一個未處理異常,應用程序應該提供對未處理異常的應對策略,例如:在發行版本中將異常信息寫入日誌,而在開發版本中啓用調試器定位。 l try塊內定義的變量對try塊外是不可見的,所以對於try塊內進行初始化的變量,應該定義在try塊以前,不然try塊外的調用將致使編譯錯誤。例如示例中的StreamWriter的對象定義,必定要放在try塊以外,不然沒法在finally子句內完成資源清理操做。 8.6.4 .NET系統異常類
1.異常體系 .NET框架提供了不一樣層次的異常類來應對不一樣種類的異常,而且造成必定的繼承體系,全部的異常類型都繼承自System.Exception類。例如,圖8-4是異常繼承層次的一個片斷,繼承自上而下由通用化向特定化延伸。 FCL定義了一個龐大的異常體系,熟悉和了解這些異常類型是有效應用異常和理解異常體系的有效手段,可是顯然這一工做只能交給搜索MSDN來完成了。然而,咱們仍是應該對一些重要的.NET系統異常有必定的瞭解,主要包括: l OverflowException,算術運算、類型轉換時的溢出。 圖8-4 異常類的部分繼承體系 l StackOverflowException,密封類,不可繼承,表示堆棧溢出,在應用程序中拋出該異常是不適當的作法,由於通常只有CLR自己會拋出堆棧溢出的異常。 l OutOfMemoryException,內存不足引起的異常。 l NullReferenceException,引用空引用對象時引起。 l InvalidCastException,無效類型轉換引起。 l IndexOutOfRangeException,試圖訪問越界的索引而引起的異常。 l ArgumentException,無效參數異常。 l ArgumentNullException,給方法傳遞一個不可接受的空參數的空引用。 l DivideByZeroException,被零除引起。 l ArithmeticException,算術運行、類型轉換等引起的異常。
l FileNotFoundException,試圖訪問不存在的文件時引起。 注意,這裏羅列的並不是所有的常見異常,更非FCL定義的全部系統異常類型。對於異常類而言,更多的精力應該放在關注異常基類System.Exception的理解上,以期提綱挈領。 2.System.Exception類解析 關於System.Exception類型,它是一切異常類的最終基類,而它自己又繼承自System.Object類型,用於捕獲任何與CLS兼容的異常。Exception類提供了全部異常類型的基本屬性與規則,例如: l Message屬性,用於描述異常拋出緣由的文本信息。 l InnerException屬性,用於獲取致使當前異常的異常集。 l StackTrack屬性,提供了一個調用棧,其中記錄了異常最初被拋出的位置,所以在程序調試時很是有用,例如: public static void Main() { try { TestException(); } catch (Exception ex) { //輸出當前調用堆棧上的異常的拋出位置 Console.WriteLine(ex.StackTrace); } } private static void TestException() { //直接拋出異常 throw new FileNotFoundException("Error."); }
l HResult受保護屬性,可讀寫HRESULT值,分配特定異常的編碼數值,主要應用於託管代碼與非託管代碼的交互操做。 還有其餘的方法,例如HelpLink用於獲取幫助文件的連接,TargetSite方法用於獲取引起異常的方法。 還有不少公有方法輔助完成異常信息的獲取、異常類序列化等操做。其中,實現ISerializable接口方法GetObjectData值得關注,異常類新增字段必須經過該方法填充SerializationInfo,異常類進行序列化和反序列化必須實現該方法,其定義可表示爲: [ComVisible(true)] public interface ISerializable { [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermission Flag.SerializationFormatter)] void GetObjectData(SerializationInfo info, StreamingContext context); } 參數info表示要填充的SerializationInfo對象,而context則表示要序列化的目標流。咱們在下文的自定義異常中將會有所瞭解。 .NET還提供了兩個直接繼承於Exception的重要子類:ApplicationException和SystemException類。其中,ApplicationException類型爲FCL爲應用程序預留的基類型,因此自定義異常能夠選擇ApplicationException或者直接從Exception繼承;SystemException爲系統異常基類,CLR自身拋出的異常繼承自SystemException類型。 8.6.5 定義本身的異常類 FCL定義的系統異常,不能解決全部的問題。異常機制與面向對象有效的集成,意味着咱們能夠很容易的經過繼承System.Exception及其派生類,來實現自定義的錯誤處理,擴展異常處理機制。
上文中,咱們簡單學習了System.Exception類的實現屬性和方法,應該說研究Exception類型對於實現自定義異常類具備很好的參考價值,微軟工程師已經實現了最好的實現體驗。咱們以實際的示例出發,來講明自定義異常類的實現,總結其實現與應用規則,首先是自定義異常類的實現: //Serializable指定了自定義異常能夠被序列化 [Serializable] public class MyException : Exception, ISerializable { //自定義本地文本信息 private string myMsg; public string MyMsg { get { return myMsg; } } //重寫只讀本地文本信息屬性 public override string Message { get { string msgBase = base.Message; return myMsg == null ? msgBase : msgBase + myMsg; } } //實現基類的各公有構造函數 public MyException() : base(){ } public MyException(string message) : base(message) { } public MyException(string message, Exception innerException) : base(message, innerException) { } //爲新增字段實現構造函數
public MyException(string message, string myMsg) : this(message) { this.myMsg = myMsg; } public MyException(string message, string myMsg, Exception innerException) : this(message, innerException) { this.myMsg = myMsg; } //用於序列化的構造函數,以支持跨應用程序域或遠程邊界的封送處理 protected MyException(SerializationInfo info, StreamingContext context) : base(info, context) { myMsg = info.GetString("MyMsg"); } //重寫基類GetObjectData方法,實現向SerializationInfo中添加自定義字段信息 public override void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("MyMsg", myMsg); base.GetObjectData(info, context); } } 而後,咱們實現一個自定義異常測試類,來進一步瞭解.NET異常機制的執行過程: class Test_CustomException { public static void Main() { try { try
{ string str = null; Console.WriteLine(str.ToString()); } catch (NullReferenceException ex) { //向高層調用方拋出自定義異常 throw new MyException("這是系統異常信息。", "\n這是自定義異常信息。", ex); } } catch (MyException ex) { Console.WriteLine(ex.Message); } } } 結合示例的實踐,總結自定義異常類的規則與規範,主要包括: l 首先,選擇合適的基類繼承,通常狀況下咱們都會選擇Exception類或其派生類做爲自定義異常類的基類。可是異常的繼承深度不宜過多,通常在2~3層是可接受的維護範圍。 l System.Exception類型提供了三個公有構造函數,在自定義類型中也應該實現三個構造函數,而且最好調用基類中相應的構造函數;若是自定義類型中有新的字段要處理,則應該爲新的字段實現新的構造函數來實現。 l 全部的異常類型都是可序列化的,所以必須爲自定義異常類添加SerializableAttribute特性,並實現ISerializable接口。 l 以Exception做爲異常類名的後綴,是良好的編程習慣。 l 在自定義異常包括本地化描述信息,也就是實現異常類的Message屬性,而不是從基類繼承,這顯然違反了Message自己的語義。
l 雖然異常機制提升了自定義特定異常的方法,可是大部分時候咱們應該優先考慮.NET的系統異常,而不是實現自定義異常。 l 要想使自定義異常能應用於跨應用程序域,應該使異常可序列化,給異常類實現ISerializable接口是個好的選擇。 l 若是自定義異常沒有必要實現子類層次結構,那麼異常類應該定義爲密封類(sealed),以保證其安全性。
8.6.6 異常法則 異常法則是使用異常的最佳體驗規則與設計規範要求,在實際的應用中有指導做用,主要包含如下幾個方面: l 儘量以邏輯流程控制來代替異常,例如非空字段的處理不要延遲到業務處理階段,而應在代碼校驗時完成。對於文件操做的處理,應該首先進行路徑是否存在的校驗,而不是將責任一股腦推給FileNotFoundException異常來處理。 l 將異常理解爲程序的錯誤,顯然曲解了對異常本質的認識。正如前文所言,異常是對程序接口隱含假設的一種違反,而這種假設經常和錯誤沒有關係,反倒更多的是規則與約定。例如客戶端「無理」的用Word來打開媒體文件,對程序開發者來講,這種「錯誤」是不可見的,這種問題只是違反了媒體文件只能用相關播放器打開的假設,而並不是程序開發者的錯誤。 l 對異常造成文檔,詳細描述關於異常的緣由和相關信息,是減小引起異常的有效措施。 l .NET 2.0提供了不少新特性來簡化異常的處理,同時從性能的角度考慮也是很好的選擇,例如: public static void Main() { DateTime now; if(DateTime.TryParse("2007/11/7 23:31:00", out now)) { Console.WriteLine("Now it's {0}", now); } } 上例中實際實現了一個Try-Parse模式,以最大限度地減小異常形成的性能損失。對於不少經常使用的基礎類型成員來講,實現Try-Parse模式是避免處理異常性能的一種不錯的選擇,.NET類庫的不少基礎類型都實現了這一模式,例如Int3二、Char、Byte、DateTime等等。 還有一種Tester-Doer模式,一樣是用來減小異常的性能問題,在此就不作深刻的研究。 l 對於多個catch塊的狀況,應該始終保證由最特定異常到最不特定異常的順序來排列,以保證特定異常老是首先被執行。
l 異常提示應該準確而有效,提供豐富的信息給異常查看者來進行正確的判斷和定位。 l 異常必須有針對性,盲目地拋出System.Exception意味着對於異常的緣由是盲目的,並且容易形成異常被吞現象的發生。什麼時候拋出異常,拋出什麼異常,創建在對上下文環境的理解基礎上。 l 儘可能避免在Finally子句拋出異常。 l 應該避免在循環中拋出異常。 l 能夠選擇以using語句代替try/finally塊來完成資源清理,詳見6.3節「using的多重身份」。 另外,微軟還提供了Enterprise Library異常處理應用程序塊(簡稱EHAB)來實現更靈活、可擴展、可定製的異常處理框架,力圖體現對異常處理的最新實踐方式。 8.6.7 結論 本節旨在提綱挈領的對異常機制及其應用實踐作以鋪墊,關於異常的性能、未見異常處理及堆棧跟蹤等問題只能淺嘗於此。在從此的實踐中,還應注意應用異常機制處理,要關注上下文的環境作出適當選擇。
第10章 接觸泛型
二十:C#泛型 C#泛型 C#泛型類與結構 C#除可單獨聲明泛型類型(包括類與結構)外,也可在基類中包含泛型類型的聲明。但基類若是是泛型類,它的類型參數要麼已實例化,要麼來源於子類(一樣是泛型類型)聲明的類型參數。 class C<U, V> {} //合法 class D: C<string,int>{} //合法 class E<U, V>: C<U, V> {} //合法 class F<U, V>: C<string, int> {} //合法
class G : C<U, V> { } //非法 泛型類型的成員 class C<V>{ public V f1; //聲明字段 public D<V> f2; //做爲其餘泛型類型的參數 public C(V x) { this.f1 = x; } } 泛型類型的成員能夠使用泛型類型聲明中的類型參數。但類型參數若是沒有任何約束,則只能在該類型上使用從System.Object繼承的公有成員。 泛型接口 interface IList<T> { T[] GetElements(); } interface IDictionary<K,V> { void Add(K key, V value); } // 泛型接口的類型參數要麼已實例化, // 要麼來源於實現類聲明的類型參數 class List<T> : IList<T>, IDictionary<int, T> { public T[] GetElements() { return null; } public void Add(int index, T value) { } } 泛型委託
delegate bool Predicate<T>(T value); class X { static bool F(int i) {...} static bool G(string s) {...} static void Main() { Predicate<string> p2 = G; Predicate<int> p1 = new Predicate<int>(F); } } 泛型委託支持在委託返回值和參數上應用參數類型,這些參數類型一樣能夠附帶合法的約束。 泛型方法簡介 • C#泛型機制只支持「在方法聲明上包含類型參數」——即泛型方法 • C#泛型機制不支持在除方法外的其餘成員(包括屬性、事件、索引器、構造器、析構器)的聲明上包含類 型參數,但這些成員自己能夠包含在泛型類型中,並使用泛型類型的類型參數 • 泛型方法既能夠包含在泛型類型中,也能夠包含在非泛型類型中 泛型方法的聲明與調用 //不是泛型類,是一個具體的類,這個類不須要泛型類型的實例化 public class Finder { // 可是是一個泛型方法,請看泛型方法的聲明,參數要求泛型化 public static int Find<T> ( T[] items, T item) { for(int i=0;i<items.Length;i++){ if (items[i].Equals(item)) { return i; } } return -1; }
} // 泛型方法的調用<int>不是放到Finder後面,而是放在Find後面。 int i=Finder.Find<int> ( new int[]{1,3,4,5,6,8,9}, 6); 泛型方法的重載 class MyClass { void F1<T>(T[] a, int i); // 不能夠構成重載方法 void F1<U>(U[] a, int i); void F2<T>(int x); //能夠構成重載方法 void F2(int x); //兩句申明同樣,where字句,T繼承A,泛型參數必須要繼承A void F3<T>(T t) where T : A; //不能夠構成重載方法 void F3<T>(T t) where T : B; } 泛型方法的重寫 abstract class Base { public abstract T F<T,U>(T t, U u) where U: T; public abstract T G<T>(T t) where T: IComparable; } class Derived: Base{ //合法的重寫,約束被默認繼承,只須要寫方法的簽名 public override X F<X,Y>(X x, Y y) { } //非法的重寫,指定任何約束都是多餘的 //重寫的時候,不能寫約束,也不添加新的約束,只能繼承父類的約束。 public override T G<T>(T t) where T: IComparable {} }
泛型約束簡介 • C#泛型要求對「全部泛型類型或泛型方法的類型參數」的任何假定,都要基於「顯式的約束」,以維護 C#所要求的類型安全。 • 「顯式約束」由where子句表達,能夠指定「基類約束」,「接口約束」,「構造器約束」「值類型/引用類型約束」共四種約束。 • 「顯式約束」並不是必須,若是沒有指定「顯式約束」,泛型類型參數將只能訪問System.Object類型中的公有方法。 基類約束 class A { public void F1() {…} } class B { public void F2() {…} } class C<S,T> where S: A // S繼承自A where T: B // T繼承自B { // 能夠在類型爲S的變量上調用F1, // 能夠在類型爲T的變量上調用F2 …. } 接口約束 interface IPrintable { void Print(); } interface IComparable<T> { int CompareTo(T v);} interface IKeyProvider<T> { T GetKey(); } class Dictionary<K,V> where K: IComparable<K> where V: IPrintable, IKeyProvider<K> {
// 能夠在類型爲K的變量上調用CompareTo, // 能夠在類型爲V的變量上調用Print和GetKey …. } 構造器約束 class A { public A() { } } class B { public B(int i) { } } class C<T> where T : new() { //能夠在其中使用T t=new T(); …. } C<A> c=new C<A>(); //能夠,A有無參構造器 C<B> c=new C<B>(); //錯誤,B沒有無參構造器 值類型/引用類型約束 public struct A { … } public class B { … } class C<T> where T : struct { // T在這裏面是一個值類型 … } C<A> c=new C<A>(); //能夠,A是一個值類型 C<B> c=new C<B>(); //錯誤,B是一個引用類型
總結 • C#的泛型能力由CLR在運行時支持,它既不一樣於C++在編譯時所支持的靜態模板,也不一樣於Java在編譯器層面使用「搽拭法」支持的簡單的泛型。 • C#的泛型支持包括類、結構、接口、委託共四種泛型類型,以及方法成員。 • C#的泛型採用「基類, 接口, 構造器, 值類型/引用類型」的約束方式來實現對類型參數的「顯式約束」,它不支持C++模板那樣的基於簽名的隱式約束。
泛型續: 根據微軟的視頻教程"跟我一塊兒學Visual Studio 2005C#語法篇"來學,由於裏面有比較多的代碼示例,學起來比較容易好理解 1.未使用泛型的Stack類 1using System; 2 3public class Stack 4{ 5 readonly int m_Size; 6 int m_StackPointer = 0; 7 object[] m_Items; 8 public Stack(): this(100) 9 { } 10 public Stack(int size) 11 { 12 m_Size = size; 13 m_Items = new object[m_Size]; 14 } 15 public void Push(object item) 16 { 17 if (m_StackPointer >= m_Size) 18 throw new StackOverflowException(); 19 20 m_Items[m_StackPointer] = item; 21 m_StackPointer++; 22 } 23 public object Pop() 24 { 25 m_StackPointer--; 26 if (m_StackPointer >= 0) 27 { 28 return m_Items[m_StackPointer]; 29 } 30 else 31 { 32 m_StackPointer = 0; 33 throw new InvalidOperationException("Cannot pop an empty stack"); 34 } 35 } 36} 37 2.使用泛型的類
1using System; 2 3public class Stack<T> 4{ 5 readonly int m_Size; 6 int m_StackPointer = 0; 7 T[] m_Items; 8 public Stack() 9 : this(100) 10 { 11 } 12 public Stack(int size) 13 { 14 m_Size = size; 15 m_Items = new T[m_Size]; 16 } 17 public void Push(T item) 18 { 19 if (m_StackPointer >= m_Size) 20 throw new StackOverflowException(); 21 22 m_Items[m_StackPointer] = item; 23 m_StackPointer++; 24 } 25 public T Pop() 26 { 27 m_StackPointer--; 28 if (m_StackPointer >= 0) 29 { 30 return m_Items[m_StackPointer]; 31 } 32 else 33 { 34 m_StackPointer = 0; 35 //throw new InvalidOperationException("Cannot pop an empty stack"); 36 return default(T); 37 } 38 } 39} 40 41public class Stack1<T> : Stack<T> 42{ 43 44} 45 下爲PDF文檔,我感受挺好的,很簡單,我聽的懂就是好的 /Clingingboy/one.pdf 多個泛型 1class Node<K, T> 2{ 3 public K Key; 4 public T Item; 5 public Node<K, T> NextNode; 6 public Node() 7 { 8 Key = default(K); 9 Item = default(T); 10 NextNode = null; 11 } 12 public Node(K key, T item, Node<K, T> nextNode)
13 { 14 Key = key; 15 Item = item; 16 NextNode = nextNode; 17 } 18} 泛型別名 1using list = LinkedList<int, string>; 泛型約束 1public class LinkedList<K, T> where K : IComparable 2{ 3 Node<K, T> m_Head; 4 public LinkedList() 5 { 6 m_Head = new Node<K, T>(); 7 } 8 public void AddHead(K key, T item) 9 { 10 Node<K, T> newNode = new Node<K, T>(key, item, m_Head.NextNode); 11 m_Head.NextNode = newNode; 12 } 13 14 T Find(K key) 15 { 16 Node<K, T> current = m_Head; 17 while (current.NextNode != null) 18 { 19 if (current.Key.CompareTo(key) == 0) 20 break; 21 else 22 current = current.NextNode; 23 } 24 return current.Item; 25 } 26 27} 28 1using System; 2using System.Collections.Generic; 3using System.Text; 4 5namespace VS2005Demo1 6{ 7 public class MyBaseClassGeneric // sealed,static 8 { 9 } 10 11 interface IMyBaseInterface 12 { 13 void A(); 14 } 15 16 internal class GenericClass<T> where T : MyBaseClassGeneric,IMyBaseInterface 17 { 18 19 } 20 21 class GClass<K, T> where K : MyBaseClassGeneric,IMyBaseInterface,new() where T : K
22 { 23 24 } 25 26 class GUClass<K, T> where T : K where K : MyBaseClassGeneric,IMyBaseInterface, new() 27 { 28 GClass<K, T> obj = new GClass<K, T>(); 29 } 30 31 32 不能將引用/值類型約束與基類約束一塊兒使用,由於基類約束涉及到類#region 不能將引用/值類型約束與基類約束一塊兒使用,由於基類約束涉及到類 33 34 //class A<T> where T : struct,class 35 //{} 36 37 #endregion 38 39 不能使用結構和默認構造函數約束,由於默認構造函數約束也涉及到類#region 不能使用結構和默認構造函數約束,由於默認構造函數約束也涉及到類 40 41 //class A<T> where T : struct,new() 42 //{} 43 44 #endregion 45 46 雖然您能夠使用類和默認構造函數約束,但這樣作沒有任何價值#region 雖然您能夠使用類和默認構造函數約束,但這樣作沒有任何價值 47 48 class A<T> where T : new() 49 { 50 T obj = new T(); 51 } 52 53 class TypeA 54 { 55 public TypeA() { } 56 } 57 58 class TestA 59 { 60 A<TypeA> obj = new A<TypeA>(); 61 } 62 63 #endregion 64 65 能夠將引用/值類型約束與接口約束組合起來,前提是引用/值類型約束出如今約束列表的開頭#region 能夠將引用/值類型約束與接口約束組合起來,前提是引用/值類型約束出如今約束列表的開頭 66 67 class SClass<K> where K : struct, IMyBaseInterface 68 { } 69 70 class CClass<K> where K : class, IMyBaseInterface 71 { } 72 73 #endregion 74} 75
第二十一回:認識全面的null
說在,開篇以前 說在,開篇以前
null、nullable、??運算符、null object模式,這些閃亮的概念在你眼前晃動,咱們有理由相信「存在即合理」,事實上,null不光合理,並且重要。本文,從null的基本認知開始,逐層瞭解可空類型、??運算符和null object模式,在循序之旅中瞭解不同的null。
你必須知道的.NET,繼續全新體驗,分享更多色彩。
1 從什麼是null開始?
null,一個值得尊敬的數據標識。
通常說來,null表示空類型,也就是表示什麼都沒有,可是「什麼都沒有」並不意味「什麼都不是」。實際上,null是如此的重要,以至於在JavaScript中,Null類型就做爲5種基本的原始類型之一,與Undefined、Boolean、Number和String並駕齊驅。這種重要性一樣表如今.NET中,可是必定要澄清的是,null並不等同於0,"",string.Empty這些一般意義上的「零」值概念。相反,null具備實實在在的意義,這個意義就是用於標識變量引用的一種狀態,這種狀態表示沒有引用任何對象實例,也就是表示「什麼都沒有」,既不是Object實例,也不是User實例,而是一個空引用而已。
在上述讓我都拗口抓狂的表述中,其實中心思想就是澄清一個關於null意義的無力訴說,而在.NET中null又有什麼實際的意義呢?
在.NET中,null表示一個對象引用是無效的。做爲引用類型變量的默認值,null是針對指針(引用)而言的,它是引用類型變量的專屬概念,表示一個引用類型變量聲明但未初始化的狀態,例如: object obj = null;
此時obj僅僅是一個保存在線程棧上的引用指針,不表明任何意義,obj未指向任何有效實例,而被默認初始化爲null。
object obj和object obj = null的區別?
那麼,object obj和object obj = null有實際的區別嗎?答案是:有。主要體如今編譯器的檢查上。默認狀況下,建立一個引用類型變量時,CLR即將其初始化爲null,表示不指向任何有效實例,因此本質上兩者表示了相同的意義,可是有有所區別: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0
//編譯器檢測錯誤:使用未賦值變量obj //object obj; //編譯器理解爲執行了初始化操做,因此不引起編譯時錯誤 object obj = null; if (obj == null) { //運行時拋出NullReferenceException異常 Console.WriteLine(obj.ToString()); }
注:當我把這個問題拋給幾個朋友時,對此的想法都未造成統一的共識,幾位同志各有各的理解,也各有個的道理。固然,我也慎重的對此進行了一番探討和分析,可是並未造成徹底100%肯定性的答案。不過,在理解上我更傾向於本身的分析和判斷,因此在給出上述結論的基礎上,也將這個小小的思考留給你們來探討,好的思考和分析別忘了留給你們。事實上,將 static void Main(string[] args) { object o; object obj = null; }
反編譯爲IL時,兩者在IL層仍是存在必定的差異: .method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 1 .locals init ( [0] object o, [1] object obj) L_0000: nop L_0001: ldnull L_0002: stloc.1 L_0003: ret }
前者沒有發生任何附加操做;然後者經過ldnull指令推動一個空引用給evaluation stack,而stloc則將空引用保存。
回到規則
在.NET中,對null有以下的基本規則和應用:
null爲引用類型變量的默認值,爲引用類型的概念範疇。
null不等同於0,"",string.Empty。
引用is或as模式對類型進行判斷或轉換時,須要作進一步的null判斷。
快捷參考
關於is和as模式,能夠參考《你必須知道的.NET》 7.5節「恩怨情仇:is和as
」
第一回:恩怨情仇:is和as
www.anytao.com
判斷一個變量是否爲null,能夠應用==或!=操做符來完成。
對任何值爲nul的l變量操做,都會拋出NullReferenceException異常。
2 Nullable<T>(可空類型)
一直以來,null都是引用類型的特有產物,對值類型進行null操做將在編譯器拋出錯誤提示,例如: //拋出編譯時錯誤 int i = null; if (i == null) { Console.WriteLine("i is null.");
}
正如示例中所示,不少狀況下做爲開發人員,咱們更但願可以以統一的方式來處理,同時也但願可以解決實際業務需求中對於「值」也能夠爲「空」這一實際狀況的映射。所以,自.NET 2.0以來,這一特權被新的System.Nullable<T>(即,可空值類型)的誕生而打破,解除上述詬病能夠很容易如下面的方式被實現: //Nullable<T>解決了這一問題 int? i = null; if (i == null) { Console.WriteLine("i is null."); }
你可能很奇怪上述示例中並無任何Nullable的影子,實際上這是C#的一個語法糖,如下代碼在本質上是徹底等效的: int? i = null; Nullable<int> i = null;
顯然,咱們更中意以第一種簡潔而優雅的方式來實現咱們的代碼,可是在本質上Nullable<T>和T?他們是一路貨色。
可空類型的偉大意義在於,經過Nullable<T>類型,.NET爲值類型添加「可空性」,例如Nullable<Boolean>的值就包括了true、false和null,而Nullable<Int32>則表示值便可覺得整形也能夠爲null。同時,可空類型實現了統一的方式來處理值類型和引用類型的「空」值問題,例如值類型也能夠享有在運行時以NullReferenceException異常來處理。
另外,可空類型是內置於CLR的,因此它並不是c#的獨門絕技,VB.NET中一樣存在相同的概念。
Nullable的本質(IL)
那麼咱們如何來認識Nullable的本質呢?當你聲明一個: Nullable<Int32> count = new Nullable<Int32>();
時,到底發生了什麼樣的過程呢?咱們首先來了解一下Nullable在.NET中的定義: public struct Nullable<T> where T : struct { private bool hasValue; internal T value;
public Nullable(T value); public bool HasValue { get; } public T Value { get; } public T GetValueOrDefault(); public T GetValueOrDefault(T defaultValue); public override bool Equals(object other); public override int GetHashCode(); public override string ToString(); public static implicit operator T?(T value); public static explicit operator T(T? value); }
根據上述定義可知,Nullable本質上還是一個struct爲值類型,其實例對象仍然分配在線程棧上。其中的value屬性封裝了具體的值類型,Nullable<T>進行初始化時,將值類型賦給value,能夠從其構造函數獲知: public Nullable(T value) { this.value = value; this.hasValue = true; }
同時Nullable<T>實現相應的Equals、ToString、GetHashCode方法,以及顯式和隱式對原始值類型與可空類型的轉換。所以,在本質上Nullable能夠看着是預約義的struct類型,建立一個Nullable<T>類型的IL表示能夠很是清晰的提供例證,例如建立一個值爲int型可空類型過程,其IL能夠表示爲: .method private hidebysig static void Main() cil managed { .entrypoint .maxstack 2 .locals init ( [0] valuetype [mscorlib]System.Nullable`1<int32> a) L_0000: nop L_0001: ldloca.s a L_0003: ldc.i4 0x3e8 L_0008: call instance void [mscorlib]System.Nullable`1<int32>::.ctor(!0)
L_000d: nop L_000e: ret }
對於可空類型,一樣須要必要的小結:
可空類型表示值爲null的值類型。
不容許使用嵌套的可空類型,例如Nullable<Nullable<T>> 。
Nullable<T>和T?是等效的。
對可空類型執行GetType方法,將返回類型T,而不是Nullable<T>。
c#容許在可空類型上執行轉換和轉型,例如: int? a = 100; Int32 b = (Int32)a; a = null;
同時爲了更好的將可空類型於原有的類型系統進行兼容,CLR提供了對可空類型裝箱和拆箱的支持。
3 ??運算符
在實際的程序開發中,爲了有效避免發生異常狀況,進行null斷定是常常發生的事情,例如對於任意對象執行ToString()操做,都應該進行必要的null檢查,以避免發生沒必要要的異常提示,咱們經常是這樣實現的: object obj = new object(); string objName = string.Empty; if (obj != null) { objName = obj.ToString(); }
Console.WriteLine(objName);
然而這種實現實在是使人做嘔,滿篇的if語句老是讓人看着渾身不適,那麼還有更好的實現方式嗎,咱們能夠嘗試(? :)三元運算符: object obj = new object(); string objName = obj == null ? string.Empty : obj.ToString(); Console.WriteLine(objName);
上述obj能夠表明任意的自定義類型對象,你能夠經過覆寫ToString方法來輸出你想要輸出的結果,由於上述實現是如此的頻繁,因此.NET 3.0中提供了新的操做運算符來簡化null值的判斷過程,這就是:??運算符。上述過程能夠以更加震撼的代碼表現爲: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 object obj = null; string objName = (obj ?? string.Empty).ToString(); Console.WriteLine(objName);
那麼??運算符的具體做用是什麼呢?
??運算符,又稱爲null-coalescing operator,若是左側操做數爲null,則返回右側操做數的值, 若是不爲null則返回左側操做數的值。它既能夠應用於可空類型,有能夠應用於引用類型。 插播廣告,個人新書
4 Nulll Object模式
模式之於設計,正如祕笈之於功夫。正如咱們前文所述,null在程序設計中具備舉足輕重的做用,所以如何更優雅的處理「對象爲空」這一廣泛問題,大師們提出了Null Object Pattern概念,也就是咱們常說的Null Object模式。例如Bob大叔在《敏捷軟件開發--原則、模式、實踐》一書,Martin Fowler在《Refactoring: Improving the Design of Existing Code》一書,都曾就Null Object模式展開詳細的討論,可見23中模式以外仍是有不少設計精髓,可能稱爲模式有礙經典。可是仍然
值得咱們挖據、探索和發現。 下面就趁熱打鐵,在null認識的基礎上,對null object模式進行一點探討,研究null object解決的問題,並提出通用的null object應用方式。 解決什麼問題? 簡單來講,null object模式就是爲對象提供一個指定的類型,來代替對象爲空的狀況。說白了就是解決對象爲空的狀況,提供對象「什麼也不作」的行爲,這種方式看似無聊,但倒是很聰明的解決之道。舉例來講,一個User類型對象user須要在系統中進行操做,那麼典型的操做方式是: if (user != null) { manager.SendMessage(user); }
這種相似的操做,會遍及於你的系統代碼,無數的if判斷讓優雅遠離了你的代碼,若是大意忘記null判斷,那麼只有無情的異常伺候了。因而,Null object模式就應運而生了,對User類實現相同功能的NullUser類型,就能夠有效的避免繁瑣的if和沒必要要的失誤: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 public class NullUser : IUser { public void Login() { //不作任何處理 } public void GetInfo() { } public bool IsNull { get { return true; } } }
IsNull屬性用於提供統一斷定null方式,若是對象爲NullUser實例,那麼IsNull必定是true的。
那麼,兩者的差異體如今哪兒呢?其實主要的思路就是將null value轉換爲null object,把對user == null這樣的判斷,轉換爲user.IsNull雖然只有一字之差,可是本質上是徹底兩回事兒。經過null object模式,能夠確保返回有效的對象,而不是沒有任何意義的null值。同時,「在執行方法時返回null object而不是null值,能夠避免NullReferenceExecption異常的發生。」,這是來自Scott Dorman的聲音。
通用的null object方案
下面,咱們實現一種較爲通用的null object模式方案,並將其實現爲具備.NET特點的null object,因此咱們採起實現.NET中INullable接口的方式來實現,INullable接口是一個包括了IsNull屬性的接口,其定義爲: public interface INullable { // Properties bool IsNull { get; } }
仍然以User類爲例,實現的方案能夠表達爲:
圖中僅僅列舉了簡單的幾個方法或屬性,旨在達到說明思路的目的,其中User的定義爲: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com
// Release : 2008/07/31 1.0 public class User : IUser { public void Login() { Console.WriteLine("User Login now."); } public void GetInfo() { Console.WriteLine("User Logout now."); } public bool IsNull { get { return false; } } }
而對應的NullUser,其定義爲: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 public class NullUser : IUser { public void Login() { //不作任何處理 } public void GetInfo() { } public bool IsNull {
get { return true; } } }
同時經過UserManager類來完成對User的操做和管理,你很容易思考經過關聯方式,將IUser做爲UserManger的屬性來實現,基於對null object的引入,實現的方式能夠爲: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 class UserManager { private IUser user = new User(); public IUser User { get { return user; } set { user = value ?? new NullUser(); } } }
固然有效的測試是必要的: public static void Main() { UserManager manager = new UserManager(); //強制爲null manager.User = null; //執行正常 manager.User.Login(); if (manager.User.IsNull) { Console.WriteLine("用戶不存在,請檢查。");
} }
經過強制將User屬性實現爲null,在調用Login時仍然可以保證系統的穩定性,有效避免對null的斷定操做,這至少可讓咱們的系統少了不少沒必要要的斷定代碼。
詳細的代碼能夠經過本文最後的下載空間進行下載。實際上,能夠經過引入Facotry Method模式來構建對於User和NullUser的建立工做,這樣就能夠徹底消除應用if進行判斷的僵化,不過那是另一項工做罷了。
固然,這只是null object的一種實現方案,在此對《Refactoring》一書的示例進行改良,完成更具備.NET特點的null object實現,你也能夠請NullUser繼承Use並添加相應的IsNull斷定屬性來完成。
借力c# 3.0的Null object
在C# 3.0中,Extension Method(擴展方法)對於成就LINQ居功至偉,可是Extension Method的神奇遠不是止於LINQ。在實際的設計中,靈活而巧妙的應用,一樣能夠給你的設計帶來意想不到的震撼,以上述User爲例咱們應用Extension Method來取巧實現更簡潔IsNull斷定,代替實現INullable接口的方法而採用更簡單的實現方式。從新構造一個實現相同功能的擴展方法,例如: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 public static class UserExtension { public static bool IsNull(this User user) { return null == user; } }
固然,這只是一個簡單的思路,僅僅將對null value的判斷轉換爲null object的判斷角度來看,擴展方法帶來了更有效的、更簡潔的表現力。
null object模式的小結
有效解決對象爲空的狀況,爲值爲null提供可靠保證。
保證可以返回有效的默認值,例如在一個IList<User> userList中,可以保證任何狀況下都有有效值返回,能夠保證對userList操做的有效性,例如: // Copyright : www.anytao.com // Author : Anytao,http://www.anytao.com // Release : 2008/07/31 1.0 public void SendMessageAll(List<User> userList) { //不須要對userList進行null判斷 foreach (User user in userList) { user.SendMessage(); } }
提供統一斷定的IsNull屬性。能夠經過實現INullable接口,也能夠經過Extension Method實現IsNull斷定方法。
null object要保持原object的全部成員的不變性,因此咱們經常將其實現爲Sigleton模式。
Scott Doman說「在執行方法時返回null object而不是null值,能夠避免NullReferenceExecption異常的發生」,這徹底是對的。
5 結論
雖然形色匆匆,可是經過本文你能夠基本瞭解關於null這個話題的方方面面,堆積到一塊兒就是對一個概念清晰的把握和探討。技術的魅力,大概也正是如此而已吧,色彩斑斕的世界裏,即使是「什麼都沒有」的null,在我看來依然有不少不少。。。值得探索、思考和分享。
還有更多的null,例如LINQ中的null,SQL中的null,仍然能夠進行探討,咱們將這種思考繼續,所收穫的果實就越多。
Anytao | 2008-07-31 | 你必須知道的.NET
http://www.anytao.com/ | Blog: http://anytao.cnblogs.com/ | Anytao原創做品,轉貼請註明做者和出處,留此信息。
參考文獻
(Book)Martin Fowler,Refactoring: Improving the Design of Existing Code
(cnblogs)zhuweisky,使用Null Object設計模式
(blogs)Scott Dorman,Null Object pattern
二十二回 學習方法論
學習方法論
本文將介紹如下內容:
• .NET的核心知識彙總
• 學習.NET的聖經心得
1. 引言
最近經常爲學習中的問題而傷神,幸有管偉一塊兒經常就技術問題拿來討論,我已想將討論的內容以基本原貌的方式,造成一個系列[和管子對話] ,經過記錄的方式將曾經的友情和激情記錄在園子裏,除了勉勵本身,也可受用他人。所以[和管子對話] 系列,純屬口頭之說,一家之言,並且東拉西撤。可是卻給我一個很好的啓示,就是將學習的東西,尤爲是基礎性的本質做爲系統來經常回味在腦子裏,案頭間。
因此纔有了這個系統[你必須知道的.NET]浮出水面,系列的主要內容就是.NET技術中的精華要點,以基礎內容爲主,以設計思想爲輔,有本身的體會,有拿來的精品,初步的思路就是以實例來說述概念,以簡單來表達本質。由於是總結,由於是探索,因此post中的內容難免有取之於民的東西,我將盡己可能的標註出處。
2. 目錄
談起.NET基礎,首先我將腦子的清單列出,本系列的框架也就天然而然的和盤推出,同時但願園子的朋友盡力補充,但願能把這個系列作好,爲初學的人,爲迷茫的人,開一條通途
第二十一回:學習方法論
本文,源自我回答剛畢業朋友關於.NET學習疑惑的回覆郵件。
本文,其實早計劃在《你必須知道的.NET》寫做之初的後記部分,可是由於箇中緣由未能如願,算是補上本書的遺憾之一。
本文,做爲[《你必須知道的.NET》]系列的第20回,預示着這個系列將開始新的征程,算是[你必須知道的.NET]2.0的開始。
本文,做爲一個非技術篇章,加塞兒到《你必須知道的.NET》隊伍中,我想至少由於回答瞭如下幾個必須知道的非技術問題:.NET應該學習什麼? .NET應該如何學習? .NET的學習方法?
本文,不適合全部的人。
開始正文:
關於這個問題,也有很多剛剛入行的朋友向我問起。我想可能一千我的就有一千個答案,我不能保證本身的想法適合於全部的人,可是這確實是我本身的體會和經歷,但願能給你一些參考的價值。同時,我也嚴正的聲明,我也是個學習者,也在不斷的追求,因此這裏的體會只是交流,並不是說教。
做爲同行,首先恭喜你進入了一個艱難困苦和其樂無窮並存的行業,這是軟件的現狀,也是軟件的將來。若是你想迅速成功,或者發家致富,顯然是個難以實現的夢想。老Bill和李彥宏在這個行業是難以複製的,因此作好長期堅苦卓絕的準備是必須的。至少,我身邊的朋友,包括我本身都是經歷了這個過程,並且依然在這個過程當中,累並快樂着。因此,如此辛苦,又沒有立竿見影的「錢」途,想要在這個領域有所發展,只能靠堅持和興趣了。兩者缺一不可,對於剛剛畢業的你來講,這個準備是必須有的。這是個人第一個體會,可能比較虛,可是這個在我看來倒是最重要的一條。
第一條很關鍵,可是除了在思想上作好準備,還有應該就是你關心的如何下手這個問題了?從本身的感受來講,我以爲比較重要的因素主要包括:
1 基礎至上。
其實早在兩年前,我也存在一樣的疑惑,不少的精力和時間花費在了追求技術技巧、技術應用和技術抄襲的自我陶醉狀態。歷數過去的種種光輝歷程,不少寶貴的人生都花在交學費的道路上了。因此,當我把所有的精力投入到基礎和本質研究的課題上時,居然發現了別樣的天地。原來再花哨的應用,再絕妙的技巧,其實都架構在技術基礎的基礎上,沒有對技術本質的深入理解,談何來更進一步瞭解其餘。這種體會是真實而有效的,因此我將體會、研究和心得,一路分享和記錄下來,因而就有了《你必須知道的.NET》這本書的誕生,我切實的以爲從這個起點開始,瞭解你必須知道的,才能瞭解那些更廣闊的技術領域。
因此,若是可以堅持,不放棄枯燥,從基礎開始踏踏實實的學習基礎,我想你必定會有所突破。而這個突破,其實也有着由量到質的飛躍,以.NET爲例,我認爲了解CLR運行機制,深入的認識內存管理,類型系統,異常機制,熟悉FCL基本架構,學習c#語言基礎,認識MSIL、元數據、Attribute、反射、委託等等,固然還包括面向對象和設計架構,都是必不可少的基礎內容。你能夠從《你必須知道的.NET》的目錄中來大體瞭解到應該掌握的基礎內容,順便廣告了:-)
話音至此,順便推薦幾本基礎方面的書,若是有時間能夠好好研究研究:
Don Box, Chris Sells, Essential .NET,一本聖經,深入而又深邃,爲何不出第二卷?
Jeffrey Richter, Applied Microsoft .NET Framework Programming,.NET世界的惟一經典,偶像級的Jeffrey是個人導師。
Patrick Smacchia, Pracical .NET2 and C#2,.NET領域的百科全書,能夠看成新華字典來讀技術。
Richard Jones, Rafael D Lins, Garbage Collection: Algorithms for Automatic Dynamic Memory Management,內存管理方面,就靠它了。
Christian Nagel, Bill Evjen, Jay Glynn, Professional C# 2005,c#基礎大全,你們都在看,因此就看吧。
Thinking in Java,是的,一本Java書,可是帶來的不只僅是Java,寫書寫到這份上,不可不謂牛叉。
Anytao, 你必須知道的.NET,我很自信,沒有理由不推薦,這本書有其餘做品所沒有的特別之處,雖不敢恬列於大師的經典行列,可是推薦仍是經得起考驗。
我一直主張,書不在多,有仙則靈。上面的幾本,在我看來就足以打好基礎這一關。固然若是有更多的追求和思索,還遠遠不夠,由於技術的腳步從未止步。可是,至少至少,應該從這裏開始。。。
2 你夠OO嗎?
無論對業界對OO如何詬病,無論大牛對OO如何不懈,那是他們折騰的事業。而咱們的事業卻沒法遠離這片看似神祕的王國,由於但凡從項目和產品一路廝殺而來的高手,都理解OO的強大和神祕。站在高高的塔尖來看軟件,玩來玩去就是這些玩意兒了。因此,在我看來OO其實也是軟件技術的必要基礎,也是技術修煉的基本功之一,所以我也絕不猶豫的將對面向對象的理解歸入了《你必須知道的.NET》一書的第一部分範疇。
然而,實話實說,OO的修煉卻遠沒有.NET基礎來得那麼容易,苦嚼一車好書,狂寫萬行代碼,也未必可以徹底領悟OO精妙。說得玄乎點兒,這有些像悟道,想起明代前無古人後無來着的心學開創者王陽名先生,年輕時天天格物修煉的癡呆場景,我就以爲這玩意兒實在不靠譜。其實,不多有人能徹底在OO面前說徹悟,因此咱們你們都不例外。可是由於如此重要,以致於咱們必須找點兒東西或者思路來摩拳擦掌,瞭解、深刻和不斷體會,因此我對面向對象的建議是:始終如一的修煉,打好持久戰。
如何打好仗呢,不例外的先推薦幾本經典做品吧:
EricFreeman, Elisabeth Freeman. Head First Design Patterns,標準的言簡意賅,形象生動,可貴佳做。
Erich Gamma, Richard Helm, Ralph Johnson, John Vlisside,設計模式-可複用面向對象軟件的基礎,開山祖師的做品,不獨白不讀。
Martin Fowler, Refactoring: Improving the Design of Existing Code,一樣的經典,很拉風。
Robert C. Martin,敏捷軟件開發:原則、模式與實踐,對於設計原則,無出其右者。
張逸,軟件設計精要與模式,國內做品的優秀做品,園子裏的經典之做。
有了好書,仍是遠遠不夠的。因此,還得繼續走王陽明的老路,今天格一物,明天格一物,看見什麼格什麼。用我們的專業術語說,就是不斷的學習和實踐他人的智慧結晶,看經典框架,寫熟練代碼。個人一位偶像曾語重心長的告訴我,作軟件的不寫上千萬行代碼,根本就沒感受。按照這個標準衡量一下本身,我發現我還只是小學生一個,因此廢話少說,仍是去格物吧。
那麼OO世界的物又是什麼,又該如何去格,在我看來大體能夠包括下面這些內容,可是分類不按學科標準:
面向對象的基本內容:類、對象、屬性、方法、字段。
面向對象的基本要素:封裝、繼承、多態,我再外加一個接口。
設計原則:接口隔離、單一職責、開放封閉、依賴倒置、Liskov替換,沒什麼可說的,這些實在過重要了。
設計模式:也沒有可說的,實在過重要了。
Singleton
Abstract Factory
Factory Method
Composite
Adapter
Bridge
Decorator
Facade
Proxy
Command
Observer
Template Method
Strategy
Visitor
分層思想:例如經典的三層架構
模塊化
AOP
SOA
ORM
......
這些OO領域的基本內容,看起來令郎滿目,其實互相聯繫、互爲補充,沒有獨立的分割,也沒有獨立的概念,瞭解這個必然牽出那個,因此修煉起來並不孤單,反倒在不斷的領悟中可以竊喜原來軟件也能夠如此精彩。
3 捨得,是門藝術。
有了技術基礎,懂得修煉OO,下面就是捨得的問題了。捨得捨得,不捨怎得?
.NET技術有着近乎誇張的應用範疇,從Windows GDI應用,到ASP.NET Web應用,到WCF分佈式應用,到Window Mobile嵌入式應用,到ADO.NET數據處理,到XML Webservice,.NET無處不在。因此,對於.NET技術的學習,你應該有個起碼的認識,那就是:我不可能瞭解.NET的整個面貌, 還有個起碼的問題繼續,那就是:我還要學嗎?
固然不可能瞭解全部,所以你必須選擇和捨得,選擇有方向,捨得有興趣;我還要學嗎?固然要學,可是應該首先清楚如何學?在這麼多眼花繚亂的技術應用中,有一個基礎始終支撐着.NET技術這艘航母在穩步前行,無論是什麼應用,無論是什麼技術,無論是什麼框架,CLR老是.NET技術的心臟。經過表面來傾聽心臟的聲音,才能更好的瞭解機器的運轉,順着血管的脈絡瞭解框架,才能明白機制背後的玄機。層出不窮的新技術和新名詞老是能吸引你的眼球,可是永遠不要只盯着那塊蛋糕,而掉了整個禮物,因此對.NET的學習必定要打好基礎,從瞭解CLR底層機制和.NET框架類庫開始,逐漸的追求你的技術選擇。
善於分辨,不盲從。天天上cnblogs、MSDN和其餘的訂閱技術文章,是個人習慣,可是若是每篇都讀,每篇都看,那就基本沒有其餘的時間,因此你必須有分辨的能力,和抵抗誘惑的心態。找準本身的方向,而且堅持下來,是難能難得的。
在這方面,沒有參考,也沒有推薦,全屏本身的慧眼。眼光,是個關鍵。
4 讀幾本經典的做品。
這一點其實並不須要多說,推薦的幾本做品值得花點兒功夫來學習,由於這的確是最初的開始,走在路上從起跑線就走錯了方向,大體快速追上是比較可貴。因此經典的做品就是一個好的起點,我也會不時的在我的博客中推薦更好的專著,但願你繼續關注J
5 遵照規範,養成良好的編程習慣。
其實這是個看似無足輕重的小事兒,我常常看到自覺得天下無敵的高手,胡亂的在編輯器中揮灑天賦,一陣高歌猛進,但最後本身都不知道當初的本意是什麼。軟件是個可持續的資源,於人於己都遵照點兒規則,出來混是要有點兒職業道德。對本身而言,良好的編程習慣正是一個良好學習習慣的開始。看着本身的代碼,感受像藝術通常優雅,大體也就是周杰倫聽到東風破時候的感受吧,怎一個爽字了得。
推薦一本這方面的書:
Krzysztof Cwalina,Brad Abrams , .NET 設計規範--.NET約定、慣用法與模式
6 學習,講究方法。
具體的學習方法,實在是因人而異,我歷來不主張學習他人的方法,由於人性是難以複製的東西。本身的只有本身最清楚,因此你能夠模仿他人的技藝,可是用於沒法刻畫其靈魂。關於學習方法這檔子事兒,我向來不喜歡參考他人,也更不喜歡推薦。
可是,即使如此,絲絕不減弱學習方法的重要性,懂得了解本身的人是真正的智者,因此挖掘自身潛力永遠是擺在本身眼前的課題。尋找一套行之有效的方式方法,很是的重要,可是不要學着模仿,這方面我以爲只有創新才能成功。
若是實在沒有本身的方法,我就以爲沒有方法就是好方法,苦練多看,永遠不過期。
7 找一個好老師。
若是有幸能有一位德高望重而又樂於奉獻的師長指導,那的確是人生之幸運,可是這種機率實在是過小了。我沒有遇上,因此大部分人也無法遇上。沒辦法,仍是須要好的老師,那麼哪兒有這樣才高而又德厚的人才呢?
答案是互聯網。google,baidu,一個都不能少。
MSDN是個好工具,博客園是個好地方,《.NET禪意花園》是個好開始。
8 英文,無可避免。
前面說過,要不斷的修煉和格物,要學習好的做品,認識好的框架。很不幸的是,這些好事兒全被老外佔了,由於原本就是從他們那裏開始的,因此也不須要泄氣。中國人自古都是師夷長技以制夷的高手,但願軟件產業的大旗別在咱們手上倒下。可是,話說回來,英文就成了一個必須而又傷神的攔路虎,可是沒辦法使勁的嚼吧。多看多寫多讀,也就能應付了。
關於英文的學習和成長,我並不寄但願於在什麼英語速成班裏走回頭路,學校苦幹這麼多年也每隔名趟,因此下手仍是務實點兒,我推薦幾個好的英文網站和大牛博客,算是提升技術的同時提升英語,一舉兩得,一箭雙鵰:
http://www.gotdotnet.com/
http://codeproject.com/
http://www.asp.net/
http://codeguru.com/
http://www.c-sharpconer.com/
http://blogs.msdn.com/bclteam/
http://blogs.msdn.com/ricom/
http://samgentile.com/blog/
http://martinfower.com/bliki
http://blogs.msdn.com/kcwalina/
http://www.pluralsight.com/blogs/dbox/default.aspx
http://blogs.msdn.com/cbrumme/
固然這裏羅列的並不是所有,MSDN、asp.net自沒必要說,能夠有選擇的瀏覽。
上述1+7條,是一些並不是經驗的經驗,誰都知道,但不是誰都能作到。累並快樂着,永遠是這個行業,這羣人的主旋律。在技術面前,我嫣然一笑,發現本身其實很專一,這就夠了。
好了,囉裏囉唆,多是經驗,多是廢話。正如一開始所說,做爲一個過來人,我只想將本身的心得拿出來交流,絕沒有強加於人的想法。除了推薦的幾本做品,你能夠有選擇的參考,其餘的甚至能夠全盤否認。心懷坦誠的交流,說到底就是但願更多的人少走我曾經曲曲折折的彎路,那條路上實在是幸福與心酸一股腦子毀了一段青春。
祝晚安。程序員