《果殼中的C# C# 5.0 權威指南》 - 學習筆記

**《果殼中的C# C# 5.0 權威指南》

========== ========== ==========
[作者] (美) Joseph Albahari (美) Ben Albahari
[譯者] (中) 陳昇 管學理 曾少寧 楊慶川
[出版] 中國水利水電出版社
[版次] 2013年08月 第1版
[印次] 2013年08月 第1次 印刷
[定價] 118.00元
========== ========== ==========

【前言】

C# 5.0 是微軟旗艦編程語言的第4次重大升級。

C# 5.0 及相關 Framework 的新特性已經被標註清楚,因此也可以將本書作爲 C# 4.0 參考書使用。

【第01章】

(P001)

C# 在面向對象方面的特性包括:

  1. 統一的類型系統 —— C# 中的基礎構建塊是一種被稱爲類型的數據與函數的封裝單元。C# 有一個統一的類型系統,其中所有類型最終都共享一個公共的基類。這意味着所有的類型,不管它們是表示業務對象,或者像數字等基本類型,都共享相同的基本功能集;

  2. 類與接口 —— 在純粹的的面向對象泛型中,唯一的類型就是類。但是 C# 中還有其他幾種類型,其中一種是接口。接口與類相似,但它只是某種類型的定義,而不是實現。在需要用多繼承時,它是非常有用的;

  3. 方法、屬性與事件 —— 在純粹的面向對象泛型中,所有函數都是方法。在 C# 中,方法只是一種函數成員,也包含一些屬性和事件以及其他組成部分。屬性是封裝了一部分對象狀態的函數成員。事件是簡化對象狀態變化處理的函數成員;

C# 首先是一種類型安全的語言,這意味着類型只能夠通過它們定義的協議進行交互,從而保證每一種類型的內部一致性。

C# 支持靜態類型化,這意味着這種語言會在編譯時執行靜態類型安全性檢查。

(P002)

靜態類型化能夠在程序運行之前去除大量的錯誤。

C# 允許部分代碼通過新的 dynamic 關鍵字來動態指定類型。然而,C# 在大多數情況下仍然是一種靜態類型化的語言。

C# 之所以被稱爲一種強類型語言,是因爲它的類型規則是非常嚴格的。

C# 依靠運行時環境來執行自動的內存管理。

C# 並沒有去除指針 : 它只是使大多數編程任務不需要使用指針。對於性能至關重要的熱點和互操作性方面,還是可以使用指針,但是隻允許在顯式標記爲不安全的代碼塊中使用。

C# 依賴於一個運行時環境,它包括許多特性,如自動內存管理和異常處理。

(P003)

.NET Framework 由名爲 Common Language Runtime (CLR) 的運行時環境和大量的程序庫組成。這些程序庫由核心庫和應用庫組成。

CLR 是執行託管代碼的運行時環境。C# 是幾種將源代碼編譯爲託管語言之一。託管代碼會被打包成程序集,它可以是可執行文件或程序庫的形式,包括類型信息或元數據。

託管代碼用 Intermediate Language 或 IL 表示。

Red Gate 的 .Net Reflector 是一個重要的分析程序集內容的工具 (可以將它作爲反編譯器使用) 。

CLR 是無數運行時服務的主機。這些服務包括內存管理、程序庫加載和安全性服務。

CLR 是與語言無關的,它允許開發人員用多種語言開發應用程序。

(P004)

.NET Framework 由只支持基於所有 Windows 平臺或 Web 的應用程序的程序庫組成。

C# 5.0 還實現了 Windows Runtime (WinRT) 庫的互操作。

WinRT 是一個擴展接口和運行時環境,它可以用面向對象和與語言無關的方式訪問庫。Windows 8 帶有這個運行時庫,屬於 Microsoft 組件對象模型或 COM 的擴展版本。

Windows 8 帶有一組非託管 WinRT 庫,它是通過 Microsoft 應用商店交付的支持觸摸屏的 Metro 風格應用程序框架。作爲 WinRT ,這些程序庫不僅可以通過 C# 和 VB 訪問,也可以通過 C++ 和 JavaScript 訪問。

WinRT 與普通 COM 的區別是,WinRT 的程序庫支持多種語言,包括 C# 、 VB 、 C++ 和 JavaScript,所以每一種語言 (幾乎) 都將 WinRT 類型視爲自己的專屬類型。

(P005)

C# 5.0 兩個較大的新特性是通過兩個關鍵字 (async 和 await) 支持異步功能 (asynchronous function)。

C# 4.0 增加的新特性有 : 動態綁定、可選參數和命名參數、用泛型接口和代理實現類型變化、改進 COM 互操作性。

C# 3.0 增加的這些特性主要集中在語言集成查詢功能上 (Language Integrated Query,簡稱 LINQ) 。

C# 3.0 中用於支持 LINQ 的新特性還包括隱式類型化局部變量 (Var) 、匿名類型、對象構造器、 Lambda 表達式、擴展方法、查詢表達式和表達式樹。

(P006)

C# 3.0 也增加了自動化和局部方法。

【第02章】

(P007)

在 C# 中語句按順序執行。每個語句都以分號 (;) 結尾。

C# 語句按順序執行,以分號 (;) 結尾。

(P008)

方法是執行一系列語句的行爲。這些語句叫做語句塊。語句塊由一對大括號中的 0 個或多個語句組成。

編寫可調用低級函數的高級函數可以簡化程序。

方法可以通過參數來接收調用者輸入的數據,並通過返回類型給調用者返回輸出數據。

C# 把 Main 方法作爲程序的默認執行入口。 Main 方法也可以返回一個整數 (而不是 void) ,從而爲程序執行的環境返回一個值。 Main 方法也可以接受一個字符串數組作爲參數 (數組中包含可傳遞給可執行內容的任何參數) 。

數組代表某種特定類型,固定數量的元素的集合。數組由元素類型和它後面的方括號指定。

類由函數成員和數據成員組成,形成面向對象的構建塊。

(P009)

在程序的最外層,類型被組織到命名空間中。

.NET Framework 的組織方式爲嵌套的命名空間。

using 指令僅僅是爲了方便,也可以用 “命名空間 + 類型名” 這種完全限定名稱來引用某種類型。

C# 編譯器把一系列 .cs 擴展名的源代碼文件編譯成程序集。

程序集是 .NET 中的最小打包和部署單元。

一個程序集可以是一個應用程序,或者是一個庫。

一個普通的控制檯程序或 Windows 應用程序是一個 .exe 文件,包含一個 Main 方法。

一個庫是一個 .dll 文件,它相當於一個沒有入口的 .exe 文件。

庫是用來被應用程序或其他的庫調用 (引用) 的。

.NET Framework 就是一組庫。

C# 編譯器名稱是 csc.exe。可以使用像 Visual Studio 這樣的 IDE 編譯 C# 程序,也可以在命令行中手動調用 csc 命令編譯 C# 程序。

(P010)

標識符是程序員爲類、方法、變量等選擇的名字。

標識符必須是一個完整的詞、它是由字母和下劃線開頭的 Unicode 字符組成的。

C# 標識符是區分大小寫的。

通常約定參數、局部變量和私有變量字段應該由小寫字母開頭,而其他類型的標識符則應該由大寫字母開頭。

關鍵字是編譯器保留的名稱,不能把它們用作標識符。

如果用關鍵字作爲標識符,可以在關鍵字前面加上 @ 前綴。

@ 並不是標識符的一部分。

@ 前綴在調用其他有不同關鍵字的 .NET 語言編寫的庫時非常有用。

(P011)

點號 (.) 表示某個對象的成員 (或數字的小數點)。

括號在聲明或調用方法時使用,空括號在方法沒有參數時使用。

等號則用於賦值操作。

C# 提供了兩種方式的註釋 : 單行註釋和多行註釋。

單行註釋由雙斜線開始,到本行結束爲止。

多行註釋由 / 開始,由 / 結束。

變量代表它的值可以改變,而常量則表示它的值不可以更改。

(P012)

C# 中所有值都是一種類型的實例。一個值或一個變量所包含的一組可能值均由其類型決定。

預定義類型是指那些由編譯器特別支持的類型。

預定義類型 bool 只有兩種值 : true 和 false 。 bool 類型通常與 if 語句一起用於條件分支。

在 C# 中,預定義類型 (也稱爲內建類型) 被當做 C# 關鍵字。在 .NET Framework 中的 System 命名空間下包含了很多並不是預定義類型的重要類型。

正如我們能使用簡單函數來構建複雜函數一樣,也可以使用基本類型來構建複雜類型。

(P013)

類型包含數據成員和函數成員。

C# 的一個優點就是預定義類型和自定義類型只有很少的不同。

實例化某種類型即可創建數據。

預定義類型可以簡單地通過字面值進行實例化。

new 運算符用於創建自定義類型的實例。

使用 new 運算符後會立刻實例化一個對象,對象的構造方法會在初始化時被調用。

構造方法像方法一樣被定義,不同的是方法名和返回類型簡化成它所屬的類型名。

由類型的實例操作的數據成員和函數成員被稱爲實例成員。

在默認情況下,成員就是實例成員。

(P014)

那些不是由類型的實例操作而是由類型本身操作的數據成員和函數成員必須標記爲 static 。

public 關鍵字將成員公開給其他類。

把成員標記爲 public 就是在說 : “這就是我想讓其他類型看到的,其他的都是我自己私有的” 。

用面嚮對象語言,我們稱之爲公有 (public) 成員封裝了類中的私有 (private) 成員。

在 C# 中,兼容類型的實例可以相互轉換。

轉換始終會根據一個已經存在的值創建一個新的值。

轉換可以是隱式或顯式。

隱式轉換自動發生,而顯式轉換需要 cast 關鍵字。

long 容量是 int 的兩倍。

(P015)

隱式轉換只有在下列條件都滿足時才被允許 :

  1. 編譯器能保證轉換總是成功;

  2. 沒有信息在轉換過程中丟失;

只有在滿足下列條件時才需要顯式轉換:

  1. 編譯器不能保證轉換總是能成功;

  2. 信息在轉換過程中有可能丟失;

C# 還支持引用轉換,裝箱轉換和自定義轉換。

對於自定義轉換,編譯器並沒有強制遵守上面的規則,所以設計不好的類型有可能在轉換時出現預想不到的結果。

所有 C# 類型可以分成以下幾類 : 值類型、引用類型、泛型類型、指針類型。

值類型包含大多數內建類型 (具體包括所有的數值類型、 char 類型和 bool 類型) 以及自定義 struct 類型和 enum 類型。

引用類型包括所有的類、數據、委託和接口類型。

值類型和引用類型最根本的不同是它們在內存中的處理方式。

值類型變量或常量的內容僅僅是一個值。

可以通過 struct 關鍵字定義一個自定義值類型。

對值類型實例的賦值操作總是會複製這些實例。

將一個非常大的 long 轉換成 double 類型時,有可能造成精度丟失。

(P016)

引用類型比值類型複雜,它由兩部分組成 : 對象和對象的引用。

引用類型變量或常量的內容是對一個包含值的對象的引用。

(P017)

一個引用可以賦值爲字面值 null,這表示它不指向任何對象;

相對的,值類型通常不能有 null 值;

C# 中也有一種代表類型值爲 null 的結構,叫做可空 (nullable) 類型。

(P018)

值類型實例正好佔用需要存儲其字段的內存。

從技術上說,CLR 用整數倍字段的大小來分配內存地址。

引用類型要求爲引用和對象單獨分配存儲空間。

對象佔用了和字段一樣的字節數,再加上額外的管理開銷。

每一個對象的引用都需要額外的 4 或 8 字節,這取決於 .NET 運行時是運行在 32 位平臺還是 64 位平臺上。

C# 中的預定義類型又稱框架類型,它們都在 System 命名空間下。

在 CLR 中,除了 decimal 之外的一系列預定義值類型被認爲是基本類型。之所以將其稱爲基本類型,是因爲它們在編譯過的代碼中被指令直接支持。因此它們通常被翻譯成底層處理器直接支持的指令。

(P019)

System.IntPtr 和 System.UIntPtr 類型也是基本類型。

在整數類型中,int 和 long 是最基本的類型, C# 和運行時都支持它們。其他的整數類型通常用於實現互操作性或存儲空間使用效率非常重要的情況。

在實數類型中,float 和 double 被稱爲浮點類型,通常用於科學計算。

decimal 類型通常用於要求10位精度以上的數值計算和高精度的金融計算。

整型字面值可使用小數或十六進制小數標記,十六進制小數用 0x 前綴表示。

實數字面值可使用小數和指數標記。

從技術上說,decimal 也是一種浮點類型,但是在 C# 語言規範中通常不將其認爲是浮點類型。

(P020)

默認情況下,編譯器認爲數值字面值或者是 double 類型或者是整數類型 :

  1. 如果這個字面值包含小數點或指數符號 (E),那麼它被認爲是 double ;

  2. 否則,這個字面值的類型就是下列能滿足這個字面值的第一個類型 : int 、 uint 、 long 和 ulong ;

數值後綴顯式地定義了一個字面值的類型。後綴可以是下列小寫或大寫字母 : F (float) 、 D (double) 、 M (decimal) 、 U (uint) 、 L (long) 、 UL (ulong) 。

後綴 U 、 L 和 UL 很少需要,因爲 uint 、 long 和 ulong 總是可以表示 int 或從 int 隱式轉換過來的類型。

從技術上講,後綴 D 是多餘的,因爲所有帶小數點的字面值都被認爲是 double 類型。總是可以給一個數字類型加上小數點。

後綴 F 和 M 是最有用的,它在指定 float 或 decimal 字面值時使用。

double 是無法隱式轉換成 float 的,同樣的規則也適用於 decimal 字面值。

整型轉換在目標類型能表示源類型所有可能的值時是隱式轉換,否則需要顯式轉換。

(P021)

float 能隱式轉換成 double ,因爲 double 能表示所有可能的 float 的值。反過來則必須是顯式轉換。

所有的整數類型可以隱式轉換成浮點數,反過來則必須是顯式轉換。

將浮點數轉換成整數時,小數點後的數值將被截去,而不會四捨五入。

靜態類 System.Convert 提供了在不同值類型之間轉換的四捨五入方法。

把一個大的整數類型隱式轉換成浮點類型會保留整數部分,但是有時會丟失精度。這是因爲浮點類型總是有比整數類型更大的數值,但是可能只有更少的精度。

所有的整數類型都能隱式轉換成 decimal 類型,因爲小數類型能表示所有可能的整數值。其他所有的數值類型轉換成小數類型或從小數類型轉換到數值類型必須是顯式轉換。

算術運算符 (+ 、 - 、 * 、 / 、 %) 用於除了 8 位和 16 位的整數類型之外的所有數值類型。

自增和自減運算符 (++ 、 --) 給數值加 1 或減 1 。這兩個運算符可以放在變量的前面或後面,這取決於你想讓變量在計算表達式之前還是之後被更新。

(P022)

整數類型的除法運算總是會截斷餘數。用一個值爲 0 的變量做除數將產生一個運行時錯誤 (DivisionByZeroException) 。

用字面值 0 做除數將產生一個編譯時錯誤。

整數類型在運行算術運算時可能會溢出。默認情況下,溢出默默地發生而不會拋出任何異常。儘管 C# 規範不能預知溢出的結果,但是 CLR (通用語言運行時) 總是會造成溢出行爲。

checked 運算符的作用是在運行時當整型表達式或語句達到這個類型的算術限制時,產生一個 OverflowException 異常而不是默默的失敗。

checked 運算法在有 ++ 、 -- 、 + 、 - (一元運算符和二元運算符) 、 * 、 / 和整數類型間顯式轉換運算符的表達式中起作用。

checked 操作符對 double 和 float 數據類型沒有作用,對 decimal 類型也沒有作用 (這種類型總是受檢的)。

checked 運算符能用於表達式或語句塊的周圍。

可以通過在編譯時加上 /checked+ 命令行開關 (在 Visual Studio 中,可以在 Advanced Build Settings 中設置) 來默認使程序中所有表達式都進行算術溢出檢查。如果你只想禁用指定表達式或語句的溢出檢查,可以用 unchecked 運算符。

(P023)

無論是否使用了 /checked 編譯器開關,編譯時的表達式計算總會檢測溢出,除非應用了 unchecked 運算符。

C# 支持如下的位運算符 : ~ (按位取反) 、 & (按位與) 、 | (按位或) 、 ^ (按位異或) 、 << (按位左移) 、 >> (按位右移) 。

8 位和 16 位整數類型指的是 byte 、 sbyte 、 short 和 ushort 。這些類型缺少它們自己的算術運算符,所以 C# 隱式把它們轉換成所需的大一些類型。

不同於整數類型,浮點類型包含某些操作要特殊對待的值。這些特殊的值是 NaN (Not a Number) 、 +∞ 、 -∞ 和 -0 。

float 和 double 類型包含用於 NaN 、 +∞ 、 -∞ 值 (MaxValue 、 MinValue 和 Epsilon) 的常量。

(P024)

非零值除以零的結果是無窮大。

零除以零或無窮大減去無窮大的結果是 NaN。

使用比較運算符 (==) 時,一個 NaN 的值永遠也不等於其他的值,甚至不等於其他的 NaN 值。

必須使用 float.IsNaN 或 double.IsNaN 方法來判斷一個值是不是 NaN 。

無論何時使用 object.Equals 方法,兩個 NaN 的值都是相等的。

NaN 在表示特殊值時很有用。

float 和 double 遵循 IEEE 754 格式類型規範,原生支持幾乎所有的處理器。

double 類型在科學計算時很有用。

decimal 類型在金融計算和計算那些 “人爲” 的而非真實世界的值時很有用。

(P025)

float 和 double 在內部是基於 2 來表示數值的。因此只有基於 2 表示的數值才能被精確的表示。事實上,這意味着大多數有小數的字面值 (它們基於10) 將無法精確的表示。

decimal 基於 10,它能夠精確地表示基於10的數值 (也包括它的因子,基於2和基於5) 。因爲實型字面值是基於 10 的,所以 decimal 能精確地表示像 0.1 這樣的數。然而,double 和 decimal 都不能精確表示那些基於 10 的極小數。

C# 中的 bool (System.Boolean 類型的別名) 能表示 true 和 false 的邏輯值。

儘管布爾類型值僅需要 1 位存儲空間,但是運行時卻用 1 字節空間。這是因爲字節是運行時和處理器能夠有效使用的最小單位。爲避免在使用數組時的空間浪費,.NET Framework 提供了 System.Collections 命名空間下的 BitArray 類,它被設置成每個布爾值使用 1 位。

bool 不能轉換成數值類型,反之亦然。

== 和 != 運算符用於判斷任何類型相等還是不相等,總是返回一個 bool 值。

(P026)

對於引用類型,默認情況的相同是基於引用的,而不是底層對象的實際值。

相等和比較運算符 == 、 != 、 < 、 > 、 >= 和 <= 用於所有的數值類型,但是用於實數時要特別注意。

比較運算符也用於枚舉 (enum) 類型成員,它比較枚舉的潛在整數值。

&& 和 || 運算符用於判斷 “與” 和 “或” 條件。它們常常與代表 “非” 的 (!) 運算符一起使用。

&& 和 || 運算符會在可能的情況下執行短路計算。

短路計算在允許某些表達式時是必要的。

& 和 | 運算符也用於判斷 “與” 和 “或” 條件。

不同之處是 & 和 | 運算符不支持短路計算。因此它們很少用於代替條件運算符。

不同於 C 和 C++ , & 和 | 運算符在用於布爾表達式時執行布爾比較 (非短路計算) 。& 和 | 運算符只在用於數值運算時才執行位操作。

三元條件運算符 (簡稱爲條件運算符) 使用 q ? a : b 的形式,它在條件 q 爲真時,計算 a,否則計算 b 。

(P027)

條件表達式在 LINQ 語句中特別有用。

C# 中的 char (System.Char 類型的別名) 表示一個 Unicode 字符,它佔用 2 個字節。字符字面值在單引號 (') 中指定。

轉義字符不能按照字面表示或解釋。轉義字符由反斜槓()和一個表示特殊意思的字符組成。

\' 單引號
\" 雙引號
\ 斜線
\0 空
\a 警告
\b 退格
\f 走紙
\n 換行
\r 回車
\t 水平製表符
\v 垂直製表符

\u (或 \x ) 轉義字符通過 4 位十六進制代碼來指定任意 Unicode 字符。

從字符類型到數值類型的隱式轉換隻在這個數值類型可以容納無符號 short 類型時有效。對於其他的數值類型,則需要顯式轉換。

(P028)

C# 中的字符串類型 (System.String 的別名) 表示一些不變的、按順序的 Unicode 字符。字符串字面值在雙引號 (") 中指定。

string 類型是引用類型而不是值類型,但是它的相等運算符卻遵守值類型的語義。

對 char 字面值有效的轉移字符在字符串中也有效。

C# 允許逐字字符串字面值,逐字字符串字面值要加前綴 @ ,它不支持轉義字符。

逐字字符串字面值也可以貫穿多行。

可以通過在逐字字符串中寫兩次的方式包含雙引號字符。

(+) 運算符連接兩個字符串。

右面的操作對象可以是非字符串類型的值,在這種情況下這個值的 ToString 方法將被調用。

既然字符串是不變的,那麼重複地用 (+) 運算符來組成字符串是低效率的 : 一個更好的解決方案是用 System.Text.StringBuilder 類型。

(P029)

字符串類型並不支持 < 和 > 的比較,必須使用字符串類型的 CompareTo 方法。

數組代表固定數量的特定類型元素,爲了高效率地讀取,數組中的元素總是存儲在連續的內存塊中。

數組用元素類型後加方括號表示。

方括號也可以檢索數組,通過位置讀取特定元素。

數組索引從 0 開始。

數組的 Length 屬性返回數組中的元素數量。一旦數組被建立,它的長度將不能被更改。

System.Collection 命名空間和子命名空間提供了像可變數組等高級數據結構。

數組初始化語句定義了數組中的每個元素。

所有的數組都繼承自 System.Array 類,它提供了所有數組的通用服務。這些成員包括與數組類型無關的獲取和定義元素的方法。

建立數組時總是用默認值初始化數組中的元素,類型的默認值是值爲 0 的項。

無論數組元素類型是值類型還是引用類型都有重要的性能影響,若元素類型是值類型,每個元素的值將作爲數組的一部分進行分配。

(P030)

無論是任何元素類型,數組本身總是引用類型對象。

多維數組分爲兩種類型 : “矩形數組” 和 “鋸齒形數組” 。 “矩形數組” 代表 n 維的內存塊,而 “鋸齒形數組” 則是數組的數組。

矩形數組聲明時用逗號 (,) 分隔每個維度。

數組的 GetLength() 方法返回給定維度的長度 (從 0 開始) 。

鋸齒形數組在聲明時用兩個方括號表示每個維度。

鋸齒形數組內層維度在聲明時可不指定。

不同於矩形數組,鋸齒形數組的每個內層數組都可以是任意長度;每個內層數組隱式初始化成空 (null) 而不是一個空數組;每個內層數組必須手工創建。

有兩種方式可以簡化數組初始化表達式。第一種是省略 new 運算符和類型限制條件,第二種是使用 var 關鍵字,使編譯器隱式確定局部變量類型。

(P032)

隱式類型轉換能進一步用於一維數組的這種情況,能在 new 關鍵字之後忽略類型限制符,而由編譯器推斷數組類型。

爲了使隱式確定數組類型正常工作,所有的元素都必須可以隱式轉換成同一種類型。

運行時給所有的數組索引進行邊界檢查,如果使用了不合法的索引,就會拋出 IndexOutOfRangeException 異常。

和 Java 一樣,數組邊界檢查對類型安全和簡化調試是很有必要的。

通常來說,邊界檢查的性能消耗很小,即時編譯器會進行優化。像在進入循環之前預先檢查所有的索引是不安全的,以此來避免在每輪循環中都檢查索引。

C# 提供 "unsafe" 關鍵字來顯式繞過邊界檢查。

變量表示存儲着可變值的存儲空間,變量可以是局部變量、參數 (value 、 ref 或 out) 、 字段 (instance 或 static) 或數組元素。

“堆” 和 “棧” 是存儲變量和常量的地方,它們每個都有不同的生存期語義。

“棧” 是存儲局部變量和參數的內存塊,棧在進入和離開一個函數時邏輯增加和減少。

(P033)

“堆” 是指對象殘留的內存塊,每當一個新的對象被創建時,它就被分配進堆,同時返回這個對象的引用。

當程序執行時,堆在新對象創建時開始填充。

.NET 運行時有垃圾回收器,它會定期從堆上釋放對象。

只要對象沒有被引用,他就會被選中釋放。

無論變量在哪裏聲明,值類型實例以及對象引用一直存在。如果聲明的實例作爲對象中的字段或數組元素,那麼實例存儲於堆上。

在 C# 中你無法顯式刪除對象,但在 C++ 中可以。未引用的對象最終被垃圾回收器回收。

堆也存儲靜態字段和常量。不同於堆上被分配的對象 (可以被垃圾回收器回收),靜態字段和常量將一直存在直到應用程序域結束。

C# 遵守明確賦值的規定。在實踐中,這是指在沒有 unsafe 上下文情況下是不能訪問未初始化內存的。明確賦值有三種含義 :

  1. 局部變量在讀取之前必須被賦值;

  2. 當調用方法時必須提供函數的參數;

  3. 其他的所有變量 (像字段和數組元素) 都自動在運行時被初始化;

(P034)

字段和數組元素都會用其類型的默認值自動初始化。

所有類型實例都有默認值。預定義類型的默認值是值爲 0 的項 :

[類型] - [默認值]

所有引用類型 - null
所有數值和枚舉類型 - 0
字符類型 - '\0'
布爾類型 - false

能夠對任何類型使用 default 關鍵字來獲得其默認值。

自定義值類型中的默認值與自定義類型定義的每個字段的默認值相同。

方法有一連串的參數,其中定義了一系列必須提供給方法的參數。

(P035)

能通過 ref 和 out 修飾符來改變參數傳遞的方式 :

[參數修飾符] - [傳遞類型] - [必須明確賦值的參數]

none - 值類型 - 傳入
ref - 引用類型 - 傳入
out - 引用類型 - 傳出

通常,C# 中參數默認是按值傳遞的,這意味着在將參數值傳給方法時創建參數值的副本。

值傳遞引用類型參數將賦值給引用而不是對象本身。

(P036)

如果按引用傳遞參數,C# 使用 ref 參數修飾符。

注意 ref 修飾符在聲明和調用時都是必需的,這樣就清楚地表明瞭將執行什麼。

ref 修飾符對於轉換方法是必要的。

無論參數是引用類型還是值類型,都可以實現值傳遞或引用傳遞。

out 參數和 ref 參數類似,除了 :

  1. 不需要在傳入函數之前賦值;

  2. 必須在函數結束之前賦值;

(P037)

out 修飾符通常用於獲得方法的多個返回值。

和 ref 參數一樣, out 參數是引用傳遞。

當引用傳遞參數時,是爲已存變量的存儲空間起了個別名,而不是創建了新的存儲空間。

params 參數修飾符在方法最後的參數中指定,它使方法接收任意數量的指定類型參數,參數類型必須聲明爲數組。

(P038)

也可以將通常的數組提供給 params 參數。

從 C# 4.0 開始,方法、構造方法和索引器都可以被聲明成可選參數,只要在聲明時提供默認值,這個參數就是可選參數。

可選參數在調用方法時可以被省略。

編譯器在可選參數被用到的地方用了默認值代替了可選參數。

被其他程序集調用的 public 方法在添加可選參數時要求重新編譯所有的程序集,因爲參數是強制的。

可選參數的默認值必須由常量表達式或無參數的值類型構造方法指定,可選參數不能被標記爲 ref 或 out 。

強制參數必須在可選參數方法聲明和調用之前出現 (params 參數例外,它總是最後出現)。

相反的,必須將命名參數和可選參數聯合使用。

命名參數可以按名稱而不是按參數的位置確定參數。

(P039)

命名參數能按任意順序出現。

不同的是參數表達式按調用端參數出現的順序計算。通常,這隻對相互作用的局部有效表達式有所不同。

命名參數和可選參數可以混合使用。

按位置的參數必須出現在命名參數之前。

命名參數在和可選參數混合使用時特別有用。

如果編譯器能夠從初始化表達式中推斷出變量的類型,就能夠使用 var 關鍵字 (C# 3.0 中引入) 來代替類型聲明。

因爲是直接等價,所以隱式類型變量是靜態指定類型的。

(P040)

當無法直接從變量聲明中推斷出變量類型時,var 關鍵字將降低代碼的可讀性。

表達式本質上表示的是值。最簡單的表達式是常量和變量。表達式能夠用運算符進行轉換和組合。運算符用一個或多個輸入操作數來輸出新的表達式。

C# 中的運算符分爲一元運算符、二元運算符和三元運算符,這取決它們使用的操作數數量 (1 、 2 或 3) 。

二元運算符總是使用中綴標記法,運算符在兩個操作數中間。

基礎表達式由 C# 語言內置的基礎運算符表達式組成。

(. 運算符) 執行成員查找;

(() 運算符) 執行方法調用;

空表達式是沒有值的表達式。

因爲空表達式沒有值,所以不能作爲操作數來創建更復雜的表達式。

賦值表達式用 = 運算符將一個表達式的值賦給一個變量。

(P041)

賦值表達式不是空表達式,實際上它包含了賦值操作的值,因此能再加上另一個表達式。

複合賦值運算符是由其他運算符組合而成的簡化運算符。

當表達式包含多個運算符時,運算符的優先級和結合性決定了計算的順序。

優先級高的運算符先於優先級低的運算符執行。

如果運算符的優先級相同,那麼運算符的結合性決定計算的順序。

二元運算符 (除了賦值運算符、 lambda 運算符 、 null 合併運算符) 是左結合運算符。換句話說,它們是從左往右計算。

賦值運算符、 lambda 運算符、 null 合併運算符和條件運算符是右結合運算符。換句話說,它們從右往左計算。右結合運算符允許多重賦值。

(P043)

函數包含按出現的字面順序執行的語句。語句塊是大括號 ({}) 中出現的一系列語句。

(P044)

聲明語句可以聲明新變量,也可以用表達式初始化變量。聲明語句以分號結束。可以用逗號分隔的列表聲明多個同類型的變量。

常量的聲明和變量聲明類似,除了不能在聲明之後改變它的值和必須在聲明時初始化。

局部變量和常量的作用範圍是在當前的語句塊中。不能在當前的或嵌套的語句塊中聲明另一個同名的局部變量。

變量的作用範圍是它所在的整個代碼段。

表達式語句是表達式也是合法的語句,表達式語句必須改變狀態或調用某些改變的狀態,改變的狀態本質上是指改變一個變量。

可能的表達式語句是 :

  1. 賦值表達式 (包括自增和自減表達式) ;

  2. 方法調用表達式 (有返回值的和無返回值的) ;

  3. 對象實例化表達式;

(P045)

當調用有返回值的構造函數或方法時,並不一定要使用返回值。除非構造函數或方法改變了某些狀態,否則這些語句完全沒作用。

C# 有下面幾種語句來有條件地控制程序的執行順序 :

  1. 選擇語句 (if, switch) ;

  2. 條件語句 (? :) ;

  3. 循環語句 (while 、 do-while 、 for 、 foreach) ;

if 語句是否執行代碼體取決於布爾表達式是否爲真。

如果代碼體是一條語句,可以省略大括號。

if 語句之後可以緊跟 else 分句。

在 else 分句中,能嵌套另一個 if 語句。

(P046)

else 分句總是與其前語句塊中緊鄰的未配對的 if 語句結合。

可以通過改變大括號的位置來改變執行順序。

大括號可以明確地表明結構,這能提高嵌套 if 語句的可讀性 (即使編譯器並不需要)。

從語義上講,緊跟着每一個 if 語句的 else 語句從功能上都是嵌套在 else 語句之中的。

switch 語句可以根據變量可能值的選擇來轉移程序的執行。

switch 語句可以擁有比嵌套 if 語句更加簡短的代碼,因爲 switch 語句只要求表達式計算一次。

(P047)

只能在支持靜態計算的類型表達式中使用 switch 語句,因此限制了它只適用於整數類型、字符串類型和枚舉類型。

在每個 case 分句的結尾,必須用某種跳轉語句明確說明下一步要執行的代碼。這裏有選項 :

  1. break (跳轉到 switch 語句結尾) ;

  2. goto case x (跳轉到另一個 case 分句) ;

  3. goto default (跳轉到 default 分句) ;

  4. 任何其他的跳轉語句 —— return 、 throw 、 continue 或 goto 標籤;

當多於一個值要執行相同代碼時,可以按順序列出共同的 case 條件。

switch 語句的這種特性對於寫出比嵌套 if-else 語句更清晰的代碼來說很重要。

C# 能夠用 while 、 do-while 、 for 和 foreach 語句重複執行一系列語句。

while 循環在布爾表達式爲真時重複執行一段代碼,這個表達式在循環體被執行之前被檢測。

(P048)

do-while 循環在功能上不同於 while 循環的是它在語句塊執行之後檢測表達式 (保證語句塊至少被執行一次) 。

for 循環類似有特殊分句的 while 循環,這些特殊分句用於初始化和累積循環變量。

for 循環有下面的3個分句 :

for (initialization-clause; condition-clause; interation-clause) {statement-or-statement-block}

initialization-clause : 在循環之前執行,用於初始化一個或多個循環變量;
condition-clause : 是布爾表達式,當它爲真時,將執行循環體;
interation-clause : 在每次循環語句體之後執行,通常用於更新循環變量;

for 語句的3個部分都可以被省略,可以通過下面的代碼來實現一個無限循環 (也可以用 while(true) 代替) : for (;;)

(P049)

foreach 語句遍歷可枚舉對象的每一個元素,大多數 C# 和 .NET Framework 中表示集合或元素列表的類型都是可枚舉的。

數組和字符串都是可枚舉的。

C# 中的跳轉語句有 break 、 continue 、 goto 、 return 和 throw 。

跳轉語句違背了 try 語句的可靠性規則,這意味着 :

  1. 跳轉到 try 語句塊之外的跳轉總是在到達目的地之前執行 try 語句的 finally 語句塊;

  2. 跳轉語句不能從 finally 語句塊內跳到塊外;

break 語句用來結束循環體或 switch 語句體的執行。

continue 語句放棄循環體中其後的語句,繼續下一輪循環。

(P050)

goto 語句用於轉移執行到語句塊中的另一個標籤處,或者用於 switch 語句內。

標籤語句僅僅是語句塊中的佔位符,用冒號後綴表示。

goto case case-constant 用於轉移執行到 switch 語句塊中的另一個條件。

return 語句退出方法,如果這個方法有返回值,同時必須返回方法指定返回類型的表達式。

return 語句能出現在方法的任意位置。

throw 語句拋出異常來表示有錯誤發生。

using 語句用於調用在 finally 語句塊中實現 IDisposable 接口的 Dispose 方法。

C# 重載了 using 關鍵字,使它在不同上下文中有不同的含義。

特別注意 using 指令不同於 using 語句。

(P051)

lock 語句是調用 Monitor 類 Enter() 方法和 Exit() 方法的簡化操作。

命名空間是類型名稱必須唯一的作用域,類型通常被組織到分層的命名空間裏,這樣既避免了命名衝突又使類型名更容易被找到。

命名空間組成了類型名的基本部分。

命名空間是獨立於程序集的。

程序集是像 .exe 或 .dll 一樣的部署單元。

命名空間不影響成員的可見性 —— public 、 internal 、 private 等。

namespace 關鍵字爲其中的類型定義了命名空間。

命名空間中的點 (.) 表明嵌套命名空間的層次結構。

可以用包含從外到內的所有命名空間的完全限定名來指代一種類型。

如果類型沒有在任何命名空間中被定義,則說明它存在於全局命名空間內。

全局命名空間也包含了頂級命名空間。

using 指令用於導入命名空間。這是不使用完全限定名來指代某種類型的便捷方法。

(P052)

在不同命名空間定義相同類型名稱是合法的 (而且通常是需要的)。

外層命名空間中聲明的名稱能夠直接在內層命名空間中使用。

如果想使用同一命名空間分層結構的不同分支中的類型,你就要使用部分限定名。

如果相同的類型名出現在內層和外層命名空間中,內層的類型優先。如果要使用外層命名空間中的類型,必須使用它的完全限定名。

(P053)

所有的類型名在編譯時都被轉換成完全限定名,中間語言 (IL) 代碼不包含非限定名和部分限定名。

可以重複聲明同一命名空間,只要它裏面的類型名不衝突。

我們能在命名空間中使用嵌套 using 指令,可以在命名空間聲明中指定 using 指令的範圍。

(P054)

引入命名空間有可能引起類型名的衝突,因此可以只引入需要的類型而不是整個命名空間,爲每個類型創建別名。

外部別名允許引用兩個完全限定名相同的類型,這種特殊情況只發生在兩種類型來自不同的程序集。

(P055)

內層命名空間中的名稱隱藏了外層命名空間中的名稱,但是,有時候即使使用類型的完全限定名也無法解決衝突。

(::) 用於限定命名空間別名。

【第03章】

(P057)

類是最常見的一種引用類型。

複雜的類可能包含一下內容 :

  1. 類屬性 —— 類屬性及類修飾符。非嵌套的類修飾符有 : public 、 internal 、 abstract 、 sealed 、 static 、 unsafe 、 partial ;

  2. 類名 —— 各種類型參數、唯一基類,多個接口;

  3. 花括號內 —— 類成員 (方法、成員屬性、索引器、事件、字段、構造方法、運算符函數、嵌套類型和終止器) ;

字段是類或結構體中的變量。

以下修飾符可以用來修飾字段 :

[靜態修飾符] —— static

[訪問權限修飾符] —— public internal private protected

[繼承修飾符] —— new

[不安全代碼修飾符] —— unsafe

[只讀修飾符] —— readonly

[跨線程訪問修飾符] —— volatile

(P058)

“只讀修飾符” 防止字段值在構造後被更改,只讀字段只能在聲明時或在其所屬的類構造方法中被賦值。

字段不一定要初始化,沒有被初始化的字段系統會賦一個默認值 ( 0 、 \0 、 null 、 false ) 。字段初始化語句在構造方法之前執行。

爲了簡便,可以用逗號分隔的列表聲明一組同類型的字段,這是聲明具有共同屬性和修飾符的一組字段的簡潔寫法。

方法是用一組語句實現某個行爲。方法能從調用語句的特定類型的傳入參數中接收輸入數據,並把輸出數據以特定的返回值類型返回給調用語句。方法也可以返回 void 類型,表明這個方法不向調用方返回任何值。此外,方法還可以通過 ref / out 參數向調用方返回值。

方法簽名在整個類中必須是唯一的,方法簽名包括方法名、參數類型 (但不包括參數名及返回值類型) 。

方法可以用以下的修飾符 :

[靜態修飾符] —— static

[訪問權限修飾符] —— public internal private protected

[繼承修飾符] —— new virtual abstract override sealed

[部分方法修飾符] —— partial

[非託管代碼修飾符] —— unsafe extern

只要確保方法簽名不同,可以在類中重載方法 (多個方法共用同一個方法名) 。

返回值類型和參數修飾符不屬於方法簽名的一部分。

參數是按值傳遞還是按引用傳遞,也是方法簽名的一部分。

構造方法執行類或結構體的初始化代碼,構造方法的定義和方法的定義類似,區別僅在於構造方法名和返回值只能和封裝它的類相同。

(P059)

構造方法支持以下修飾符 :

[訪問權限修飾符] —— public internal private protected

[非託管代碼修飾符] —— unsafe extern

類或結構體可以重載構造方法,爲了避免重複編碼,一個構造方法可以用 this 關鍵字調用另一個構造方法。

(P060)

當一個構造方法調用另一個時,被調用的構造方法先執行。

C# 編譯器自動爲沒有顯式定義構造方法的類生成構造方法。但是,一旦顯式定義了構造方法,系統將不再生成無參數構造方法。

對於結構體來說,無參數構造方法是結構體所固有的,因此,不能自己定義。結構體的隱式構造方法的作用是用默認值初始化每個字段。

字段初始化按聲明的先後順序,在構造方法之前執行。

構造方法不一定都是公有的。通常,定義非公有的構造方法的原因是爲了在一個靜態方法中控制類實例的創建。

靜態方法可以用於從池中返回類對象,而不必創建一個新對象實例,或用來根據不同的輸入屬性返回不同的子類。

(P061)

爲了簡化類對象的初始化,可以在調用構造方法的語句中直接初始化對象的可訪問字段或屬性。

使用臨時變量是爲了確保在初始化過程中如果拋出異常,不會得到一個初始化未完成的對象。

對象初始化器是 C# 3.0 引入的新概念。

(P062)

如果想使程序在不同版本的程序集中保持二進制兼容,最好避免在公有方法中使用可選參數。

this 引用指的是引用類實例自身。

this 引用也用來避免類字段和局部變量或屬性相混淆。

this 引用僅對類或結構體的非靜態成員有效。

屬性內部像方法一樣包含邏輯。

屬性和字段的聲明很類似,但屬性比字段多了一個 get / set 塊。

(P063)

get 和 set 提供屬性的訪問器。

讀取屬性值時會運行 get 訪問器,它必須返回屬性類型的值。

給屬性賦值時,運行 set 訪問器,它有一個命名爲 value 的隱含參數,類型和屬性類型相同,值直接被指定給私有字段。

儘管訪問屬性和字段的方法相同,但不同之處在於,屬性在獲取和設置值時,給實現者提供了完全的控制能力。這種控制能力使得實現者可以選擇所需的任何的內部通信機制,而無需將屬性的內部細節暴露給用戶。

在實際應用中,爲了提高封裝性,可能更多地在公有字段上應用公有屬性。

屬性可以用下面的修飾符 :

[靜態修飾符] —— static

[訪問權限修飾符] —— public internal private protected

[繼承修飾符] —— new virtual abstract override sealed

[非託管代碼修飾符] —— unsafe extern

如果只定義了 get 訪問器,屬性就是隻讀的;如果定義了 set 訪問器,屬性就是隻寫的,但很少用到只寫屬性。

通常屬性會用一個簡短的後臺字段來存儲其所代表的數據,但屬性也可以從其他數據計算出來。

屬性最常見的實現方法是 get 訪問器和 set 訪問器,對一個同類型的私有字段進行簡單的讀寫操作。自動屬性的聲明表明由編譯器提供上述實現方法。編譯器會自動產生一個後臺的私有字段,該字段名由編譯器生成,且不能被引用。

如果希望屬性對外暴露成只讀屬性, set 訪問器可以標記爲 private 的。

在 C# 3.0 中引入了自動屬性。

get 和 set 訪問器可以有不同的訪問級別。

注意,屬性本身被聲明具有較高的訪問權限,然後在需要較低級別的訪問器上添加較低級別的訪問權限修飾符。

C# 屬性訪問器在系統內部被編譯成名爲 get_XXX 和 set_XXX 的方法。

簡單的非虛擬屬性訪問器被 JIT (即時) 編譯器編譯成內聯的,消除了屬性和字段訪問方法的性能差別。內聯是一種優化方法,它用方法的函數體替代方法調用。

通過 WinRT 的屬性,編譯器就可以假設是 put_XXX 命名轉換,而不是 set_XXX 。

索引器爲訪問類或結構體中封裝的列表或字典型數據元素提供了自然的訪問接口。索引器和屬性很相似,但索引器通過索引值而非屬性名訪問數據元素。

string 類具有索引器,可以通過 int 索引訪問其中的每一個 char 值。

當索引是整型時,使用索引器的方法類似於使用數組。

索引器和屬性具有相同的修飾符。

要編寫一個索引器,首先定義一個名爲 this 的屬性,將參數定義放在一對方括號中。

(P065)

如果省略 set 訪問器,索引器就變成只讀的。

索引器在系統內部被編譯成名爲 get_Item 和 set_Item 的方法。

常量是值永遠不會改變的字段。常量在編譯時靜態賦值,並且在使用時,編譯器直接替換該值,類似於 C++ 中的宏。常量可以是內置的數據類型 : bool 、 char 、 string 或枚舉類型。

常量用關鍵字 const 定義,並且必須以特定值初始化。

常量在使用時比靜態只讀字段有更多限制 : 不僅能使用的類型有限,而且初始化字段的語句含義也不同。常量和靜態只讀變量的不同之處還有,常量是在編譯時賦值的。

(P066)

靜態只讀字段可以在每個應用中有不同的值。

靜態只讀字段的好處還有,當提供給其他程序集時,可以更新數值。

從另一角度看,將來可能發生變化的任意值都不受其定義約束,所以不應該表示爲一個常量。

常量也可以在方法內聲明。

常量可以使用以下修飾符 :

[訪問權限修飾符] —— public internal private protected

[繼承修飾符] —— new

靜態構造方法是每個類執行一次,而不是每個類實例執行一次。一個類只能定義一個靜態構造方法,並且必須沒有參數,必須和類同名。

運行時在使用類之前自動調用靜態構造方法,下面兩種行爲可以觸發靜態構造函數 :

  1. 實例化類;

  2. 訪問類的靜態成員;

靜態構造方法只有兩個修飾符 : unsafe 和 extern 。

如果靜態構造方法拋出一個未處理異常,類在整個應用程序的生命週期內都是不可用的。

(P067)

靜態字段在調用靜態構造方法之前執行初始化。如果一個類沒有靜態構造方法,字段在類被使用前初始化或在運行時隨機選一個更早的時間執行初始化 (這說明靜態構造方法的存在可能使字段初始化比正常時間晚執行)。

靜態字段按字段聲明的先後順序初始化。

類可以標記爲 static ,表明它必須僅由靜態成員組成,並且不能產生子類。

System.Console 和 System.Math 類就是靜態類的最好示例。

終止器是只能在類中使用的方法,它在垃圾收集器回收沒有被應用的對象前執行。

終止器的語法是類名加前綴 (~) 。

實際上,這是重載對象的 Finalize() 方法的 C# 語法。

(P068)

終止器允許使用以下修飾符 :

[非託管代碼修飾符] —— unsafe

局部類允許一個類分開定義,典型的用法是分開在多個文件中。從其他源文件自動生成的類需要和自定義的方法交互時,通常使用 partial 類。

每個類必須由 partial 聲明。

局部類的各組成部分不能有衝突的成員。

局部類完全由編譯器處理,也就是說,各組成部分在編譯時必須可用,並必須編譯在同一個程序集中。

有兩個方法爲 partial 類定義基類 : 在每個部分定義同一個基類、僅在其中一部分定義基類。

每個部分都可以獨立定義並實現接口。

局部類可以包含局部方法,這些方法使自動生成的局部類可以爲自定義方法提供自定義鉤子 (hook) 。

(P069)

局部方法由兩部分組成 : 定義和實現。定義一般由代碼生成器產生,而實現多爲手工編寫。

如果沒有提供方法的實現,方法的定義會被編譯器清除。這使得自動代碼生成可以自由提供鉤子 (hook) ,而不用擔心代碼過於臃腫。

局部方法必須是 void 型,並且默認是 private 的。

局部方法在 C# 3.0 中引入。

爲了擴展或自定義原類,類可以繼承另一個類。繼承類讓你可以重用另一個類的方法,而無需重新構建。

一個類只能繼承自唯一的類,但可以被多個類繼承,從而形成類的層次。

子類也被稱爲派生類;基類也被稱爲超類。

(P070)

引用是多態的,意味着 X 類型的變量可以指向 X 子類的對象。

多態性之所以能實現,是因爲子類具有基類的全部特徵。反過來,則不正確。

對象引用可以被 :

  1. 隱式向上轉換成基類的引用;

  2. 顯式向下轉換爲子類的引用;

在可兼容的類型引用之間向上類型轉換或向下類型轉換即爲引用轉換 : 生成一個新的引用指向同一個對象。向上轉換總是能成功,而向下轉換只有在對象的類型符合要求時才能成功。

向上類型轉換創建一個基類指向子類的引用。

向上轉換以後,被引用的對象本身不會被替換或改變。

(P071)

向下類型轉換創建一個子類指向基類的引用。

對於向上轉換,隻影響了引用,被引用的對象沒有變化。

向下轉換必須是顯式轉換,因爲它可能導致運行時錯誤。

如果向下轉換出錯,會拋出 InvalidCastException 。

as 運算符在向下類型轉換出錯時爲變量賦值 null (而不是拋出異常) 。

這個操作相當有用,接下來只需判斷結果是否爲 null 。

如果不用判斷結果是否爲 null ,使用 cast 更好,因爲如果發生錯誤,cast 會拋出描述更清楚的異常。

as 運算符不能用來實現自定義轉換,也不能用於數值型轉換。

as 和 cast 運算符也可以用來實現向上類型轉換,但不常用,因爲隱式轉換就可以實現。

is 運算符用於檢查引用的轉換能否成功,換句話說,它是檢查一個對象是否是從某個特定類派生 (或是實現某個接口),經常在向下類型轉換前使用。

(P072)

is 運算符不能用於自定義類型轉換和數值型類型轉換,但它可以用於拆箱機制的類型轉換。

標識爲 virtual 的函數可以被提供特定實現的子類重載。

方法、屬性、索引器和事件都可以被聲明爲 virtual 。

子類通過 override 修飾符重載虛方法。

虛方法和重載方法的標識、返回值以及訪問權限必須完全一致。

重載方法可以通過 base 關鍵字調用其基類的實現。

從構造方法調用虛方法可能很危險,因爲編寫子類的人在重寫方法時不可能知道正在操作一個未完全實例化的對象。換而言之,重寫方法最終會訪問到一些依賴於未被構造方法初始化的域的方法或屬性。

被聲明爲 abstract 的抽象類不能被實例化,只有抽象類的具體實現子類才能被實例化。

抽象類中可以定義抽象成員,抽象成員和虛成員相似,但抽象成員不提供默認的實現。實現必須由子類提供,除非子類也被聲明爲抽象類。

(P073)

基類和子類可能定義相同的成員。

有時需要故意隱藏一個成員,這種情況下,可以在子類中使用 new 修飾符。

new 修飾符的作用僅爲防止編譯器發出警告。

修飾符 new 把你的意圖傳達給編譯器以及其他編程人員,即重複的成員不是無意的。

C# 在不同的上下文環境中使用 new 關鍵字表達完全不同的含義,特別要注意 new 運算符和 new 成員修飾符的不同。

(P074)

重載的方法成員可用 sealed 關鍵字密封它的實現,以防止該方法被它的更深層次的子類再次重載。

可以在類中使用 sealed 修飾符來密封整個類,含義是密封類中所有的虛方法。

密封類比密封方法成員更常見。

關鍵字 base 和關鍵字 this 很類似,它有兩個重要目的 :

  1. 從子類訪問重載的基類方法成員;

  2. 調用基類的構造方法;

(P075)

子類必須聲明自己的構造方法。

子類必須重新定義它想對外公開的任何構造方法。不過,定義子類的構造方法,也可以通過使用關鍵字 base 調用基類的某個構造方法實現。

關鍵字 base 和 this 用法類似,但 base 關鍵字調用的是基類中的構造方法。

基類的構造方法總是先執行,這保證了 base 的初始化發生在作爲子類的特例初始化之前。

如果子類中的構造方法省略 base 關鍵字,那麼基類的無參構造方法將被隱式調用。

如果基類沒有無參數的構造方法,子類的構造方法中就必須使用 base 關鍵字。

當對象被實例化時,初始化按以下順序進行 :

(1) 從子類到基類 : a. 初始化字段 b. 指定被調用基類的構造方法中的變量;

(2) 從基類到子類 : a. 構造方法體執行;

(P076)

繼承對方法的重載有特殊的影響。

當重載被調用時,類型最明確的優先匹配。

具體調用哪個重載是靜態決定的 (編譯時) 而不是在運行時決定。

object 類 (System.Object) 是所有類型的最終基類。

任何類型都可以向上轉換成 object 類型。

(P077)

棧是一種遵循 LIFO (Last-In First-Out,後進先出法) 的數據結構。

棧有兩種操作 : push 表示一個元素進棧和 pop 表示一個元素出棧。

承載了類的優點,object 是引用類型。

當數值類型和 object 類型之間相互轉換時,公共語言運行時 (CLR) 必須作一些特定的工作,實現數值類型和引用類型的轉換這個過程被稱爲裝箱和拆箱。

裝箱是將數值類型實例轉換成引用類型實例的行爲。

引用類型可以是 object 類或接口。

拆箱需要顯式進行。

運行時檢查提供的值類型是否與真正的對象類型相匹配,並在檢查出錯誤時,拋出 InvalidCastException 。

(P078)

裝箱是把數值類型的實例複製到新對象中,而拆箱是把對象的內容複製回數值類型的實例中。

C# 在靜態 (編譯時) 和運行時都會進行類型檢查。

靜態類型檢查使編譯器能在程序沒有運行的情況下檢查正確性。

在引用或拆箱操作的向下類型轉換時,由 CLR 執行運行時類型檢查。

可以進行運行時類型檢查,是因爲堆棧中的每個對象都在內部存儲了類型標識,這個標識可以通過調用 object 類的 GetType() 方法讀取。

所有 C# 的類型在運行時都會維護 System.Type 類的實例。有兩個基本方法可以獲得 System.Type 對象 :

  1. 在類實例上調用 GetType 方法;

  2. 在類名上使用 typeof 運算符;

GetType 在運行時賦值;typeof 在編譯時靜態賦值 (如果使用泛型類型,那麼它將由即使編譯器解析)。

(P079)

System.Type 有針對類型名、程序集、基類等的屬性。

同時 System.Type 還有作爲運行時反射模式的訪問器。

ToString 方法返回類實例的默認文本表述。這個方法被所有內置類型重載。

如果不重寫 ToString ,那麼這個方法會返回類型名稱。

當直接在數值型對象上調用像 ToString 這樣的重載的 object 成員時,不會發生裝箱。只有進行類型轉換時,纔會執行裝箱操作。

(P080)

結構體和類相似,不同之處在於 :

  1. 結構體是值類型,而類是引用類型;

  2. 結構體不支持繼承 (除了隱式派生自 object 類的,更精確些說,是派生自 System.ValueType) 。

除了以下三項內容,結構體可以包含類的所有成員 :

  1. 無參數的構造方法;

  2. 終止器;

  3. 虛成員;

當表示值類型時使用結構體更理想而不用類。

結構體是值類型,每個實例不需要在堆棧上實例化。

結構體的構造語義如下 :

  1. 隱含存在一個無法重載的無參數構造方法,將字段按位置零;

  2. 定義結構體的構造方法時,必須顯式指定每個字段;

  3. 不能在結構體內初始化字段;

(P081)

爲了提高封裝性,類或類成員會在聲明中添加五個訪問權限修飾符之一,來限制其他類和其他程序集對它的訪問權限 :

[public] —— 完全訪問權限;“枚舉類型成員” 或 “接口” 隱含的訪問權限;

[internal] —— 僅可訪問程序集和友元程序集;“非嵌套類型” 的默認訪問權限;

[private] —— 僅在包含類型可見;類和結構體 “成員” 的默認訪問權限;

[protected] —— 僅在包含類型和子類中可見;

[protected internal] —— protected 和 internal 的訪問權限並集 Eric Lippert 是這樣解釋的 : 默認情況下儘可能將所有成員定義爲私有,然後每一個修飾符都會提高其訪問級別。所以用 protected internal 修飾的成員在兩個方面的訪問級別都提高了。

CLR 有對 protected 和 internal 訪問權限交集的定義,但 C# 並不支持。

(P082)

在高級語義應用中,加上 System.Runtime.CompilerServices.InternalsVisibleTo 屬性,就可以把 internal 成員提供給其他的友元程序集。

類權限是它內部聲明的成員訪問權限的封頂,關於權限封頂最常用的示例是 internal 類中的 public 成員。

當重載基類的函數時,重載函數的訪問權限必須一致。

(P083)

編譯器會阻止使用任何不一致的訪問權限修飾符。

子類可以比基類訪問權限低,但不能比基類訪問權限高。

接口和類相似,但接口只爲成員提供定義而不提供實現。

接口和類的不同之處有 :

  1. 接口的成員都是隱含抽象的。相反,類可以包含抽象成員和有具體實現的成員;

  2. 一個類 (或結構體) 可以實現多個接口。相反,類只能繼承一個類,而結構體完全不支持繼承 (只能從 System.ValueType 派生)。

接口聲明和類聲明很類似,但接口不提供其成員的實現,因爲它的所有成員都是隱式定義爲抽象的,這些成員將由實現接口的類或結構體實現。

接口只能包含方法、屬性、事件、索引器,這些正是類中可以定義爲抽象的成員。

接口成員總是隱式地定義成 public 的,並且不能用訪問修飾符聲明。

實現接口意味着爲其所有成員提供 public 的實現。

可以把對象隱式轉換爲它實現的任意一個接口。

(P084)

接口可以從其他接口派生。

當實現多個接口時,有時成員標識符會有衝突。顯式實現接口成員可以解決衝突。

調用顯式實現成員的唯一方法是先轉換爲相應的接口。

(P085)

另一個使用顯式實現接口成員的原因是,隱藏那些和類的正常用法差異很大或有嚴重干擾的成員。

默認情況下,接口成員的實現是隱式定義爲 sealed 。爲了能重載,必須在基類中標識爲 virtual 或者 abstract 。

顯式實現的接口成員不能標識爲 virtual 的,也不能實現通常意義的重載。但是它可以被重新實現。

子類可以重新實現基類中已經被實現的任意一個接口。不管基類中該成員是不是 virtual 的,當通過接口調用時,重新實現都能夠屏蔽成員的實現。它不管接口成員是隱式還是顯式實現都有效,但後者效果更好。

(P086)

重新實現屏蔽僅當通過接口調用成員時有效,從基類調用時無效。

(P087)

將結構體轉換成接口會引發裝箱機制。調用結構體的隱式實現接口成員不會引發裝箱。

枚舉類型是一種特殊的數值類型,可以在枚舉類型中定義一組命名的數值常量。

(P088)

每個枚舉成員都對應一個整數型,默認情況下 :

  1. 對應的數值是 int 型的;

  2. 按枚舉成員的聲明順序,自動指定的常量爲 0 、 1 、 2 ······ ;

可以指定其他的整數類型代替默認類型。

也可以顯式指定每個枚舉成員對應的值。

編譯器還支持顯式指定部分枚舉成員,沒有指定的枚舉成員,在最後一個顯式指定的值的基礎上遞增。

枚舉類型的實例可以和它對應的整型值互相顯式轉換。

也可以顯式地將一個枚舉類型轉換成另一個。

兩個枚舉類型之間的轉換通過對應的數值進行。

在枚舉表達式中,編譯器對數值 0 進行特別處理,不需要顯式轉換。

(P089)

對 0 進行特別管理原因有兩個 :

  1. 第一個枚舉成員經常被用作 “默認” 值;

  2. 在合併枚舉類型中,0 表示不標識類型;

枚舉類型成員可以合併。爲了避免混淆,合併枚舉類型的成員要顯式指定值,典型的增量爲 2 。

使用位運算符操作合併枚舉類型的值,例如 | 和 & ,它們作用在對應的整型數值上。

依照慣例,當枚舉類型元素被合併時,一定要應用 Flags 屬性。

如果聲明瞭一個沒有標註 Flags 屬性的枚舉類型,枚舉類型的成員仍然可以合併,但是當在該枚舉實例上調用 ToString 方法時,輸出一個數值而非一組名字。

一般來說,合併枚舉類型通常用複數名而不用單數名。

位運算符、算數運算符和比較運算符都返回對應整型值的運算結果。

枚舉類型和整型之間可以做加法,但兩個枚舉類型之間不能做加法。

因爲枚舉類型可以和它對應的整型值相互轉換,枚舉的真實值可能超出枚舉類型成員的數值範圍。

位操作和算數操作也會產生非法值。

(P090)

檢查枚舉值的合法性,靜態方法 Enum.IsDefined 有此功能。

Enum.IsDefined 對標識枚舉類型不起作用。

(P091)

嵌套類型是聲明在另一個類型內部的類型。

嵌套類型有如下特徵 :

  1. 可以訪問包含它的外層類中的私有成員、以及外層類所能訪問的所有內容;

  2. 可以使用所有的訪問權限修飾符,而不僅限於 public 和 internal ;

  3. 嵌套類型的默認訪問權限是 private 而不是 internal ;

  4. 從外層類以外訪問嵌套類型,需要用外層類名稱限定 (就像訪問靜態成員一樣);

所有類型都可以被嵌套,但只有類和結構體才能嵌套其他類型。

(P092)

嵌套類型在編譯器中的應用也很普遍,如編譯器用於生成捕獲迭代和匿名方法結構狀態的私有類。

如果使用嵌套類型的主要原因是避免一個命名空間中類型定義雜亂無章,那麼可以考慮使用嵌套命名空間。使用嵌套類型的原因,應該是利用它較強的訪問控制能力,或者是因爲嵌套類型必須訪問其外層類的私有成員。

C# 對書寫能跨類型複用的代碼,有兩個不同的支持機制 : 繼承和泛化。但繼承的複用性來自基類,而泛化的複用性是通過帶有 “佔位符” 類的 “模板” 。和繼承相比,泛化能提高類型的安全性以及減少類型的轉換和裝箱。

C# 的泛化和 C++ 的模板是相似概念,但它們的工作方法不同。

泛型中聲明類型參數 —— 佔位符類型,由泛型的使用者填充,它支持類型變量。

(P093)

在運行時,所有泛型的實例都是關閉的 —— 佔位符類型填充。

只有在類或方法內部,T 纔可以被定義爲類型參數。

泛化是爲了代碼能跨類型複用而設計的。

泛化方法指在方法的標識符內聲明類參數。

(P094)

通常不需要提供參數的類型給泛化方法,因爲編譯器可以在後臺推斷出類型。

在泛型中,只有新引入類型參數的方法才被歸爲泛化方法 (用尖括號標出) 。

唯有方法和類可以引入類型參數。屬性、索引器、事件、字段、構造方法、運算符都不能聲明類型參數,雖然它們可以參與使用所在的類中已經聲明的類型參數。

構造方法可以參與使用已存在的類型參數,但不能引入新的類型參數。

可以在聲明類、結構體、接口、委託和方法時引入類型參數。其他的結構 (如屬性) 不能引入類型參數,但可以使用類型參數。

泛型類或泛型方法可以有多個參數。

(P095)

泛型類名和泛型方法名可以被重載,只要類型參數的數量不同即可。

習慣上,泛型類和泛型方法如果只有一個類型參數,只要參數的含義明確,一般把這個類型參數命名爲 T 。當使用多個類型參數時,每個類型參數都使用 T 作爲前綴,後面跟一個更具描述性的名稱。

在運行時不存在開放的泛型 : 開放泛型被彙編成程序的一部分而關閉。但運行時可能存在無綁定 (unbound) 泛型,只用作類對象。C# 中唯一指定無綁定泛型的方法是使用 typeof 運算符。

開放泛型類型一般與反射 API 一起使用。

可以用 default 關鍵字獲取賦給泛型類參數的默認值。引用類型的默認值是 null ,數值類型的默認值是將類的所有字段位置 0 。

默認情況下,類型參數可以被任何類型替換。在類型參數上應用約束,可以定義類型參數爲指定類型。

where T : base-class // 基類約束
where T : interface // 接口約束
where T : class // 引用類型約束
where T : struct // 數值類型約束 (排除可空類型)
where T : new() // 無參數構造方法約束
where U : T // 裸類型約束

(P096)

約束可以應用在方法和類的任何類型參數的定義中。

“基類約束” 或 “接口約束” 規定類型參數必須是某個類的子類或實現特定類或接口。這允許參數類可以被隱式轉換成特定類或接口。

“類約束” 和 “結構體約束” 規定 T 必須是引用類型或數值類型 (不能爲空)。

“無參數構造方法約束” 要求 T 有一個公有的無參數構造方法。如果定義了這個約束,就可以在 T 中調用 new() 。

“裸類型約束” 要求一個類型參數從另一個類型參數派生。

(P097)

泛型類和非泛型的類一樣,都可以作爲子類。子類可以讓基類中的類型參數保持開放。

子類也可以用具體類型關閉泛型參數。

子類還可以引入新的類型變量。

技術上,子類型中所有類型參數都是新的 : 可以說子類型關閉後又重新開放了基類的基類參數。這表明子類可以爲其重新打開的類型參數使用更有意義的新名稱。

當關閉類型參數時,類可以用自己作爲實體類。

對每個封裝的類來說,靜態數據是全局唯一的。

(P098)

C# 的類型轉換運算符可以進行多種轉換,包括 :

  1. 數值型轉換;

  2. 引用型轉換;

  3. 裝箱 / 拆箱 轉換;

  4. 自定義轉換 (通過運算符重載) ;

根據原數據的類型,在編譯時決定轉換成何種類型,並實現轉換。因爲編譯時還不知道原數據的確切類型,使得泛型參數具有有趣的語義。

(P099)

假定 S 是 B 的子類,如果 X<S> 允許引用轉換成 X<B> ,那麼稱 X 爲協變類。

由於 C# 符號的共變性 (和逆變性) ,所以 “可改變” 表示可以通過隱式引用轉換進行改變 —— 如 A 是 B 的子類,或者 A 實現 B。數字轉換、裝箱轉換和自定義轉換都不包含在內。

C# 4.0 中,泛化接口支持協變 (泛化委託也支持) ,但泛化類不支持。數組也支持協變 (如 S 是 B 的子類,S[] 可以轉換成 B[]) 。

爲了保證靜態類的安全性,泛化類不是協變的。

(P100)

由於歷史原因,數組 array 類型具有協變性。

在 C# 4.0 中,泛化接口對用 out 修飾符標註的類型參數支持協變。和數組不同,out 修飾符保證了協變性的接口是完全類型安全的。

T 前的 out 修飾符是 C# 4.0 的新特性,表明 T 只用在輸出的位置。

接口中的協變和逆變的典型應用是使用接口 : 很少需要向協變性接口寫入。確切地說,由於 CLR 的限制,爲了協變性將方法參數標註爲 out 是不合法的。

(P101)

不管泛型還是數組,協變 (逆變) 僅對引用轉換的元素有效而對裝箱轉換無效。

泛化接口支持逆變當泛型參數只出現在輸入的位置,且被指定了 in 修飾符時。

【第04章】

(P103)

委託將方法調用者和目標方法動態關聯起來。

代理類型定義了代理實例可調用的方法。

(P104)

委託實例實際上是調用者的代表 : 調用者先調用委託,然後委託調用目標方法。這種間接調用方式可以將調用者和目標方法分開。

調用委託和調用方法類似 (因爲委託的目的僅僅是提供一定程序的間接性) 。

委託和回調相似,是捕獲 C 函數指針等結構體的一般方法。

委託變量動態指定調用的方法。這個特性對於編寫插入式方法非常有用。

(P105)

所有的委託實例都有多播能力。意思是一個委託實例不僅可以引用一個目標方法,而且可以引用一組目標方法。用運算符 + 和 += 聯合多個委託實例。

委託按照添加的順序依次被觸發。

運算符 - 和 -= 從左邊的委託操作數中移除右邊的委託操作數。

可以在委託變量上 + 或 += null 值,等價於爲變量指定一個新值。

同樣,在只有唯一目標方法的委託上調用 -= 等價於爲該變量指定 null 值。

委託是不可變的,因此調用 += 或 -= 的實質是創建一個新的委託實例,並把它賦值給已有變量。

如果多播委託有非 void 的返回類型,調用者從最後一個觸發的方法接收返回值。前面的方法仍然被調用,但返回值都被丟棄了。大部分情況下調用的多播委託都返回 void 類型,所以這個細小的差別就沒有了。

所有委託類型都是從 System.MulticastDelegate 派生的,System.MulticastDelegate 繼承自 System.Delegate。C# 將委託中使用的 + 、 - 、 += 和 -= 都編譯成 System.Delegate 的靜態 Combine 和 Remove 方法。

(P106)

當委託對象指向一個實例方法時,委託對象不僅需維護到方法的引用,而且需維護到方法所屬類實例的引用。 System.Delegate 類的 Target 屬性表示這個類實例 (當委託引用靜態方法時爲 null) 。

(P107)

委託類可以包含泛型參數。

public delegate T Transformer<T>(T arg);

有了泛化委託,我們就可以寫非常泛化的小型委託類,它們可以爲具有任意返回類型和任意多參數的方法服務。

(P108)

在 Framework 2.0 之前,並不存在 Func 和 Action 代理 (因爲那時還不存在泛型)。由於有這個歷史問題,所以 Framework 的許多代碼都使用自定義代理類型,而不使用 Func 和 Action 。

能用委託解決的問題,都可以用接口解決。

在下面的情形中,委託可能是比接口更好的選擇 :

  1. 接口內只定義一個方法;

  2. 需要多播能力;

  3. 訂閱者需要多次實現接口;

(P109)

即使簽名相似,委託類也互不兼容。

如果委託實例指向相同的目標方法,則認爲它們是等價的。

如果多播委託按照相同的順序引用相同的方法,則認爲它們是等價的。

當調用一個方法時,可以給方法的參數提供大於其指定類型的變量,這是正常的多態行爲。基於同樣的原因,委託也可以有大於它目標方法參數類型的參數,這稱爲逆變。

(P110)

標準事件模式的設計宗旨是在其使用公共基類 EventArgs 時應用逆變。

如果調用一個方法,得到的返回值類型可能大於請求的類型,這是正常的多態性行爲。基於同樣的原因,委託的返回類型可以小於它的目標方法的返回值類型,這被稱爲協變。

如果要定義一個泛化委託類型,最好按照如下準則 :

  1. 將只用在返回值的類型參數標註爲協變 (out) ;

  2. 將只用在參數的類型參數標註爲逆變 (in) ;

(P111)

當使用委託時,一般會出現兩種角色 : 廣播者和訂閱者。

廣播者是包含委託字段的類,它決定何時調用委託廣播。

訂閱者是方法目標的接收者,通過在廣播者的委託上調用 += 和 -= ,決定何時開始和結束監聽。一個訂閱者不知道也不干涉其他的訂閱者。

事件是使這一模式正式化的語言形態。事件是隻顯示委託中 廣播 / 訂閱 需要的子特性的結構。使用事件的主要目的在於 : 保護訂閱互不影響。

聲明事件最簡單的方法是,在委託成員的前面加上 event 關鍵字。

(P113)

.NET 框架爲事件定義了一個標準模式,它的目的是保持框架和用戶代碼之間的一致性。

標準事件模式的核心是 System.EventArgs —— 預定義的沒有成員的框架類 (不同於靜態 Empty 屬性) 。

EventArgs 是用於爲事件傳遞信息的基類。

考慮到複用性,EventArgs 子類根據它包含的內容命名 (而非根據將被使用的事件命名),它一般以屬性或只讀字段將數據。

定義了 EventArgs 的子類,下一步是選擇或定義事件的委託,需遵循三條原則 :

  1. 委託必須以 void 作爲返回值;

  2. 委託必須接受兩個參數 : 第一個是 object 類,第二個是 EventArgs 的子類。第一個參數表明事件的廣播者,第二個參數包含需要傳遞的額外信息。

  3. 委託的名稱必須以 EventHandler 結尾。

框架定義一個名爲 System.EventHandler<>的泛化委託,該委託滿足如下條件 :

public delegate void EventHandler<TEventArgs> (object source, TEventArgs e) where TEventArgs : EventArgs

(P114)

最後,該模式要求寫一個受保護的 (protected) 虛方法引發事件。方法名必須和事件名一致,以 On 作前綴,並接受唯一的 EventArgs 參數。

(P115)

如果事件不傳遞額外的信息,可以使用預定義的非泛化委託 EventHandler 。

(P116)

事件訪問器是對 += 和 -= 功能的實現。默認情況下,訪問器由編譯器隱式實現。

編譯器把它轉換爲 :

  1. 一個私有的委託字段;

  2. 一對公有的事件訪問器函數,它們實現私有委託字段的 += 、 -= 運算;

通過自定義事件訪問器,指示 C# 不要產生默認的字段和訪問器邏輯。

顯式定義的事件訪問器,可以在委託的存儲和訪問上進行更復雜的操作。有以下三種常用情形 :

  1. 當事件訪問器僅爲廣播該事件的另一個類作交接;

  2. 當類定義了大量事件,而大部分時間有很少訂閱者。這種情況下,最好在字典中存儲訂閱者的委託實例,因爲字典比大量的空委託字段的引用需要更少的存儲開銷;

  3. 當顯式實現聲明事件的接口時;

事件的 add 和 remove 部分被編譯成 add_XXX 和 remove_XXX 方法。

和方法相似,事件可以是虛擬的 (virtual) 、重載的 (overriden) 、抽象的 (abstract) 或密封的 (sealed) 。事件還可以是靜態的 (static)。

(P117)

Lambda 表達式是寫在委託實例上的匿名方法。

編譯器立即將 Lambda 表達式轉換成下面兩種情形其中的一種 :

  1. 委託實例;

  2. Expression<Tdelegate> 類型的表達式樹,該表達式樹將 Lambda 表達式內的代碼顯示爲可遍歷的對象模式,這使得對 Lambda 表達式的解釋可以延遲到運行時。

編譯器在內部將這種 Lambda 表達式編譯成一個私有方法,並把表達式代碼移到該方法中。

Lambda 表達式有以下形式 : (參數) => 表達式或語句塊。

爲了方便,在只有一個可推測類型的參數時,可以省略小括號。

Lambda 表達式使每個參數和委託的參數一致,表達式的參數 (可以爲 void) 和委託的返回值類型一致。

Lambda 表達式代碼除了可以是表達式還可以是語句塊。

Lambda 表達式通常和 Func 或 Action 委託一起使用,因此可以將前面的表達式寫成下面的形式。

(P118)

Lambda 表達式是 C# 3.0 中引入的概念。

編譯器通常可以根據上下文推斷出 Lambda 參數的類型,但當不能推斷時,必須明確指定每個參數的類型。

Lambda 表達式可以引用方法內的內部變量和參數 (外部變量) 。

Lambda 表達式引用的外部變量稱爲捕獲變量。捕獲變量的表達式稱爲一個閉包。

捕獲的變量在真正調用委託時被賦值,而不是在捕獲時賦值。

Lambda 表達式可以自動更新捕獲變量。

捕獲變量的生命週期可以延伸到和委託的生命週期相同。

(P119)

在 Lambda 表達式內實例化的局部變量,在每次調用委託實例期間是唯一的。

在內部捕獲是通過把被捕獲的變量 “提升” 到私有類的字段實現的。當方法被調用時,實例化該類,並將其生命週期綁定在委託的實例上。

當捕獲 for 或 foreach 語句中的循環變量時,C# 把這些循環變量看做是聲明在循環外部的。這表明每個循環捕獲的是相同的變量。

(P120)

匿名方法是 C# 2.0 引入的特性,並通過 C# 3.0 的 Lambda 表達式得到大大擴展。

匿名方法類似於 Lambda 表達式,但沒有下面的特性 :

  1. 確定類型的參數;

  2. 表達式語法 (匿名方法必須是語句塊) ;

  3. 在指定到 Expression<T> 時,編譯成表達式樹的功能;

寫匿名方法的方法是 : delegate 關鍵字後面跟參數聲明 (可選) ,然後是方法體。

(P121)

完全省略參數聲明是匿名方法獨有的特性 —— 即使委託需要這些參數聲明。

匿名方法和 Lambda 表達式使用同樣的方法捕獲外部變量。

try 語句是爲了處理錯誤或清理代碼而定義的語句塊。try 塊後面必須跟有 catch 塊或 finally 塊或兩個塊都有。

當 try 塊執行發生錯誤時,執行 catch 塊;當結束 try 塊時 (如果當前是 catch 塊,則當結束 catch 塊時),不管有沒有發生錯誤,都執行 finally 塊來清理代碼。

catch 塊可以訪問 Exception 對象,該對象包含錯誤信息。catch 中可以彌補錯誤也可以再次拋出異常。當僅僅是記錄錯誤或要拋出更高層次的錯誤時,我們選擇再次拋出異常。

finally 塊在程序中起決定作用,因爲任何情況下它都被執行,通常用於清除任務。

(P122)

異常處理需要幾百個時鐘週期,代價相對較高。

當拋出異常時,公共語言運行時 CLR 詢問 : 當前是否在能捕獲異常的 try 語句塊中運行 ?

  1. 如果是,執行轉到相應的 catch 塊,如果 catch 塊成功地運行結束,執行轉到 try 下面的語句 (如果存在,finally 塊優先執行) ;

  2. 如果否,執行跳轉到調用函數,重複上述詢問 (在執行 finally 塊之後) ;

如果沒有用於處理異常的函數,用戶將看到一個錯誤提示框,並且程序終止。

catch 子句定義捕獲哪些類型的異常,這些異常應該是 System.Exception 或 System.Exception 的子類。

捕獲 System.Exception 表示捕獲所有可能的異常,用於以下情況 :

  1. 不管哪種特定類型的異常,程序都可以修復;

  2. 希望重新拋出該異常 (可以在記入日誌後);

  3. 程序終止前的最後一個錯誤處理;

(P123)

更常見的做法是,爲了避免處理程序沒有被定義的情況,只捕獲特定類型的異常。

可以在多個 catch 子句中處理各種異常類型。

對於每一種給定的異常,只有一個 catch 子句執行。如果想要建立捕獲更普遍的異常的安全網,必須把處理特定異常的語句放在前面。

如果不需要使用變量值,不指定變量也可以捕獲異常。

甚至,變量和類型可以都省略,表示指捕獲所有異常。

除 C# 外的其他語言中,可以拋出不是派生自 Exception 類的對象 (但不推薦) 。 CLR 自動把此對象封裝在 RuntimeWrappedException 類中 (該類派生自 Exception) 。

無論是否拋出異常,也不管 try 程序塊是否完全執行,finally 程序塊總是被執行。通常用 finally 程序塊來清除代碼。

在以下情況下執行 finally 程序塊 :

  1. catch 塊執行完成;

  2. 由於跳轉語句 (如 return 或 goto) 離開 try 塊;

  3. try 塊結束;

(P124)

finally 塊爲程序添加了決定性內容,在下面實例中,無論是否符合以下條件,打開的文件總能被關閉 :

  1. try 塊正常結束;

  2. 因爲是空文件,提前返回 EndOfStream ;

  3. 讀取文件時拋出 IOException 異常;

在 finally 塊中調用對象的 Dispose 方法是貫穿 .NET 框架的標準約定,且在 C# 的 using 語句中也明確支持。

許多類內部封裝了非託管資源,例如文件管理、圖像管理、數據庫連接等。這些類實現 System.IDisposable 接口,這個接口定義了一個名爲 Dispose 的無參數方法,用於清除這些非託管資源。

using 語句提供了一種在 finally 塊中調用 IDisposable 接口對象的 Dispose 方法的優雅方法。

(P125)

可以在運行時或用戶代碼中拋出異常。

可以捕獲異常後再重新拋出。

如果將 throw 替換爲 throw ex,那麼這個例子仍然有效,但是新產生異常的 StackTrace 屬性不再反映原始的錯誤。

(P126)

重新拋出異常不會影響異常的 StackTrace 屬性,當重新拋出一個不同類型的異常時,可以設置 InnerException 屬性爲原始的異常,這樣有利於調試。幾乎所有類型的異常都可以實現這一目的。

System.Exception 類的最重要的屬性有下面幾個 :

  1. StackTrace —— 表示從異常的起源到 catch 塊的所有方法的字符串;

  2. Message —— 描述異常的字符串;

  3. InnerException —— 導致外部異常的內部異常 (如果有的話) ,它本身還可能有另一個 InnerException ;

所有的 C# 異常都是運行時異常,沒有和 Java 對等的編譯時檢查異常。

下面的異常類型在 CLR 和 .NET 框架中廣泛使用,可以在程序中自主拋出這些異常或者將它們作爲基類來派生自定義異常類 :

  1. System.ArgumentException —— 當使用不恰當的參數調用函數時拋出,這通常表明程序有 bug ;

  2. System.ArgumentNullException —— ArgumentException 的子類,當函數參數爲 null (意料外的) 時拋出;

  3. System.ArgumentOutOfRangeException —— ArgumentException 的子類,當屬性值太大或太小時拋出 (通常是數值型) ;

  4. System.InvalidOperationException —— 不管是哪種特定的屬性值,當對象的狀態不符合方法正確執行的要求時拋出;

  5. System.NotSupportedException —— 該異常拋出表示不支持特定功能;

  6. System.NotImplementedException —— 該異常拋出表明某個方法還沒有具體實現;

  7. System.ObjectDisposedException —— 當函數調用的對象已被釋放時拋出;

另一個常見的異常類型是 NullReferenceException 。當一個對象的值爲 null 並訪問它的成員時,CLR 就會拋出這個異常 (表示代碼有 bug) 。

當方法出錯時,可以選擇返回某種類型的錯誤代碼或拋出異常。一般情況下,如果錯誤發生在正常的工作流之外或者希望方法的直接調用者不進行錯誤處理時,拋出異常。但有些情況下最好給調用者提供兩種選擇。

如果類型解析失敗,Parse 方法拋出異常,TryParse 方法返回 false 。

(P128)

Enumerator 是隻讀的,且遊標只能在順序值上向前移,實現下面對象之一 :

  1. System.Collections.IEnumerator ;

  2. System.Collections.Generic.IEnumerator<T> ;

從技術上講,任何具有 MoveNext 方法和 Current 屬性的對象,都被看作是 enumerator 類型的。

foreach 語句用來在可枚舉的對象上執行迭代操作。可枚舉對象是順序表的邏輯表示,它本身不是一個遊標,但對象自身產生遊標。

可枚舉對象可以是 :

  1. IEnumerable 或 IEnumerable<T> 的實現;

  2. 具有名爲 GetEnumerator 的方法返回一個 enumerator ;

IEnumerator 和 IEnumerable 在 System.Collections 命名空間中定義。

IEnumerator<T> 和 IEnumerable<T> 在 System.Collection.Generic 命名空間中定義。

如果 enumerator 實現了 IDisposable ,那麼 foreach 語句也起到 using 語句的作用。

(P129)

可以通過一個簡單的步驟實例化和填充可枚舉的對象,它要求可枚舉對象實現 System.Collections.IEnumerable 接口,並且有可調用的帶適當個數參數的 Add 方法。

和 foreach 語句是枚舉對象的使用者相對,迭代器是枚舉對象的生產者。

(P130)

return 語句表示該方法返回的值,而 yield return 語句表示從本枚舉器產生的下一個元素。

迭代器是包含一個或多個 yield 語句的方法、屬性或索引器,迭代器必須返回以下四個接口之一 (否則,編譯器會報錯) :

// Enumerable 接口
System.Collections.IEnumerable
System.Collections.Generic.IEnumerable<T>

// Enumerator 接口
System.Collections.IEnumerator
System.Collections.Generic.IEnumerator<T>

返回 enumerable 接口和返回 enumerator 接口的迭代器具有不同的語義。

yield break 語句表明迭代器不返回後面的元素而是提前結束。

(P131)

迭代器塊中使用 return 語句是不合法的,必須使用 yield break 語句來代替。

yield return 語句不能出現在帶 catch 子句的 try 語句塊中。

yield return 語句也不能出現在 catch 或 finally 語句塊中。出現這些限制的原因是編譯器必須將迭代器轉換爲帶有 MoveNext 、 Current 和 Dispose 成員的普通類,而且轉換異常處理語句塊可能會大大增加代碼複雜性。

但是,可以在只帶 finally 語句塊的 try 塊中使用 yield 語句。

迭代器具有高度可組合性。

(P132)

迭代器模式的組合性在 LINQ 中是非常有用的。

引用類型可以表示一個不存在的值,即空引用。

(P133)

若要在數值類型中表示空值,必須使用特殊的結構即可空類型 (Nullable)。可空類型是由數據類型後加一個 “?” 表示的。

T? 轉換成 System.Nullable<T> 。而 Nullable<T> 是一個輕量的不變結構,它只有兩個域,分別是 Value 和 HasValue 。System.Nullable<T> 實質上是很簡單的。

public struct Nullable<T> where T : struct
{
public T Value {get;}
public bool HasValue {get;}
public T GetValueOrDefault();
public T GetValueOrDefault(T defaultValue);
}

當 HasValue 爲假時嘗試獲取 Value,程序會拋出一個 InvalidOperationException 異常。

當 HasValue 爲真時,GetValueOrDefault() 會返回 Value ,否則返回 new T() 或者一個特定的自定義默認值。

T? 的默認值是 null 。

從 T 到 T? 的轉換是隱式的,而從 T? 到 T 的轉換則必須是顯式的。

顯式強制轉換與直接調用可空對象的 Value 屬性實際上是等價的。因此,當 HasValue 爲假時,程序會拋出一個 InvalidOperationException 異常。

如果 T? 是裝箱的,那麼堆中的裝箱值包含的是 T ,而不是 T? 。這種優化方式是可以實現的,因爲裝箱值是一個可能已經賦值爲空的引用類型。

(P134)

C# 允許通過 as 運算符對一個可空類型進行拆箱。如果強制轉換出錯,那麼結果爲 null 。

Nullable<T> 結構體並沒有定義諸如 < 、 > 或者 == 的運算符。儘管如此,下面的代碼仍然能夠正常編譯和執行。

運算符提升表示可以隱式地使用 T 的運算符來處理 T? 。

編譯器會基於運算符類型來執行空值邏輯。

提升 “等於運算符” 處理空值的方式與引用類型相似,這意味着兩個空值是相等的。而且 :

  1. 如果只有一個操作數爲空,那麼結果不相等;

  2. 如果兩個操作數都不爲空,那麼比較它們的 Value ;

(P135)

關係運算符的運算原則表明空值操作數的比較是無意義的,這意味着比較兩個空值或比較一個空值與一個非空值的結果都是 false 。

可以混合使用可空和不可空類型,這是因爲 T 與 T? 之間存在隱式轉換機制。

如果操作數的類型是 bool? ,那麼 & 和 | 運算符會將 null 作爲一個未知值看待。所以,null | true 的結果爲真,因爲 :

  1. 如果未知值爲假,那麼結果爲真;

  2. 如果未知值爲真,那麼結果爲真;

(P136)

?? 運算符是空值合併運算符,它既可用來計算可空值類型,也可用來計算引用類型。也就是說,如果操作數不爲空,直接計算;否則,計算器默認值。

?? 運算符的結果等同於使用一個顯式默認值調用 GetValueOrDefault ,除非當變量不爲空時傳遞給 GetValueOrDefault 的表達式從未求值。

可空類型在將 SQL 映射到 CLR 時是非常有用的。

可空類型還可用於表示所謂環境屬性的後備字段,如果環境屬性爲空,那麼返回其父類的值。

(P137)

運算符可以經過重載實現更自然的自定義類型語法,運算符重載非常適合用來表示最普通的基本數據類型的自定義結構體。

下面的運算符也可以重載 :

  1. 隱式和顯式轉換 (使用 implicit 和 explicit 關鍵字實現) ;

  2. 常量 true 和 false;

下面的運算符可以間接進行重載 :

  1. 複合賦值運算符 (例如 += 、 /=) 可以通過重載非複合運算符 (例如 + 、 /) 進行隱式重載;

  2. 條件運算符 && 和 || 可以通過重載位運算符 & 和 | 進行隱式重載;

(P138)

運算符是通過聲明一個運算符函數進行重載的。運算符函數具有以下規則 :

  1. 函數名是通過 operator 關鍵字及其後的運算符指定的;

  2. 運算符函數必須標記爲 static 和 public ;

  3. 運算符函數的參數表示的是操作數;

  4. 運算符函數的返回類型表示的是表達式的結果;

  5. 運算符函數所聲明的類型至少有一個操作數;

重載一個賦值運算符會自動支持相應的複合賦值運算符。

成對重載 : C# 編譯器要求邏輯上成對的運算符必須同時定義。這些運算符包括 (== 、 !=) 、 (< 、 >) 和 (<= 、 >=) 。

Equals 和 GetHashCode : 在大多數情況中,如果重載了 (==) 和 (!=) ,那麼通常也需要重載對象中定義的 Equals 和 GetHashCode 方法,使之具有合理的行爲。如果沒有按要求重載,那麼 C# 編譯器將會發出警告。

IComparable 和 IComparable<T> : 如果重載了 (< 、 >) 和 (<= 、 >=),那麼還應該實現 IComparable 和 IComparable<T> 。

(P139)

隱式和顯式轉換也是可重載的運算符,這些轉換經過重載後一般能使強關聯類型之間的轉換變得更加簡明和自然。

如果要在弱關聯類型之間進行轉換,那麼更適合採用以下方式 :

  1. 編寫一個具有該轉換類型的參數的構造函數;

  2. 編寫 ToXXX 和 (靜態) FromXXX 方法進行類型轉換;

(P140)

擴展方法允許一個現有類型擴展新的方法而不需要修改原始類型的定義。

擴展方法是靜態類的靜態方法,其中第一個參數需要使用 this 修飾符,類型就是擴展的類型。

(P141)

擴展方法是 C# 3.0 後增加的特性。

擴展方法類似於實例方法,也支持一種鏈接函數的方法。

只有命名空間在定義域內,我們才能夠訪問擴展方法。

任何兼容的實例方法總是優先於擴展方法。

如果兩個擴展方法名稱相同,那麼擴展方法必須作爲一個普通的靜態方法調用,才能夠區分所調用的方法。然而,如果其中一個擴展方法具有更具體的參數,那麼有更具體參數的方法優先級更高。

(P143)

匿名類型是一個由編譯器臨時創建來存儲一組值的簡單類。如果要創建一個匿名類型,我們可以使用 new 關鍵字,後面加上對象初始化語句,在其中指定該類型包含的屬性和值。

必須使用 var 關鍵字來引用一個匿名類型,因爲類型的名稱是編譯器產生的。

匿名類型的屬性名可以從本身是一個標識符或以標識符結尾的表達式得到。

如果這兩個匿名類型實例的元素是相同類型的,並且它們在相同的程序集中聲明,那麼它們在內部是相同的類型。

匿名類型的 Equals 方法也被重載了,從而能夠執行正確的等於比較運算。

(P144)

匿名類型主要是在編寫 LINQ 查詢時使用,並且是 C# 3.0 後纔出現的特性。

動態綁定是將綁定 (解析類型、成員和操作的過程) 從編譯時延遲到運行時。

在編譯時,如果程序員知道某個特定函數、成員或操作的存在,而編譯器不知道,那麼動態綁定是很有用的。

這種情況通常出現在操作動態語言 (如 IronPython) 和 COM 時,而且如果不使用動態綁定,就只能使用反射機制。

動態類型是通過上下文關鍵字 dynamic 聲明的。

動態綁定類型會告訴編譯器 “不要緊張” 。

無論綁定的是什麼樣的方法,其底線是已知綁定是由編譯器實現的,而且綁定是完全依賴於之前已經知道的操作數類型,這就是所謂的靜態綁定。

(P145)

動態類型類似於 object ,同樣不表現爲一種類型。其區別是能夠在編譯時在不知道它存在的情況下使用它。

動態對象是基於其運行時類型進行綁定的,而不是基於編譯時類型。

當編譯器遇到一個動態綁定表達式時 (通常是一個包含任意動態類型值的表達式) ,它僅僅對表達式進行打包,而綁定則在後面的運行時執行。

在運行時,如果一個動態對象實現了 IDynamicMetaObjectProvider ,那麼這個接口將用來執行綁定。否則,綁定的發生方式就幾乎像是編譯器已經事先知道動態對象的運行時類型一樣。我們將這兩種方式稱爲自定義綁定和語言綁定。

COM 可認爲是第三種綁定方式。

自定義綁定是通過實現了 IDynamicMetaObjectProvider (IDMOP) 而實現的。

(P146)

動態綁定會損壞靜態類型安全性,但不會影響運行時類型安全性。與反射機制不同,不能通過動態綁定繞過成員訪問規則。

靜態和動態綁定之間最顯著的差異在於擴展方法。

動態綁定也會對性能產生影響。然而,由於 DLR 的緩存機制對同一個動態表達式的重複調用進行了優化,允許在一個循環中高效地調用動態表達式。這個優化機制能夠使一個簡單的動態表達式的處理負載對硬件的性能影響控制在 100ms 以內。

如果一個成員綁定失敗,那麼程序會拋出 RuntimeBinderException 異常,可以將它看作是一個運行時的編譯錯誤。

dynamic 和 object 類型之間可以執行一個深度等值比較。在運行時,下面這個表達式的結果爲 true :

typeof(dynamic) = typeof (object)

(P147)

與對象引用相似,動態引用可以指向除指針類型以外的任意類型的對象。

在結構上,對象引用和動態引用之間沒有任何區別。

動態引用可以直接在它所指的對象上執行動態操作。

動態類型會對其他所有類型進行隱式轉換。

如果要成功進行轉換,動態對象的運行時類型必須能夠隱式轉換到目標的靜態類型上。

(P148)

var 和 dynamic 類型表面上是相似的,但是它們實際上是有區別的 :

var 由編譯器確定類型。

dynamic 由運行時確定類型。

一個由 var 聲明的變量的靜態類型可以是 dynamic 。

域、屬性、方法、事件、構造函數、索引器、運算符和轉換都是可以動態調用的。

dynamic 的標準用例是包含一個動態接受者。

然而,還可以使用動態參數調用已知的靜態函數。這種調用受到動態重載解析的影響,並且可能包括 :

  1. 靜態方法;

  2. 實例構造函數;

  3. 已知靜態類型的接收者的實例方法;

(P149)

動態類型用在動態綁定中。但是,靜態類型在可能的情況下也用在動態綁定中。

(P150)

有一些函數是不能夠動態調用的,如下 :

  1. 擴展方法 (通過擴展方法語法) ;

  2. 接口的所有成員;

  3. 子類隱藏的基類成員;

擴展方法成爲只適用於編譯時的概念。

using 指令在編譯後會消失 (當它們在綁定過程中完成了將簡單的名稱映射到完整命名空間的任務之後) 。

(P151)

特性是添加自定義信息到代碼元素 (程序集、類型、成員、返回值和參數) 的擴展機制;

特性的一個常見例子是序列化,就是將任意對象轉換爲一個特定格式或從特定格式生成一個對象的過程。在這情況中,某個字段的屬性可以指定該字段的 C# 表示方式和該字段的表示方式之間的轉換。

特性是通過直接或間接地繼承抽象類 System.Attribte 的方式定義的。

如果要將一個特性附加到一個代碼元素中,那麼就需要在該代碼元素之前用方括號指定特性的類型名稱。

編譯器能夠識別這個特性,如果某個標記爲棄用的類型或成員被引用時,編譯器會發出警告。

按照慣例,所有特性類型都以 Attribute 結尾,C# 能夠識別這個後綴,也可以在附加一個屬性時省略這個後綴。

C# 語言和 .NET Framework 包含了大量的預定義特性。

特性可能具有一些參數。

特性參數分爲兩類 : 位置和命名。

位置參數對應於特性類型的公開構造函數的參數;命令參數則對應於該特性類型的公開字段或公開屬性。

當指定一個特性時,必須包含對應於其中一個特性構造函數的位置參數。命名參數則是可選的。

(P152)

特性目標不需要顯式指定,特性目標就是它後面緊跟的代碼元素而且一般是一個類型或類型成員。然而,也可以給程序集附加一些特性,這要求顯式地指定特性的目標。

一個代碼元素可以指定多個特性,每一個特性可以列在同一對方括號中 (用逗號分割) 或者在多對方括號中或者結合兩種方式。

從 C# 5 開始,可以給可選參數添加 3 個調用者信息屬性中的一個,它們可以讓編譯器從調用者代碼獲取參數的默認值 :

  1. [CallerMemberName] —— 表示調用者的成員名稱;

  2. [CallerFilePath] —— 表示調用者的源代碼文件路徑;

  3. [CallerLineNumber] —— 表示調用者源代碼文件的行號;

(P153)

調用者信息特性很適合用於記錄日誌以及實現一些模式,如當一個對象的某個屬性發生變化時,觸發一個變化通知事件。事實上,.NET 框架有一個專門實現這個效果的標準接口 INotifyPropertyChanged (位於 System.ComponentModel) 。

(P154)

C# 支持通過標記爲不安全和使用 /unsafe 編譯器選項編譯的代碼塊中的指針直接進行內存操作。指針類型主要用來與 C 語言 API 進行互操作,但是也可用來訪問託管堆以外的內存,或者分析嚴重影響性能的熱點。

使用 unsafe 關鍵字標記一個類型、類型成員或語句塊,就可以在該範圍內使用指針類型和對內存執行 C++ 中的指針操作。

不安全代碼與對應的安全實現相比運行速度更快。

fixed 語句是用來鎖定託管對象的。

由於這可能對運行時效率產生一定的影響,所以 fixed 代碼塊只能短暫使用,而且堆分配應該避免出現在 fixed 代碼塊中。

(P155)

除了 & 和 * 運算符,C# 還支持 C++ 中的 -> 運算符,可以在結構體中使用。

我們可以在代碼中顯式地通過 stackalloc 關鍵字分配棧中的內存,由於這部分內存是從棧上分配的,所以其生命週期僅限於方法的執行時間,這點與其他的局部變量相同,這個代碼塊可以使用 [] 運算符實現內存索引。

我們也可以使用 fixed 關鍵字在一個結構體代碼塊中分配內存。

fixed 表示兩個不同的方面 : 大小固定和位置固定。

(P156)

空指針 (void) 不給出假定底層數據的具體類型,它對於處理原始內存的函數是非常有用的。任意指針類型都可以隱式地轉換爲 void 。 void* 不可以被解除引用,算術運算符不能通過 void 指針執行。

指針也很適於訪問位於託管堆之外的數據 (如與 C DLL 或 COM 交互時) ,以及處理不在主存中的數據 (如圖形化內存或嵌入式設備的存儲介質) 。

(P157)

預處理指令向編譯器提供關於代碼範圍的額外信息。最常用的預處理指令是條件指令,它提供了一種將某些代碼加入或排除出編譯範圍的方法。

通過 #if 和 #elif 指令,可以使用 || 、 && 和 ! 運算符在多個符號上執行或、與、非操作。

#error 和 #warning 符號會要求編譯器在遇到一些不符合要求的編譯符號時產生一條警告信息或錯誤信息,從而防止出現條件指令的偶然誤用。

(P158)

使用 Conditional 修飾的特性只有在出現指定的預處理符號時才編譯。

(P159)

文檔註釋是一種嵌入的、記錄類型或成員的 XML 。文檔註釋位於類型或成員聲明之前,以三個斜線開頭。

也可以採用以下方法 (注意開頭有兩個星號) 。/ */

如果使用 /doc 指令進行編譯,那麼編譯器會將文檔註釋存儲到一個 XML 文件中,並進行校對,這個特性主要有兩種作用 :

  1. 如果與編譯的程序集位於同一個文件夾,那麼 Visual Studio 會自動讀取這個 XML 文件,使用這些信息向同名程序集的使用者提供 IntelliSense 成員清單;

  2. 第三方工具 (如 Sandcastle 和 NDoc) 可以將 XML 文件轉換成 HTML 幫助文件;

【第05章】

(P163)

.NET Framework 中幾乎所有的功能都是通過大量的託管類型提供的,這些類型被組織成有層次的命名空間,並且被打包成一套程序集,與 CLR 一起構成 .NET 平臺。

有些 .NET 類型是由 CLR 直接使用的,並且對於託管的宿主環境而言是必不可少的。這些類型位於一個名爲 mscorlib.dll 的程序集中,包括 C# 的內置類型,以及基本的集合類、流處理類型、序列化、反射、多線程和原生互操作性。

除此之外是一些附加類型,它們充實了 CLR 層面的功能,提供了其他一些特性,如 XML 、網絡和 LINQ 等 。這些類型位於 System.dll 、 System.Xml.dll 和 System.Core.dll 中,並且與 mscorlib 一起提供豐富的編程環境供 .NET Framework 的其他部分使用。

.NET Framework 的其餘部分是由一些實用 API 組成的,主要包括以下三個方面的功能 :

  1. 用戶接口技術;

  2. 後臺技術;

  3. 分佈式系統技術;

C# 5.0 對應 CLR 4.5,這個版本比較特殊,因爲它屬於 CLR 4.0 的補丁版本。

這意味着安裝 CLR 4.5 之後,目標平臺是 CLR 4.0 的應用實際上運行在 CLR 4.5 上。

(P164)

程序集和命名空間在 .NET Framework 中是相互交叉的。

(P164)

[.NET Framework 4.5 新特性]

Framework 4.5 新特性包括 :

  1. 通過返回 Task 的方法廣泛支持異步編程;

  2. 支持 zip 壓縮協議;

  3. 通過新增 HttpClient 類改進 HTTP 支持;

  4. 改進垃圾收集器和程序集資源回收的性能;

  5. 支持 WinRT 互操作性和開發 Metro 風格平板應用的 API ;

此外,還有一個新的 TypeInfo 類,以及可以指定與正則表達工作超過時間匹配的超時時間。

在並行計算領域,還有一個全新庫 Dataflow,可用於開發 生產者 / 消費者 風格的網格。

此外,WPF 、 WCF 和 WF (工作流基礎) 庫也有一些改進。

許多核心類型定義在以下程序集中 : mscorlib.dll 、 System.dll 和 System.Core.dll 。第一個程序集 mscorlib.dll 包括運行時環境本身所需要的類型;System.dll 和 System.Core.dll 包含程序員所需要的其他核心類型。

[.NET Framework 4.0 新特性]

Framework 4.0 增加了以下新特性 :

  1. 新的核心類型 : BigInteger (大數字) 、 Complex (複數) 和元組;

  2. 新的 SortedSet 集合;

  3. 代碼協定,使方法能夠通過共同的義務和責任實現更可靠的交互;

  4. 直接支持內存映射文件;

  5. 延遲的文件和目錄 I / O 方法,它們返回 IEnumerable<T> 而不是數組;

  6. 動態語言運行時 (DLR) 成爲 .NET Framework 的一部分;

  7. 安全透明,簡化了保證部分可信環境中程序庫安全性的方法;

  8. 新的多線程結構,包括更強大的 Monitor.Enter 重載、新的信號發送類 (Barrier 和 CountdownEvent) 和延遲初始化原語;

  9. 支持多核處理的並行計算 API ,包括 Parallel LINQ (PLINQ) 、命令式數據與任務並行性結構、支持併發的集合和低延遲同步機制與 spinning 原語;

  10. 用於監控應用程序域資源的方法;

Framework 4.0 還包含了一些 ASP.NET 的改進,包括 MVC 框架和 Dynamic Data,以及 Entity Framework 、 WPF 、 WCF 和 Workflow 等方面的改進。此外,它還包含了新的 Managed Extensibility Framework 庫,以幫助運行時環境實現組合、發現和依賴注入。

(P165)

大多數的基礎類型都直接位於 System 命名空間。其中包括 C# 的內置類型、 Exception 基類、 Enum 、 Array 和 Delegate 基類、以及 Nullable 、 Type 、 DateTime 、 TimeSpan 和 Guid 。System 命名空間也包含執行數字計算功能 (Math) 、生成隨機數 (Random) 和各種數據類型轉換 (Convert 和 BitConvert) 的類型。

System 命名空間還定義了 IDisposable 接口和與垃圾回收器交互的 GC 類。

在 System.Text 命名空間中有一個 StringBuilder 類,以及處理文本編碼的類型。

在 System.Text.RegularExpressions 命名空間中有一些執行基於模式的搜索和替換操作的高級類型。

.NET Framework 提供了各種處理集合項目的類,其中包括基於鏈表和基於字典的結構,以及一組統一它們常用特性的標準接口。

System.Collections //非泛型類型
System.Collections.Generic //泛型框架
System.Collections.Specialized //強類型框架
System.Collections.ObjectModel //自定義框架基類
System.Collections.ConCurrent //線程安全框架

(P166)

Framework 3.5 增加了語言集成查詢 (Language Integrated Query,LINQ) 。LINQ 允許對本地和遠程集合 (例如 SQL Server 表) 執行類型安全查詢。

LINQ 的最大優勢是提供了一種跨多個域的統一查詢 API 。

Metro 模板不包含整個 System.Data.* 命名空間。

LINQ to SQL 和 Entity Framework API 使用了 System.Data 命名空間的 ADO.NET 底層類型。

XML 在 .NET Framework 中被廣泛使用,同時也得到廣泛支持。

操作線程和異步操作的類型位於 System.Threading 和 System.Threading.Tasks 命名空間。

(P167)

Framework 提供了基於流的模型進行底層 輸入 / 輸出 操作。流一般用於文件和網絡連接的直接讀寫操作,它們可以被鏈接和封裝到裝飾流中,從而實現壓縮或加密功能。

Stream 和 I / O 類型是在 System.IO 命名空間中定義的。

可以通過 System.Net 中的類型直接訪問標準的網絡協議,如 HTTP 、 FTP 、 TCP / IP 和 SMTP 。

Framework 提供了幾個可以將對象保存爲二進制或文本方式的系統,這些系統是分佈式應用程序技術所必需的,如 WCF 、 Web Services 和 Remoting ,它們也可用於將對象保存到文件和從文件恢復對象。

Metro 模板不包含二進制序列化引擎。

C# 程序編譯產生的程序集包含可執行指令 (存儲爲中間語言或 IL) 和元數據,它描述了程序的類型、成員和屬性。通過反射機制,可以在運行時檢查元數據或者執行某些操作,如動態調用方法。

通過 Reflection.Emit 可以隨時創建新代碼。

(P168)

動態編程的類型位於 System.Dynamic 中。

.NET Framework 具有自己的安全層,從而能夠將程序集裝入沙箱,甚至將自己裝入沙箱。

Metro 模板只包含 System.Security ;加密操作則在 WinRT 中處理。

C# 5 的異步函數可以顯著簡化併發編程,因爲它們減少了底層技術的使用。然而,開發者有時候仍然需要使用信號發送結構、線程內存儲、讀 / 寫 鎖等。

線程類型位於 System.Threading 命名空間。

CLR 支持在一個進程中增加額外的隔離級別,即應用程序域。

AppDomain 類型定義在 System 命名空間中。

原生互操作性使您能夠調用未託管 DLL 中的函數、註冊回調函數、映射數據結構和操作原生數據類型。COM 互操作性使您能夠調用 COM 類型和將 .NET 類型傳遞給 COM 。

.NET Framework 提供了 4 種支持基於用戶界面的應用程序的 API 。

  1. ASP.NET (System.Web.UI) 編寫運行在標準網頁瀏覽器上的瘦客戶端應用程序;

  2. Silverlight 在網頁瀏覽器上實現富用戶界面;

  3. Windows Presentation Foundation (System.Windows) 編寫富客戶端應用程序;

  4. Windows Forms (System.Windows.Forms) 支持遺留富客戶端應用程序;

(P169)

一般而言,瘦客戶端應用程序指的是網站;而富客戶端應用程序則是最終用戶必須下載或安裝在客戶端計算機上的程序。

富客戶端的方法是在客戶端和數據庫之間插入一箇中間層,中間層運行在一臺遠程應用程序服務器上 (通常與數據庫服務器一起) ,並通過 WCF 、 Web Services 或 Remoting 與富客戶端通信。

在編寫網頁時,可以選擇傳統的 Web Forms 或者新的 MVC (模型 - 視圖 - 控制器) API 。這兩種方法都基於 ASP.NET 基礎框架。從一開始,Framework 就支持 Web Forms ;MVC 則是在後來 Ruby on Rails 和 MonoRail 流行之後纔出現的。

Web Forms 仍然適合用來編寫主要包含靜態內容的網頁。

AJAX 的使用可以通過注入 jQuery 等庫進行簡化。

編寫 ASP.NET 應用程序的類型位於 System.Web.UI 命名空間及其子命名空間中,並且屬於 System.Web.dll 程序集。

Silverlight 在技術上並不屬於 .NET Framework 的主框架 : 它是一個獨立的框架,包含了一部分的 Framework 核心特性,增加了作爲網頁瀏覽器插件運行的功能。

(P170)

Silverlight 主要用於一些邊緣場景。

Windows Metro 庫同樣不屬於 .NET 框架,它只用於在 Windows 8 中開發平板電腦界面。

Metro API 源於 WPF 的啓發,並且使用 XAML 實現佈局。其命名空間包括 Windows.UI 和 Windows.UI.Xaml 。

WPF 是在 Framework 3.0 時引入的,用來編寫富客戶端應用程序。

WPF 的規模和複雜性使學習週期比較長。

編寫 WPF 應用程序的類型位於 System.Windows 命名空間以及除 System.Windows.Forms 之外的所有子命名空間中。

與 WPF 相比,Windows Forms 相對簡單,它支持編寫一般 Windows 應用程序時所需要使用的大多數特性,也能夠良好地兼容遺留應用程序。

Windows Forms 的學習過程相對簡單,並有豐富的第三方控件支持。

(P171)

Windows Forms 類型位於命名空間 System.Windows.Forms (在 System.Windows.Forms.dll 中) 和 System.Drawing (在 System.Drawing.dll) 中。其中後者包含了繪製自定義控件的 GDI+ 類型。

ADO.NET 是託管的數據訪問 API 。雖然它的名稱源於 20 世紀 90 年代的 ADO (ActiveX Data Objects) ,但是這兩種技術是完全不同的。

ADO.NET 包含兩個主要的底層組件 :

  1. 提供者層 —— 提供者模型定義了數據庫提供者底層訪問的通用類和接口。這些接口包括連接、命令,適配器和讀取器 (數據庫的只向前的只讀遊標) 。 Framework 包含對 Microsoft SQL Server 和 Oracle 的原生支持,具有 OLE-DB 和 ODBC 提供者。

  2. DataSet 模型 —— 一個 DataSet 是一個數據的結構化緩存。它類似於一個常駐內存的原始數據庫,其中定義了 SQL 結構,如表、記錄行、字段、關係、約束和視圖。通過對數據緩存的編程,可以減少數據庫的交互數量、增加服務器可擴展性以及加快富客戶端用戶界面的響應速度。 DataSet 是可序列化的,它支持通過客戶端和服務器應用程序之間的線路傳輸。

提供者層只有兩個 API ,它們提供了通過 LINQ 查詢數據庫的功能 :

  1. LINQ to SQL (從 Framework 3.5 開始引入) ;

  2. Entity Framework (從 Framework 3.5 SP1 開始引入) ;

這兩種技術都包含 對象 / 關係 映射器 (ORM) ,意味着它們會自動將對象 (基於定義的類) 映射到數據庫的記錄行。這允許用戶通過 LINQ 查詢這些對象,而不需要編寫 SQL 語句查詢並且不需要手動編寫 SQL 語句進行對象更新。

DataSet 仍然是唯一能夠存儲和序列化狀態變化的技術 (這在多層應用程序中是非常有用的) 。

現在還沒有現成的便捷方法可以使用 Microsoft 的 ORM 來編寫 N 層應用程序。

LINQ to SQL 比 Entity Framework 更簡單、更快速,並且一般會產生更好的 SQL 。Entity Framework 則更具靈活性,可以在數據庫和查詢的類之間創建複雜的映射。除了 SQL Server ,Entity Framework 還支持一些第三方數據庫。

Windows Workflow 是一個對可能長期運行的業務過程進行建模和管理的框架。Workflow 目標是成爲一個標準的提供一致性和互操作性的運行時庫。Workflow 有助於減少動態控制的決策樹的編碼量。

Windows Workflow 嚴格意義上並不是一種後臺技術,可以在任何地方使用它。

Workflow 是從 .NET Framework 3.0 開始出現的,它的類型定義在 System.Workflow 命名空間中。實際上 Workflow 在 Framework 4.0 中進行了修改,增加的新類型位於 System.Activities 命名空間。

(P172)

Framework 允許通過 System.EnterpriseServices 命名空間中的類型與 COM+ 進行互操作,以實現諸如分佈式事物等服務。它也支持通過 System.Messaging 中的類型使用 MSMQ (Microsoft Message Queuing) ,微軟消息隊列實現異步的單向消息傳遞。

WCF 是 Framework 3.0 引入的一個複雜的通信基礎架構。WCF 非常靈活且可配置,這使它的兩個預處理器 —— Remoting 和 (.ASMX) Web Services ,大多是冗餘的。

WCF 、 Remoting 和 Web Services 很相似的方面就是它們都實現以下允許客戶端和服務器應用程序進行通信的基本模型 :

  1. 在服務器端,可以指定希望遠程客戶端應用程序能夠調用的方法;

  2. 在客戶端,可以指定或推斷將要調用的服務器方法的簽名;

  3. 在服務器端和客戶端,都可以選擇一種傳輸和通信協議 (在 WCF 中,這是通過一個綁定完成的) ;

  4. 客戶端建立一個服務器連接;

  5. 客戶端調用遠程方法,並在服務器上透明地執行;

WCF 會通過服務協定和數據協定進一步對客戶端和服務器進行解耦。概念上,客戶端會發送一條 (XML 或二進制) 消息給遠程服務的終端,而非直接調用一個遠程方法。這種解耦方式的好處是客戶端不會依賴於 .NET 平臺或任意私有的通信協議。

WCF 是高度可配置的,它支持廣泛的標準化消息協議,包括 WS-* 。

WCF 的另一個好處是可以直接修改協議,而不需要修改客戶端或服務器應用程序的其他內容。

與 WCF 通信的類型位於 System.ServiceModel 命名空間中。

Remoting 和 .ASMX Web Services 是 WCF 的預處理器,雖然 Remoting 仍然適合在相同進程中的應用程序域之間進行通信,但是它們在 WCF 中幾乎是冗餘的。

Remoting 的功能針對一些緊密耦合的應用程序。

Web Services 針對一些低耦合或 SOA 類型應用程序。

Web Services 只能使用 HTTP 或 SOAP 作爲傳輸和格式化協議,而應用程序一般是運行在 IIS 上。

互操作性的好處在於性能成本方面 —— Web Services 應用程序一般在執行和開發時間上的速度都比精心設計的 Remoting 應用程序慢。

Remoting 的類型位於 System.Runtime.Remoting 命名空間中;而 Web Services 的類型則位於 System.Web.Services 中。

(P173)

通過一個安全的 HTTP 通道進行連接時,WCF 允許通過 System.IdentityModel.Claims 和 System.IdentityModel.Policy 命名空間中的類型指定一個 CardSpace 身份。

【第06章】

(P174)

編程所需要的許多核心工具都不是由 C# 語言提供的,而是由 .NET Framework 中的類型提供的。

一個 C# 的 char 表示一個 Unicode 字符,它是 System.Char 結構體的別名。

System.Char 定義了許多處理字符的靜態方法,如 ToUpper 、 ToLower 和 IsWhiteSpace 。可以通過 System.Char 類型或它的別名 char 調用這些方法。

ToUpper 和 ToLower 會受到最終用戶的語言環境的影響,這可能會導致出現細微的缺陷。

(P175)

System.Char 、 System.String 還提供了針對語言變化的 ToUpper 和 ToLower ,它們加上後綴 Invariant 。

char 保留的大多數靜態方法都與字符分類有關。

(P176)

對於更細的分類,char 提供了一個名爲 GetUnicodeCategory 的靜態方法,它返回一個 UnicodeCategory 枚舉值。

通過顯式轉換一個整數,可以產生一個位於 Unicode 集之外的 char 。要檢測字符的有效性,我們可以調用 char.GetUnicodeCategory : 如果結果是 UnicodeCategory.OtherNotAssigned ,那麼這個字符就是無效的。

一個 char 佔用 16 個二進制位。

C# 的 string (== System.String) 是一個不可變的 (不可修改的) 字符序列。

創建字符串的最簡單的方法就是給變量定義一個字面值。

要創建一個重複的字符序列,可以使用 string 的構造函數。

還可以從 char 數組創建字符串,而 ToCharArray 方法則是執行相反操作。

我們還可以重載 string 的構造方法來接受各種 (不安全的) 指針類型,以便創建其他類型字符串。

空字符串是長度爲 0 的字符串。如果要創建空字符串,可以使用一個字母值或靜態的 string.Empty 字段;如果要測試空字符串,可以執行一個等值比較或測試它的 Length 屬性。

由於字符串是引用類型,它們也可能是 null 。

(P177)

靜態的 string.IsNullOrEmpty 方法是測試一個給定字符串是 null 還是空白的快捷方法。

字符串的索引器可以返回一個指定索引位置的字符。與所有操作字符串的方法相似,它是從 0 開始計數的索引。

string 還實現了 IEnumerable<char> ,所以可以用 foreach 遍歷它的字符。

在字符串內搜索的最簡單方法是 Contains 、 StartsWith 和 EndsWith 。所有這些方法都返回 true 或 false 。

Contains 方法並沒有提供這種重載的便利方法,但是可以使用 IndexOf 方法實現相同的效果。

IndexOf 方法更強大 : 它會返回指定字符或子字符串的首次出現位置 (-1 表示該子字符串不存在) 。

StartsWith 、 EndsWith 和 IndexOf 都有重載方法,我們可以指定一個 StringComparison 枚舉變量或 CultureInfo 對象,控制大小寫和文字順序。默認爲使用當前文化規則執行區分大小寫的匹配。

LastIndexOf 與 IndexOf 類似,但是它是從後向前開始搜索的。

IndexOfAny 則返回任意一系列字符的首次匹配位置。

LastIndexOfAny 則在相反方向執行相同的操作。

由於 String 是不可變的,所有 “處理” 字符串的方法都會返回一個新的字符串,而原始字符串則不受影響 (其效果與重新賦值一個字符變量一樣) 。

Substring 是取字符串的一部分。

(P178)

如果省略長度,那麼會得到剩餘的字符串。

Insert 和 Remove 會從一個指定位置插入或刪除一些字符。

PadLeft 和 PadRight 會用特定字符將字符串 (如果未指定,則使用空格) 填充成指定的長度。

如果輸入字符串長度大於填充長度,那麼返回不發生變化的原始字符串。

TrimStart 和 TrimEnd 會從字符串的開始或結尾刪除指定的字符;Trim 則用兩個方法執行刪除操作。默認情況下,這些函數會刪除空白字符 (包括空格、製表符、換行符和這些字符的 Unicode 變體) 。

Replace 會替換字符串中出現的特定字符或子字符串。

ToUpper 和 ToLower 會返回輸入字符串相應的大寫和小寫字符。默認情況下,它們會受用戶的當前語言設置的影響;ToUpperInvariant 和 ToLowerInvariant 總是採用英語字母表規則。

Split 接受一個句子,返回一個單詞數組。

默認情況下,Split 使用空白字符作爲分隔符;經過重載後也可以接受包含 char 或 string 分隔符的 params 數組。

Split 還可以選擇接受一個 StringSplitoptions 枚舉值,它支持刪除一些空項 : 這在一行單詞由多種分隔符分隔時很有用。

靜態的 Join 方法執行與 Split 相反的操作,它需要一個分隔符和字符串數組。

靜態的 Concat 方法與 Join 類似,但是它只接受字符串數組參數,並且沒有分隔符。

Concat 與 + 操作符效果完全相同 (實際上,編譯器會將 + 轉換成 Concat) 。

(P179)

靜態的 Format 方法提供了創建嵌入變量字符串的便利方法。嵌入的變量可以是任意類型;而 Format 會直接調用它們的 ToString 。

包含嵌入變量的主字符串稱爲 “組合格式字符串” 。調用 String.Format 時,需要提供一個組合格式字符串,後面緊跟每一個嵌入式變量。

花括號裏面的每一個數字稱爲格式項。這些數字對應參數位置,後面可以跟 :

  1. 逗號與應用的最小寬度;

  2. 冒號與格式字符串;

最小寬度用於對齊各個列,如果這個值爲複數,那麼數據就是左對齊;否則,數據就是右對齊的。

信用額度是通過 “C” 格式字符串格式化爲貨幣值。

組合格式字符串的缺點是它很容易出現一些只有在運行時才能發現的錯誤。

進行兩個值比較時,.NET Framework 有兩個不同的概念 : 等值比較和順序比較。等值比較會判斷兩個實例在語義上是否是相同的;而順序比較則將兩個 (如果有) 實例按照升序或降序排列,然後判斷哪一個首先出現。

(P180)

等值比較並不是順序比較的一個子集,這兩種方法有各自不同的用途。

對於字符串等值比較,可以使用 == 操作符或者其中一個字符串的 Equals 方法。後者功能更強一些,因爲它們允許指定一些選項,如區分大小寫。

另一個不同點是,如果變量被轉換成 object 類型,那麼 == 就不一定是按字符串處理。

對於字符串順序比較,可以使用 CompareTo 實例方法或靜態的 Compare 和 CompareOrdinal 方法 : 這些方法會返回一個正數、負數或 0 ,這取決於第一個值是在第二個值之後、之前還是同時出現。

字符串比較有兩種基本的算法 : 按順序的和區分文化的。順序比較會直接將字符解析爲數字 (根據它們的 Unicode 數值);文化比較則參照特定的字母表來解析字符。特殊的文化有兩種 : “當前文化” ,這是基於計算機控制面板的設置;“不變文化” ,這在任何計算機上都是相同的。

對於等值比較,順序和特定文化的算法都是很有用的。然而,在排序時,人們通常選擇詞義相關的比較 : 對字符串按字母表排序時,需要一個字母順序表。順序比較則使用 Unicode 數字位置值,這可能會使英語字符按字母順序排序 —— 但是即使這樣也可能不滿足你的期望。

不變文化封裝了一個字母表,它認爲大寫字符與其對應的小寫字符是相鄰的。

順序算法將所有大寫字母排列在前面,然後纔是全部小寫字符。

儘管順序比較有一些侷限性,但是字符串的 == 操作符總是執行區分大小寫的順序比較。當不帶參數調用時,string.Equals 的實例版本也是一樣的;這定義了 string 類型的 “默認” 等值比較行爲。

字符串的 == 和 Equals 函數選擇順序算法的原因是它既高效又具有確定性。字符串等值比較被認爲是基礎操作,並且遠比順序比較的使用更頻繁。

等式的 “嚴格” 概念也與常見的 == 操作符用途保持一致。

(P181)

靜態方法會更適合一些,因爲即使其中一個或兩個字符串爲 null 它也一樣有效。

String 的 CompareTo 實例方法執行區分文化和區分大小寫的順序比較。與 == 操作符不同,CompareTo 不使用順序比較 : 對於順序比較,區分文化的算法更有效。

Compare 實例方法實現了 IComparable 泛型接口,這是在整個 .NET Framework 中使用的標準比較協議。這意味着字符串的 CompareTo 定義了默認的順序行爲字符串。

所有順序比較的方法都會返回正數、負數 或 0 ,這取決於第一個值是在第二個值之後、之前還是相同位置。

(P182)

StringBuilder 類 (System.Text 命名空間) 表示一個可變 (可編輯) 的字符串。使用 StringBuilder ,可以 Append 、 Insert 、 Remove 和 Replace 子字符串,而不需要替換整個 StringBuilder 。

StringBuilder 的構建函數可以選擇接受一個初始字符串值,以及其內部容量的初始值 (默認是 16 個字符) 。如果需要更大的容量,那麼 StringBuilder 會自動調整它的內部結構,以容納 (會有一些性能開銷) 最大的容量 (默認爲 int.MaxValue) 。

StringBuilder 的一個普通使用方法是通過重複調用 Append 來創建一個長字符串。這個方法比複雜連接普通字符串類型要高效得多。

AppendLine 執行新添加一行字符串 (在 Windows 中是 "\r\n") 的 Append 操作。

AppendFormat 接受一個組合格式字符串,與 String.Format 類似。

除了 Insert 、 Remove 和 Replace 方法 (Replace 函數類似於字符串的 Replace),StringBuilder 定義了一個 Length 屬性和一個可寫的索引器,可用來 獲取 / 設置 每個字符串。

如果要清除 StringBuilder 的內容,我們可以創建一個新的 StringBuilder 或者將它的 Length 設爲 0 。

(P183)

將 StringBuilder 的 Length 設置爲 0 不會減少它的內部容量。

Unicode 具有約一百萬個字符的地址空間,目前已分配的大約有十萬個。

.NET 類型系統的設計使用的是 Unicode 字符集。但是,ASCII 是隱含支持的,因爲它是 Unicode 的子集。

UTF-8 對於大多數文本而言是最具空間效率的 : 它使用 1~4 個字節來表示每個字符。

UTF-8 是最普遍的文本文件和流的編碼方式 (特別是在互聯網上) ,它是 .NET 中默認的流 I / O 編碼方式 (事實上,它幾乎是所有語言隱含的默認編碼方式) 。

UTF-16 使用一個或兩個 16 位字來表示一個字符,它是 .NET 內部用來表示字符和字符串的方式。有一些程序也使用 UTF-16 寫文件。

UTF-32 是空間效率最低的 : 每一個代碼點直接對應一個 32 位數,所以每個字符都會佔用 4 個字節。因此,UTF-32 很少使用。然而,它可以簡化隨機訪問,因爲每個字符都對應相同的字節數。

System.Text 中的 Encoding 類是封裝文本編碼類的通用基本類型。它有一些子類,它們的作用是封裝各種編碼方式的相似特性。初始化一個正確配置類的最簡單方法是用一個標準的 IANA 名稱調用 Encoding.GetEncoding 。

最常用的編碼也可以通過專用的 Encoding 靜態屬性獲取。

(P184)

靜態的 GetEncodings 方法會返回所有支持的編碼方式清單以及它們的標準 IANA 名稱。

Encoding 對象最常見的應用是控制文件或流的文本讀寫操作。

UTF-8 是所有文件和流 I / O 的默認文本編碼方式。

Encoding 對象和字節數組之間也可以進行互相轉換。GetBytes 方法將使用指定的編碼方式將 string 轉換爲 byte[];而 GetString 則將 byte[] 轉換爲 string 。

(P185)

.NET 將字符和字符串存儲爲 UTF-16 格式。

在 System 命名空間中有三個不可變結構可用來表示日期和時間 : DateTime 、 DateTimeOffset 和 TimeSpan 。而 C# 沒有定義與這些類型相對應的關鍵字。

TimeSpan 表示一段時間間隔或者是一天內的時間。對於後者,他就是一個 “時鐘” 時間 (不包括日期) ,它等同於從半夜 12 點開始到現在的時間 (假設沒有夏時制) 。TimeSpan 的最小單位爲 100 納秒,最大值爲 1 千萬天,可以爲正數或負數。

創建 TimeSpan 的方法有三種 :

  1. 通過其中一個構造方法;

  2. 通過調用其中一個靜態的 From... 方法;

  3. 通過兩個 DateTime 相減得到;

(P186)

如果希望指定一個單位的時間間隔,如分鐘、小時等,那麼靜態的 From.. 方法更方便。

TimeSpan 重載了 < 、 > 、 + 和 - 操作符。

Total... 屬性則返回表示整個時間跨度的 double 類型值。

靜態的 Parse 方法則執行與 ToString 相反的操作,它能將一個字符串轉換爲一個 TimeSpan 。

TryParse 執行與 ToString 相同的操作,但是當轉換失敗時,它會返回 false ,而不是拋出異常。

XmlConvert 類也提供了符合標準 XML 格式化協議的 TimeSpan 字符串轉換方法。

TimeSpan 的默認值是 TimeSpan.Zero 。

TimeSpan 也可用於表示一天內時間 (從半夜 12 點開始經過的時間) 。要獲得當前的時間,我們可以調用 DateTime.Now.TimeOfDay 。

(P187)

DateTime 和 DateTimeOffset 表示日期或者時間的不可變結構。它們的最小單位爲 100 納秒,值的範圍從 0001 到 9999 年。

DateTimeOffset 是從 Framework 3.5 開始引入的,在功能上類似於 DateTime 。它的主要特性是能夠存儲 UTC 偏移值,這允許我們比較不同時區的時間值時得到更有意義的結果。

DateTime 和 DateTimeOffset 在處理時區方式上是不同的。DateTime 具有三個狀態標記,可表示 DateTime 是否與下列因素相關 :

  1. 當前計算機的本地時間;

  2. UTC (相當於現代的格林威治時間) ;

  3. 不確定;

DateTimeOffset 更加特殊 —— 它將 UTC 的偏移量存儲爲一個 TimeSpan 。

這會影響等值比較結果,而且是在 DateTime 和 DateTimeOffset 之間進行選擇的主要依據 :

  1. DateTime 會忽略三個比較狀態標記,並且當兩個值的年、月、日、時、分等相等時就認爲它們是相等的;

  2. 如果兩個值引用相同的時間點,那麼 DateTimeOffset 就認爲它們是相等的;

夏時制會使這個結果差別很大,即使應用程序不需要處理多個地理時區。

在大多數情況中,DateTimeOffset 的等值比較邏輯會更好一些。

(P188)

如果在運行時指定與本地計算機相關的值,使用 DateTime 會更好。

DateTime 定義了能夠接受年、月和日以及可選的時、分、秒和毫秒的構造方法。

如果只指定日期,那麼時間會被隱含地設置爲半夜時間 (00:00:00) 。

DateTime 構造方法也允許指定一個 DateTimeKind —— 這是一個具有以下值的枚舉值 : Unspecified 、 Local 、 Utc 。

這三個值與前一節所介紹的三個狀態標記相對應。

Unspecified 是默認值,它表示 DateTime 是未指定時區的。

Local 表示與當前計算機的本地時區相關。

本地 DateTime 不包含它引用了哪一個特定的時區,而且與 DateTimeOffset 不同的是,它也不包含 UTC 偏移值。

DateTime 的 Kind 屬性返回它的 DateTimeKind 。

DateTime 的構造方法也經過重載從而可以接受 Calendar 對象 —— 允許使用 System.Globalization 中所定義的日曆子類指定一個時間。

DateTime 總是使用默認的公曆。

如果要使用另一個日曆進行計算,那麼必須使用 Calendar 子類自己的方法。

也可以使用 long 類型的計數值 (ticks) 來創建 DateTime,其中計數值是從午夜開始算起的 100 納秒數。

在互操作性上,DateTime 提供了靜態的 FromFileTime 和 FromFileTimeUtc 方法來轉換一個 Windows 文件時間 (由 long 指定),並且提供了 FromOADate 來轉換一個 OLE 自動日期 / 日期 (由 double 指定) 。

要從字符串創建 DateTime,我們必須調用靜態的 Parse 或 ParseExact 方法。

這兩個方法都接受可選標記和格式提供者;ParseExact 還接受格式字符串。

(P189)

DateTimeOffset 具有類似的構造方法,其區別是還需要指定一個 TimeSpan 類型的 UTC 偏移值。

TimeSpan 必須剛好是整數分鐘,否則函數會拋出一個異常。

DateTimeOffset 也有一些接受 Calendar 對象、 long 計數值的構造方法,以及接受字符串的靜態的 Parse 和 ParseExact 方法。

還可以通過構造方法從現有的 DateTime 創建 DateTimeOffset 。

也可以通過隱式轉換創建。 從 DateTime 隱式轉換到 DateTimeOffset 是很簡單的,因爲大多數的 .NET Framework 類型都支持 DateTime —— 而不是 DateTimeOffset 。

如果沒有指定偏移量,那麼可以使用以下規則從 DateTime 值推斷出偏移值 :

  1. 如果 DateTime 具有一個 UTC 的 DateTimeKind ,那麼其偏移量爲 0 ;

  2. 如果 DateTime 具有一個 Local 或 Unspecified (默認) 的 DateTimeKind ,那麼偏移量從當前的本地時區計算得到;

爲了在其他方法中進行轉換,DateTimeOffset 提供了三個屬性,它們返回 DateTime 類型的值 :

  1. UtcDateTime 屬性會返回一個 UTC 時間表示的 DateTime ;

  2. LocalDateTime 屬性返回一個以當前本地時區 (在需要時進行轉換) 表示的 DateTime ;

  3. DateTime 屬性返回一個以任意指定的時區表示的 DateTime ,以及一個 Unspecified 的 Kind ;

DateTime 和 DateTimeOffset 都具有一個靜態的 Now 屬性,它會返回當前的日期和時間;

DateTime 也具有 Today 屬性,它返回日期部分;

(P190)

靜態的 UtcNow 屬性會返回以 UTC 表示的當前日期和時間。

所有這些方法的精度取決於操作系統,並且一般是在 10 ~ 20 毫秒內。

DateTime 和 DateTimeOffset 提供了返回各種 日期 / 時間 的類似實例屬性。

DateTimeOffset 也有一個類型爲 TimeSpan 的 Offset 屬性。

調用 DateTime 的 ToString 會將結果格式化爲一個短日期 (全部是數字) ,後跟一個長時間 (包括秒) 。

(P191)

默認情況下,操作系統的控制面板決定日、月或年是否在前、是否使用前導零,以及是使用 12 小時還是 24 小時時間格式。

調用 DateTimeOffset 的 ToString 效果是一樣的,只是它同時返回偏移值。

ToShortDateString 和 ToLongDateString 方法只返回日期部分。

ToShortTimeString 和 ToLongTimeString 方法只返回時間部分。

剛剛介紹的這四個方法實際上是四個不同的格式字符串的快捷方式。ToString 重載後可以接受一個格式字符串和提供者,這允許指定大量的選項,並且控制區域設置的應用方式。

靜態的 Parse 和 ParseExact 方法執行與 ToString 相反的操作,它們將一個字符串轉換成一個 DateTime 或 DateTimeOffset 。Parse 方法重載後也可以接受格式提供者。

因爲 DateTime 和 DateTimeOffset 是結構體,它們是不可爲空的。當需要將它們設置爲空時,可以使用以下兩種方法 :

  1. 使用一個 Nullable 類型值;

  2. 使用靜態域 DateTime.MinValue 或 DateTimeOffset.MinValue (這些類型的默認值) ;

使用一個可空值通常是最佳方法,因爲編譯器會防止出現錯誤。DateTime.MinValue 對於兼容 C# 2.0 (引入了可空類型) 之前編寫的代碼是很有用的。

(P192)

當比較兩個 DateTime 實例時,只有它們的計數值是可以比較的,它們的 DateTimeKinds 是被忽略的。

TimeZone 和 TimeZoneInfo 類提供了關於時區名稱、 UTC 偏移量和夏令時規則等信息。

TimeZoneInfo 在兩者中較爲強大,並且是 Framework 3.5 的新增特性。

這兩種類型的最大區別是 TimeZone 只能訪問當前的本地時區,而 TimeZoneInfo 則能夠訪問全世界的時區。而且,TimeZoneInfo 具有更豐富的 (雖然有時不宜使用) 基於規則的夏令時描述模型。

(P193)

靜態的 TimeZone.CurrentTimeZone 方法會基於當前的本地設置返回一個 TimeZone 對象。

TimeZoneInfo 類採用類似的處理方式。TimeZoneInfo.Local 返回當前的本地時區。

靜態的 GetSystemTimeZones 方法則返回全世界所有的時區。

(P197)

格式化表示將對象轉換爲一個字符串;而解析表示將一個字符串轉換爲某種對象。

最簡單的格式化機制是 ToString 方法,它能夠爲所有簡單的值類型產生有意義的輸出。對於反向轉換,這些類型都定義了靜態的 Parse 方法。

如果解析失敗,它會拋出一個 FormatException 。許多類型還定義了 TryParse 方法,如果轉換失敗,它會返回 false ,而不是拋出一個異常。

(P198)

如果遇到錯誤,在異常處理代碼塊中調用 TryParse 是更快速且更好的處理方式。

使用格式提供者的方法是 IFormattable 。所有數字類型和 DateTime(Offset) 都實現了這個接口。

格式字符串提供一些指令;而格式提供者則決定了這些指令是如何轉換的。

大多數類型都重載了 ToString 方法,可以省略 null 提供者。

(P199)

.NET Framework 定義了以下三種格式提供者 (它們都實現了 IFormatProvider) : NumberFormatInfo 、 DateTimeFormatInfo 、 CultureInfo 。

所有 enum 類型都可以格式化,但是它們沒有具體的 IFormatProvider 類。

在格式提供者的上下文中,CultureInfo 作爲其他兩個格式提供者的間接機制,返回一個適合文化區域設置的 NumberFormatInfo 或 DateTimeFormatInfo 。

(P200)

組合格式字符串可以包含組合變量替代符和格式字符串。

Console 類本身重載了它的 Write 和 WriteLine 方法,以接受一個組合格式字符串。

所有格式提供者都實現了 IFormatProvider 接口 。

(P202)

標準格式字符串決定數字類型或 DateTime / DateTimeOffset 集是如何轉換爲字符串的。格式字符串有兩種 :

  1. 標準格式字符串 —— 可以使用標準格式字符串是實現基本的控制。標準格式字符串是由一個字母及其後面一個可選的數字 (它的作用由前面的字母決定) 組成;

  2. 自定義格式字符串 —— 可以使用自定義格式字符串作爲模板對每一個字符進行精細控制;

自定義格式字符串與自定義格式提供者無關。

(P203)

如果不提供數字格式字符串或者使用 null 或空字符串,那麼相當於使用不帶數字的 “G” 標準格式化字符串。

每一種數字類型都定義了一個靜態的 Parse 方法,它接受 NumberStyles 參數。NumberStyles 是一個標記枚舉值,可以判斷如何讀取轉換爲數字類型的字符串。

(P208)

.NET Framework 將以下類型稱爲基本類型 :

  1. bool 、 char 、 string 、 System.DateTime 和 System.DateTimeOffset ;

  2. 所有 C# 數值類型;

靜態 Convert 類定義了將每一個基本類型轉換成其他基本類型的方法。可是,這些方法大多數都是無用的 : 它們或者拋出異常,或者是隱式轉換的冗餘方法。

(P209)

所有基本類型都 (顯式地) 實現了 IConvertible ,它定義了轉換到其他基本類型的方法。在大多數情況中,每一種方法的實現都直接調用 Convert 類中的方法。在少數情況中,編寫一個接受 IConvertible 類型的參數是很有用的。

允許在數字類型之間執行的隱式和顯式轉換,概括爲 :

  1. 隱式轉換隻支持無值丟失的轉換;

  2. 只有會出現值丟失的轉換才需要使用顯式轉換;

轉換是經過效率優化的,,因此它們將截斷不符合要求的數據。

Convert 的數值轉換方法採用圓整的方式。

Convert 採用銀行的圓整方式,將中間值轉換爲偶整數 (這樣可以避免正負偏差) 。

To (整數類型) 方法隱含了一些重載方法,它們可以將數字轉換爲其他進制。第二個參數指定了進制數,它可以是任何一種進制 (二、八、十或十六進制) 。

ChangeType 的缺點是無法指定一個格式字符串或解析標記。

Convert 的 ToBase64String 方法能夠將一個字節數組轉換爲 Base 64 ;FromBase64String 則執行相反操作。

(P211)

大多數基本類型都可以通過調用 BitConverter.GetBytes 轉換爲字節數組。

應用程序的國際化包括兩個方面 : 全球化和本地化。

全球化注重於三個任務 (重要性由大到小) :

  1. 保證程序在其他文化環境中運行時不會出錯;

  2. 採用一種本地文化的格式化規則;

  3. 設計程序,使之能夠從將來可能編寫和部署的附屬程序集讀取與文化相關的數據和字符串;

本地化表示爲特定文化編寫附屬程序集以結束最終任務。

(P213)

Round 方法能夠指定圓整的小數位數以及如何處理中間值 (遠離 0 ,或者使用銀行的圓整方式) 。

Floor 和 Ceiling 會圓整到最接近的整數 : Floor 總是向下圓整,而 Ceiling 則總是向上圓整 —— 即使是負數 。

(P214)

BigInteger 結構體是 .NET Framework 新增的特殊數值類型。它位於 System.Numerics.dll 中新的 System.Numerics 命名空間,可以用於表示一個任意大的整數而不會丟失精度。

C# 並不提供 BigInteger 的原生支持,所以無法表示 BigInteger 值。然而,可以從任意整數類型隱式地轉換到 BigInteger 。

可以將一個 BigInteger 隱式地轉換爲標準數值類型,也可以顯式地進行反向轉換。

BigInteger 重載了所有的算術運算符,以及比較、等式、求模 (%) 和負值運算符。

將一個數字存儲到一個 BigInteger 中而不是字節數組的優點是可以獲得值類型的語義,調用 ToByteArray 可以將一個 BigInteger 轉換回字節數組。

Complex 結構體是 Framework 4.0 新增的另一個特殊數值類型,用來表示用 double 類型的實數和虛數構成的複數。

要使用 Complex ,我們需要實例化這個結構體,指定實數和虛數值。

(P215)

Complex 結構體具有實數和虛數值的屬性,以及階和量級。

還可以通過指定量級和階來創建複數。

複數也重載了標準的算術操作符。

Complex 結構體具有一些支持更高級功能的靜態方法,其中包括 :

  1. 三角函數;

  2. 取對數與求冪;

  3. 共軛;

Random 類能夠生成一個隨機 byte 、 integer 或 double 類型的僞隨機數序列。

要使用 Random ,首先要實例化,可選擇提供一個種子來實例化隨機數序列。使用相同的種子一定會產生相同序列的數字,當希望有可再現性時,是非常有用的。

如果不希望可再現性,那麼可以不使用種子來創建 Random 而是使用當前系統時間來創建。

因爲系統時鐘只有有限的粒度,創建時間間隔很小 (一般是 10ms 內) 的兩個 Random 將會產生相同序列的值。常用的方法是每次需要一個隨機數時才實例化一個新的 Random 對象,而不是重用同一個對象。

調用 Next(n) 可以生成一個 0 至 n-1 之間的隨機整數。NextDouble 可以生成一個 0 至 1 之間的隨機 double 數值。NextBytes 會用隨機數填充一個字節數組。

(P216)

System.Enum 的靜態實用方法主要是與轉換和獲取成員清單相關。

(P217)

每一種整型 (包括 ulong) 都可以轉換爲十進制數而不會丟失值。

Enum.ToObject 能夠將一個整型值轉換爲一個指定類型的 enum 實例。

(P218)

ToObject 已經重載,可以接受所有的整數類型和對象 (後者支持任何裝箱的整數類型) 。

Enum.Parse 可以將一個字符串轉換爲一個 enum 。它接受 enum 類型和一個包含多個成員的字符串。

Enum.GetValues 返回一個包含某特定 enum 類型的所有成員。

Enum.GetNames 執行相同的操作,但是返回的是一個字符串數組。

在內部,CLR 通過反射 enum 類型的字段實現 GetValues 和 GetNames ,其結果會被緩存以提高效率。

枚舉類型的語義很大程序上是由編譯器決定的。在 CLR 中,enum 實例 (未拆箱) 與它實際的整型值在運行時是沒有任何區別的。而且,在 CLR 中定義的 enum 僅僅是 System.Enum 的子類型,它的每個成員都是靜態的整型域。

(P219)

C# 會在調用 enum 實例的虛方法之前對它進行顯式裝箱。而且,當 enum 實例被裝箱後,它會獲得一個引用其 enum 類型的封裝。

Framework 4.0 提供了一組新的泛型類來保存不同類型的元素集,稱爲元組。

每種元組都有名爲 Item1 、 Item2 等的只讀屬性,分別對應一種類型參數。

可以通過它的構造方法實例化一個元組,或者通過靜態幫助方法 Tuple.Create 。後者使用的是泛型推斷方法,可以將這種方法與隱式類型轉換結合使用。

元組可以很方便地用來實現從一個方法返回多個值或者創建值對集合。

元組的替代方法是使用對象數組。然而,這種方法會影響靜態類型安全性,增加了值類型的 裝箱 / 開箱 開銷,並且需要作一些編譯器無法驗證的複雜轉換。

(P220)

元組是一些類 (也就是引用類型) 。

Guid 結構體表示一個全局唯一標識符 : 一個隨機生成的 16 位值,幾乎可以肯定具有唯一性。Guid 在應用程序和數據庫中通常用作各種排序的鍵。

我們可以調用靜態的 Guid.NewGuid 方法創建一個新的隨機 Guid 。

ToByteArray 方法可以將一個 Guid 轉換爲一個字節數組。

靜態的 Guid.Empty 屬性會返回一個空的 Guid (全爲零) ,通常用來替換 null 。

(P221)

相等有兩種類型 :

  1. 值相等 —— 兩個值在某種意義上是相等的;

  2. 引用相等 —— 兩個引用指向完全相同的對象;

默認情況下 :

  1. 值類型採用的是值相等;

  2. 引用類型採用的是引用相等;

事實上,值類型只能使用值相等形式進行比較 (除非已裝箱) 。

引用類型默認是採用引用相等的比較形式。

(P222)

有三種標準方法可以實現等值比較 :

  1. == 和 != 運算符;

  2. 對象的虛方法 Equals ;

  3. IEquatable<T> 接口;

Equals 在 System.Object 中定義,所以所有類型都支持這個方法。

Equals 是在運行時根據對象的實際類型解析的。

對於結構體,Equals 會調用每個字段的 Equals 執行結構比較。

(P223)

Equals 很適合用來比較兩個未知類型的對象。

object 類提供了一個靜態的幫助方法,它的名稱是 Equals ,與虛方法相同,但是不會有衝突,因爲它接受兩個參數。

如果在處理編譯時未知類型對象,這是一種能夠避免 null 值異常的等值比較算法。

(P224)

靜態方法 object.ReferenceEquals 可以實現引用等值比較。

另一種採用引用等值比較的方法是將值轉換爲 object ,然後再使用 == 運算符。

調用 object.Equals 的結果是強制對值類型執行裝箱。這在對性能高度敏感的情況下是不太適合的,因爲裝箱操作相對於實際比較操作的開銷還要高。C# 2.0 引入了一個解決辦法,那就是使用 IEquatable<T> 接口。

關鍵在於實現 IEquatable<T> 所返回的結果與調用 object 的虛方法 Equals 是一樣的,但是執行速度會更快。大多數 .NET 基本類型都實現了 IEquatable<T> 。可以在泛型中使用 IEquatable<T> 作爲一個約束。

(P225)

默認的等值比較操作有 :

  1. 值類型採用的是值相等;

  2. 引用類型採用的是引用相等;

此外 :

結構體的 Equals 方法默認採用的是結構值相等。

有時創建一個類型時重載這個行爲是很有用的,有以下兩種情況我們需要這樣做 :

  1. 修改相等的語義 —— 當 == 和 Equals 默認行爲不符合要求的類型,並且這種行爲一般人難以想象時,修改相等的語義是很有用的。

  2. 提高結構體的等值比較的執行速度 —— 結構體的默認結構等值比較算法相對較慢。通過重載 Equals 來實現這個過程可以將性能提高 20% 。重載 == 運算符和實現 IEquatable<T> 接口可以實現等值比較的拆箱,並且同樣能夠將比較速度提高 20% 。

(P226)

重載引用類型的等值語義並不能提高性能。引用等值比較的默認算法已經非常快速,因爲它只比較兩個 32 位或 64 位引用。

重載等值語義操作步驟總結 :

  1. 重載 GetHashCode() 和 Equals() ;

  2. (可選) 重載 != 和 == ;

  3. (可選) 實現 IEquatable<T> ;

在 System.Object 中定義的 GetHashCode 對於散列表而言非常重要,所以每一種類型都具有一個散列碼。

引用類型和值類型都只有默認的 GetHashCode 實現,這意味着不需要重載這個方法 —— 除非重載了 Equals 。 (反之亦然,如果重載了 GetHashCode ,那麼也必須重載 Equals) 。

下面是重載 object.GetHashCode 的其他規則 :

  1. 它必須爲 Equals 方法都返回 true 的兩個對象返回相同的值,因此, GetHashCode 和 Equals 必須同時重載;

  2. 它不能拋出異常;

  3. 如果重複調用相同對象,必須返回相同的值 (除非對象改變) ;

(P227)

結構體的默認散列方法只是在每個字段上執行按位異或操作,通常會比編寫的算法產生更多的重複碼。

類的默認 GetHashCode 實現基於一個內部對象標識,它在 CLR 當前實現中的每一個實例上都是唯一的。

object.Equals 的執行邏輯如下 :

  1. 對象不能是 null (除非它是可空類型) ;

  2. 相等是自反性的 (對象與其本身相等) ;

  3. 相等是可交換的 (如果 a.Equals(b) ,那麼 b.Equals(a)) ;

  4. 相等時可傳遞的 (如果 a.Equals(b) 且 b.Equals(c) ,那麼 a.Equals(c)) ;

  5. 等值操作是可重複且可靠的 (它們不會拋出異常) ;

除了重載 Equals ,還可以選擇重載相等和不等運算符。這種重載幾乎都發生在結構體上,否則 == 和 != 運算符無法正確判斷類型。

對於類,與兩種方法可以處理 :

  1. 保留 == 和 != ,這樣它們會應用引用相等;

  2. 重載 Equals 同時重載 == 和 != ;

(P228)

爲了保持完整性,在重載 Equals 時,最好也要實現 IEquatable<T> ,其結果應該總是與被重載對象 Equals 方法保持一致,如果自己編寫 Equals 方法實現,那麼實現 IEquatable<T> 並沒有任何的程序開銷。

(P229)

除了標準等值協議,C# 和 .NET 還定義了用於確定對象之間相對順序的協議。基本的協議包括 :

  1. IComparable 接口 (IComparable 和 IComparable<T>) ;

  2. > 和 < 運算符;

IComparable 接口可用於普通的排序算法。

< 和 > 操作符比較特殊,它們大多數情況用於比較數字類型。因爲它們是靜態解析的,所以可以轉換爲高效的字節碼,適用於一些密集型算法。

.NET Framework 也通過 IComparer 接口實現了可插入的排序協議。

(P230)

CompareTo 方法按如下方式執行 :

  1. 如果 a 在 b 之後,那麼 a.CompareTo(b) 返回一個正數;

  2. 如果 a 與 b 位置相同,那麼 a.CompareTo(b) 返回 0 ;

  3. 如果 a 在 b 之前,那麼 a.CompareTo(b) 返回一個負數;

(P231)

在重載 < 和 > 後,同時實現 IComparable 接口,這也是一種標準方法,但是反之不成立。事實上,大多數實現了 IComparable 的 .NET 類型都沒用重載 < 和 > 。與等值的處理方法不同的是,在等值中如果重載了 Equals ,一般也會重載 == 。

字符串不支持 < 和 > 運算符。

【第07章】

(P234)

System.Diagnostics 中的 Process 類可以用於啓動一個新的進程。

Process 類也允許查詢計算機上運行的其他進程,並與之交互。

(P235)

.Net Framework 提供了標準的存儲和管理對象集合的類型集。其中包括可變大小列表、鏈表和排序或不排序字典以及數組。在這些類型中,只有數組屬於 C# 語言;其餘的集合只是一些類,可以像使用其他類一樣進行實例化。

Framework 中的集合類型可以分成以下三類 :

  1. 定義標準集合協議的接口;

  2. 隨時可用的集合類 (列表、字典等) ;

  3. 編寫應用程序特有集合的基類;

集合命名空間有以下幾種 :

System.Collections —— 非泛型集合類和接口;
System.Collections.Specialized —— 強類型非泛型集合類;
System.Collections.Generic —— 泛型集合類和接口;
System.Collections.ObjectModel —— 自定義集合的委託和基類;
System.Collections.Concurrent —— 線程安全的集合;

(P236)

IEnumerator 接口定義了以向前方式遍歷或枚舉集合元素的基本底層協議。

MoveNext 將當前元素或 “遊標” 向前移動到下一個位置,如果集合沒有更多的元素,那麼它會返回 false 。Current 返回當前位置的元素 (通常需要從 object 轉換爲更具體的類型) 。在取出第一個元素之前,我們必須先調用 MoveNext —— 即使是空集合也支持這個操作。如果 Reset 方法實現了,那麼它的作用就是將位置移回到起點,允許再一次遍歷集合。 (通常是不需要調用 Reset 的,因爲並非所有枚舉器都支持這個方法) 。

IEnumerable 可以看作是 “IEnumerator 的提供者” ,它是集合類需要實現的最基礎接口。

(P237)

IEnumerable<T> 實現了 IDisposable 。它允許枚舉器保存資源引用,並保證這些資源在枚舉結束或者中途停止時能夠被釋放。foreach 語句能夠識別這個細節。

(P238)

using 語句保證清理操作的執行。

有時由於下面一個或多個原因而希望實現 IEnumerable 或 IEnumerable<T> :

  1. 爲了支持 foreach 語句;

  2. 爲了與任何使用標準集合的組件交互;

  3. 作爲一個更復雜集合接口實現的一部分;

  4. 爲了支持集合初始化器;

爲了實現 IEnumerable / IEnumerable<T> ,必須提供一個枚舉器。可以採用以下三個方法來實現 :

  1. 如果這個類 “包裝” 了任何一個集合,那麼就返回所包裝集合的枚舉器;

  2. 使用 yield return 的迭代器;

  3. 實例化 IEnumerator / IEnumerator<T> ;

還可以創建一個現有集合類的子類,Collection<T> 正是基於此目的而設計的。

返回另一個集合的枚舉器就是調用內部集合的 GetEnumerator 。然而,這種方法僅僅適合一些最簡單的情況,那就是內部集合的元素正好是所需要的類型。

更好的方法是使用 C# 的 yield return 語句編寫迭代器。

迭代器是 C# 語言的一個特性,它能夠協助完成集合編寫,與 foreach 語句協助完成集合遍歷的方式是一樣的。

迭代器會自動處理 IEnumerable 和 IEnumerator 或者它們的泛型類的實現。

注意, GetEnumerator 實際上不返回一個枚舉器,通過解析 yield return 語句,編譯器編寫一個隱藏的枚舉器類,然後重構 GetEnumerator 來實例化和返回這個類。

迭代器很強大,也很簡單,並且是 LINQ 實現的基礎。

(P240)

因爲 IEnumerable<T> 實現了 IEnumerable ,所以必須同時實現泛型和非泛型的 GetEnumerator 。

最後一種編寫 GetEnumerator 的方法是編寫一個直接實現 IEnumerator 的類。

(P241)

實現 Reset 方法不是必需的,相反,可以拋出一個 NotSupportedException 。

注意,第一次調用 MoveNext 會將位置移到列表的第一個 (而非第二個) 元素。

(P242)

IEnumerable<T> (和 IEnumerable ) —— 支持最少的功能 (只支持枚舉) 。

ICollection<T> (和 ICollection ) —— 支持一般的功能 。

IList<T> / IDictionary<K, V> 及其非泛型版本 —— 支持最多的功能 。

大多數情況下不需要實現這些接口,幾乎在需要編寫一個集合類的任何時候,都可以使用子類 Collection<T> 替代。

泛型和非泛型版本的差別很大,特別是對於 ICollection 。

因爲泛型出現在後,而泛型接口是爲了後面出現的泛型而開發的。

ICollection<T> 並沒有繼承 ICollection ;

IList<T> 也沒有繼承 IList ;

而且 IDictionary<TKey, TValue> 也同樣不繼承 IDictionary 。

當然,在有利的情況下,集合類本身通常是可以實現某個接口的兩個版本的。

.NET Framework 中並沒有一種統一使用集合 (collection) 和 列表 (list) 這兩個詞的方法。我們通常將 集合 (collection) 和 列表 (list) 這兩個術語看作在很多方面是同義的,只有在使用具體類型時例外。

ICollection<T> 是對象的可計數集合的標準接口。它提供了很多功能,包括確定集合大小 (Count) 、確定集合中是否存在某個元素 (Contains) 、將集合複製到一個數組 (ToArray) 以及確定集合是否爲只讀 (IsReadOnly) 。對於可寫集合,可能還需要對集合元素執行 Add 、 Remove 和 Clear 操作。而且,由於它繼承了 IEnumerable<T> ,所以也支持通過 foreach 語句進行遍歷。

(P243)

非泛型的 ICollection 具有與可計數集合類似的功能,但是它不支持修改列表或檢查元素成員的功能。

IList<T> 是標準的可按位置索引的接口,除了從 ICollection<T> 和 IEnumerable<T> 繼承的功能,它還提供了按位置 (通過一個索引器) 讀寫元素和按位置 插入 / 刪除 元素的功能。

IndexOf 方法可以對列表執行線性搜索,如果未找到指定項,那麼返回 -1 。

IList 非泛型版本具有更多的成員方法,因爲它繼承了少量的 ICollection 成員方法。

(P244)

非泛型 IList 接口的 Add 方法返回一個整數,這是最新添加元素的索引。相反,ICollection<T> 的 Add 方法的返回類型爲 void 。

通用的 List<T> 類是 IList<T> 和 IList 的典型表現。C# 數組也同時實現了泛型和非泛型的 IList 。

爲了與只讀的 Windows Runtime 集合實現互操作,Framework 4.5 引入了一個新的集合接口 IReadOnlyList<T> 。這個接口本身很有用,並且可以看作爲 IList<T> 的縮減版本,它只包含列表只讀操作所需要的成員。

因爲它的類型參數只用在輸出位置,所以它被標記爲協變式 (covariant) 。

IReadOnlyList<T> 表示一個鏈表的只讀版本,它並不意味着底層實現也是隻讀的。

IReadOnlyList<T> 與 Windows 運行時類型 IVectorView<T> 相對應。

(P245)

Array 類是所有一維和多維數組的隱式基類,它是實現標準集合接口的最基本類型之一。

Array 類提供了類型統一性,所以常見的方法都適用於所有的數組,而與它們聲明或實際的元素類型無關。

由於數組是基本類型,所以 C# 提供了明確的聲明和初始化語法。

當使用 C# 語法聲明一個數組時,CLR 會在內部將它轉化爲 Array 的子類 —— 合成一個對應數組維數和元素類型的僞類型。

CLR 也會特別處理數組類型的創建,將它們分配到一塊連續的內存空間。因此數組的索引非常高效,但是不允許在創建後修改數組大小。

Array 實現了 IList<T> 的泛型與非泛型的集合接口。

Array 類實例也提供了一個靜態的 Resize 方法,但是它實際上是創建一個新數組,然後將每一個元素複製到新數組中。Resize 方法是很低效的,而且程序的數組引用無法修改爲新位置。

實現可變大小集合的最好方法是使用 List<T> 類。

(P246)

因爲 Array 是一個類,所以無論數組的元素是什麼類型,數組 (本身) 總是引用類型。

兩個不同的數組在等值比較中總是不相等的 —— 除非使用自定義的等值比較。

Framework 4.0 提供了一種用於比較數組或元組元素的比較方式,可以通過 StructuralComparisons 類型進行訪問。

數組可以通過 Clone 方法進行復制。然而,這是一個淺克隆,表示只有數組本身表示的內存會被複制。如果數組包含的是值類型的對象,那麼這些值會被複制類;如果數組包含的是引用類型的對象,那麼只有引用被複制。

如果要進行深度複製即複製引用類型子對象,必須遍歷整個數組,然後手動克隆每個元素。相同的規則也適用於其他 .NET 集合類型。

CLR 不允許任何對象 (包括數組) 在大小上超過 2GB (無論是運行在 32 位或是 64 位環境上) 。

(P247)

你可能會以爲 Array 類的許多方法是實例方法,但是實際上它們是靜態方法。這是一個奇怪的設計方法,意味着在尋找 Array 方法時,應該同時查看靜態方法和實例方法。

最簡單的創建和索引數組的方法是使用 C# 的語言構造。

此外,可以通過調用 Arrray.CreateInstance 動態實例化一個數組,可以在運行時指定元素類型和維數以及爲非零開始索引的數組指定下界。非零開始索引的數組不符合 CLS (Common Language Specification ,公共語言規範) 。

靜態的 GetValue 和 SetValue 方法訪問動態創建的數組的元素 (它們也支持普通數組的元素訪問) 。

動態創建的從零開始索引的數組可以轉換爲一種類型匹配或兼容 (兼容標準數組變化規則) 的 C# 數組。

爲什麼不使用 object[] 作爲統一的數組類型,而要使用 Array 類呢?原因就是 object[] 既不兼容多維數組,也不兼容值類型以及非零開始索引的數組。

GetValue 和 SetValue 也支持編譯器創建的數組,並且它們對於編寫能夠處理任意類型和任意維數數組的方法是很有用的。

(P248)

如果元素與數組類型不一致,SetValue 方法會拋出一個異常。

當實例化數組時,無論是通過語言語法還是 Array.CreateInstance ,數組元素都會自動初始化。對於引用類型元素的數組,這意味着寫入 null 值;對於值類型元素的數組,這意味着調用值類型的默認構造函數 (實際上是成員的 “歸零” 操作)。

數組可以通過 foreach 語句進行枚舉。

也可以使用靜態的 Array.ForEach 方法進行枚舉。

(P249)

GetLength 和 GetLongLength 會返回一個指定維度的長度 (0 表示一維數組),而 Length 和 LongLength 返回數組的元素總數 (包括所有維數) 。

GetLowerBound 和 GetUpperBound 在處理非零開始索引的數組時是很有用的。GetUpperBound 返回的結果與任意維度的 GetLowerBound 和 GetLength 相加的結果是相同的。

(P250)

Array.Sort 要求數組中的元素實現 IComparable ,這意味着 C# 的最基本類型都可以進行排序。

如果元素是不可比較的,或者希望重寫默認的順序比較,那麼必須給 Sort 提供一個自定義的比較提供者,用來判斷兩個元素的相對位置。可以採用以下方法 :

  1. 通過一個實現 IComparer / IComparer<T> 的幫助對象;

  2. 通過一個 Comparison 委託 : public delegate int Comparison<T> (T x, T y) ;

Comparison 委託採用與 IComparer<T>.CompareTo 相同的語義。

(P251)

作爲 Sort 的替代方法,可以使用 LINQ 的 OrderBy 和 ThenBy 運算符。與 Array.Sort 不同的是,LINQ 運算符不會修改原始數組,而是將排序結果保存在一個新的 IEnumerable<T> 序列中。

Array 有 4 個方法可以執行淺拷貝操作 : Clone 、 CopyTo 、 Copy 和 ConstrainedCopy 。前兩個方法都是實例方法;後兩個方法是靜態方法。

Clone —— 方法返回一個全新 (淺拷貝) 的數組;

CopyTo 和 Copy —— 方法複製數組的若干連續元素;

ConstrainedCopy —— 執行一個原子操作 : 如果所有請求的元素都無法成功複製,那麼操作會回滾;

Array 還有一個 AsReadOnly 方法,它會返回一個包裝器,可以防止元素被重新賦值。

(P252)

System.Linq 命名空間包含另外一些適合用於執行數組轉換的擴展方法。這些方法會返回一個 IEnumerable<T> ,它可以通過 Enumerable 的 ToArray 方法轉換回一個數組。

在靈活性和性能方面,泛型類更具優勢,而它們的非泛型冗餘實現則是爲了實現向後兼容。

泛型 List<T> 和非泛型 ArrayList 類提供了一種動態調整大小的對象數組實現,它們是集合類中使用最廣泛的類。 ArrayList 實現了 IList ,而 List<T> 同時實現了 IList 和 IList<T> 。與數組不同,所有接口都是公開實現的。

在內部,List<T> 和 ArrayList 都維護了一個對象數組,並在超出容量時替換爲一個更大的數組。添加元素是很高效的 (因爲數組末尾通常還有空閒存儲位置) ,但是插入元素的速度會慢一些 (因爲插入位置之後的所有元素都必須向後移動才能留出插入空間) 。與數組一樣,如果對已排序列表執行 BinarySearch 方法,那麼查找是很高效的,但是其他情況效率就不高,因爲查找時必須檢查每一個元素。

如果 T 是一種值類型,那麼 List<T> 的速度會比 ArrayList 快好幾倍,因爲 List<T> 不需要元素執行裝箱和開箱操作。

List<T> 和 ArrayList 具有可以接受已有元素集合的構造函數,它們會將已有集合的每一個元素複製到新的 List<T> 或 ArrayList 中。

(P254)

非泛型 ArrayList 類主要用於向後兼容 Framework 1.x 代碼。

ArrayList 的功能與 List<object> 類型相似。當需要一個包含不共享任何相同基類的混合類型元素時,這兩種類型是很有用的。在這種情況下,如果需要使用反射機制處理列表,那麼選擇使用 ArrayList 更具優勢。相比於 List<object> ,反射機制更容易處理非泛型的 ArrayList 。

如果定義 System.Linq 命名空間,那麼可以通過先調用 Cast 再調用 ToList 的方式將一個 ArrayList 轉換爲一個泛型 List 。

Cast 和 ToList 是 System.Linq.Enumerable 的擴展方法,是從 .NET Framework 3.5 開始支持的。

LinkedList<T> 是一個泛型的雙向鏈表。雙向鏈表是一系列互相引用的節點,其中每個節點都引用前一個節點、後一個節點及實際存儲數據的元素。它的主要優點是元素總是能夠高效地插入到鏈表的任意位置,因爲插入節點只需要創建一個新節點,然後修改引用值。然而,查找插入節點的位置可能減慢執行速度,因爲鏈表本身沒有直接索引的內在機制;我們必須遍歷每一個節點,並且無法執行二叉查找。

(P255)

LinkedList<T> 實現了 IEnumerable<T> 和 ICollection<T> 及其非泛型版本,但是沒有實現 IList<T> ,因爲它不支持根據索引進行訪問。

(P256)

Queue<T> 和 Queue 是一種先進先出 (FIFO) 的數據結構,它們提供了 Enqueue (將一個元素添加到隊列末尾) 和 Dequeue (取出並刪除隊列的第一個元素) 方法。它們還包括一個只返回而不刪除隊列第一個元素的 Peek 方法,以及一個 Count 屬性 (可用來檢查出列前的元素個數) 。

雖然隊列是可枚舉的,但是它們都沒有實現 IList<T> / IList ,因爲不能夠直接通過索引訪問它的成員。

隊列內部是使用一個可根據需要調整大小的數組來操作的,這與一般的 List 類很類似。隊列具有一個直接指向頭和尾元素的索引,因此,入列和出列操作是及其快速的 (除非內部的大小需要調整) 。

(P257)

Stack<T> 和 Stack 是後進先出 (LIFO) 的數據結構,它們提供了 Push (添加一個元素到堆棧的頂部) 和 Pop (從堆棧頂部取出並刪除一個元素) 方法。它們還提供了一個只讀而不刪除元素的 Peek 方法,以及 Count 屬性和用於導出數據以實現隨機訪問的 ToArray 方法。

堆棧內部也是使用一個可根據需要調整大小的數組來操作,這一點和 Queue<T> 與 List<T> 類似。

BitArray 是一個保存壓縮 bool 值的可動態調整大小的集合。它具有比簡單的 bool 數組和 bool 泛型 List 更高的內存使用效率,因爲它的每個值只佔用一位,而 bool 類型的每個值佔用一個字節。

(P258)

HashSet<T> 和 SortedSet<T> 分別是 Framework 3.5 和 4.0 新增加的泛型集合。這兩個類都具有以下特點 :

  1. 它們的 Contains 方法都使用基於散列的查找而實現快速執行;

  2. 它們都不保存重複元素,並且都忽略添加重複值的請求;

  3. 無法根據位置訪問元素;

SortedSet<T> 按一定順序保存元素,而 HashSet<T> 則不是。

這些類型的共同點是由接口 ISet<T> 提供的。

HashSet<T> 是通過使用只存儲鍵的散列表實現的;而 SortedSet<T> 則是通過一個 紅 / 黑 樹實現的。

兩個集合都實現了 ICollection<T> 接口。

因爲 HashSet<T> 和 SortedSet<T> 實現了 IEnumerable<T> 接口,所以可以將另一種集合作爲任意集合操作方法的參數。

SortedSet<T> 的構造函數還接受一個可選的 IComparer<T> 參數 (而非一個等值比較器) 。

(P259)

字典是一種所包含元素均爲 鍵 / 值 對的集合。字典通常都用來執行列表查找和排序。

Framework 通過接口 IDictionary 和 IDictionary<TKey, TValue> 及一組通用的字典類定義了一個標準字典協議。這些類在以下方面有區別 :

  1. 元素是否按有序序列存儲;

  2. 元素是否按位置 (索引) 或按鍵訪問;

  3. 類是泛型還是非泛型的;

  4. 集合變大時的性能;

(P260)

IDictionary<TKey, TValue> 定義了所有基於 鍵 / 值 的集合的標準協議。它擴展了 ICollection<T> ,增加了一些基於任意類型的鍵訪問元素的方法和屬性。

(P261)

從 Framework 4.5 開始,還出現了一個接口 IReadOnlyDictionary<TKey, TValue> ,它定義了字典成員的只讀子集。它與 Windows Runtime 類型 IMapView<K, V> 相對應,當時也是因爲相同原因而引入的。

重複的鍵在所有字典實現中都是禁止的,所以用相同的鍵調用兩次 Add 會拋出一個異常。

直接通過一個 IDictionary<TKey, TValue> 進行枚舉會返回一個 KeyValuePair 結構體序列。

非泛型的 IDictionary 接口在原理上與 IDictionary<TKey, TValue> 相同,但是存在以下兩個重要的功能區別 :

  1. 通過索引器查找一個不存在的鍵會返回 null (而不是拋出一個異常) ;

  2. 使用 Contains 而非 ContainsKey 來檢測成員是否存在 ;

枚舉一個非泛型 IDictionary 會返回一個 DictionaryEntry 結構體序列。

泛型 Dictionary (和 List<T> 集合一樣) 是使用最廣泛的集合之一。它使用一個散列表結構來存儲鍵和值,而且快速、高效。

Dictionary<TKey, TValue> 的非泛型版本是 Hashtable ;Framework 中不存在名爲 Dictionary 的非泛型類。當我們提到 Dictionary 時,指的是泛型的 Dictionary<TKey, TValue> 類。

Dictionary 同時實現了泛型和非泛型的 IDictionary 接口,而泛型 IDictionary 是公開的接口。

事實上, Dictionary 是泛型 IDictionary 的一個標準實現。

(P262)

Dictionary 和 Hashtable 的缺點是元素是無序的。而且,添加元素時不保存原始順序。此外,所有字典類型都不允許出現重複值。

(P263)

OrderedDictionary 是一種非泛型字典,它能夠保存添加元素的原始順序。通過使用 OrderedDictionary ,既可以根據索引訪問元素,也可以根據鍵進行訪問。

OrderedDictionary 並不是一個有序的字典。

OrderedDictionary 是 Hashtable 和 ArrayList 的組合。

這個類是在 .NET 2.0 中引入的,特殊的是,它沒有泛型版本。

ListDictionary 和 HybridDictionary 這兩個類都只有非泛型版本。

Framework 只支持兩種在內部結構中將內容根據鍵進行排序的字典 :

  1. SortedDictionary<TKey, TValue> ;

  2. SortedList <TKey, TValue> (SortedList 是具有相同功能的非泛型版本) ;

(P265)

Collection<T> 類是一個可定製的 List<T> 包裝類。

(P267)

CollectionBase 是 Framework 1.0 引入的 Collection<T> 的非泛型版本。它提供了大多數與 Collection<T> 相似的特性,但是使用方式不太靈活。

KeyedCollection<TKey, TItem> 是 Collection<Item> 的子類。它增加也刪去了一些功能。它增加的功能是按鍵訪問元素,這與字典很相似,刪去的功能是委託自己的內部列表。

KeyedCollection<TKey, TItem> 通常看作是實現了按鍵進行快速查找的 Collection<TItem> 。

(P269)

KeyedCollection 的非泛型版本稱爲 DictionaryBase 。

DictionaryBase 存在的目的就是爲了向後兼容。

ReadOnlyCollection<T> 是一個包裝器,或者稱爲委託,它提供了集合的一種只讀視圖。它的用途是允許一個類公開地顯示集合的只讀訪問,但是同時這個類仍然可以在內部進行修改。

【第08章】

(P277)

LINQ 是 Language Integrated Query 的簡寫,它可以被視爲一組語言和框架特性的集合,我們可以使用 LINQ 對本地對象和遠程數據源進行結構化的類型安全的查詢操作。

在 C# 3.0 和 Framework 3.5 中引入了 LINQ 。

LINQ 可用於查詢任何實現了 IEnumerable<T> 接口的集合類型。

LINQ 具有編譯時的類型檢查及動態查詢組合這兩大優點。

LINQ 中所有核心類型都包含在 System.Linq 和 System.Linq.Expressions 這兩個命名空間中。

LINQ 數據源的基本組成部分是序列和元素。在這裏,序列是指任何實現了 IEnumerable<T> 接口的對象,其中的每一項則稱爲一個元素。

查詢運算符是 LINQ 中用於轉換序列的方法。通常,查詢運算符可接收一個輸入序列,並將其轉換爲一個輸出序列。在 System.Linq 命名空間的 Enumerable 類中定義了約 40 種查詢運算符,這些運算符都是以靜態擴展方法的形式來實現的,稱爲標準查詢運算符。

我們把對本地序列進行的查詢操作稱爲本地查詢或者是 LINQ 到對象查詢。

LINQ 還支持對那些從遠程數據源中動態獲取的序列進行查詢,這些序列需要實現 IQueryable<T> 接口,而在 Queryable 類中則有一組相應的標準查詢運算符對其進行支持。

(P278)

一個查詢可以理解爲一個使用查詢運算符對所操作的序列進行轉換的表達式。

由於標準查詢運算符都是以靜態擴展方法的方式來實現的,因此我們可以像使用對象的實例方法那樣直接使用。

大多數查詢運算符都接受一個 Lambda 表達式作爲參數。

Lambda 表達式用於對查詢進行格式化。

(P279)

運算符流語法和查詢表達式語法是兩種互補的 LINQ 表達方法。

運算符流是最基本同時也是最靈活的書寫 LINQ 表達式的方式。

如果想創建更復雜的查詢表達式,只需在前面的表達式後面添加新的查詢運算符。

(P280)

查詢運算符絕不會修改輸入序列,相反,它會返回一個新序列。這種設計是符合函數式編程規範的, LINQ 的思想實際上就起源於函數式編程。

(P281)

每個查詢運算符對應着一個擴展方法。

(P282)

返回一個 bool 值的表達式我們稱之爲 “斷言” 。

查詢運算符的 Lambda 表達式針對的是集合中的每個元素,而不是集合整體。

標準的查詢運算符使用了一個泛型 Func 委託。 Func 是 System.Linq 命名空間中一組通用的泛型委託,它的作用是保證 Func 中的參數順序和 Lambda 表達式中的參數順序一致。

(P283)

標準的查詢運算符使用下面這些泛型 :

  1. TSource —— 輸入集合的元素類型;

  2. TResult —— 輸出集合的元素類型 (不同於 TSource) ;

  3. TKey —— 在排序、分組或者連接操作中所用的鍵 ;

這裏的 TSource 由輸入集合的元素類型決定。而 TResult 和 TKey 則由我們給出的 Lambda 表達式指定。

Lambda 表達式可以指定輸出序列的類型,也就是說 Select 運算符可以根據 Lambda 表達式中的定義將輸入類型轉化成輸出類型。

Where 查詢運算符的內部操作比 Select 查詢運算符要簡單一些,因爲它只篩選集合,不對集合中的元素進行類型轉換,因此不需要進行類型推斷。

Func<TSource, TKey> 將每個輸入元素關聯到一個排序鍵 TKey ,TKey 的類型也是由 Lambda 表達式中推測出來的,但它的類與同輸入類型、輸出類型是沒有關係的,三者是獨立的,類型可以相同也可以不同。

(P284)

實際上我們可以使用傳統的方式直接調用 Enumerable 中的各種方法來實現查詢運算符的功能,此時在查詢過程可以不使用 Lambda 表達式。這種直接調用的方式在對本地集合進行查詢時非常好用,尤其是在 LINQ to XML 這種操作中應用最爲方便。

傳統調用方式並不適合對 IQueryable<T> 類型集合的查詢,最典型的就是對數據庫的查詢,因爲在對 IQueryable<T> 類型數據進行查詢時,Queryable 類中的運算符需要 Lambda 表達式來生成完整的查詢表達式樹,沒有 Lambda 表達式,這個表達式樹將不能生成。

LINQ 中集成了對集合的排序功能,這種內置的排序對整個 LINQ 體系來說有重要意義。因爲一些查詢操作直接依賴於這種排序。

Take 運算符 —— 會輸出集合中前 x 個元素,這個 x 以參數的形式指定;

Skip 運算符 —— 會跳過集合中的前 x 個元素,輸出其餘元素;

Reverse 運算符 —— 則會將集合中的所有元素反轉,也就是按照元素當前順序的逆序排列;

Where 和 Select 這兩個查詢運算符在執行時,會將集合中元素按照原有的順序進行輸出。實際上,在 LINQ 中,除非有必要,否則各個查詢運算符都不會改變集合中元素的排序方式。

(P285)

Union 運算符會將結果集合中相同的元素去掉;

(P286)

查詢表達式一般以 from 子句開始,最後以 select 或者 group 子句結束。

(P287)

查詢表達式中的所有邏輯都可以用運算符流語法來書寫。

緊跟在 from 關鍵字之後的標識符實際上是一個範圍變量,範圍變量指向當前序列中將要進行操作的元素。

在每個子查詢的 Lambda 表達式中,範圍變量都會被重新定義。

要定義存儲中間結果的變量,需要使用下面幾個子句 : let 、 into 、一個新的 from 子句、 join 。

(P288)

查詢表達式語法和運算符流語法各有優勢。

在包含以下運算符的查詢操作中,使用查詢表達式語法更加方便 :

  1. 在查詢中使用 let 子句導入新的查詢變量;

  2. 在查詢中用到 SelectMany 、 Join 或者 GroupJoin 這些運算符;

對於只包含 Where 、 OrderBy 或者 Select 的查詢語句,這兩種查詢方式都可以。

一般來說,查詢表達式語法由單個的運算符組成,結構比較清晰;而運算符流語法寫出的代碼相對簡潔。

在不含以下運算符的查詢中,選用運算符流語法進行查詢會更加方便 : Where 、 Select 、 SelectMany 、 OrderBy 、 ThenBy 、 OrderByDescending 、 ThenByDescending 、 GroupBy 、 Join 、 GroupJoin 。

如果一個查詢運算符沒有適合的查詢語法,可以混合使用兩種查詢方式來得到最終結果,這樣做的唯一限制是,在整個查詢中,每個查詢表達式的表達必須是完整的 (必須由 from 子句開始,由 select 或者 group 子句結束) 。

(P289)

在比較複雜的查詢中,混合使用兩種查詢語法進行查詢的方式非常高效。

有時候,即使混合使用了兩種查詢語法,也沒有寫出真正簡練的 LINQ 查詢,但注意不要因此養成只使用一種查詢語法的習慣。如果習慣只使用一種語法形式的,在遇到複雜查詢情況時,很難找到一種真正高效的方式去解決問題。

在 LINQ 中,另一個很重要的特性是延遲執行,也可以說是延遲加載,它是指查詢操作並不是在查詢運算符定義的時候執行,而是在真正使用集合中的數據時才執行。

絕大部分標準的 LINQ 查詢運算符都具有延遲加載這種特性,當然也有例外,以下是幾個例外的運算符 :

  1. 那些返回單個元素或者返回一個數值的運算符;

  2. 轉換運算符 : ToArray 、 ToList 、 ToDictionary 、 ToLookup ;

以上這些運算符都會觸發 LINQ 語句立即執行,因爲它們的返回值類型不支持延遲加載。

(P290)

在 LINQ 中,延遲加載特性有很重要的意義,這種設計將查詢的創建和查詢的執行進行了解耦,這使得我們可以將查詢分成多個步驟來創建,有利於查詢表達式的書寫,而且在執行的時候按照一個完整的結構去查詢,減少了對集合的查詢次數,這種特性在對數據庫的查詢中尤爲重要。

子查詢中的表達式有額外的延遲加載限制。無論是聚合運算符還是轉換運算符,如果出現在子查詢中,它們都會被強制地進行延遲加載。

(P292)

LINQ 查詢運算符之所以有延遲加載功能,是因爲每個運算符的返回值不是一個一般的數組或者集合,而是一個經過封裝的序列,這種序列通常情況下並不直接存儲數據元素,它封裝並使用運行時傳遞給它的集合,元素也由其他集合來存儲它實際上只是維護自己與數據集合的一種依賴關係,當有查詢請求時,再到它依賴的序列中進行真正的查詢。

查詢運算符實際上是封裝一系列的轉換函數,這種轉換函數可以將與之關聯的數據集轉換爲各種形式的序列。如果輸出集合不需要轉換的話,那麼就不用執行查詢運算符封裝的轉換操作,這個時候查詢運算符實際上就是一個委託,進行數據轉發而已。

(P293)

如果使用運算符流語法對集合進行查詢,會創建多個層次的封裝集合。

在使用 LINQ 語句的返回集合時,實際是在原始的輸入集合中進行查詢,只不過在進入原始集合之前,會經過上面這些封裝類的處理,在不同層次的封裝類中,系統都會對查詢做相應的修改,這使得 LINQ 語句使用的各種查詢條件會被反映到最終的查詢結果中。

(P294)

如果在 LINQ 查詢語句的最後加上 ToList 方法,會強制 LINQ 語句立刻執行,查詢結果會被保存到一個 List 類型的集合中。

LINQ 的延遲加載特性有這樣一種功能 : 不論查詢語句是連續書寫的還是分多個步驟完成的,在執行之前,都會被組合成一個完整的對象模型,而且兩種書寫方式所產生的對象模型是一樣的。

LINQ 查詢是一個低效率的流水線。

(P295)

LINQ 使用的是需求驅動的模型,先請求再有數據。

在 LINQ 中,所謂子查詢就是包含在另一個查詢的 Lambda 表達式中的查詢語句。

一個子查詢實際上就是一個獨立的 C# 表達式,可以是 LINQ 表達式,也可以是普通的邏輯判斷,所以只要是符合 C# 語法規則的內容,都可以放在 Lambda 表達式的右側作爲子查詢來使用。也就是說,子查詢的使用規則是由 Lambda 表達式的規則所決定的。

“子查詢” 這個詞,在通常意義下,概念非常寬泛,我們只關注 LINQ 下的子查詢。在運算符流語法中,子查詢是指包含在 Lambda 表達式中的查詢語句。在查詢表達式中,只要包含在其他查詢語句中的查詢,都是子查詢,但是 from 子句除外。

子查詢一般有兩個作用 : 一個是爲父查詢確定查詢範圍,一般是一個較小的查詢範圍,另一個作用是爲外層查詢的 Lambda 表達式提供參數。

(P296)

子查詢在什麼時候執行完全是由外部查詢決定的,當外部查詢開始執行時,子查詢也同時執行,它們是同步的,在整個查詢中,子查詢的執行結果被作爲父查詢的某個組成部分。我們可以認爲查詢的開始命令是從外向內傳遞的,對本地集合的查詢嚴格按照這種由外向內的順序進行;但對數據庫的查詢,則沒有那麼嚴格,只是原則上按照這種方式進行。

另一種理解方式是,子查詢會在需要返回查詢結果時執行,那什麼時候需要子查詢返回查詢結果決定於外部查詢什麼時候被執行。

(P297)

在執行本地查詢時,單獨書寫子查詢是一種常用的查詢方式。但是當子查詢中的數據和外部查詢有緊密關聯的時候,即內部數據需要用到外部數據的值時,這種方式不適合,最好寫成一個表達式。

(P298)

在子查詢中使用單個元素或者聚合函數的時候,整個 LINQ 查詢語句並不會被強制執行,外部查詢還是以延遲加載的方式執行。這是因爲子查詢是被間接執行的,在本地集合查詢中,它通過委託的驅動來執行;而在遠程數據源的查詢中,它通過表達式樹的方式執行。

如果 Select 語句中已經包含了子查詢,在這種情況下如果是本地查詢,那麼相當於將源序列重新封裝到一個新的序列中,集合中的每個元素都是以延遲加載的方式執行的。

書寫複雜的 LINQ 查詢表達式的三種方式 :

  1. 遞增式的書寫方式;

  2. 使用 into 關鍵字;

  3. 包裝查詢語句;

實際上無論用何種書寫方式,在運行時,LINQ 查詢表達式都會被編譯成相同的查詢語句來運行。

在使用多個查詢條件進行查詢的時候,這種遞增式的書寫方式比較實用。

(P299)

根據上下文的不同, into 關鍵字在查詢表達式中有兩種完全不同的功能。這裏首先介紹如何使用 into 關鍵字延長查詢 (另一種是和 GroupJoin 配合使用) 。

在 LINQ 查詢中,一般會用到集合的映射,也就是在 Select 方法中將查詢結果直接組裝成新的集合,這種映射一般在查詢的最後執行。但是如果在映射之後還想對新集合執行查詢的話,就可以使用 into 關鍵字來完成。

(P300)

注意,into 關鍵字只能出現在 select 和 group 關鍵字之後,into 會重新創建一個新的查詢,在新的查詢中,我們可以再次使用 where 、 orderby 、 select 關鍵字。

into 關鍵字的作用就是在原來的查詢中重新創建一次新的查詢,在執行前,這種帶 into 的查詢表達式會被編譯成運算符流的查詢語句,因此使用 into 運算符並不會帶來性能上的損失。

包含了多個層次的查詢表達式,在語義和執行上都和遞增式的 LINQ 查詢語句相同,它們本質上沒有區別,唯一的區別就是查詢關鍵字的使用順序。

在多層次查詢中,內部查詢是在傳遞帶之前執行的。而子查詢則是傳送帶上的一部分,它會隨着整個傳送帶的運行而執行。

(P302)

所謂匿名類型指的是沒有顯式定義過的類型,在查詢過程中,可以使用這種類型來封裝查詢結果。實際上這個類並不是沒有定義,只是不用我們自己定義,編譯器會自動定義這個類型。

要在 C# 代碼中定義一種編譯時才能確定的類型,唯一的選擇是使用 var 關鍵字,此時 var 關鍵字就不僅僅是爲了便於書寫,而是不得不這麼寫,因爲我們不知道匿名類型的名字。

(P303)

使用 let 關鍵字,可以在查詢中定義一個新的臨時變量來存放某些步驟的查詢結果。

編譯器在編譯 let 關鍵字的時候,會把它翻譯成一個匿名類型,這個匿名類型中包含了之前的範圍變量 n 和一個新的表達式變量。也就是說,編譯器將 n 翻譯成了前面的匿名類型查詢。

let 還有以下兩個優點 :

  1. 保留了前面查詢中的範圍變量;

  2. 在一個查詢中可以重複使用它定義的變量;

在 LINQ 查詢中,在 where 關鍵字之前或之後可以使用任意多個 let 關鍵字。後面的 let 關鍵字會使用前面 let 關鍵字的返回類型,顯然,let 關鍵字會在每一次使用時重新組成結果集。

let 關鍵字一般不用來返回數值類型的結果,更多使用在子查詢中。

LINQ 包含兩種查詢 : 對本地集合的本地查詢以及對遠程數據的解釋型查詢。

對本地集合的查詢,這種查詢調用 IEnumerable<> 接口中定義的 Enumerable 方法實現了接口中所有的方法來完成具體的查詢。

在解釋型的查詢中,所有的查詢操作都是通過 IQueryable<T> 接口中的方法完成的,具體的方法實現是在 Queryable 類中。在這種查詢中,LINQ 語句不會被編譯成 .NET Framework 中間語言 (IL),而會在運行時被解釋成查詢表達式樹來執行。

(P304)

實際上,可以使用 Enumerable 中的方法來查詢 IQueryable<T> 類型的數據源,但會遇到一個問題,那就是查詢的時候,遠端的數據源必須被加載到本地內存中,然後以本地數據源的方式進行處理。可以想象,這種查詢的效率非常低,每次都需要讀取大量的數據,在本地進行篩選。這正是創建解釋型查詢的原因。

在 .NET Framework 中有兩個類都實現了 IQueryable<T> 接口,這兩個類用於實現兩種不同的查詢 :

  1. LINQ to SQL;

  2. Entity Framework (EF);

這兩種 LINQ-to-db 的查詢技術實際上非常相似。

在對本地數據源的查詢中,也可以使用 IQueryable<T> 接口中的方法進行查詢,只要在本地集合的最後使用一個 AsQueryable 方法即可。

IQueryable<T> 實際上是對 IEnumerable<T> 方法的擴展。

(P306)

查詢表達式樹是 System.Linq.Expression 命名空間下的一種對象模型,這種對象是在運行時被解釋運行的 (這也是爲什麼 LINQ to SQL 和 EF 支持延遲加載)。

解釋型的查詢和本地數據查詢的本質不同在於它們的執行方式。在遍歷解釋型的集合時,整個 LINQ 查詢語句會被編譯成一個完整的查詢表達式樹來加以執行。

(P307)

Entity Framework 也需要類似的標籤,但是除了這些之外,他還需要一個額外的 XML 文件 Entity Data Model (EDM),在這個文件中定義了數據表和實體類的對應關係。

LINQ to SQL 和 EF 中可能定義了 30 種查詢方式,但是在 SQL Server 的 SQL 查詢中只有 10 種查詢方式,而最終 LINQ 查詢表達式要被翻譯成 SQL 來執行,那麼只能在 10 種查詢方法中選一種來使用。如果在 LINQ 使用了一個功能很強大的運算符,但是在 SQL 中卻沒有相同功能的運算符,那麼 LINQ 中的這個運算符就會被翻譯成其他的 SQL 語句來完成這項功能。

一個 LINQ 查詢中可以同時使用解釋型查詢運算符和本地查詢運算符。應用的典型方式就是把本地查詢操作放在外層,將解釋型的查詢操作放在內層,在執行查詢的時候,解釋型的操作先執行,返回一個結果集合給外層的本地查詢使用。這種查詢模式經常用於 LINQ 對數據庫的查詢操作。

查詢運算符絕不會修改輸入序列,相反,它會返回一個新序列。這種設計是符合函數式編程規範的,LINQ 的思想實際上就起源於函數式編程。

(P309)

兩種方式可以間接地調用 AsEnumerable 方法,那就是 ToArray 方法和 ToList 方法。使用 AsEnumerable 方法有下面兩點好處,一是這個方法不會強制查詢立即執行,但是如果希望查詢立即執行的話,就要使用另外兩個方法了;二是它不會創建本地的存儲結構,因此它會比較節省資源。

當查詢邏輯從數據庫移到本地會降低查詢的性能,特別是當查詢的數據量比較大的時候,效率損失更加嚴重。同樣針對上面這個示例,有一個更有效 (同時也更復雜) 的方式來完成上面的查詢,那就是使用 SQL CLR 在數據庫端實現正則表達式的查詢。

(P310)

LINQ to SQL 和 EF 都是用 LINQ 來實現的對象的映射工具,它們之間的不同在於映射的方式,我們知道,在數據庫查詢中,映射的一端是數據庫表,LINQ to SQL 可以將數據庫表結構映射成對象,然後供調用者使用,這種映射嚴格按照數據庫表結構,映射成的對象不需要我們定義。與之不同的是,EF 對這種映射做了一些改進,那就是允許我們定義實體類,也就是允許開發者定義數據庫表被映射成什麼類型。這種映射提供了一種更靈活的解決方案,但是它會降低查詢性能,也增加了使用的複雜度,因爲需要佔用額外的時間去維護數據庫和自定義的實體類間的映射關係。

L2S是由微軟的 C# 團隊完成的,在 Framework 3.5 中發佈,而 EF 是由 ADO.NET 團隊在 ADO.NET SP1 中發佈的。後來 L2S 的開發和維護由 ADO.NET 團隊來接管,由於開發重心的不同,在 .NET Framework 4.0 中對 L2S 的改變很少,而主要的改進集中在 EF 方面。

儘管在性能上和易用性上,EF 在 .NET Framework 4.0 中已經有了極大的改進,但是兩種技術還是各有優勢。L2S 的優點是簡單易用、執行性能好,此外它生成的 SQL 語句的解釋質量更好一些。EF 的優點是允許我們創建自定義的持久化的實體類,用於數據庫的映射。另外 EF 允許使用同一個查詢機制查詢 SQL Server 之外的數據源,實際上 L2S 也支持這個功能,但是爲了鼓勵第三方的查詢機制的出現,L2S 中沒有對外公佈這些機制。

EF 4.0 突出的改進是它支持幾乎所有的 L2S 中的查詢方法。

L2S 允許任何類來承載數據,只要類中加入了合適的標籤即可。

[Table] 標籤定義在 System.Data.Linq.Mapping 命名空間中,它定義的類型用來承載數據表中的一行數據。默認情況下,L2S 會認爲這個類名和它對應的表名是相同的,如果想讓兩者不同的話,由於表名已經固定,只能更改對應的類名,更改方式是在 [Table] 標籤中顯式地指定類名。

在 L2S 中,如果一個類具有 [Table] 標籤,就稱這個類爲實體,爲了能夠順利使用,這個實體的結構必須與數據表的結構相匹配,多字段或少字段都不行。這種限制使得這種映射是一種低級別的映射。

(P311)

[Column] 標籤用來指示數據表中的某列,如果實體中定義的列名和數據表中的別名不同,那麼需要在 [Column] 標籤中特別指出所對應的列名。

[Column] 標籤中的 IsPrimaryKey 屬性用於指示當前列是主鍵,在數據中這列用於唯一標識一條數據,在程序中也用這列區分不同的實體,將實體中的變換更新到數據庫的時候,也需要使用這一列來確定寫入的目標。

總的來講,在定義實體類的時候,L2S 允許將數據庫的字段映射對象 (實體中的屬性) 定義成私有的,它可以訪問到實體類中的私有變量。

實際上與數據庫表對應的實體類是可以自動生成的,不用逐行書寫,常用的生成工具有 Visual Studio (需要在 “工程” 菜單添加一個 “LINQ to SQL Classes” 選項)和命令行工具 SqlMetal 。

和 L2S 中的實體類相似,EF中允許開發者定義自己的實體類用於承載數據,不同的是,EF 中的實體類的定義要靈活得多,在理論上允許任何類型的類來作爲實體類使用 (在某些特殊情況下需要實現一些接口) ,也就是說實體類中的結構不用和數據表中的字段完全對應。

和 L2S 不同的是,在 EF 中,要完成數據的映射和查詢,之定義上面這個實體類是不夠的。因爲在 EF 中,查詢並不是直接針對數據庫進行的,它使用了一種更高級別的抽象模型,稱爲實體數據模型 (EDM , Entity Data Model) ,我們的查詢語句是針對這個模型來定義的。

EDM 實際上是使用 XML 定義的一個 .edmx 類型的文件,這個文件包含三部分內容 :

  1. 概念模型 : 定義了數據庫的信息,不同的數據庫有不同的概念模型內容;

  2. 存儲模型 : 定義了數據庫的表結構;

  3. 映射 : 定義了數據庫表和實體類之間的映射關係;

(P312)

創建 .edmx 文件最簡單的方式是使用 Visual Studio ,在 “項目” 菜單中點擊 “添加新項” ,在彈出的窗口中選擇 “ADO.NET Entity Data Model” 。之後使用嚮導就可以完成實體類到數據庫表的映射配置。這一系列操作不僅添加一個 .edmx 文件,還會創建涉及到的實體類。

在 EF 中實體類都是映射到概念模型上,所有對概念模型的查詢和更新操作,都是由 Object Services 發起的。

EF 的設計者在設計的時候將映射關係想得比較簡單,他們假設數據表和實體類之間的映射關係是 1 : 1 的,所以並沒有提供專門的機制去完成一對多或者多對一的映射。儘管這樣,如果確實需要這種特殊的映射關係,還是可以通過修改 .edmx 文件中的相關內容來實現。下面是幾個常用的修改操作 :

  1. 多個表映射到一個實體類;

  2. 一個表映射到多個實體類;

  3. 按照 ORM 世界中的三種繼承方式將繼承的類映射到表;

三種繼承策略是 :

  1. 每個分層結構一張表 : 一張表映射到整個類分層結構。該表中包含分隔符列,用於指出每個行應該映射到哪個類;

  2. 每個類一張表 : 一張表映射到一個類,意味着繼承的類映射到多張表。查詢某個實體時,EF 生成 SQL JOIN ,以合併其所有基類;

  3. 每個具體類一張表 : 一張單獨的表映射到每個具體的類。這意味着基類映射到多張表,並且在查詢基類的實體時, EF 生成 SQL UNION ;

比較一下,L2S 僅支持每個分層結構一張表。

EF 還支持 LINQ 之外的查詢方式,有一種語言叫 Entity SQL (ESQL),使用這種語言,我們可以通過 EDM 查詢數據庫。這種查詢方式非常便於動態地構建查詢語句。

在創建了實體類之後 (如果是 EF 的話還需要有 EDM 文件),就可以對數據庫進行查詢了。在查詢之前,首先要創建 DataContext (L2S) 或者 ObjectContext (EF) 對象,這個對象用於指定數據庫連接字符串。

(P313)

直接創建 DataContext / ObjectContext 實例是一種很底層的使用方式,它可以展示出這兩種類型是如何工作的。但在實際應用中,更常用的方式是創建類型化的 Context (繼承自 DataContext / ObjectContext) 來使用。

對於 L2S 來說,我們只需爲 DataContext 傳遞一個數據庫連接字符串即可;而對於 EF ,傳遞的是數據庫連接實體,這個實體中除了數據庫連接字符串之外,還包括 EDM 文件的路徑信息。 (如果通過 Visual Studio 創建 EDM 文件,那麼系統會自動在項目的 app.config 文件中添加完整的數據庫連接實體,可以從這個文件得到需要的信息) 。

然後我們就可以使用 GetTable (L2S) 或者 CreateObjectSet (EF) 對象了,這兩個對象都是用於從數據庫中讀取數據。

Single 運算符會根據主鍵從結果集中取出一行記錄。和 First 關鍵字不同的是,Single 運算符要求結果集中只有一條記錄,當結果集中的結果多於一行時,它會拋出異常;而 First 關鍵字在這種情況下則不會拋出異常。

DataContext / ObjectContext 這兩個對象實際上只做兩件事情。第一,它作爲一個工廠,將我們查詢的數據組合成對象。第二,它會維護實體類的狀態,如果查詢出實體類中的值在類外改變了,它會記錄下這個字段,然後便於更新回數據庫。

在 EF 中,唯一的不同點是使用 SaveChanges 方法代替 SubmitChanges 方法。

(P314)

在對數據庫的查詢中,一個更好的方式是爲每個數據庫定義一個繼承自 DataContext / ObjectContext 的子類,一般會爲每個實體類都添加一個這樣的屬性,這種屬性我們稱之爲類型化的 Context 。

儘管 DataContext / ObjectContext 都實現了 IDisposable 接口,而且 Dispose 方法會強制斷開數據庫連接,但是我們一般不通過調用 Dispose 方法來銷燬這兩個對象,因爲 L2S 和 EF 在返回查詢結果後會自動斷開連接。

(P315)

DataContext / ObjectContext 對象有跟蹤實體類狀態的功能,當取出一個表中的數據保存到本地內存之後,如果下次再到數據庫中查詢某條已經存在的數據, DataContext / ObjectContext 並不會去數據庫中讀取數據,而是直接從內存中取出需要的數據。也就是說,在一個 context 的生命週期中,他不會將數據庫中的某行記錄返回兩次 (數據記錄之間使用主鍵進行區分) 。

L2S 和 EF 都允許關閉對象狀態跟蹤功能,爲避免這些限制,在 L2S 中將 DataContext 對象的 ObjectTrackingEnabled 屬性設置成 false 即可。在 EF 中禁用對象跟蹤的功能要麻煩一點,它需要在每個實體中都添加下面的代碼 :

context.Customers.MergeOption = MergeOption.NoTracking;

關閉對象狀態跟蹤功能之後,爲了數據安全,通過 context 向數據庫中提交更新的功能也同時被禁用。

(P316)

如果要從數據庫中得到最新的數據,必須定義一個新的 context 對象,將舊的實體類傳給這個對象,然後調用 Refresh 方法,這樣,最新的數據就會被更新到實體類中。

在一個多層次的系統中,不能在系統的中間層定義一個靜態的 DataContext 或者 ObjectContext 實例完成所有的數據庫查詢操作,因爲 context 對象不能保證線程安全。正確的做法是在中間層的方法中,爲每個請求的客戶創建一個 context ,這樣做的好處是可以減輕數據庫的負擔,因爲維護和更新實體的任務被多個 context 對象分擔。對於數據庫來說,更新操作會通過多個事務執行完成,這顯然比一個很大的事務要高效很多。

使用實體類生成工具還有一個特點,當表之間有關聯關係的時候,我們可以直接使用關聯表中的屬性,實體類自動完成了關聯的字段和關聯表的映射。

(P317)

L2S 查詢中 [Association] 標籤的作用是提供生成 SQL 語句所需的信息;而 EF 中的 [EdmRelationshipNavigationProperty] 標籤的作用是告訴 EF 要到 EDM 中去查找兩個表的關聯關係。

L2S 和 EF 的查詢方式仍然是延遲加載,在 L2S 查詢中,真正的查詢會在遍歷結果集時進行,而 EF 的查詢則是在顯式地調用了 Load 方法之後纔會執行。

(P318)

可以通過設置下面這個屬性使 EF 和 L2S 以相同的方式返回 EntityCollection 和 EntityReferences :

context.ContextOptions.DeferredLoadingEnabled = true;

(P319)

DataLoadOptions 類是 L2S 中一個特有的類,它有兩個作用 :

  1. 它允許我們爲 EntitySet 所關聯的類指定一個篩選條件;

  2. 它可以強制加載特定的 EntitySets ,這樣可以減少整個數據查詢的次數;

(P320)

L2S 和 EF 都會跟蹤實體類的狀態,如果實體中的數據有所改變,我們可以將這些改變更新回數據庫,更新的方式是調用 DataContext 類中的 SubmitChanges 方法,在 EF 中則是使用 ObjectContext 對象的 SaveChanges 方法。

除此之外,L2S 的 Table<T> 類還提供了 InsertOnSubmit 和 DeleteOnSubmit 方法用於插入和刪除數據表中的記錄;而 EF 的 ObjectSet<T> 類提供了 AddObject 和 DeleteObject 方法來完成相同的功能。

(P321)

SubmitChanges / SaveChanges 會記錄 context 創建以來實體類中所有數據變化,然後將這些變化更新回數據庫中,在更新的過程中,需要創建一個 TransactionScope 對象來幫助完成,以免更新過程中造成的錯誤數據。

也可以使用 EntitySet / EntityCollection 類中的 Add 方法向數據庫中添加新的記錄。在調用了 SubmitChanges 或者 SaveChanges 方法之後,實體中新添加的記錄的外鍵信息會被自動取出來。

爲新添加的實體對象添加主鍵值比較繁瑣,因爲我們需要保證這個主鍵是唯一的,解決辦法是可以在數據庫中定義自增類型的主鍵,或者使用 Guid 作爲主鍵。

L2S 能夠識別它們的關聯關係並賦值是因爲實體類中有這樣的關聯定義,而 EF 之所以可以自動識別關聯並賦值是因爲 EDM 中存儲了這兩種實體間的關聯關係以及關聯字段。

(P322)

當從 EntitySet / EntityCollection 對象中移除一行後,它的外鍵的值會自動設置成 null 。

L2S 和 EF 的 API 對比 :

  1. 各種操作的基礎類 : DataContext (L2S) - ObjectContext (EF);

  2. 從數據庫中取出指定類型的所有記錄 : GetTable (L2S) - CreateObjectSet (EF);

  3. 方法的返回類型 : Table<T> (L2S) - ObjectSet<T> (EF);

  4. 將實體中的屬性值的變化 (添加、刪除等) 更新回數據庫 : SubmitChanges (L2S) - SaveChanges (EF);

  5. 使用 conetext 更新的方式向數據庫中添加新的記錄 : InsertOnSubmit (L2S) - AddObject (EF);

  6. 使用 context 更新的方式刪除記錄 : DeleteOnSubmit (L2S) - DeleteObject (EF);

  7. 關聯表中用於存放多條關聯記錄的屬性 : EntitySet<T> (L2S) - EntityCollection<T> (EF);

  8. 關聯表中用於存放單條關聯記錄的屬性 : EntityRef<T> (L2S) - EntityReference<T> (EF);

  9. 加載關聯屬性的默認方式 : Lazy (L2S) - Explicit (EF);

  10. 構建立即加載的查詢方式 : DataLoadOptions (L2S) - Include() (EF);

(P325)

一個查詢表達式樹是由一個微型的 DOM (Document Object Model ,文檔對象模型) 來描述的。這個 DOM 中每個節點都代表了 System.Linq.Expressions 命名空間中的一個類型。

(P326)

Expression<T> 的基類是 LambdaExpression ,LambdaExpression 是 Lambda 表達式樹中所有節點的基類型,所有的節點類型都可以轉換成這種基類型,因此保證了表達式樹中節點的類型一致性。

Lambda 表達式需要接收參數,而普通的表達式則沒有參數。

【第09章】

(P329)

標準查詢運算符可以分爲三類 :

  1. 輸入是集合,輸出是集合;

  2. 輸入是集合,輸出是單個元素或者標量值;

  3. 沒有輸入,輸出是集合 (生成方法) ;

(P330)

[集合] --> [集合]

  1. 篩選運算符 —— 返回原始序列的一個子集。使用的運算符有 : Where 、 Take 、 TakeWhile 、 Skip 、 SkipWhile 、 Distinct ;

  2. 映射運算符 —— 這種運算符可以按照 Lambda 表達式指定的形式,將每個輸入元素轉換成輸出元素。 SelectMany 用於查詢嵌套的集合;在 LINQ to SQL 和 EF 中 Select 和 SelectMany 運算符可以執行內連接、左外連接、交叉連接以及非等連接等各種連接查詢。使用的運算符有 : Select 、 SelectMany ;

  3. 連接運算符 —— 用於將兩個集合連接之後,取得符合條件的元素。連接運算符支持內連接和左外連接,非常適合對本地集合的查詢。使用運算符有 : Join 、 GroupJoin 、 Zip ;

  4. 排序運算符 —— 返回一個經過重新排序的集合,使用的運算符有 : OrderBy 、 ThenBy 、 Reverse ;

(P331)

  1. 分組運算符 —— 將一個集合按照某種條件分成幾個不同的子集。使用的運算符有 : GroupBy ;

  2. 集合運算符 —— 主要用於對兩個相同類型集合的操作,可以返回兩個集合中共有的元素、不同的元素或者兩個集合的所有元素。使用的運算符有 : Concat 、 Unoin 、 Intersect 、 Except ;

  3. 轉換方法 Import —— 這種方法包括 OfType 、 Cast ;

  4. 轉換方法 Export —— 將 IEnumerable<TSource> 類型的集合轉換成一個數組、清單、字典、檢索或者序列,這種方法包括 : ToArray 、 ToList 、 ToDictionary 、 ToLookup 、 AsEnumerable 、 AsQueryable ;

[集合] --> [單個元素或標量值]

  1. 元素運算符 —— 從集合中取出單個特定的元素,使用的運算符有 : First 、 FirstOrDefault 、 Last 、 LastOrDefault 、 Single 、 SingleOrDefault 、 ElementAt 、 ElementAtOrDefault 、 DefaultIfEmpty ;

  2. 聚合方法 —— 對集合中的元素進行某種計算,然後返回一個標量值 (通常是一個數字) 。使用的運算符有 : Aggregate 、 Average 、 Count 、 LongCount 、 Sum 、 Max 、 Min ;

  3. 數量詞 —— 一種返回 true 或者 false 的聚合方法,使用的運算符有 : All 、 Any 、 Contains 、 SequenceEqual ;

(P332)

[空] --> [集合]

第三種查詢運算符不需要輸入但可以輸出一個集合。

生成方法 —— 生成一個簡單的集合,使用的方法有 : Empty 、 Range 、 Repeat ;

(P333)

經過各種方法的篩選,最終得到的序列中的元素只能比原始序列少或者相等,絕不可能比原始序列還多。在篩選過程中,集合中的元素類型及元素值是不會改變的,和輸入時始終保持一致。

如果和 let 語句配合使用的話,Where 語句可以在一個查詢中出現多次。

(P334)

標準的 C# 變量作用域規則同樣適用於 LINQ 查詢。也就是說,在使用一個查詢變量前,必須先聲明,否則不能使用。

Where 判斷選擇性地接受一個 int 型的第二參數。這個參數用於指定輸入序列中特定位置上的元素,在查詢中可以使用這個數值進行元素的篩選。

下面幾個關鍵字如果用在 string 類型的查詢中將會被轉換成 SQL 中的 LIKE 關鍵字 : Contains 、 StartsWith 、 EndsWith 。

Contains 關鍵字僅用於本地集合的比較。如果想要比較兩個不同列的數據,則需要使用 SqlMethods.Like 方法。

SqlMethods.Like 也可以進行更復雜的比較操作。

在 LINQ to SQL 和 EF 中,可以使用 COntains 方法來查詢一個本地集合。

如果本地集合是一個對象集合或其他非數值類型的集合,LINQ to SQL 或者 EF ,也可能把 Contains 關鍵字翻譯成一個 EXISTS 子查詢。

(P335)

Take 返回集合的前 n 個元素,並且放棄其餘元素;Skip 則是跳過前 n 個元素,並且返回其餘元素。

在 SQL Server 2005 中,LINQ to SQL 和 EF 中的 Take 和 Skip 運算符會被翻譯成 ROW_NUMBER 方法,而在更早的 SQL Server 數據庫版本中則會被翻譯成 Top n 查詢。

TakeWhile 運算符會遍歷輸入集合,然後輸出每個元素,直到給定的判斷爲 false 時停止輸出,並忽略剩餘的元素。

SkipWhile 運算符會遍歷輸入集合,忽略判斷條件爲真之前的每個元素,直到給定的判斷爲 false 時輸出剩餘的元素。

在 SQL 中沒有與 TakeWhile 和 SkipWhile 對應的查詢方式,如果在 LINQ-to-db 查詢中使用,將會導致一個運行時錯誤。

(P336)

Distinct 的作用是返回一個沒有重複元素的序列,它會刪除輸入序列中的重複元素。在這裏,判斷兩個元素是否重複的規則是可以自定義的,如果沒有自定義,那麼就使用默認的判斷規則。

因爲 string 實現了 IEnumerable<char> 接口,所以我們可以在一個字符串上直接使用 LINQ 方法。

在查詢一個數據庫時, Select 和 SelectMany 是最常用的連接操作方法;對於本地查詢來說,使用 Join 和 Group 的效率最好。

在使用 Select 時,通常不會減少序列中的元素數量。每個元素可以被轉換成需要的形式,並且這個形式需要通過 Lambda 表達式來定義。

(P337)

在條件查詢中,一般不需要對查詢結果進行映射,之所以要使用 select 運算符,是爲了滿足 LINQ 查詢必須以 select 或者 group 語句結尾的語法要求。

Select 表達式還接受一個整型的可選參數,這個參數實際上是一個索引,使用它可以得到輸入序列中元素的位置。需要注意的是,這種參數只能在本地查詢中使用。

可以在 Select 語句中再嵌套 Select 子句來構成嵌套查詢,這種嵌套查詢的結果是一個多層次的對象集合。

(P338)

內部的子查詢總是針對外部查詢的某個元素進行。

Select 內部的子查詢可以將一個多層次的對象映射成另一個多層次的對象,也可以將一組關聯的單層次對象映射成一個多層次的對象模型。

在對本地集合的查詢中,如果 Select 語句中包含 Select 子查詢,那麼整個查詢是雙重的延遲加載。

子查詢的映射在 LINQ to SQL 和 EF 中都可以實現,並且可以用來實現 SQL 的連接功能。

(P339)

我們將查詢結果映射到匿名類中,這種映射方式適用於查詢過程中暫存中間結果集的情況,但是當需要將結果返回給客戶端使用的時候,這種映射方式就不能滿足需求了,因爲匿名類型只能在一個方法內作爲本地變量存在。

(P341)

SelectMany 可以將兩個集合組成一個更大的集合。

(P342)

在分層次的數據查詢中,使用 SelectMany 和 Select 得到的結果是相同的,但是在查詢單層次的數據源 (如數組) 的時候,Select 要完成同樣的任務,就需要使用嵌套循環了。

SelectMany 的好處就是在於,無論輸入集合是什麼類型的,它輸出的集合肯定是一個數組類型的二維集合,結果集的數據不會有層次關係。

在查詢表達式語法中,from 運算符有兩個作用,在查詢一開始的 from 的作用都是引入查詢集合和範圍變量;其他任何位置再出現 from 子句,編譯器都會將其翻譯成 SelectMany 。

(P343)

在需要用到外部變量的情況下,選擇使用查詢表達式語法是最佳選擇。因爲在這種情況中,這種語法不僅便於書寫,而且表達方式也更接近查詢邏輯。

(P344)

在 LINQ to SQL 和 EF 中, SelectMany 可以實現交叉連接、不等連接、內連接以及左外連接。

(P345)

在標準 SQL 中,所有的連接都要通過 join 關鍵字實現。

在 Entity Framework 的實體類中,並不會直接存儲一個外鍵值,而是存儲外鍵所關聯對象的集合,所以當需要使用外鍵所關聯的數據時,直接使用實體類屬性中附帶的數據集合即可,不用像 LINQ to SQL 查詢中那樣手動地進行連接來得到外鍵集合中的數據。

對於本地集合的查詢中,爲了提高執行效率,應該儘量先篩選,再連接。

如果有需要的話,可以引入新的表來進行連接,查詢時的連接並不限於兩個表之間,多個表也可以進行。在 LINQ 中,可以通過添加一個 from 子句來實現。

(P347)

正確的做法是在 DefaultIfEmpty 運算符之前使用 Where 語句。

Join 和 GroupJoin 的作用是連接兩個集合進行查詢,然後返回一個查詢結果集。他們的不同點在於,Join 返回的是非嵌套結構的數據集合,而 GroupJoin 返回的則是嵌套結構的數據集合。

Join 和 GroupJoin 的長處在於對本地集合的查詢,也就是對內存中數據的查詢效率比較高。它們的缺點是目前只支持內連接和左外連接,並且連接條件必須是相等連接。需要用到交叉連接或者非等值連接時,就只能選擇 Select 或者 SelectMany 運算符。在 LINQ to SQL 或者 EF 查詢中, Join 和 GroupJoin 運算符在功能上與 Select 和 SelectMany 是沒有什麼區別的。

(P352)

當 into 關鍵字出現在 join 後面的時候,編譯器會將 into 關鍵字翻譯成 GroupJoin 來執行。而當 into 出現在 Select 或者 Group 子句之後時,則翻譯成擴展現有的查詢。雖然都是 into 關鍵字,但是出現在不同的地方,差別非常大。有一點它們是相同的,into 關鍵字總是引入一個新的變量。

GroupJoin 的返回結果實際上是集合的集合,也就是一個集合中的元素還是集合。

(P355)

Zip 是在 .NET Framework 4.0 中新加入的一個運算符,它可以同時枚舉兩個集合中的元素 (就像拉鍊的兩邊一樣) ,返回的集合是經過處理的元素對。

兩個集合中不能配對的元素會直接被忽略。需要注意的是,Zip 運算符只能用於本地集合的查詢,它不支持對數據庫的查詢。

經過排序的集合中的元素值和未排序之前是相同的,只是元素的順序不同。

(P356)

OrderBy 可以按照指定的方式對集合中的元素進行排序,具體的排序方式可以在 KeySelector 表達式中定義。

如果通過 OrderBy 按照指定順序進行排序後,集合中的元素相對順序仍無法確定時,可以使用 ThenBy 。

ThenBy 關鍵字的作用是在前一次排序的基礎上再進行一次排序。在一個查詢中,可以使用任意多個 ThenBy 關鍵字。

(P357)

LINQ 中還提供了 OrderByDescending 和 ThenByDescending 關鍵字,這兩個關鍵字也是用於完成對集合的排序功能,它們的功能和 OrderBy / ThenBy 相同,用法也一樣,只是它們排序後的集合中的元素是按指定字段的降序排序。

在對本地集合的查詢中,LINQ 會根據默認的 IComparable 接口中的算法對集合中的元素進行排序。如果不想使用默認的排序方式,可以自己實現一個 IComparable 對象,然後將這個對象傳遞給查詢 LINQ 。

在查詢表達式語法中我們沒有辦法將一個 IComparable 對象傳遞給查詢語句,也就不能進行自定義的查詢。

在使用了排序操作的查詢中,排序運算符會將集合轉換成 IEnumerable<T> 類型的一個特殊子類。具體來說,對 Enumerable 類型的集合查詢時,返回 IOrderedEnumerable 類型的集合;在對 Queryable 類型的集合查詢時,返回 IOrderedQueryable 類型的集合。這兩種子類型是爲排序專門設計的,在它們上面可以直接使用 ThenBy 運算符來進行多次排序。

(P358)

在對遠程數據源的查詢中,需要用 AsQueryable 代替 AsEnumerable 。

(P359)

GroupBy 可以將一個非嵌套的集合按某種條件分組,然後將得到的分組結果以組爲單位封裝到一個集合中。

Enumerable.GroupBy 的內部實現是,首先將集合中的所有元素按照鍵值的關係存儲到一個臨時的字典類型的集合中。然後再將這個臨時集合中的所有分組返回給調用者。這裏一個分組就是一個鍵和它所對應的一個小集合。

默認情況下,分組之後的元素不會對原始元素做任何處理,如果需要在分組過程中對元素做某些處理的話,可以給元素選擇器指定一個參數。

(P360)

GroupBy 只對集合進行分組,並不做任何排序操作,如果想要對集合進行排序的話,需要使用額外的 OrderBy 關鍵字。

在查詢表達式語法中,GroupBy 可以使用下面這個格式來創建 : group 元素表達式 by 鍵表達式 。

和其他的查詢一樣,當查詢語句中出現了 select 或者 group 的時候,整個查詢就結束了,如果不想讓查詢就此結束,那麼就需要擴展整個查詢,可以使用 into 關鍵字。

在 group by 查詢中,經常需要擴展查詢語句,因爲需要對分組後的集合進一步進行處理。

在 LINQ 中, group by 後面跟着 where 查詢相當於 SQL 中的 HAVING 關鍵字。這個 where 所作用的對象是整個集合或者集合中的每個分組,而不是單個元素。

分組操作同樣適用於對數據庫的查詢。如果是在 EF 中,在使用了關聯屬性的情況下,分組操作並不像在 SQL 中那樣常用。

(P361)

LINQ 中的分組功能對 SQL 中的 “GROUP BY” 進行了很大的擴展,可以認爲 LINQ 中的分組是 SQL 中分組功能的一個超集。

和傳統 SQL 查詢不同點是,在 LINQ 中不需要對分組或者排序子句中的變量進行映射。

當需要使用集合中多個鍵來進行分組時,可以使用匿名類型將這幾個鍵封裝到一起。

(P362)

Concat 運算符的作用是合併兩個集合,合併方式是將第一個集合中所有元素放置到結果集中,然後再將第二個集合中的元素放在第一個結果集的後面,然後返回結果集。Union 執行的也是這種合併操作,但是它最後會將結果集中重複的元素去除,以保證結果集中每個元素都是唯一的。

當對兩個不同類型但基類型卻相同的序列執行合併時,需要顯式地指定這兩個集合的類型以及合併之後的集合類型。

Intersect 運算符用於取出兩個集合中元素的交集。Except 用於取出只出現在第一個集合中的元素,如果某個元素在兩個集合中都存在,那麼這個元素就不會包含在結果中。

Enumerable.Except 的內部實現方式是,首先將第一個集合中的所有元素加載到一個字典集合中,然後再對比第二個集合中的元素,如果字典中的某個元素在第二個集合中出現了,那麼就將這個元素從字典中移除。

(P363)

從根本上講,LINQ 處理的是 IEnumerable<T> 類型的集合,之所以現在衆多的集合類型都可以使用 LINQ 進行處理,是因爲編譯器內部可以將其他類型的序列轉換成 IEnumerable<T> 類型的。

OfType 和 Cast 可以將非 IEnumerable 類型的集合轉換成 IEnumerable<T> 類型的集合。

Cast 和 OfType 運算符的唯一不同就是它們遇到不相容類型時的處理方式 : Cast 會拋出異常,而 OfType 則會忽略這個類型不相容的元素。

元素相容的規則與 C# 的 is 運算符完全相同,因此只能考慮引用轉換和拆箱轉換。

Cast 運算符的內部實現與 OfType 完全相同,只是省略了類型檢查那行代碼。

OfType 和 Cast 的另一個重要功能是 : 按類型從集合中取出元素。

(P365)

ToArray 和 ToList 可以分別將集合轉換成數組和泛型集合。這兩個運算符也會強制 LINQ 查詢語句立即執行,也就是說當整個查詢是延遲加載的時候,一旦遇到 ToArray 或者 ToList ,整個語句會被立即執行。

ToDictionary 方法也會強制查詢語句立即執行,然後將查詢結果放在一個 Dictionary 類型的集合中。 ToDictionary 方法中的鍵選擇器必須爲每個元素提供一個唯一的鍵,也就是說不同元素的鍵是不能重複的,否則在查詢的時候系統會拋出異常。而 Tolookup 方法的要求則不同,它允許多個元素共用相同的鍵。

AsEnumerable 將一個其他類型的集合轉換成 IEnumerable<T> 類型,這樣可以強制編譯器使用 Enumerable 類中的方法來解析查詢中的運算符。

AsQueryable 方法則會將一個其他類型的集合轉換成 IQueryable<T> 類型的集合,前提是被轉換的集合實現了 IQueryable<T> 接口。否則 IQueryable<T> 會實例化一個對象,然後存儲在本地數組外面,看起來是可以調用 IQueryable 中的方法,但實際上這些方法並沒有真正的意義。

(P366)

所有以 "OrDefault" 結尾的方法有一個共同點,那就是當集合爲空或者集合中沒有符合要求的元素時,這些方法不拋出異常,而是返回一個默認類型的值 default(TSource) 。

對於引用類型的元素來說 default(TSource) 是 null ,而對於值類型的元素來說,這個默認值通常是 0 。

爲了避免出現異常,在使用 Single 運算符時必須保證集合中有且僅有一個元素;而 SingleOrDefault 運算符則要求集合中有一個或零個元素。

Single 是所有元素運算符中要求最多的,而 FirstOrDefault 和 LastOrDefault 則對集合中的元素沒有什麼要求。

(P367)

在 LINQ to SQL 和 EF 中, Single 運算符通常應用於使用主鍵到數據庫中查找特定的單個元素。

ElementAt 運算符可以根據指定的下標取出集合中的元素。

Enumerable.ElementAt 的實現方式是,如果它所查詢的集合實現了 IList<T> 接口,那麼在取元素的時候,就使用 IList<T> 中的索引器。否則,就使用自定義的循環方法,在循環中依次向後查找元素,循環 n 次之後,返回下一個元素。ElementAt 運算符不能在 LINQ to SQL 和 EF 中使用。

DefaultIfEmpty 可以將一個空的集合轉換成 null 或者 default() 類型。這個運算符一般用於定義外連接查詢。

(P368)

Count 運算符的作用是返回集合中元素的個數。

Enumerable.Count 方法的內部實現方式如下 : 首先判斷輸入集合有沒有實現 ICollection<T> 接口,如果實現了,那麼它的就調用 ICollection<T>.Count 方法得到元素個數。否則就遍歷整個集合中的元素,統計出元素的個數,然後返回。

還可以爲 Count 這個方法添加一個篩選條件。

LongCount 運算符的作用和 Count 是相同的,只是它的返回值類型是 int64 ,也就是它能用於大數據量的統計, int64 能統計大概 20 億個元素的集合。

Min 和 Max 返回集合中最小和最大的元素。

如果集合沒有實現 IComparable<T> 接口的話,那麼我們就必須爲這兩個運算符提供選擇器。

選擇器表達式不僅定義了元素的比較方式,還定義了最後的結果集的類型。

(P369)

Sum 和 Average 的返回值類型是有限的,它們內置了以下幾種固定的返回值類型 : int 、 long 、 float 、 double 、 decimal 以及這幾種類型的可空類型。這裏返回值都是值類型,也就是,Sum 和 Average 的預期結果都是數字。而 Min 和 Max 則會返回所有實現了 IComparable<T> 接口的類型。

更進一步講, Average 值返回兩種類型 : decimal 和 double 。

Average 爲了避免查詢過程中數值的精度損失,會自動將返回值類型的精度升高一級。

(P370)

Aggregate 運算符我們可以自定義聚合方法,這個運算符只能用於本地集合的查詢中,不支持 LINQ to SQL 和 EF 。這個運算符的具體功能要根據它在特定情況下的定義來看。

Aggregate 運算符的第一個參數是一個種子,用於指示統計結果的初始值是多少;第二個參數是一個表達式,用於更新統計結果,並將統計結果賦值給新的變量;第三個參數是可選的,用於將統計結果映射成期望的形式。

Aggregate 運算符最大的問題是,它實現的功能通過 foreach 語句也可以實現,而且 foreach 語句的語法更清晰明瞭。 Aggregate 的主要用處在於處理比較大或者比較複雜的聚合操作。

(P372)

Contains 關鍵字接收一個 TSource 類型的參數;而 Any 的參數則定義了篩選條件,這個參數是可選的。

Any 關鍵字對集合中元素的要求低一點,只要集合中有一個元素符合要求,就返回 true 。

Any 包含了 Contains 關鍵字的所有功能。

如果在使用 Any 關鍵字的時候不帶參數,那麼只要集合中有一個元素符合要求,就返回 true 。

Any 關鍵字在子查詢中使用特別廣泛,尤其是在對數據庫的查詢中。

當集合中的元素都符合給定的條件時, All 運算符返回 true 。

SequenceEqual 用於比較兩個集合中的元素是否相同,如果相同則返回 true 。它的篩選條件要求元素個數相同、元素內容相同而且元素在集合中的順序也必須是相同的。

(P373)

Empty 、 Repeat 和 Range 都是靜態的非擴展方法,它們只能用於本地集合中。

Empty 用於創建一個空的集合,它需要接收一個用於標識集合類型的參數。

和 “??” 運算符配合使用的話,Empty 運算符可以實現 DefaultEmpty 的功能。

Range 和 Repeat 運算符只能使用在整型集合中。

Range 接收兩個參數,分別用於指示起始元素的下標和查詢元素的個數。

Repeat 接收兩個參數,第一個參數是要創建的元素,第二個參數用於指示重複元素的個數。

【第10章】

(P375)

在 .NET Framework 中提供了很多用於處理 XML 數據的 API 。從 .NET Framework 3.5 之後,LINQ to XML 成爲處理通用 XML 文檔的首選工具。它提供了一個輕量的集成了 LINQ 友好的 XML 文檔對象模型,當然還有相應的查詢運算符。在大多數情況下,它完全可以替代之前 W3C 標準的 DOM 模型 (又稱爲 XmlDocument) 。

LINQ to XML 中 DOM 的設計非常完善且高效。即使沒有 LINQ ,單純的 LINQ to XML 中 DOM 對底層 XmlReader 和 XmlWriter 類也進行了很好的封裝,可以通過它來更簡單地使用這兩個類中的方法。

LINQ to XML 中所有的類型定義都包含在 System.Xml.Linq 命名空間中。

所有 XML 文件一樣,在文件開始都是聲明部分,然後是根元素。

屬性由兩部分組成 : 屬性名和屬性值。

(P376)

聲明、元素、屬性、值和文本內容這些結構都可以用類來表示。如果這種類有很多屬性來存儲子內容,我們可以用一個對象樹來完全描述文檔。這個樹狀結構就是文檔對象模型 (Document Object Model) ,簡稱 DOM 。

LINQ to XML 由兩部分組成 :

  1. 一個 XML DOM ,我們稱之爲 X-DOM ;

  2. 約 10 個用於查詢的運算符;

可以想象, X-DOM 是由諸如 XDocument 、 XElement 、 XAttribute 等類組成的。有意思的是, X-DOM 類並沒有和 LINQ 綁定在一起,也就是說,即使不使用 LINQ 查詢,也可以加載、更新或存儲 X-DOM 。

X-DOM 是集成了 LINQ 的模型 :

  1. X-DOM 中的一些方法可以返回 IEnumerable 類型的集合,使 LINQ 查詢變得非常方便;

  2. X-DOM 的構造方法更加靈活,可以通過 LINQ 將數據直接映射成 X-DOM 樹;

XObject 是整個繼承結構的根, XElement 和 XDocument 則是平行結構的根。

XObject 是所有 X-DOM 內容的抽象基類。在這個類型中定義了一個指向 Parent 元素的鏈接,這樣就可以確定節點之間的層次關係。另外這個類中還有一個 XDocument 類型的對象可供使用。

除了屬性之外, XNode 是其他大部分 X-DOM 內容的基類。 XNode 的一個重要特性是它可以被有順序地存放在一個混合類型的 XNodes 集合中。

XAttribute 對象的存儲方式 —— 多個 XAttribute 對象必須成對存放。

(P377)

雖然 XNode 可以訪問它的父節點 XElement ,但是它卻對自己的子節點一無所知,因爲管理子節點的工作是由子類 XContainer 來做的。 XContainer 中定義了一系列成員和方法來管理它的子類,並且是 XElement 和 XDocument 的抽象基類。

除了 Name 和 Value 之外, XElement 還定義了其他的成員來管理自己的屬性,在絕大多數情況下, XElement 會包含一個 XText 類型的子節點, XElement 的 Value 屬性同時包含了存取這個 XText 節點的 get 和 set 操作,這樣可以更方便地設置節點值。由於 Value 屬性的存在,我們可以不必直接使用 XText 對象,這使得對節點的賦值操作變得非常簡單。

(P378)

XML 樹的根節點是 XDocument 對象。更準確地說,它封裝了根 XElement ,添加了 XDeclaration 以及一些根節點需要執行的指令。與 W3C 標準的 DOM 有所不同,即使沒有創建 XDocument 也可以加載、操作和保存 X-DOM 。這種對 XDocument 的不依賴性使得我們可以很容易將一個節點子樹移到另一個 X-DOM 層次結構中。

XElement 和 XDocument 都提供了靜態 Load 和 Parse 方法,使用這兩個方法,開發者可以根據已有的數據創建 X-DOM :

  1. Load 可以根據文件、 URI 、 Stream 、 TextReader 或者 XmlReader 等構建 X-DOM ;

  2. Parse 可以根據字符串構建 X-DOM ;

(P379)

在節點上調用 ToString 方法可將這個節點中的內容轉換成 XML 字符串,默認情況下,轉換後的 XML 字符串是經過格式化的,即使用換行和空格將 XML 字符串按層次結構逐行輸出,且使用正確的縮進格式。如果不想讓 ToString 方法格式化 XML ,那麼可以指定 SaveOptions.DisableFormatting 參數。

XElement 和 XDocument 還分別提供了 Save 方法,使用這個方法可將 X-DOM 寫入文件、 Stream 、 TextWriter 或者 XmlWriter 中。如果選擇將 X-DOM 寫入到一個文件中,則會自動寫入 XML 聲明部分。另外, XNode 類還提供了一個 WriteTo 方法,這個方法只能向 XmlWriter 中寫入數據。

創建 X-DOM 樹常用的方法是手動實例化多個節點,然後通過 XContainer 的 Add 方法將所有節點拼裝成 XML 樹,而不是通過 Load 或者 Parse 方法。

要構建 XElement 和 XAttribute ,只需提供屬性名和屬性值。

構建 XElement 時,屬性值不是必須的,可以只提供一個元素名並在其後添加內容。

注意,當需要爲一個對象添加屬性值時,只需設置一個字符串即可,不用顯式創建並添加 XText 子節點, X-DOM 的內部機制會自動完成這個操作,這使得活加屬性值變得更加容易。

(P380)

X-DOM 還支持另一種實例化方式 : 函數型構建 (源於函數式編程) 。

這種構建方式有兩個優點 : 第一,代碼可以體現出 XML 的結構;第二,這種表達式可以包含在 LINQ 查詢的 select 子句中。

之所以以函數型構建的方式定義 XML 文件,是因爲 XElement (和 XDocument) 的構造方法都可重載,以接受 params 對象數組 : public XElement(XName name, params object[] content) 。

XContainer 類的 Add 方法同樣也接收這種類型的參數 : public void Add(params object[] content) 。

所以,我們可以在構建或添加 X-DOM 時指定任意數目、任意類型的子對象。這是因爲任何內容都是合法的。

XContainer 類內部的解析方式 :

  1. 如果傳入的對象是 null ,那麼就忽略這個節點;

  2. 如果傳入對象是以 XNode 或者 XStreamingElement 作爲基類,那麼就將這個對象添加爲 Node 對象,放到 Nodes 集合中;

  3. 如果傳入對象是 XAttribute ,那麼就將這個對象作爲 Attribute 集合來處理;

  4. 如果對象是 string ,那麼這個對象會被封裝成一個 XText 節點,然後添加到 Nodes 集合中;

  5. 如果對象實現了 IEnumerable 接口,則對其進行枚舉,每個元素都按照上面的規則來處理;

  6. 如果某個類型不符合上述任一條件,那麼這個對象會被轉換成 string ,然後被封裝在 XText 節點上,並添加到 Nodes 集合中;

上述所有情況最終都是 : Nodes 或 Attributes 。另外,所有對象都是有效的,因爲最終肯定可以調用它的 ToString 方法並將其作爲 XText 節點來處理。

實際上, X-DOM 內部在處理 string 類型的對象時,會自動執行一些優化操作,也就是簡單地將文本內容存放在字符串中。直到 XContainer 上調用 Nodes 方法時,纔會生成實際的 XText 節點。

(P382)

與在 XML 中一樣, X-DOM 中的元素和屬性名是區分大小寫的。

使用 FirstNode 與 LastNode 可以直接訪問第一個或最後一個子節點;Nodes 返回所有的子節點並形成一個序列。這三個函數只用於直系的子節點。

(P383)

Elements() 方法返回類型爲 XElement 的子節點。

Elements() 方法還可以只返回指定名字的元素。

(P384)

Element() 方法返回匹配給定名稱的第一個元素。Element 對於簡單的導航是非常有用的。

Element 的作用相當於調用 Elements() ,然後再應用 LINQ 的 FirstOrDefault 查詢運算符給定一個名稱作爲匹配斷言。如果沒有找到所請求的元素,則 Element 返回 null 。

XContainer 還定義了 Descendants 和 DescendantNodes 方法,它們遞歸地返回子元素或子節點。

Descendants 接受一個可選的元素名。

(P385)

所有的 XNodes 都包含一個 Parent 屬性,另外還有一個 AncestorXXX 方法用來找到特定的父節點。一個父節點永遠是一個 XElement 。

Ancestors 返回一個序列,其第一個元素是 Parent ,下一個元素則是 Parent.Parent ,依次類推,直到根元素。

還可以使用 LINQ 查詢 AncestorsAndSelf().Last() 來取得根元素。

另外一種方法是調用 Document.Root ,但只有存在 XDocument 時才能執行。

使用 PreviousNode 和 NextNode (以及 FirstNode / LastNode) 方法查找節點時,相當於從一個鏈表中遍歷所有節點。事實上 XML 中節點的存儲結構確實是鏈表。

(P386)

XNode 存儲在一個單向鏈表中,所以 PreviousNode 並不是當前元素的前序元素。

Attributes 方法接受一個名稱並返回包含 0 或 1 個元素的序列;在 XML 中,元素不能包含重複的屬性名。

可以使用下面這幾種方式來更新 XML 中的元素和屬性 :

  1. 調用 SetValue 方法或者重新給 Value 屬性賦值;

  2. 調用 SetElementValue 或 SetAttributeValue 方法;

  3. 調用某個 RemoveXXX 方法;

  4. 調用某個 AddXXX 或 ReplaceXXX 方法指定更新的內容;

也可以爲 XElement 對象重新設置 Name 屬性。

使用 SetValue 方法可以使用簡單的值替換元素或者屬性中原來的值。通過 Value 屬性賦值會達到相同的效果,但只能使用 string 類型的數據。

調用 SetValue 方法 (或者爲 Value 重新賦值) 的結果就是它替換了所有的子節點。

(P387)

最好的兩個方法是 : SetElementValue 和 SetAttributeValue 。它們提供了一種非常便捷的方式來實例化 XElement 或 XAttribute 對象,然後調用父節點的 Add 方法,將新節點加入到父節點下面,從而替換相同名稱的任何現有元素或屬性。

Add 方法將一個子節點添加到一個元素或文檔中。AddFirst 也一樣,但它將節點插入集合的開頭而不是結尾。

我們也可以通過調用 RemoveNodes 或 RemoveAttributes 將所有的子節點或屬性全部刪除。 RemoveAll 相當於同時調用了這兩個方法。

ReplaceXXX 方法等價於調用 Removing ,然後再調用 Adding 。它們擁有輸入參數的快照,因此 e.ReplaceNodes(e.Nodes) 可以正常進行。

AddBeforeSelf 、 AddAfterSelf 、 Remove 和 ReplaceWith 方法不能操作一個節點的子節點。它們只能操作當前節點所在的集合。這就要求當前節點都有父元素,否則在使用這些方法時就會拋出異常。此時 AddBeforeSelf 和 AddAfterSelf 方法非常有用,這兩個方法可以將一個新節點插入到 XML 中的任意位置。

(P388)

Remove 方法可以將當前節點從它的父節點中移除。ReplaceWith 方法實現同樣的操作,只是它在移除舊節點之後還會在同一位置插入其他內容。

通過 System.Xml.Linq 中的擴展方法,我們可以使用 Remove 方法整組地移除節點或者屬性。

(P389)

Remove 方法的內部實現機制是這樣的 : 首先將所有匹配的元素讀取到一個臨時列表中,然後枚舉該臨時列表並執行刪除操作。這避免了在刪除的同時進行查詢操作所引起的錯誤。

XElement 和 XAttribute 都有一個 string 類型的 Value 屬性,如果一個元素有 XText 類型的子節點,那麼 XElement 的 Value 屬性就相當於訪問此節點的快捷方式,對於 XAttribute 的 Value 屬性就是指屬性值。

有兩種方式可以設置 Value 屬性值 : 調用 SetValue 方法或者直接給 Value 屬性賦值。 SetValue 方法要複雜一些,因爲它不僅可以接收 string 類型的參數,也可以設置其他簡單的數據類型。

(P390)

由於有了 Value 的值,你可能會好奇什麼時候才需要直接和 XText 節點打交道?答案是 : 當擁有混合內容時。

(P391)

向 XElement 添加簡單的內容時, X-DOM 會將新添加的內容附加到現有的 XText 節點後面,而不會新建一個 XText 節點。

如果顯式地指定創建新的 XText 節點,最終會得到多個子節點。

XDocument 封裝了根節點 XElement ,可以添加 XDeclaration 、處理指令、說明文檔類型以及根級別的註釋。

XDocument 是可選的,並且能夠被忽略或者省略,這點與 W3C DOM 不同。

XDocument 提供了和 XElement 相同的構造方法。另外由於它也繼承了 XContainer 類,所以也支持 AddXXX 、 RemoveXXX 和 ReplaceXXX 等方法。但與 XElement 不同,一個 XDocument 節點可添加的內容是有限的 :

  1. 一個 XElement 對象 (根節點) ;

  2. 一個 XDeclaration 對象;

  3. 一個 XDocumentType 對象 (引用一個 DTD) ;

  4. 任意數目的 XProcessingInstruction 對象;

  5. 任意數目的 XComment 對象;

(P392)

對於 XDocument 來說,只有根 XElement 對象是必須的。 XDeclaration 是可選的,如果省略,在序列化的過程中會應用默認設置。

(P393)

XDocument 有一個 Root 屬性,這個屬性是取得當前 XDocument 對象單個 XElement 的快捷方式。其反向的鏈接是由 XObject 的 Document 屬性提供的,並且可以應用於樹中的所有對象。

XDocument 對象的子節點是沒有 Parent 信息的。

XDeclaration 並不是 XNode 類型的,因此它不會出現在文檔的 Nodes 集合中,而註釋、處理指令和根元素等都會出現在 Nodes 集合中。

XDeclaration 對象專門存放在一個 Declaration 屬性中。

XML 聲明是爲了保證整個文件被 XML 閱讀器正確解析並理解。

XElement 和 XDocument 都遵循下面這些 XML 聲明的規則 :

  1. 在一個文件名上調用 Save 方法時,總是自動寫入 XML 聲明;

  2. 在 XmlWriter 對象上調用 Save 方法時,除非 XmlWriter 特別指出,都則都會寫入 XML 聲明;

  3. ToString 方法從來都不返回 XML 聲明;

如果不想讓 XmlWriter 創建 XML 聲明,可以在構建 XmlWriter 對象時,通過設置 XmlWriterSettings 對象的 OmitXmlDeclaration 和 ConformanceLevel 屬性來實現。

是否有 XDeclaration 對象對是否寫入 XML 聲明沒有任何影響。 XDeclaration 的目的是提示進行 XML 序列化進程,方式有兩種 :

  1. 使用的文本編碼標準;

  2. 定義 XML 聲明中 encoding 和 standalone 兩個屬性的值 (如果寫入聲明) ;

XDeclaration 的構造方法接受三個參數,分別用於設置 version 、 encoding 和 standalone 屬性。

(P394)

XML 編寫器會忽略所指定的 XML 版本信息,始終寫入 “1.0” 。

需要注意的是,XML 聲明中指定的必須是諸如 “utf-16” 這樣的 IETF 編碼方式。

XML 命名空間有兩個功能。首先,與 C# 的命名空間一樣,它們可以幫助避免命名衝突。當要合併來自兩個不同 XML 文件的數據時,這可能會成爲一個問題。其次,命名空間賦予了名稱一個絕對的意義。

(P395)

xmlns 是一個特殊的保留屬性,以上用法使它執行下面兩種功能 :

  1. 它爲有疑問的元素指定了一個命名空間;

  2. 它爲所有後代元素指定了一個默認的命名空間;

有前綴的元素不會爲它的後代元素定義默認的命名空間。

(P396)

使用 URI (自定義的 URI) 作爲命名空間是一種通用的做法,這可以有效地保證命名空間的唯一性。

對於屬性來說,最好不使用命名空間,因爲屬性往往是對本地元素起作用。

有多種方式可以指定 XML 命名空間。第一種方式是在本地名字前面使用大括號來指定。第二種方式 (也是更好的一種方式) 是通過 XNamespace 和 XName 爲 XML 設置命名空間。

(P397)

XName 還重載了 + 運算符,這樣無需使用大括號即可直接將命名空間和元素組合在一起。

在 X-DOM 中有很多構造方法和方法都能接受元素名或者屬性名作爲參數,但它們實際上接受 XName 對象,而不是字符串。到目前爲止我們都是在用字符串作參數,之所以可以這麼用,是因爲字符串可以被隱式轉換成 XName 對象。

除非需要輸出 XML ,否則 X-DOM 會忽略默認命名空間的概念。這意味着,如果要構建子 XElement ,必須顯式地指定命名空間,因爲子元素不會從父元素繼承命名空間。

(P398)

在使用命名空間時,一個很容易犯的錯誤是在查找 XML 的元素時沒有指定它所屬的命名空間。

如果在構建 X-DOM 樹時沒有指定命名空間,可以在隨後的代碼中爲每個元素分配一個命名空間。

【第11章】

(P407)

System.Xml ,命名空間由以下命名空間和核心類組成 :

System.Xml.* ——

  1. XmlReader 和 XmlWriter : 高性能、只向前地讀寫 XML 流;

  2. XmlDocument : 代表基於 W3C 標準的文檔對象模型 (DOM) 的 XML 文檔;

System.Xml.XPath —— 爲 XPath (一種基於字符串的查詢 XML 的語言) 提供基礎結構和 API (XPathNavigator 類) ;

System.Xml.XmlSchema —— 爲 (W3C) XSD 提供基礎機構和 API ;

System.Xml.Xsl —— 爲使用 (W3C) XSLT 對 XML 進行解析提供基礎結構和 API ;

System.Xml.Serialization —— 提供類和 XML 之間的序列化;

System.Xml.XLinq —— 先進的、簡化的、 LINQ 版本的 XmlDocument 。

W3C 是 World Web Consortium (萬維網聯盟) 的縮寫,定義了 XML 標準。

靜態類 XmlConvert 是解析和格式化 XML 字符串的類。

XmlReader 是一個高性能的類,能夠以低級別、只向前的方式讀取 XML 流。

(P408)

通過調用靜態方法 XmlReader.Create 來實例化一個 XmlReader 對象,可以向這個方法傳遞一個 Stream 、 TextReader 或者 URI 字符串。

因爲 XmlReader 可以讀取一些可能速度較慢的數據源 (Stream 和 URI) ,所以它爲大多數方法提供了異步版本,這樣我們可以方便編寫非阻塞代碼。

XML 流以 XML 節點爲單位。讀取器按文本順序 (深度優先) 來遍歷 XML 流, Depth 屬性返回遊標的當前深度。

從 XmlReader 讀取節點的最基本的方法是調用 Read 方法。它指向 XML 流的下一個節點,相當於 IEnumerator 的 MoveNext 方法。第一次調用 Read 會把遊標放置在第一個節點,當 Read 方法返回 false 時,說明遊標已經到達最後一個節點 在這個時候 XmlReader 應該被關閉。

(P409)

屬性沒有包含在基於 Read 的遍歷中。

XmlReader 提供了 Name 和 Value 這兩個 string 類型的屬性來訪問節點的內容。根據節點類型,內容可能定義在 Name 或 Value 上,或者兩者都有。

(P410)

驗證失敗會導致 XmlReader 拋出 XmlException ,這個異常包含錯誤發生的行號 (LineNumber) 和位置 (LinePosition) 。當 XML 文件很大時記錄這些信息會比較關鍵。

(P413)

XmlReader 提供了一個索引器以直接 (隨機) 地通過名字或位置來訪問一個節點的屬性,使用索引器等同於調用 GetAttributes 方法。

(P415)

XmlWriter 是一個 XML 流的只向前的編寫器。 XmlWriter 的設計和 XmlReader 是對稱的。

和 XmlReader 一樣,可以通過調用靜態方法 Create 來構建一個 XmlWriter 。

(P416)

除非使用 XmlWriterSettings ,並設置其 OmitXmlDeclaration 爲 true 或者 ConfermanceLevel 爲 Fragment ,否則 XmlWriter 會自動地在頂部寫上聲明。並且後者允許寫多個根節點,如果不設置的話會拋出異常。

WriteValue 方法寫一個文本節點。它不僅接受 string 類型的參數,還可以接受像 bool 、 DateTime 類型的參數,實際在內部調用了 XmlConvert 來實現符合 XML 字符串解析。

WriteString 和調用 WriteValue 傳遞一個 string 參數實現的操作是等價的。

在寫完開始節點後可以立即寫屬性。

(P417)

WriteRaw 直接向輸出流注入一個字符串。也可以通過接受 XmlReader 的 WriteNode 方法,把 XmlReader 中的所有內容寫入輸出流。

XmlWriter 使代碼非常簡潔,如果相同的命名空間在父元素上已聲明,它會自動地省略子元素上命名空間的聲明。

(P420)

可以在使用 XmlReader 或 XmlWriter 使代碼複雜時使用 X-DOM ,使用 X-DOM 是處理內部元素的最佳方式,這樣就可以兼併 X-DOM 的易用性和 XmlReader 、 XmlWriter 低內存消耗的特點。

(P421)

XmlDocument 是一個 XML 文檔的內存表示,這個類型的對象模型和方法與 W3C 所定義的模式一致。如果你熟悉其他符合 W3C 的 XML DOM 技術,就會同樣熟悉 XmlDocument 類。但是如果和 X-DOM 相比的話, W3C 模型就顯得過於複雜。

(P422)

可以實例化一個 XmlDocument ,然後調用 Load 或 LoadXml 來從一個已知的源加載一個 XmlDocument :

  1. Load 接受一個文件名、 流 (Stream) 、 文本讀取器 (TextReader) 或者 XML 讀取器 (XmlReader) ;

  2. LoadXml 接受一個 XML 字符串;

相對應的,通過調用 Save 方法,傳遞文件名, Stream 、 TextReader 或者 XmlWriter 參數來保存一個文檔。

通過定義在 XNode 上的 ChildNodes 屬性可以深入到此節點的下層樹型結構,它返回一個可索引的集合。

而使用 ParentNode 屬性,可以返回其父節點。

XmlNode 定義了一個 Attributes 屬性用來通過名字或命名空間或順序位置來訪問屬性。

(P423)

InnerText 屬性代表所有子文本節點的聯合。

設置 InnerText 屬性會用一個文本節點替換所有子節點,所以在設置這個屬性時要謹慎以防止不小心覆蓋了所有子節點。

InnerXml 屬性表示當前節點中的 XML 片段。

如果節點類型不能有子節點, InnerXml 會拋出一個異常。

XmlDocument 創建和添加新節點 :

  1. 調用 XmlDocument 其中一個 CreateXXX 方法;

  2. 在父節點上調用 AppendChild 、 PrependChild 、 InsertBefore 或者 InsertAfter 來添加新節點到樹上;

要創建節點,首先要有一個 XmlDocument ,不能像 X-DOM 那樣簡單地實例化一個 XmlElement 。節點需要 “寄生” 在一個 XmlDocument 宿主上。

(P424)

可以以任何屬順序來構建這棵樹,即便重新排列添加子節點後的語句順序,對此也沒有影響。

也可以調用 RemoveChild 、 ReplaceChild 或者 RemoveAll 來移除節點。

使用 CreateElement 和 CreateAttribute 的重載方法可以指定命名空間和前綴。

CreateXXX (string name);
CreateXXX (string name, string namespaceURI);
CreateXXX (string prefix, string localName, string namespaceURI);

參數 name 既可以是本地名稱 (沒有前綴) ,也可以是帶前綴的名稱。

參數 namespaceURI 用在當且僅當聲明 (而不是僅在引用) 一個命名空間時。

XPath 是 XML 查詢的 W3C 標準。在 .NET Framework 中, XPath 可以查詢一個 XmlDocument ,就像用 LINQ 查詢 X-DOM 。然而 XPath 應用更廣泛,它也在其他 XML 技術中被使用,例如 XML Schema 、 XLST 和 XAML 。

XPath 查詢按照 XPath 2.0 數據模型 (XPath Data Model) 來表示。 DOM 和 XPath 數據模型都表示一個 XML 文檔樹。區別是 XPath 數據模型純粹以數據爲中心,採取了 XML 文本的格式。例如在 XPath 數據模型中,CDATA 部分不是必需的,因爲 CDATA 存在的唯一原因是可以在文本中包含 XML 的一些標識符。

(P425)

可以使用下面的方式在代碼中實現 XPath 查詢 :

  1. 在一個 XmlDocument 或 XmlNode 上調用 SelectXXX 方法;

  2. 從一個 XmlDocument 或者 XPathDocument 上生成一個 XPathNavigator ;

  3. 在 XNode 上調用一個 XPathXXX 擴展方法;

SelectXXX 方法接受一個 XPath 查詢字符串。

(P426)

XPathNavigator 是 XML 文檔的 XPath 數據模型上的一個遊標,他被加載並提供了一些基本方法可以在文檔樹上移動光標。

XPathNavigator 的 Select* 方法可以使用一個 XPath 字符串來表達更復雜的導航或查詢以返回多個節點。

可以從一個 XmlDocument 、 XPathDocument 或者另一個 XPathNavigator 上來生成 XPathNavigator 實例。

(P427)

在 XPath 數據模型中,一個節點的值是文本元素的連接,等同於 XmlDocument 的 InnerText 屬性。

SelectSingleNode 方法返回一個 XPathNavigator 。 Select 方法返回一個 XPathNodeInterator 以在多個 XPathNavigator 上進行簡便地遍歷。

爲了更快地查詢,可以把 XPath 編譯成一個 XPathExpression ,然後傳遞給 Select* 方法。

(P428)

XmlDocument 和 XPathNavigator 的 Select* 方法有對應的重載函數來接受一個 XmlNamespaceManager 。

XPathDocument 是符合 W3C XPath 數據模型的只讀的 XML 文檔。使用 XPathDocument 後跟一個 XPathNavigator 要比一個單純的 XmlDocument 快,但是不能對底層的文檔進行更改。

(P429)

XSD 文檔本身就是用 XML 來寫的,並且 XSD 文檔也是用 XSD 來介紹的。

可以在讀或處理 XML 文件或文檔時用一個或多個模式來驗證它,這樣做有以下幾個理由 :

  1. 可以避免更少的錯誤檢查和異常處理;

  2. 模式檢驗可以查出注意不到的錯誤;

  3. 錯誤信息比較詳細重要;

爲進行驗證,可以把模式加入到 XmlReader 、 XmlDocument 或者 X-DOM 對象中,然後像通常那樣讀取或加載 XML 文檔。模式驗證會在內容被讀的時候自動進行,所以輸入流沒有被讀取兩次。

(P430)

在 System.Xml 命名空間下包含一個 XmlValidatingReader 類,這個類存於 .NET Framework 2.0 之前的版本中,用來進行模式驗證,現在已經不再使用。

(P431)

XSLT (Entensible Stylesheet Language Transformations ,擴展樣式錶轉換語言) 是一種 XML 語言,它介紹瞭如何把一種 XML 語言轉化爲另一種。這種轉化的典型就是把一個 (描述數據的) XML 文檔轉化爲一個 (描述格式化文檔的) XHTML 文檔。

【第12章】

(P432)

有些對象要求顯式地卸載代碼來釋放資源,如打開的文件、鎖、執行中的系統句柄和非託管對象。在 .NET 的術語中,這叫做銷燬 (Disposal) ,它由 IDisposable 接口來實現。

那些佔用託管內存的未使用對象必須在某些時候被回收,這個功能被稱爲垃圾回收,它由 CLR 執行。

銷燬不同於垃圾回收的是,銷燬通常是顯式調用,而垃圾回收則完全自動進行。換言之,程序員要關心釋放文件句柄、鎖和操作系統資源等,而 CLR 則關心釋放內存。

C# 的 using 語句從語法上提供了對實現 IDisposable 接口的對象調用 Dispose 方法的捷徑,它還使用了 try / finally 塊。

(P433)

finally 語句塊保證 Dispose 方法一定被調用,即使是拋出異常或代碼提前離開這個語句塊。

在簡單的情況下,編寫自定義的可銷燬類型只需要實現 IDisposable 接口並編寫 Dispose 方法。

在銷燬的邏輯中,.NET Framework 遵循了一系列實際存在的規則。這些規則並不是硬編碼在 .NET Framework 或 C# 語言中;它們的目的是爲使用者定義一致的協議。它們是 :

  1. 一旦被銷燬,對象無法恢復。對象也不能重新被激活,調用它的方法或屬性將拋出 ObjectDisposedException 異常;

  2. 重複調用對象的 Dispose 方法不會產生異常;

  3. 如果可銷燬對象 x 包含或 “封裝” 或 “佔有” 可釋放資源對象 y , x 的 Dispose 方法自動調用 y 的 Dispose 方法 —— 除非接收到其他指令;

除了 Dispose 方法,一些類還定義了 Close 方法。 .NET Framework 對 Close 方法的語義並不是完全一致,儘管幾乎所有的情況都是下面的一種 :

  1. 從功能上等同於 Dispose 方法;

  2. 從功能上是 Dispose 方法的子集;

(P434)

一些類定義了 Stop 方法,它們可以像 Dispose 方法一樣釋放非託管資源,但不同於 Dispose 方法的是,它允許重新開始。

在 WinRT 中, Close 可以認爲與 Dispose 相同。事實上,運行時會將 Close 方法映射到 Dispose 方法上,使它們的類型同樣可以在 using 語句中使用。

包含非託管資源句柄的對象幾乎總是要求銷燬,目的是爲了釋放這些句柄。

如果一個類型是可銷燬的,它經常 (而非總是) 直接或間接地引用非託管句柄。

有 3 種情況不能釋放 :

  1. 當通過靜態字段或屬性獲得共享對象時;

  2. 當對象的 Dispose 方法執行不需要的操作時;

  3. 當對象的方法在設計時不是必須的,而且釋放那個對象將增加程序的複雜性時;

(P436)

StreamWriter 必須公開另一個方法 (Flush 方法) 來保證使用者不調用 Dispose 方法也能執行必要的清理工作。

Dispose 方法本身並沒有釋放內存,只有垃圾回收時才釋放內存。

無論對象是否要求使用 Dispose 方法來自定義清理邏輯,某些情況下在堆上被佔用的內存必須被釋放。 CLR 通過垃圾回收器完全自動地處理這方面工作。永遠不能自動釋放託管內存。

(P437)

垃圾回收並不是在對象沒有引用之後立即執行。

垃圾回收器在每次回收時並沒有回收所有的垃圾。相反的,內存管理器將對象分爲不同的代,垃圾回收器收集新代 (最近分配的對象) 的垃圾比舊代 (長時間存活的對象) 的垃圾更頻繁。

垃圾回收器試圖在垃圾回收所花費的時間和應用程序內存使用 (工作區) 上保持平衡。因此,應用程序會使用比實際需要更多的內存,特別是構造大的臨時數組。

根保持對象存活。如果對象沒有直接或間接地由根引用,那麼它將被垃圾回收器選中。

根有以下三種 :

  1. 局部變量或執行方法中的參數 (或在調用它的棧的方法中);

  2. 靜態變量;

  3. 準備運行終止器的對象;

(P438)

Windows Runtime 依靠 COM 的引用計數機制來釋放內存,而非依靠自動化的垃圾回收器。

在對象從內存中被釋放之前,它的終止器將運行 (如果它有終止器的話) 。終止器像構造方法一樣聲明,但是它有 ~ 符號作前綴。

雖然與構造函數的聲明相似,但是析構器無法聲明爲 public 或 static ,不能有參數,而且不能調用基類。

(P439)

終止器很有用,但是它有一些附帶條件 :

  1. 終止器使分配和內存回收變得緩慢 (垃圾回收器將對執行的終止器保持追蹤) ;

  2. 終止器延長了對象和任意引用對象的生命週期 (它們必須等待下一次垃圾回收來實際刪除) ;

  3. 無法預測終止器以什麼順序調用一系列的對象;

  4. 對對象的終止器何時被調用只有有限的控制;

  5. 如果終止器的代碼被阻礙,其他對象也不能被終結;

  6. 如果應用程序沒有被完全地卸載,終止器也許會被規避;

總之,終止器儘管在有些時候你確實需要它,通常你不想使用它,除非絕對必要。如果確實要使用它,需要 100% 確定理解它所做的一切。

實現終止器的準則 :

  1. 保證終止器執行得很快;

  2. 永遠不要在終止器中中斷;

  3. 不要引用其他可終結對象;

  4. 不要拋出異常;

終止器的一個很好的用途是當忘記對可銷燬對象調用 Dispose 方法的時候提供一個備份;對象遲一點被銷燬通常比沒有被銷燬好。

(P440)

無參數的版本沒有被聲明成虛方法 (virtual) ,它只是簡單地用 true 作爲參數調用的增強版本。

增強版本包含實際的銷燬邏輯,它是受保護的 (protected) 和虛擬的 (virtual) ,這爲子類添加它們自己的銷燬邏輯提供了安全的方法。

請注意我們在沒有參數的 Dispose 方法中調用了 GC.SuppressFinalize 方法,這防止當垃圾回收器在之後捕捉這個對象時終止器也同時運行的情況。從技術上講這並不必要,因爲 Dispose 方法能夠接受重複調用。但是,這樣可以提高效率,因爲允許對象 (和它引用的對象) 在一個週期中被回收。

(P441)

復活對象的終止器不會第二次執行,除非調用 GC.ReRegisterForFinalize 方法。

(P442)

請注意在終止器方法中只調用一次 ReRegisterForFinalize 方法。如果調用了兩次,對象將會被註冊兩次並且經歷兩次終結過程。

CLR 使用分代式 “標記-緊縮型” 垃圾回收器來執行存儲在託管堆上對象的自動內存管理。垃圾回收器被認爲是追蹤型垃圾回收器,因爲它不會干涉每次對對象的訪問,而是立刻激活並追蹤存儲在託管堆上對象的記錄,以此來決定哪些對象被認爲是垃圾並被回收。

垃圾回收器通過執行內存分配 (通過 new 關鍵字) 開始一次垃圾回收,在內存分配或者某個內存起始點被分配之後,或者在其他減少應用程序內存的時候。這個過程也可以通過調用 System.GC.Collect 方法手動開始。在垃圾回收時,所有的線程也許都會被凍結。

垃圾回收器從根對象引用開始,按對象記錄前進,標記它所有接觸的對象爲可到達的。一旦這個過程結束,所有沒有被標記的對象被認爲是無用的,將會被垃圾回收器回收。

沒有終止器的無用對象將立刻被刪除;有終止器的對象將在垃圾回收結束之後在終止器中排隊進行處理。這些對象將在下一次對這代對象的垃圾回收過程中被選中回收 (除非復活) 。

然後將剩餘的 “活動” 對象移到堆的開頭 (緊縮) ,釋放出更多的對象空間。這種壓縮操作有兩個目的 : 避免出現內存片段,允許垃圾回收器在分配新對象時始終在堆的末尾分配內存。這可避免爲可能非常耗時的任務維護剩餘內存片段的列表。

如果在垃圾回收之後沒有足夠的內存來分配新的對象,操作系統將無法分配更多的內存,這時將拋出 OutOfMemoryException 異常。

垃圾回收包含多種優化技術來減少垃圾回收的時間。

(P443)

最重要的優化是垃圾回收是分代的。

基本上講,垃圾回收器將託管堆分爲三代。剛剛被分配的對象在 Gen 0 裏,在一輪迴收倖存下來的對象在 Gen 1 裏,其他所有對象都在 Gen 2 裏。

CLR 將 Gen 0 部分保持在相對較小的空間內 (在 32 位工作站 CLR 上最大是 16MB ,典型的大小是幾百 KB 到幾 MB) 。當 Gen 0 部分被填滿之後,垃圾回收器引發 Gen 0 的回收,這經常發生。垃圾回收器對 Gen 1 執行相似的內存限制 (Gen 1 扮演着 Gen 2 的緩存角色) ,因此 Gen 1 的回收也相對地快速和頻繁。然後,包括 Gen 2 的完全回收花費更長的時間,發生得不那麼頻繁。

存活週期短的對象非常有效地被垃圾回收器使用。

(P444)

對大於某一限度 (當前是 85000 字節) 的對象,垃圾回收器使用特殊的堆即 “大對象堆” 。這避免了過多的 Gen 0 回收,分配一系列 16MB 的對象也許會在每次分配之後引起一次 Gen 0 的回收。

大對象堆並不是分代的 : 所有對象都按 Gen 2 來處理。

垃圾回收器在回收的時候必定會凍結 (阻止) 執行線程一段時間,這包括 Gen 0 和 Gen 1 回收發生的整個時間。

可以在任何時間通過調用 GC.Collect 方法強制垃圾回收。調用 GC.Collect 方法而沒有參數將發起完全回收。如果傳入一個整數值,只有整數值的那一代將被回收,因此 GC.Collect(0) 只執行一次快速的 Gen 0 回收。

(P445)

總的來說,通過允許垃圾回收器來決定何時回收來獲得最好的性能。強制回收不必要地將 Gen 0 對象提升到 Gen 1 中,這將降低性能,也將影響垃圾回收器的自我調節能力,即垃圾回收器動態調整每一代回收的開始時間,以保證在應用程序執行的時候性能最大化。

(P446)

在 WPF 的主題中,數據綁定是另一個導致內存泄露的常見情況。

忘記計時器也能造成內存泄露。

(P447)

一個很好的準則是如果類中的任何字段被賦值給實現 IDisposable 接口的對象,類也應該實現 IDisposable 接口。

【第13章】

(P452)

可以使用預處理器指令有條件地編譯 C# 中的任何代碼段。預處理器指令是以 C# 符號開頭特殊的編譯器指令。不同於其他 C# 結構體的是,它必須出現在單獨的一行。條件編譯的預處理指令有 #if 、 #else 、 #endif 和 # elif 。

#if 指令表示編譯器將忽略一段代碼,除非定義了特定的符號。可以用 #define 指令或編譯開關來定義一個符號。 #define 指令應用於特定的文件;編譯開關應用於整個程序集。

define 指令必須在文件頂端。

(P453)

#else 語句和 C# 的 else 語句很類似, #elif 等同於 #if 其後的 #else 。

|| 、 && 和 ! 運算符用於執行或、與和非運算。

要在程序集範圍內定義符號,可在編譯時指定 /define 開關。

Visual Studio 在 “項目屬性” 中提供了輸入條件編譯符號的選項。

如果在程序集級別定義了符號,之後想在某些特定文件中取消定義,可使用 #undef 指令。

(P454)

[Conditional] 的另一個好處是條件性檢測在調用方法被編譯時執行,而不是在調用的方法被編譯時。

Conditional 屬性在運行時被忽略,因爲它僅僅是給編譯器的指令而已。

如果需要在運行時動態地啓用或禁用某種功能, Conditional 屬性將毫無用處,而是必須使用基於變量的方法。

(P455)

Debug 和 Trace 是提供基本日誌和斷言功能的靜態類。這兩個類很類似,主要的不同是它們的特定用途。 Debug 類用於調試版本; Trace 類用於調試和發佈版本。

所有 Debug 類的方法都用 [Conditional("DEBUG")] 定義;

所有 Trace 類的方法都用 [Conditional("TRACE)] 定義;

這意味着所有調用標記爲 DEBUG 或 TRACE 的方法都會被編譯器忽略,除非定義了 DEBUG 或 TRACE 符號。默認情況下, Visual Studio 在項目的調試配置中定義了 DEBUG 和 TRACE 符號,同時只在發佈配置中定義了 TRACE 符號。

Debug 和 Trace 類都提供了 Write 、 WriteLine 和 WriteIf 方法。默認情況下,這些方法向調試器的輸出窗口發送消息。

Trace 類也提供了 TraceInformation 、 TraceWarning 和 TraceError 方法。這些方法和 Write 方法在行爲上的不同取決於 TraceListeners 類。

Debug 和 Trace 類都提供了 Fail 和 Assert 方法。

Fail 方法給每一個在 Debug 或 Trace 類的 Listeners 集合中的 TraceListener 發送消息,默認在調試輸出窗口和對話框中顯示消息。

Assert 方法在布爾參數爲 false 時僅僅調用 Fail 方法,這叫做使用斷言。指定錯誤消息也是可選的。

Write 、 Fail 和 Assert 方法也被重載來接受字符串類型的額外信息,這在處理輸出時很有用。

(P456)

Debug 和 Trace 類都有 Listeners 屬性,包含了 TraceListener 實例的靜態集合。它們負責處理由 Write 、 Fail 和 Trace 方法發起的內容。

(P457)

對於 Windows 事件日誌,通過 Wirte 、 Fail 或 Assert 方法輸出的消息在 Windows 事件查看器中總是顯示爲 “消息” 。但是,通過 TraceWarning 和 TraceError 方法輸出的消息,則顯示爲 “警告” 或 “錯誤” 。

Trace 和 Debug 類提供了靜態的 Close 和 Flush 方法來調用所有監聽器的 Close 和 Flush 方法 (依次調用它所屬的編寫器和流的 Close 或 Flush 方法) 。 Close 方法隱式地調用 Flush 方法,關閉文件句柄,防止數據進一步被寫入。

作爲一般的規則,要在應用程序結束前調用 Close 方法,隨時調用 Flush 方法來保證當前的消息數據被寫入。這適用於使用流或基於文件的監聽器。

(P458)

Trace 和 Debug 類也提供了 AutoFlush 屬性,如果它爲 true ,則在每條消息之後強制執行 Flush 方法。

如果使用任何文件或基於流的監聽器,將 AutoFlush 設爲 true 是很好的方法。否則,如果任何未處理的異常或關鍵的錯誤發生,最後 4KB 的診斷信息也許會丟失。

Framework 4.0 提供了叫做 “代碼契約” 的新特性,用統一的系統代替了這些方法。這種系統不但支持簡單的斷言,也支持更加強大的基於契約的斷言。

代碼契約由 Eiffel 編程語言中的契約式設計原則而來,函數之間通過相互有義務和好處的系統進行交互。本質上講,客戶端 (調用方) 必須滿足函數指定的先決條件和保證當函數返回時客戶端能夠依賴的後置條件。

代碼契約的類型存在於 System.Diagnostics.Contracts 命名空間中。

先決條件由 Contract.Requires 定義,它在方法開始時被驗證。後置條件由 Contract.Ensures 定義,它並不在它出現的地方被驗證,而是當方法結束時被驗證。

(P459)

先決條件和後置條件必須出現在方法的開始。優點是如果沒有在按順序編寫的方法中實現契約,錯誤就會被檢測出來。

代碼契約的另一個限制是不能用它們來執行安全性檢查,因爲它們在運行時被規避 (通過處理 ContractFailed 事件) 。

代碼契約由先決條件、後置條件、斷言和對象不變式組成。這些都是可發現的斷言。不同之處是它們何時被驗證 :

  1. 先決條件在函數開始時被驗證;

  2. 後置條件在函數結束之前被驗證;

  3. 斷言在它出現的地方被驗證;

  4. 對象不變式在每個類中的公有函數之後被驗證;

(P460)

代碼契約完全通過調用 Contract 類中的 (靜態) 方法來定義,這與契約語言無關。

契約不僅在方法中出現,也可以在其他函數中出現,例如構造方法、屬性、索引器和運算符。

(P465)

無論重寫的方法是否調用了基方法,二進制重寫器能保證基方法的先決條件總是在子類中被執行。

(P467)

以下兩個原因使 Contract.Assert 比 Debug.Assert 更受歡迎 :

  1. 通過代碼契約提供的失敗處理機制能獲得更多的靈活性;

  2. 靜態檢測工具能嘗試驗證 Contract.Asserts ;

(P473)

DbgCLR 是 Visual Studio 中的調試器,和 .NET Framework SDK 一起免費下載,它是當沒有 IDE 時最簡單的調試選擇,儘管必須下載整個 SDK 。

(P474)

Process.GetProcessXXX 方法通過名稱或進程 ID 檢索指定進程,或檢索所有運行在當前或指定名稱計算機中的進程,包括所有託管和非託管的進程。每一個 Process 實例都有很多屬性映射到各種統計數據上,例如名稱、 ID 、優先級、內存和處理器利用率、窗口句柄等。

Process.GetCurrentProcess 方法返回當前的進程。如果創建了額外的應用程序域,它們將共享同一個進程。

可以通過調用 Kill 方法來終止一個進程。

(P475)

也可以用 Process.Threads 屬性遍歷其他進程的所有線程。然而,獲得的對象並不是 System.Threading.Thread 對象,而是 ProcessThread 對象,它用於管理而不是同步任務。

ProcessThread 對象提供了潛在線程的診斷信息,並允許控制它的一些屬性,例如優先級和處理器親和度。

(P476)

Exception 已經有 StackTrace 屬性,但是這個屬性返回的是簡單的字符串而不是 StackTrace 對象。

如果註冊了 EventLogTraceListener 類,之前使用的 Debug 和 Trace 類可以寫入 Windows 事件日誌。但是,可以使用 EventLog 類直接寫入 Windows 事件日誌而不使用 Trace 或 Debug 類。也可以使用這個類來讀取和監視事件數據。

寫入事件日誌對 Windows 服務應用程序來說很有意義,因爲如果出錯了,不能彈出用戶界面來提供給用戶一些包含診斷信息的特殊文件。也因爲 Windows 服務通常都寫入 Windows 事件日誌,如果服務出現問題, Windows 事件日誌幾乎是管理員首先要查看的地方。

(P477)

有三種標準的 Windows 事件日誌,按名稱分類 :

  1. 應用程序;

  2. 系統;

  3. 安全;

應用程序日誌是大多數應用程序通常寫入的地方。

要寫入 Windows 事件日誌 :

  1. 選擇三種事件日誌中的一種 (通常是應用程序日誌) ;

  2. 決定源名稱,必要時創建;

  3. 用日誌名稱、源名稱和消息數據來調用 EventLog.WriteEntry 方法;

源名稱使應用程序更容易分類。必須在使用它之前註冊源名稱,使用 CreateEventSource 方法可以實現這個功能,之後可以調用 WriteEntry 方法。

EventLogEntryType 可以是 Information 、 Warning 、 Error 、 SuccessAudit 或 FailureAudit 。

每一個在 Windows 事件查看器中都顯示不同的圖標。

CreateEventSource 也允許指定計算機名 : 這可以寫入其他計算機的事件日誌,如果有足夠的權限。

要讀取事件日誌,用想訪問的日誌名來實例化 EventLog 類,並選擇性地使用日誌存在的其他計算機名。每一個日誌項目能夠通過 Entries 集合屬性來讀取。

(P478)

可以通過靜態方法 EventLog.GetEventLogs 來遍歷當前 (或其他) 計算機上的所有日誌 (這需要管理員權限) 。通常這至少會打印應用程序日誌、安全日誌和系統日誌。

通過 EntryWritten 事件,一條項目被寫入到 Windows 事件日誌時,將獲得通知。對工作在本機的事件日誌,無論什麼應用程序記錄日誌都會被觸發。

要開啓日誌監視 :

  1. 實例化 EventLog 並設置它的 EnableRaisingEvents 屬性爲 true ;

  2. 處理 EntryWritten 事件;

(P483)

Stopwatch 類提供了一種方便的機制來衡量執行時間。Stopwatch 使用了操作系統和硬件提供的最高分辨率機制,通常少於 1ms (對比一下, DateTime.Now 和 Environment.TickCount 有大約 15ms 的分辨率) 。

要使用 Stopwatch 調用 StartNew() 方法,它實例化 Stopwatch 對象並開始計時 (換句話說,可以手動實例化並在之後調用 Start 方法) 。 Elapsed 返回表示過去的時間間隔的 TimeSpan 對象。

Stopwatch 也公開了 ElapsedTicks 屬性,它返回表示過去時間的 long 類型的數字。要將時間轉換成秒,請除以 Stopwatch.Frequency 。 Stopwatch 也有 ElapsedMilliseconds 屬性,這通常是最方便的。

調用 Stop 方法將終止 Elapsed 和 ElapsedTicks 。運行的 Stopwatch 並不會引起任何後臺活動,因此調用 Stop 方法是可選的。

【第14章】

(P484)

程序併發執行代碼的通用機制是多線程 (multithreading) 。 CLR 和操作系統都支持多線程,它是一種基礎併發概念。因此,最基本的要求是理解線程的基本概念,特別是線程的共享狀態。

(P485)

線程 (thread) 是一個獨立處理的執行路徑。

每一個線程都運行在一個操作系統進程中,這個進程是程序執行的獨立環境。在單線程 (single-threaded) 程序中,在進程的獨立環境中只有一個線程運行,所以該線程具有獨立使用進程資源的權利。

在多線程 (multi-threaded) 程序中,在進程中有多個線程運行,它們共享同一個執行環境 (特別是內存) 。這在一定程度上反映了多線程處理的作用 : 例如,一個線程在後臺獲取數據,同時另一個線程顯示所獲得的數據,這些數據就是所謂的共享狀態 (shared state) 。

Windows Metro 配置文件不允許直接創建和啓動線程;相反,必須通過任務來操作線程。任務增加了間接創建線程的方法,這種方法增加了學習複雜性,所以最好從控制檯應用程序開始,熟悉它們的使用方法,然後再直接創建線程。

客戶端程序 (Console 、 WPF 、 Metro 或 Windows 窗體) 都從操作系統自動創建一個線程 (主線程) 開始。除非創建更多的線程 (直接或間接) ,否則這就是單線程應用程序的運行環境。

實例化一個 Thread 對象,然後調用它的 Start 方法,就可以創建和啓動一個新的線程。

最簡單的 Thread 構造方法接受一個 ThreadStart 代理 : 一個無參數方法,表示執行開始位置。

在單核計算機上,操作系統會給每一個線程分配一些 “時間片” (Windows 一般爲 20 毫秒) ,用於模擬併發性,因此這段代碼會出現連續的 x 和 y 。在 多核 / 多處理器 主機上執行時,雖然這個例子仍然會出現重複的 x 和 y (受控制檯處理併發請求的機制影響) ,但是線程卻能夠真正實現並行執行 (分別由計算機上其他激活處理器完成) 。

(P486)

線程被認爲是優先佔用 (preempted) 它在執行過程與其他線程代碼交叉執行的位置。這個術語通常可以解釋出現的問題。

在線程啓動之後,線程的 IsAlive 屬性就會變成 true ,直到線程停止。當 Thread 的構造函數接收的代理執行完畢時,線程就會停止。在停止之後,線程無法再次啓發。

每個線程都有一個 Name 屬性,它用於調試程序。它在 Visual Studio 中特別有用,因爲線程的名稱會顯示在 Threads 窗口和 Debug Location 工具欄上。線程名稱只能設置一次;修改線程名稱會拋出異常。

靜態屬性 Thread.CurrentThread 可以返回當前執行的線程。

在等待另一個線程結束時,可以調用另一個線程的 Join 方法。

Thread.Sleep 會將當前線程暫停執行一定的時間。

(P487)

調用 Thread.Sleep(0) ,會馬上放棄線程的當前時間片,自動將 CPU 交給其他線程。

Thread.Yield() 方法也有相同的效果,但是它只會將資源交給在同一個處理器上運行的線程。

有時候,在生產代碼中使用 Sleep(0) 或 Yield ,可以優化性能。它還是一種很好的診斷工具,可以幫助開發者發現線程安全問題 : 如果在代碼任意位置插入 Thread.Yield() 會破壞程序,那麼代碼肯定存在 Bug 。

在等待線程 Sleep 或 Join 的過程中,還可以阻塞線程。

線程阻塞是指線程由於特定原因暫停執行,如 Sleeping 或執行 Join 後等待另一個線程停止。阻塞的線程會立刻交出 (yield) 它的處理器時間片,然後從這時開始不再消耗處理器時間,直至阻塞條件結束。使用線程的 ThreadState 屬性,可以測試線程的阻塞狀態。

ThreadState 是一個標記枚舉量,它由三 “層” 二進制位數據組成。

ThreadState 屬性可用於診斷程序,但是不適用於實現同步,因爲線程狀態可能在測試 ThreadState 和獲取這個信息的時間段內發生變化。

當線程阻塞或未阻塞時,操作系統會執行環境切換 (context switch) 。這個操作會稍微增加負載,幅度一般在 1~2 毫秒左右。

如果一個操作將大部分時間用於等待一個條件的發生,那麼它就稱爲 I / O 密集 (I / O - bound) 操作。

I / O 密集操作一般都會涉及輸入或輸出,但是這不是硬性要求 : Thread.Sleep 也是一種 I / O 密集操作。

如果一個操作將大部分時間用於執行 CPU 密集操作,那麼它就稱爲計算密集 (compute-bound) 操作。

I / O 密集操作可以以兩種方式執行 : 同步等待當前線程的操作完成 (如 Console.ReadLine 、Thread.Sleep 或 Thread.Join) ,或者異步執行,然後在將來操作完成時觸發一個回調函數。

異步等待的 I / O 密集操作會將大部分時間花費在線程阻塞上。它們也可能在一個定期循環中自旋。

(P488)

自旋與阻塞有一些細微差別。首先,非常短暫的自旋可能非常適用於設置很快能滿足的條件 (也許是幾毫秒之內) ,因爲它可以避免過載和環境切換延遲。

CLR 會給每一個線程分配獨立的內存堆,從而保證本地變量的隔離。

如果線程擁有同一個對象實例的通用引用,那麼這些線程就共享相同的數據。

(P489)

編譯器會將 Lambda 表達式或匿名代理捕獲的局部變量轉換爲域,所以它們也可以共享。

靜態域是在線程之間共享數據的另一種方法。

(P490)

當兩個線程同時爭奪一個鎖時 (它可以是任意引用類型的對象,這裏是 _locker) ,其中一個線程會等待 (或阻塞) ,直到鎖釋放。這個例子保證一次只有一個線程能夠進入它的代碼塊,因此 “Done” 只打印一次。在複雜的多線程環境中,採用這種方式來保護的代碼就是具有線程安全性 (thread-safe) 。

鎖並不是解決線程安全的萬能法寶 —— 人們很容易在訪問域時忘記鎖,而且鎖本身也存在一些問題 (如死鎖) 。

(P491)

ParameterizedThreadStart 的侷限性在於 : 它只接受一個參數。而且因爲參數屬於類型 object ,所以它通常需要進行強制轉換。

Lambda 表達式是向線程傳遞數據的最方便且最強大的方法。

(P492)

在線程創建時任何生效的 try / catch / finally 語句塊開始執行後都與線程無關。

(P493)

在運行環境中,應用程序的所有線程入口方法都需要添加一個異常處理方法 —— 就和主線程一樣 (通常位於更高一級的執行堆棧中) 。

默認情況下,顯示創建的線程都是前臺線程 (foreground thread) 。無論是否還有後臺線程 (background thread) 運行,只要有一個前臺線程仍在運行,整個應用程序就會保持運行狀態。當所有前臺線程結束時,應用程序就會停止,而且所有仍在運行的後臺線程也會隨之中止。

線程的 前臺 / 後臺 狀態與線程的優先級 (執行時間分配) 無關。

使用線程的 IsBackground 屬性,可以查詢或修改線程的後臺狀態。

(P494)

線程的 Priority 屬性可以確定它與其他激活線程在操作系統中的相對執行時間長短。

如果同時激活多個線程,優先級就會變得很重要。提高一個線程的優先級時,要注意不要過度搶佔其他線程的執行時間。如果希望一個線程擁有比其他進程的線程更高級的優先級,那麼還必須使用 System.Diagnostics 的 Process 類,提高進程本身的優先級。

這種方法非常適合於一些工作量較少但要求較低延遲時間 (能夠快速響應) 的 UI 進程中。在計算密集特別是帶有用戶界面的應用程序中,提高進程優先級可能會搶佔其他進程的執行時間,從而影響整個計算機的運行速度。

有時候,一個線程需要等待來自其他線程的通知,這就是所謂的發送信號 (singaling) 。最簡單的發送信號結構是 ManualResetEvent 。在一個 ManualResetEvent 上調用 WaitOne ,可以阻塞當前線程,使之一直等待另一個線程通過調用 Set “打開” 信號。

(P495)

在調用 Set 之後,信號仍然保持打開;調用 Reset ,就可以再次將它關閉。 ManualResetEvent 是 CLR 提供的多個信號發送結構之一。

(P496)

System.ComponentModel 命名空間中有一個抽象類 SynchronizationContext ,它實現了編程編列一般化。

WPF 、 Metro 和 Windows 窗體都定義和實例化了 SynchronizationContext 的子類,當運行在 UI 線程上時,它可以通過靜態屬性 SynchronizationContext.Current 獲得。捕獲這個屬性,將來就可以在工作者線程上提交數據到 UI 控件。

(P497)

SynchronizationContext 還有一個專門用在 ASP.NET 的子類,它這時作爲一個更微妙的角色,保證按照異步操作方式處理頁面處理事件,並且保留 HttpContext 。

在 Dispatcher 或 Control 上調用 Post 與調用 BeginInvoke 的效果相同;另外 Send 方法與 Invoke 的效果相同。

Framework 2.0 引入了 BackgroundWorker 類,它使用 SynchronizationContext 類簡化富客戶端應用程序的工作者線程。BackgroundWorker 增加了相同的 Tasks 和異步功能,它也使用 SynchronizationContext 。

無論何時啓動一個線程,都需要一定時間 (幾百毫秒) 用於創建新的局部變量堆。線程池 (thread pool) 預先創建了一組可回收線程,因此可以縮短這段過載時間。要實現高效的並行編程和細緻的併發性,必須使用線程池;它可用於運行一些短暫操作,而不會受到線程啓動過載的影響。

在使用線程池中的線程 (池化線程) 時,還需要考慮下面這些問題 :

  1. 由於不能設置池化線程的 Name ,因此會增加代碼調試難度;

  2. 池化線程通常都是後臺線程;

  3. 池化線程阻塞會影響性能;

池化線程的優先級可以隨意修改 —— 在釋放回線程池時,優先級會恢復爲普通級別。

使用屬性 Thread.CurrentThread.IsThreadPoolThread ,可以確定當前是否運行在一個池化線程上。

在池化線程上運行代碼的最簡單方法是使用 Task.Run 。

(P498)

由於 Framework 4.0 之前不支持任務,所以可以改爲調用 ThreadPool.QueueUserWorkItem 。

(P498)

使用線程池的情況有 :

  1. WCF 、 遠程處理 (Remoting) 、 ASP.NET 和 ASMX Web Services 應用服務器;

  2. System.Timers.Timer 和 System.Threading.Timer;

  3. 並行編程結構;

  4. BackgroundWorker 類 (現在是多餘的) ;

  5. 異步代理 (現在是多餘的) ;

線程池還有另一個功能,即保證計算密集作業的臨時過載不會引起 CPU 超負荷 (oversubscription) 。

超負荷是指激活的線程數量多於 CPU 內核數量,因此操作系統必須按時間片執行線程調度。超負荷會影響性能,因爲劃分時間片需要大量的上下文切換開銷,並且可能使 CPU 緩存失效,而這是現代處理器實高性能的必要條件。

CLR 能夠將任務進行排序,並且控制任務啓動數量,從而避免線程池超負荷。它首先運行與硬件內核數量一樣多的併發任務,然後通過爬山算法調整併發數量,在一個方向上不停調整工作負荷。如果吞吐量提升,那麼它會在這個方向上繼續調整 (否則切換到另一個方向) 。這樣就保證能夠發現最優性能曲線 —— 即使是計算機上同時發生的活動。

如果滿足以下兩個條件,則適合使用 CLR 的策略 :

  1. 大多數工作項目的運行時間都非常短 (小於 250ms ,最理想情況是小於 100ms) ,這樣 CLR 就有大量的機會可以測量和調整;

  2. 線程池不會出現大量將大部分時間都浪費在阻塞上的作業;

阻塞是很麻煩的,因爲它會讓 CLR 錯誤地認爲它佔用了大量的 CPU 。 CLR 能夠檢測並補償 (往池中注入更多的線程) ,但是這可能使線程池受到超負荷的影響。此外,這樣也會增加延遲,因爲 CLR 會限制注入新線程的速度,特別是應用程序生命週期的前期 (在客戶端操作系統上更嚴重,因爲它有嚴格的低資源消耗要求) 。

如果想要提高 CPU 的利用率,那麼一定要保持線程池的整潔性。

線程是創建併發的底層工具,因此它具有一定的侷限性。特別是 :

  1. 雖然很容易向啓動的線程傳入數據,但是並沒有簡單的方法可以從聯合 (Join) 線程得到 “返回值” 。因此,必須創建一些共享域。當操作拋出一個異常時,捕捉和處理異常也是非常麻煩的;

  2. 當線程完成之後,無法再次啓動該線程;相反,只能夠聯合 (Join) 它 (在進程中阻塞當前線程) 。

(P499)

這些侷限性會影響併發性的實現;換而言之,不容易通過組合較小的併發操作實現較大的併發操作 (這對於異步編程而言非常重要) 。因此,這會增加對手工同步處理 (加鎖、發送信號) 的依賴,而且很容易出現問題。

直接使用線程會對性能產生影響。而且,如果需要運行大量併發 I / O 密集操作,那麼基於線程的方法僅僅在線程過載方面就會消耗大量的內存。

Task 類可以解決所有這些問題。與線程相比, Task 是一個更高級的抽象概念,它表示一個通過或不通過線程實現的併發操作。任務是可組合的 (compositional) —— 使用延續 (continuation) 將它們串聯在一起。它們可以使用線程池減少啓動延遲,而且它們可以通過 TaskCompletionSource 使用回調方法,避免多個線程同時等待 I / O 密集操作。

Task 類型是 Framework 4.0 引入的,作爲並行編程庫的組成部分。然後,它們後來 (通過使用等待者 awaiter) 進行了很多改進,從而在常見併發場景中發揮越來越大的作用,並且也是 C# 5.0 異步功能的基礎類型。

從 Framework 4.5 開始,啓動一個由後臺線程實現的 Task ,最簡單的方法是使用靜態方法 Task.Run (Task 類似於 System.Threading.Tasks 命名空間) 。調用時只需要傳入一個 Action 代理。

Task.Run 是 Framework 4.5 新引入的方法。在 Framework 4.0 中,調用 Task.Factory.StartNew ,可以實現相同的效果。前者相當於是後者的快捷方式。

Task 默認使用池化線程,它們都是後臺線程。這意味着當主線程結束時,所有任務也會隨之停止。因此,要在控制檯應用程序中運行這些例子,必須在啓動任務之後阻塞主線程。例如,掛起 (Waiting) 該任務,或者調用 Console.ReadLine 。

(P500)

Task.Run 會返回一個 Task 對象,它可用於監控任務執行過程,這一點與 Thread 對象不同。

注意這裏沒有調用 Start ,因爲 Task.Run 創建的是 “熱” 任務;相反,如果想要創建 “冷” 任務,則必須使用 Task 的構造函數,但是這種用法在實踐中很少使用。

任務的 Status 屬性可用於跟蹤任務的執行狀態。

調用任務的 Wait 方法,可以阻塞任務,直至任務完成,其效果等同於調用線程的 Join 。

可以在 Wait 中指定一個超時時間和一個取消令牌 (用於提前中止停止等待狀態) 。

在默認情況下, CLR 會運行在池化線程上,這種線程非常適合執行短計算密集作業。如果要執行長阻塞操作,則可以按以下方式避免使用池化線程。

在池化線程上運行一個長任務問題並不大;但是如果要同時運行多個長任務 (特別是會阻塞的任務) ,則會對性能產生影響。在這種情況下,通常更好的方法是使用 TrackCreationOptions.LongRunning :

  1. 如果是運行 I / O 密集任務,則可以使用 TaskCompletionSource 和異步操作 (asynchronous functions) ,通過回調函數 (延續) 實現併發性,而不通過線程實現;

  2. 如果是運行計算密集任務,則可以使用一個 生產者 / 消費者 隊列,控制這些任務的併發數量,避免出現線程和進程阻塞的問題;

Task 有一個泛型子類 Task<TResult> ,它允許任務返回一個值。調用 Task.Run ,傳入一個 Func<TResult> 代理 (或者兼容的 Lambda 表達式) , 代替 Action ,就可以獲得一個 Task<TResult> 。

然後,查詢 Result 屬性,就可以獲得結果。如果任務還沒有完成,那麼訪問這個屬性會阻塞當前線程,直至任務完成。

(P501)

Task<TResult> 可以看作是 “將來” ,其中封裝了後面很快生效的 Result 。

有趣的是,當 Task 和 Task <TResult> 第一次出現在早期的 CTP 時,後者實際上是 Future<TResult> 。

與線程不同,任務可以隨時拋出異常。所以,如果任務中的代碼拋出一個未處理異常 (換而言之,任務出錯) , 那麼這個異常會自動傳遞到調用 Wait() 的任務上或者訪問 Task<TResult> 的 Result 屬性的代碼上。

使用的 Task 的 IsFaulted 和 IsCanceled 屬性,就可以不重新拋出異常而檢測出錯的任務。如果這兩個屬性都返回 false ,則表示沒有錯誤發生;如果 IsCanceld 爲 true ,則任務拋出了 OperationCanceledOperation ;如果 IsFaulted 爲 true , 則任務拋出了另一種異常,而 Exception 屬性包含了該錯誤。

如果使用了自主的 “設置後忘記的” 任務 (不通過 Wait() 或 Result 控制的任務,或者實現相同效果的延續) ,那麼最好在任務代碼中顯式聲明異常處理,避免出現靜默錯誤,就像線程的異常處理一樣。

自主任務上的未處理異常稱爲未監控異常 (unobserved exception) ,在 CLR 4.0 中,它們實際上會中止程序 (當任務跳出運行範圍並被垃圾回收器回收時, CLR 會在終結線程上重新拋出異常) 。這種方式有利於提醒一些悄悄發生的問題;然而,錯誤發生時間可能並不準確,因爲垃圾回收器可能會明顯滯後於發生問題的任務。因此,在發現這種行爲具有複雜的不同步性模式時 , CLR 4.5 刪除了這個特性。

如果異常僅僅表示無法獲得一些不重要的結果,那麼忽略異常是最好的處理方式。

如果異常反映了程序的重大缺陷,那麼忽略異常是很有問題。這其中的原因有兩個 :

  1. 這個缺陷可能使程序處於無效狀態;

  2. 這個缺陷可能導致更多的異常發生,而且無法記錄初始錯誤也會增加診斷難度;

使用靜態事件 TaskScheduler.UnobservedTaskException ,可以在全局範圍訂閱未監控的異常;處理這個事件,然後記錄發生的錯誤,是一個很好的異常處理方法。

未監控異常有一些有趣的細微差別 :

  1. 如果在超時週期之後發生錯誤,那麼等待超時的任務將生成一個未監控異常;

  2. 在錯誤發生之後檢查任務的 Exception 屬性,會使異常變成 “已監控異常” ;

延續 (continuation) 會告訴任務在完成後繼續執行下面的操作。延續通常由一個回調方法實現,它會在操作完成之後執行一次。給一個任務附加延續的方法有兩種。第一種方法是 Framework 4.5 新增加的,它非常重要,因爲 C# 5.0 的異步功能使用了這種方法。

調用 GetAwaiter 會返回一個等待者 (awaiter) 對象,它的方法會讓先導 (antecedent) 任務 (primeNumberTask) 在完成 (或出錯) 之後執行一個代理。已經完成的任務也可以附加一個延續,這時延續就馬上執行。

等待者 (awaiter) 可以是任意對象,但是它必須包含前面所示兩個方法 (OnCompleted 和 GetResult) 和一個 Boolean 類型屬性 IsCompleted 的對象,它不需要實現包含所有這些成員的特定接口或繼承特定基類 (但是 OnCompleted 屬性接口 INotifyCompletion) 。

(P503)

如果先導任務出現錯誤,那麼當延續代碼調用 awaiter.GetResult() 時就會重新拋出異常。我們不需要調用 GetResult ,而是直接訪問先導任務的 Result 屬性。調用 GetResult 的好處是,當先導任務出現錯誤時,異常可以直接拋出,而不會封裝在 AggregateException 之中,從而可以實現更簡單且更清晰的異常捕捉代碼。

對於非泛型任務,GetResult() 會返回空值 (void) ,然後它的實用函數會單獨重新拋出異常。

如果出現同步上下文,那麼會自動捕捉它,然後將延續提交到這個上下文中。這對於富客戶端應用程序而言非常實用,因爲會將延續彈回 UI 線程。然而,在編寫庫時,通常不採用這種方法,因爲開銷相對較大的 UI 線程只會在離開庫時運行一次,而不會在方法調用期間運行。

如果不出現同步上下文或者使用 ConfigureAwait(false) ,那麼通常延續會運行在先導任務所在的線程上,從而避免不必要的過載。

ContinueWith 本身會返回一個 Task ,它非常適用於添加更多的延續。然而,如果任務出現錯誤,我們必須直接處理 AggregateException ,然後編寫額外代碼,將延續編列到 UI 應用程序中。而在非 UI 下文中,如果想要讓延續運行在同一個線程上,則必須指定 TaskContinuationOptions.ExecuteSynchronously ;否則它會彈回線程池。 ContinueWith 特別適用於並行編程場景。

TaskCompletionSource 可以創建一個任務,它不包含任何必須在後面啓動和結束的操作。它的實現原理是提供一個可以手工操作的 “附屬” 任務 —— 用於指定操作完成或出錯的時間。這種方法非常適合於 I / O 密集作業 : 可以利用所有任務的優點 (它們能夠生成返回值、異常和延續) ,但不會在操作執行期間阻塞線程。

TaskCompletionSource 用法很簡單、直接初始化就可以。它包含一個 Task 屬性,它返回一個可以等待和附加延續的任務 —— 和其他任務一樣。然而,這個任務完全通過下面的方法由 TaskCompletionSource 對象進行控制。

(P504)

調用這些方法,就可以給任務發送信號,將任務修改爲完成、異常或取消狀態。這些方法都只能調用一次 : 如果多次調用 SetResult 、 SetException 或 SetCanceled ,它們就會拋出異常,而 Try * 等方法則會返回 false 。

TaskCompletionSource 的真正作用是創建一個不綁定線程的任務。

(P505)

Delay 方法非常實用,因此它成爲 Task 類的一個靜態方法。

Task.Delay 是 Thread.Sleep 的異步版本。

(P506)

同步操作 (synchronous operation) 在返回調用者之前才完成它的工作。

在大多數情況下,異步操作 (asynchronous operation) 則在返回調用者之後才完成它的工作。

異步方法使用頻率較小,並且需要初始化併發編程,因爲它的作業會繼續與調用者並行處理。

異步方法一般會快速 (或立刻) 返回給調用者;因此,它們也稱爲非阻塞方法。

到目前爲止,我們學習的異步方法都可以認爲是通用方法 :

  1. Thread.Start ;

  2. Task.Run ;

  3. 給任務附加延續的方法;

異步編程的原則是以異步方式編寫運行時間很長 (或可能很長) 的函數。這與編寫長運行時間函數的傳統同步方法相反,它會在一個新線程或任務上調用這些函數,從而實現所需要的併發性。

異步方法的不同點是它會在長運行時間函數之中而非在函數之外初始化併發性。這樣做有兩個優點 :

  1. I / O 密集併發性的實現不需要綁定線程,因此可以提高可伸縮性和效率;

  2. 富客戶端應用程序可以減少工作者線程的代碼,因此可以簡化線程的安全實現;

在傳統的同步調用圖中,如果圖中出現一個運行時間很長的操作,我們就必須將整個調用圖轉移到一個工作者線程中,以保證 UI 的高速響應。因此,我們最終會得到一個跨越許多方法的併發操作 (過程級併發性) ,而且這時需要考慮圖中每一個方法的線程安全性。

使用異步調用圖,就可以在真正需要時才啓動線程,因此可以降低調用圖中線程的使用頻率 (或者在特定操作中完全不需要使用線程,如 I / O 密集操作) 。其他方法則可以在 UI 線程上運行,從而可以大大簡化線程安全性的實現。

(P507)

Metro 和 Silverlight .NET 鼓勵使用異步編程,甚至一些運行時間較長的方法完全不會出現同步執行版本。相反,它們使用一些可以返回任務 (或者可以通過擴展方法轉換爲任務的對象) 的異步方法。

任務非常適合異步編程,因爲它們支持異步編程所需要的延續。編寫 Delay 時使用了 TaskCompletionSource ,它是一種實現 “底層” I / O 密集異步方法的標準方法。

(P509)

如果不想增加程序複雜性,那麼必須使用 async 和 await 關鍵字實現異步性。

(P510)

C# 5.0 引入了 async 和 await 關鍵字。這兩個關鍵字可用於編寫異步代碼,它具有與同步代碼相當的結構和簡單性,並且摒棄了異步編程的複雜結構。

爲了完成編譯,我們必須在包含的方法上添加 async 修飾符。

(P511)

修飾符 async 會指示編譯器將 await 視爲一個關鍵字,而非在方法中隨意添加的修飾符 (這樣可以保證 C# 5.0 之前編寫並使用 await 作爲修飾符的代碼不會出現編譯錯誤) 。

async 修飾符只能應用到返回 void 、 Task 或 Task <TResult> 的方法 (和 lambda 表達式) 上。

添加 async 修飾符的方法就是所謂的異步函數,因爲它們通常本身也是異步的。

await 表達式的最大特點在於它們可以出現在代碼的任意位置。具體地, await 表達式可以出現在異步方法中除 catch 或 finally 語句塊、 lock 表達式、 unsafe 上下文或可執行入口 (Main 方法) 之外的任意位置。

(P513)

直接併發的代碼要避免訪問共享狀態或 UI 控件。

(P514)

在 C# 5.0 之前,異步編程很難實現,原因不僅僅在缺少語言支持,還因爲 .NET 框架是通過 EAP 和 APM 等模式實現異步功能,而非通過任務返回方法。

(P515)

在調用圖上層啓動工作者線程是很冒險的做法。

如果使用異步函數,則可以將返回類型 void 修改爲 Task ,使方法本身適合採用異步實現 (即可等待的) ,其他方面都不需要修改。

注意,方法體內不需要顯式返回一個任務。編譯器會負責生成任務,它會在方法完成或者出現未處理異常時發出信號。這樣就很容易創建異步調用鏈。

編譯器會擴展異步函數,它會將任務返回給使用 TaskCompletionSource 的代碼,用於創建任務,然後再發送信號或異常中止。

(P516)

當一個返回任務的異步方法結束時,執行過程會返回等待它的程序 (通過一個延續) 。

如果方法體返回 TResult ,則可以返回一個 Task<TResult> 。

(P517)

使用 C# 異步函數進行程序設計的基本原則 :

  1. 以同步方式編寫方法;

  2. 使用異步方法調用替換同步方法,然後等待它們;

  3. 除了 “最頂級的” 方法 (一般是 UI 控件的事件處理器) ,將異步方法的返回類型修改爲 Task 或 Task<TResult> ,使它們變成可等待的方法;

編譯能夠爲異步函數創建任務,意味着在很大程度上,我們只需要在創建 I / O 密集併發性的底層方法中顯式創建一個 TaskCompletionSource 實例。 (而對於創建計算密集併發性的方法,則可以使用 Task.Run 創建函數) 。

(P519)

只要添加 async 關鍵字、 未命名 (unnamed) 方法 (lambda 表達式和匿名方法) 也一樣可以採用異步方式執行。

在 WinRT 中,與 Task 等價的是 IAsyncAction ,而與 Task<TResult> 等價的是 IAsyncOperation<TResult> (位於 Windows.Foundation 命名空間) 。

這兩個類都可以通過 System.Runtime.WindowsRuntime.dll 程序集的 AsTask 擴展方法轉換爲 Task 或 Task<TResult> 。這個程序集也定義了一個 GetAwaiter 方法,他可以操作 IAsyncAction 和 IAsyncOpera
tion<TResult> 類型,它們可以直接執行等待操作。

(P520)

由於 COM 類型系統的限制, IAsyncOperation<TResult> 並不是基於 IAsyncAction ,它們繼承一個通用基本類型 IAsyncInfo 。

AsTask 方法也有重載方法,可以接受一個取消令牌和一個對象 IProgress<T> 。

AsyncVoidMethodBuilder 會捕捉未處理異常 (在無返回值的異步函數中) ,然後將它們提交到同步上下文中 (如果有) ,以保證觸發全局異常處理事件。

(P521)

注意,在 await 之前或之後拋出異常並沒有任何區別。

(P526)

Framework 4.5 提供了大量返回任務的異步方法,它們都可用於代替 await (主要與 I / O 相關) 。很多方法 (至少有一部分) 採用了一種基於任務的異步模式 (Task-based Asynchronous Pattern , TAP) ,這是到目前爲止最合理的形式。一個 TAP 方法必須 :

  1. 返回一個 “熱” (正在運行的) Task 或 Task<TResult> ;

  2. 擁有 “Async” 後綴 (除了一些特殊情況) ;

  3. 如果支持取消 和 / 或 進度報告,重載後可接受取消令牌 和 / 或 IProgress<T> ;

  4. 快速返回調用者 (具有一小段初始同步階段) ;

  5. 在 I / O 密集操作中不佔線程 ;

TAP 方法很容易通過 C# 異步函數實現。

(P527)

使用統一協議調用異步函數 (它們都一致返回任務) 的一個優點是,可以使用和編寫任務組合器 (Task Combinator) —— 一些適用於組合各種用途的任務的函數。

CLR 包含兩個任務組合器 : Task.WhenAny 和 Task.WhenAll 。

Task.WhenAny 返回這樣一個任務 : 當任務組中任意一個任務完成,它也就完成。

(P528)

Task.WhenAll 返回這樣一個任務 : 當傳入的所有任務都完成時,它才完成。

(P530)

最老的模式是 APM (Asynchronous Programming Model) ,它使用一對以 “Begin” 和 “End” 開頭的方法,以及一個接口 IAsyncResult 。

基於事件的異步模式 (Event-based Asynchronous Pattern, EAP) 在 Framework 2.0 時引入,它是代替 APM 的更簡單方法,特別是在 UI 場景中。然而,他只能通過有限的類型實現。

EAP 只是一個模式,它並沒有任何輔助類型。

實現 EAP 需要編寫大量的模板代碼,因此這個模式的代碼相當複雜。

(P532)

位於 System.ComponentModel 的 BackgroundWorker 是 EAP 的通用實現。它允許富客戶端應用啓動一個工作者線程,然後執行完成和報告百分比進度,而不需要顯式捕捉同步上下文。

RunWorkerAsync 啓動操作,然後觸發一個池化工作者線程的 DoWork 事件。它還會捕捉同步上下文,而且當操作完成或出錯時, RunWorkerCompleted 事件就會通過同步上下文觸發 (像延續一樣) 。

BackgroundWorker 可以創建過程級併發性,其中 DoWork 事件完全運行在工作者線程上。如果需要在該事件處理器上更新 UI 控件 (而非提交完成百分比進度) ,則必須使用 Dispatcher.BeginInvoke 或類似的方法。

【第15章】

(P533)

System.IO 命名空間中的類型,即底層 I / O 功能的基礎。

.NET Framework 也支持一些更高級的 I / O 功能,形式包括 SQL 連接和命令 、 LINQ to SQL 和 LINQ to XML 、 WCF 、 Web Services 和 Remoting 。

.NET 流體系結構主要包括以下概念 : 後備存儲流、裝飾器流和流適配器。

後備存儲是支持輸入和輸出的終端,例如文件或網絡連接。準確地說,它可以是下面的一種或兩種 :

  1. 支持順序讀取字節的源;

  2. 支持順序寫入字節的目標;

但是,除非對程序員公開,否則後備存儲是無用的。

Stream 正是實現這個功能的標準 .NET 類;它支持標準的讀、寫和尋址方法。與數組不同,流不是直接將所有數據保存到內存中,而是按序列方式處理數據 —— 一次一個字節或一個可管理大小的塊。因此,無論後備存儲的大小如何,流都只佔用很少的內存。

流分成兩類 :

後備存儲流 —— 它們是與特定類型後備存儲硬連接的, 例如 FileStream 或 NetworkStream ;

裝飾器流 —— 它們使用另一種流,以某種方式轉換數據,例如 DeflateStream 或 CryptoStream ;

(P534)

裝飾器流具有以下體系結構優勢 :

  1. 它們能夠釋放用於實現自我壓縮和加密的後備存儲流;

  2. 在裝飾後,流不受接口變化的影響;

  3. 裝飾器支持實時連接;

  4. 裝飾器支持相互連接 (例如,壓縮器後緊跟一個加密器) ;

後備存儲流和裝飾器流都只支持字節。雖然這種方式既靈活又高效,但是應用程序通常採用更高級的方式,例如文本或 XML 。通過在一個類中創建專門支持特定格式的類型化方法,並在這個類中封裝一個流,適配器彌補了這個缺陷。

適配器會封裝一個流,這與裝飾器類似。然而,與裝飾器不同的是,適配器本身不是一個流;它一般會完全隱藏面向字節的方法。

總之,後備存儲流負責處理原始數據;裝飾器流支持透明的二進制轉換。

適配器支持一些處理更高級類型的類型化方法。

爲了構成一個關係鏈,我們只需要將一個對象傳遞給另一個對象的構造函數。

抽象的 Stream 類是所有流的基類。它定義了三種基礎操作的方法和屬性 : 讀取、寫入和查找;以及一些管理任務,例如關閉、清除和配置超時。

(P536)

要實現異步讀或寫,只需要調用 ReadAsync / WriteAsync ,替代 Read / Write ,然後等待表達式。

使用異步方法,不需要捆綁線程就可以輕鬆編寫適應慢速流 (可能是網絡流) 的響應式和可擴展應用。

一個流可能支持只讀、只寫、讀寫。如果 CanWrite 返回 false ,那麼流就是隻讀的;如果 CanRead 返回 false ,那麼流就是隻寫的。

Read 可以將流中的一個數據塊讀取到數組中。它返回接收到的一些字節,字節數一定小於或等於 count 參數。如果它小於 count ,那麼表示已經到達流的結尾,或者流是以小塊方式提供數據的 (通常是網絡流) 。無論是哪一種情況,數組的剩餘字節都是不可寫的,它們之前的值都是保留的。

(P537)

ReadByte 方法簡單一些 : 它每次只讀取一個字節,在流結束時返回 -1 。ReadByte 實際上返回的是一個 int ,而不是 byte ,因爲後者不能爲 -1 。

Write 和 WriteByte 方法都支持將數據發送到流中。當它們無法發送指定的字節時,就會拋出一個異常。

在 Read 和 Write 方法中,參數 offset 指的是緩衝數組中開始讀或寫的索引位置,而不是流中的位置。

如果 CanSeek 返回 true ,那麼表示流是可查找的。在一個可查找的流中 (例如文件流) ,我們可以通過調用 SetLength 查詢或修改它的 Length ,也可以隨時修改正在讀寫的 Position 。 Position 屬性是與流的開始位置相關的;然而,Seek 方法則支持移動到流的當前位置或結束位置。

修改 FileStream 的 Position 屬性一般需要幾毫秒時間。如果要在循環中執行幾百萬次位置修改,那麼 Framework 4.0 中新的 MemoryMappedFile 類可能比 FileStream 更適合。

如果流不支持查找 (例如加密流) ,那麼確定其長度的唯一方法是遍歷整個流。而且,如果需要重新讀取之前的位置,必須先關閉這個流,然後再重新從頭開始讀取。

流在使用完畢之後必須清理,以釋放底層資源,例如文件和套接字句柄。一個保證關閉的簡單方法是在塊中初始化流。通常,流採用以下標準的清理語法 :

  1. Dispose 和 Close 的功能相同;

  2. 重複清除或關閉流不會產生錯誤;

(P538)

關閉一個裝飾流會同時關閉裝飾器及其後備存儲流。在裝飾器系列中,關閉最外層的裝飾器 (系列的頭部) 會關閉整個系列。

有一些流 (例如文件流) 會將數據緩衝到後備存儲並從中取回數據,減少回程,從而提升性能。這意味着寫入到流中的數據不會直接存儲到後備存儲器;而是等到緩衝區填滿時再寫入。Flush 方法可以強制將所有內部緩衝的數據寫入。當流關閉時,Flush 會自動被調用。

如果 CanTimeout 返回 true ,那麼流支持讀寫超時設定。網絡流支持超時設定;文件流和內存流則不支持。對於支持超時設定的流,ReadTimeout 和 WriteTimeout 屬性可用來確定以毫秒爲單位的預期超時時間,其中 0 表示不設定超時。 Read 和 Write 方法會在超時發生時拋出一個異常。

通過 Stream 的靜態 Null 域,能夠獲得一個 “空流” 。

(P539)

FileStream 不適用於 Metro 應用。相反,要轉而使用 Windows.Storage 的 Windows Runtime 類型。

實例化 FileStream 的最簡單方法是使用 File 類的以下靜態方法之一 :

  1. File.OpenRead() // 只讀;

  2. File.OpenWrite() //只寫;

  3. File.Create() // 讀-寫;

如果文件已經存在,那麼 OpenWrite 和 Create 的行爲是不同的。 Create 會截去全部已有的內容; OpenWrite 則會原封不動地保留流中從位置 0 開始的已有內容。如果寫入的字節小於文件已有字節,那麼 OpenWrite 所產生的流會同時保存新舊內容。

我們還可以直接實例化一個 FileStream 。它的構造函數支持所有特性,允許指定文件名或底層文件句柄、文件創建和訪問模式、共享選項、緩衝和安全性。

下面的靜態方法能夠一次性將整個文件讀取到內存中 :

  1. File.ReadAllText (返回一個字符串);

  2. File.ReadAllLines (返回一個字符串數組);

  3. File.ReadAllBytes (返回一個字節數組);

下面的靜態方法能夠一次性寫入一個完整的文件 :

  1. File.WriteAllText ;

  2. File.WriteAllLines ;

  3. File.WriteAllBytes ;

  4. File.AppendAllText (適用於給日誌文件附加內容) ;

從 Framework 4.0 開始,增加了一個靜態方法 File.ReadLines 。這個方法與 ReadAllLines 類似,唯一不同的是它返回一個延後判斷的 IEnumerable<string> 。這個方法效率更高,因爲它不會一次性將整個文件加載到內存中。

(P540)

文件名可以是絕對路徑,也可以是當前目錄的相對路徑。我們可以通過靜態的 Environment.CurrentDirectory 屬性來訪問或修改當前目錄。

當程序啓動時,當前目錄不一定是程序執行文件所在的路徑。因此,一定不要使用當前目錄來定位與可執行文件一起打包的額外運行時文件。

AppDomain.CurrentDomain.BaseDirectory 會返回應用程序根目錄,正常情況下它就是程序可執行文件所在的文件夾。使用 Path.Combine 可以指定相對於這個目錄的文件名。

我們還可以通過 UNC 路徑讀寫一個網絡文件。

FileStream 的所有構造函數接受文件名需要一個 FileMode 枚舉參數。

用 File.Create 和 FileMode.Create 處理隱藏文件會拋出一個異常。必須先刪除隱藏文件再重新創建。

只使用文件名和 FieMode 創建一個 FileStream 會得到 (只有一種異常) 一個可讀寫的流。如果傳入一個 FileAccess 參數,就可以要求降低讀寫模式。

(P541)

FileMode.Append 是最奇怪的一個方法 : 使用這個模式會得到只寫流。相反,要附加讀寫支持,我們使用 FileMode.Open 或 FileMode.OpenOrCreate ,然後再查找流的結尾。

創建 FileStream 時可以選擇的其他參數 :

  1. 一個 FileShare 枚舉值,描述了在完成文件處理之前,可以給同一個文件的其他進程授予的訪問權限 (None 、 Read[default] 、 ReadWrite 或者 Write) ;

  2. 以字節爲單位的內部緩衝區大小 (當前的默認值是 4KB) ;

  3. 一個標記,表示是否由操作系統管理異步 I / O ;

  4. 一個 FileSecurity 對象,描述給新文件分配什麼用戶和角色權限;

  5. 一個 FileOptions 標記枚舉值,包括請求操作系統加密 (Encrypted) 、在臨時文件關閉時自動刪除 (DeleteOnClose) 和優化提示 (RandomAccess 和 SequentialScan) 。此外,還有一個 WriteThrough 標記要求操作系統禁用寫後緩存,適用於事務文件或日誌。

使用 FileShare.ReadWrite 打開一個文件允許其他進程或用戶同時讀寫同一個文件。爲了避免混亂,我們可以使用以下方法在讀或寫文件之前鎖定文件的特定部分 :

public virtual void Lock(long position, long length);

public virtual voio UnLock(long position, long length);

如果所請求的文件段的部分或全部已經被鎖定,那麼 Lock 會拋一個異常。

MemoryStream 使用一個數組作爲後備存儲。這在一定程度是與使用流的目的相違背的,因爲整個後備存儲都必須一次性駐留在內存中。然而, MemoryStream 仍然有一定的用途,一個示例是隨機訪問一個不可查找的流。

(P542)

調用 ToArray 可以將一個 MemoryStream 轉換爲一個字節數組。GetBuffer 方法也可以實現相同操作,而且效率更高,它將返回一個底層存儲數組的直接引用。但是,這個數組通常會比流的實際長度長一些。

MemoryStream 的關閉和清除不是必需的。如果關閉了一個 MemoryStream ,我們就無法再讀或寫這個流,但是我們仍然可以調用 ToArray 來獲得底層數據。消除實際上不會對內存流執行任何操作。

PipeStream 是在 Framework 3.5 引入的。它支持一種簡單的方法,其中一個進程可以通過 Windows 管道協議與另一個進程進行通信。

管道的類型有兩種 :

  1. 匿名管道 —— 支持在同一個 computer.id 的父子進程之間單向通信;

  2. 命名管道 —— 支持同一臺計算機或 Windows 網絡中不同計算機的任意進程之間進行通信;

管道很適合用於在同一臺計算機上進行進程間通信 (IPC) : 它不依賴於任何網絡傳輸,性能更高,也不會有防火牆問題。

管道是基於流實現的,所以一個進程會等待接收字節,而另一個進程則負責發送字節。另一種進程通信方法可以通過共享內存塊實現。

PipeStream 是一個抽象類,它有 4 個實現子類。其中兩個支持匿名管道和兩個支持命名管道 :

  1. 匿名管道 —— AnonymousPipeServerStream 和 AnonymousPipeClientStream ;

  2. 命名管道 —— NamedPipeServerStream 和 NamedPipeClientStream ;

命名管道使用更簡單。

(P543)

管道是一個底層概念,它支持發送和接收字節 (或消息,即字節組) 。

WCF 和 Remoting API 支持使用 IPC 通道進行通信的更高級消息框架。

通過命名管道,各方將使用一個同名管道進行通信。這個協議定義了兩個不同的角色 : 客戶端和服務器。客戶端與服務器之間的通信採用以下方式 :

  1. 服務器初始化一個 NamedPipeServerStream , 然後調用 WaitForConnection ;

  2. 客戶端初始化一個 NamedPipeClientStream , 然後調用 Connect (使用一個可選的超時時間) ;

命名管道流默認是雙向通信的,所以任何一方都可以讀或寫它們的流。這意味着客戶端和服務器都必須同意使用一種協議來協調它們的操作,所以雙方是不能同時發送或接收消息的。

通信雙方還需要統一每次傳輸的數據長度。

爲了支持傳輸更長的消息,管道提供了一種消息傳輸模式。如果啓用這個模式,調用 Read 的一方可以通過檢查 IsMessageComplete 屬性來確定消息是否完成傳輸。

(P544)

只需要等待 Read 返回 0 ,我們就可以確定一個 PipeStream 是否完成消息的讀取。這是因爲,與其他大多數流不同,管道流和網絡流並沒有確定的結尾。相反,它們會在消息傳輸期間臨時中斷。

匿名管道支持在父子進程之間進行單向通信。匿名管道不使用系統級名稱,而是通過一個私有句柄進行調整。

與命名管道一樣,匿名管道也區分客戶端和服務器角色。然而,通信系統有一些不同,它採用以下方法 :

  1. 服務器初始化一個 AnonymousPipeServerStream ,提交一個 In 或 Out 的 PipeDirection ;

  2. 服務器調用 GetClientHandleAsString 獲取管道的標識,然後傳遞迴客戶端 (一般作爲啓動子進程的一個參數) ;

  3. 子進程初始化一個 AnonymousPipeClientStream ,指定相反的 PipeDirection ;

  4. 服務器調用 DisposeLocalCopyOfClientHandle ,釋放第 2 步產生的本地句柄;

  5. 父子進程通過 讀 / 寫 流來進行通信;

因爲匿名管道是單向的,所以服務器必須爲雙向通信創建兩個管道。

(P545)

與命名管道一樣,客戶端和服務器必須協調它們的發送和接收,並且統一每一次傳輸的數據長度。但是,匿名管道不支持消息模式,所以必須實現自己的消息長度認同協議。一種方法是在每次傳輸的前 4 個字節中發送一個整數值,定義後續消息的長度。

BitConverter 類具有一些用於轉換整數和 4 字節數組的方法。

BufferedStream 可以裝飾或包裝另一個具有緩衝功能的流,它是 .NET Framework 的諸多核心裝飾流類型之一。

(P546)

緩衝能夠減少後備存儲的方法,從而提高性能。

組合使用 BufferedStream 和 FileStream 的好處並不明顯,因爲 FileStream 已經有內置的緩衝了。它的唯一用途可能就是擴大一個已有 FileStream 的緩衝區。

關閉一個 BufferedStream 會自動關閉底層的後備存儲流。

Stream 只支持字節處理;要讀寫一些數據類型,例如字符串、整數或 XML 元素,我們必須插入適配器。下面是 Framework 支持的適配器 :

  1. 文本適配器 (處理字符串和字符數據) —— TextReader 、 TextWriter 、 StreamReader 、 StreamWriter 、 StringReader 、 StreamWriter ;

  2. 二進制適配器 (處理基本數據類型,例如 int 、 bool 、 string 和 float) —— BinaryReader 、 BinaryWriter ;

  3. XML 適配器 —— XmlReader 、 XmlWriter ;

(P547)

TextReader 和 TextWriter 都是專門處理字符和字符串的適配器的抽象基類。它們在框架中都是兩個通用的實現 :

  1. StreamReader / StreamWriter —— 使用 Stream 存儲它的原始數據,將流的字節轉換成字符或字符串;

  2. StringReader / StringWriter —— 使用內存字符串實現 TextReader / TextWriter ;

不需要將位置前移,Peek 就可以返回流中的下一個字符。

如果到達流的末尾,那麼 Peek 與不帶參數的 Read 都會返回 -1 ;否則,它們會返回一個能夠強制轉換爲 char 的整數。

接收一個char[] 緩衝區參數的 Read 重載函數功能與 ReadBlock 方法相似。

Windows 的新換行字符是模仿機械打字機的 : 回車符後面加上一個換行符。 C# 字符串表示是 “\r\n” 。如果順序調換,結果可能是兩行,也可能一行也沒有。

WriteLine 會給指定文本附加 CR + LF 。我們可以使用 NewLine 屬性修改這些字符,這對於支持 UNIX 文件格式的互操作性很有用。

和 Stream 一樣,TextReader 和 TextWriter 爲它們的 讀 / 寫 方法提供了基於任務的異步版本。

因爲文本適配器通常與文件有關,所以 File 類也有一些靜態方法支持快捷處理,例如 CreateText 、 AppendText 和 OpenText 。

(P549)

TextReader 和 TextWriter 本身是與流或後備存儲無關的抽象類。然而,類型 StreamReader 和 StreamWriter 都與底層的字節流相關,所以它們必須進行字符和字節之間的轉換。它們是通過 System.Text 命名空間的 Encoding 類進行這些操作的,創建 StreamReader 或 StreamWriter 需要選擇一種編碼方式。如果不進行選擇,那麼就使用默認的 UTF-8 編碼。

如果明確指定一個編碼方式,默認情況下 StreamWriter 會在流開頭寫入一個前綴,用於指定編碼方式。這通常不是一種好做法。

最簡單的編碼方式是 ASCII ,因爲每一個字符都是用一個字節表示的。

ASCII 編碼將 Unicode 字符集的前 127 個字符映射爲一個字節,其中包括鍵盤上的所有字符。

默認的 UTF-8 編碼方式也能夠映射所有分配的 Unicode 字符,但是更復雜一些。它將前 127 個字符編碼爲一個字節,以兼容 ASCII ;其他字符則編碼爲一定數量的字節 (通常是兩個或三個) 。

UTF-8 在處理西方字母時很高效,因爲最常用的字符只需 1 個字節。只需要忽略 127 之後的字節,它就能夠輕鬆向下兼容 ACSII 。缺點是在流中查找是很麻煩的,因爲字符的位置與它在流中的字節位置是無關的。

另一種方式是 UTF-16 (在 Encoding 類中僅僅標記爲 “Unicode”) 。

技術上, UTF-16 使用 2 個或 4 個字節來表示一個字符 (所分配或保護的 Unicode 字符接近一百萬個,所以 2 個字節並不總是足夠的) 。然而,因爲 C# 的 char 類型本身只有 16 位,所以 UTF-16 編碼方式總是使用 2 個字節來表示一個 .NET 的 char 類型。這樣就能夠很容易轉到流中特定的字符索引。

UTF-16 使用 2 個字節前綴來確定字節對採用 “小字節序” 還是 “大字節序” (最低有效字節在前還是最高有效字節在前) 。 Windows 系統採用的默認標準是小字節序。

(P551)

StringReader 和 StringWriter 適配器並不封裝流;相反,它們使用一個字符串或 StringBuilder 作爲底層數據源。這意味着不需要進行任何的字節轉換,事實上,這些類所執行的操作都可以通過字符串或 StringBuilder 與一個索引變量輕鬆實現。並且它們的優點是與 StreamReader / StreamWriter 使用相同的基類。

BinaryReader 和 BinaryWriter 能夠讀寫基本的數據類型 : bool 、 byte 、 char 、 decimal 、 float 、 double 、 short 、 int 、 long 、 sbyte 、 unshort 、 uint 和 ulong 以及字符串和數組等。

與 StreamReader 和 StreamWriter 不同的是,二進制適配器能夠高效地存儲基本數據類型,因爲它們位於內存中。所以,一個 int 佔用 4 個字節;一個 double 佔用 8 個字節。字符串是通過文本編碼 (與 StreamReader 和 STreamWriter 一樣) 寫入的,但是帶有長度前綴,從而不需要特殊分隔符就能夠讀取一系列字符串。

(P552)

BinaryReader 也支持讀入字節數組。

清理流適配器有 4 種方法 :

  1. 只關閉適配器;

  2. 先關閉適配器,然後再關閉流;

  3. (對於編寫器) 先清理適配器,然後再關閉流;

  4. (對於讀取器) 直接關閉流;

對於適配器和流, Close (關閉) 和 Dispose (清理) 是同義詞。

關閉一個適配器會自動關閉底層的流。

因爲嵌入語句是從內向外清理的,所以適配器先關閉,然後再關閉流。

一定不要在關閉和清理編寫器之前關閉一個流,這樣會丟失仍在適配器中緩存的所有數據。

(P553)

我們要調用 Flush 來保證將 StreamWriter 的緩衝區數據寫入到底層的流中。

流適配器及其可選的清理語法並沒有實現擴展的清理模式,即在終結器中調用 Dispose 。這可以避免垃圾回收器找到棄用的適配器時自動清理這個適配器。

從 Framework 4.5 開始, StreamReader / StreamWriter 有一個新的構造方法,它可以讓流在清理之後仍然保持打開。

System.IO.Compression 命名空間提供了兩個通用壓縮流 : DeflateStream 和 GZipStream 。這兩個類都使用與 ZIP 格式類似的流行壓縮算法。它們的區別是 : GZipStream 會在開頭和結尾寫入一個額外的協議 —— 其中包括檢測錯誤的 CRC 。 GZipStream 還遵循一個其他軟件可識別的標準。

這兩種流都支持讀寫操作,但是有以下限制條件 :

  1. 壓縮時總是在寫入流;

  2. 解壓縮時總是在讀取流;

DeflateStream 和 GZipStream 都是裝飾器;它們負責壓縮或解壓縮構造方法傳入的另一個流。

非重複性二進制文件數據的壓縮效果很差 (缺少設計規範性的加密數據的壓縮比是最差的), 這種壓縮適用於大多數文本文件。

在 DeflateStream 構造方法傳入的額外標記,表示在清除底層流時不採用普通的協議。

(P555)

Framework 4.5 引入了一個新特性 : 支持流行的 Zip 文件壓縮格式,實現方法是 System.IO.Compression 中 (位於 System.IO.Compression.dll) 新增加的 ZipArchive 和 ZipFile 類。與 DeflateStream 和 GZipStream 相比,這種格式的優點是可以處理多個文件,並且兼容 Windows 資源管理器及其他壓縮工具創建的 Zip 文件。

ZipArchive 可以操作流,而 ZipFile 則負責操作更常見的文件。 (ZipFile 是 ZipArchive 的靜態幫助類) 。

ZipFile 的 CreateFromDirectory 方法可以將指定目錄的所有文件添加到一個 Zip 文件中。

而 ExtractToDirectory 則執行相反操作,可以將一個 Zip 文件解壓縮到一個目錄中。

在壓縮時,可以指定是否優化文件大小或壓縮速度,以及是否在存檔文件中包含源文件目錄名稱。

ZipFile 包含一個 Open 方法,它可以 讀 / 寫 各個文件項目。這個方法會返回一個 ZipArchive 對象 (也可以通過使用一個 Stream 對象創建 ZipArchive 實例而獲得) 。當調用 Open 時,必須指定一個文件名,並且指定存檔文件操作方式 : Read 、 Create 或 Update 。然後,使用 Entries 屬性遍歷現有的項目,或者使用 GetEntry 查詢某個文件。

ZipArchiveEntry 還有 Delete 方法, ExtractToFile 方法 (實際是 ZipFileExtensions 類的擴展方法) 和 Open 方法 (返回一個 可讀 / 可寫 的 Stream) 。調用 ZipArchive 的 CreateEntry 或者 CreateEntryFromFile 擴展方法,可以創建新項目。

使用 MemoryStream 創建 ZipArchive ,也可以在內存中實現相同效果。

System.IO 命名空間有一些執行 “實用的” 文件與目錄操作的類型。對於大多數特性,我們可以選擇兩種類型 : 一種採用靜態方法,另一種採用實例方法 :

  1. 靜態類 —— File 和 Directory ;

  2. 實例方法類 (使用文件或目錄名創建) —— FileInfo 和 DirectoryInfo ;

(P556)

此外,還有一個靜態類 Path ,它不操作文件或目錄;相反,它具有一些文件名或目錄路徑的字符處理方法。 Path 也能夠幫助處理臨時文件。

所有這些類都不適用於 Metro 應用。

File 是一個靜態類,它的方法都接受文件名參數。這個文件名可以是相對當前目錄的路徑,也可以是一個目錄的完整路徑。

如果目標文件已存在,那麼 Move 會拋出一個異常;但是 Replace 不會,這兩個方法允許將文件重命名或移動到另一個目錄。

如果文件被標記爲只讀,那麼 Delete 會拋出一個 UnauthorizedAccessException ;調用 GetAttribtes 可以預先判斷其屬性。

(P557)

FileInfo 提供了一個更簡單的修改文件只讀標記的方法 (IsReadOnly) 。

執行解壓縮,可以將 CompressEx 替換成 UncompressEx 。

透明加密和壓縮需要特殊的文件系統支持。 NTFS (硬盤中使用最廣泛的格式) 支持這些特性; CDFS (在 CD-ROM 中) 和 FAT (在可移動內存卡中) 則不支持。

(P558)

GetAccessControl 和 SetAccessControl 方法支持通過 FileSecurity 對象 (位於命名空間 System.Security .AccessControl) 查詢和修改操作系統授予用戶和角色的權限。在創建一個新文件時,我們可以給FileStream 的構造函數傳入一個 FileSecurity ,以指定它的權限。

(P559)

靜態的 Directory 類具有一組與 File 類相似的方法,用於檢查目錄是否存在 (Exists) 、移動目錄 (Move) 、 刪除目錄 (Delete) 、獲取 / 設置 創建時間或最後訪問時間,以及 獲取 / 設置 安全權限。

使用 File 和 Directory 的靜態方法,我們可以方便地執行一個文件或目錄操作。如果需要一次性調用多個方法, FileInfo 和 DirectoryInfo 類支持一種簡化這種調用的對象模型。

FileInfo 以實例方法的形式支持大部分的 File 靜態方法以及一些額外的屬性,例如 Extension 、 Length 、 IsReadOnly 和 Directory (返回一個 DirectoryInfo 對象) 。

(P560)

靜態的 Path 類定義了一些處理路徑和文件名的方法和字段。

(P561)

Combine 是非常有用的,它可用來組合目錄和文件名或者兩個目錄,而不需要先檢查名稱後面是否有反斜槓。

GetFullPath 可以將一個相對於當前目錄的路徑轉換爲一個絕對路徑。它接受例如 ....\file.txt 這樣的值。

GetRandomFileName 會返回一個完全唯一的 8.3 格式文件名,但不會真正創建文件。

GetTempFileName 會使用一個自增計數器生成一個臨時文件名,這個計數器每隔 65,000 次重複一遍。然後,它會用這個名稱在本地臨時目錄創建一個 0 字節的文件。

System.Environment 類的 GetFolderPath 方法提供查找特殊文件夾的功能。

Environment.SpecialFolder 是一個枚舉類型,它的值包括 Windows 中的所有特殊目錄。

(P563)

DriveInfo 類可用來查詢計算機的驅動器信息。

(P564)

靜態的 GetDrives 方法會返回所有映射的驅動器,包括 CD-ROM 、內存卡和網絡連接。

FileSystemWatcher 類可用來監控一個目錄 (或者子目錄) 的活動。當有文件或子目錄被創建、修改、重命名、刪除以及屬性變化時, FileSystemWatcher 都會觸發相應的事件。無論是用戶還是進程執行這些操作,這些事件都會觸發。

(P565)

因爲 FileSystemWatcher 在一個獨立線程上接收事件,所以事件處理代碼中必須使用異常處理語句,防止錯誤使應用程序崩潰。

Error 事件不會通知文件系統錯誤;相反,它表示的是 FileSystemWatcher 的事件緩衝區溢出,因爲它已經被 Changed 、 Created 、 Deleted 或 Renamed 佔用。我們可以通過 InternalBufferSize 屬性修改緩衝區大小。

IncludeSubdirectories 會遞歸執行。

Metro 應用都不能使用 FileStream 和 Directory / File 類。相反, Windows.Storage 命名空間包含一些具有相同用途的 WinRT 類型,其中兩個主要類是 StorageFolder 和 StorageFile 。

StorageFolder 類表示一個目錄,調用 StorageFolder 的靜態方法 GetFolderFromPathAsync ,指定文件夾的完整路徑,就可以獲得一個 StorageFolder 對象。

(P566)

StorageFile 是操作文件的基礎類。使用靜態類 StorageFile.GetFileFromPathAsync ,可以使用完整路徑獲得一個文件實例;調用 StorageFolder 或 IsStorageFolder 對象的 GetFileAsync 方法,則可以使用相對路徑獲得一個文件實例。

(P567)

內存映射文件是 Framework 4.0 新增加的。它們有兩個主要特性 :

  1. 文件數據的高效隨機訪問;

  2. 在同一臺計算機的不同進程之間共享內存;

內存映射文件的類型位於 System.IO.MemoryMappedFiles 命名空間。在內部,它們是封裝了支持內存映射文件的 Win32 API 。

雖然常規的 FileStream 也支持隨機文件 I / O (通過設置流的 Position 屬性實現) ,但是它在連續 I / O 方面進行了優化。一般原則大致是 :

  1. FileStream 的連續 I / O 速度要比內存映射文件快 10 倍;

  2. 內存映射文件的隨機 I / O 速度要比 FileStream 快 10 倍;

修改 FileStream 的 Position 屬性可能需要耗費幾毫秒時間,並在循環中會進一步累加。 FileStream 不適用於多線程訪問,因爲它在讀或寫時位置會發生改變。

要創建一個內存映射文件,我們要 :

  1. 獲取一個普通的 FileStream ;

  2. 使用文件流實例化 MemoryMappedFile ;

  3. 在內存映射文件對象上調用 CreateViewAccessor ;

最後一步可以得到一個 MemoryMappedViewAccessor 對象,它具有一些隨機讀寫簡單類型、結構和數組的方法。

(P568)

內存映射文件也可以作爲同一臺計算機上不同進程間共享內存的一種手段。一個進程可以調用 MemoryMappedFile.CreateNew 創建一個共享內存塊,而另一個進程則可以用相同的名稱調用 MemoryMappedFile.OpenExisting 來共享同一個內存塊。雖然它仍然是一個內存映射文件,但是已經完全脫離磁盤而進入內存中。

在 MemoryMappedFile 中調用 CreateViewAccessor 可以得到一個視圖訪問器,它可以用來執行隨機位置的 讀 / 寫。

(P569)

Read / Write 方法可以接受數字類型、 bool 、 char 以及包含值類型元素或域的數組和結構體。引用類型 (及包含引用類型的數組或結構體) 是禁止使用的,因爲它們無法映射到一個未託管的內存中。

我們還可以通過指針直接訪問底層的未託管內存。

指針在處理大結構時的優勢是 : 它們可以直接處理原始數據,而不需要使用 Read / Write 在託管內存和未託管內存之間進行數據複製。

每一個 .NET 程序都可以訪問該程序獨有的本地存儲區域,即獨立存儲 (isolated storage) 。如果程序無法訪問標準文件系統,那麼很適合使用獨立存儲。使用受限 “互聯網” 權限的 Silverlight 應用和 ClickOnce 應用就屬於這種情況。

(P570)

在安全性方面,隔離存儲區的作用更多的是阻止其他的應用程序進入,而不是阻止其中的應用程序出去。隔離存儲區的數據受到嚴格保護,不會受到其他運行在最嚴格權限集之下的 .NET 應用程序的***。

在沙箱中運行的應用程序可以通過權限設置獲得有限的隔離存儲區配額。默認情況下,互聯網和 Silverlight 應用程序在 Framework 4.0 中的配額是 1MB 。

【第16章】

(P575)

Framework 在 System.Net.* 命名空間中包含各種支持標準網絡協議通信的類,例如 HTTP 、 TCP / IP 和 FTP 。下面是其中一些主要組件的小結 :

  1. WebClient 外觀類 —— 支持通過 HTTP 或 FTP 執行簡單的 下載 / 上傳 操作;

  2. WebRequest 和 WebResponse 類 —— 支持更多的客戶端 HTTP 或 FTP 操作;

  3. HttpListener 類 —— 可用來編寫 HTTP 服務器;

  4. SmtpClient 類 —— 通過支持 SMTP 創建和發送電子郵件;

  5. Dns 類 —— 支持域名和地址之間的轉換;

  6. TcpClient 、 UdpClient 、 TcpListener 和 Socket 類 —— 支持傳輸層和網絡層的直接訪問。

Framework 支持主要的 Internet 協議,但是它的功能不僅限於 Internet 連接,諸如 TCP / IP 等協議也可以廣泛應用於局域網上。

大多數類型都位於傳輸層或應用層。

傳輸層定義了發送和接受字節的基礎協議 (TCU 或 UDP) ;

應用層則定義支持特定應用程序的上層協議,例如獲取 Web 頁 (HTTP) 、 傳輸文件 (FTP) 、 發送郵件 (SMTP) 和域名與 IP 地址轉換 (DNS) 。

通常,在應用層編程是最方便的。然而,有一些原因要求我們必須直接在傳輸層上進行操作,例如當需要使用一種 Framework 不支持的應用程序協議 (例如 POP3) 來接收郵件時。此外,當需要爲某個特殊應用程序 (例如對等客戶端) 發明一種自定義協議時,也是如此。

HTTP 屬於應用層協議,它專門用於擴展通用的通信。它的基本運行方式是 “請給我這個 URL 的網頁” ,可以很好地理解爲 “返回使用這些參數調用這個方法的結果值” 。 HTTP 具有豐富的特性,它們在多層次業務應用程序和麪向服務的體系結構中是非常有用的,例如驗證和加密協議、消息組塊、可擴展頭信息和 Cookie ,並且多個服務器應用程序可以共享一個端口和 IP 地址。因此, HTTP 在 Framework 中得到很好的支持,包括直接支持以及通過 WCF 、 Web Services 和 ASP.NET 等技術實現的更高級支持。

(P576)

Framework 提供 FTP 客戶端支持,這是最常用的 Internet 文件發送和接受協議。服務器端支持是通過 IIS 或 UNIX 服務器軟件等形式實現的。

DNS (Domain Name Service : 域名服務) —— 域名和 IP 地址轉換;

FTP (File Transfer Protocol : 文件傳輸協議) —— Internet 文件發送和接收的協議;

HTTP (Hypertext Transfer Protocol : 超文本傳輸協議) —— 查詢網頁和運行 Web 服務;

IIS (Internet Information Services : Internet 信息服務) —— 微軟的 Web 服務器軟件;

IP (Internet Protocol : Internet 協議) —— TCP 與 UDP 之下的網絡層協議;

LAN (Local Area Network : 局域網) —— 大多數 LAN 使用 TCP / IP 等 Internet 協議;

POP (Post Office Protocol : 郵局協議) —— 查詢 Internet 郵件;

SMTP (Simple Mail Transfer Protocol : 簡單郵件傳輸協議) —— 發送 Internet 郵件;

TCP (Transmission and Control Protocol : 傳輸和控制協議) —— 傳輸層 Internet 協議,大多數更高級服務的基礎;

UDP (Universal Datagram Protocol : 低開銷服務使用傳輸層 Internet 協議,例如 “通用數據報協議” ) ;

UNC (Universal Naming Convention : 通用命名轉換) —— \computer\sharename\filename

URI (Uniform Resource Identifier : 統一資源標識符) —— 使用普遍的資源命名系統;

URL (Uniform Resource Locator : 統一資源定位符) —— 技術意義(逐漸停止使用) - URI 子集;流行意義 - URI 簡稱;

(P577)

要實現通信,計算機或設備都需要一個地址。 Internet 使用了兩套系統 :

  1. IPv4 : 這是目前的主流地址系統; IPv4 地址有 32 位。當用字符串表示時, IPv4 地址可以寫爲用點號分隔的 4 個十進制數。地址可能是全世界唯一的,也可能在一個特定子網中是唯一的;

  2. IPv6 : 這是更新的 128 位地址系統。這些地址用字符串表示爲用冒號分隔的十六進制。 .NET Framework 中要求地址加上方括號;

System.Net 命名空間的 IPAddress 類是採用其中一種協議的地址。它有一個構造函數可以接收字節數組,以及一個靜態的 Parse 方法接收正確格式的字符串。

TCP 和 UDP 協議將每一個 IP 地址劃分爲 65535 個端口,從而允許一臺計算機在一個地址上運行多個應用程序,每一個應用程序使用一個端口。許多程序都分配有標準端口,例如,HTTP 使用端口 80 ;SMTP 使用端口 25 。

從 49152 到 65535 的 TCP 和 UDP 端口是官方保留的,它們只用於測試和小規模部署。

IP 地址和端口組合在 .NET Framework 中是使用 IPEndPoint 類表示的。

(P578)

防火牆可以阻擋端口。在許多企業環境中,事實上只有少數端口是開放的,通常情況下,只開放端口 80 (不加密 HTTP) 和端口 443 (安全 HTTP) 。

URI 是一個具有特殊格式的字符串,它描述了一個 Internet 或 LAN 的資源,例如網頁、文件或電子郵件地址。

正確的格式是由 IETF (Internet Engineering Task Force) 定義的。

URI 一般分成三個元素 : 協議 (scheme) 、 權限 (authority) 和路徑 (path) 。

System 命名空間的 Uri 類正是採用這種劃分方式,爲每一種元素提供對應的屬性。

Uri 類適合用來驗證 URI 字符串的格式或將 URI 分割成相應的組成部分。另外,可以將 URI 作爲一個簡單的字符串進行處理,大多數網絡連接方法都有接收 Uri 對象或字符串的重載方法。

在構造函數中傳入以下字符串之一,就可以創建一個 Uri 對象 :

  1. URI 字符串;

  2. 硬盤中的一個文件的絕對路徑;

  3. LAN 中一個文件的 UNC 路徑;

文件和 UNC 路徑會自動轉換爲 URI : 添加協議 “file:” ,反斜槓會轉換爲斜槓。 Uri 的構造函數在創建 Uri 之前也會對傳入的字符串執行一些基本的清理操作,包括將協議和主機名轉換爲小寫、刪除默認端口號和空端口號。如果傳入一個不帶協議的 URI 字符串,那麼會拋出一個 UriFormatException 。

(P579)

Uri 有一個 IsLoopback 屬性,它表示 Uri 是否引用本地主機 (IP 地址爲 127.0.0.1) ;以及一個 IsFile 屬性,它表示 Uri 是否引用一個本地或 UNC (IsUnc) 路徑。如果 IsFile 返回 true , LocalPath 屬性會返回一個符合本地操作系統習慣的 AbsolutePath (帶反斜槓) ,然後可以用它來調用 File.Open 。

Uri 的實例有一些只讀屬性。要修改一個 Uri ,我們需要實例化一個 UriBuilder 對象,這是一個可寫屬性,它可以通過 Uri 屬性轉換爲 Uri 。

Uri 也具有一些比較和截取路徑的方法。

URI 後面的斜槓是很重要的,服務器會根據它來決定是否處理路徑組成部分。

WebRequest 和 WebResponse 是管理 HTTP 和 FTP 客戶端活動及 “file:” 協議的通用基類。它們封裝了這些協議共用的 “請求 / 響應” 模型 : 客戶端發起請求,然後等待服務器的響應。

WebClient 是一個便利的門店 (facade) 類,它負責調用 WebRequest 和 WebResponse ,可以節省很多編碼。 WebClient 支持字符串、字節數組、文件或流;而 WebRequest 和 WebResponse 只支持流。但是, WebClient 也不是萬能的,因爲它也不支持某些特性 (如 cookie) 。

HttpClient 是另一個基於 WebRequest 和 WebResponse 的類 (更準確說是基於 HttpWebRequest 和 HttpWebResponse) ,並且是 Framework 4.5 新引入的類。

WebClient 主要作爲 請求 / 響應 類之上薄薄的一層,而 HttpClient 則增加了更多的功能,能夠處理基於 HTTP 的 Web API 、 基於 REST 的服務和自定義驗證模式。

(P580)

WebClient 和 HttpClient 都支持以字符串或字節數組方式處理簡單的文件 下載 / 上傳 操作。它們都擁有一些異步方法,但是隻有 WebClient 支持進度報告。

WinRT 應用程序不能使用 WebClient ,它必須使用 WebRequest / WebResponse 或 HttpClient (用於 HTTP 連接) 。

WebClient 的使用步驟 :

  1. 實例化一個 WebClient 對象;

  2. 設置 Proxy 屬性值;

  3. 在需要驗證時設置 Credentials 屬性值;

  4. 使用相應的 URI 調用 DownloadXXX 或 UploadXXX 方法;

UploadValues 方法可用於以 POST 方法參數提交一個 HTTP 表單的值。

WebClient 還包含一個 BaseAddress 屬性,可用於爲所有地址添加一個字符串前綴。

(P581)

WebClient 被動實現了 IDisposable —— 因爲它繼承了 Component 。然而,它的 Dispose 方法在運行時並沒有執行太多實際操作,所以不需要清理 WebClient 的實例。

從 Framework 4.5 開始, WebClient 提供了長任務方法的異步版本,它們會返回可以等待的任務。

await webClient.DownloadTaskAsync() 這些方法使用 “TaskAsync” 後綴,不同於使用 “Async” 後綴的 EAP 舊異步方法。但是,新方法不支持取消操作和進度報告的標準 “TAP” 模式。相反,在處理延續時,必須調用 WebClient 對象的 CancelAsync 方法;而處理進度報告時,則需要處理 DownloadProgressChanged / UploadProgressChanged 事件。

如果需要使用取消操作或進度報告,那麼要避免使用同一個 WebClient 對象依次執行多個操作,因爲這樣會形成競爭條件。

WebRequest 和 WebResponse 比 WebClient 複雜,但是更加靈活。下面是開始使用的步驟 :

  1. 使用一個 URI 調用 WebRequest.Create ,創建一個 Web 請求實例;

  2. 設置 Proxy 屬性;

  3. 如果需要驗證身份,則設置 Credentials 屬性;

如果要上傳數據,則 :

  1. 調用請求對象的 GetRequestStream ,然後在流中寫入數據。如果需要處理響應,則轉到第 5 步。

如果要下載數據,則 :

  1. 調用請求對象的 GetResponse ,創建一個 Web 響應實例;

  2. 調用響應對象的 GetResponseStream ,然後 (可以使用 StreamReader) 從流中讀取數據;

(P582)

靜態方法 Create 會創建一個 WebRequest 類型的子類實例。

將 Web 請求對象轉換爲具體的類型,就可以訪問特定協議的特性。

“https:” 協議是指通過安全套接層 (Secure Sockets Layer, SSL) 實現的安全 (加密) HTTP 。 WebClient 和 WebRequest 都會在遇到這種前綴時激活 SSL 。

“file:” 協議會將請求轉發到一個 FileStream 對象,其目的是確定一個與讀取 URI 一致的協議,它可能是一個網頁、 FTP 站點或文件路徑。

(P583)

WebRequest 包含一個 Timeout 屬性,其單位爲毫秒。如果出現超時,那麼程序就會拋出一個 WebException 異常,其中包含一個 Status 屬性 : WebExceptionStatus.Timeout 。 HTTP 的默認超時時間爲 100 秒,而 FTP 的超時時間爲無限。

WebRequest 對象不能回收並用於處理多個請求 —— 每一個實例只適用於一個作業。

HttpClient 是 Framework 4.5 新引入的類,它在 HttpWebRequest 和 HttpWebResponse 之上提供了另一層封裝。它的設計是爲了支持越來越多的 Web API 和 REST 服務,在處理比獲取網頁等更復雜的協議時實現比 WebClient 更佳的體驗。具體地 :

  1. 一個 HttpClient 就可以支持併發請求。如果要使用 WebClient 處理併發請求,則需要爲每一個併發線程創建一個新實例,這時需要自定義請求頭、 cookie 和 驗證模式,因此會比較麻煩;

  2. HttpClient 可用於編寫和插入自定義消息處理器。這樣可以創建單元測試樁函數,以及創建自定義管道 (用於記錄日誌、壓縮、加密等) 。調用 WebClient 的單元測試代碼則很難編寫;

  3. HttpClient 包含豐富且可擴展的請求頭與內容類型系統;

HttpClient 不能完全代替 WebClient ,因爲它不支持進度報告。

WebClient 也有一個優點,它支持 FTP 、 file:// 和 自定義 URI 模式,它也適用於所有 Framework 版本。

使用 HttpClient 的最簡單方法是創建一個實例,然後使用 URI 調用其中一個 Get* 方法。

HttpClient 的所有 I / O 密集型方法都是異步的 (它們沒有同步實現版本) 。

與 WebClient 不同,想要獲得最佳性能的 HttpClient ,必須重用相同的實例 (否則諸如 DNS 解析操作會出現不必要的重複執行)。

HttpClient 允許併發操作。

HttpClient 包含一個 Timeout 屬性和一個 BaseAddress 屬性,它會爲每一個請求添加一個 URI 前綴。

HttpClient 在一定程度上就是一層實現 : 通常使用的大部分屬性都定義在另一個類中,即 HttpClientHandler 。

(P584)

GetStringAsync 、 GetByteArrayAsync 和 GetStreamAsync 方法是更常用的 GetAsync 方法的快捷方法。

HttpResponseMessage 包含一些訪問請求頭 和 HTTP StatusCode 的屬性。與 WebClient 不同,除非顯式調用 EnsureSuccessStatusCode ,否則返回不成功狀態不會拋出異常。然而,通信或 DNS 錯誤會拋出異常。

HttpResponseMessage 包含一個 CopyToAsync 方法,它可以將數據寫到另一個流中,適用於將輸入寫到一個文件中。

GetAsync 是 HTTP 的 4 種動作相關的 4 個方法之一 (其他方法是 PostAsync 、 PutAsync 和 DeleteAsync) 。

創建一個 HttpRequestMessage 對象,意味着可以自定義請求的屬性,如請求頭和內容本身,它們可用於上傳數據。

在創建一個 HttpRequestMessage 對象之後,設置它的 Content 的屬性,就可以上傳內容。這個屬性的類型是抽象類 HttpContent 。

大多數自定義請求的屬性都不是在 HttpClient 中定義,而是在 HttpClientHandler 中定義。後者實際上是抽象類 HttpMessageHandler 的子類。

HttpMessageHandler 非常容易繼承,同時也提供了 HttpClient 的擴展點。

(P586)

代理服務器 (proxy server) 是一箇中間服務器,負責轉發 HTTP 和 FTP 請求。

代理本身擁有地址,並且可能需要執行身份驗證,所以只有特定的局域網用戶可以訪問互聯網。

創建一個 WebClient 或 WebRequest 對象,就可以使用 WebProxy 對象通過代理服務器轉發請求。

(P587)

如果要使用 HttpClient 訪問代理,那麼首先要創建一個 HttpClientHandler ,設置它的 Proxy 屬性,然後將它傳遞給 HttpClient 的構造方法。

如果已知不存在代理,那麼可以在 WebClient 和 WebRequest 對象上將 Proxy 屬性設置爲 null 。否則, Framework 可能會嘗試自動檢查代理設置,這會給請求增加 30 秒延遲。如果 Web 請求執行速度過慢,那麼很可能就是這個原因造成的。

HttpClientHandler 還有一個 UseProxy 屬性,將它設置爲 false ,就可以將 Proxy 屬性置空,從而禁止自動檢測。

如果在創建 NetworkCredential 時提供一個域,那麼就會使用基於 Windows 的身份驗證協議。如果想要使用當前已驗證的 Windows 用戶,則可以在代理的 Credentials 屬性上設置靜態的 CredentialCache.DefaultNetworkCredentials 值。

創建一個 NetworkCredential 對象,將它設置到 WebClient 或 WebRequest 的 Credentials 屬性上,就可以向 HTTP 或 FTP 站點提供用戶名和密碼。

(P588)

身份驗證最終由一個 WebRequest 子類型處理,它會自動協商一個兼容協議。

(P589)

WebRequest 、 WebResponse 、 WebClient 及其流都會在遇到網絡或協議錯誤時拋出一個 WebException 異常。

HttpClient 也有相同行爲,但是它將 WebException 封裝在一個 HttpRequestException 中。

使用 WebException 的 Status 屬性,就可以確定具體的錯誤類型,它會返回一個枚舉值 WebExceptionStatus 。

(P591)

WebClient 、 WebRequest 和 HttpClient 都可以添加自定義 HTTP 請求頭,以及在響應中列舉請求頭信息。請求頭只是一些 鍵 / 值 對,其中包含相應的元數據,如消息內容類型或服務器軟件。

HttpClient 包含了一些強類型集合,其中包含與標準 HTTP 頭信息相對應的屬性。 DefaultRequestHeaders 屬性包含適用於每一個請求的頭信息。

HttpRequestMessage 類的 Headers 屬性包含請求特有的頭信息。

查詢字符串只是通過問號 (?) 附加到 URI 後面的字符串,它可用於向服務器發送簡單的數據。

WebClient 包含一個字典風格的屬性,它可以簡化查詢字符串的操作。

(P592)

如果要使用 WebRequest 或 HttpClient 實現相同效果,那麼必須手工賦給請求 URI 正確格式的字符串。

如果查詢中包含符號或空格,那麼必須使用 Uri 的 EscapeDataString 方法才能創建合法的 URI 。

EscapeDataString 與 EscapeUriString 類似,唯一不同的是前者進行了特殊字符的編碼,如 & 和 = ,否則它們會破壞查詢字符串。

WebClient 的 UploadValues 方法可以以 HTML 表單的方式提交數據。

NameValueCollection 中的鍵與 HTML 表單的輸入框相對應。

使用 WebRequest 上傳表單數據的操作更爲複雜,如果需要使用 cookies 等特性,則必須採用這種方法。下面是具體的操作過程 :

  1. 將請求的 ContentType 設置爲 “application/x-www-form-urlencoded” ,將它的方法設置爲 “POST” ;

  2. 創建一個包含上傳數據的字符串,並且將其編碼爲 : name1=value1&name2=value2&name3=value3...

  3. 使用 Encoding.UTF8.GetBytes 將字符串轉換爲字節數組;

  4. 將 Web 請求的 ContentLength 屬性設置爲字節數組的長度;

  5. 調用 Web 請求的 GetRequestStream ,然後寫入數據數組;

  6. 調用 GetResponse ,讀取服務器的響應。

(P593)

Cookie 是一種 名稱 / 值 字符串對,它是 HTTP 服務器通過響應頭髮送到客戶端的。 Web 瀏覽器客戶端通常會記住 cookie ,然後在終止之前,後續請求都會將它們重複發送給服務器 (相同地址) 。

Cookie 使服務器知道它是否正在連接之前連接過的相同客戶端,從而不需要在 URI 重複添加複雜的查詢字符串。

默認情況下, HttpWebRequest 會忽略從服務器接收的任意 cookie 。爲了接收 cookie ,必須創建一個 CookieContainer 對象,然後將它分配到 WebRequest 。然後,就可以列舉響應中接收到的 cookie 。

(P594)

WebClient 門面類不支持 cookie 。

(P596)

可以使用 HttpListener 類編寫自定義 HTTP 服務器。

(P599)

對於簡單的 FTP 上傳和下載操作,可以使用 WebClient 按照前面的方式實現。

(P600)

靜態的 Dns 類封裝了 DNS (Domain Name Service ,域名服務) ,它可以執行原始 IP 地址和人性化的域名之間的轉換操作。

GetHostAddresses 方法可以將域名轉換爲 IP 地址 (或地址) 。

(P601)

GetHostEntry 方法則執行相反操作,將地址轉換爲域名。

GetHostEntry 方法還接受一個 IPAddress 對象,所以我們可以用一個字節數組來表示 IP 地址。

在使用 WebRequest 或 TcpClient 等類時,域名會自動解析爲 IP 地址。然而,如果想要在應用程序的生命週期內向同一個地址發送多個網絡請求,有時候需要先使用 DNS 將域名顯式地轉換爲 IP 地址,然後再直接使用得到的 IP 地址進行通信,從而提高運行性能。這樣就能夠避免重複解析同一個域名,有助於 (使用 TcpClient 、 UdpClient 或 Socket ) 處理傳輸層協議。

System.Net.Mail 命名空間的 SmtpClient 類可用來通過普遍使用的簡單郵件傳輸協議 (Simple Mail Transfer Protocol ,SMTP) 發送郵件消息。

要發送一條簡單的文本消息,我們需要實例化 SmtpClient ,將它的 Host 屬性設置爲 SMTP 服務器地址,然後調用 Send 。

爲了防止垃圾郵件, Internet 中大多數 SMTP 服務器都只接受來自 ISP 訂閱者的連接,所以我們需要使用適合當前連接的 SMTP 地址才能成功發送郵件。

MailMessage 對象支持更多的選項,包括添加附件。

SmtpClient 可以爲需要執行身份驗證的服務器指定 Credentials ,如果支持 EnableSsl ,也可以將 TCP Port 修改爲非默認值。通過修改 DeliveryMethod 屬性,我們可以使用 SmtpClient 代替 IIS 發送郵件消息,或者直接將消息寫到指定目錄下的一個 .eml 文件中。

(P602)

TCP 和 UDP 是大多數 Internet (與局域網) 服務所依賴的傳輸層協議的基礎。

HTTP 、 FTP 和 SMTP 使用 TCP ; DNS 使用 UDP 。

TCP 是面向連接的,具有可靠性機制; UDP 是無連接的,負載更小,並且支持廣播。

BitTorrent 和 Voice over IP 都使用 UDP 。

傳輸層比其他上層協議具有更高靈活性,性能可能也更高,但是它要求用戶自己處理一些具體任務,如身份驗證和加密。

對於 TCP ,我們可以選擇使用簡單易用的 TcpClient 和 TcpListener 外觀類,或者使用功能豐富的 Socket 類。事實上,它們可以混合使用,因爲我們可以通過 TcpClient 的 Client 屬性獲得底層的 Socket 對象。Socket 類包含更多的配置選項,它支持網絡層 (IP) 的直接訪問,也支持一些非 Internet 協議,如 Novell 的 SPX/IPX 。

和其他協議一樣, TCP 也區分客戶端和服務器 : 客戶端發起請求,而服務器則等待請求。

NetworkStream 提供一種雙向通信手段,同時支持從服務器發送和接收字節數據。

(P604)

TcpClient 和 TcpListener 提供了基於任務的異步方法,可用於實現可擴展的併發性。使用這些方法,只需要將阻塞方法替換爲它們對應的 *Async 版本方法,然後等待任務返回。

(P605)

.NET Framework 並沒有提供任何 POP3 的應用層支持,所以要從一個 POP3 服務器接收郵件,必須在 TCP 層編寫代碼。

(P606)

Windows Runtime 通過 Windows.Networking.Sockets 命名空間實現 Tcp 功能。與 .NET 實現一樣,其中主要有兩個類,分別充當服務器和客戶端角色。在 WinRT 中,它們分別是 StreamSocketListener 和 StreamSocket 。

【第17章】

(P608)

序列化與反序列化,通過它對象可以表示成一個純文本或者二進制形式。

序列化是把內存中的一個對象或者對象圖 (一組互相引用的對象) 轉換成一個字節流或者一組可以保存或傳輸的 XML 節點。反序列化正好相反,它把一個數據流重新構造成一個內存中的對象或對象圖。

序列化和反序列化通常用於 :

  1. 通過網絡或應用程序邊界傳輸對象;

  2. 在文件或數據庫中保存對象的表示;

序列化與反序列化也用於深度克隆對象。

數據契約和 XML 序列化引擎也可以被用作通用目的工具來加載和保存已知結構的 XML 文件。

.NET Framework 從兩個角度來支持序列化與反序列化 : 第一,從想進行序列化和反序列化對象的客戶端角度; 第二,從想控制其如何被序列化的類型角度。

在 .NET Framework 中有 4 種序列化機制 :

  1. 數據契約序列化器;

  2. 二進制序列化器;

  3. (基於屬性的) XML 序列化器 (XmlSerializer) ;

  4. IXmlSerializable 接口;

(P609)

其中前三種 “引擎” 可以完成大部分或所有序列化操作。而最後的 IXmlSerializable 接口是一個可以通過使用 XmlReader 和 XmlWriter 進行序列化的起橋樑作用的鉤子 (hook) 。

IXmlSerializable 可以聯合數據契約序列化器或者 XmlSerializer 來處理更復雜的 XML 序列化任務。

IXmlSerializable 的分數假設已經使用 XmlReader 和 XmlWriter 最優化地 (手) 寫代碼。

XML 序列化引擎要求回收相同的 XmlSerializer 對象以達到更佳的性能。

出現這三種引擎在一定程度上是由於歷史原因。 Framework 在序列化上基於兩個完全不同的目的 :

  1. 真實的序列化包含類型及其引用的 .NET 對象圖;

  2. XML 和 SOAP 消息之間的互操作標準;

第一種由 Remoting 的需求而產生;而第二種是由於 Web 服務。寫一個序列化引擎來同時完成這兩項任務非常困難,所以 Microsoft 編寫了兩個引擎 : 二進制序列化器和 XML 序列化器。

後來在 .NET Framework 3.0 中出現 WCF 時,其部分目標在於統一 Remoting 和 Web 服務。這就要求一個新的序列化引擎,所以就出現了數據契約序列化器。數據契約序列化器統一了舊有的兩個和消息有關的引擎的特性。但是在這個上下文之外,這兩個舊的序列化引擎還是很重要的。

數據契約序列化器在這三種序列化引擎中是最新的也是最有用的引擎,並被 WCF 使用。它在下面兩種情形下尤其強大 :

  1. 通過符合標準的消息協議來交換信息;

  2. 需要好的版本容差能力,並且能夠保留對象引用;

數據契約序列化器支持一種數據契約模型 : 它能幫助把類型的底層細節與被序列化過的數據結構解耦。這爲我們提供了優秀的版本容差性,也就意味着我們可以反序列化從早期或者後來版本序列化過來的數據類型。甚至可以反序列化已經被重命名或者被移到不同程序集中的類型。

(P610)

數據契約序列化器可以處理大多數的對象圖,儘管它需要比二進制序列化器更多的輔助。如果能夠靈活地構造 XML ,它也可被用作通用目的的讀寫 XML 文件的工具。但是如果需要存儲數據屬性或者要處理隨機出現的 XML 元素,就不能使用數據契約序列化器了。

二進制序列化器比較容易使用、非常的自動化,並且在 .NET Remoting 中自始至終都被很好地支持。

Remoting 在同一進程中的兩個應用域之間通信時使用二進制序列化器。

二進制序列化器被高度地自動化了 : 只需要一個屬性就可以使一個複雜類型可完全序列化。當所有類型都要求被高保真序列化時,二進制序列化器要比數據契約序列化器快。但是它把類型的內部結構與被序列化數據的格式緊密耦合,導致了比較差的版本容差性 (在 Framework 2.0 之前,即使添加一個字段也會成爲破壞版本的變化) 。二進制引擎也不是真正地爲生成 XML 而設計的,儘管它爲基於 SOAP 的消息提供了一個有限的可以和簡單類型互操作的格式化器。

XML 序列化引擎只能產生 XML ,它沒有其他能夠保持和恢復複雜對象圖的引擎那麼強大 (它不能夠恢復共享的對象引用) 。但是對於處理比較隨意的 XML 結構,它是三者之中最靈活的。

XML 引擎也提供了較好的版本容差性。

XMLSerializer 被 ASMX Web 服務使用。

實現 IXmlSerializable 意味着通過使用一個 XmlReader 和 XmlWriter 來完成序列化。 IXmlSerializable 接口被 XmlSerializer 和數據契約序列化器所識別,所以它可以有選擇地被用來處理更復雜的類型。它也可以直接被 WCF 和 ASMX Web 服務使用。

(P611)

WCF 總是使用數據契約序列化器,儘管它可以和其他引擎的屬性和接口進行互操作。

Remoting 總是使用二進制序列化引擎。

Web 服務總是使用 XMLSerializer 。

使用數據契約序列化器的基本步驟 :

  1. 決定是使用 DataContractSerializer 還是 NetDataContractSerializer ;

  2. 使用 [DataContract] 和 [DataMember] 屬性修飾要序列化的對象和成員;

  3. 實例化序列化器後調用 WriteObject 或 ReadObject ;

如果選擇 DataContractSerializer ,同時需要註冊已知類型 (也能夠被序列化的子類型) ,並且要決定是否保留對象引用。

可能也需要採取特殊措施來保證集合能被正確地序列化。

與數據契約序列化器相關的類型被定義在 System.Runtime.Serialization 命名空間中,幷包含在同名的程序集中。

有兩個數據契約序列化器 :

  1. DataContractSerializer —— .NET 類型與數據契約類型鬆耦合;

  2. NetDataContractSerializer —— .NET 類型與數據契約類型緊耦合;

DataContractSerializer 可以產生可互操作的符合標準的 XML 。

(P612)

如果通過 WCF 通信或者 讀 / 寫 一個 XML 文件,可能傾向於使用 DataContractSerializer 。

選擇序列化器後,下一步就是添加相應的屬性到要序列化的類型和成員上,至少應該 :

  1. 添加 [DataContract] 屬性到每個類型上;

  2. 添加 [DataMember] 屬性到每個包含的成員上;

(P613)

DataContractSerializer 的構造方法需要一個根對象類型 (顯式序列化的對象類型) ,相反的, NetDataContractSerializer 就不需要。

NetDataContractSerializer 在其他方面與 DataContractSerializer 的用法相同。

兩種序列化器都默認使用 XML 格式化器。

使用 XmlReader ,可以爲了可讀性讓輸出包含縮進。

指定名稱和命名空間可以把契約標識與 .NET 類型名稱解耦。它能夠保證當重構和改變類型的名稱或命名空間時,序列化不會受到影響。

(P614)

[DataMember] 可以支持 public 和 private 字段和屬性。字段和屬性的數據類型可以是下列類型的任何一種 :

  1. 任何基本類型;

  2. DateTime 、 TimeSpan 、 Guid 、 Uri 或 Enum 值;

  3. 上述類型的 Nullable 類型;

  4. Byte[] (在 XML 中序列化爲 base 64) ;

  5. 任何用 DataContract 修飾的已知類型;

  6. 任何 IEnumerable 類型;

  7. 任何被 [Serializable] 修飾,或者實現了 ISerializable 的類型;

  8. 實現了 IXmlSerializable 的任何類型;

可以同時使用二進制格式化器和 DataContractSerializer 或者 NetDataContractSerializer ,過程是一樣的。

二進制格式化器輸出會比 XML 格式化器稍微小一些,當類型中包含大的數組時就會明顯地看到小得多。

在使用 NetDataContractSerializer 時,不需要特別地處理子類的序列化,除非子類需要 [DataContract] 屬性。

DataContractSerializer 必須要了解它可能序列化或反序列化的所有子類型。

(P616)

當序列化子類型時,不管使用哪種序列化器, NetDataContractSerializer 會導致性能上的損失。就好像是當遇到子類型時,它就必須停下來思考一下。

當在一個應用程序服務器上處理大量併發請求時纔會考慮序列化性能。

(P617)

NetDataContractSerializer 總是會保留引用相等性。而 DataContractSerializer 不會,除非指定它保留。

可以在構造 DataContractSerializer 時指定參數 preserveObjectReferences 爲 true 來要求引用完整性。

(P618)

如果某個成員對於一個類型是非常重要的,可以通過指定 [IsRequired] 要求它必須出現,如果成員沒有出現,在序列化時會拋出一個異常。

數據契約序列化器對數據成員的數據要求極其苛刻。反序列化器實際上會跳過任何被認爲在序列外的成員。

在序列化成員時按下面的順序 :

  1. 從基類到子類;

  2. 根據 Order 從低到高 (對於 [Order] 屬性被設置的數據成員) ;

  3. 字母表順序 (使用傳統的字符串比較法) ;

(P619)

要指定順序的主要原因是爲了遵循特定的 XML Schema 。 XML 元素的順序等同於數據成員順序。

(P620)

數據契約序列化器可以保持和恢復可遍歷集合。

(P622)

如果要在序列化之前或之後執行一個自定義方法,可以通過在方法上標記以下屬性 :

  1. [OnSerializing] —— 指示在序列化之前調用這個方法;

  2. [OnSerialized] —— 指示在序列化之後調用這個方法;

  3. [OnDeserializing] —— 指示在反序列化之前調用這個方法;

  4. [OnDeserialized] —— 指示在反序列化之後調用這個方法;

自定義方法只能定義一個 StreamingContext 類型的參數。這個參數是爲了與二進制引擎保持一致而被要求的,它不被數據契約序列化器使用。

[OnSerializing] 和 [OnDeserialized] 在處理超出數據契約引擎能力之外的成員時有用,例如一個超額的集合或者沒有實現標準接口的集合。

(P623)

[OnSerializing] 標記的方法也可以被用作有條件的序列化字段。

注意數據契約反序列化器會繞過字段初始化器和構造方法。標記了 [OnDeserializing] 的方法在反序列化過程中起着僞造構造方法的作用,並且它對初始化被排除在序列化外的字段很有用。

使用這 4 個屬性修飾的方法可能是私有的,如果子類需要參與其中,那麼它們可以使用相同的屬性定義自己的方法,然後它們一樣可以執行。

(P624)

數據契約序列化器也可以序列化標記了二進制序列化引擎中的屬性或接口類型。這種功能是非常重要的,因爲這是爲了支持已經被寫入 Framework 3.0 以下版本 (包括 .NET Framework) 中的二進制引擎。

下面兩項可以標記一個可被二進制引擎序列化的類型 :

  1. [Serializable] 屬性;

  2. 實現 ISerializable ;

二進制互操作性對於序列化已有類型並且需要同時支持這兩種引擎的情況比較有用。它也提供了擴展數據契約序列化器的另一種方式,因爲二進制引擎的 ISerializable 要比數據契約屬性更靈活。但是,數據契約序列化器不能通過 ISerializable 格式化添加的數據。

(P625)

數據契約序列化器的一個限制是它幾乎不能控制 XML 的結構。在一個 WCF 應用程序中,這實際上是有好處的,因爲它使得基礎結構更容易符合標準消息協議。

如果需要控制 XML 的結構,可以實現 IXmlSerializable 接口,然後使用 XmlReader 和 XmlWriter 來手動地讀和寫 XML ,數據契約序列化器僅允許在那些需要這一控制的類型上執行這些操作。

二進制序列化引擎被 Remoting 隱式地使用,它可以用來完成把對象保存到磁盤或從磁盤上還原對象之類的任務。二進制序列化被高度地自動化了,並可以用最少的操作來處理複雜的對象圖。

有兩種方式讓一個類型支持二進制序列化。第一種是基於屬性;第二種是實現 ISerializable 接口。添加屬性相對比較簡單,而實現 ISerializable 更靈活。實現 ISerializable 主要是爲了 :

  1. 動態地控制什麼要被序列化;

  2. 讓可序列化類型能夠被其他部分更友好地繼承;

一個類型可以使用單個屬性指定爲可序列化的。

[Serializable] 屬性使序列化器包含類型中所有的字段。這既包含私有字段,也包含公共字段 (但不包含屬性) 。每一個字段本身都可序列化,否則就會拋出一個異常。基本 .NET 類型,例如 string 和 int 支持序列化 (許多其他 .NET 類型也是) 。

[Serializable] 屬性不能被繼承,所以子類不會自動成爲可序列化的,除非也在子類上標記上這個屬性。

對於自動屬性,二進制序列化引擎會序列化底層的被編譯出的字段。但是,當增加屬性時,重新編譯這個類型會改變這個字段的名稱,這就會破壞已序列化數據的兼容性。處理方法就是在 [Serializable] 的類型裏避免使用自動屬性或者實現 ISerializable 接口。

(P626)

爲了序列化一個實例,可以實例化一個格式化器,然後調用 Serialize 方法。在二進制引擎中有兩個可用的格式化器 :

  1. BinaryFormatter —— 兩者之中效率稍高,在更少的時間裏產生更小的輸出。它的命名空間是 System.Runtime.Serialization.Formatters.Binary ,程序集爲 mscorlib 。

  2. SoapFormatter —— 它支持在使用 Remoting 時基本的 SOAP 樣式的消息。它的命名空間是 System.Runtime.Serialization.Formatters.Soap ,程序集爲 System.Runtime.Serialization.Formatters.Soap.dll ;

SoapFormatter 沒有 BinaryFormatter 實用。 SoapFormatter 不支持泛型或者篩選對版本容差有必要的額外數據。

反序列化器在重新創建對象時會繞過所有的構造方法。在這個過程中實際調用了 FormatterServices.GetUninitializedObject 方法來完成這個工作。可以自己調用這個方法來實現可能會非常複雜的設計模式。

序列化過的數據包含類型和程序集的全部信息,所以如果試圖把序列化的結果轉換到一個不同程序集中的類型,結果會產生一個錯誤。在反序列化過程中,序列化器會完全恢復對象引用到序列化的狀態。集合同樣如此,它會對集合像其他類型一樣處理 (所有在 System.Collections.* 下的類型都被標記爲可序列化) 。

二進制引擎可以處理大且複雜的對象圖而不需要特別輔助 (不用保證所有參與的成員都可序列化) 。唯一要注意的是,序列化器的性能會隨着對象圖的引用數量的增加而降低。這樣在一個要處理大量併發請求的 Remoting 服務器上就會成爲一個問題。

(P627)

不同於數據契約對要序列化的字段使用選擇性加入方針,二進制引擎使用選擇性排除方針。

對於不想序列化的字段,必須顯式地使用 [NonSerialized] 屬性來標記它們。

不序列化的成員在反序列化後總是爲空或 null ,即使在構造方法或字段初始化器中設置了它們。

(P628)

二進制引擎也支持 [OnSerializing] 和 [OnSerialized] 屬性,這兩個屬性用來標記在序列化之前或之後要被調用的方法上。

默認,添加一個字段會破壞已經序列化的數據的兼容性,除非新的字段附加了 [OptionalField] 屬性。

(P629)

版本健壯性十分重要,避免重命名和刪除字段,同時避免追溯性地添加 [NonSerialized] 屬性,永遠不要改變字段的類型。

如果在雙向通信時,要求版本健壯性,必須使用二進制格式化器,否則需要通過實現 ISerializable 來手動地控制序列化。

實現 ISerializable 可以讓一個類型完全控制其二進制序列化和反序列化。

GetObjectData 在序列化時被觸發,它的任務就是把想序列化的所有字段存放到 SerializationInfo (一個 名稱 / 值 的字典) 對象裏。

(P630)

把 GetObjectData 方法設置爲 virtual 可以讓子類擴展序列化而不用重新實現這個接口。

SerializationInfo 也包含相應的屬性以用來控制實例應該反序列化的類型和程序集。

StreamingContext 參數是它包含的結構,一個枚舉值指示這個序列化的實例保存的位置 (磁盤、 Remoting 等,儘管這個值不總是有) 。

除了實現 ISerializable ,一個控制其序列化的類型也需要提供一個反序列化構造方法,這個方法包含和 GetObjectData 方法一樣的兩個參數。構造方法可以被聲明爲任何訪問級別,運行時總能夠找到它。特別是,可以聲明它爲 protected 級別,這樣子類就可以調用它了。

(P632)

Framework 提供了專門的 XML 序列化引擎,即在 System.Xml.Serializaion 命名空間下的 XmlSerializer 。它適合把 .NET 類型序列化爲 XML 文件,它也被 ASMX Web 服務隱式地使用。

和二進制類似,可以使用以下兩種方式 :

  1. 在類型上使用定義在 System.Xml.Serialization 上的屬性;

  2. 實現 IXmlSerializable ;

然而不同於二進制引擎,實現接口 (例如 IXmlSerializable ) 就會完全避開引擎,要完全使用 XmlReader 和 XmlWriter 來實現序列化。

爲了使用 XmlSerializer ,要實例化它,並調用 Serialize 和 Deserialize 方法傳入 Stream 和對象實例。

(P633)

Serialize 和 Deserialize 方法可以與 Stream 、 XmlWriter / XmlReader 或者 TextWriter / TextReader 一起工作。

XmlSerializer 可以序列化沒有標記任何屬性的類型。

默認,它會序列化類型上的所有公共字段和屬性。

可以使用 [XmlIgnore] 屬性來排除不想被序列化的成員。

不同於其他兩個引擎, XmlSerializer 不能識別 [OnDeserializing] 屬性,在反序列化時依賴於一個無參數的構造方法,如果沒有無參的構造方法,就會拋出一個異常。

儘管 XmlSerializer 可以序列化任何類型,但是它會識別以下類型,並且會進行特殊的處理 :

  1. 基本類型、 DateTime 、 TimeSpan 、 Guid 以及這些類型的可空類型版本;

  2. Byte[] (它會被轉化爲 base64 編碼) ;

  3. 一個 XmlAttribute 或者 XmlElement (它們的內容會被注入到流中) ;

  4. 任何實現了 IXmlSerializable 的類型;

  5. 任何集合類型;

XML 反序列化器允許版本容差 : 如果缺少元素或屬性,或者有多餘的數據出現,它都可以正常工作。

(P634)

字段和屬性默認都被序列化爲 XML 元素。

默認的 XML 命名空間爲空 (不同於數據契約序列化器使用類型的命名空間) 。

爲了指定一個 XML 命名空間, [XmlElement] 和 [XmlAttribute] 都接受一個 Namespace 的參數。也可以對類型本身使用 [XmlRoot] 來給它分配名稱和命名空間。

XmlSerializer 會按照成員在類中定義的順序寫元素。可以通過在 XmlElement 屬性上指定 Order 值來改變這個順序。

一旦使用了 Order ,所有要序列化的成員都得使用。

而反序列化器並不關心元素的順序,不管元素以任何順序出現,類型總能夠被恰當地反序列化。

(P635)

XmlSerializer 會自動地遞歸對象引用。

(P636)

如果有兩個屬性或字段引用了相同的對象,那麼這個對象會被序列化兩次。如果想保留引用相等性,必須使用其他的序列化引擎。

(P637)

XmlSerializer 識別和序列化具體的集合類型,而不需要其他干涉。

(P640)

實現 IXmlSerializable 的規則如下 :

  1. ReadXml 應該讀取最外層起始元素,然後讀取內容,最後纔是最外層結束元素;

  2. WriteXml 應該只寫入內容;

通過 XmlSerializer 序列化和反序列化時會自動調用 WriteXml 和 ReadXml 方法。

【第18章】

(P641)

程序集是 .NET 中的基本部署單元,也是所有類的容器。

程序集包含已編譯的類和它們的 IL 代碼、運行時資源,以及用於控制版本、安全性和引用其他程序集的信息。

程序集也爲類解析和安全許可定義了邊界。

一般來說,一個程序集包含單個 PE (Windows Portable Executable ,可移植的執行體) 文件,如果是應用程序,則帶有 .exe 擴展名;如果是可重用的庫,則擴展名爲 .dll 。

程序集包含 4 項內容 :

  1. 一個程序集清單 —— 向 .NET 運行時提供信息,例如程序集的名稱、版本、請求的權限以及引用的其他程序集;

  2. 一個應用程序清單 —— 向操作系統提供信息,例如程序集應該被如何部署和是否需要管理提升;

  3. 一些已編譯的類 —— 程序集中定義的類的 IL 代碼和元數據;

  4. 資源 —— 嵌入程序集中的其他數據,例如圖像和可本地化的文本;

所有這些內容中,只有程序集清單是必需的,儘管程序集幾乎總是包含已編譯的類。

程序集不管是可執行文件還是庫,結構是類似的。主要的不同點是,可執行文件定義一個入口點。

(P641)

程序集清單有兩個目的 :

  1. 向託管宿主環境描述程序集;

  2. 到程序集中模塊、類和資源的目錄;

因此,程序集是自描述的。

(P642)

消費者可以發現程序集的數據、類和函數等所有內容,無需額外的文件。

程序集清單不是顯式地添加到程序集的,而是作爲編譯的一部分自動嵌入到程序集中的。

下面總結了程序集清單中存儲的主要數據 :

  1. 程序集的簡單名稱;

  2. 版本號 (AssemblyVersion) ;

  3. 程序集的公共密鑰和已簽名的散列 (如果是強命名的) ;

  4. 一系列引用的程序集,包括它們的版本和公共密鑰;

  5. 組成程序集的一系列模塊;

  6. 程序集定義的一系列類和包含每個類的模塊;

  7. 一組可選的由程序集要求或拒絕的安全權限 (AssemblyPermission) ;

  8. 附屬程序集針對的文化 (AssemblyCulture) ;

清單也可以存儲以下信息數據 :

  1. 完整的標題和描述 (AssemblyTitle 和 AssemblyDescription) ;

  2. 公司和版權信息 (AssemblyCompany 和 AssemblyCopyright) ;

  3. 顯式版本 (AssemblyInformationVersion) ;

  4. 自定義數據的其他屬性;

這些數據有些來自提供給編譯器的參數,其他的數據來自程序集屬性 (括號中的內容) 。

可以利用 .NET 工具 ildasm.exe 查看程序集清單的內容。

可以利用程序集屬性指定絕大部分清單內容。

這些聲明通常都定義在項目的一個文件中。

Visual Studio 爲此對每個新 C# 項目都在 Properties 文件夾中自動創建一個名爲 AssemblyInfo.cs 的文件,預定義了一組默認的程序集屬性,爲進一步的自定義提供起點。

應用程序清單是一個 XML 文件,它向操作系統提供關於程序集的信息。如果存在的話,應用程序清單在 .NET 託管宿主環境加載程序集之前被讀取和處理,因而可以影響操作系統如何啓動應用程序的進程。

(P643)

Metro 應用有更詳細的配置清單,它包含程序功能聲明,它決定了操作系統所分配的權限。編輯這個文件的最簡單方法是使用 Visual Studio ,雙擊配置清單文件就可以顯示編輯界面。

可以用兩種方式部署 .NET 應用程序清單 :

  1. 作爲程序集所在文件夾中的一個特殊命名的文件;

  2. 嵌入程序集中;

作爲一個單獨的文件,其名稱必須匹配程序集的名稱,後綴爲 .manifest 。

.NET 工具 ildasm.exe 對嵌入式應用程序清單的存在視而不見。但是如果在 Solution Explorer 中雙擊程序集, Visual Studio 會指出嵌入式應用程序清單是否存在。

程序集的內容實際上存儲在一個或多個稱爲模塊的中間容器中。

一個模塊對應於一個包含程序集內容的文件。

採用額外的容器層的原因是,爲了在構建包含多種編程語言中編譯的代碼的程序集時,允許程序集跨多個文件,這是一個很有用的特性。

(P644)

在多文件程序集中,主模塊總是包含清單;其他的模塊可以包含 IL 和資源。清單描述組成程序集的所有其他模塊的相對位置。

多文件程序集必須從命令行編譯, Visual Studio 中不支持。

爲了編譯程序集,需要利用 /t 開關調用 csc 編譯器來創建每個模塊,然後再用程序集鏈接器工具 al.exe 將它們鏈接起來。

儘管很少有需要多文件程序集的情況,即使在處理單模塊程序集時,但是時常需要了解模塊這一額外的容器層。主要應用場景跟反射有關。

System.Refelction 中的 Assembly 類是在運行時訪問程序集元數據的入口。

有很多方式可以獲得程序集對象,最簡單的方式是通過 Type 的 Assembly 屬性。

(P645)

也可以通過調用 Assembly 的靜態方法來獲得 Assembly 對象 :

  1. GetExecutingAssembly —— 返回定義當前正在執行的函數的程序集;

  2. GetCallingAssembly —— 跟 GetExecutingAssembly 執行相同的操作,但是針對的是調用當前正在執行的函數的函數;

  3. GetEntryAssembly —— 返回定義應用程序初始入口方法的程序集;

一旦有了 Assembly 對象,就可以使用它的屬性和方法來查詢程序集的元數據和反射它的類。

程序集成員 :

  1. FullName 、 GetName —— 返回完全限定的名稱或者 AssemblyName 對象;

  2. CodeBase 、 Location —— 程序集文件的位置;

  3. Load 、 LoadFrom 、 LoadFile —— 手動將程序集加載到當前應用程序域中;

  4. GlobalAssemblyCache —— 指出程序集是否定義在 GAC 中;

  5. GetSatelliteAssembly —— 找到給定文化的衛星程序集;

  6. GetType 、 GetTypes —— 返回定義在程序集中的一個或所有類;

  7. EntryPoint —— 返回應用程序的入口方法,例如 MethodInfo ;

  8. GetModules 、 ManifestModule —— 返回程序集的所有模塊或主模塊;

  9. GetCustomAttributes —— 返回程序集的屬性;

強命名的程序集具有唯一的、不可更改的身份。通過向清單添加以下兩類元數據來實現 :

  1. 屬於程序集創作者的唯一編號;

  2. 程序集的已簽名散列,證實程序集產生的唯一編號持有者;

這需要一個 公共 / 私有 密鑰對。公共密鑰提供唯一的身份識別號,私有密鑰幫助簽名。

強名稱簽名不同於 Authenticode 簽名。

公共密鑰對於保證程序集引用的唯一性有價值 : 強命名的程序集將公共密鑰合併到它的身份中。簽名對於安全性有價值,它防止惡意人員篡改程序集。沒有私有密鑰,無法發佈程序集的修改版本時不出現其簽名中斷 (導致加載時錯誤) 。

(P646)

向弱命名的程序集添加一個強名稱會更改它的身份。因此,有必要一開始就給生產型程序集 (Production Assembly) 命名一個強名稱。

強命名的程序集也可以註冊在 GAC 中。

要給程序集命名一個強名稱,首先利用實用工具 sn.exe 生成一個 公共 / 私有 密鑰對。

強命名的程序集不能引用弱命名的程序集。這是要強命名所有生產型程序集的另一個重要原因。

每個程序集具有一個獨立的密鑰對是有利的,在以後轉移某個特定應用程序 (以及它引用的程序集) 的所有權時,可以做到最小暴露。但是使得創建可以識別所有程序集的安全策略更難了,也使得驗證動態加載的程序集更爲困難了。

在有數百個開發人員的組織中,你可能想要限制對程序集進行簽名的密鑰對的訪問,原因有兩個 :

  1. 如果密鑰對泄露,你的程序集就不再是不可篡改的了;

  2. 測試程序集如果已簽名和泄露,就會被惡意地宣稱爲真正的程序集;

延遲簽名的程序集用正確的公共密鑰進行標記,但是沒有用私有密鑰簽名。

(P647)

延遲簽名的程序集相當於被篡改的程序集,通常會被 CLR 拒絕。

要延遲簽名,需要一個只包含公共密鑰的文件。

必須從命令行手動禁用程序集驗證,否則,程序集將不會執行。

程序集的身份包含四種來自其清單的元數據 :

  1. 它的簡單名稱;

  2. 它的版本 (如果未指定,就是 0.0.0.0 ) ;

  3. 它的文化 (如果不是衛星程序集, 就是 neutral) ;

  4. 它的公共密鑰標記 (如果不是強命名的, 就是 null) ;

(P648)

完全限定程序集名稱是一個包含 4 個身份識別組件的字符串。

如果程序集沒有 AssemblyVersion 屬性,則版本顯示爲 “0.0.0.0” 。如果未簽名,則其公共密鑰標記顯示爲 “null” 。

Assembly 對象的 FullName 屬性返回它的完全限定名稱。編譯器在清單中記錄程序集引用時總是使用完全限定名稱。

完全限定程序集名稱不包含它在磁盤上的目錄路徑。

AssemblyName 類的完全限定程序集名稱的每一個組件都具有一個類型化屬性。 AssemblyName 有兩個目的 :

  1. 解析或構建完全限定程序集名稱;

  2. 存儲一些額外的數據,以幫助解析 (尋找) 程序集;

可以通過以下三種方式獲得 AssemblyName :

  1. 實例化一個 AssemblyName ,提供完全限定名稱;

  2. 在一個現有 Assembly 上調用 GetName ;

  3. 調用 AssemblyName.GetAssemblyName ,提供到磁盤上程序集文件的路徑;

(P649)

可以不用任何參數實例化一個 AssemblyName ,然後設置它的每個屬性以構建完全限定名稱。以這種方式構造的 AssemblyName 是易變的。

Version 本身是一個強類型化的表示,具有 Major 、 Minor 、 Build 和版本號屬性。

GetPublicKey 返回完全加密的公共密鑰。

GetPublicToken 返回建立身份時使用的最後 8 個字節。

由於版本是程序集名稱的一個有機部分,所以改變 AssemblyVersion 屬性就會改變程序集的身份。這將影響與引用程序集的兼容性,在不間斷的更新中會出現意想不到的情況。要解決這個問題,有以下兩個獨立的程序集級別的屬性用於表示與版本相關的信息,兩者都被 CLR 省略 :

  1. AssemblyInformationVersion —— 顯示給最終用戶的版本。這在 “Windows File Properties” 對話框中作爲 “Product Version” 出現。可以包含任何字符串。通常程序中的所有程序集會被分配相同的信息版本號;

  2. AssemblyFileVersion —— 用於引用此程序集的構建號。這在 “Windows File Properties” 對話框中作爲 “File Version” 出現。跟 AssemblyVersion 一樣,它必須包含一個字符串,最多由 4 個用句點分隔的數字組成;

Authenticode 是一個代碼簽名系統,其目的是證明發行商的身份。

Authenticode 和強名稱簽名是獨立的,可以用任何一個或同時用兩個系統對程序集進行簽名。

(P651)

如果還想對程序集進行強名稱簽名 (強烈推薦) ,那麼必須在 Authenticode 簽名之前進行強名稱簽名。

(P652)

最好避免對 .NET 3.5 或更早的程序集進行 Authenticode 簽名。

作爲安裝 .NET Framework 的一部分,在計算機上創建一箇中心倉庫,用於存儲 .NET 程序集,這就是所謂的全局程序集高速緩存 ( Global Assembly Cache , GAC) 。 GAC 包含 .NET Framework 本身的一個集中副本,並且它也可以用來集中自定義的程序集。

(P653)

對於非常大的程序集, GAC 可以縮短啓動時間,因爲 CLR 只需要在安裝時驗證一次 GAC 中程序集的簽名,而不是每次加載程序集時都要驗證。按百分比來說,如果用 ngen.exe 工具爲程序集生成了本機映射 (選擇非重疊的基地址) ,就會有這一優勢。

GAC 中的程序集總是完全受信任的,即使是從運行在受限的沙箱中調用程序集。

要將程序集安裝到 GAC ,第一步是給程序集命名一個強名稱。

(P654)

應用程序通常不僅僅包含可執行代碼,還包含諸如文本、圖像或 XML 文件等內容。這些內容可以表示爲程序集中的資源。資源有兩個重疊的用例 :

  1. 合並不能進入源代碼的數據,例如圖像;

  2. 存儲在多語言應用程序中可能需要轉換的數據;

程序集資源最終是一個帶有名稱的字節流,可以將程序集看作包含一個按字符串排列的字節數組字典。

Framework 可以通過中間的 .resources 容器添加內容。一些容器包含可能需要轉換成不同語言的內容。

(P655)

本地化的 .resources 可打包爲在運行時根據用戶的操作系統語言被自動挑選的單個衛星程序集。

要使用 Visual Studio 直接嵌入資源 :

  1. 將文件添加到項目;

  2. 將構建操作設置爲 “Embedded Resource” ;

資源名稱區分大小寫,所以 Visual Studio 中包含資源的項目子文件夾名稱也區分大小寫。

(P656)

要獲得一個資源,可以在包含該資源的程序集上調用 GetManifestResourceStream ,返回一個流,然後可以將其讀作任何其他名字。

GetManifestResourceNames 返回程序集中所有資源的名稱。

.resources 文件包含的是潛在地可本地化的內容。 .resources 文件最終是程序集中的一個嵌入式資源,就像任何其他類型的文件一樣。區別在於必須 :

  1. 首先將內容打包到 .resources 文件中;

  2. 通過 ResourceManager 或 pack URI 而不是 GetManifestResourceStream 訪問它的內容;

.resources 文件的結構形式是二進制的,所以不是可讀的;因此,必須依賴於 Framework 或 Visual Studio 提供的工具來處理它們。

處理字符串或簡單數據類的標準方法是使用 .resx 格式,該格式可以通過 Visual Studio 或 resgen 工具轉換成 .resources 文件。

.resx 格式也適合於針對 Windows Forms 或 ASP.NET 應用程序的圖像。

在 WPF 應用程序中,必須對需要由 URI 引用的圖像或類似的內容使用 Visual Studio 的 “Resource” 構建操作。無論是否需要本地化,這一點都是適用的。

(P657)

.resx 文件是一種用於生成 .resources 文件的設計時格式。

.resx 文件使用 XML 通過 名 / 值 對進行構造。

要在 Visual Studio 中創建 .resx 文件,可以添加一個 “Resource File” 類的項目條目。其他工作都是自動完成的 :

  1. 創建正確的頭部;

  2. 設計器提供用於添加字符串、圖像、文件和其他類型的數據;

  3. .resx 文件自動轉轉成 .resources 格式,並在編譯時嵌入到程序集中;

  4. 編寫一個類用於以後訪問數據;

資源設計器將圖像添加爲類型化的 Image 對象 (System.Drawing.dll) ,而不是作爲字節數組,這使得它們不適用於 WPF 應用程序。

(P659)

可以簡單地通過添加新衛星程序集而增強語言支持,無需更改主程序集。

衛星程序集不能包含可執行代碼,只能包含資源。

衛星程序集部署在程序集文件夾的子目錄中。

(P661)

文化分成文化和子文化。一種文化代表一種特定的語言;一種子文化代表該語言的一個地區變種。

在 .NET 中用 System.Globalization.CultureInfo 類表示文化,可以檢查應用程序的當前文化。

CurrentCulture 反映 Windows 控制面板的區域設置,而 CurrentUICulture 反映操作系統的語言。

一個典型的應用程序包含一個可執行的主程序集和一組引用的庫程序集。

程序集解析是指定位所引用程序集的過程。

程序集解析發生在編譯時和運行時。

(P662)

在自定義程序集加載和解析方面, Metro 應用只有很少的支持。特別是,它從不支持從任意文件位置加載程序集,而且沒有 AssemblyResolve 事件。

所有類都在程序集範圍內。

程序集就像類的地址。

程序集組成類的運行時身份的重要部分。

程序集也是類到它的代碼和元數據的句柄。

AssemblyResolve 事件允許干預和手動加載 CLR 找不到的程序集。如果處理該事件,可以在各個位置散發引用的程序集,並加載它們。

在 AssemblyResolve 事件處理程序中,通過調用 Assembly 類中三個靜態方法 ( Load 、LoadFrom 或 LoadFile ) 中的一個,找到並加載程序集。這些方法返回對新加載的程序集的引用,然後再返回給調用者。

(P663)

ResolveEventArgs 事件比較特殊,因爲它具有返回類。如果有多個處理程序,那麼第一個返回非空 Assembly 的程序優先。

Assembly 類中的三個 Load 方法在 AssemblyResolve 處理程序內部和外部都很有用。在事件處理程序外部時,它們可以加載和執行編譯時沒有引用的程序集。可能會加載程序集的一個示例情況是在執行插件時。

在調用 Load 、 LoadFrom 或 LoadFile 之前慎重考慮 : 這些方法將程序集永久地加載到當前應用程序域,即使不對產生的 Assembly 對象執行任何操作。加載程序集具有一些副作用 : 它會鎖定程序集文件,還會影響後續的類解析。

卸載程序集的唯一方式是卸載整個應用程序域 (另一個避免鎖定程序集的方法是對檢測路徑的程序集執行陰影拷貝 (shadow copying)) 。

如果只想檢查一個程序集,不想執行它的任何代碼,那麼可以加載到只反射上下文中。

要從完全限定名稱 (不帶位置) 加載程序集,可調用 Assembly.Load 。這指示 CLR 使用普遍自動解析系統尋找程序集。 CLR 本身使用 Load 尋找所引用的程序集。

要從文件名加載程序集,可調用 LoadFrom 或 LoadFile 。

要從 URI 加載程序集,可調用 LoadFrom 。

要從字節數組加載程序集,可調用 Load 。

通過調用 AppDomain 的 GetAssemblies 方法,可以看到哪些程序集當前被加載到內存中。

LoadFrom 和 LoadFile 都可以從文件名加載程序集。它們有兩點區別。首先,如果有一個相同身份的程序集從另一個位置加載到了內存中,那麼 LoadFrom 提供前一副本。

LoadFile 提供新副本。

但是,如果從同一位置加載了兩次,那麼兩種方法都提供前一次已緩存的副本。

相反,從同一字節數組兩次加載一個程序集,會提供兩個不同的 Assembly 對象。

(P664)

在內存中,來自 2 個相同程序的類型是兼容的,這是避免加載重複程序集的主要原因,也是儘量使用 LoadFrom 而不使用 LoadFile 的原因。

LoadFrom 和 LoadFile 的另一個區別是, LoadFrom 會告訴 CLR 前向引用的位置,而 LoadFile 則不會。

如果直接在代碼中引用一個類型,那麼就稱爲靜態引用 (statically referencing) 該類型。編譯器會將該類型的引用添加到正在編譯的程序集中,以及包含該類型的程序集名稱 (但是不包含如何在運行時尋找該類型的信息) 。

在解析靜態引用時, CLR 會先檢查 GAC ,然後檢查檢測路徑 (通常是應用的基目錄) ,最後觸發 AssemblyResolve 事件。但是,在這些操作之前,它會先檢查程序集是否已經加載。然而,它只考慮以下情況的程序集 :

  1. 已經從一個路徑加載,否則就會出現在自己的路徑上 (檢測路徑) ;

  2. 已經從 AssemblyResolve 事件的響應中加載;

在調用 LoadFrom / LoadFile 時必須非常小心,要先檢查程序集是否已經存在於應用的基目錄 (除非確實想加載同一個程序集的多個版本) 。

(P665)

如果在 AssemblyResolve 事件響應中加載,則不存在這個問題 (無論是使用 LoadFrom 、 LoadFile 或後面將會介紹的從字節數組加載), 因爲事件只觸發檢測路徑之外的程序集。

無論使用 LoadFrom 還是 LoadFile , CLR 都一定會先在 GAC 中查找所請求的程序集。

使用 ReflectionOnlyLoadFrom (它會將程序加載到只有反映的環境中), 可以跳過 GAC 。

程序集的 Location 屬性通常會返回其在文件系統的物理位置 (如果有) 。

而 CodeBase 屬性則以 URI 形式映射這個位置。

如果要尋找程序集在磁盤的位置,只使用 Location 是不可靠的。更好的方法是同時檢查兩個屬性。

【第19章】

(P670)

在運行時檢查元數據和編譯代碼的操作稱爲 “反射” 。

System.Type 的實例代表了類型的元數據。因爲 Type 的應用領域非常廣泛,所以它存在於 System 命名空間中,而非 System.Reflection 命名空間中。

通過調用對象上的 GetType 或者使用 C# 的 typeof 運算符,可以獲得 System.Type 實例。

(P671)

還可以通過名稱獲取類型。如果引用了該類型的程序集。

如果沒有程序集對象,可以通過其程序集限定名稱獲取類型 (該類型的全稱會帶有程序集完整的限定名稱)。

一旦擁有了 System.Type 對象,就可以使用它的屬性訪問類型的名稱、程序集、基礎類型、可見性等。

一個 System.Type 實例就是打開類型 (及其定義的程序集) 的全部元數據的一個入口。

System.Type 是個抽象的概念,因此實際上 typeof 運算符獲得的是 Type 子類。對於 mscorlib 來說, CLR 使用的這些子類都是內部的,稱爲 RuntimeType 。

Metro 應用模板隱藏了大多數類型成員,轉而將它們封裝在 TypeInfo 類中。調用 GetTypeInfo ,就可以得到這個類。

完整的 .NET 框架也包含 TypeInfo ,所以能在 Metro 中正常運行的代碼也可以在標準庫 .NET 應用中運行,但是隻適用於 Framework 4.5 (舊版本不支持) 。

(P672)

TypeInfo 還包含其他一些反射成員的屬性和方法。

Metro 應用只實現了有限的反射機制。特別是它們無法訪問非公共成員類型,也無法使用 Reflection.Emit 。

可以將 typeof 和 GetType 與數組類型一起使用。還可以通過調用元素類型上的 MakeArrayType 獲取數組類型。

可以向 MakeArray 傳遞整型參數,以創建多維矩形數組。

GetElementType 返回數組的元素類型。

GetArrayRank 返回矩形數組的維數。

要重新獲得嵌套類型,可調用包含類型的 GetNestedTypes 。

在使用嵌套類型時需要特別注意的是 CLR 會認爲嵌套類型擁有特定 “嵌套” 可訪問等級。

類型具有 Namespace 、 Name 和 FullName 特性。在大多數情況中, FullName 是前兩者的組合。

Type 還具有 AssemblyQualifiedName 特性,使用它可以返回帶有逗號和其程序集完整名稱的 FullName 值。同樣可以將該字符串傳遞給 Type.GetType ,然後會在默認的加載環境中單獨獲取類型。

(P673)

對於嵌套類型來說,包含類型僅在 FullName 中出現。

  • 表示將包含類型與嵌套的命名空間區分開。

泛型類型名稱帶有‘後綴,還帶有類型參數的編號。如果泛型類型被綁定,那麼該法則同時應用於 Name 和 FullName 。

然而,如果該泛型類型是封閉式的, FullName (僅僅) 獲得基本的額外附加信息。

數組通過在 typeof 表達式中使用的相同後綴表示。

指針類型也與數組類似。

描述 ref 和 out 參數的類型帶有 & 後綴。

(P674)

類型可以公開 BaseType 特性。

GetInterfaces 方法會返回類型實現的接口。

反射爲 C# 的靜態 is 運算符提供了兩種等價的動態運算符 :

  1. IsInstanceOfType —— 可以接收類型和實例;

  2. IsAssignableFrom —— 可以接收兩個類型;

可以使用兩種方法通過對象的類型動態地實例化對象 :

  1. 調用靜態 Activator.CreateInstance 方法;

  2. 調用 ConstructorInfo 對象上的 Invoke , ConstructorInfo 對象是通過調用類型 (高級環境) 上的 GetConstructor 獲得的;

Activator.CreateInstance 可以接收已傳遞到構造方法的 Type 和可選的參數。

(P675)

使用 CreateInstance 可以設定許多其他選項,如用於加載類型的程序集、目標應用程序域和是否與非全局構造方法綁定。如果運行時無法找到適當的構造方法,那麼會拋出 MissingMethodException 。

當參數值無法在重載的構造方法之間消除時,必須調用 ConstructorInfo 上的 Invoke 。

當類型不明確時,應該將一個 null 參數傳遞給 Activator.CreateInstance 。在這種情況需要使用 ConstructorInfo 進行替換。

在構造對象時進行動態實例化會增加幾微妙的時間。相對而言這是一個較長的時間,因爲 CLR 實例化對象的速度非常快 (在小型類上簡單的 new 操作不足十納秒) 。

要根據元素類型動態實例化數組,應首先調用 MakeArrayType 。

(P676)

Type 可以代表封閉式或未綁定的泛型類型。

在編譯時,封閉式泛型類型可以實例化,而未綁定的類型不能實例化。

MakeGenericType 方法可以將未綁定的泛型類型轉換爲封閉式泛型類型。只需傳遞需要的類型參數就可以實現。

使用 GetGenericTypeDefinition 方法可以實現相反的操作。

當 Type 爲泛型時, IsGenericType 會返回 true ,而當泛型類型爲未綁定時, IsGenericTypeDefinition 會返回 true 。

GetGenericArguments 可以爲封閉式泛型類型返回類型參數。

對於未綁定的泛型類型來說, GetGenericArguments 會返回在泛型類型定義中指定爲佔位符類型的僞類型。

在運行時,所有泛型類型不是未綁定的就是封閉式的。

在 typeof(Foo<>) 這類表達式中泛型類型是未綁定的 (相對來說這種情況比較常見);在其他情況中,泛型類型是封閉式的。

在運行時不存在開放式泛型類型 : 所有開放式類型都會被編譯器關閉。

(P677)

使用 GetMembers 方法可以返回類型的成員。

TypeInfo 提供了另一個 (更簡單的) 成員反射協議。這個 API 對於目標平臺爲 Framework 4.5 的應用是可選的,而 Metro 應用則是強制選擇的,因爲 Metro 應用沒有與 GetMethods 方法等價的方法。

TypeInfo 並沒有像 GetMethods 這樣可以返回數組的方法,而只有返回 IEnumerable<T> 的屬性,它們一般用於運行 LINQ 查詢。其中使用最廣泛的是 DeclaredMembers 。

如果在調用時沒有使用參數, GetMembers 會返回類型 (及其基本類型) 的所有公共成員。

GetMember 通過名稱檢索特定成員,但是因爲成員可能會被重新加載, GetMember 仍舊會返回一個數組。

(P678)

MemberInfo 也具有 MemberTypes 類型的 MemberType 特性。

下面列出的是該特性的典型值 : All 、 Custom 、 Field 、 NestedType 、 TypeInfo 、 Constructor 、 Event 、 Method 、 Property ;

當調用 GetMembers 時,可以傳遞一個 MemberTypes 實例,以限定它返回的成員類型。還可以通過調用 GetMethods 、 GetFields 、 GetProperties 、 GetEvents 、 GetConstructors 或 GetNestedTypes ,限定返回的結果。這些方法還有專門用於特定成員的版本。

對類型的成員進行檢索時應儘可能地具體,因而如果要在以後添加成員,就無需拆分代碼。如果要通過名稱檢索方法,指定所有參數類型可以確保出現方法重載時,代碼仍舊可以運行。

MemberInfo 對象具有 Name 特性和以下兩個 Type 特性 :

  1. DeclaringType —— 返回定義該成員的類型;

  2. ReflectedType —— 根據所調用種類的 GetMembers 返回類型;

當根據基礎類型定義的成員進行調用時,會出現兩種不同情況 : DeclaringType 會返回基礎類型;而 ReflectedType 會返回子類型。

(P679)

MemberInfo 還定義了用於返回自定義屬性的方法。

MemberInfo 本身在成員中不重要,因爲它是類型的概要基礎。

可以根據 MemberInfo 的 MemberType 特性,將 MemberInfo 投射到其子類型上。如果通過 GetMethod 、 GetField 、 GetProperty 、 GetEvent 、 GetConstructor 或 GetNestedType (或者它們的複數版本) 獲取成員,就不必進行投射。

(P680)

每個 MemberInfo 子類都具有大量特性和方法,以便公開成員元數據的可見性、修飾符、泛型類型參數、參數、返回類型和自定義屬性。

有些 C# 構造 (即索引器、枚舉、運算符和終止器) 在涉及 CLR 時就被設計出來了。尤其應該注意以下幾點 :

  1. C# 索引器可以轉換爲接收一個或多個參數的特性,而且可以標識爲類型的 [DefaultMembber] ;

  2. C# 枚舉可以通過每個成員的靜態域轉換爲 System.Enum 的子類型;

  3. C# 運算符可以轉換爲被特殊命名的靜態方法,而且帶有 “op_” 前綴;

  4. C# 析構函數可以轉換爲覆蓋 Finalize 的方法;

另一種複雜的情況是特性或事件實際上由兩部分組成 :

  1. 描述特性或事件的元數據 (由 PropertyInfo 或 EventInfo 封裝) ;

  2. 一個或兩個反向方法 (backing Method) ;

在 C# 程序中,反向方法被封裝在特性或事件定義中。但是當將它們編譯爲 IL 時,反向方法會被表示爲原始方法,而且可以像其他方法那樣調用。這意味着 GetMethods 會返回與原始方法並列的特性和事件反向方法。

(P681)

既可以爲未綁定的泛型類型獲取成員元數據,也可以爲封閉式泛型類型獲取成員元數據。

從未綁定的和封閉式泛型類型返回的 MemberInfo 對象總是獨特的,即使對於簽名中不帶泛型類型參數的成員也是如此。

未綁定泛型類型的成員不能被動態調用。

(P682)

一旦擁有了 MemberInfo 對象,就可以動態地調用它或者 獲取 / 設置 它的值。這種操作稱爲動態綁定或後期綁定,因爲要在運行時選擇調用成員,而不是在編譯時選擇調用成員。

使用 GetValue 和 SetValue 可以獲取和設置 PropertyInfo 或 FieldInfo 的值。

要動態調用方法 (如在 MethodInfo 上調用 Invoke) ,應爲該方法提供一組參數。如果參數類型錯誤,那麼在運行時就會出現異常。在進行動態調用時,會失去編譯時的類型安全,但是仍舊可以擁有運行時的類型安全 (就像使用 dynamic 關鍵字一樣) 。

(P688)

通過調用 Assembly 對象上的 GetType 或 GetTypes ,可以動態反射程序集。

GetTypes 僅會返回頂級類型和非嵌套類型。

(P694)

System.Reflection.Emit 命名空間含有用於在運行時創建元數據和 IL 的類。

(P697)

IL 中沒有 while 、 do 和 for 循環;這些循環是通過標籤、相等 goto 和條件 goto 語句實現的。

(P698)

new 等價於 IL 中的 Newobj 操作碼。

【第20章】

(P718)

C# 依靠動態語言運行時 (DLR) 執行動態綁定。

Framework 4.0 是第一個帶有 DLR 的 Framework 版本。

(P719)

每種對動態綁定提供支持的語言都會提供專門的綁定器,以幫助 DLR 以專門方式爲該語言解釋表達式。

(P724)

C# 的靜態類型化嚴格說是一把雙刃劍。一方面,它在編譯時保證程序的正確性。另一方面,它偶爾會導致編碼困難或無法使用代碼進行表述,在這種情況中必須使用反射,動態綁定比反射更清晰、更快速。

(P726)

對象可以通過實現 IDynamicMetaObjectProvider 提供其綁定語義 (或者通過子類化 DynamicObject 更容易地提供其綁定語義, DynamicObject 提供了對該接口的默認實現) 。

(P729)

真正的動態語言 (如 IronPython 和 IronRuby) 確實允許執行隨機字符串。而且該功能對一些任務 (如編寫腳本、動態配置和實現動態規則引擎) 很有用。

【第21章】

(P731)

.NET 中的權限提供了一個獨立於操作系統的安全層。其功能有兩部分 :

  1. 沙箱 —— 限制不能完全可信的 .NET 程序集可以執行的操作類型;

  2. 授權 —— 限制誰可以做什麼;

通過 .NET 中支持的加密功能可以存儲或交換機密、防偷聽、檢測信息篡改、爲存儲密碼生成單向哈希表和創建數字簽名。

Framework 對沙箱和授權都使用權限。權限根據條件阻止代碼的執行。沙箱使用代碼訪問權限;授權使用身份和角色權限。

代碼訪問安全最常通過 CLR 或託管環境 (如 ASP.NET 或 Internet Explorer) 對你進行限制,而授權通常由你實現,以防止未授權的調用程序訪問你的程序。

(P732)

身份和角色安全主要用於編寫中間層應用程序和網頁應用服務。通常可以對一組角色進行決定,然後對於提供的每個方法,可以要求調用程序爲特定角色。

(P738)

爲了幫助避免特權提升***,默認情況下 CLR 不允許部分可信的程序集調用完全可信的程序集。

(P753)

System.Security.Cryptography 中的大多數類型位於 mscorlib.dll 和 System.dll 中。 ProtectedData 是一個例外,它位於 System.Security.dll 中。

(P754)

散列法提供了一種加密方式。這種加密方式非常適用於存儲數據庫中的密碼,因爲不需要 (或不想要) 看到解密的版本。要進行驗證,僅需散列用戶輸入的信息,然後將其與數據庫中存儲的信息相比較即可。

不論源數據的長度有多少,散列編碼永遠爲較小的固定大小。這使其在比較文件或檢查數據流 (與校驗和不同) 時發揮重要作用。源數據中更改任何位置的單個位都會使得散列編碼發生巨大的變化。

要進行散列操作,可調用 HashAlgorithm 某個子類 (如 SHA256 或 MD5) 上的 ComputeHash 。

ComputeHash 方法還可以接收字節數組,這對散列法密碼非常方便。

Encoding 對象上的 GetBytes 方法將一個字符串轉換爲一個字節數組; GetString 方法將該數組重新轉換爲字符串。然而, Encoding 對象無法將加密的或散列的字節數組轉換爲字符串,因爲編碼數據通常會破壞文本編碼規則。可以使用下列 Convert.ToBase64String 方法和 Convert.FromBase64String 方法代替。這些方法可以使用字節數組和合法 (與 XML 友好) 的字符串相互轉換。

MD5 和 SHA256 是 HashAlgorithm 的兩個子類型,它們是由 .NET Framework 提供的。下面按照安全等級的升序 (和以字節爲單位的散列長度) 列出了主要算法 :

MD5(16) -> SHA1(20) -> SHA256(32) -> SHA384(48) -> SHA512(64)

算法的長度越短,其執行速度就越快。

MD5 的執行速度比 SHA512 的執行速度快 20 多倍,而且非常適合計算文件的校驗和。

使用 MD5 每秒鐘可以加密數百兆字節,然後將結果存儲到 Guid 中 (Guid 的長度恰好爲 16 字節,而且作爲一個值類型它比字節數組更易於處理) 。然而,較短的散列會增加破解密碼的可能性 (兩個不同的文件生成相同的散列) 。

在加密密碼或其他區分安全等級的數據時,至少應該使用 SHA256 。人們認爲在這些情況中使用 MD5 、 SHA1 是不安全的, MD5 和 SHA1 僅適用於防止意外破解,而無法防止有預謀的篡改。

SHA384 的執行速度並不快於 SHA512 的執行速度,如果需要獲取比 SHA256 更高的安全性,可以使用 SHA512 。

較長的 SHA 算法適用於密碼加密,但是它們需要增強密碼策略的強度以減弱字典的威脅 (字典是指者通過對字典中的每個詞應用散列算法,創建密碼查詢表的策略) 。

(P755)

Rfc2898DeriveBytes 和 PasswordDeriveBytes 類可以準確地執行這類增加密碼長度的任務。

Framework 還提供了 160 位的 RIPEMD 散列算法,其安全性比 SHA1 稍好。但是,它會受到 .NET 低效實現的影響,這使得其執行速度比 SHA512 的執行速度更慢。

對稱加密在加密和解密時使用相同的密鑰。 Framework 提供了 4 種對稱加密算法,這些算法中 Rijndael 是最方便的。 Rijndael 既快速又安全,而且擁有兩個實現 :

  1. Rijndael 類,從 Framework 1.0 之後的版本可以使用它;

  2. Aes 類,它是在 Framework 3.5 中引入的;

除了 Aes 不允許通過更改塊尺寸消弱密碼外,這兩個類幾乎相同。

Aes 是 CLR 安全團隊推薦使用的類。

(P756)

各個類使用不同的密碼系統。 Aes 使用數據密碼系統,通過 encryptor 和 decryptor 轉換應用密碼算法;

CryptoStream 使用數據流加密算法,用於數據流加密。可以使用不同的對稱算法替換 Aes ,而仍舊需要使用 CryptoStream 。

CryptoStream 是雙向的,因此可以根據是選擇 CryptoStreamMode.Read 還是 CryptoStreamMode.Write ,讀取數據流或向數據流中寫入信息。加密機和解密機都是對讀和寫的理解,這生成了 4 種組合,這些選擇可能使人感到茫然!將讀取創建爲 “拉” 模型和將寫入創建爲 “推” 模型可以幫助理解。如果仍舊有疑問,可以將加密的寫入和解密的讀取作爲起點;這通常是最常見的方式。

使用 System.Cryptography 中的 RandomNumberGenerator 可以生成隨機密鑰或 IV 。實際上它生成的數字是無法預測的或具有密碼強度的 (System.Random 類沒有提供相同的保證) 。

使用 MemoryStream 完全可以在內存中進行加密和解密。

(P757)

CryptoStream 是一個鏈接器,它可以將其他流鏈接起來。

(P759)

公共密鑰加密是非對稱的,因此加密和解密使用不同的密鑰。

(P760)

.NET Framework 提供了許多非對稱算法,其中 RSA 是最流行的算法。

【第22章】

(P763)

同步 (Synchronization) 是指協調併發操作,實現可預測的結果。如果有多個線程訪問相同的數據,那麼同步就非常重要;這個應用領域很容易出現問題。

(P764)

排他鎖結構有三種 : lock 語句、 Mutex 和 SpinLock 。 lock 是最方便和最常用的結構 :

  1. Mutex 可以跨越多個進程 (計算機範圍的鎖) ;

  2. SpinLock 實現了微優化,可以減少高度併發場景的上下文切換;

(P765)

事實上, C# 的 lock 語句是 Monitor.Enter 和 Monitor.Exit 方法調用及 try / finally 語句塊的簡寫語法。

如果未先調用同一個對象的 Monitor.Enter ,而直接調用 Monitor.Exit ,就會拋出異常。

(P766)

爲訪問任意可寫共享域的代碼添加鎖。即使是最簡單的情況 (如某個域的賦值操作) ,也必須考慮同步問題。

(P768)

如果 lock 語句塊中拋出異常,則可能破壞通過鎖實現的原子操作。

線程可以用嵌套 (重入) 的方式重複鎖住同一個對象。

在這些情況中,只有當最外層 lock 語句退出時,或者執行相同數量的 Monitor.Exit 語句,對象纔會解除鎖。

(P769)

如果兩個線程互相等待對方所佔用的資源,就會形成死鎖,使得雙方都無法繼續執行。

死鎖是多線程中最難解決的問題 —— 特別是其中涉及許多相關對象時。基本上,最難的問題是無法確定調用獲取了哪些鎖。

(P770)

鎖的執行速度很快 : 在目前的計算機上,如果未出現爭奪者,那麼一般可以在 80 納秒內獲得和釋放一個鎖;如果出現爭奪者,那麼相應的上下文切換會將過載增加到毫秒級,但是這個時間遠遠小於線程的實際調度時間。

Mutex 類似於 C# 的鎖,但是它可以支持多個進程。換而言之, Mutex 可用於計算機範圍或應用程序範圍。

獲得和釋放一個無爭奪的 Mutex 只需要幾毫秒 —— 時間比鎖操作慢 50 倍。

使用一個 Mutex 類,就可以調用 WaitOne 方法獲得鎖,或者調用 ReleaseMutex 釋放鎖。關閉或去掉一個 Mutex 會自動釋放互斥鎖。與 lock 語句一樣, Mutex 只能在它所在的線程上釋放。

(P771)

線程安全性主要是通過鎖和減少線程交互可能性而實現。

(P789)

從 Framework 4.0 開始,我們可以使用 Lazy<T> 類實現延後初始化。

(P793)

Suspend 和 Resume 可以凍結和解凍另一個線程。雖然在概念上與阻塞不同 (可以通過它的 ThreadState 查詢) ,但是凍結的線程就像進入了阻塞狀態。與 Interrupt 一樣, Suspend / Resume 也缺少有效的用例,並且也可能存在危險;如果暫停一個獲得了鎖的線程,那麼其他線程就無法獲得這個鎖 (包括自己的鎖) ,這使得程序很容易發生死鎖。因此, Framework 2.0 廢棄了 Suspend 和 Resume 。

(P794)

.NET Framework 提供了四種定時器,以下兩種是通用的多線程定時器 :

  1. System.Threading.Timer ;

  2. System.Timers.Timer ;

其他兩種是特殊用途的單線程定時器 :

  1. System.Windows.Forms.Timer (Windows Forms 定時器) ;

  2. System.Windows.Threading.DispatcherTimer (WPF 定時器) ;

多線程定時器更加強大、精確和靈活,而在運行需要更新 Windows Forms 控件或 WPF 元素的簡單任務時,單線程定時器更加安全和方便。

System.Threading.Timer 是最簡單的多線程定時器,它只有一個構造方法和兩個方法。

(P795)

.NET Framework 提供另一個與 System.Timers 命名空間中名稱相同的定時器類。它簡單地封裝了 System.Threading.Timer ,在使用完全相同的底層引擎時更加方便。

(P796)

單線程定時器不能在各自環境之外使用。

【第23章】

(P797)

Parallel 類和任務並行結構統稱爲任務並行庫 (Task Parallel Library , TPL) ;

(P798)

通過編程方式利用多內核或多處理器稱爲並行編程,它是多線程更寬泛概念的一個子集。

(P799)

PFX (Parallel Framework , 並行框架) 主要用於並行編程 : 利用多內核處理器加快計算密集型代碼的執行速度。

PLINQ 將自動並行化本地的 LINQ 查詢。 PLINQ 的優勢是易於使用,因爲它把工作劃分和結果整理的任務轉給了 Framework 。

要使用 PLINQ ,只要在輸入序列上調用 AsParallel() 方法,然後繼續執行 LINQ 查詢。

(P800)

AsParallel 是 System.Linq.ParallelEnumerable 中的一個擴展方法。它基於 ParallelQuery<TSource> 封裝輸入序列,使隨後調用的 LINQ 查詢運算符綁定到 Parallel-Enumerbale 中定義的另一組方法。這爲每個標準查詢運算符提供了並行實現。基本上,它們的工作原理是將輸入序列劃分爲在不同線程上執行的小塊,然後將結果整理到一個輸出序列中以供使用。

對於接受兩個輸入序列的查詢運算符 (Join 、 GroupJoin 、 Concat 、 Union 、 Intersect 、 Except 和 Zip) ,必須對這兩個輸入序列應用 AsParallel() 方法,否則將拋出異常。但不需要在查詢進行時一直對它應用 AsParallel ,因爲 PLINQ 的查詢運算符輸出另一個 ParallelQuery 序列。事實上,再次調用 AsParallel 會降低效率,因爲它會強制合併和重新劃分查詢。

PLINQ 僅用於本地集合 : 它不能與 LINQ to SQL 或 Entity Framework 一起使用,因爲在這種情況下, LINQ 會轉換爲 SQL ,然後在數據庫服務器上執行。然而,可以使用 PLINQ 基於從數據庫查詢獲得的數據集來執行另外的本地查詢。

(P801)

大多數 LINQ to Objects 查詢執行速度很快,不僅沒有必要並行化,而且劃分、整理和協調額外線程的開銷實際上會降低執行速度。

和普通的 LINQ 查詢一樣, PLINQ 查詢也是延遲求值的。

(P804)

因爲 PLINQ 在並行線程上運行查詢,必須注意不能執行非線程安全的操作。

(P806)

PLINQ 的優點之一是,它能夠方便地把來自並行工作的結果整理到一個輸出序列中。但有時,結束時要做的全部工作就是讓序列在每個元素上運行一些函數。

如果這是實情,而且可以忽略元素被處理的順序,使用 PLINQ 的 ForAll 方法可以提高效率。

ForAll 方法在 ParallelQuery 的每個輸出元素上運行一個委託。它正確關聯到 PLINQ 的內部,省略了整理和枚舉結果的步驟。

(P807)

整理和枚舉結果不是複雜的大型操作,因此當存在大量快速執行的輸入元素時, ForAll 優化能夠獲得最佳效果。

PLINQ 有三種用於給線程指派輸入元素的劃分策略 : 塊劃分、範圍劃分、哈希劃分;

哈希劃分效率相對較低,因爲它必須預先計算每個元素的哈希代碼,才能在同一線程上處理帶有相同哈希代碼的元素。如果覺得這樣做太慢,唯一的選擇就是調用 AsSequential 來禁用並行化。

概括地說,範圍劃分用於較長的序列,而且當每個元素花費的 CPU 時間大致相等時速度更快。否則,塊劃分的速度一般更快。

(P816)

Task.Run 可以創建和啓動一個 Task 或 Task<TResult> 。這個方法實際上是 Task.Factory.StartNew 的簡寫方法,只是後者有更多的重載版本,所以也更加靈活一些。

【第24章】

(P833)

應用域是指運行中的 .NET 程序所在的獨立區域。它提供了一個可控內存區域作爲程序集和相關配置的容器,同時劃定分佈式程序的交互區域。

每個 .NET 進程通常擁有一個應用域 : 默認域。默認域在進程開始時由 CLR 自動創建。可以爲應用程序建立額外的應用域,並且額外的應用域可以提供隔離,而且與單獨的進程相比,降低額外系統開銷和交互複雜性。它也可以應用於加載測試、應用程序補丁和運行穩定性錯誤恢復機制中。

通常情況下,進程的應用域是在用戶雙擊可執行文件或者啓動一個系統服務程序的時候,由操作系統建立的。

但是,通過 CLR 的整合,互聯網信息服務進程 (IIS) 和數據庫服務進程 (SQL) 等也可以擁有應用域。

對於簡單應用程序,進程和默認域同時結束運行。但是對於 IIS 和 SQL ,進程控制着 .NET 應用域的生命週期,在合適的時候生成應用域和銷燬應用域。

在進程中,可以通過調用靜態方法 AppDomain.CreateDomain 和 AppDomain.Unload 創建和銷燬應用域。

謹記 : 當由 CLR 在程序開始時創建的應用域即默認域銷燬時,應用程序關閉並且銷燬該程序其他所有應用域。通過 AppDomain 屬性 IsDefaultDomain ,可以確定應用域是否是默認域。

(P834)

ApplicationBase 屬性控制應用域的根文件夾,該根文件夾指定了自動檢測程序集時的範圍。默認域的根文件夾是主要的可執行文件所在的文件夾。對於創建的新應用域,其根文件夾可根據需要任意選取。

(P839)

應用域可以通過命名管道共享數據。

(P840)

管道在其第一次使用的時候被建立。

進程化是指通過委託在其他應用域內實例化對象,這是與其他應用域交互最靈活的方法。

【第25章】

(P844)

P / Invoke 是平臺調用服務 (Platform Invocation Services) 的簡稱,允許訪問未託管 DLL 中的函數、構件和回調。

通過在該函數的定義中添加 extern 關鍵字和 DllImport 屬性,可以將該函數定義或一個同名的靜態方法,從而在程序中直接調用。

CLR 中包括一個封送器,可以實現 .NET 類型和非託管類型的相互轉換。

IntPtr 是一個用來封裝非託管句柄的結構,在 32 位平臺下,它的位寬是 32 位;在 64 位平臺下,它的位寬是 64 位。

(P845)

在 .NET 程序內,仍然有多種類型可以選擇。以非託管句柄爲例,可以映射爲 IntPtr 類型、 int 類型、 uint 類型、 long 類型和 ulong 類型。

大多數情況下非託管句柄封裝一個地址或者指針,因此必須轉換成一個 IntPtr 類型以匹配 32 位和 64 位的系統。一個典型的示例是 HWND 句柄。

(P846)

如果不能確定怎樣調用一個 Win32 方法,通常可以通過搜索方法的名字和 DllImport ,在網絡上找到相關的示例。

(P847)

P / Invoke 層作爲在託管和非託管代碼中一個固有的編程模型,對兩者相關的結構映射起到了很大作用。 C# 不但可以調用 C 函數,而且可以作爲 C 函數的回調函數,前提是 P / Invoke 層需要映射非託管函數指針到託管代碼空間的的合法結構。託管代碼中的委託等同於一個指針,因此 P / Invoke 層會將 C# 中的委託與 C 中的指針相互映射。

(P854)

.NET 程序對 COM 對象都有特殊的支持,使得 COM 程序可以在 .NET 程序中調用,反之亦然。 C# 5.0 和 CLR 4.0 增強了在 .NET 中部署和使用 COM 的功能。

(P855)

某種程度上來說, .NET 程序是在 COM 規則上進化而來的 : .NET 平臺有助於跨語言開發並且允許二進制組件的更新而不影響依賴於該組件的程序正常運行。

【第26章】

(P861)

正則表達式可以對字符串進行模式化識別。 .NET 中的正則表達式規範是基於編程語言 Perl 5 的,並且支持查找替換功能。

正則表達式一般用於處理下列問題 :

  1. 判定輸入字符是否是密碼或者手機號;

  2. 將文本數據轉換成結構化形式;

  3. 替換文檔中固定形式的文本;

一個常用的正則表達式運算符是量詞。量詞 “?” 表示前面的字符出現一次或者零次。換句話說, “?” 表示前面的字符是可選的。前面的字符可以是單個字符,也可以是放在方括號內的由多個字符構成的複雜結構。

(P862)

Regex.Match 方法可以搜索大型字符串。它返回的對象具有匹配的長度、索引位和匹配的真實值等屬性。

可以將 Regex.Match 方法認爲是字符串索引方法 IndexOf 的增強版。不同的是 Regex.Match 搜索的是一種模式而非普通字符串。

IsMatch 方法是 Match 的一種捷徑,它首先調用 Match 方法,然後判斷返回對象的 Success 屬性。

默認狀態下,正則表達式引擎按照字符串從左到右的順序進行匹配,所以返回的是左起第一個匹配字符串。可以使用 NextMatch 方法返回更多的匹配值。

Matches 方法通過數組返回所有的匹配值。

另一個常見的正則表達式運算符是交替符,用一個豎線表示 —— “|” 。交替符前後的表達式是可選的。

圓括號將可選的表達式同其他表達式分隔開。

(P863)

Regex 實例是不可更改的。

正則表達式匹配引擎是很快的,就算沒有編譯,一個簡單的匹配也用不了一毫秒。

RegexOptions 標誌可以控制正則表達式匹配的行爲。

(P864)

當要查找的串中含有元字符,需要在元字符前加反斜槓。

(P865)

\d 表示一個十進制數字,所以 \d 可以匹配任何數字。 \D 表示非數字。

\w 表示一個單詞字符,包括字母、數字和下劃線。 \W 表示非單詞字符,可以用於表示非英語字母。

. 匹配所有字符,除了 \n (但是包括 \r ) 。

如果將 \d 、 \w 、 . 與量詞一起使用,可以得到很多的變化。

(P867)

錨點 ^ 和 $ 代表確定的位置,默認表示 :

  1. ^ —— 匹配字符串的開頭;

  2. $ —— 匹配字符串的結束;

(P868)

\b 常用來匹配整個單詞。

(P870)

Regex.Replace 方法與 string.Replace 的功能類似,不過它使用正則表達式進行查找。

(P871)

靜態的 Regex.Split 方法是 string.Split 方法加強版,它使用了正則表達式替換了分隔符的模式。
**

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章