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

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

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

【前言】

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

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

【第01章】

(P001)

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

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

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

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

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

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

(P002)

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

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

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

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

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

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

(P003)

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

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

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

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

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

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

(P004)

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

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

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

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

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

(P005)

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

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

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

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

(P006)

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

【第02章】

(P007)

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

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

(P008)

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

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

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

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

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

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

(P009)

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

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

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

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

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

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

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

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

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

.NET Framework 就是一組庫。

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

(P010)

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

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

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

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

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

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

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

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

(P011)

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

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

等號則用於賦值操作。

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

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

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

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

(P012)

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

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

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

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

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

(P013)

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

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

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

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

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

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

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

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

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

(P014)

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

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

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

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

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

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

轉換可以是隱式或顯式。

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

long 容量是 int 的兩倍。

(P015)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(P016)

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

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

(P017)

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

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

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

(P018)

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

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

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

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

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

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

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

(P019)

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

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

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

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

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

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

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

(P020)

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

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

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

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

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

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

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

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

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

(P021)

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

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

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

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

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

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

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

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

(P022)

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

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

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

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

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

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

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

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

(P023)

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

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

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

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

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

(P024)

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

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

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

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

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

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

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

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

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

(P025)

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

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

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

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

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

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

(P026)

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

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

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

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

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

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

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

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

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

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

(P027)

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

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

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

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

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

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

(P028)

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

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

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

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

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

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

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

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

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

(P029)

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

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

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

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

數組索引從 0 開始。

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

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

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

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

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

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

(P030)

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

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

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

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

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

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

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

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

(P032)

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

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

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

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

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

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

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

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

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

(P033)

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

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

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

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

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

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

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

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

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

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

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

(P034)

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

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

[類型] - [默認值]

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

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

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

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

(P035)

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

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

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

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

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

(P036)

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

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

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

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

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

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

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

(P037)

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

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

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

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

(P038)

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

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

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

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

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

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

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

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

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

(P039)

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

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

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

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

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

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

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

(P040)

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

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

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

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

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

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

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

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

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

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

(P041)

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

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

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

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

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

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

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

(P043)

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

(P044)

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

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

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

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

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

可能的表達式語句是 :

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

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

3. 對象實例化表達式;

(P045)

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

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

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

2. 條件語句 (? :) ;

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

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

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

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

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

(P046)

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

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

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

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

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

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

(P047)

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

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

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

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

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

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

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

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

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

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

(P048)

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

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

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

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

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

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

(P049)

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

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

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

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

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

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

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

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

(P050)

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

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

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

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

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

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

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

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

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

(P051)

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

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

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

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

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

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

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

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

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

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

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

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

(P052)

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

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

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

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

(P053)

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

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

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

(P054)

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

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

(P055)

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

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

【第03章】

(P057)

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

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

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

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

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

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

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

[靜態修飾符] —— static

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

[繼承修飾符] —— new

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

[只讀修飾符] —— readonly

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

(P058)

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

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

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

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

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

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

[靜態修飾符] ——  static

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

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

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

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

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

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

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

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

(P059)

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

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

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

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

(P060)

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

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

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

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

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

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

(P061)

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

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

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

(P062)

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

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

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

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

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

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

(P063)

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

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

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

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

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

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

[靜態修飾符] —— static

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(P065)

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

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

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

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

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

(P066)

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

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

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

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

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

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

[繼承修飾符] —— new

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

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

1. 實例化類;

2. 訪問類的靜態成員;

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

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

(P067)

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

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

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

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

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

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

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

(P068)

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

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

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

每個類必須由 partial 聲明。

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

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

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

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

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

(P069)

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

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

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

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

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

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

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

(P070)

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

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

對象引用可以被 :

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

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

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

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

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

(P071)

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

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

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

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

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

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

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

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

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

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

(P072)

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

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

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

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

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

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

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

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

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

(P073)

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

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

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

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

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

(P074)

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

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

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

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

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

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

(P075)

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

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

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

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

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

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

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

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

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

(P076)

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

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

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

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

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

(P077)

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

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

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

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

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

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

拆箱需要顯式進行。

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

(P078)

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

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

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

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

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

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

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

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

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

(P079)

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

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

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

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

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

(P080)

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

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

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

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

1. 無參數的構造方法;

2. 終止器;

3. 虛成員;

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

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

結構體的構造語義如下 :

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

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

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

(P081)

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

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

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

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

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

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

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

(P082)

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

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

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

(P083)

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

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

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

接口和類的不同之處有 :

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

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

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

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

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

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

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

(P084)

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

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

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

(P085)

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

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

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

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

(P086)

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

(P087)

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

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

(P088)

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

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

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

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

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

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

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

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

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

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

(P089)

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

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

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

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

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

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

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

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

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

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

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

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

(P090)

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

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

(P091)

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

嵌套類型有如下特徵 :

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

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

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

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

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

(P092)

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

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

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

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

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

(P093)

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

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

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

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

(P094)

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

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

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

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

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

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

(P095)

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

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

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

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

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

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

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

(P096)

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

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

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

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

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

(P097)

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

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

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

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

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

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

(P098)

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

1. 數值型轉換;

2. 引用型轉換;

3. 裝箱 / 拆箱 轉換;

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

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

(P099)

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

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

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

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

(P100)

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

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

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

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

(P101)

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

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

【第04章】

(P103)

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

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

(P104)

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

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

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

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

(P105)

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

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

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

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

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

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

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

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

(P106)

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

(P107)

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

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

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

(P108)

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

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

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

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

2. 需要多播能力;

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

(P109)

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

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

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

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

(P110)

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

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

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

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

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

(P111)

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

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

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

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

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

(P113)

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

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

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

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

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

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

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

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

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

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

(P114)

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

(P115)

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

(P116)

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

編譯器把它轉換爲 :

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

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

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

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

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

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

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

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

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

(P117)

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

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

1. 委託實例;

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

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

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

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

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

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

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

(P118)

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

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

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

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

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

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

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

(P119)

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

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

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

(P120)

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

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

1. 確定類型的參數;

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

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

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

(P121)

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

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

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

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

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

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

(P122)

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

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

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

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

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

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

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

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

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

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

(P123)

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

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

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

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

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

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

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

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

1. catch 塊執行完成;

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

3. try 塊結束;

(P124)

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

1. try 塊正常結束;

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

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

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

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

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

(P125)

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

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

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

(P126)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(P128)

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

1. System.Collections.IEnumerator ;

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

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

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

可枚舉對象可以是 :

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

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

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

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

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

(P129)

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

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

(P130)

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

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

// Enumerable 接口
System.Collections.IEnumerable
System.Collections.Generic.IEnumerable<T>

// Enumerator 接口
System.Collections.IEnumerator
System.Collections.Generic.IEnumerator<T>

返回 enumerable 接口和返回 enumerator 接口的迭代器具有不同的語義。

yield break 語句表明迭代器不返回後面的元素而是提前結束。

(P131)

迭代器塊中使用 return 語句是不合法的,必須使用 yield break 語句來代替。

yield return 語句不能出現在帶 catch 子句的 try 語句塊中。

yield return 語句也不能出現在 catch 或 finally 語句塊中。出現這些限制的原因是編譯器必須將迭代器轉換爲帶有 MoveNext 、 Current 和 Dispose 成員的普通類,而且轉換異常處理語句塊可能會大大增加代碼複雜性。

但是,可以在只帶 finally 語句塊的 try 塊中使用 yield 語句。

迭代器具有高度可組合性。

(P132)

迭代器模式的組合性在 LINQ 中是非常有用的。

引用類型可以表示一個不存在的值,即空引用。

(P133)

若要在數值類型中表示空值,必須使用特殊的結構即可空類型 (Nullable)。可空類型是由數據類型後加一個 “?” 表示的。

T? 轉換成 System.Nullable<T> 。而 Nullable<T> 是一個輕量的不變結構,它只有兩個域,分別是 Value 和 HasValue 。System.Nullable<T> 實質上是很簡單的。

public struct Nullable<T> where T : struct
{
    public T Value {get;}
    public bool HasValue {get;}
    public T GetValueOrDefault();
    public T GetValueOrDefault(T defaultValue);
}

當 HasValue 爲假時嘗試獲取 Value,程序會拋出一個 InvalidOperationException 異常。

當 HasValue 爲真時,GetValueOrDefault() 會返回 Value ,否則返回 new T() 或者一個特定的自定義默認值。

T? 的默認值是 null 。

從 T 到 T? 的轉換是隱式的,而從 T? 到 T 的轉換則必須是顯式的。

顯式強制轉換與直接調用可空對象的 Value 屬性實際上是等價的。因此,當 HasValue 爲假時,程序會拋出一個 InvalidOperationException 異常。

如果 T? 是裝箱的,那麼堆中的裝箱值包含的是 T ,而不是 T? 。這種優化方式是可以實現的,因爲裝箱值是一個可能已經賦值爲空的引用類型。

(P134)

C# 允許通過 as 運算符對一個可空類型進行拆箱。如果強制轉換出錯,那麼結果爲 null 。

Nullable<T> 結構體並沒有定義諸如 < 、 > 或者 == 的運算符。儘管如此,下面的代碼仍然能夠正常編譯和執行。

運算符提升表示可以隱式地使用 T 的運算符來處理 T? 。

編譯器會基於運算符類型來執行空值邏輯。

提升 “等於運算符” 處理空值的方式與引用類型相似,這意味着兩個空值是相等的。而且 :

1. 如果只有一個操作數爲空,那麼結果不相等;

2. 如果兩個操作數都不爲空,那麼比較它們的 Value ;

(P135)

關係運算符的運算原則表明空值操作數的比較是無意義的,這意味着比較兩個空值或比較一個空值與一個非空值的結果都是 false 。

可以混合使用可空和不可空類型,這是因爲 T 與 T? 之間存在隱式轉換機制。

如果操作數的類型是 bool? ,那麼 & 和 | 運算符會將 null 作爲一個未知值看待。所以,null | true 的結果爲真,因爲 :

1. 如果未知值爲假,那麼結果爲真;

2. 如果未知值爲真,那麼結果爲真;

(P136)

?? 運算符是空值合併運算符,它既可用來計算可空值類型,也可用來計算引用類型。也就是說,如果操作數不爲空,直接計算;否則,計算器默認值。

?? 運算符的結果等同於使用一個顯式默認值調用 GetValueOrDefault ,除非當變量不爲空時傳遞給 GetValueOrDefault 的表達式從未求值。

可空類型在將 SQL 映射到 CLR 時是非常有用的。

可空類型還可用於表示所謂環境屬性的後備字段,如果環境屬性爲空,那麼返回其父類的值。

(P137)

運算符可以經過重載實現更自然的自定義類型語法,運算符重載非常適合用來表示最普通的基本數據類型的自定義結構體。

下面的運算符也可以重載 :

1. 隱式和顯式轉換 (使用 implicit 和 explicit 關鍵字實現) ;

2. 常量 true 和 false;

下面的運算符可以間接進行重載 :

1. 複合賦值運算符 (例如 += 、 /=) 可以通過重載非複合運算符 (例如 + 、 /) 進行隱式重載;

2. 條件運算符 && 和 || 可以通過重載位運算符 & 和 | 進行隱式重載;

(P138)

運算符是通過聲明一個運算符函數進行重載的。運算符函數具有以下規則 :

1. 函數名是通過 operator 關鍵字及其後的運算符指定的;

2. 運算符函數必須標記爲 static 和 public ;

3. 運算符函數的參數表示的是操作數;

4. 運算符函數的返回類型表示的是表達式的結果;

5. 運算符函數所聲明的類型至少有一個操作數;

重載一個賦值運算符會自動支持相應的複合賦值運算符。

成對重載 : C# 編譯器要求邏輯上成對的運算符必須同時定義。這些運算符包括 (== 、 !=) 、 (< 、 >) 和 (<= 、 >=) 。

Equals 和 GetHashCode : 在大多數情況中,如果重載了 (==) 和 (!=) ,那麼通常也需要重載對象中定義的 Equals 和 GetHashCode 方法,使之具有合理的行爲。如果沒有按要求重載,那麼 C# 編譯器將會發出警告。

IComparable 和 IComparable<T> : 如果重載了 (< 、 >) 和 (<= 、 >=),那麼還應該實現 IComparable 和 IComparable<T> 。

(P139)

隱式和顯式轉換也是可重載的運算符,這些轉換經過重載後一般能使強關聯類型之間的轉換變得更加簡明和自然。

如果要在弱關聯類型之間進行轉換,那麼更適合採用以下方式 :

1. 編寫一個具有該轉換類型的參數的構造函數;

2. 編寫 ToXXX 和 (靜態) FromXXX 方法進行類型轉換;

(P140)

擴展方法允許一個現有類型擴展新的方法而不需要修改原始類型的定義。

擴展方法是靜態類的靜態方法,其中第一個參數需要使用 this 修飾符,類型就是擴展的類型。

(P141)

擴展方法是 C# 3.0 後增加的特性。

擴展方法類似於實例方法,也支持一種鏈接函數的方法。

只有命名空間在定義域內,我們才能夠訪問擴展方法。

任何兼容的實例方法總是優先於擴展方法。

如果兩個擴展方法名稱相同,那麼擴展方法必須作爲一個普通的靜態方法調用,才能夠區分所調用的方法。然而,如果其中一個擴展方法具有更具體的參數,那麼有更具體參數的方法優先級更高。

(P143)

匿名類型是一個由編譯器臨時創建來存儲一組值的簡單類。如果要創建一個匿名類型,我們可以使用 new 關鍵字,後面加上對象初始化語句,在其中指定該類型包含的屬性和值。

必須使用 var 關鍵字來引用一個匿名類型,因爲類型的名稱是編譯器產生的。

匿名類型的屬性名可以從本身是一個標識符或以標識符結尾的表達式得到。

如果這兩個匿名類型實例的元素是相同類型的,並且它們在相同的程序集中聲明,那麼它們在內部是相同的類型。

匿名類型的 Equals 方法也被重載了,從而能夠執行正確的等於比較運算。

(P144)

匿名類型主要是在編寫 LINQ 查詢時使用,並且是 C# 3.0 後纔出現的特性。

動態綁定是將綁定 (解析類型、成員和操作的過程) 從編譯時延遲到運行時。

在編譯時,如果程序員知道某個特定函數、成員或操作的存在,而編譯器不知道,那麼動態綁定是很有用的。

這種情況通常出現在操作動態語言 (如 IronPython) 和 COM 時,而且如果不使用動態綁定,就只能使用反射機制。

動態類型是通過上下文關鍵字 dynamic 聲明的。

動態綁定類型會告訴編譯器 “不要緊張” 。

無論綁定的是什麼樣的方法,其底線是已知綁定是由編譯器實現的,而且綁定是完全依賴於之前已經知道的操作數類型,這就是所謂的靜態綁定。

(P145)

動態類型類似於 object ,同樣不表現爲一種類型。其區別是能夠在編譯時在不知道它存在的情況下使用它。

動態對象是基於其運行時類型進行綁定的,而不是基於編譯時類型。

當編譯器遇到一個動態綁定表達式時 (通常是一個包含任意動態類型值的表達式) ,它僅僅對錶達式進行打包,而綁定則在後面的運行時執行。

在運行時,如果一個動態對象實現了 IDynamicMetaObjectProvider ,那麼這個接口將用來執行綁定。否則,綁定的發生方式就幾乎像是編譯器已經事先知道動態對象的運行時類型一樣。我們將這兩種方式稱爲自定義綁定和語言綁定。

COM 可認爲是第三種綁定方式。

自定義綁定是通過實現了 IDynamicMetaObjectProvider (IDMOP) 而實現的。

(P146)

動態綁定會損壞靜態類型安全性,但不會影響運行時類型安全性。與反射機制不同,不能通過動態綁定繞過成員訪問規則。

靜態和動態綁定之間最顯著的差異在於擴展方法。

動態綁定也會對性能產生影響。然而,由於 DLR 的緩存機制對同一個動態表達式的重複調用進行了優化,允許在一個循環中高效地調用動態表達式。這個優化機制能夠使一個簡單的動態表達式的處理負載對硬件的性能影響控制在 100ms 以內。

如果一個成員綁定失敗,那麼程序會拋出 RuntimeBinderException 異常,可以將它看作是一個運行時的編譯錯誤。

