《框架設(shè)計(jì)(第2版)》:CLR_Via_C#_第1頁
《框架設(shè)計(jì)(第2版)》:CLR_Via_C#_第2頁
《框架設(shè)計(jì)(第2版)》:CLR_Via_C#_第3頁
《框架設(shè)計(jì)(第2版)》:CLR_Via_C#_第4頁
《框架設(shè)計(jì)(第2版)》:CLR_Via_C#_第5頁
已閱讀5頁,還剩71頁未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

147第6章 類型和成員基礎(chǔ)第部分 類型的設(shè)計(jì)=第6章 類型和成員基礎(chǔ)=第7章 常量和字段=第8章 方法:構(gòu)造器、操作符、轉(zhuǎn)換操作符 和參數(shù)=第9章 屬性=第10章 事件=第6章 管理錯(cuò)誤和異常 第一類型和成員基礎(chǔ)本章內(nèi)容l 類型成員的種類l 類型的可見性l 成員的可訪問性l 靜態(tài)類l 部分類、結(jié)構(gòu)和接口l 組件、多態(tài)和版本控制在第部分“類型的使用”中,我們重點(diǎn)介紹類型以及任何類型的所有實(shí)例通用的操作,并闡述如何將類型劃分為引用類型和值類型。在本章及本部分的后續(xù)章節(jié),我們將示范如何使用可以在類型中定義的各種成員來設(shè)計(jì)類型。從第7章到第10章,我們將詳細(xì)討論各種成員。6.1 類型成員的種類類型可以定義0個(gè)或者多個(gè)下述類型的成員:l 常量(第7章) 常量就是一個(gè)用來標(biāo)識數(shù)據(jù)值恒定不變的符號。這些符號通常用于使代碼更容易閱讀和維護(hù)。常量通常與類型關(guān)聯(lián),而不與類型的實(shí)例關(guān)聯(lián)。從邏輯上講,常量通常是靜態(tài)成員。l 字段(第7章) 字段表示一個(gè)只讀或可讀可寫的數(shù)據(jù)值。字段可以是靜態(tài)的;這種情況下,字段是類型狀態(tài)的一部分;字段也可以是實(shí)例(非靜態(tài)的),這種情況下,字段是對象狀態(tài)的一部分。強(qiáng)烈建議將字段聲明為私有字段,以免被該類型外部的代碼破壞類型或者對象的狀態(tài)。l 實(shí)例構(gòu)造器(第8章) 實(shí)例構(gòu)造器是一種將新對象的實(shí)例字段初始化為有效初始狀態(tài)的特殊方法。l 類型構(gòu)造器(第8章) 類型構(gòu)造器是一種將類型的靜態(tài)字段初始化為有效初始狀態(tài)的特殊方法。l 方法(第8章) 方法是一個(gè)執(zhí)行改變或者查詢類型狀態(tài)(靜態(tài)方法)或者對象狀態(tài)(實(shí)例方法)操作的函數(shù)。方法通常讀或?qū)戭愋突蛘邔ο蟮淖侄巍 操作符重載(第8章) 操作符重載指對對象應(yīng)用特定操作符時(shí)定義對象如何操作的方法。因?yàn)椴⒉皇撬械木幊陶Z言都支持操作符重載方法,所以操作符重載方法不屬于公共語言規(guī)范(Common Language Specification,CLS)。l 轉(zhuǎn)換操作符(第8章) 轉(zhuǎn)換操作符是定義如何隱式或者顯式地將對象從一種類型轉(zhuǎn)換(或強(qiáng)制轉(zhuǎn)換)到另一種類型的方法。和操作符重載方法一樣,并不是所有的編程語言都支持轉(zhuǎn)換操作符,因此轉(zhuǎn)換操作符也不屬于CLS。l 屬性(第9章) 屬性是指允許使用一個(gè)簡單的、字段形式的語法來設(shè)置或者查詢類型(靜態(tài)屬性)或?qū)ο?實(shí)例屬性)的部分邏輯狀態(tài),并且保證該狀態(tài)不被破壞的一種機(jī)制。屬性可以沒有參數(shù)(這種情況非常普遍),也可以有多個(gè)參數(shù)(這種情況相當(dāng)少見,但是經(jīng)常在集合類(collection class)中使用)。l 事件(第10章) 靜態(tài)事件(static event)是指允許類型為監(jiān)聽類型(listening type)或者監(jiān)聽對象(listening object)發(fā)送通知的機(jī)制。實(shí)例(非靜態(tài))事件(instance event)是指允許對象為監(jiān)聽類型或者監(jiān)聽對象發(fā)送通知的機(jī)制。事件的觸發(fā)通常是為了響應(yīng)產(chǎn)生事件的類型或者對象的狀態(tài)發(fā)生的改變。事件包含兩個(gè)方法,允許類型或者對象(通常稱為監(jiān)聽者(listener)訂閱或者注銷事件。除了這兩個(gè)方法,事件通常還使用一個(gè)委托字段(delegate field)來維護(hù)已訂閱該事件的監(jiān)聽者。l 類型 類型可以在類型內(nèi)部嵌套地定義其他類型。通常使用這個(gè)方法將一個(gè)大的、復(fù)雜的類型分解成小的構(gòu)建塊(building block),以此來簡化實(shí)現(xiàn)。再次聲明本章的目的不是為了詳細(xì)地描述各種類型的成員,而是闡明各種類型的成員都擁有的共性,為后面的章節(jié)打一個(gè)基礎(chǔ)。無論使用什么編程語言,相應(yīng)的編譯器都必須能夠處理前述列表中所有類型的成員的源代碼,并且能夠?yàn)槊總€(gè)成員生成元數(shù)據(jù)(metadata)和中間語言(Intermediate Language,IL)代碼。元數(shù)據(jù)的格式與源代碼所使用的編程語言無關(guān),因此元數(shù)據(jù)的格式都是相同的,這使得CLR成為名副其實(shí)的“公共語言運(yùn)行庫”(Common Language Runtime,CLR)。元數(shù)據(jù)是所有語言都可以生成和使用的公共信息,它使某一編程語言編寫的代碼可以無縫地訪問另一個(gè)完全不同的編程語言編寫的代碼。CLR同樣也使用公共元數(shù)據(jù)格式,CLR用它們決定常量、字段、構(gòu)造器、方法、屬性和事件的行為在運(yùn)行時(shí)如何表現(xiàn)。簡單地說,元數(shù)據(jù)就是整個(gè)Microsoft.NET Framework開發(fā)平臺的關(guān)鍵,它允許編程語言、類型和對象之間的無縫集成。下面的C#代碼示范了包含所有可能的成員的類型定義,這段代碼可以通過編譯(不過會(huì)出現(xiàn)警告信息),但是它并不能代表我們通常所要?jiǎng)?chuàng)建的類型,因?yàn)檫@段代碼中定義的大部分方法根本沒有做任何有價(jià)值的事情。這里僅僅是為了示范編譯器如何將類型及其成員轉(zhuǎn)換成元數(shù)據(jù)。再次聲明,在后續(xù)章節(jié)中我們將對這些成員逐一進(jìn)行討論。using System; public sealed class SomeType / 1 /嵌套類 private class SomeNestedType / 2 /常量、只讀字段和靜態(tài)讀寫字段 private const Int32 SomeConstant = 1; / 3 private readonly Int32 SomeReadOnlyField = 2;/ 4 private static Int32 SomeReadWriteField = 3; / 5 /類型構(gòu)造器 static SomeType() / 6 /實(shí)例構(gòu)造器 public SomeType() / 7 public SomeType(Int32x) / 8 /實(shí)例方法和靜態(tài)方法 private String InstanceMethod() returnnull;/ 9 public static void Main() /10 /實(shí)例無參屬性 public Int32 SomeProp /11 get return0; /12 set /13 /實(shí)例有參屬性 public Int32 thisString s /14 getreturn0; /15 set /16 /實(shí)例事件 public event EventHandler SomeEvent; /17 如果編譯剛才定義的類型,然后用ILDasm.exe來查看得到的元數(shù)據(jù),將看到如圖6.1中所示的輸出結(jié)果。圖6.1 上述代碼的元數(shù)據(jù)在ILDasm.exe中的顯示輸出注意,編譯器為定義在源代碼中的所有成員都產(chǎn)生了相關(guān)的元數(shù)據(jù)。實(shí)際上,對于其中的一些成員編譯器,還為它們產(chǎn)生了額外的成員以及額外的元數(shù)據(jù)。例如,事件成員(代碼中編號為17)使編譯器生成了一個(gè)字段、兩個(gè)方法及一些額外的元數(shù)據(jù)。這里并不希望大家能完全理解其中的內(nèi)容。但是,當(dāng)學(xué)習(xí)完后面幾章內(nèi)容后,希望大家能回顧一下這個(gè)范例,看看成員是如何定義的,以及對編譯器生成的元數(shù)據(jù)有何影響。6.2 類型的可見性在文件范圍內(nèi)定義類型時(shí)(對應(yīng)是在某一類型中嵌套地定義另一個(gè)類型),可以指定類型的可見性為public或者internal。public類型不僅對定義該類型的程序集中的所有代碼可見,而且對其他程序集中的代碼也可見。internal類型僅對定義該類型的程序集中的所有代碼可見,而對其他程序集中的代碼不可見。定義類型時(shí),如果沒有顯式地指定類型的可見性,C#編譯器會(huì)將類型的可見性設(shè)為internal(兩者之中約束性比較強(qiáng)的一個(gè))。下面用幾個(gè)例子加以說明:using System;/下述類型的可見性為public,它既可以被本程序集中的代碼訪問,又可以被其他程序集中的代碼訪問public class ThisIsAPublicType . /下述類型的可見性為internal,它只可以被本程序集中的代碼訪問internal class ThisIsAnInternalType . /因?yàn)闆]有顯式地聲明該類型的可見性,所以下述類型的可見性為internalclass ThisIsAlsoAnInternalType . 友元程序集假定下述情形:某公司的工作組TeamA在某個(gè)程序集中定義了一組工具類型(utility type),并且他們希望公司的另一個(gè)工作組TeamB中的成員可以使用這些類型。但是由于各種原因,如時(shí)間安排、地理位置、不同的成本中心或報(bào)告結(jié)構(gòu),這兩個(gè)工作組不能將他們所有的工具類型構(gòu)建在一個(gè)程序集中,相反,每個(gè)工作組都生成了自己的程序集。為了使工作組TeamB的程序集使用工作組TeamA的程序集中的類型,TeamA必須將他們所有的工具類型的可見性定義為public。但是,這意味著工作組TeamA的程序集中的工具類型將對所有的程序集公開可見,另一公司的開發(fā)人員可以編寫使用這些公開的工具類型的代碼,而這并不是所期望的?;蛟S這些工具類型確實(shí)假定工作組TeamB保證他們編寫代碼時(shí)使用工作組TeamA的工具類型(譯注:即工作組TeamB必須使用工作組TeamA定義的工具類型,這意味著又不得不將TeamA的工具類型的可見性定義為public)。我們希望有這樣一種方法,即工作組TeamA將他們的工具類型的可見性定義為internal,同時(shí)仍然允許工作組TeamB訪問這些類型。CLR和C#通過友元程序集(friend assembly)來實(shí)現(xiàn)。在構(gòu)建程序集的過程中,程序集可以通過使用命名空間(namespace) System.Runtime. CompilerServices中定義的屬性InternalsVisibleTo來標(biāo)明程序集認(rèn)為是“友元”的其他程序集。該屬性擁有一個(gè)字符串參數(shù)來標(biāo)識友元程序集的名稱和公鑰(public key)(傳遞給屬性的字符串不能包含版本、語言文化和處理器架構(gòu)的信息)。注意,友元程序集可以訪問程序集中的所有internal類型,以及這些類型的所有internal成員。下面的范例示范了程序集如何將兩個(gè)強(qiáng)命名的程序集“Wintellect”和“Microsoft”指定為它的友元程序集:using System;using System.Runtime.CompilerServices; / 對于InternalsVisibleTo屬性/該程序集中可見性為internal的類型可以被下面兩個(gè)程序集中的任何代碼訪問(不用考慮這兩個(gè)程序集的版本和語言文化):assembly:InternalsVisibleTo(Wintellect, PublicKey=12345678.90abcdef)assembly:InternalsVisibleTo(Microsoft, PublicKey=b77a5c56.1934e089)internal sealed class SomeInternalType . internal sealed class AnotherInternalType . 從友元程序集中訪問上述程序集中的internal類型沒有什么價(jià)值。例如,下面范例示范了公鑰為“12345678.90abcdef”的友元程序集“Wintellect”如何訪問上述程序集中內(nèi)部類型SomeInternalType。using System;internal sealed class Foo private static Object SomeMethod() /這個(gè)“Wintellect”程序集可以訪問其他程序集中可見性為internal的類型,就好像這個(gè) /類型的可見性是public的一樣 SomeInternalType sit = new SomeInternalType(); return sit; 程序集中類型的internal成員可以被友元程序集訪問,所以需要認(rèn)真考慮類型成員的可訪問性以及將哪個(gè)程序集聲明為友元程序集。注意,C#編譯器在編譯友元程序集(沒有包含屬性InternalsVisibleTo的程序集)時(shí),需要使用編譯器開關(guān)/out:。使用編譯器開關(guān)的原因在于,編譯器需要知道要編譯的程序集的名稱,從而確定是否將最后所得到的程序集作為友元程序集。或許你認(rèn)為C#編譯器會(huì)主動(dòng)地確定最后得到的程序集是不是友元程序集,雖然C#編譯器通常主動(dòng)地確定輸出文件的名稱;但是,C#編譯器直到代碼結(jié)束編譯時(shí)才能確定輸出文件的名稱。因此,使用/out:編譯器開關(guān)可以極大地改善編譯的性能。同樣,如果在編譯模塊時(shí)(與程序集相對)使用C#編譯器的/t:module開關(guān),那么該模塊將成為友元程序集的一部分,另外還需要使用C#編譯器的/moduleassemblyname:開關(guān)來編譯模塊,該開關(guān)向編譯器聲明模塊將是哪個(gè)程序集的一部分,以便編譯器可以允許模塊中的代碼訪問其他程序集中的內(nèi)部類型。重要提示友元程序集特征只能由在同一時(shí)間進(jìn)度表上發(fā)布(ship)的程序集使用,甚至要求程序集一起發(fā)布。這是因?yàn)橛言绦蚣g的相互依賴程度很強(qiáng),以至于在不同時(shí)間進(jìn)度表上發(fā)布的友元程序集極有可能導(dǎo)致兼容性問題。如果希望程序集在不同的時(shí)間進(jìn)度表上發(fā)布,應(yīng)將任何一個(gè)程序集都可以使用的類設(shè)置為public,并通過設(shè)置StrongNameIdentityPermission類的LinkDemand請求限制可訪問性。6.3 成員的可訪問性定義類型(包括嵌套類型)的成員時(shí),可以指定成員的可訪問性。成員的可訪問性表明目標(biāo)代碼可以合法訪問哪些成員。CLR定義了所有可能的可訪問性修飾符,但是編程語言在對成員應(yīng)用可訪問性時(shí),挑選了一些修飾符,并將它們術(shù)語化以供開發(fā)人員使用。例如,CLR使用術(shù)語Assembly來表明成員對同一程序集內(nèi)的所有代碼可見,而C#語言將其命名為internal。表6.1展示了6個(gè)可以用于成員可訪問性的修飾符。表6.1的行按約束性從最高約束private到最低約束public的順序排列。表6.1 成員的可訪問性CLR術(shù)語C#術(shù)語描 述Privateprivate成員只能由定義該成員的類型中的方法或者該類型的所有嵌套類型中的方法訪問Familyprotected成員只能由定義該成員的類型中的方法、該類型的所有嵌套類型中的方法或者該類型的一個(gè)派生類型(與程序集無關(guān))的方法訪問Family和Assembly(不支持)成員只能由定義該成員的類型中的方法、該類型的所有嵌套類型中的方法或者同一程序集中定義的該類型的所有派生類型中的方法訪問Assemblyinternal成員只能由定義該成員的程序集中的方法訪問Family或Assemblyprotected internal成員可以由定義該成員的類型的所有嵌套類型、所有派生類型(與程序集無關(guān))的方法或者定義該成員的程序集中的所有方法訪問Publicpublic成員可以由所有程序集的所有方法訪問當(dāng)然,對于任何可訪問的成員,都必須定義在一個(gè)可見的類型內(nèi)。例如,如果程序集AssemblyA定義了一個(gè)internal類型,該類型擁有一個(gè)公共方法,那么程序集AssemblyB中的代碼不能調(diào)用程序集AssemblyA中的公共方法,因?yàn)槌绦蚣疉ssemblyA中的internal類型對于程序集AssemblyB不可見。編譯代碼時(shí),編程語言的編譯器負(fù)責(zé)檢查代碼是不是正確地引用了類型和成員。如果代碼錯(cuò)誤地引用了類型或者成員,那么編譯器負(fù)責(zé)生成一個(gè)合適的錯(cuò)誤消息。另外,在運(yùn)行時(shí),JIT編譯器在將IL代碼編譯成本機(jī)CPU指令的過程中同樣確保對字段和方法的引用是合法的。例如,如果JIT編譯器檢測到代碼正在試圖不正當(dāng)?shù)卦L問一個(gè)私有字段或者方法,那么,JIT編譯器將分別拋出一個(gè)FieldAccessException或者M(jìn)ethodAccessException 異常。盡管編程語言的編譯器忽略可訪問性的檢查,但是在運(yùn)行時(shí)檢驗(yàn)IL代碼確保已引用成員的可訪問性是值得的,因?yàn)榇嬖诹硪环N極有可能的情況,即編程語言的編譯器將代碼編譯成訪問另一個(gè)類型(另一個(gè)程序集)中的公共成員,但是,在運(yùn)行時(shí)卻加載了一個(gè)不同版本的程序集,而在新版本的程序集中,公共成員的可訪問性已經(jīng)改為protected或者private。在C#中,如果沒有顯式地聲明成員的可訪問性,那么,編譯器通常(并不總是)將成員的可訪問性默認(rèn)設(shè)為private(可訪問性修飾符中約束性最強(qiáng)的一個(gè))。CLR要求接口類型的所有成員都是公開的。C#編譯器知道這一點(diǎn),因此禁止編程人員顯式指定接口成員的可訪問性,它會(huì)將所有成員的可訪問性設(shè)置為public。更多信息更多信息請參考C#語言規(guī)范中“已聲明的可訪問性”一節(jié),了解C#語言可以對類型和成員使用什么類型的可訪問性,以及C#語言根據(jù)聲明位置的上下文選擇哪一種默認(rèn)可訪問性。另外,請注意CLR提供了一個(gè)稱為Family and Assembly的可訪問性,但是,C#語言并不支持這個(gè)可訪問性,因?yàn)镃#團(tuán)隊(duì)認(rèn)為不需要這個(gè)可訪問性,因此決定不在C#語言中集成它。派生類型重寫在基礎(chǔ)類型中定義的成員時(shí),C#編譯器要求原始成員和重寫成員擁有相同的可訪問性。也就是說,如果基類中成員是受保護(hù)的,那么派生類中的重寫成員也必須是受保護(hù)的。但是,這只是C#語言的約束,而不是CLR的約束。當(dāng)從基類派生類時(shí),CLR允許成員的可訪問性的約束性變得較低,而不允許變得較高。例如,類可以重寫在其基類中定義的受保護(hù)的方法,并將重寫的方法設(shè)置為public(更容易訪問)。但是,類不能重寫在其基類中定義的受保護(hù)的方法,并將重寫的方法設(shè)置為private(更難訪問)。類不能將基類方法的可訪問性設(shè)置得更嚴(yán)格,因?yàn)榕缮惖挠脩敉ǔ?梢詮?qiáng)制轉(zhuǎn)換基礎(chǔ)類型來獲得對基類方法的訪問。如果CLR允許了派生類型的方法更難訪問,那么,CLR將聲稱該方法不能強(qiáng)制實(shí)施。6.4 靜 態(tài) 類CLR中有一些永遠(yuǎn)不需要實(shí)例化的類,例如Console,Math,Environment和ThreadPool類。這些類僅擁有靜態(tài)成員,實(shí)際上,這些類只是將一組相關(guān)的成員組合在一起。例如,Math類定義了一組與數(shù)學(xué)運(yùn)算相關(guān)的方法。C#允許通過使用C#中的關(guān)鍵字static定義非實(shí)例化(non-instantiable)的類。關(guān)鍵字static僅可以用于類,而不能用于結(jié)構(gòu)(值類型),這是因?yàn)镃LR要求值類型必須實(shí)例化,并且沒有方法停止或阻止該實(shí)例化過程。C#編譯器對靜態(tài)類強(qiáng)加如下所示:l 靜態(tài)類必須直接從基類System.Object派生,這是因?yàn)閺钠渌惻缮念愑捎诶^承性僅適用于對象的緣故而沒有任何意義,而且不能創(chuàng)建靜態(tài)類的實(shí)例。l 靜態(tài)類不能實(shí)現(xiàn)任何接口,這是因?yàn)橹挥惺褂妙惖膶?shí)例時(shí)才去調(diào)用類的接口 方法。l 靜態(tài)類只能定義靜態(tài)成員(字段、方法、屬性和事件),任何實(shí)例成員都將導(dǎo)致編譯器產(chǎn)生錯(cuò)誤。l 靜態(tài)類不能用作字段、方法參數(shù)或者局部變量,這是因?yàn)檫@些用法都將表明變量引用了實(shí)例,而這是不允許的。如果編譯器檢測到這樣的用法,編譯器就會(huì)產(chǎn)生一個(gè)錯(cuò)誤。l 下面的代碼是靜態(tài)類的范例,該類定義了一些靜態(tài)成員;該代碼可以通過編譯(不過會(huì)出現(xiàn)警告信息),但是這個(gè)類沒有做任何有意義的事情。using System; public static class AStaticClass public static void AStaticMethod() public static String AStaticProperty get return s_AStaticField; set s_AStaticField = value; private static String s_AStaticField; public static event EventHandler AStaticEvent; 如果將上述代碼編譯為庫(DLL)程序集,并通過ILDasm.exe查看結(jié)果,會(huì)發(fā)現(xiàn)結(jié)果如圖6.2所示。通過使用關(guān)鍵字static定義的類將導(dǎo)致C#編譯器將該類同時(shí)標(biāo)記為abstract和sealed。另外,編譯器不會(huì)在類型中生成實(shí)例構(gòu)造器方法。注意,圖6.2中沒有實(shí)例構(gòu)造器(.ctor)方法。圖6.2 ILDasm.exe表明類在元數(shù)據(jù)中是抽象、密封的6.5 部分類、結(jié)構(gòu)和接口本節(jié)將討論部分類(partial class)、結(jié)構(gòu)和接口。此處需要注意C#編譯器完全支持該特征(其他編譯器同樣也支持該特征),但CLR不需要了解部分類、結(jié)構(gòu)和接口。實(shí)際上,由于本書關(guān)注的重點(diǎn)是C#所支持的CLR特性,所以為了完整性增加了本節(jié)的內(nèi)容。關(guān)鍵字partial向C#編譯器聲明某個(gè)單獨(dú)類、結(jié)構(gòu)或者接口定義的源代碼可以劃分為一個(gè)或者多個(gè)源代碼文件。將某個(gè)類型的源代碼分隔成多個(gè)文件主要有如下兩大原因:l 源代碼控制(Source control) 假定某個(gè)類型的定義由許多源代碼組成,而且有一程序開發(fā)人員在沒有使用源代碼控制方法校驗(yàn)類型定義的情況下對源代碼進(jìn)行了簽出(check out)。今后,其他程序開發(fā)人員如果不對源代碼進(jìn)行合并,就不能對類型進(jìn)行修改。使用關(guān)鍵字partial可以將類型的代碼分為多個(gè)源代碼文件,每個(gè)文件都可以單獨(dú)簽出,因此多個(gè)程序開發(fā)人員可以同時(shí)對類型進(jìn)行編輯。l 代碼拆分器(Code splitter) 在Microsoft Visual Studio中創(chuàng)建新的Windows窗體(form)或者Web窗體項(xiàng)目時(shí),就會(huì)在項(xiàng)目中自動(dòng)生成一些源代碼文件。這些源代碼文件中包含可以直接用于構(gòu)建這些類型的項(xiàng)目的模板。使用Visual Studio設(shè)計(jì)中心在Windows窗體或者Web窗體上拖放控件時(shí),Visual Studio自動(dòng)編寫源代碼,并且將代碼分別寫入多個(gè)源代碼文件中,這種方法確實(shí)提高了工作效率。事實(shí)是生成的代碼寫入同一個(gè)正在使用的源代碼文件中,這種情況的問題就在于如果偶然編輯了生成的代碼,可能會(huì)導(dǎo)致設(shè)計(jì)中心功能紊亂。以Visual Studio 2005為例,創(chuàng)建新的項(xiàng)目時(shí),Visual Studio創(chuàng)建兩個(gè)源代碼文件:一個(gè)用于用戶的代碼,另一個(gè)用于設(shè)計(jì)中心生成的代碼。因?yàn)樵O(shè)計(jì)中心生成的代碼位于一個(gè)單獨(dú)的文件,所以偶然編輯它的可能性就會(huì)很小。兩個(gè)文件的類型中都采用關(guān)鍵字partial。在將這兩個(gè)文件編譯在一起的過程中,編譯器將代碼組合,在最后所得到的結(jié)果.exe或.dll程序集(或者.netmodule模塊文件)中生成一個(gè)類型。正如本節(jié)前面所述,C#編譯器完全支持部分類型(partial type)特征,而CLR卻完全不支持部分類型,這就是為什么某個(gè)類型的所有源代碼文件必須使用同一種編程語言的原因,而且所有的源代碼文件必須編譯成一個(gè)單獨(dú)的編譯單元。6.6 組件、多態(tài)和版本控制面向?qū)ο蟮木幊陶Z言(Object-Oriented Programming,OOP)已經(jīng)面世好多年。在20世紀(jì)70年代末、80年代初首次使用面向?qū)ο蟮木幊陶Z言時(shí),應(yīng)用程序的規(guī)模非常小,而且應(yīng)用程序運(yùn)行所需的全部代碼都是由一家公司編寫的。的確,應(yīng)用程序背后還存在操作系統(tǒng),而且應(yīng)用程序確實(shí)使用了操作系統(tǒng)中可以使用的功能,但在當(dāng)時(shí),操作系統(tǒng)提供的功能與今天的操作系統(tǒng)相比確實(shí)少得可憐。今天,軟件變得相當(dāng)復(fù)雜,而且用戶要求應(yīng)用程序提供更豐富的功能,如GUI、菜單、鼠標(biāo)輸入、手寫板輸入、打印輸出、網(wǎng)絡(luò)功能等。出于這種原因,近幾年操作系統(tǒng)和開發(fā)平臺已經(jīng)取得實(shí)質(zhì)性的進(jìn)展。另外,對于應(yīng)用程序的開發(fā)人員想按照用戶的期望編寫應(yīng)用程序的所有代碼已經(jīng)不再可行,而且成本上也不劃算。今天,應(yīng)用程序由許多家不同的公司編寫的代碼組成,然后使用面向?qū)ο蟮囊?guī)范結(jié)合在一起。組件軟件編程(Component Software Programming,CSP)讓OOP達(dá)到了這樣的境界,下面是組件的相關(guān)屬性:l 組件(.NET中稱為程序集)具有“發(fā)布”的含義。l 組件擁有標(biāo)識(identity)(名稱、版本、語言文化和公鑰)。l 組件永遠(yuǎn)保存這些標(biāo)識(程序集中的代碼永遠(yuǎn)不會(huì)靜態(tài)地連接到另一個(gè)程序集;.NET通常使用動(dòng)態(tài)鏈接)。l 組件清楚地表明它所依賴的組件(引用元數(shù)據(jù)表)。l 組件應(yīng)為它的類和成員存檔。C#語言使用源代碼內(nèi)的XML文檔編制以及編譯器的/doc命令行開關(guān)提供存檔功能。l 組件必須指定所需的安全權(quán)限,CLR中的代碼訪問安全性(Code Access Security,CAS)功能提供該機(jī)制。l 組件發(fā)布一個(gè)對任何修訂(servicing)都不會(huì)改變的接口(對象模型)。修訂是組件的新版本,它的意圖是與組件的原始版本向后兼容。通常,修訂版包括程序錯(cuò)誤修復(fù)、安全補(bǔ)丁或者一些小功能的增強(qiáng),但是修訂不需要任何新的依賴關(guān)系,或者額外的安全權(quán)限。如最后一點(diǎn)所述,CSP的很大部分用來處理版本控制。組件隨著時(shí)間不斷改變,而且組件按照不同的時(shí)間進(jìn)度表其發(fā)布也不同。版本控制對CSP提出了一個(gè)全新的復(fù)雜性問題,該問題在OOP中不存在,這是因?yàn)樵贠OP中,所有的代碼都是由一家單獨(dú)的公司編寫、測試,并且作為一個(gè)單獨(dú)的單元發(fā)布。本節(jié)將討論組件的版本控制。在.NET中,版本號包含4個(gè)部分:主版本號、次版本號、內(nèi)部版本號和修訂版本號。例如,版本號為的程序集,其主版本號為1,次版本號為2,內(nèi)部版本號為3,修訂版本號為4。主/次版本號通常用于表示程序集的標(biāo)識,內(nèi)部/修訂版本號通常用于表示程序集的修訂版。假定某個(gè)公司安裝了一個(gè)版本號為的程序集。如果該公司稍后想修改該組件中的錯(cuò)誤,那么該公司將會(huì)生成一個(gè)新的程序集,新程序集的版本的內(nèi)部/修訂版本號將會(huì)發(fā)生改變,如4。這表明該程序集是一個(gè)修訂版,它與原始版本的組件(版本)向后兼容。從另一方面來講,如果該公司想生成一個(gè)新版本的程序集,而且該版本的程序集有非常明顯的改變,另外還不準(zhǔn)備與程序集的原始版本向后兼容,那么,該公司將真正創(chuàng)建一個(gè)全新的組件,新的程序集的版本號中的主/次版本號應(yīng)與已有組件的版本號不同(如)。 注意此處只是說明了如何考慮程序集的版本號。然而,CLR并不是按照這種方式對待版本號的?,F(xiàn)在,CLR將版本號看作一個(gè)不透明的值,如果某個(gè)程序集依賴于另一個(gè)版本號為的程序集,那么CLR僅試圖加載版本號為的程序集(除非此處存在一個(gè)重定向的綁定)。但是,Microsoft計(jì)劃在未來的版本中改變CLR的加載器(loader),以便加載程序集時(shí),對于指定的主/次版本號的程序集可以加載最新的內(nèi)部/修訂版本號的程序集。例如,在CLR的未來版本中,如果加載器試圖尋找版本號為的程序集,并且存在一個(gè)版本號為的程序集,那么加載器將自動(dòng)挑選程序集最新的修訂版。這將是CLR加載器最受歡迎的改進(jìn),我們翹首以待。前面討論了如何使用版本號來更新組件的標(biāo)識,以便反映出組件的新版本。下面要討論CLR和編程語言(如C#)提供的一些特征,這些特征允許程序開發(fā)人員編寫的代碼不受所用組件可能發(fā)生的變化的影響。當(dāng)某個(gè)組件(程序集)中定義的類型在另一個(gè)組件(程序集)中用作基類時(shí),版本控制問題便隨之而來。非常明顯,如果發(fā)生改變的基類的版本低于派生類的版本,派生類的行為同樣會(huì)發(fā)生改變,且可能會(huì)使類的行為不正常。在多態(tài)情形中,派生類型重寫基礎(chǔ)類型中定義的虛方法,絕對會(huì)發(fā)生這種現(xiàn)象。C#提供了5個(gè)可以用于類型或者類型成員的影響組件版本控制的關(guān)鍵字,這些關(guān)鍵字與CLR中支持組件版本控制的特征直接匹配。表6.2給出了與組件版本控制相關(guān)的C#的關(guān)鍵字,并且指出了每個(gè)關(guān)鍵字是如何影響類型和類型成員的定義的。表6.2 C#語言中的關(guān)鍵字及對組件版本控制的影響方法C#關(guān)鍵字類 型方法/屬性/事件常量/字段abstract表示該類型不能構(gòu)建實(shí)例表示在構(gòu)建派生類型的實(shí)例之前派生類型必須重寫并實(shí)現(xiàn)這個(gè)成員(不允許)virtual(不允許)表示這個(gè)成員可以由派生類型重寫(不允許)override(不允許)表示派生類型重寫了基礎(chǔ)類型的成員(不允許)sealed表示該類型不能用作基礎(chǔ)類型表示這個(gè)成員不能被派生類型重寫,該關(guān)鍵字僅用于重寫了虛方法的方法(不允許)new應(yīng)用于嵌套類型、方法、屬性、事件、常量或者字段時(shí),表示該成員與基類中類似的成員沒有關(guān)系在6.6.3節(jié)“類型版本控制過程中虛方法的處理”將示范這些關(guān)鍵字的值和使用方法。但是在討論版本控制問題之前,我們先討論一下CLR實(shí)際上是如何調(diào)用虛方法的。6.6.1 CLR如何調(diào)用虛方法、屬性和事件本節(jié)將討論方法,但是本節(jié)中的討論也與虛屬性(virtual property)和虛事件(virtual event)相關(guān)。屬性和事件實(shí)際上是作為方法實(shí)現(xiàn)的,這將在相應(yīng)的章節(jié)進(jìn)行示范。方法表示在類型(靜態(tài)方法)或者類型的實(shí)例(非靜態(tài)方法)上執(zhí)行操作的代碼。所有的方法都有名稱、簽名和返回值(可能為void)。CLR允許類型定義多個(gè)名稱相同的方法,只要這些方法具有不同的參數(shù)集或者不同的返回值。因此可以定義兩個(gè)具有相同名稱、相同參數(shù)的方法,只要這兩個(gè)方法的返回值類型不同。但是,除了IL匯編語言外,好像沒有其他語言具有該“特征”,大多數(shù)語言(包括C#語言在內(nèi))在確定惟一性時(shí),要求方法的參數(shù)不同,而忽略方法返回值的類型。(C#在定義轉(zhuǎn)換操作符方法時(shí)實(shí)際上放松了此限制,詳見第8章的介紹。)下面示范的Employee類定義了3個(gè)不同種類的方法:internal class Employee /非虛實(shí)例方法 public Int32 GetYearsEmployed() . /虛方法(虛擬隱含著實(shí)例) public virtual String GenProgressReport() . /靜態(tài)方法 public static Employee Lookup(String name) . 當(dāng)編譯器編譯上述代碼時(shí),編譯器在最后得到的程序集的方法定義表中寫入三個(gè)條目,每個(gè)條目都有一個(gè)標(biāo)記來表明方法是實(shí)例、虛的還是靜態(tài)的。所編寫的代碼調(diào)用這些方法時(shí),編譯器生成的調(diào)用代碼檢查方法定義的標(biāo)記,以此來確定如何生成正確的IL代碼,以便正確進(jìn)行調(diào)用。CLR為方法的調(diào)用提供了以下兩個(gè)IL指令:l IL指令call可以用來調(diào)用靜態(tài)方法、實(shí)例方法和虛方法。使用call指令調(diào)用靜態(tài)方法時(shí),必須指定CLR要調(diào)用的方法的類型。使用call指令調(diào)用實(shí)例方法或者虛方法時(shí),必須指定變量來引用對象。call指令假定變量不為null,換句話說,也就是變量本身的類型指出了用什么類型定義CLR要調(diào)用的方法。如果變量的類型沒有定義方法,則檢查基礎(chǔ)類型來匹配方法。指令call通常用來非虛擬地調(diào)用虛方法。l IL指令callvirt用來調(diào)用實(shí)例方法和虛方法,而不能調(diào)用靜態(tài)方法。使用callvirt指令調(diào)用實(shí)例方法或者虛方法時(shí),必須指定變量來引用對象。使用IL指令callvirt指令調(diào)用非虛實(shí)例方法時(shí),變量的類型指出了用什么類型定義CLR要調(diào)用的方法。使用IL指令callvirt調(diào)用虛實(shí)例方法時(shí),CLR查找用來調(diào)用的對象的實(shí)際類型,然后多形式地調(diào)用方法。為了決定類型,用來調(diào)用的變量通常不能為null,換句話說,也就是編譯該調(diào)用時(shí),JIT編譯器生成驗(yàn)證變量是否為null的代碼,如果變量為null,callvirt指令引發(fā)CLR拋出一個(gè)NullReferenceException異常。這種額外的檢查意味著IL指令callvirt的執(zhí)行速度比call指令稍慢。注意,即使callvirt指令用來調(diào)用非虛實(shí)例方法時(shí),也要執(zhí)行這種變量是否為null的檢查?,F(xiàn)在,我們將這兩個(gè)調(diào)用指令放在一起,看看C#是如何使用這兩個(gè)不同的IL指令的:using System; public sealed class Program public static void Main() Console.WriteLine();/調(diào)用一個(gè)靜態(tài)方法 Object o = new Object(); o.GetHashCode();/調(diào)用一個(gè)虛實(shí)例方法 o.GetType();/調(diào)用一個(gè)非虛實(shí)例方法 編譯上述代碼,查看最后得到的IL代碼,結(jié)果如下所示:.method public hideby sigstatic void Main() cil managed .entrypoint /代碼大小 26(0x1a) .maxstack 1 .locals init(objectV_0) IL_0000: call void System.Console:WriteLine() IL_0005: newobj instance void System.Object:.ctor() IL_000a: stloc.0 IL_000b: ldloc.0 IL_000c: callvirt instance int32 System.Object:GetHashCode() IL_0011: pop IL_0012: ldloc.0 IL_0013: callvirt instance class System.Type System.Object:GetType() IL_0018: pop IL_0019: ret /Program:Main方法結(jié)束首先注意,C#編譯器使用IL指令call調(diào)用Console的WriteLine方法,這與期望是相符的,因?yàn)閃riteLine方法是靜態(tài)方法。接著注意,C#編譯器使用IL指令callvirt調(diào)用GetHashCode方法,這也與期望相符,因?yàn)镚etHashCode方法是虛方法。最后注意,C#編譯器同樣使用IL指令callvirt調(diào)用GetType方法,這令人驚訝,因?yàn)镚etType方法不是虛方法。但是,該調(diào)用可以正常調(diào)用,這是因?yàn)镴IT編譯上述代碼時(shí),CLR知道GetType方法不是虛方法,因此,JIT編譯的代碼自然將以非虛的方式調(diào)用GetType方法。當(dāng)然,問題在于,為什么C#編譯器只生成call指令,而不是其他指令呢?答案就是因?yàn)镃#的工作組決定JIT編譯器應(yīng)生成驗(yàn)證所使用的對象的代碼,以確定調(diào)用不為null。這意味著對非虛的實(shí)例方法的調(diào)用要比其應(yīng)有的運(yùn)行速度稍慢一點(diǎn),同樣這也意味著下面的C#代碼將拋出一個(gè)NullReferenceException異常。在其他一些編程語言中,下述代碼將正確運(yùn)行。using System; public sealed class Program public Int32 GetFive()return5; public static void Main() Program p = null; Int32 x = p.GetFive();/在C#中,會(huì)拋出一個(gè)NullReferenceException異常 從理論上講,上述代碼運(yùn)行良好。的確,變量p為null,但是當(dāng)調(diào)用非虛方法(如GetFive)時(shí),CLR僅需要了解p的數(shù)據(jù)類型(p的數(shù)據(jù)類型為Program)。如果確實(shí)調(diào)用了GetFive方法,this參數(shù)的值將為null。因?yàn)镚etFive方法中沒有使用這個(gè)參數(shù),因此不會(huì)拋出NullReferenceException異常。但是,因?yàn)镃#編譯器生成了一個(gè)callvirt指令,而不是生成了一個(gè)call指令,所以上述代碼將拋出一個(gè)NullReferenceException異常并結(jié)束。重要提示如果將某個(gè)方法定義為非虛擬的,那么,將來永遠(yuǎn)不能將方法改為虛擬的。這是因?yàn)槟承┚幾g器會(huì)使用call指令而不是callvirt指令來調(diào)用非虛擬的方法。如果將方法從非虛擬的改為虛擬的,而且沒有重新編譯所涉及的代碼,那么虛方法將被非虛擬地調(diào)用,致使應(yīng)用程序產(chǎn)生無法預(yù)測的行為。如果所涉及的代碼是用C#編寫的,這就不是一個(gè)問題了,因?yàn)镃#使用callvirt指令調(diào)用所有的實(shí)例方法。但是,如果所涉及的代碼使用了不是C#的其他編程語言,這將產(chǎn)生問題。有時(shí),編譯器會(huì)使用call指令代替callvirt指令來調(diào)用虛方法。起初這可能會(huì)令人驚訝,但是下述代碼將說明為什么有時(shí)需要這么做:internal class SomeClass /ToString是一個(gè)定義在基類Object中的虛方法 public override String ToString() /編譯器使用IL指令call以非虛擬的方式調(diào)用object的ToString方法 /如果編譯器用callvirt指令取代call指令,那么該方法將遞歸地調(diào)用其本身,直至堆棧溢出 return base.ToString(); 調(diào)用虛方法base.ToString時(shí),C#編譯器生成一個(gè)call指令來確保非虛擬地調(diào)用基礎(chǔ)類型中的ToString方法。需要這樣做的原因在于:如果虛擬地調(diào)用ToString方法,那么調(diào)用將會(huì)遞歸執(zhí)行,直至線程的堆棧溢出,這明顯不是希望的結(jié)果。編譯器在調(diào)用值類型定義的方法時(shí)傾向于使用指令call,因?yàn)橹殿愋褪敲芊獾?。這意味著即使對于虛方法,也不存在多態(tài),這將改善調(diào)用的性能,使調(diào)用速度更快。另外,值類型實(shí)例的本質(zhì)保證了它永遠(yuǎn)不為null,因此永遠(yuǎn)不會(huì)拋出NullReferenceException異常。最后,如果虛擬地調(diào)用值類型的虛方法,那么,CLR為了在其內(nèi)部引用方法表,需要引用值類型的類型對象,這需要對值類型進(jìn)行裝箱(boxing)。裝箱給堆棧增加了更多的壓力,強(qiáng)制進(jìn)行更頻繁的垃圾收集,使性能受到影響。無論是否使用call指令和callvirt指令來調(diào)用實(shí)例方法或者虛方法,這些方法通常接收一個(gè)隱藏的this參數(shù)作為方法的第一個(gè)參數(shù)。this參數(shù)引用要進(jìn)行操作的對象。在設(shè)計(jì)類型的過程中,應(yīng)盡量減小所定義的虛方法的數(shù)量。首先,調(diào)用虛方法的速度比調(diào)用非虛方法的速度要慢;其次,JIT編譯器不能內(nèi)聯(lián)虛方法,這進(jìn)一步影響了性能;第三,虛方法使組件的版本控制更脆弱,詳見下節(jié)描述;第四,在定義基礎(chǔ)類型時(shí),通常需要提供一組有用的重載方法,如果希望這些方法是多態(tài)的,那么最好的辦法就是將最復(fù)雜的方法虛擬化,而將所有有用的重載方法非虛擬化。附帶提一下,遵循該原則同樣會(huì)改善組件的版本控制能力,而不會(huì)影響派生類型的性能。下面給出示例:public class Set private Int32 m_length = 0; /這個(gè)有用的重載是非虛擬的 public Int32 Find(Object value) return Find(value, 0, m_length); /這個(gè)有用的重載是非虛擬的 public Int32 Find(Object value, Int32 startIndex) return Find(value, 0, m_length); /功能最豐富的方法是虛擬的,它可以被重寫 public virtual Int32 Find(Object value, Int32 startIndex, Int32 endIndex) /重寫的實(shí)際實(shí)現(xiàn)在此實(shí)現(xiàn) /其他方法在此實(shí)現(xiàn) 6.6.2 巧妙使用類型的可見性和成員的可訪問性對于.NET Framework,應(yīng)用程序由多個(gè)不同公司制作的程序集中定義的類型組成。這意味著開發(fā)人員很少對所使用的組件以及這些組件中定義的類型進(jìn)行控制。開發(fā)人員通常無法訪問源代碼(甚至都不知道組件是使用何種編程語言創(chuàng)建的),而且組件對于不同的時(shí)間進(jìn)度表其版本也不同。另外,由于多態(tài)和受保護(hù)的成員,基類開發(fā)人員必須信任派生類開發(fā)人員所編寫的代碼。理所當(dāng)然,派生類的開發(fā)人員也必須信任所繼承的基類的代碼。在設(shè)計(jì)組件和類型時(shí),不得不實(shí)際考慮一下這些問題。本節(jié)中,我們將用較少的篇幅介紹一下如何在設(shè)計(jì)類型時(shí)考慮這些問題。具體來講,我們重點(diǎn)介紹設(shè)置類型的可見性和成員的可訪問性的正確方式,以便你一舉成功。首先,定義一個(gè)新的類型時(shí),編譯器應(yīng)將類默認(rèn)密封,使類不能用作基類。然而,許多編譯器,包括C#編譯器,默認(rèn)方式是非密封類,但是允許程序開發(fā)人員通過使用關(guān)鍵字sealed顯式地將類標(biāo)

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論