《果殼中的C# C# 5.0 權威指南》 (09-26章) - 學習筆記

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

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

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章