dynamic 和 object 類型之間可以執行一個深度等值比較。在運行時,下面這個表達式的結果爲 true :

typeof(dynamic) = typeof (object)

(P147)

與對象引用相似,動態引用可以指向除指針類型以外的任意類型的對象。

在結構上,對象引用和動態引用之間沒有任何區別。

動態引用可以直接在它所指的對象上執行動態操作。

動態類型會對其他所有類型進行隱式轉換。

如果要成功進行轉換,動態對象的運行時類型必須能夠隱式轉換到目標的靜態類型上。

(P148)

var 和 dynamic 類型表面上是相似的,但是它們實際上是有區別的 :

var 由編譯器確定類型。

dynamic 由運行時確定類型。

一個由 var 聲明的變量的靜態類型可以是 dynamic 。

域、屬性、方法、事件、構造函數、索引器、運算符和轉換都是可以動態調用的。

dynamic 的標準用例是包含一個動態接受者。

然而,還可以使用動態參數調用已知的靜態函數。這種調用受到動態重載解析的影響,並且可能包括 :

1. 靜態方法;

2. 實例構造函數;

3. 已知靜態類型的接收者的實例方法;

(P149)

動態類型用在動態綁定中。但是,靜態類型在可能的情況下也用在動態綁定中。

(P150)

有一些函數是不能夠動態調用的,如下 :

1. 擴展方法 (通過擴展方法語法) ;

2. 接口的所有成員;

3. 子類隱藏的基類成員;

擴展方法成爲只適用於編譯時的概念。

using 指令在編譯後會消失 (當它們在綁定過程中完成了將簡單的名稱映射到完整命名空間的任務之後) 。

(P151)

特性是添加自定義信息到代碼元素 (程序集、類型、成員、返回值和參數) 的擴展機制;

特性的一個常見例子是序列化,就是將任意對象轉換爲一個特定格式或從特定格式生成一個對象的過程。在這情況中,某個字段的屬性可以指定該字段的 C# 表示方式和該字段的表示方式之間的轉換。

特性是通過直接或間接地繼承抽象類 System.Attribte 的方式定義的。

如果要將一個特性附加到一個代碼元素中,那麼就需要在該代碼元素之前用方括號指定特性的類型名稱。

編譯器能夠識別這個特性,如果某個標記爲棄用的類型或成員被引用時,編譯器會發出警告。

按照慣例,所有特性類型都以 Attribute 結尾,C# 能夠識別這個後綴,也可以在附加一個屬性時省略這個後綴。

C# 語言和 .NET Framework 包含了大量的預定義特性。

特性可能具有一些參數。

特性參數分爲兩類 : 位置和命名。

位置參數對應於特性類型的公開構造函數的參數;命令參數則對應於該特性類型的公開字段或公開屬性。

當指定一個特性時,必須包含對應於其中一個特性構造函數的位置參數。命名參數則是可選的。

(P152)

特性目標不需要顯式指定,特性目標就是它後面緊跟的代碼元素而且一般是一個類型或類型成員。然而,也可以給程序集附加一些特性,這要求顯式地指定特性的目標。

一個代碼元素可以指定多個特性,每一個特性可以列在同一對方括號中 (用逗號分割) 或者在多對方括號中或者結合兩種方式。

從 C# 5 開始,可以給可選參數添加 3 個調用者信息屬性中的一個,它們可以讓編譯器從調用者代碼獲取參數的默認值 :

1. [CallerMemberName] —— 表示調用者的成員名稱;

2. [CallerFilePath] —— 表示調用者的源代碼文件路徑;

3. [CallerLineNumber] —— 表示調用者源代碼文件的行號;

(P153)

調用者信息特性很適合用於記錄日誌以及實現一些模式,如當一個對象的某個屬性發生變化時,觸發一個變化通知事件。事實上,.NET 框架有一個專門實現這個效果的標準接口 INotifyPropertyChanged (位於 System.ComponentModel) 。

(P154)

C# 支持通過標記爲不安全和使用 /unsafe 編譯器選項編譯的代碼塊中的指針直接進行內存操作。指針類型主要用來與 C 語言 API 進行互操作,但是也可用來訪問託管堆以外的內存,或者分析嚴重影響性能的熱點。

使用 unsafe 關鍵字標記一個類型、類型成員或語句塊,就可以在該範圍內使用指針類型和對內存執行 C++ 中的指針操作。

不安全代碼與對應的安全實現相比運行速度更快。

fixed 語句是用來鎖定託管對象的。

由於這可能對運行時效率產生一定的影響,所以 fixed 代碼塊只能短暫使用,而且堆分配應該避免出現在 fixed 代碼塊中。

(P155)

除了 & 和 * 運算符,C# 還支持 C++ 中的 -> 運算符,可以在結構體中使用。

我們可以在代碼中顯式地通過 stackalloc 關鍵字分配棧中的內存,由於這部分內存是從棧上分配的,所以其生命週期僅限於方法的執行時間,這點與其他的局部變量相同,這個代碼塊可以使用 [] 運算符實現內存索引。

我們也可以使用 fixed 關鍵字在一個結構體代碼塊中分配內存。

fixed 表示兩個不同的方面 : 大小固定和位置固定。

(P156)

空指針 (void*) 不給出假定底層數據的具體類型,它對於處理原始內存的函數是非常有用的。任意指針類型都可以隱式地轉換爲 void* 。 void* 不可以被解除引用,算術運算符不能通過 void 指針執行。

指針也很適於訪問位於託管堆之外的數據 (如與 C DLL 或 COM 交互時) ,以及處理不在主存中的數據 (如圖形化內存或嵌入式設備的存儲介質) 。

(P157)

預處理指令向編譯器提供關於代碼範圍的額外信息。最常用的預處理指令是條件指令,它提供了一種將某些代碼加入或排除出編譯範圍的方法。

通過 #if 和 #elif 指令,可以使用 || 、 && 和 ! 運算符在多個符號上執行或、與、非操作。

#error 和 #warning 符號會要求編譯器在遇到一些不符合要求的編譯符號時產生一條警告信息或錯誤信息,從而防止出現條件指令的偶然誤用。

(P158)

使用 Conditional 修飾的特性只有在出現指定的預處理符號時才編譯。

(P159)

文檔註釋是一種嵌入的、記錄類型或成員的 XML 。文檔註釋位於類型或成員聲明之前,以三個斜線開頭。

也可以採用以下方法 (注意開頭有兩個星號) 。/**    */

如果使用 /doc 指令進行編譯,那麼編譯器會將文檔註釋存儲到一個 XML 文件中,並進行校對,這個特性主要有兩種作用 :

1. 如果與編譯的程序集位於同一個文件夾,那麼 Visual Studio 會自動讀取這個 XML 文件,使用這些信息向同名程序集的使用者提供 IntelliSense 成員清單;

2. 第三方工具 (如 Sandcastle 和 NDoc) 可以將 XML 文件轉換成 HTML 幫助文件;

【第05章】

(P163)

.NET Framework 中幾乎所有的功能都是通過大量的託管類型提供的,這些類型被組織成有層次的命名空間,並且被打包成一套程序集,與 CLR 一起構成 .NET 平臺。

有些 .NET 類型是由 CLR 直接使用的,並且對於託管的宿主環境而言是必不可少的。這些類型位於一個名爲 mscorlib.dll 的程序集中,包括 C# 的內置類型,以及基本的集合類、流處理類型、序列化、反射、多線程和原生互操作性。

除此之外是一些附加類型,它們充實了 CLR 層面的功能,提供了其他一些特性,如 XML 、網絡和 LINQ 等 。這些類型位於 System.dll 、 System.Xml.dll 和 System.Core.dll 中,並且與 mscorlib 一起提供豐富的編程環境供 .NET Framework 的其他部分使用。

.NET Framework 的其餘部分是由一些實用 API 組成的,主要包括以下三個方面的功能 :

1. 用戶接口技術;

2. 後臺技術;

3. 分佈式系統技術;

C# 5.0 對應 CLR 4.5,這個版本比較特殊,因爲它屬於 CLR 4.0 的補丁版本。

這意味着安裝 CLR 4.5 之後,目標平臺是 CLR 4.0 的應用實際上運行在 CLR 4.5 上。

(P164)

程序集和命名空間在 .NET Framework 中是相互交叉的。

(P164)

[.NET Framework 4.5 新特性]

Framework 4.5 新特性包括 :

1. 通過返回 Task 的方法廣泛支持異步編程;

2. 支持 zip 壓縮協議;

3. 通過新增 HttpClient 類改進 HTTP 支持;

4. 改進垃圾收集器和程序集資源回收的性能;

5. 支持 WinRT 互操作性和開發 Metro 風格平板應用的 API ;

此外,還有一個新的 TypeInfo 類,以及可以指定與正則表達工作超過時間匹配的超時時間。

在並行計算領域,還有一個全新庫 Dataflow,可用於開發 生產者 / 消費者 風格的網格。

此外,WPF 、 WCF 和 WF (工作流基礎) 庫也有一些改進。

許多核心類型定義在以下程序集中 : mscorlib.dll 、 System.dll 和 System.Core.dll 。第一個程序集 mscorlib.dll 包括運行時環境本身所需要的類型;System.dll 和 System.Core.dll 包含程序員所需要的其他核心類型。

[.NET Framework 4.0 新特性]

Framework 4.0 增加了以下新特性 :

1. 新的核心類型 : BigInteger (大數字) 、 Complex (複數) 和元組;

2. 新的 SortedSet 集合;

3. 代碼協定,使方法能夠通過共同的義務和責任實現更可靠的交互;

4. 直接支持內存映射文件;

5. 延遲的文件和目錄 I / O 方法,它們返回 IEnumerable<T> 而不是數組;

6. 動態語言運行時 (DLR) 成爲 .NET Framework 的一部分;

7. 安全透明,簡化了保證部分可信環境中程序庫安全性的方法;

8. 新的多線程結構,包括更強大的 Monitor.Enter 重載、新的信號發送類 (Barrier 和 CountdownEvent) 和延遲初始化原語;

9. 支持多核處理的並行計算 API ,包括 Parallel LINQ (PLINQ) 、命令式數據與任務並行性結構、支持併發的集合和低延遲同步機制與 spinning 原語;

10. 用於監控應用程序域資源的方法;

Framework 4.0 還包含了一些 ASP.NET 的改進,包括 MVC 框架和 Dynamic Data,以及 Entity Framework 、 WPF 、 WCF 和 Workflow 等方面的改進。此外,它還包含了新的 Managed Extensibility Framework 庫,以幫助運行時環境實現組合、發現和依賴注入。

(P165)

大多數的基礎類型都直接位於 System 命名空間。其中包括 C# 的內置類型、 Exception 基類、 Enum 、 Array 和 Delegate 基類、以及 Nullable 、 Type 、 DateTime 、 TimeSpan 和 Guid 。System 命名空間也包含執行數字計算功能 (Math) 、生成隨機數 (Random) 和各種數據類型轉換 (Convert 和 BitConvert) 的類型。

System 命名空間還定義了 IDisposable 接口和與垃圾回收器交互的 GC 類。

在 System.Text 命名空間中有一個 StringBuilder 類,以及處理文本編碼的類型。

在 System.Text.RegularExpressions 命名空間中有一些執行基於模式的搜索和替換操作的高級類型。

.NET Framework 提供了各種處理集合項目的類,其中包括基於鏈表和基於字典的結構,以及一組統一它們常用特性的標準接口。

System.Collections //非泛型類型
System.Collections.Generic //泛型框架
System.Collections.Specialized //強類型框架
System.Collections.ObjectModel //自定義框架基類
System.Collections.ConCurrent //線程安全框架

(P166)

Framework 3.5 增加了語言集成查詢 (Language Integrated Query,LINQ) 。LINQ 允許對本地和遠程集合 (例如 SQL Server 表) 執行類型安全查詢。

LINQ 的最大優勢是提供了一種跨多個域的統一查詢 API 。

Metro 模板不包含整個 System.Data.* 命名空間。

LINQ to SQL 和 Entity Framework API 使用了 System.Data 命名空間的 ADO.NET 底層類型。

XML 在 .NET Framework 中被廣泛使用,同時也得到廣泛支持。

操作線程和異步操作的類型位於 System.Threading 和 System.Threading.Tasks 命名空間。

(P167)

Framework 提供了基於流的模型進行底層 輸入 / 輸出 操作。流一般用於文件和網絡連接的直接讀寫操作,它們可以被鏈接和封裝到裝飾流中,從而實現壓縮或加密功能。

Stream 和 I / O 類型是在 System.IO 命名空間中定義的。

可以通過 System.Net 中的類型直接訪問標準的網絡協議,如 HTTP 、 FTP 、 TCP / IP 和 SMTP 。

Framework 提供了幾個可以將對象保存爲二進制或文本方式的系統,這些系統是分佈式應用程序技術所必需的,如 WCF 、 Web Services 和 Remoting ,它們也可用於將對象保存到文件和從文件恢復對象。

Metro 模板不包含二進制序列化引擎。

C# 程序編譯產生的程序集包含可執行指令 (存儲爲中間語言或 IL) 和元數據,它描述了程序的類型、成員和屬性。通過反射機制,可以在運行時檢查元數據或者執行某些操作,如動態調用方法。

通過 Reflection.Emit 可以隨時創建新代碼。

(P168)

動態編程的類型位於 System.Dynamic 中。

.NET Framework 具有自己的安全層,從而能夠將程序集裝入沙箱,甚至將自己裝入沙箱。

Metro 模板只包含 System.Security ;加密操作則在 WinRT 中處理。

C# 5 的異步函數可以顯著簡化併發編程,因爲它們減少了底層技術的使用。然而,開發者有時候仍然需要使用信號發送結構、線程內存儲、讀 / 寫 鎖等。

線程類型位於 System.Threading 命名空間。

CLR 支持在一個進程中增加額外的隔離級別,即應用程序域。

AppDomain 類型定義在 System 命名空間中。

原生互操作性使您能夠調用未託管 DLL 中的函數、註冊回調函數、映射數據結構和操作原生數據類型。COM 互操作性使您能夠調用 COM 類型和將 .NET 類型傳遞給 COM 。

.NET Framework 提供了 4 種支持基於用戶界面的應用程序的 API 。

1. ASP.NET (System.Web.UI) 編寫運行在標準網頁瀏覽器上的瘦客戶端應用程序;

2. Silverlight 在網頁瀏覽器上實現富用戶界面;

3. Windows Presentation Foundation (System.Windows) 編寫富客戶端應用程序;

4. Windows Forms (System.Windows.Forms) 支持遺留富客戶端應用程序;

(P169)

一般而言,瘦客戶端應用程序指的是網站;而富客戶端應用程序則是最終用戶必須下載或安裝在客戶端計算機上的程序。

富客戶端的方法是在客戶端和數據庫之間插入一箇中間層,中間層運行在一臺遠程應用程序服務器上 (通常與數據庫服務器一起) ,並通過 WCF 、 Web Services 或 Remoting 與富客戶端通信。

在編寫網頁時,可以選擇傳統的 Web Forms 或者新的 MVC (模型 - 視圖 - 控制器) API 。這兩種方法都基於 ASP.NET 基礎框架。從一開始,Framework 就支持 Web Forms ;MVC 則是在後來 Ruby on Rails 和 MonoRail 流行之後纔出現的。

Web Forms 仍然適合用來編寫主要包含靜態內容的網頁。

AJAX 的使用可以通過注入 jQuery 等庫進行簡化。

編寫 ASP.NET 應用程序的類型位於 System.Web.UI 命名空間及其子命名空間中,並且屬於 System.Web.dll 程序集。

Silverlight 在技術上並不屬於 .NET Framework 的主框架 : 它是一個獨立的框架,包含了一部分的 Framework 核心特性,增加了作爲網頁瀏覽器插件運行的功能。

(P170)

Silverlight 主要用於一些邊緣場景。

Windows Metro 庫同樣不屬於 .NET 框架,它只用於在 Windows 8 中開發平板電腦界面。

Metro API 源於 WPF 的啓發,並且使用 XAML 實現佈局。其命名空間包括 Windows.UI 和 Windows.UI.Xaml 。

