第六章 類型和成員基礎
目錄:
6.1 類型的各種成員
6.2 類型的可見性
6.3 成員的可見性
6.4 靜態類
6.5 分部類,結構和接口
6.6 組件,多態和版本控制
6.1 類型的各種成員
常量:數據值恒定不變的符號。常亮總與類型管理,不與類型的實例關聯。邏輯上總是靜態成員
字段:只讀或可讀/可寫的數據值。
實例構造器:將對象的實例字段初始化為良好初始狀態的特使方法。
類型構造器:將類型的靜態字段初始化為良好初始狀態的特殊方法。
方法:更改或查詢類型或對象狀態的函數。作用於類型稱為靜態方法,作用於對象稱為實例方法。
操作符重載:實際是方法,定義了當操作符作用於對象時,應該如何操作該對象。
轉換操作符:定義如何隱士或顯示將對象從一種類型轉型為另一種類型的方法。
屬性:允許用簡單的,字段風格的語法設置或查詢類型或對象的邏輯狀態,同時保證狀態不被破壞。
事件:靜態事件允許類型向一個或多個靜態或實例方法發送通知。實例事件允許對象向一個或多個靜態或實例方法發送通知。引發事件通常是為了響應提供事件的類型或對象的狀態的改變。事件包含兩個方法,允許靜態或實例方法登記或註銷對該事件的關註。還有一個委托字段來維護已登記的方法集。
類型:類型可定義其他嵌套類型。
6.2 類型的可見性
public:不僅對定義程序集中的代碼可見,還對其他程序集中的代碼可見。
internal:僅對定義程序集中的代碼可見。
友元程序集:能夠使定義為internal的類型給另一個程序集的代碼訪問。 生程序集時,可用”InternalsVisibleTo“特性:該特性獲取友元程序集名稱和公鑰的字符串參數。
6.3 成員的可見性
CLR術語 | C#術語 | 描述 |
Private | private | 成員只能由定義類型或任何嵌套類型中方法訪問 |
Family | protected | 成員只能由定義類型,任何嵌套類型或者不管在任何程序集中的派生類型中的方法訪問 |
Family and Assembly | (不支持) | 成員只能由定義類型,任何嵌套類型或者同一程序集中定義的任何派生類型中的方法訪問 |
Assembly | internal | 成員只能由定義程序集中的方法訪問。 |
Family or Assembly | protected internal | 成員可由任何嵌套類型,任何派生類型或者定義程序集中的任何方法訪問 |
Public | public | 成員可由任何程序集的任何方法訪問 |
在C#中,如果沒有顯式聲明成員的可訪問性,編譯器通常默認選擇private。CLR要求接口類型的所有成員都具有Public可訪問性。編譯器禁止開發人員顯式指定接口成員的可訪問性,它會自動將所有成員的可訪問性設為public。
派生類重寫基類型定義的成員時,C#編譯器要求原始成員和重寫成員具有相同的可訪問性。
6.4 靜態類
靜態類的作用是組合一組相關的成員。在C#中,要用static關鍵字定義不可實例化的類。該類只能應用於類,不能應用於結構。因為CLR總是允許值類型實例化,這是沒辦法阻止的。
限制:
靜態類必須直接從基類System.Object派生,從其他任何基類派生都沒有意義。
靜態類不能實現任何接口,這是因為只有使用類的實例時,才可調用類的接口方法。
靜態類只能定義靜態成員(字段,方法,屬性和事件),任何實例成員都會導致編譯器報錯。
靜態成員不能作為字段,方法參數或局部變量使用,因為它們都代表引用了實例的變量,而這是不允許的。
6.5 分部類,結構和接口
partial關鍵字告訴編譯器:類,結構或接口的定義源代碼可能要分散到一個或多個源代碼文件中。
將類型源代碼分散到多個文件的原因有三:
源代碼控制
在同一個文件中或結構分解成不同的邏輯單元
代碼拆分
6.6 組件,多態和版本控制
組件軟件編程(Component SoftWare Programming,CSP)是OOP發展到極致的成果。下面列舉組件的一些特點。
組件(.Net Framework稱為程序集)有“已經發布”的意思。
組件有自己的標誌(名稱,版本,語言文化和公鑰)。
組件永遠維持自己的標誌(程序集中的代碼永遠不會靜態鏈接到另一個程序集中;.NET總是使用動態鏈接)
組件清楚指明它所依賴的組件(引用元數據表)。
組件應編擋它的類和成員。C#語言通過源代碼內的XML文檔和編譯器的/doc命令行開關提供這個共功能。
組件必須指定它需要的安全權限。CLR的代碼訪問安全性(Code Access Security,CAS)機提供這個功能。
組件要發布在任何“維護版本”中都不會改變的接口(對象模型)。“維護版本”代表組件的新版本,它向後兼容組件的原始版本。
.NET Framework 中的版本包含四個部分:主版本號(major version),次版本號(minor version),內部版本號(build number)和修訂號(revision)。major/minor部分通常代碼程序集的一個連續的,穩定的功能集,而build/revision部分通常代表對這個功能集的一次維護。
將一個組件(程序集)中定義的類型作為另一個組件(程序集)中的一個類型的基類型使用時,便會發生版本控制問題。顯然,如果基類的版本(被修改得)低於派生類,派生類的行為也會改變,這可能造成類的行為失常。
與組件版本控制相關的C#關鍵字
C#關鍵字 | 類型 | 方法/屬性/事件 | 常量/字段 |
abstract | 表示不能構造該類型的實例 | 表示為了構造派生類型的實例,派生類型必須重寫並實現這個成員 | (不允許) |
virtual | (不允許) | 表示這個成員可由派生類型重寫 | (不允許) |
override | (不允許) | 表示派生類型正在重寫基類型的成員 | (不允許) |
sealed | 表示該類型不能用作基類型 | 表示這個成員不能被派生類型重寫,只能將該關鍵字應用於重寫虛方法的方法 | (不允許) |
new | 應用於嵌套類型,方法,屬性,常量或字段時,表示該成員與基類中相似的成員無任何關系 |
6.6.1 CLR如何調用虛方法,屬性和事件
編譯代碼,編譯器會在程序集的方法定義表中寫人記錄項,每個記錄項都用一組標誌(flag)指明方法是實例方法,虛方法還是靜態方法。
寫代碼調用方法,生成調用代碼的編譯器會檢查方法定義的標誌,判斷應如何生成IL代碼來正確調用方法。CLR提供兩個方法調用指令。
call:該IL指令可調用靜態方法,實例方法和虛方法。用call指令調用靜態方法,必須指定方法的定義類型。用call指令調用實例方法或虛方法,必須指定引用了對象的變量。call指令假定該變量不為null。call指令經常用於以非虛方式調用虛方法。
callvirt:該IL指令可調用實例方法和虛方法,不能調用靜態方法。用callvirt指令調用實例方法或虛方法,必須指定引用了對象的變量。用callvirt指令調用非虛實例方法,變量的類型指明了方法的定義類型。用callvirt指令調用虛實例方法,CLR調查發出調用的對象的實際類型,然後以多態方式調用方法。為了確定類型,發出調用的變量絕不能是null。換言之,編譯這個調用時,JIT編譯器會生成代碼來驗證變量的值是不是null。如果是,callvirt指令造成CLR拋出NullReferenceException異常。
設計類型時應盡量減少虛方法數量。首先,調用虛方法的速度比調用非虛方法慢。其次,JIT編譯器不能內嵌虛方法,這進一步影響性能。第三,虛方法使組件版本控制變得更脆弱。第四,定義基類型時,經常要提供一組重載的簡便方法。如果希望這些方法時多態的,最好的方法是使最復雜的方法成為虛方法,使所有重載的簡便方法成為非虛方法。
6.6.2 合理使用類型的可見性和成員的可訪問
密封類相對於非密封類的優勢:
版本控制
性能
安全性和可預測性
定義類時遵循的原則:
定義類時,除非確定要將其作為基類,並允許派生類對它進行特化,否則總是顯式地指定為sealed類。
類的內部,將數據字段定義為private。
在類的內部,將自己的方法,屬性和事件定義為priavate和非虛。如果要公開就定義為public,盡量避免定義為protected或internal,因為這會使類型面臨更大的安全風險。最後才考慮使用virtual,因為虛成員會放棄許多控制,喪失獨立性,變得徹底依賴於派生類型的正確行為。
OOP有一條古老的格言,大意時當事情變得過於復雜時,就搞更多的類型出來。當算法的實現開始變得復雜時,我會定義一些輔助類型來封裝獨立的功能。如果定義的輔助類型只由一個“超類型”使用,我會在“超類型”中嵌套這些輔助類型。這樣除了可以限制範圍,還允許嵌套的輔助類型中的代碼引用“超類型”中定義的私有成員。
6.6.3 對類型進行版本控制時的虛方法的處理
使用override重寫基類虛方法。
第六章 類型和成員基礎