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

《果殼中的C# C# 5.0 權威指南》程序員

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

【前言】 正則表達式

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 (整數類型) 方法隱含了一些重載方法,它們能夠將數字轉換爲其餘進制。第二個參數指定了進制數,它能夠是任何一種進制 (2、8、十或十六進制) 。

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 方法增強版,它使用了正則表達式替換了分隔符的模式。

相關文章
相關標籤/搜索