WPF 是在 Framework 3.0 時引入的,用來編寫富客戶端應用程序。

WPF 的規模和複雜性使學習週期比較長。

編寫 WPF 應用程序的類型位於 System.Windows 命名空間以及除 System.Windows.Forms 之外的所有子命名空間中。

與 WPF 相比,Windows Forms 相對簡單,它支持編寫一般 Windows 應用程序時所需要使用的大多數特性,也能夠良好地兼容遺留應用程序。

Windows Forms 的學習過程相對簡單,並有豐富的第三方控件支持。

(P171)

Windows Forms 類型位於命名空間 System.Windows.Forms (在 System.Windows.Forms.dll 中) 和 System.Drawing (在 System.Drawing.dll) 中。其中後者包含了繪製自定義控件的 GDI+ 類型。

ADO.NET 是託管的數據訪問 API 。雖然它的名稱源於 20 世紀 90 年代的 ADO (ActiveX Data Objects) ,但是這兩種技術是完全不同的。

ADO.NET 包含兩個主要的底層組件 :

1. 提供者層 —— 提供者模型定義了數據庫提供者底層訪問的通用類和接口。這些接口包括連接、命令,適配器和讀取器 (數據庫的只向前的只讀遊標) 。 Framework 包含對 Microsoft SQL Server 和 Oracle 的原生支持,具有 OLE-DB 和 ODBC 提供者。

2. DataSet 模型 —— 一個 DataSet 是一個數據的結構化緩存。它類似於一個常駐內存的原始數據庫,其中定義了 SQL 結構,如表、記錄行、字段、關係、約束和視圖。通過對數據緩存的編程,可以減少數據庫的交互數量、增加服務器可擴展性以及加快富客戶端用戶界面的響應速度。 DataSet 是可序列化的,它支持通過客戶端和服務器應用程序之間的線路傳輸。

提供者層只有兩個 API ,它們提供了通過 LINQ 查詢數據庫的功能 :

1. LINQ to SQL (從 Framework 3.5 開始引入) ;

2. Entity Framework (從 Framework 3.5 SP1 開始引入) ;

這兩種技術都包含 對象 / 關係 映射器 (ORM) ,意味着它們會自動將對象 (基於定義的類) 映射到數據庫的記錄行。這允許用戶通過 LINQ 查詢這些對象,而不需要編寫 SQL 語句查詢並且不需要手動編寫 SQL 語句進行對象更新。

DataSet 仍然是唯一能夠存儲和序列化狀態變化的技術 (這在多層應用程序中是非常有用的) 。

現在還沒有現成的便捷方法可以使用 Microsoft 的 ORM 來編寫 N 層應用程序。

LINQ to SQL 比 Entity Framework 更簡單、更快速,並且一般會產生更好的 SQL 。Entity Framework 則更具靈活性,可以在數據庫和查詢的類之間創建複雜的映射。除了 SQL Server ,Entity Framework 還支持一些第三方數據庫。

Windows Workflow 是一個對可能長期運行的業務過程進行建模和管理的框架。Workflow 目標是成爲一個標準的提供一致性和互操作性的運行時庫。Workflow 有助於減少動態控制的決策樹的編碼量。

Windows Workflow 嚴格意義上並不是一種後臺技術,可以在任何地方使用它。

Workflow 是從 .NET Framework 3.0 開始出現的,它的類型定義在 System.Workflow 命名空間中。實際上 Workflow 在 Framework 4.0 中進行了修改,增加的新類型位於 System.Activities 命名空間。

(P172)

Framework 允許通過 System.EnterpriseServices 命名空間中的類型與 COM+ 進行互操作,以實現諸如分佈式事物等服務。它也支持通過 System.Messaging 中的類型使用 MSMQ (Microsoft Message Queuing) ,微軟消息隊列實現異步的單向消息傳遞。

WCF 是 Framework 3.0 引入的一個複雜的通信基礎架構。WCF 非常靈活且可配置,這使它的兩個預處理器 —— Remoting 和 (.ASMX) Web Services ,大多是冗餘的。

WCF 、 Remoting 和 Web Services 很相似的方面就是它們都實現以下允許客戶端和服務器應用程序進行通信的基本模型 :

1. 在服務器端,可以指定希望遠程客戶端應用程序能夠調用的方法;

2. 在客戶端,可以指定或推斷將要調用的服務器方法的簽名;

3. 在服務器端和客戶端,都可以選擇一種傳輸和通信協議 (在 WCF 中,這是通過一個綁定完成的) ;

4. 客戶端建立一個服務器連接;

5. 客戶端調用遠程方法,並在服務器上透明地執行;

WCF 會通過服務協定和數據協定進一步對客戶端和服務器進行解耦。概念上,客戶端會發送一條 (XML 或二進制) 消息給遠程服務的終端,而非直接調用一個遠程方法。這種解耦方式的好處是客戶端不會依賴於 .NET 平臺或任意私有的通信協議。

WCF 是高度可配置的,它支持廣泛的標準化消息協議,包括 WS-* 。

WCF 的另一個好處是可以直接修改協議,而不需要修改客戶端或服務器應用程序的其他內容。

與 WCF 通信的類型位於 System.ServiceModel 命名空間中。

Remoting 和 .ASMX Web Services 是 WCF 的預處理器,雖然 Remoting 仍然適合在相同進程中的應用程序域之間進行通信,但是它們在 WCF 中幾乎是冗餘的。

Remoting 的功能針對一些緊密耦合的應用程序。

Web Services 針對一些低耦合或 SOA 類型應用程序。

Web Services 只能使用 HTTP 或 SOAP 作爲傳輸和格式化協議,而應用程序一般是運行在 IIS 上。

互操作性的好處在於性能成本方面 —— Web Services 應用程序一般在執行和開發時間上的速度都比精心設計的 Remoting 應用程序慢。

Remoting 的類型位於 System.Runtime.Remoting 命名空間中;而 Web Services 的類型則位於 System.Web.Services 中。

(P173)

通過一個安全的 HTTP 通道進行連接時,WCF 允許通過 System.IdentityModel.Claims 和 System.IdentityModel.Policy 命名空間中的類型指定一個 CardSpace 身份。

【第06章】

(P174)

編程所需要的許多核心工具都不是由 C# 語言提供的,而是由 .NET Framework 中的類型提供的。

一個 C# 的 char 表示一個 Unicode 字符,它是 System.Char 結構體的別名。

System.Char 定義了許多處理字符的靜態方法,如 ToUpper 、 ToLower 和 IsWhiteSpace 。可以通過 System.Char 類型或它的別名 char 調用這些方法。

ToUpper 和 ToLower 會受到最終用戶的語言環境的影響,這可能會導致出現細微的缺陷。

(P175)

System.Char 、 System.String 還提供了針對語言變化的 ToUpper 和 ToLower ,它們加上後綴 Invariant 。

char 保留的大多數靜態方法都與字符分類有關。

(P176)

對於更細的分類,char 提供了一個名爲 GetUnicodeCategory 的靜態方法,它返回一個 UnicodeCategory 枚舉值。

通過顯式轉換一個整數,可以產生一個位於 Unicode 集之外的 char 。要檢測字符的有效性,我們可以調用 char.GetUnicodeCategory : 如果結果是 UnicodeCategory.OtherNotAssigned ,那麼這個字符就是無效的。

一個 char 佔用 16 個二進制位。

C# 的 string (== System.String) 是一個不可變的 (不可修改的) 字符序列。

創建字符串的最簡單的方法就是給變量定義一個字面值。

要創建一個重複的字符序列,可以使用 string 的構造函數。

還可以從 char 數組創建字符串,而 ToCharArray 方法則是執行相反操作。

我們還可以重載 string 的構造方法來接受各種 (不安全的) 指針類型,以便創建其他類型字符串。

空字符串是長度爲 0 的字符串。如果要創建空字符串,可以使用一個字母值或靜態的 string.Empty 字段;如果要測試空字符串,可以執行一個等值比較或測試它的 Length 屬性。

由於字符串是引用類型,它們也可能是 null 。

(P177)

靜態的 string.IsNullOrEmpty 方法是測試一個給定字符串是 null 還是空白的快捷方法。

字符串的索引器可以返回一個指定索引位置的字符。與所有操作字符串的方法相似,它是從 0 開始計數的索引。

string 還實現了 IEnumerable<char> ,所以可以用 foreach 遍歷它的字符。

在字符串內搜索的最簡單方法是 Contains 、 StartsWith 和 EndsWith 。所有這些方法都返回 true 或 false 。

Contains 方法並沒有提供這種重載的便利方法,但是可以使用 IndexOf 方法實現相同的效果。

IndexOf 方法更強大 : 它會返回指定字符或子字符串的首次出現位置 (-1 表示該子字符串不存在) 。

StartsWith 、 EndsWith 和 IndexOf 都有重載方法,我們可以指定一個 StringComparison 枚舉變量或 CultureInfo 對象,控制大小寫和文字順序。默認爲使用當前文化規則執行區分大小寫的匹配。

LastIndexOf 與 IndexOf 類似,但是它是從後向前開始搜索的。

IndexOfAny 則返回任意一系列字符的首次匹配位置。

LastIndexOfAny 則在相反方向執行相同的操作。

由於 String 是不可變的,所有 “處理” 字符串的方法都會返回一個新的字符串,而原始字符串則不受影響 (其效果與重新賦值一個字符變量一樣) 。

Substring 是取字符串的一部分。

(P178)

如果省略長度,那麼會得到剩餘的字符串。

Insert 和 Remove 會從一個指定位置插入或刪除一些字符。

PadLeft 和 PadRight 會用特定字符將字符串 (如果未指定,則使用空格) 填充成指定的長度。

如果輸入字符串長度大於填充長度,那麼返回不發生變化的原始字符串。

TrimStart 和 TrimEnd 會從字符串的開始或結尾刪除指定的字符;Trim 則用兩個方法執行刪除操作。默認情況下,這些函數會刪除空白字符 (包括空格、製表符、換行符和這些字符的 Unicode 變體) 。

Replace 會替換字符串中出現的特定字符或子字符串。

ToUpper 和 ToLower 會返回輸入字符串相應的大寫和小寫字符。默認情況下,它們會受用戶的當前語言設置的影響;ToUpperInvariant 和 ToLowerInvariant 總是採用英語字母表規則。

Split 接受一個句子,返回一個單詞數組。

默認情況下,Split 使用空白字符作爲分隔符;經過重載後也可以接受包含 char 或 string 分隔符的 params 數組。

Split 還可以選擇接受一個 StringSplitoptions 枚舉值,它支持刪除一些空項 : 這在一行單詞由多種分隔符分隔時很有用。

靜態的 Join 方法執行與 Split 相反的操作,它需要一個分隔符和字符串數組。

靜態的 Concat 方法與 Join 類似,但是它只接受字符串數組參數,並且沒有分隔符。

Concat 與 + 操作符效果完全相同 (實際上,編譯器會將 + 轉換成 Concat) 。

(P179)

靜態的 Format 方法提供了創建嵌入變量字符串的便利方法。嵌入的變量可以是任意類型;而 Format 會直接調用它們的 ToString 。

包含嵌入變量的主字符串稱爲 “組合格式字符串” 。調用 String.Format 時,需要提供一個組合格式字符串,後面緊跟每一個嵌入式變量。

花括號裏面的每一個數字稱爲格式項。這些數字對應參數位置,後面可以跟 :

1. 逗號與應用的最小寬度;

2. 冒號與格式字符串;

最小寬度用於對齊各個列,如果這個值爲複數,那麼數據就是左對齊;否則,數據就是右對齊的。

信用額度是通過 “C” 格式字符串格式化爲貨幣值。

組合格式字符串的缺點是它很容易出現一些只有在運行時才能發現的錯誤。

進行兩個值比較時,.NET Framework 有兩個不同的概念 : 等值比較和順序比較。等值比較會判斷兩個實例在語義上是否是相同的;而順序比較則將兩個 (如果有) 實例按照升序或降序排列,然後判斷哪一個首先出現。

(P180)

等值比較並不是順序比較的一個子集,這兩種方法有各自不同的用途。

對於字符串等值比較,可以使用 == 操作符或者其中一個字符串的 Equals 方法。後者功能更強一些,因爲它們允許指定一些選項,如區分大小寫。

另一個不同點是,如果變量被轉換成 object 類型,那麼 == 就不一定是按字符串處理。

對於字符串順序比較,可以使用 CompareTo 實例方法或靜態的 Compare 和 CompareOrdinal 方法 : 這些方法會返回一個正數、負數或 0 ,這取決於第一個值是在第二個值之後、之前還是同時出現。

字符串比較有兩種基本的算法 : 按順序的和區分文化的。順序比較會直接將字符解析爲數字 (根據它們的 Unicode 數值);文化比較則參照特定的字母表來解析字符。特殊的文化有兩種 : “當前文化” ,這是基於計算機控制面板的設置;“不變文化” ,這在任何計算機上都是相同的。

對於等值比較,順序和特定文化的算法都是很有用的。然而,在排序時,人們通常選擇詞義相關的比較 : 對字符串按字母表排序時,需要一個字母順序表。順序比較則使用 Unicode 數字位置值,這可能會使英語字符按字母順序排序 —— 但是即使這樣也可能不滿足你的期望。

不變文化封裝了一個字母表,它認爲大寫字符與其對應的小寫字符是相鄰的。

順序算法將所有大寫字母排列在前面,然後纔是全部小寫字符。

儘管順序比較有一些侷限性,但是字符串的 == 操作符總是執行區分大小寫的順序比較。當不帶參數調用時,string.Equals 的實例版本也是一樣的;這定義了 string 類型的 “默認” 等值比較行爲。

字符串的 == 和 Equals 函數選擇順序算法的原因是它既高效又具有確定性。字符串等值比較被認爲是基礎操作,並且遠比順序比較的使用更頻繁。

等式的 “嚴格” 概念也與常見的 == 操作符用途保持一致。

(P181)

靜態方法會更適合一些,因爲即使其中一個或兩個字符串爲 null 它也一樣有效。

String 的 CompareTo 實例方法執行區分文化和區分大小寫的順序比較。與 == 操作符不同,CompareTo 不使用順序比較 : 對於順序比較,區分文化的算法更有效。

Compare 實例方法實現了 IComparable 泛型接口,這是在整個 .NET Framework 中使用的標準比較協議。這意味着字符串的 CompareTo 定義了默認的順序行爲字符串。

所有順序比較的方法都會返回正數、負數 或 0 ,這取決於第一個值是在第二個值之後、之前還是相同位置。

(P182)

StringBuilder 類 (System.Text 命名空間) 表示一個可變 (可編輯) 的字符串。使用 StringBuilder ,可以 Append 、 Insert 、 Remove 和 Replace 子字符串,而不需要替換整個 StringBuilder 。

StringBuilder 的構建函數可以選擇接受一個初始字符串值,以及其內部容量的初始值 (默認是 16 個字符) 。如果需要更大的容量,那麼 StringBuilder 會自動調整它的內部結構,以容納 (會有一些性能開銷) 最大的容量 (默認爲 int.MaxValue) 。

StringBuilder 的一個普通使用方法是通過重複調用 Append 來創建一個長字符串。這個方法比複雜連接普通字符串類型要高效得多。

AppendLine 執行新添加一行字符串 (在 Windows 中是 "\r\n") 的 Append 操作。

AppendFormat 接受一個組合格式字符串,與 String.Format 類似。

除了 Insert 、 Remove 和 Replace 方法 (Replace 函數類似於字符串的 Replace),StringBuilder 定義了一個 Length 屬性和一個可寫的索引器,可用來 獲取 / 設置 每個字符串。

如果要清除 StringBuilder 的內容,我們可以創建一個新的 StringBuilder 或者將它的 Length 設爲 0 。

(P183)

將 StringBuilder 的 Length 設置爲 0 不會減少它的內部容量。

Unicode 具有約一百萬個字符的地址空間,目前已分配的大約有十萬個。

.NET 類型系統的設計使用的是 Unicode 字符集。但是,ASCII 是隱含支持的,因爲它是 Unicode 的子集。

UTF-8 對於大多數文本而言是最具空間效率的 : 它使用 1~4 個字節來表示每個字符。

