1. 程式人生 > WINDOWS開發 >《果殼中的C# C# 5.0 權威指南》 - 學習筆記

《果殼中的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)

5. 分組運算子 —— 將一個集合按照某種條件分成幾個不同的子集。使用的運算子有 : GroupBy ;

6. 集合運算子 —— 主要用於對兩個相同型別集合的操作,可以返回兩個集合中共有的元素、不同的元素或者兩個集合的所有元素。使用的運算子有 : Concat 、 Unoin 、 Intersect 、 Except ;

7. 轉換方法 Import —— 這種方法包括 OfType 、 Cast ;

8. 轉換方法 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 屬性;

如果要上傳資料,則 :

4. 呼叫請求物件的 GetRequestStream ,然後在流中寫入資料。如果需要處理響應,則轉到第 5 步。

如果要下載資料,則 :

5. 呼叫請求物件的 GetResponse ,建立一個 Web 響應例項;

6. 呼叫響應物件的 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 方法加強版,它使用了正則表示式替換了分隔符的模式。