.NET 開發基金會/系統型別主題
| .NET 開發基金會 | |
|---|---|
.NET 本質上是一套用於構建和執行計算機程式的工具。計算機程式是一組給定給計算機的指令,計算機自動執行這些指令以操作某些資料。.NET 遵循面向物件正規化,這意味著指令和資料圍繞表示事物或概念的“物件”進行分組。共享相同指令並操作相同型別資料的物件被分組到同一型別中。
面向物件程式是一系列型別的定義。對於每種型別,都會指定資料型別和指令。指令被分組在方法中。可用的指令之一是建立已定義型別的物件。另一種指令是請求執行與物件關聯的方法。這稱為呼叫方法。當呼叫方法時,其指令將被執行,直到方法終止(返回)。當方法返回時,呼叫(呼叫)方法將執行其下一個指令(語句)。
當您啟動程式的執行時,系統將建立一個第一個物件並呼叫該物件的某個方法(這稱為main方法)。在該方法中,通常會建立其他物件並呼叫這些物件的某些方法。這些方法將呼叫其他方法,並建立其他物件,依此類推。逐漸地,呼叫的方法將返回,並且“main”方法最終將完成,標誌著程式執行的結束。
對於有經驗的面向物件開發人員來說,本節將顯而易見,但考試的一些具體目標與型別系統直接相關。
型別是一種對語言中的概念或物件進行分類的方法。這種分類的組織方式稱為“型別系統”。型別系統還可以透過不同的方式對型別本身進行分類。
在 .NET 中對型別進行分類的第一種方法是在框架類庫的一部分型別(系統型別)和由開發人員構建的型別(自定義型別)之間進行區分。
編寫面向物件程式可以被視為定義一個或多個自定義型別。然後,這些型別被打包在某種執行單元中(在 .NET 的情況下為程式集)。程式集被編譯,然後從某個入口點開始執行,該入口點將是某個自定義型別的指定方法。
這些自定義型別使用
- 系統型別來執行“預程式設計”的指令序列
- 其他自定義型別
系統型別也被打包在程式集中。自定義程式集必須引用系統程式集才能使用系統型別。
在 .NET 中還有其他方法可以對型別進行分類。其中一種方法是根據基於這些型別建立的物件對映到計算機記憶體的方式。這將為我們提供值型別和引用型別。
另一種方法是透過反射類別(類、值型別、介面、泛型等)。
另一種方法是區分由執行時直接支援的型別(內建型別)與在類庫或自定義中定義的型別。
這些類別也可以相互交叉,這將為我們提供諸如“內建值型別”或“系統介面”之類的型別。當您遇到這些組合時,請注意所使用的分類。
名稱空間是組織型別的常用方法,因此可以更容易地找到它們。有關名稱空間的討論,請參閱此處。
在名稱空間的上下文中,系統型別是包含在 System 名稱空間或其子名稱空間中的型別,而自定義型別(非系統型別)應使用其他名稱空間。
要了解 Microsoft 如何描述 .NET 型別系統,請參閱MSDN。然後,要概述類庫(系統型別),請參閱MSDN。
事實上,考試的絕大部分內容都是基於如何使用型別庫(系統型別)的常用部分。這就是為什麼考試目標列表(以及本書的目錄)如此之長的原因。
對於 .NET 的新手,您可能希望暫時從這裡討論的概念中休息一下,並確保您瞭解到目前為止所討論的概念如何在非常簡單的示例中使用。接下來的概念並非如此簡單。我們將在此處放置這樣一個示例。
值型別表示型別系統中值/引用分類的一部分。
值型別的例項直接包含其資料(值)。例如,Int32 區域性變數的記憶體直接分配在堆疊上。
值型別本身分為 3 個類別
- 內建值型別
- 使用者定義的值型別
- 列舉
請記住,內建型別是執行時直接支援的型別。
它們是任何程式的構建塊,因為它們是機器指令最終作用的物件。其餘部分本質上是這些型別的組合。
所有內建型別都是值型別,除了 Object 和 String。
內建值型別包括
- 整數型別(Byte、SByte、Int16、Int32、Int64、UInt16、UInt32 和 UInt64)
- 浮點型別(Single 和 Double)
- 邏輯型別(Boolean)
- 其他型別(Char、Decimal、InPtr 和 UInPtr)
所有內建值型別都在 System 名稱空間中定義(例如 System.Int32),並且在 VB 和 C# 中都有代表它們的關鍵字(例如 C# 中的 int,VB.NET 中的 integer),除了 InPtr 和 UInPtr。
所有值型別都直接或間接地從 System.ValueType 派生,對於內建型別和使用者定義型別來說,它們直接從 System.ValueType 派生;對於列舉來說,它們則透過 System.Enum 間接派生。
列舉是一種為一組底層整型值(帶符號或無符號)命名的方式。作為整型型別的限制,它們的作用與它們的底層型別相同。
使用者定義的值型別和列舉都包含系統型別和自定義型別。
System 使用者定義值型別的一個例子是 System.Drawing.Point,它用於繪圖。
您可以使用特定的語言結構(C# 中的 struct,VB.NET 中的 Structure)來構建自定義值型別。
System 列舉的一個例子是 System.Data.CommandType 列舉,它指定表是文字命令、儲存過程呼叫等。
您可以使用特定的語言結構(C# 中的 enum,VB.NET 中的 Enum)來構建自定義列舉。
有關使用值型別的示例和說明,請參見此 部分。另請參見 構建 和 使用 使用者定義值型別的示例。
引用型別
[edit | edit source]引用型別代表型別系統中值/引用分類的另一部分。
與值型別相反,引用型別的例項不直接包含其資料(值),而是包含指向該值記憶體位置的某種引用。例如,String 區域性變數在堆疊上分配了記憶體來儲存對包含字串的引用,而不是字串本身。在這種情況下,字串本身將在堆上分配並進行垃圾回收(稍後會詳細介紹)。
有兩個內建型別被認為是引用型別:Object 和 String,它們也在 System 庫中被直接引用(例如 System.String),並且在 .NET 語言中擁有自己的結構(例如 C# 中的 string)。
引用型別本身分為四類:
- 指標
- 陣列
- 類
- 介面
我們不會在本書中討論指標,因為沒有任何考試目標涉及它們。
接下來的三個部分將介紹陣列、類和介面。
要比較值/引用型別,請嘗試 這個。
有關使用引用型別的其他說明和示例,請參見 這個。
陣列
[edit | edit source]陣列本質上是一組物件,通常型別相同,可以透過索引訪問。
要全面瞭解陣列,您可以檢視維基百科文章(在右側)或訪問關於 資料結構 的華夏公益教科書。
陣列曾經是程式語言中最常用的功能之一,因為您可以以非常高效的方式從陣列中的一個專案跳轉到下一個專案(如果您想了解更多資訊,可以參考 C 指標和陣列)。如今,隨著“計算機能力”的顯著提升,人們將注意力從陣列轉移到了集合。陣列的兩個主要問題是:
- 它們是固定長度的
- 它們只支援一種內部組織
集合解決了陣列的大部分缺點(雖然代價不菲)。對於需要高效操作的固定組物件,陣列仍然可以考慮使用。
我們將在 使用陣列 部分提供一些示例。
類
[edit | edit source]類本身又分為三類:
- 使用者定義的類
- 裝箱的值型別
- 委託
裝箱的值型別將在後面的 裝箱/拆箱 部分進行討論。
委託也將在其 部分 中介紹。
使用者定義的類是面向物件概念的基本實現。
關於類,有很多內容可以討論,但由於沒有考試目標涉及它們,因此我們假設讀者已經熟悉該概念,並已使用過它們(在 .NET 或其他地方)。
您可以使用系統類(數百個甚至更多)和自定義類(您將在其中編寫程式邏輯的本質)。
介面
[edit | edit source]如果您想真正弄清楚面向物件程式設計到底是什麼,請檢視維基百科關於多型性的文章:-)。
其思想是,能夠使用單個型別的引用來引用具有“共同點”的不同型別中“共同部分”。
矩形和橢圓都是“形狀”,那麼我們如何才能讓程式的一部分操作“形狀”,而無需瞭解矩形或橢圓的具體情況呢?在這種情況下,我們說形狀是多型的(字面意思是它們可以採用多種形式)。
在面向物件程式設計中,您可以透過繼承獲得這種行為。指向父類(Shape)的引用將毫無問題地操作子物件(矩形或橢圓)。
介面的開發是為了在基於元件的計算環境中獲得相同的行為,在這種環境中,您無法訪問原始碼,因此無法使用繼承。介面結構是 COM 中所有元件通訊的基礎。
介面是按照明確定義的方式(方法簽名)實現一組方法的契約。如果您知道一個類實現了某個介面,那麼您就知道可以使用任何定義的方法。
從這個定義可以很容易地想象擁有指向“介面例項”的引用,您可以從該引用呼叫任何介面方法。實際上,框架提供了這樣一種機制。
現在假設,我們沒有將 Shape 作為 Rectangle 的父類,而是僅僅有一個 Shape 介面,它由 Rectangle 和 Ellipse 同時實現。如果我有一個指向 Rectangle 物件的引用,那麼我將能夠將其“強制轉換為”指向 Shape 介面的引用,並將其傳遞給只瞭解“Shape 物件”的程式部分。
這與我們透過繼承獲得的多型行為完全相同。
類庫嚴重依賴介面,對該概念的清晰理解至關重要。
與繼承相比,介面的一個有趣問題是,您沒有獲得預設實現(虛擬父方法),因此您必須重新編碼所有內容。雖然存在技術和工具,但始終需要額外的工作。更多關於泛型的內容……
我們提供了 使用 和 構建 介面的示例。一些 System 名稱空間的介面在 標準介面 部分中有所展示。
屬性
[edit | edit source]現在,我們暫時中斷對型別的討論,來談談屬性。首先,屬性不是型別。它們是新增到程式元素(程式集、類、方法等)中的一些資訊元素,用於在該元素的正常程式碼之外對其進行限定。
這些新增的“屬性”用於對底層程式元素進行操作,而無需修改該元素的執行邏輯。
為什麼我們要在執行程式碼之外操作程式元素?簡短的答案是,面向物件的概念很難處理所謂的橫切關注點,即應用於所有或大多數類的程式方面。此類方面的例子包括安全性、永續性、序列化等。與其修改每個類以新增序列化邏輯(這與業務規則無關),不如在類中新增序列化屬性來直接控制這些類的序列化過程,而無需更改業務邏輯(執行程式碼)。
框架的許多功能都依賴於屬性向程式元素新增資訊。屬性在編譯過程中保留,並與程式集中的限定元素相關聯。它們在執行時使用反射以程式設計方式進行分析,以調整“橫切”功能的行為。
至於型別,我們有系統屬性(框架的一部分)和自定義屬性(由開發人員構建)。自定義屬性的開發將在反射部分中進行處理。在此之前,我們將限制使用系統屬性,並專注於定義它們以及瞭解它們如何確定框架的行為。
在屬性使用部分中,將提供一些關於如何使用簡單系統屬性的示例。
需要注意的是,屬性並不是向程式新增“非執行”資訊以修改其行為的唯一方式。例如,XML 配置檔案可用於指定將影響程式執行的引數。我們將在本書的配置部分討論這些內容。
有關 C# 中屬性的討論,請參閱MSDN。
集合
[edit | edit source]集合是一組物件。框架中包含許多型別的集合,涵蓋了大多數處理物件組的情況。瞭解這些集合型別可以節省您重新編碼等效邏輯的時間,並使您的程式更易於維護。
與陣列不同,集合有多種型別,每種型別都有其特定的內部組織。每種型別的集合都與特定型別的問題相關聯。我們將在給出每種型別集合的示例時指出問題的型別。
我們有兩個部分包含集合示例
集合的主要缺點是,在“現實生活中”,分組在一起的物件通常具有某些共同特徵(它們是相同型別、具有共同的父型別或支援共同的介面)。因此,大多數情況下,我們對物件瞭解的不僅僅是它們的組織方式。集合不允許我們使用這些知識來驗證傳遞給集合的物件或適用於集合中所有物件的程式碼邏輯(沒有強制轉換和異常處理)。
泛型集合在框架的 2.0 版本中引入。它們在保持集合其他優勢的同時解決了這個問題。因此,應儘可能使用泛型集合,而不是“普通”集合。
有關集合的一些外部連結:GotDotNet 和 AspNetResources
有關陣列、集合和資料結構的總體討論,請參閱MSDN。
泛型
[edit | edit source]泛型程式設計或使用引數化型別不是面向物件的概念。從這個意義上說,泛型有點像屬性,它們被新增到主流面向物件平臺中,以處理面向物件技術難以解決的情況。
要開始我們的討論,請閱讀以下有趣的連結文章(附錄),它介紹了泛型集合的背景下泛型的概念。這個外部教程也是如此。
泛型的概念很有趣,因為它將泛化概念應用於型別。型別本身是物件的泛化(面向物件程式設計的基礎)。所以這裡我們開始操作型別,即使用型別作為引數。
實際型別將在用特定型別替換引數型別時獲得(例如,在宣告該型別的變數時)。
// C# List<int> myIntList = new List<int>() '// VB.NET Dim myIntList As New List(Of Integer)
在 .NET 框架中,這種替換是在第二次編譯(從 CIL 到機器程式碼的即時編譯)期間完成的。換句話說,泛型由 CIL 支援,因此是框架本身的一部分,而不是所用語言的一部分(例如 C#)。
有關泛型的更多內容,請參閱MSDN。
有關泛型集合的示例,請參閱使用泛型集合部分。
異常
[edit | edit source]以下是如何丟擲一般異常
// C#
throw new Exception("This is an exception.");
'// VB.NET
Throw New Exception("This is an exception.")
以下是如何處理異常
// C#
try
{
throw new Exception("This is an exception.");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
'// VB.NET
Try
Throw New Exception("This is an exception.")
Catch ex As Exception
Console.WriteLine(ex.Message)
End Try
事件和委託
[edit | edit source]有關事件和委託的“官方”討論,請參閱MSDN。我們這裡將對所涉及的許多概念進行更一般的討論。
第一個概念是委託,即類的部分功能可以在程式中的“其他位置”完成,可以委託出去。委託的重要好處是,“委託”物件不必知道委託功能的實際實現方式。一種“委託”方法是定義介面。如果一個物件引用了一個介面,它可以將部分功能委託給實現該介面的物件,而無需瞭解該物件的太多資訊。
.NET 2.0 定義了另一種名為委託的引用型別,它以略微不同的方式實現了委託模式。委託是一種型別,它定義了對單個函式的引用,該函式必須與委託定義具有相同的簽名。簽名是函式引數型別及其返回型別的描述。與類一樣,您可以建立該型別的物件。建立的物件是對函式的引用,可以將其分配(將引用設定為特定函式)、作為引數傳遞或執行(引用到的函式實際上被執行)。與介面不同,委託只定義一個函式,並且可以直接建立委託例項(無需另一個類實現介面)。
.NET 中的大多數委託都派生自多播委託。多播委託是一種委託,它維護一個指向具有與委託定義相同簽名的函式的函式引用列表,而不是單個引用。可以使用 += 運算子向多播委託新增函式引用。當您執行多播委託時,每個引用的函式將依次執行。
如果一個物件來自實現多播委託成員的類,那麼如果您擁有該物件的引用,就可以向多播委託“新增”您選擇的函式的引用。如果該物件決定“執行”其委託成員,那麼您添加了引用的函式將被執行。
此多播委託成員正是 .NET 中的事件。這就是為什麼事件和委託幾乎總是被一起討論的原因。
定義事件的步驟是
- 您將委託型別定義為指向具有特定簽名的函式的引用
- 您將 Event 成員新增到類,並將其與您剛定義的委託型別關聯,這將例項化一個與事件關聯的多播委託
- 當您擁有該類的物件的引用時,您可以向任何具有與委託型別相同簽名的方法新增引用。
- 當該物件引發事件時,將執行關聯的多播委託,從而觸發對引用函式的執行。
大多數情況下,您將新增對您自己的方法之一的引用。當被引用的物件觸發其事件時,它實際上會執行您的方法之一,而無需知道它。這種設定是釋出/訂閱模式的實現。您訂閱某個事件,表示您希望在發生特定“事件”時得到通知。許多物件可以訂閱同一個事件。當被引用的物件觸發其事件時,它會“釋出”一條訊息,表明事件已有效地發生,形式是向所有“註冊”的函式發出函式呼叫。
因此,任何類都可以定義事件。許多系統類定義事件,以向您的程式碼傳達在執行環境中“發生了某些事情”的事實(滑鼠移動、按鍵按下、套接字接收訊息等)。構造一個“等待”事件發生然後對事件做出反應的程式被稱為事件程式設計。大多數線上應用程式和服務都遵循這種設計。我們將在關於多執行緒的部分更詳細地討論捕獲系統事件的方式。
有關事件和委託的示例,請參見本節.