UTF-8 是最普遍的文本文件和流的編碼方式 (特別是在互聯網上) ,它是 .NET 中默認的流 I / O 編碼方式 (事實上,它幾乎是所有語言隱含的默認編碼方式) 。

UTF-16 使用一個或兩個 16 位字來表示一個字符,它是 .NET 內部用來表示字符和字符串的方式。有一些程序也使用 UTF-16 寫文件。

UTF-32 是空間效率最低的 : 每一個代碼點直接對應一個 32 位數,所以每個字符都會佔用 4 個字節。因此,UTF-32 很少使用。然而,它可以簡化隨機訪問,因爲每個字符都對應相同的字節數。

System.Text 中的 Encoding 類是封裝文本編碼類的通用基本類型。它有一些子類,它們的作用是封裝各種編碼方式的相似特性。初始化一個正確配置類的最簡單方法是用一個標準的 IANA 名稱調用 Encoding.GetEncoding 。

最常用的編碼也可以通過專用的 Encoding 靜態屬性獲取。

(P184)

靜態的 GetEncodings 方法會返回所有支持的編碼方式清單以及它們的標準 IANA 名稱。

Encoding 對象最常見的應用是控制文件或流的文本讀寫操作。

UTF-8 是所有文件和流 I / O 的默認文本編碼方式。

Encoding 對象和字節數組之間也可以進行互相轉換。GetBytes 方法將使用指定的編碼方式將 string 轉換爲 byte[];而 GetString 則將 byte[] 轉換爲 string 。

(P185)

.NET 將字符和字符串存儲爲 UTF-16 格式。

在 System 命名空間中有三個不可變結構可用來表示日期和時間 : DateTime 、 DateTimeOffset 和 TimeSpan 。而 C# 沒有定義與這些類型相對應的關鍵字。

TimeSpan 表示一段時間間隔或者是一天內的時間。對於後者,他就是一個 “時鐘” 時間 (不包括日期) ,它等同於從半夜 12 點開始到現在的時間 (假設沒有夏時制) 。TimeSpan 的最小單位爲 100 納秒,最大值爲 1 千萬天,可以爲正數或負數。

創建 TimeSpan 的方法有三種 :

1. 通過其中一個構造方法;

2. 通過調用其中一個靜態的 From... 方法;

3. 通過兩個 DateTime 相減得到;

(P186)

如果希望指定一個單位的時間間隔,如分鐘、小時等,那麼靜態的 From.. 方法更方便。

TimeSpan 重載了 < 、 > 、 + 和 - 操作符。

Total... 屬性則返回表示整個時間跨度的 double 類型值。

靜態的 Parse 方法則執行與 ToString 相反的操作,它能將一個字符串轉換爲一個 TimeSpan 。

TryParse 執行與 ToString 相同的操作,但是當轉換失敗時,它會返回 false ,而不是拋出異常。

XmlConvert 類也提供了符合標準 XML 格式化協議的 TimeSpan 字符串轉換方法。

TimeSpan 的默認值是 TimeSpan.Zero 。

TimeSpan 也可用於表示一天內時間 (從半夜 12 點開始經過的時間) 。要獲得當前的時間,我們可以調用 DateTime.Now.TimeOfDay 。

(P187)

DateTime 和 DateTimeOffset 表示日期或者時間的不可變結構。它們的最小單位爲 100 納秒,值的範圍從 0001 到 9999 年。

DateTimeOffset 是從 Framework 3.5 開始引入的,在功能上類似於 DateTime 。它的主要特性是能夠存儲 UTC 偏移值,這允許我們比較不同時區的時間值時得到更有意義的結果。

DateTime 和 DateTimeOffset 在處理時區方式上是不同的。DateTime 具有三個狀態標記,可表示 DateTime 是否與下列因素相關 :

1. 當前計算機的本地時間;

2. UTC (相當於現代的格林威治時間) ;

3. 不確定;

DateTimeOffset 更加特殊 —— 它將 UTC 的偏移量存儲爲一個 TimeSpan 。

這會影響等值比較結果,而且是在 DateTime 和 DateTimeOffset 之間進行選擇的主要依據 :

1. DateTime 會忽略三個比較狀態標記,並且當兩個值的年、月、日、時、分等相等時就認爲它們是相等的;

2. 如果兩個值引用相同的時間點,那麼 DateTimeOffset 就認爲它們是相等的;

夏時制會使這個結果差別很大,即使應用程序不需要處理多個地理時區。

在大多數情況中,DateTimeOffset 的等值比較邏輯會更好一些。

(P188)

如果在運行時指定與本地計算機相關的值,使用 DateTime 會更好。

DateTime 定義了能夠接受年、月和日以及可選的時、分、秒和毫秒的構造方法。

如果只指定日期,那麼時間會被隱含地設置爲半夜時間 (00:00:00) 。

DateTime 構造方法也允許指定一個 DateTimeKind —— 這是一個具有以下值的枚舉值 : Unspecified 、 Local 、 Utc 。

這三個值與前一節所介紹的三個狀態標記相對應。

Unspecified 是默認值,它表示 DateTime 是未指定時區的。

Local 表示與當前計算機的本地時區相關。

本地 DateTime 不包含它引用了哪一個特定的時區,而且與 DateTimeOffset 不同的是,它也不包含 UTC 偏移值。

DateTime 的 Kind 屬性返回它的 DateTimeKind 。

DateTime 的構造方法也經過重載從而可以接受 Calendar 對象 —— 允許使用 System.Globalization 中所定義的日曆子類指定一個時間。

DateTime 總是使用默認的公曆。

如果要使用另一個日曆進行計算,那麼必須使用 Calendar 子類自己的方法。

也可以使用 long 類型的計數值 (ticks) 來創建 DateTime,其中計數值是從午夜開始算起的 100 納秒數。

在互操作性上,DateTime 提供了靜態的 FromFileTime 和 FromFileTimeUtc 方法來轉換一個 Windows 文件時間 (由 long 指定),並且提供了 FromOADate 來轉換一個 OLE 自動日期 / 日期 (由 double 指定) 。

要從字符串創建 DateTime,我們必須調用靜態的 Parse 或 ParseExact 方法。

這兩個方法都接受可選標記和格式提供者;ParseExact 還接受格式字符串。

(P189)

DateTimeOffset 具有類似的構造方法,其區別是還需要指定一個 TimeSpan 類型的 UTC 偏移值。

TimeSpan 必須剛好是整數分鐘,否則函數會拋出一個異常。

DateTimeOffset 也有一些接受 Calendar 對象、 long 計數值的構造方法,以及接受字符串的靜態的 Parse 和 ParseExact 方法。

還可以通過構造方法從現有的 DateTime 創建 DateTimeOffset 。

也可以通過隱式轉換創建。 從 DateTime 隱式轉換到 DateTimeOffset 是很簡單的,因爲大多數的 .NET Framework 類型都支持 DateTime —— 而不是 DateTimeOffset 。

如果沒有指定偏移量,那麼可以使用以下規則從 DateTime 值推斷出偏移值 :

1. 如果 DateTime 具有一個 UTC 的 DateTimeKind ,那麼其偏移量爲 0 ;

2. 如果 DateTime 具有一個 Local 或 Unspecified (默認) 的 DateTimeKind ,那麼偏移量從當前的本地時區計算得到;

爲了在其他方法中進行轉換,DateTimeOffset 提供了三個屬性,它們返回 DateTime 類型的值 :

1. UtcDateTime 屬性會返回一個 UTC 時間表示的 DateTime ;

2. LocalDateTime 屬性返回一個以當前本地時區 (在需要時進行轉換) 表示的 DateTime ;

3. DateTime 屬性返回一個以任意指定的時區表示的 DateTime ,以及一個 Unspecified 的 Kind ;

DateTime 和 DateTimeOffset 都具有一個靜態的 Now 屬性,它會返回當前的日期和時間;

DateTime 也具有 Today 屬性,它返回日期部分;

(P190)

靜態的 UtcNow 屬性會返回以 UTC 表示的當前日期和時間。

所有這些方法的精度取決於操作系統,並且一般是在 10 ~ 20 毫秒內。

DateTime 和 DateTimeOffset 提供了返回各種 日期 / 時間 的類似實例屬性。

DateTimeOffset 也有一個類型爲 TimeSpan 的 Offset 屬性。

調用 DateTime 的 ToString 會將結果格式化爲一個短日期 (全部是數字) ,後跟一個長時間 (包括秒) 。

(P191)

默認情況下,操作系統的控制面板決定日、月或年是否在前、是否使用前導零,以及是使用 12 小時還是 24 小時時間格式。

調用 DateTimeOffset 的 ToString 效果是一樣的,只是它同時返回偏移值。

ToShortDateString 和 ToLongDateString 方法只返回日期部分。

ToShortTimeString 和 ToLongTimeString 方法只返回時間部分。

剛剛介紹的這四個方法實際上是四個不同的格式字符串的快捷方式。ToString 重載後可以接受一個格式字符串和提供者,這允許指定大量的選項,並且控制區域設置的應用方式。

靜態的 Parse 和 ParseExact 方法執行與 ToString 相反的操作,它們將一個字符串轉換成一個 DateTime 或 DateTimeOffset 。Parse 方法重載後也可以接受格式提供者。

因爲 DateTime 和 DateTimeOffset 是結構體,它們是不可爲空的。當需要將它們設置爲空時,可以使用以下兩種方法 :

1. 使用一個 Nullable 類型值;

2. 使用靜態域 DateTime.MinValue 或 DateTimeOffset.MinValue (這些類型的默認值) ;

使用一個可空值通常是最佳方法,因爲編譯器會防止出現錯誤。DateTime.MinValue 對於兼容 C# 2.0 (引入了可空類型) 之前編寫的代碼是很有用的。

(P192)

當比較兩個 DateTime 實例時,只有它們的計數值是可以比較的,它們的 DateTimeKinds 是被忽略的。

TimeZone 和 TimeZoneInfo 類提供了關於時區名稱、 UTC 偏移量和夏令時規則等信息。

TimeZoneInfo 在兩者中較爲強大,並且是 Framework 3.5 的新增特性。

這兩種類型的最大區別是 TimeZone 只能訪問當前的本地時區,而 TimeZoneInfo 則能夠訪問全世界的時區。而且,TimeZoneInfo 具有更豐富的 (雖然有時不宜使用) 基於規則的夏令時描述模型。

(P193)

靜態的 TimeZone.CurrentTimeZone 方法會基於當前的本地設置返回一個 TimeZone 對象。

TimeZoneInfo 類採用類似的處理方式。TimeZoneInfo.Local 返回當前的本地時區。

靜態的 GetSystemTimeZones 方法則返回全世界所有的時區。

(P197)

格式化表示將對象轉換爲一個字符串;而解析表示將一個字符串轉換爲某種對象。

最簡單的格式化機制是 ToString 方法,它能夠爲所有簡單的值類型產生有意義的輸出。對於反向轉換,這些類型都定義了靜態的 Parse 方法。

如果解析失敗,它會拋出一個 FormatException 。許多類型還定義了 TryParse 方法,如果轉換失敗,它會返回 false ,而不是拋出一個異常。

(P198)

如果遇到錯誤,在異常處理代碼塊中調用 TryParse 是更快速且更好的處理方式。

使用格式提供者的方法是 IFormattable 。所有數字類型和 DateTime(Offset) 都實現了這個接口。

格式字符串提供一些指令;而格式提供者則決定了這些指令是如何轉換的。

大多數類型都重載了 ToString 方法,可以省略 null 提供者。

(P199)

.NET Framework 定義了以下三種格式提供者 (它們都實現了 IFormatProvider) : NumberFormatInfo 、 DateTimeFormatInfo 、 CultureInfo 。

所有 enum 類型都可以格式化,但是它們沒有具體的 IFormatProvider 類。

在格式提供者的上下文中,CultureInfo 作爲其他兩個格式提供者的間接機制,返回一個適合文化區域設置的 NumberFormatInfo 或 DateTimeFormatInfo 。

(P200)

組合格式字符串可以包含組合變量替代符和格式字符串。

Console 類本身重載了它的 Write 和 WriteLine 方法,以接受一個組合格式字符串。

所有格式提供者都實現了 IFormatProvider 接口 。

(P202)

標準格式字符串決定數字類型或 DateTime / DateTimeOffset 集是如何轉換爲字符串的。格式字符串有兩種 :

1. 標準格式字符串 —— 可以使用標準格式字符串是實現基本的控制。標準格式字符串是由一個字母及其後面一個可選的數字 (它的作用由前面的字母決定) 組成;

2. 自定義格式字符串 —— 可以使用自定義格式字符串作爲模板對每一個字符進行精細控制;

自定義格式字符串與自定義格式提供者無關。

(P203)

如果不提供數字格式字符串或者使用 null 或空字符串,那麼相當於使用不帶數字的 “G” 標準格式化字符串。

每一種數字類型都定義了一個靜態的 Parse 方法,它接受 NumberStyles 參數。NumberStyles 是一個標記枚舉值,可以判斷如何讀取轉換爲數字類型的字符串。

(P208)

.NET Framework 將以下類型稱爲基本類型 :

1. bool 、 char 、 string 、 System.DateTime 和 System.DateTimeOffset ;

2. 所有 C# 數值類型;

靜態 Convert 類定義了將每一個基本類型轉換成其他基本類型的方法。可是,這些方法大多數都是無用的 : 它們或者拋出異常,或者是隱式轉換的冗餘方法。

(P209)

所有基本類型都 (顯式地) 實現了 IConvertible ,它定義了轉換到其他基本類型的方法。在大多數情況中,每一種方法的實現都直接調用 Convert 類中的方法。在少數情況中,編寫一個接受 IConvertible 類型的參數是很有用的。

允許在數字類型之間執行的隱式和顯式轉換,概括爲 :

1. 隱式轉換隻支持無值丟失的轉換;

2. 只有會出現值丟失的轉換才需要使用顯式轉換;

轉換是經過效率優化的,,因此它們將截斷不符合要求的數據。

Convert 的數值轉換方法採用圓整的方式。

Convert 採用銀行的圓整方式,將中間值轉換爲偶整數 (這樣可以避免正負偏差) 。

To (整數類型) 方法隱含了一些重載方法,它們可以將數字轉換爲其他進制。第二個參數指定了進制數,它可以是任何一種進制 (二、八、十或十六進制) 。

ChangeType 的缺點是無法指定一個格式字符串或解析標記。

Convert 的 ToBase64String 方法能夠將一個字節數組轉換爲 Base 64 ;FromBase64String 則執行相反操作。

(P211)

大多數基本類型都可以通過調用 BitConverter.GetBytes 轉換爲字節數組。

應用程序的國際化包括兩個方面 : 全球化和本地化。

全球化注重於三個任務 (重要性由大到小) :

1. 保證程序在其他文化環境中運行時不會出錯;

2. 採用一種本地文化的格式化規則;

3. 設計程序,使之能夠從將來可能編寫和部署的附屬程序集讀取與文化相關的數據和字符串;

本地化表示爲特定文化編寫附屬程序集以結束最終任務。

(P213)

Round 方法能夠指定圓整的小數位數以及如何處理中間值 (遠離 0 ,或者使用銀行的圓整方式) 。

Floor 和 Ceiling 會圓整到最接近的整數 : Floor 總是向下圓整,而 Ceiling 則總是向上圓整 —— 即使是負數 。

(P214)

BigInteger 結構體是 .NET Framework 新增的特殊數值類型。它位於 System.Numerics.dll 中新的 System.Numerics 命名空間,可以用於表示一個任意大的整數而不會丟失精度。

C# 並不提供 BigInteger 的原生支持,所以無法表示 BigInteger 值。然而,可以從任意整數類型隱式地轉換到 BigInteger 。

可以將一個 BigInteger 隱式地轉換爲標準數值類型,也可以顯式地進行反向轉換。

BigInteger 重載了所有的算術運算符,以及比較、等式、求模 (%) 和負值運算符。

將一個數字存儲到一個 BigInteger 中而不是字節數組的優點是可以獲得值類型的語義,調用 ToByteArray 可以將一個 BigInteger 轉換回字節數組。

Complex 結構體是 Framework 4.0 新增的另一個特殊數值類型,用來表示用 double 類型的實數和虛數構成的複數。

要使用 Complex ,我們需要實例化這個結構體,指定實數和虛數值。

(P215)

Complex 結構體具有實數和虛數值的屬性,以及階和量級。

還可以通過指定量級和階來創建複數。

複數也重載了標準的算術操作符。

Complex 結構體具有一些支持更高級功能的靜態方法,其中包括 :

1. 三角函數;

2. 取對數與求冪;

3. 共軛;

Random 類能夠生成一個隨機 byte 、 integer 或 double 類型的僞隨機數序列。

要使用 Random ,首先要實例化,可選擇提供一個種子來實例化隨機數序列。使用相同的種子一定會產生相同序列的數字,當希望有可再現性時,是非常有用的。

如果不希望可再現性,那麼可以不使用種子來創建 Random 而是使用當前系統時間來創建。

因爲系統時鐘只有有限的粒度,創建時間間隔很小 (一般是 10ms 內) 的兩個 Random 將會產生相同序列的值。常用的方法是每次需要一個隨機數時才實例化一個新的 Random 對象,而不是重用同一個對象。

調用 Next(n) 可以生成一個 0 至 n-1 之間的隨機整數。NextDouble 可以生成一個 0 至 1 之間的隨機 double 數值。NextBytes 會用隨機數填充一個字節數組。

(P216)

System.Enum 的靜態實用方法主要是與轉換和獲取成員清單相關。

(P217)

每一種整型 (包括 ulong) 都可以轉換爲十進制數而不會丟失值。

Enum.ToObject 能夠將一個整型值轉換爲一個指定類型的 enum 實例。

(P218)

ToObject 已經重載,可以接受所有的整數類型和對象 (後者支持任何裝箱的整數類型) 。

Enum.Parse 可以將一個字符串轉換爲一個 enum 。它接受 enum 類型和一個包含多個成員的字符串。

Enum.GetValues 返回一個包含某特定 enum 類型的所有成員。

Enum.GetNames 執行相同的操作,但是返回的是一個字符串數組。

在內部,CLR 通過反射 enum 類型的字段實現 GetValues 和 GetNames ,其結果會被緩存以提高效率。

枚舉類型的語義很大程序上是由編譯器決定的。在 CLR 中,enum 實例 (未拆箱) 與它實際的整型值在運行時是沒有任何區別的。而且,在 CLR 中定義的 enum 僅僅是 System.Enum 的子類型,它的每個成員都是靜態的整型域。

(P219)

C# 會在調用 enum 實例的虛方法之前對它進行顯式裝箱。而且,當 enum 實例被裝箱後,它會獲得一個引用其 enum 類型的封裝。

Framework 4.0 提供了一組新的泛型類來保存不同類型的元素集,稱爲元組。

每種元組都有名爲 Item1 、 Item2 等的只讀屬性,分別對應一種類型參數。

可以通過它的構造方法實例化一個元組,或者通過靜態幫助方法 Tuple.Create 。後者使用的是泛型推斷方法,可以將這種方法與隱式類型轉換結合使用。

元組可以很方便地用來實現從一個方法返回多個值或者創建值對集合。

元組的替代方法是使用對象數組。然而,這種方法會影響靜態類型安全性,增加了值類型的 裝箱 / 開箱 開銷,並且需要作一些編譯器無法驗證的複雜轉換。

(P220)

元組是一些類 (也就是引用類型) 。

Guid 結構體表示一個全局唯一標識符 : 一個隨機生成的 16 位值,幾乎可以肯定具有唯一性。Guid 在應用程序和數據庫中通常用作各種排序的鍵。

我們可以調用靜態的 Guid.NewGuid 方法創建一個新的隨機 Guid 。

ToByteArray 方法可以將一個 Guid 轉換爲一個字節數組。

靜態的 Guid.Empty 屬性會返回一個空的 Guid (全爲零) ,通常用來替換 null 。

(P221)

相等有兩種類型 :

1. 值相等 —— 兩個值在某種意義上是相等的;

2. 引用相等 —— 兩個引用指向完全相同的對象;

默認情況下 :

1. 值類型採用的是值相等;

2. 引用類型採用的是引用相等;

事實上,值類型只能使用值相等形式進行比較 (除非已裝箱) 。

引用類型默認是採用引用相等的比較形式。

(P222)

有三種標準方法可以實現等值比較 :

1. == 和 != 運算符;

2. 對象的虛方法 Equals ;

3. IEquatable<T> 接口;

Equals 在 System.Object 中定義,所以所有類型都支持這個方法。

Equals 是在運行時根據對象的實際類型解析的。

對於結構體,Equals 會調用每個字段的 Equals 執行結構比較。

(P223)

Equals 很適合用來比較兩個未知類型的對象。

object 類提供了一個靜態的幫助方法,它的名稱是 Equals ,與虛方法相同,但是不會有衝突,因爲它接受兩個參數。

如果在處理編譯時未知類型對象,這是一種能夠避免 null 值異常的等值比較算法。

(P224)

靜態方法 object.ReferenceEquals 可以實現引用等值比較。

另一種採用引用等值比較的方法是將值轉換爲 object ,然後再使用 == 運算符。

調用 object.Equals 的結果是強制對值類型執行裝箱。這在對性能高度敏感的情況下是不太適合的,因爲裝箱操作相對於實際比較操作的開銷還要高。C# 2.0 引入了一個解決辦法,那就是使用 IEquatable<T> 接口。

關鍵在於實現 IEquatable<T> 所返回的結果與調用 object 的虛方法 Equals 是一樣的,但是執行速度會更快。大多數 .NET 基本類型都實現了 IEquatable<T> 。可以在泛型中使用 IEquatable<T> 作爲一個約束。

(P225)

默認的等值比較操作有 :

1. 值類型採用的是值相等;

2. 引用類型採用的是引用相等;

此外 :

結構體的 Equals 方法默認採用的是結構值相等。

有時創建一個類型時重載這個行爲是很有用的,有以下兩種情況我們需要這樣做 :

1. 修改相等的語義 —— 當 == 和 Equals 默認行爲不符合要求的類型,並且這種行爲一般人難以想象時,修改相等的語義是很有用的。

2. 提高結構體的等值比較的執行速度 —— 結構體的默認結構等值比較算法相對較慢。通過重載 Equals 來實現這個過程可以將性能提高 20% 。重載 == 運算符和實現 IEquatable<T> 接口可以實現等值比較的拆箱,並且同樣能夠將比較速度提高 20% 。

(P226)

重載引用類型的等值語義並不能提高性能。引用等值比較的默認算法已經非常快速,因爲它只比較兩個 32 位或 64 位引用。

重載等值語義操作步驟總結 :

1. 重載 GetHashCode() 和 Equals() ;

2. (可選) 重載 != 和 == ;

3. (可選) 實現 IEquatable<T> ;

在 System.Object 中定義的 GetHashCode 對於散列表而言非常重要,所以每一種類型都具有一個散列碼。

引用類型和值類型都只有默認的 GetHashCode 實現,這意味着不需要重載這個方法 —— 除非重載了 Equals 。 (反之亦然,如果重載了 GetHashCode ,那麼也必須重載 Equals) 。

下面是重載 object.GetHashCode 的其他規則 :

1. 它必須爲 Equals 方法都返回 true 的兩個對象返回相同的值,因此, GetHashCode 和 Equals 必須同時重載;

2. 它不能拋出異常;

3. 如果重複調用相同對象,必須返回相同的值 (除非對象改變) ;

(P227)

結構體的默認散列方法只是在每個字段上執行按位異或操作,通常會比編寫的算法產生更多的重複碼。

類的默認 GetHashCode 實現基於一個內部對象標識,它在 CLR 當前實現中的每一個實例上都是唯一的。

object.Equals 的執行邏輯如下 :

1. 對象不能是 null (除非它是可空類型) ;

2. 相等是自反性的 (對象與其本身相等) ;

3. 相等是可交換的 (如果 a.Equals(b) ,那麼 b.Equals(a)) ;

4. 相等時可傳遞的 (如果 a.Equals(b) 且 b.Equals(c) ,那麼 a.Equals(c)) ;

5. 等值操作是可重複且可靠的 (它們不會拋出異常) ;

除了重載 Equals ,還可以選擇重載相等和不等運算符。這種重載幾乎都發生在結構體上,否則 == 和 != 運算符無法正確判斷類型。

對於類,與兩種方法可以處理 :

1. 保留 == 和 != ,這樣它們會應用引用相等;

2. 重載 Equals 同時重載 == 和 != ;

(P228)

爲了保持完整性,在重載 Equals 時,最好也要實現 IEquatable<T> ,其結果應該總是與被重載對象 Equals 方法保持一致,如果自己編寫 Equals 方法實現,那麼實現 IEquatable<T> 並沒有任何的程序開銷。

(P229)

除了標準等值協議,C# 和 .NET 還定義了用於確定對象之間相對順序的協議。基本的協議包括 :

1. IComparable 接口 (IComparable 和 IComparable<T>) ;

2. > 和 < 運算符;

IComparable 接口可用於普通的排序算法。

< 和 > 操作符比較特殊,它們大多數情況用於比較數字類型。因爲它們是靜態解析的,所以可以轉換爲高效的字節碼,適用於一些密集型算法。

.NET Framework 也通過 IComparer 接口實現了可插入的排序協議。

(P230)

CompareTo 方法按如下方式執行 :

1. 如果 a 在 b 之後,那麼 a.CompareTo(b) 返回一個正數;

2. 如果 a 與 b 位置相同,那麼 a.CompareTo(b) 返回 0 ;

3. 如果 a 在 b 之前,那麼 a.CompareTo(b) 返回一個負數;

(P231)

在重載 < 和 > 後,同時實現 IComparable 接口,這也是一種標準方法,但是反之不成立。事實上,大多數實現了 IComparable 的 .NET 類型都沒用重載 < 和 > 。與等值的處理方法不同的是,在等值中如果重載了 Equals ,一般也會重載 == 。

字符串不支持 < 和 > 運算符。

【第07章】

(P234)

System.Diagnostics 中的 Process 類可以用於啓動一個新的進程。

Process 類也允許查詢計算機上運行的其他進程,並與之交互。

(P235)

.Net Framework 提供了標準的存儲和管理對象集合的類型集。其中包括可變大小列表、鏈表和排序或不排序字典以及數組。在這些類型中,只有數組屬於 C# 語言;其餘的集合只是一些類,可以像使用其他類一樣進行實例化。

Framework 中的集合類型可以分成以下三類 :

1. 定義標準集合協議的接口;

2. 隨時可用的集合類 (列表、字典等) ;

3. 編寫應用程序特有集合的基類;

集合命名空間有以下幾種 :

System.Collections —— 非泛型集合類和接口;
System.Collections.Specialized —— 強類型非泛型集合類;
System.Collections.Generic —— 泛型集合類和接口;
System.Collections.ObjectModel —— 自定義集合的委託和基類;
System.Collections.Concurrent —— 線程安全的集合;

(P236)

IEnumerator 接口定義了以向前方式遍歷或枚舉集合元素的基本底層協議。

MoveNext 將當前元素或 “遊標” 向前移動到下一個位置,如果集合沒有更多的元素,那麼它會返回 false 。Current 返回當前位置的元素 (通常需要從 object 轉換爲更具體的類型) 。在取出第一個元素之前,我們必須先調用 MoveNext —— 即使是空集合也支持這個操作。如果 Reset 方法實現了,那麼它的作用就是將位置移回到起點,允許再一次遍歷集合。 (通常是不需要調用 Reset 的,因爲並非所有枚舉器都支持這個方法) 。

IEnumerable 可以看作是 “IEnumerator 的提供者” ,它是集合類需要實現的最基礎接口。

(P237)

IEnumerable<T> 實現了 IDisposable 。它允許枚舉器保存資源引用,並保證這些資源在枚舉結束或者中途停止時能夠被釋放。foreach 語句能夠識別這個細節。

(P238)

using 語句保證清理操作的執行。

有時由於下面一個或多個原因而希望實現 IEnumerable 或 IEnumerable<T> :

1. 爲了支持 foreach 語句;

2. 爲了與任何使用標準集合的組件交互;

3. 作爲一個更復雜集合接口實現的一部分;

4. 爲了支持集合初始化器;

爲了實現 IEnumerable / IEnumerable<T> ,必須提供一個枚舉器。可以採用以下三個方法來實現 :

1. 如果這個類 “包裝” 了任何一個集合,那麼就返回所包裝集合的枚舉器;

2. 使用 yield return 的迭代器;

3. 實例化 IEnumerator / IEnumerator<T> ;

還可以創建一個現有集合類的子類,Collection<T> 正是基於此目的而設計的。

返回另一個集合的枚舉器就是調用內部集合的 GetEnumerator 。然而,這種方法僅僅適合一些最簡單的情況,那就是內部集合的元素正好是所需要的類型。

更好的方法是使用 C# 的 yield return 語句編寫迭代器。

迭代器是 C# 語言的一個特性,它能夠協助完成集合編寫,與 foreach 語句協助完成集合遍歷的方式是一樣的。

迭代器會自動處理 IEnumerable 和 IEnumerator 或者它們的泛型類的實現。

注意, GetEnumerator 實際上不返回一個枚舉器,通過解析 yield return 語句,編譯器編寫一個隱藏的枚舉器類,然後重構 GetEnumerator 來實例化和返回這個類。

迭代器很強大,也很簡單,並且是 LINQ 實現的基礎。

(P240)

因爲 IEnumerable<T> 實現了 IEnumerable ,所以必須同時實現泛型和非泛型的 GetEnumerator 。

最後一種編寫 GetEnumerator 的方法是編寫一個直接實現 IEnumerator 的類。

(P241)

實現 Reset 方法不是必需的,相反,可以拋出一個 NotSupportedException 。

注意,第一次調用 MoveNext 會將位置移到列表的第一個 (而非第二個) 元素。

(P242)

IEnumerable<T> (和 IEnumerable ) —— 支持最少的功能 (只支持枚舉) 。

ICollection<T> (和 ICollection ) —— 支持一般的功能 。

IList<T> / IDictionary<K, V> 及其非泛型版本 —— 支持最多的功能 。

大多數情況下不需要實現這些接口,幾乎在需要編寫一個集合類的任何時候,都可以使用子類 Collection<T> 替代。

泛型和非泛型版本的差別很大,特別是對於 ICollection 。

因爲泛型出現在後,而泛型接口是爲了後面出現的泛型而開發的。

ICollection<T> 並沒有繼承 ICollection ;

IList<T> 也沒有繼承 IList ;

而且 IDictionary<TKey, TValue> 也同樣不繼承 IDictionary 。

當然,在有利的情況下,集合類本身通常是可以實現某個接口的兩個版本的。

.NET Framework 中並沒有一種統一使用集合 (collection) 和 列表 (list) 這兩個詞的方法。我們通常將 集合 (collection) 和 列表 (list) 這兩個術語看作在很多方面是同義的,只有在使用具體類型時例外。

ICollection<T> 是對象的可計數集合的標準接口。它提供了很多功能,包括確定集合大小 (Count) 、確定集合中是否存在某個元素 (Contains) 、將集合複製到一個數組 (ToArray) 以及確定集合是否爲只讀 (IsReadOnly) 。對於可寫集合,可能還需要對集合元素執行 Add 、 Remove 和 Clear 操作。而且,由於它繼承了 IEnumerable<T> ,所以也支持通過 foreach 語句進行遍歷。

(P243)

非泛型的 ICollection 具有與可計數集合類似的功能,但是它不支持修改列表或檢查元素成員的功能。

IList<T> 是標準的可按位置索引的接口,除了從 ICollection<T> 和 IEnumerable<T> 繼承的功能,它還提供了按位置 (通過一個索引器) 讀寫元素和按位置 插入 / 刪除 元素的功能。

IndexOf 方法可以對列表執行線性搜索,如果未找到指定項,那麼返回 -1 。

IList 非泛型版本具有更多的成員方法,因爲它繼承了少量的 ICollection 成員方法。

(P244)

非泛型 IList 接口的 Add 方法返回一個整數,這是最新添加元素的索引。相反,ICollection<T> 的 Add 方法的返回類型爲 void 。

通用的 List<T> 類是 IList<T> 和 IList 的典型表現。C# 數組也同時實現了泛型和非泛型的 IList 。

爲了與只讀的 Windows Runtime 集合實現互操作,Framework 4.5 引入了一個新的集合接口 IReadOnlyList<T> 。這個接口本身很有用,並且可以看作爲 IList<T> 的縮減版本,它只包含列表只讀操作所需要的成員。

因爲它的類型參數只用在輸出位置,所以它被標記爲協變式 (covariant) 。

IReadOnlyList<T> 表示一個鏈表的只讀版本,它並不意味着底層實現也是隻讀的。

IReadOnlyList<T> 與 Windows 運行時類型 IVectorView<T> 相對應。

(P245)

Array 類是所有一維和多維數組的隱式基類,它是實現標準集合接口的最基本類型之一。

Array 類提供了類型統一性,所以常見的方法都適用於所有的數組,而與它們聲明或實際的元素類型無關。

由於數組是基本類型,所以 C# 提供了明確的聲明和初始化語法。

當使用 C# 語法聲明一個數組時,CLR 會在內部將它轉化爲 Array 的子類 —— 合成一個對應數組維數和元素類型的僞類型。

CLR 也會特別處理數組類型的創建,將它們分配到一塊連續的內存空間。因此數組的索引非常高效,但是不允許在創建後修改數組大小。

Array 實現了 IList<T> 的泛型與非泛型的集合接口。

Array 類實例也提供了一個靜態的 Resize 方法,但是它實際上是創建一個新數組,然後將每一個元素複製到新數組中。Resize 方法是很低效的,而且程序的數組引用無法修改爲新位置。

實現可變大小集合的最好方法是使用 List<T> 類。

(P246)

因爲 Array 是一個類,所以無論數組的元素是什麼類型,數組 (本身) 總是引用類型。

兩個不同的數組在等值比較中總是不相等的 —— 除非使用自定義的等值比較。

Framework 4.0 提供了一種用於比較數組或元組元素的比較方式,可以通過 StructuralComparisons 類型進行訪問。

數組可以通過 Clone 方法進行復制。然而,這是一個淺克隆,表示只有數組本身表示的內存會被複制。如果數組包含的是值類型的對象,那麼這些值會被複制類;如果數組包含的是引用類型的對象,那麼只有引用被複制。

如果要進行深度複製即複製引用類型子對象,必須遍歷整個數組,然後手動克隆每個元素。相同的規則也適用於其他 .NET 集合類型。

CLR 不允許任何對象 (包括數組) 在大小上超過 2GB (無論是運行在 32 位或是 64 位環境上) 。

(P247)

你可能會以爲 Array 類的許多方法是實例方法,但是實際上它們是靜態方法。這是一個奇怪的設計方法,意味着在尋找 Array 方法時,應該同時查看靜態方法和實例方法。

最簡單的創建和索引數組的方法是使用 C# 的語言構造。

此外,可以通過調用 Arrray.CreateInstance 動態實例化一個數組,可以在運行時指定元素類型和維數以及爲非零開始索引的數組指定下界。非零開始索引的數組不符合 CLS (Common Language Specification ,公共語言規範) 。

靜態的 GetValue 和 SetValue 方法訪問動態創建的數組的元素 (它們也支持普通數組的元素訪問) 。

動態創建的從零開始索引的數組可以轉換爲一種類型匹配或兼容 (兼容標準數組變化規則) 的 C# 數組。

爲什麼不使用 object[] 作爲統一的數組類型,而要使用 Array 類呢?原因就是 object[] 既不兼容多維數組,也不兼容值類型以及非零開始索引的數組。

GetValue 和 SetValue 也支持編譯器創建的數組,並且它們對於編寫能夠處理任意類型和任意維數數組的方法是很有用的。

(P248)

如果元素與數組類型不一致,SetValue 方法會拋出一個異常。

當實例化數組時,無論是通過語言語法還是 Array.CreateInstance ,數組元素都會自動初始化。對於引用類型元素的數組,這意味着寫入 null 值;對於值類型元素的數組,這意味着調用值類型的默認構造函數 (實際上是成員的 “歸零” 操作)。

數組可以通過 foreach 語句進行枚舉。

也可以使用靜態的 Array.ForEach 方法進行枚舉。

(P249)

GetLength 和 GetLongLength 會返回一個指定維度的長度 (0 表示一維數組),而 Length 和 LongLength 返回數組的元素總數 (包括所有維數) 。

GetLowerBound 和 GetUpperBound 在處理非零開始索引的數組時是很有用的。GetUpperBound 返回的結果與任意維度的 GetLowerBound 和 GetLength 相加的結果是相同的。

(P250)

Array.Sort 要求數組中的元素實現 IComparable ,這意味着 C# 的最基本類型都可以進行排序。

如果元素是不可比較的,或者希望重寫默認的順序比較,那麼必須給 Sort 提供一個自定義的比較提供者,用來判斷兩個元素的相對位置。可以採用以下方法 :

1. 通過一個實現 IComparer / IComparer<T> 的幫助對象;

2. 通過一個 Comparison 委託 : public delegate int Comparison<T> (T x, T y) ;

Comparison 委託採用與 IComparer<T>.CompareTo 相同的語義。

(P251)

作爲 Sort 的替代方法,可以使用 LINQ 的 OrderBy 和 ThenBy 運算符。與 Array.Sort 不同的是,LINQ 運算符不會修改原始數組,而是將排序結果保存在一個新的 IEnumerable<T> 序列中。

Array 有 4 個方法可以執行淺拷貝操作 : Clone 、 CopyTo 、 Copy 和 ConstrainedCopy 。前兩個方法都是實例方法;後兩個方法是靜態方法。

Clone —— 方法返回一個全新 (淺拷貝) 的數組;

CopyTo 和 Copy —— 方法複製數組的若干連續元素;

ConstrainedCopy —— 執行一個原子操作 : 如果所有請求的元素都無法成功複製,那麼操作會回滾;

Array 還有一個 AsReadOnly 方法,它會返回一個包裝器,可以防止元素被重新賦值。

(P252)

System.Linq 命名空間包含另外一些適合用於執行數組轉換的擴展方法。這些方法會返回一個 IEnumerable<T> ,它可以通過 Enumerable 的 ToArray 方法轉換回一個數組。

在靈活性和性能方面,泛型類更具優勢,而它們的非泛型冗餘實現則是爲了實現向後兼容。

泛型 List<T> 和非泛型 ArrayList 類提供了一種動態調整大小的對象數組實現,它們是集合類中使用最廣泛的類。 ArrayList 實現了 IList ,而 List<T> 同時實現了 IList 和 IList<T> 。與數組不同,所有接口都是公開實現的。

在內部,List<T> 和 ArrayList 都維護了一個對象數組,並在超出容量時替換爲一個更大的數組。添加元素是很高效的 (因爲數組末尾通常還有空閒存儲位置) ,但是插入元素的速度會慢一些 (因爲插入位置之後的所有元素都必須向後移動才能留出插入空間) 。與數組一樣,如果對已排序列表執行 BinarySearch 方法,那麼查找是很高效的,但是其他情況效率就不高,因爲查找時必須檢查每一個元素。

如果 T 是一種值類型,那麼 List<T> 的速度會比 ArrayList 快好幾倍,因爲 List<T> 不需要元素執行裝箱和開箱操作。

List<T> 和 ArrayList 具有可以接受已有元素集合的構造函數,它們會將已有集合的每一個元素複製到新的 List<T> 或 ArrayList 中。

(P254)

非泛型 ArrayList 類主要用於向後兼容 Framework 1.x 代碼。

ArrayList 的功能與 List<object> 類型相似。當需要一個包含不共享任何相同基類的混合類型元素時,這兩種類型是很有用的。在這種情況下,如果需要使用反射機制處理列表,那麼選擇使用 ArrayList 更具優勢。相比於 List<object> ,反射機制更容易處理非泛型的 ArrayList 。

如果定義 System.Linq 命名空間,那麼可以通過先調用 Cast 再調用 ToList 的方式將一個 ArrayList 轉換爲一個泛型 List 。

Cast 和 ToList 是 System.Linq.Enumerable 的擴展方法,是從 .NET Framework 3.5 開始支持的。

LinkedList<T> 是一個泛型的雙向鏈表。雙向鏈表是一系列互相引用的節點,其中每個節點都引用前一個節點、後一個節點及實際存儲數據的元素。它的主要優點是元素總是能夠高效地插入到鏈表的任意位置,因爲插入節點只需要創建一個新節點,然後修改引用值。然而,查找插入節點的位置可能減慢執行速度,因爲鏈表本身沒有直接索引的內在機制;我們必須遍歷每一個節點,並且無法執行二叉查找。

(P255)

LinkedList<T> 實現了 IEnumerable<T> 和 ICollection<T> 及其非泛型版本,但是沒有實現 IList<T> ,因爲它不支持根據索引進行訪問。

(P256)

Queue<T> 和 Queue 是一種先進先出 (FIFO) 的數據結構,它們提供了 Enqueue (將一個元素添加到隊列末尾) 和 Dequeue (取出並刪除隊列的第一個元素) 方法。它們還包括一個只返回而不刪除隊列第一個元素的 Peek 方法,以及一個 Count 屬性 (可用來檢查出列前的元素個數) 。

雖然隊列是可枚舉的,但是它們都沒有實現 IList<T> / IList ,因爲不能夠直接通過索引訪問它的成員。

隊列內部是使用一個可根據需要調整大小的數組來操作的,這與一般的 List 類很類似。隊列具有一個直接指向頭和尾元素的索引,因此,入列和出列操作是及其快速的 (除非內部的大小需要調整) 。

(P257)

Stack<T> 和 Stack 是後進先出 (LIFO) 的數據結構,它們提供了 Push (添加一個元素到堆棧的頂部) 和 Pop (從堆棧頂部取出並刪除一個元素) 方法。它們還提供了一個只讀而不刪除元素的 Peek 方法,以及 Count 屬性和用於導出數據以實現隨機訪問的 ToArray 方法。

堆棧內部也是使用一個可根據需要調整大小的數組來操作,這一點和 Queue<T> 與 List<T> 類似。

BitArray 是一個保存壓縮 bool 值的可動態調整大小的集合。它具有比簡單的 bool 數組和 bool 泛型 List 更高的內存使用效率,因爲它的每個值只佔用一位,而 bool 類型的每個值佔用一個字節。

(P258)

HashSet<T> 和 SortedSet<T> 分別是 Framework 3.5 和 4.0 新增加的泛型集合。這兩個類都具有以下特點 :

1. 它們的 Contains 方法都使用基於散列的查找而實現快速執行;

2. 它們都不保存重複元素,並且都忽略添加重複值的請求;

3. 無法根據位置訪問元素;

SortedSet<T> 按一定順序保存元素,而 HashSet<T> 則不是。

這些類型的共同點是由接口 ISet<T> 提供的。

HashSet<T> 是通過使用只存儲鍵的散列表實現的;而 SortedSet<T> 則是通過一個 紅 / 黑 樹實現的。

兩個集合都實現了 ICollection<T> 接口。

因爲 HashSet<T> 和 SortedSet<T> 實現了 IEnumerable<T> 接口,所以可以將另一種集合作爲任意集合操作方法的參數。

SortedSet<T> 的構造函數還接受一個可選的 IComparer<T> 參數 (而非一個等值比較器) 。

(P259)

字典是一種所包含元素均爲 鍵 / 值 對的集合。字典通常都用來執行列表查找和排序。

Framework 通過接口 IDictionary 和 IDictionary<TKey, TValue> 及一組通用的字典類定義了一個標準字典協議。這些類在以下方面有區別 :

1. 元素是否按有序序列存儲;

2. 元素是否按位置 (索引) 或按鍵訪問;

3. 類是泛型還是非泛型的;

4. 集合變大時的性能;

(P260)

IDictionary<TKey, TValue> 定義了所有基於 鍵 / 值 的集合的標準協議。它擴展了 ICollection<T> ,增加了一些基於任意類型的鍵訪問元素的方法和屬性。

(P261)

從 Framework 4.5 開始,還出現了一個接口 IReadOnlyDictionary<TKey, TValue> ,它定義了字典成員的只讀子集。它與 Windows Runtime 類型 IMapView<K, V> 相對應,當時也是因爲相同原因而引入的。

重複的鍵在所有字典實現中都是禁止的,所以用相同的鍵調用兩次 Add 會拋出一個異常。

直接通過一個 IDictionary<TKey, TValue> 進行枚舉會返回一個 KeyValuePair 結構體序列。

非泛型的 IDictionary 接口在原理上與 IDictionary<TKey, TValue> 相同,但是存在以下兩個重要的功能區別 :

1. 通過索引器查找一個不存在的鍵會返回 null (而不是拋出一個異常) ;

2. 使用 Contains 而非 ContainsKey 來檢測成員是否存在 ;

枚舉一個非泛型 IDictionary 會返回一個 DictionaryEntry 結構體序列。

泛型 Dictionary (和 List<T> 集合一樣) 是使用最廣泛的集合之一。它使用一個散列表結構來存儲鍵和值,而且快速、高效。

Dictionary<TKey, TValue> 的非泛型版本是 Hashtable ;Framework 中不存在名爲 Dictionary 的非泛型類。當我們提到 Dictionary 時,指的是泛型的 Dictionary<TKey, TValue> 類。

Dictionary 同時實現了泛型和非泛型的 IDictionary 接口,而泛型 IDictionary 是公開的接口。

事實上, Dictionary 是泛型 IDictionary 的一個標準實現。

(P262)

Dictionary 和 Hashtable 的缺點是元素是無序的。而且,添加元素時不保存原始順序。此外,所有字典類型都不允許出現重複值。

(P263)

OrderedDictionary 是一種非泛型字典,它能夠保存添加元素的原始順序。通過使用 OrderedDictionary ,既可以根據索引訪問元素,也可以根據鍵進行訪問。

OrderedDictionary 並不是一個有序的字典。

OrderedDictionary 是 Hashtable 和 ArrayList 的組合。

這個類是在 .NET 2.0 中引入的,特殊的是,它沒有泛型版本。

ListDictionary 和 HybridDictionary 這兩個類都只有非泛型版本。

Framework 只支持兩種在內部結構中將內容根據鍵進行排序的字典 :

1. SortedDictionary<TKey, TValue> ;

2. SortedList <TKey, TValue> (SortedList 是具有相同功能的非泛型版本) ;

(P265)

Collection<T> 類是一個可定製的 List<T> 包裝類。

(P267)

CollectionBase 是 Framework 1.0 引入的 Collection<T> 的非泛型版本。它提供了大多數與 Collection<T> 相似的特性,但是使用方式不太靈活。

KeyedCollection<TKey, TItem> 是 Collection<Item> 的子類。它增加也刪去了一些功能。它增加的功能是按鍵訪問元素,這與字典很相似,刪去的功能是委託自己的內部列表。

KeyedCollection<TKey, TItem> 通常看作是實現了按鍵進行快速查找的 Collection<TItem> 。

(P269)

KeyedCollection 的非泛型版本稱爲 DictionaryBase 。

DictionaryBase 存在的目的就是爲了向後兼容。

ReadOnlyCollection<T> 是一個包裝器,或者稱爲委託,它提供了集合的一種只讀視圖。它的用途是允許一個類公開地顯示集合的只讀訪問,但是同時這個類仍然可以在內部進行修改。

【第08章】

(P277)

LINQ 是 Language Integrated Query 的簡寫,它可以被視爲一組語言和框架特性的集合,我們可以使用 LINQ 對本地對象和遠程數據源進行結構化的類型安全的查詢操作。

在 C# 3.0 和 Framework 3.5 中引入了 LINQ 。

LINQ 可用於查詢任何實現了 IEnumerable<T> 接口的集合類型。

LINQ 具有編譯時的類型檢查及動態查詢組合這兩大優點。

LINQ 中所有核心類型都包含在 System.Linq 和 System.Linq.Expressions 這兩個命名空間中。

LINQ 數據源的基本組成部分是序列和元素。在這裏,序列是指任何實現了 IEnumerable<T> 接口的對象,其中的每一項則稱爲一個元素。

查詢運算符是 LINQ 中用於轉換序列的方法。通常,查詢運算符可接收一個輸入序列,並將其轉換爲一個輸出序列。在 System.Linq 命名空間的 Enumerable 類中定義了約 40 種查詢運算符,這些運算符都是以靜態擴展方法的形式來實現的,稱爲標準查詢運算符。

我們把對本地序列進行的查詢操作稱爲本地查詢或者是 LINQ 到對象查詢。

LINQ 還支持對那些從遠程數據源中動態獲取的序列進行查詢,這些序列需要實現 IQueryable<T> 接口,而在 Queryable 類中則有一組相應的標準查詢運算符對其進行支持。

(P278)

一個查詢可以理解爲一個使用查詢運算符對所操作的序列進行轉換的表達式。

由於標準查詢運算符都是以靜態擴展方法的方式來實現的,因此我們可以像使用對象的實例方法那樣直接使用。

大多數查詢運算符都接受一個 Lambda 表達式作爲參數。

Lambda 表達式用於對查詢進行格式化。

(P279)

運算符流語法和查詢表達式語法是兩種互補的 LINQ 表達方法。

運算符流是最基本同時也是最靈活的書寫 LINQ 表達式的方式。

如果想創建更復雜的查詢表達式,只需在前面的表達式後面添加新的查詢運算符。

(P280)

查詢運算符絕不會修改輸入序列,相反,它會返回一個新序列。這種設計是符合函數式編程規範的, LINQ 的思想實際上就起源於函數式編程。

(P281)

每個查詢運算符對應着一個擴展方法。

(P282)

返回一個 bool 值的表達式我們稱之爲 “斷言” 。

查詢運算符的 Lambda 表達式針對的是集合中的每個元素,而不是集合整體。

標準的查詢運算符使用了一個泛型 Func 委託。 Func 是 System.Linq 命名空間中一組通用的泛型委託,它的作用是保證 Func 中的參數順序和 Lambda 表達式中的參數順序一致。

(P283)

標準的查詢運算符使用下面這些泛型 :

1. TSource —— 輸入集合的元素類型;

2. TResult —— 輸出集合的元素類型 (不同於 TSource) ;

3. TKey —— 在排序、分組或者連接操作中所用的鍵 ;

這裏的 TSource 由輸入集合的元素類型決定。而 TResult 和 TKey 則由我們給出的 Lambda 表達式指定。

Lambda 表達式可以指定輸出序列的類型,也就是說 Select 運算符可以根據 Lambda 表達式中的定義將輸入類型轉化成輸出類型。

Where 查詢運算符的內部操作比 Select 查詢運算符要簡單一些,因爲它只篩選集合,不對集合中的元素進行類型轉換,因此不需要進行類型推斷。

Func<TSource, TKey> 將每個輸入元素關聯到一個排序鍵 TKey ,TKey 的類型也是由 Lambda 表達式中推測出來的,但它的類與同輸入類型、輸出類型是沒有關係的,三者是獨立的,類型可以相同也可以不同。

(P284)

實際上我們可以使用傳統的方式直接調用 Enumerable 中的各種方法來實現查詢運算符的功能,此時在查詢過程可以不使用 Lambda 表達式。這種直接調用的方式在對本地集合進行查詢時非常好用,尤其是在 LINQ to XML 這種操作中應用最爲方便。

傳統調用方式並不適合對 IQueryable<T> 類型集合的查詢,最典型的就是對數據庫的查詢,因爲在對 IQueryable<T> 類型數據進行查詢時,Queryable 類中的運算符需要 Lambda 表達式來生成完整的查詢表達式樹,沒有 Lambda 表達式,這個表達式樹將不能生成。

LINQ 中集成了對集合的排序功能,這種內置的排序對整個 LINQ 體系來說有重要意義。因爲一些查詢操作直接依賴於這種排序。

Take 運算符 —— 會輸出集合中前 x 個元素,這個 x 以參數的形式指定;

Skip 運算符 —— 會跳過集合中的前 x 個元素,輸出其餘元素;

Reverse 運算符 —— 則會將集合中的所有元素反轉,也就是按照元素當前順序的逆序排列;

Where 和 Select 這兩個查詢運算符在執行時,會將集合中元素按照原有的順序進行輸出。實際上,在 LINQ 中,除非有必要,否則各個查詢運算符都不會改變集合中元素的排序方式。

(P285)

Union 運算符會將結果集合中相同的元素去掉;

(P286)

查詢表達式一般以 from 子句開始,最後以 select 或者 group 子句結束。

(P287)

查詢表達式中的所有邏輯都可以用運算符流語法來書寫。

緊跟在 from 關鍵字之後的標識符實際上是一個範圍變量,範圍變量指向當前序列中將要進行操作的元素。

在每個子查詢的 Lambda 表達式中,範圍變量都會被重新定義。

要定義存儲中間結果的變量,需要使用下面幾個子句 : let 、 into 、一個新的 from 子句、 join 。

(P288)

查詢表達式語法和運算符流語法各有優勢。

在包含以下運算符的查詢操作中,使用查詢表達式語法更加方便 :

1. 在查詢中使用 let 子句導入新的查詢變量;

2. 在查詢中用到 SelectMany 、 Join 或者 GroupJoin 這些運算符;

對於只包含 Where 、 OrderBy 或者 Select 的查詢語句,這兩種查詢方式都可以。

一般來說,查詢表達式語法由單個的運算符組成,結構比較清晰;而運算符流語法寫出的代碼相對簡潔。

在不含以下運算符的查詢中,選用運算符流語法進行查詢會更加方便 : Where 、 Select 、 SelectMany 、 OrderBy 、 ThenBy 、 OrderByDescending 、 ThenByDescending 、 GroupBy 、 Join 、 GroupJoin 。

如果一個查詢運算符沒有適合的查詢語法,可以混合使用兩種查詢方式來得到最終結果,這樣做的唯一限制是,在整個查詢中,每個查詢表達式的表達必須是完整的 (必須由 from 子句開始,由 select 或者 group 子句結束) 。

(P289)

在比較複雜的查詢中,混合使用兩種查詢語法進行查詢的方式非常高效。

有時候,即使混合使用了兩種查詢語法,也沒有寫出真正簡練的 LINQ 查詢,但注意不要因此養成只使用一種查詢語法的習慣。如果習慣只使用一種語法形式的,在遇到複雜查詢情況時,很難找到一種真正高效的方式去解決問題。

在 LINQ 中,另一個很重要的特性是延遲執行,也可以說是延遲加載,它是指查詢操作並不是在查詢運算符定義的時候執行,而是在真正使用集合中的數據時才執行。

絕大部分標準的 LINQ 查詢運算符都具有延遲加載這種特性,當然也有例外,以下是幾個例外的運算符 :

1. 那些返回單個元素或者返回一個數值的運算符;

2. 轉換運算符 : ToArray 、 ToList 、 ToDictionary 、 ToLookup ;

以上這些運算符都會觸發 LINQ 語句立即執行,因爲它們的返回值類型不支持延遲加載。

(P290)

在 LINQ 中,延遲加載特性有很重要的意義,這種設計將查詢的創建和查詢的執行進行了解耦,這使得我們可以將查詢分成多個步驟來創建,有利於查詢表達式的書寫,而且在執行的時候按照一個完整的結構去查詢,減少了對集合的查詢次數,這種特性在對數據庫的查詢中尤爲重要。

子查詢中的表達式有額外的延遲加載限制。無論是聚合運算符還是轉換運算符,如果出現在子查詢中,它們都會被強制地進行延遲加載。

(P292)

LINQ 查詢運算符之所以有延遲加載功能,是因爲每個運算符的返回值不是一個一般的數組或者集合,而是一個經過封裝的序列,這種序列通常情況下並不直接存儲數據元素,它封裝並使用運行時傳遞給它的集合,元素也由其他集合來存儲它實際上只是維護自己與數據集合的一種依賴關係,當有查詢請求時,再到它依賴的序列中進行真正的查詢。

查詢運算符實際上是封裝一系列的轉換函數,這種轉換函數可以將與之關聯的數據集轉換爲各種形式的序列。如果輸出集合不需要轉換的話,那麼就不用執行查詢運算符封裝的轉換操作,這個時候查詢運算符實際上就是一個委託,進行數據轉發而已。

(P293)

如果使用運算符流語法對集合進行查詢,會創建多個層次的封裝集合。

在使用 LINQ 語句的返回集合時,實際是在原始的輸入集合中進行查詢,只不過在進入原始集合之前,會經過上面這些封裝類的處理,在不同層次的封裝類中,系統都會對查詢做相應的修改,這使得 LINQ 語句使用的各種查詢條件會被反映到最終的查詢結果中。

(P294)

如果在 LINQ 查詢語句的最後加上 ToList 方法,會強制 LINQ 語句立刻執行,查詢結果會被保存到一個 List 類型的集合中。

LINQ 的延遲加載特性有這樣一種功能 : 不論查詢語句是連續書寫的還是分多個步驟完成的,在執行之前,都會被組合成一個完整的對象模型,而且兩種書寫方式所產生的對象模型是一樣的。

LINQ 查詢是一個低效率的流水線。

(P295)

LINQ 使用的是需求驅動的模型,先請求再有數據。

在 LINQ 中,所謂子查詢就是包含在另一個查詢的 Lambda 表達式中的查詢語句。

一個子查詢實際上就是一個獨立的 C# 表達式,可以是 LINQ 表達式,也可以是普通的邏輯判斷,所以只要是符合 C# 語法規則的內容,都可以放在 Lambda 表達式的右側作爲子查詢來使用。也就是說,子查詢的使用規則是由 Lambda 表達式的規則所決定的。

“子查詢” 這個詞,在通常意義下,概念非常寬泛,我們只關注 LINQ 下的子查詢。在運算符流語法中,子查詢是指包含在 Lambda 表達式中的查詢語句。在查詢表達式中,只要包含在其他查詢語句中的查詢,都是子查詢,但是 from 子句除外。

子查詢一般有兩個作用 : 一個是爲父查詢確定查詢範圍,一般是一個較小的查詢範圍,另一個作用是爲外層查詢的 Lambda 表達式提供參數。

(P296)

子查詢在什麼時候執行完全是由外部查詢決定的,當外部查詢開始執行時,子查詢也同時執行,它們是同步的,在整個查詢中,子查詢的執行結果被作爲父查詢的某個組成部分。我們可以認爲查詢的開始命令是從外向內傳遞的,對本地集合的查詢嚴格按照這種由外向內的順序進行;但對數據庫的查詢,則沒有那麼嚴格,只是原則上按照這種方式進行。

另一種理解方式是,子查詢會在需要返回查詢結果時執行,那什麼時候需要子查詢返回查詢結果決定於外部查詢什麼時候被執行。

(P297)

在執行本地查詢時,單獨書寫子查詢是一種常用的查詢方式。但是當子查詢中的數據和外部查詢有緊密關聯的時候,即內部數據需要用到外部數據的值時,這種方式不適合,最好寫成一個表達式。

(P298)

在子查詢中使用單個元素或者聚合函數的時候,整個 LINQ 查詢語句並不會被強制執行,外部查詢還是以延遲加載的方式執行。這是因爲子查詢是被間接執行的,在本地集合查詢中,它通過委託的驅動來執行;而在遠程數據源的查詢中,它通過表達式樹的方式執行。

如果 Select 語句中已經包含了子查詢,在這種情況下如果是本地查詢,那麼相當於將源序列重新封裝到一個新的序列中,集合中的每個元素都是以延遲加載的方式執行的。

書寫複雜的 LINQ 查詢表達式的三種方式 :

1. 遞增式的書寫方式;

2. 使用 into 關鍵字;

3. 包裝查詢語句;

實際上無論用何種書寫方式,在運行時,LINQ 查詢表達式都會被編譯成相同的查詢語句來運行。

在使用多個查詢條件進行查詢的時候,這種遞增式的書寫方式比較實用。

(P299)

根據上下文的不同, into 關鍵字在查詢表達式中有兩種完全不同的功能。這裏首先介紹如何使用 into 關鍵字延長查詢 (另一種是和 GroupJoin 配合使用) 。

在 LINQ 查詢中,一般會用到集合的映射,也就是在 Select 方法中將查詢結果直接組裝成新的集合,這種映射一般在查詢的最後執行。但是如果在映射之後還想對新集合執行查詢的話,就可以使用 into 關鍵字來完成。

(P300)

注意,into 關鍵字只能出現在 select 和 group 關鍵字之後,into 會重新創建一個新的查詢,在新的查詢中,我們可以再次使用 where 、 orderby 、 select 關鍵字。

into 關鍵字的作用就是在原來的查詢中重新創建一次新的查詢,在執行前,這種帶 into 的查詢表達式會被編譯成運算符流的查詢語句,因此使用 into 運算符並不會帶來性能上的損失。

包含了多個層次的查詢表達式,在語義和執行上都和遞增式的 LINQ 查詢語句相同,它們本質上沒有區別,唯一的區別就是查詢關鍵字的使用順序。

在多層次查詢中,內部查詢是在傳遞帶之前執行的。而子查詢則是傳送帶上的一部分,它會隨着整個傳送帶的運行而執行。

(P302)

所謂匿名類型指的是沒有顯式定義過的類型,在查詢過程中,可以使用這種類型來封裝查詢結果。實際上這個類並不是沒有定義,只是不用我們自己定義,編譯器會自動定義這個類型。

要在 C# 代碼中定義一種編譯時才能確定的類型,唯一的選擇是使用 var 關鍵字,此時 var 關鍵字就不僅僅是爲了便於書寫,而是不得不這麼寫,因爲我們不知道匿名類型的名字。

(P303)

使用 let 關鍵字,可以在查詢中定義一個新的臨時變量來存放某些步驟的查詢結果。

編譯器在編譯 let 關鍵字的時候,會把它翻譯成一個匿名類型,這個匿名類型中包含了之前的範圍變量 n 和一個新的表達式變量。也就是說,編譯器將 n 翻譯成了前面的匿名類型查詢。

let 還有以下兩個優點 :

1. 保留了前面查詢中的範圍變量;

2. 在一個查詢中可以重複使用它定義的變量;

在 LINQ 查詢中,在 where 關鍵字之前或之後可以使用任意多個 let 關鍵字。後面的 let 關鍵字會使用前面 let 關鍵字的返回類型,顯然,let 關鍵字會在每一次使用時重新組成結果集。

let 關鍵字一般不用來返回數值類型的結果,更多使用在子查詢中。

LINQ 包含兩種查詢 : 對本地集合的本地查詢以及對遠程數據的解釋型查詢。

對本地集合的查詢,這種查詢調用 IEnumerable<> 接口中定義的 Enumerable 方法實現了接口中所有的方法來完成具體的查詢。

在解釋型的查詢中,所有的查詢操作都是通過 IQueryable<T> 接口中的方法完成的,具體的方法實現是在 Queryable 類中。在這種查詢中,LINQ 語句不會被編譯成 .NET Framework 中間語言 (IL),而會在運行時被解釋成查詢表達式樹來執行。

(P304)

實際上,可以使用 Enumerable 中的方法來查詢 IQueryable<T>  類型的數據源,但會遇到一個問題,那就是查詢的時候,遠端的數據源必須被加載到本地內存中,然後以本地數據源的方式進行處理。可以想象,這種查詢的效率非常低,每次都需要讀取大量的數據,在本地進行篩選。這正是創建解釋型查詢的原因。

在 .NET Framework 中有兩個類都實現了 IQueryable<T> 接口,這兩個類用於實現兩種不同的查詢 :

1. LINQ to SQL;

2. Entity Framework (EF);

這兩種 LINQ-to-db 的查詢技術實際上非常相似。

在對本地數據源的查詢中,也可以使用 IQueryable<T> 接口中的方法進行查詢,只要在本地集合的最後使用一個 AsQueryable 方法即可。

IQueryable<T> 實際上是對 IEnumerable<T> 方法的擴展。

(P306)

查詢表達式樹是 System.Linq.Expression 命名空間下的一種對象模型,這種對象是在運行時被解釋運行的 (這也是爲什麼 LINQ to SQL 和 EF 支持延遲加載)。

解釋型的查詢和本地數據查詢的本質不同在於它們的執行方式。在遍歷解釋型的集合時,整個 LINQ 查詢語句會被編譯成一個完整的查詢表達式樹來加以執行。

(P307)

Entity Framework 也需要類似的標籤,但是除了這些之外,他還需要一個額外的 XML 文件 Entity Data Model (EDM),在這個文件中定義了數據表和實體類的對應關係。

LINQ to SQL 和 EF 中可能定義了 30 種查詢方式,但是在 SQL Server 的 SQL 查詢中只有 10 種查詢方式,而最終 LINQ 查詢表達式要被翻譯成 SQL 來執行,那麼只能在 10 種查詢方法中選一種來使用。如果在 LINQ 使用了一個功能很強大的運算符,但是在 SQL 中卻沒有相同功能的運算符,那麼 LINQ 中的這個運算符就會被翻譯成其他的 SQL 語句來完成這項功能。

一個 LINQ 查詢中可以同時使用解釋型查詢運算符和本地查詢運算符。應用的典型方式就是把本地查詢操作放在外層,將解釋型的查詢操作放在內層,在執行查詢的時候,解釋型的操作先執行,返回一個結果集合給外層的本地查詢使用。這種查詢模式經常用於 LINQ 對數據庫的查詢操作。

查詢運算符絕不會修改輸入序列,相反,它會返回一個新序列。這種設計是符合函數式編程規範的,LINQ 的思想實際上就起源於函數式編程。

(P309)

兩種方式可以間接地調用 AsEnumerable 方法,那就是 ToArray 方法和 ToList 方法。使用 AsEnumerable 方法有下面兩點好處,一是這個方法不會強制查詢立即執行,但是如果希望查詢立即執行的話,就要使用另外兩個方法了;二是它不會創建本地的存儲結構,因此它會比較節省資源。

當查詢邏輯從數據庫移到本地會降低查詢的性能,特別是當查詢的數據量比較大的時候,效率損失更加嚴重。同樣針對上面這個示例,有一個更有效 (同時也更復雜) 的方式來完成上面的查詢,那就是使用 SQL CLR 在數據庫端實現正則表達式的查詢。

(P310)

LINQ to SQL 和 EF 都是用 LINQ 來實現的對象的映射工具,它們之間的不同在於映射的方式,我們知道,在數據庫查詢中,映射的一端是數據庫表,LINQ to SQL 可以將數據庫表結構映射成對象,然後供調用者使用,這種映射嚴格按照數據庫表結構,映射成的對象不需要我們定義。與之不同的是,EF 對這種映射做了一些改進,那就是允許我們定義實體類,也就是允許開發者定義數據庫表被映射成什麼類型。這種映射提供了一種更靈活的解決方案,但是它會降低查詢性能,也增加了使用的複雜度,因爲需要佔用額外的時間去維護數據庫和自定義的實體類間的映射關係。

L2S是由微軟的 C# 團隊完成的,在 Framework 3.5 中發佈,而 EF 是由 ADO.NET 團隊在 ADO.NET SP1 中發佈的。後來 L2S 的開發和維護由 ADO.NET 團隊來接管,由於開發重心的不同,在 .NET Framework 4.0 中對 L2S 的改變很少,而主要的改進集中在 EF 方面。

儘管在性能上和易用性上,EF 在 .NET Framework 4.0 中已經有了極大的改進,但是兩種技術還是各有優勢。L2S 的優點是簡單易用、執行性能好,此外它生成的 SQL 語句的解釋質量更好一些。EF 的優點是允許我們創建自定義的持久化的實體類,用於數據庫的映射。另外 EF 允許使用同一個查詢機制查詢 SQL Server 之外的數據源,實際上 L2S 也支持這個功能,但是爲了鼓勵第三方的查詢機制的出現,L2S 中沒有對外公佈這些機制。

EF 4.0 突出的改進是它支持幾乎所有的 L2S 中的查詢方法。

L2S 允許任何類來承載數據,只要類中加入了合適的標籤即可。

[Table] 標籤定義在 System.Data.Linq.Mapping 命名空間中,它定義的類型用來承載數據表中的一行數據。默認情況下,L2S 會認爲這個類名和它對應的表名是相同的,如果想讓兩者不同的話,由於表名已經固定,只能更改對應的類名,更改方式是在 [Table] 標籤中顯式地指定類名。

在 L2S 中,如果一個類具有 [Table] 標籤,就稱這個類爲實體,爲了能夠順利使用,這個實體的結構必須與數據表的結構相匹配,多字段或少字段都不行。這種限制使得這種映射是一種低級別的映射。

(P311)

[Column] 標籤用來指示數據表中的某列,如果實體中定義的列名和數據表中的別名不同,那麼需要在 [Column] 標籤中特別指出所對應的列名。

[Column] 標籤中的 IsPrimaryKey 屬性用於指示當前列是主鍵,在數據中這列用於唯一標識一條數據,在程序中也用這列區分不同的實體,將實體中的變換更新到數據庫的時候,也需要使用這一列來確定寫入的目標。

總的來講,在定義實體類的時候,L2S 允許將數據庫的字段映射對象 (實體中的屬性) 定義成私有的,它可以訪問到實體類中的私有變量。

實際上與數據庫表對應的實體類是可以自動生成的,不用逐行書寫,常用的生成工具有 Visual Studio (需要在 “工程” 菜單添加一個 “LINQ to SQL Classes” 選項)和命令行工具 SqlMetal 。

和 L2S 中的實體類相似,EF中允許開發者定義自己的實體類用於承載數據,不同的是,EF 中的實體類的定義要靈活得多,在理論上允許任何類型的類來作爲實體類使用 (在某些特殊情況下需要實現一些接口) ,也就是說實體類中的結構不用和數據表中的字段完全對應。

和 L2S 不同的是,在 EF 中,要完成數據的映射和查詢,之定義上面這個實體類是不夠的。因爲在 EF 中,查詢並不是直接針對數據庫進行的,它使用了一種更高級別的抽象模型,稱爲實體數據模型 (EDM , Entity Data Model) ,我們的查詢語句是針對這個模型來定義的。

EDM 實際上是使用 XML 定義的一個 .edmx 類型的文件,這個文件包含三部分內容 :

1. 概念模型 : 定義了數據庫的信息,不同的數據庫有不同的概念模型內容;

2. 存儲模型 : 定義了數據庫的表結構;

3. 映射 : 定義了數據庫表和實體類之間的映射關係;

(P312)

創建 .edmx 文件最簡單的方式是使用 Visual Studio ,在 “項目” 菜單中點擊 “添加新項” ,在彈出的窗口中選擇 “ADO.NET Entity Data Model” 。之後使用嚮導就可以完成實體類到數據庫表的映射配置。這一系列操作不僅添加一個 .edmx 文件,還會創建涉及到的實體類。

在 EF 中實體類都是映射到概念模型上,所有對概念模型的查詢和更新操作,都是由 Object Services 發起的。

EF 的設計者在設計的時候將映射關係想得比較簡單,他們假設數據表和實體類之間的映射關係是 1 : 1 的,所以並沒有提供專門的機制去完成一對多或者多對一的映射。儘管這樣,如果確實需要這種特殊的映射關係,還是可以通過修改 .edmx 文件中的相關內容來實現。下面是幾個常用的修改操作 :

1. 多個表映射到一個實體類;

2. 一個表映射到多個實體類;

3. 按照 ORM 世界中的三種繼承方式將繼承的類映射到表;

三種繼承策略是 :

1. 每個分層結構一張表 : 一張表映射到整個類分層結構。該表中包含分隔符列,用於指出每個行應該映射到哪個類;

2. 每個類一張表 : 一張表映射到一個類,意味着繼承的類映射到多張表。查詢某個實體時,EF 生成 SQL JOIN ,以合併其所有基類;

3. 每個具體類一張表 : 一張單獨的表映射到每個具體的類。這意味着基類映射到多張表,並且在查詢基類的實體時, EF 生成 SQL UNION ;

比較一下,L2S 僅支持每個分層結構一張表。

EF 還支持 LINQ 之外的查詢方式,有一種語言叫 Entity SQL (ESQL),使用這種語言,我們可以通過 EDM 查詢數據庫。這種查詢方式非常便於動態地構建查詢語句。

在創建了實體類之後 (如果是 EF 的話還需要有 EDM 文件),就可以對數據庫進行查詢了。在查詢之前,首先要創建 DataContext (L2S) 或者 ObjectContext (EF) 對象,這個對象用於指定數據庫連接字符串。

(P313)

直接創建 DataContext / ObjectContext 實例是一種很底層的使用方式,它可以展示出這兩種類型是如何工作的。但在實際應用中,更常用的方式是創建類型化的 Context (繼承自 DataContext / ObjectContext) 來使用。

對於 L2S 來說,我們只需爲 DataContext 傳遞一個數據庫連接字符串即可;而對於 EF ,傳遞的是數據庫連接實體,這個實體中除了數據庫連接字符串之外,還包括 EDM 文件的路徑信息。 (如果通過 Visual Studio 創建 EDM 文件,那麼系統會自動在項目的 app.config 文件中添加完整的數據庫連接實體,可以從這個文件得到需要的信息) 。

然後我們就可以使用 GetTable (L2S) 或者 CreateObjectSet (EF) 對象了,這兩個對象都是用於從數據庫中讀取數據。

Single 運算符會根據主鍵從結果集中取出一行記錄。和 First 關鍵字不同的是,Single 運算符要求結果集中只有一條記錄,當結果集中的結果多於一行時,它會拋出異常;而 First 關鍵字在這種情況下則不會拋出異常。

DataContext / ObjectContext 這兩個對象實際上只做兩件事情。第一,它作爲一個工廠,將我們查詢的數據組合成對象。第二,它會維護實體類的狀態,如果查詢出實體類中的值在類外改變了,它會記錄下這個字段,然後便於更新回數據庫。

在 EF 中,唯一的不同點是使用 SaveChanges 方法代替 SubmitChanges 方法。

(P314)

在對數據庫的查詢中,一個更好的方式是爲每個數據庫定義一個繼承自 DataContext / ObjectContext 的子類,一般會爲每個實體類都添加一個這樣的屬性,這種屬性我們稱之爲類型化的 Context 。

儘管 DataContext / ObjectContext 都實現了 IDisposable 接口,而且 Dispose 方法會強制斷開數據庫連接,但是我們一般不通過調用 Dispose 方法來銷燬這兩個對象,因爲 L2S 和 EF 在返回查詢結果後會自動斷開連接。

(P315)

DataContext / ObjectContext 對象有跟蹤實體類狀態的功能,當取出一個表中的數據保存到本地內存之後,如果下次再到數據庫中查詢某條已經存在的數據, DataContext / ObjectContext 並不會去數據庫中讀取數據,而是直接從內存中取出需要的數據。也就是說,在一個 context 的生命週期中,他不會將數據庫中的某行記錄返回兩次 (數據記錄之間使用主鍵進行區分) 。

L2S 和 EF 都允許關閉對象狀態跟蹤功能,爲避免這些限制,在 L2S 中將 DataContext 對象的 ObjectTrackingEnabled 屬性設置成 false 即可。在 EF 中禁用對象跟蹤的功能要麻煩一點,它需要在每個實體中都添加下面的代碼 :

context.Customers.MergeOption = MergeOption.NoTracking;

關閉對象狀態跟蹤功能之後,爲了數據安全,通過 context 向數據庫中提交更新的功能也同時被禁用。

(P316)

如果要從數據庫中得到最新的數據,必須定義一個新的 context 對象,將舊的實體類傳給這個對象,然後調用 Refresh 方法,這樣,最新的數據就會被更新到實體類中。

在一個多層次的系統中,不能在系統的中間層定義一個靜態的 DataContext 或者 ObjectContext 實例完成所有的數據庫查詢操作,因爲 context 對象不能保證線程安全。正確的做法是在中間層的方法中,爲每個請求的客戶創建一個 context ,這樣做的好處是可以減輕數據庫的負擔,因爲維護和更新實體的任務被多個 context 對象分擔。對於數據庫來說,更新操作會通過多個事務執行完成,這顯然比一個很大的事務要高效很多。

使用實體類生成工具還有一個特點,當表之間有關聯關係的時候,我們可以直接使用關聯表中的屬性,實體類自動完成了關聯的字段和關聯表的映射。

(P317)

L2S 查詢中 [Association] 標籤的作用是提供生成 SQL 語句所需的信息;而 EF 中的 [EdmRelationshipNavigationProperty] 標籤的作用是告訴 EF 要到 EDM 中去查找兩個表的關聯關係。

L2S 和 EF 的查詢方式仍然是延遲加載,在 L2S 查詢中,真正的查詢會在遍歷結果集時進行,而 EF 的查詢則是在顯式地調用了 Load 方法之後纔會執行。

(P318)

可以通過設置下面這個屬性使 EF 和 L2S 以相同的方式返回 EntityCollection 和 EntityReferences :

context.ContextOptions.DeferredLoadingEnabled = true;

(P319)

DataLoadOptions 類是 L2S 中一個特有的類,它有兩個作用 :

1. 它允許我們爲 EntitySet 所關聯的類指定一個篩選條件;

2. 它可以強制加載特定的 EntitySets ,這樣可以減少整個數據查詢的次數;

(P320)

L2S 和 EF 都會跟蹤實體類的狀態,如果實體中的數據有所改變,我們可以將這些改變更新回數據庫,更新的方式是調用 DataContext 類中的 SubmitChanges 方法,在 EF 中則是使用 ObjectContext 對象的 SaveChanges 方法。

除此之外,L2S 的 Table<T> 類還提供了 InsertOnSubmit 和 DeleteOnSubmit 方法用於插入和刪除數據表中的記錄;而 EF 的 ObjectSet<T> 類提供了 AddObject 和 DeleteObject 方法來完成相同的功能。

(P321)

SubmitChanges / SaveChanges 會記錄 context 創建以來實體類中所有數據變化,然後將這些變化更新回數據庫中,在更新的過程中,需要創建一個 TransactionScope 對象來幫助完成,以免更新過程中造成的錯誤數據。

也可以使用 EntitySet / EntityCollection 類中的 Add 方法向數據庫中添加新的記錄。在調用了 SubmitChanges 或者 SaveChanges 方法之後,實體中新添加的記錄的外鍵信息會被自動取出來。

爲新添加的實體對象添加主鍵值比較繁瑣,因爲我們需要保證這個主鍵是唯一的,解決辦法是可以在數據庫中定義自增類型的主鍵,或者使用 Guid 作爲主鍵。

L2S 能夠識別它們的關聯關係並賦值是因爲實體類中有這樣的關聯定義,而 EF 之所以可以自動識別關聯並賦值是因爲 EDM 中存儲了這兩種實體間的關聯關係以及關聯字段。

(P322)

當從 EntitySet / EntityCollection 對象中移除一行後,它的外鍵的值會自動設置成 null 。

L2S 和 EF 的 API 對比 :

1. 各種操作的基礎類 : DataContext (L2S) - ObjectContext (EF);

2. 從數據庫中取出指定類型的所有記錄 : GetTable (L2S) - CreateObjectSet (EF);

3. 方法的返回類型 : Table<T> (L2S) - ObjectSet<T> (EF);

4. 將實體中的屬性值的變化 (添加、刪除等) 更新回數據庫 : SubmitChanges (L2S) - SaveChanges (EF);

5. 使用 conetext 更新的方式向數據庫中添加新的記錄 : InsertOnSubmit (L2S) - AddObject (EF);

6. 使用 context 更新的方式刪除記錄 : DeleteOnSubmit (L2S) - DeleteObject (EF);

7. 關聯表中用於存放多條關聯記錄的屬性 : EntitySet<T> (L2S) - EntityCollection<T> (EF);

8. 關聯表中用於存放單條關聯記錄的屬性 : EntityRef<T> (L2S) - EntityReference<T> (EF);

9. 加載關聯屬性的默認方式 : Lazy (L2S) - Explicit (EF);

10. 構建立即加載的查詢方式 : DataLoadOptions (L2S) - Include() (EF);

(P325)

一個查詢表達式樹是由一個微型的 DOM (Document Object Model ,文檔對象模型) 來描述的。這個 DOM 中每個節點都代表了 System.Linq.Expressions 命名空間中的一個類型。

(P326)

Expression<T> 的基類是 LambdaExpression ,LambdaExpression 是 Lambda 表達式樹中所有節點的基類型,所有的節點類型都可以轉換成這種基類型,因此保證了表達式樹中節點的類型一致性。

Lambda 表達式需要接收參數,而普通的表達式則沒有參數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章