《C# 6.0 本質論》 - 學習筆記

《C# 6.0 本質論》

========== ========== ==========
[作者] (美) Mark Michaelis (美) Eric Lippert
[譯者] (中) 周靖 龐燕
[出版] 人民郵電出版社
[版次] 2017年02月 第5版
[印次] 2017年02月 第1次 印刷
[定價] 108.00元
========== ========== ==========

【前言】 

成功學習 C# 的關鍵在於,要儘可能快地開始編程。不要等自己成爲一名理論方面的 “專家” 之後,纔開始寫代碼。

學習一門計算機語言最好的方法就是在動手中學習,而不是等熟知了它的所有 “理論” 之後再動手。

爲了從簡單程序過渡到企業級開發, C# 開發者必須熟練地從對象及其關係的角度來思考問題。

一名知道語法的程序員和一名能因時宜地寫出最高效代碼的專家的區別,關鍵就是這些編碼規範。專家不僅讓代碼通過編譯,還遵循最佳實踐,降低產生 bug 的概率,並使代碼的維護變得更容易。編碼規範強調了一些關鍵原則,開發時務必注意。

總地說來,軟件工程的宗旨就是對複雜性進行管理。

【第01章】 

(P001) 

學習新語言最好的辦法就是動手寫代碼。

(P003) 

一次成功的 C# 編譯生成的肯定是程序集,無論它是程序還是庫。

在 Java 中,文件名必須和類名一致。

從 C# 2.0 開始,一個類的代碼可以拆分到多個文件中,這一特性稱爲 “部分類” 。

編譯器利用關鍵字來識別代碼的結構與組織方式。

(P004) 

C# 1.0 之後沒有引入任何新的保留關鍵字,但在後續版本中,一些構造使用了上下文關鍵字 (contextual keyword) ,它們在特定位置纔有意義。除了那些位置,上下文關鍵字沒有任何特殊意義。這樣,大多數的 C# 1.0 代碼都完全兼容於後續的版本。

分配標識符之後,以後就能用它引用所標識的構造。因此,開發人員應分配有意義的名稱,不要隨意分配。

好的程序員總能選擇簡潔而有意義的名稱,這使代碼更容易理解和重用。

(P005) 

[規範] 

1. 要更注重標識符的清晰而不是簡短;

2. 不要在標識符名稱中使用單詞縮寫;

3. 不要使用不被廣泛接受的首字母縮寫詞,即使被廣泛接受,非必要時也不要用;

下劃線雖然合法,但標識符一般不要包含下劃線、連字符或其他非 字母 / 數字 字符。

[規範] 

1. 要把只包含兩個字母的首字母縮寫詞全部大寫,除非它是駝峯大小寫風格標識符的第一個單詞;

2. 包含 3 個或更多字母的首字母縮寫詞,僅第一個字母纔要大寫,除非該縮寫詞是駝峯大小寫風格標識符的第一個單詞;

3. 在駝峯大小寫風格標識符開頭的首字母縮寫詞中,所有字母都不要大寫;

4. 不要使用匈牙利命名法 (也就是,不要爲變量名稱附加類型前綴) ;

關鍵字附加 “@” 前綴可作爲標識符使用。

C# 中所有代碼都出現在一個類型定義的內部,最常見的類型定義是以關鍵字 class 開頭的。

(P006) 

對於包含 Main() 方法的類, Program 是個很好的名稱。

[規範] 

1. 要用名詞或名詞短語命名類;

2. 要爲所有類名使用 Pascal 大小寫風格;

一個程序通常包含多個類型,每個類型都包含多個方法。

方法可以重用,可以在多個地方調用,所以避免了代碼的重複。

方法聲明除了負責引入方法之外,還要定義方法名以及要傳入和傳出方法的數據。

C# 程序從 Main 方法開始執行,該方法以 static void Main() 開頭。

程序會啓動並解析 Main 的位置,然後執行其中第一條語句。

雖然 Main 方法聲明可以進行某種程度的改變,但關鍵字 static 和方法名 Main 是始終都是程序必需的。

C# 要求 Main 方法的返回類型爲 void 或 int ,而且要麼不帶參數,要麼接收一個字符串數組作爲參數。

(P007) 

args 參數是一個字符串數組,用於接收命令行參數。

Main 返回的 int 值是狀態碼,標識程序執行是否成功。返回非零值通常意味着錯誤。

C# 的 Main 方法名使用大寫 M ,以便與 C# 的 Pascal 大小寫風格命名約定保持一致。

Main() 之前的 void 表明該方法不返回任何數據。

C# 通常用分號標識語句結束,每條語句都由代碼要執行的一個或多個行動構成。

由於換行與否不影響語句的分隔,所以可以將多條語句放到同一行, C# 編譯器會認爲這一行包含多條指令。

C# 還允許一條語句跨越多行。同樣地, C# 編譯器會根據分號判斷語句的結束位置。

(P008) 

分號使 C# 編譯器能忽略代碼中的空白。除了少數例外情況, C# 允許在代碼中隨意插入空白而不改變其語義。

空白是一個或多個連續的格式字符 (如製表符、空格和換行符) 。刪除單詞間的所有空白肯定會造成歧義。刪除引號字符串中的任何空白也會造成歧義。

程序員經常利用空白對代碼進行縮進來增強可讀性。

爲了增強可讀性,利用空白對代碼進行縮進是非常重要的。寫代碼時要遵循已經建立的編碼標準和約定,以增強代碼的可讀性。

(P009) 

聲明變量就是定義它,需要 : 

1. 指定變量要包含的數據的類型;

2. 爲它分配標識符 (變量名) ;

一個變量聲明所指定的數據的類型稱爲數據類型。數據類型,或者簡稱爲類型,是具有相似特徵和行爲的個體的分類。

在編程語言中,類型是被賦予了相似特性的一些個體的定義。

(P010) 

局部變量名採用的是駝峯大小寫風格命名 (即除了第一個單詞,其他的每個單詞的首字母大寫) ,而且不包含下劃線。

[規範] 

1. 要爲局部變量使用 camel 大小寫風格的命名;

局部變量聲明後必須在引用之前爲其賦值。

C# 允許在同一條語句中進行多個賦值操作。

(P011) 

賦值後就能用變量標識符引用值。

所有 string 類型的數據,不管是不是字符串字面量,都是不可變的 (或者說是不可修改的) 。也就是說,不能修改變量最初引用的數據,只能重新爲變量賦值,讓它引用內存中的新位置。

System.Console.ReadLine() 方法的輸出,也稱爲返回值,就是用戶輸入的文本字符串。

(P012) 

System.Console.Read() 方法返回的是與讀取的字符值對應的整數,如果沒有更多的字符可用,就返回 -1 。爲了獲取實際字符,需要先將整數轉型爲字符。

除非用戶按回車鍵,否則 System.Console.Read() 方法不會返回輸入。按回車鍵之前不會對字符進行處理,即使用戶已經輸入了多個字符。

C# 2.0 以上的版本可以使用 System.Console.ReadKey() 方法。它和 System.Console.Read() 方法不同,用戶每按下一個鍵就返回用戶所按的鍵。可用它攔截用戶按鍵操作,並執行相應行動,如校驗按鍵,限制只能按數字鍵。

(P013) 

在字符串插值中,編譯器將字符串花括號中的部分解釋爲可以嵌入代碼 (表達式) 的區域,編譯器將對嵌入的表達式估值並將其轉換爲字符串。字符串插值不需要先逐個執行很多個代碼片段,最後再將結果組合成字符串,它可以一步完成這些輸出。這使得代碼更容易理解。

C# 6.0 之前的版本利用的是複合格式化 (composite formatting) 來進行一次性輸出。在複合格式化中,代碼首先提供格式字符串 (format string) 來定義輸出格式。

(P014) 

佔位符在格式字符串中不一定按順序出現。

佔位符除了能在格式字符串中按任意順序出現之外,同一個佔位符還能在一個格式字符串中多次使用。

(P015) 

[規範] 

1. 不要使用註釋,除非代碼本身 “一言難盡” ;

2. 要儘量編寫清晰的代碼,而不是通過註釋澄清複雜的算法;

(P016) 

在 .NET 中,一個程序集包含的所有類型 (以及這些類型的成員) 構成這個程序集的 API 。

同樣,對於程序集的組合,例如 .NET Framework 中的程序集組合,每個程序集的 API 組合在一起構成一個更大的 API 。這個更大的 API 組通常被稱爲框架 (framework) , .NET Framework 就是指 .NET 包含的所有程序集對外暴露的 API 。

一般地, API 包括一系列接口和協議 (或指令) ,它們定義了程序和一組部件交互的規則。實際上,在 .NET 中,協議本身就是 .NET 程序集執行的規則。

(P017) 

一個公共編程框架,稱爲基類庫 (Base Class Library , BCL) ,提供開發者能夠 (在所有 CLI 實現中) 依賴的大型代碼庫,使他們不必親自編寫這些代碼。

(P018) 

.NET Core 不同於完整的 .NET Framework 功能集,它包含了整個 (ASP.NET) 網站可以在 Windows 之外的操作系統上部署所需的功能以及 IIS (Internet Information Server , 因特網信息服務器) 。這意味着,同樣的代碼可以被編譯和執行成跨平臺運行的應用程序。

.NET Core 包含了 .NET 編譯平臺 (“Roslyn”) 、 .NET Core 運行時、 .NET 版本管理 (.NET Version Manager , DNVM) 以及 .NET 執行環境 (.NET Execution Environment , DNX) 等工具,可以在 Linux 和 OS X 上執行。

(P020) 

事實上,一些免費工具 (如 Red Gate Reflector 、 ILSpy 、 JustDecompile 、 dotPeek 和 CodeReflect) 可以將 CIL 自動反編譯成 C# 。

【第02章】 

(P022) 

C# 有幾種類型非常簡單,被視爲其他所有類型的基礎。這些類型稱爲預定義類型 (predefined type) 。

C# 語言的預定義類型包括 8 種整數類型、 2 種用於科學計算的二進制浮點類型、 1 種用於金融計算的十進制浮點類型、 1 種布爾類型以及 1 種字符類型。

decimal 是一種特殊的浮點類型,能夠存儲大數值而無表示錯誤。

(P023) 

C# 的所有基本類型都有短名稱和完整名稱。完整名稱對應於 BCL (Base Class Library , 基類庫) 中的類型命名。

由於基本數據類型是其他類型的基礎,所以 C# 爲基本數據類型的完整名稱提供了短名稱或縮寫的關鍵字。

C# 開發人員一般選擇使用 C# 關鍵字。

[規範] 

1. 要在指定數據類型時使用 C# 關鍵字而不是 BCL 名稱 (例如,使用 string 而不是 String) ;

2. 要保持一致而不要變來變去;

(P024) 

浮點數的精度是可變的。

與浮點數不同, decimal 類型保證範圍內的所有十進制數都是精確的。

雖然 decimal 類型具有比浮點類型更高的精度,但它的範圍較小。

decimal 的計算速度稍慢 (雖然這個差別可以忽略不計) 。

除非超過範圍,否則 decimal 數字表示的十進制數都是完全準確的。

(P025) 

默認情況下,輸入帶小數點的字面量,編譯器會自動把它解釋成 double 類型。

整數值 (沒有小數點) 通常默認爲 int ,但前提是該值不要太大,以至於無法用 int 來存儲。

要顯示具有完整精度的數字,必須將字面量顯式聲明爲 decimal 類型,這是通過追加一個 M (或者 m) 後綴來實現的。

(P026) 

d 表示 double ,之所以用 m 表示 decimal ,是因爲這種數據類型經常用於貨幣 (monetary) 計算。

對於整數數據類型,相應的後綴是 U 、 L 、 LU 和 UL 。整數字面量的類型是像下面這樣確定的 : 

1. 沒有後綴的數值字面量按照以下順序,解析成能夠存儲該值的第一種數據類型 : int 、 uint 、 long 、 ulong ;

2. 具有後綴 U 的數值字面量按照以下順序,解析成能夠存儲該值的第一種數據類型 : uint 、 ulong ;

3. 具有後綴 L 的數值字面量按照以下順序,解析成能夠存儲該值的第一種數據類型 : long 、 ulong ;

4. 如果數值字面值的後綴是 UL 或 LU ,則解析成 ulong 類型;

注意,字面量的後綴不區分大小寫。但對於 long ,一般推薦使用大寫字母 L ,因爲小寫字母 l 和數字 1 不好區分。

[規範] 

1. 要使用大寫的字面量後綴;

2. 十六進制和十進制的相互轉換不會改變數本身,改變的只是數的表示形式;

(P027) 

要以十六進制形式輸出一個數值,必須使用 x 或 X 數值格式說明符。大小寫決定了十六進制字母的大小寫。

(P028) 

雖然從理論上說,一個二進制位就足以容納一個布爾類型的值,但 bool 數據類型的實際大小是一個字節。

字符類型 char 表示 16 位字符,其取值範圍對應於 Unicode 字符集。

從技術上說, char 的大小和 16 位無符號整數 (ushort) 相同,後者的取值範圍是 0 ~ 65535 。

(P029) 

爲了輸入 char 類型的字面量,需要將字符放到一對單引號中。

反斜槓和特殊字符代碼組成轉義序列 (escape sequence) 。

可以使用 Unicode 代碼表示任何字符。爲此,請爲 Unicode 值附加 \u 前綴。

(P030) 

零或多個字符組成的有限序列稱爲字符串。

爲了將字面量字符串輸入代碼,要將文本放入雙引號 (") 內。

字符串由字符構成,所以轉義序列可以嵌入字符串內。

雙引號要用轉義序列輸出,否則會被用於定義字符串開始與結束。

在 C# 中,可以在字符串前面使用 @ 符號,指明轉義序列不被處理。

結果是一個逐字字符串字面量 (verbatim string literal) ,它不僅將反斜槓當作普通字符處理,還會逐字解釋所有空白字符。

(P031) 

在以 @ 開頭的字符串中,唯一支持的轉義序列是 "" ,它代表一個雙引號,這個雙引號不會終止字符串。

假如同一個字符串字面量在程序集中多次出現,編譯器在程序集中只定義字符串一次,且所有變量都將指向同一個字符串。

通過使用字符串插值格式,字符串可以支持嵌入的表達式。字符串插值語法在一個字符串字面量前加上一個 $ 符號前綴,然後將表達式嵌入大括號中。

注意,字符串字面量可以通過在 “@” 符號前加上 “$” 符號的字符串插值組合而成。

(P032) 

字符串插值是調用 string.Format() 方法的簡寫。

(P033) 

string.Format() 不是在控制檯窗口中顯示結果,而是返回結果。

增加了字符串插值功能之後, string.Format() 的重要性減弱了不少 (除了對本地化功能的支持) 。在後臺,字符串插值是利用了 string.Format() 編譯成 CIL 的。

目前靜態方法的調用通常是包含一個命名空間的前綴後面跟類型名。

(P034) 

using static 指令必須放在文件的最開始。

using static 指令只對靜態方法和屬性有效,對於實例成員不起作用。

using 指令與 using static 指令類似,使用後也可以省略命名空間前綴。與 using static 指令不同的是, using 指令在文件 (或命名空間) 中應用非常普遍,不僅只應用於靜態成員。無論是實例化,或是靜態方法調用,抑或是使用 C# 6.0 中新增的 nameof 操作符時,使用 using 指令都可以隨意地省略所有的命名空間引用。

無論是使用 string.Format() 還是用 C# 6.0 的字符串插值功能構造複雜格式的字符串,總要用一組豐富的、複雜的格式模板來顯示數字、日期、時間、時間段等等。

如果想在一個插值的字符串或格式化的字符串中真正出現左大括號或者右大括號,可以通過連續輸入兩個大括號表明這個大括號不是引入的格式模板。

輸出新行所需的字符取決於執行代碼的操作系統。

(P035) 

字符串的長度不能直接設置,它是根據字符串中的字符數計算得到的。此外,字符串的長度不能更改,因爲字符串是不可變的。

string 類型的關鍵特徵在於它是不可變的 (immutable) 。

(P036) 

與類型有關的兩個額外的關鍵字是 null 和 void 。 null 值由關鍵字 null 標識,表明變量不引用任何有效的對象。 void 表示沒有類型,或者沒有任何值。

(P037) 

null 也可以作爲字符串字面量的類型使用。 null 表示將變量設爲 “無” 。 null 值只能賦給引用類型、指針類型和可空值類型。

將變量設爲 null ,會顯式地設置引用,使它不指向任何位置。

必須注意,和根本不賦值相比,將 null 賦給引用類型的變量是完全不同的概念。換言之,賦值爲 null 的變量已被設置,而未賦值的變量未被設置,所以假如在賦值前使用變量會造成編譯時錯誤。

將 null 值賦給一個 string 變量,並不等同於將空字符串 "" 賦給它。 null 意味着變量無任何值,而 "" 意味着變量有一個稱爲 “空字符串” 的值。這種區分相當有用。

在返回類型的位置使用 void 意味着方法不返回任何數據,同時告訴編譯器不要期望會有一個值。 void 本質上並不是一個數據類型,它只是用於指出沒有數據返回這一事實。

(P038) 

C# 3.0 新增了上下文關鍵字 var 來聲明隱式類型的局部變量。

雖然允許使用 var 取代顯式的數據類型,但在數據類型已知的情況下最好不要使用 var 。

用 var 聲明變量,右側的數據類型應該是非常明顯的;否則應該考慮避免使用 var 聲明。

C# 3.0 添加 var 的目的是支持匿名類型。匿名類型是在方法內部動態聲明的數據類型,而不是通過顯式的類定義來聲明的。

(P039) 

所有類型都可以歸爲值類型或引用類型。它們的區別在於複製方式 : 值類型的數據總是進行值複製,而引用類型的數據總是進行引用複製。

值類型變量直接包含值。換言之,變量引用的位置就是值在內存中實際存儲的位置。因此,將第一個變量的值賦給第二個變量會在新變量的位置創建原始變量的值的一個內存副本。相同值類型的第二個變量不能引用和第一個變量相同的內存位置。所以,更改第一個變量的值不會影響第二個變量的值。

由於值類型需要創建內存副本,因此定義時不要讓它們佔用太多內存 (通常應該小於 16 字節) 。

(P040) 

引用類型的值存儲的是對數據存儲位置的引用,而不是直接存儲數據。要去那個位置才能找到真正的數據。因此,爲了訪問數據, “運行時” 要先從變量中讀取內存位置,再 “跳轉” 到包含數據的內存位置。引用類型指向的內存區域稱堆 (heap) 。

引用類型不像值類型那樣要求創建數據的內存副本,所以複製引用類型的實例比複製大的值類型實例更高效。

將引用類型的變量賦給另一個引用類型的變量,只會複製引用而不需要複製所引用的數據。

事實上,每個引用總是處理器的 “原生大小” 。也就是, 32 位處理器只需複製 32 位引用, 64 位處理器只需複製 64 位引用,以此類推。

顯然,複製對一個大數據塊的引用,比複製整個數據塊快得多。

由於引用類型只複製對數據的引用,所以兩個不同的變量可引用相同的數據。

如果兩個變量引用同一個對象,利用一個變量更改對象的字段,用另一個對象訪問字段時將看到更改結果。無論賦值還是方法調用都會如此。

在決定定義引用類型還是值類型時,一個決定性的因素就是 : 如果對象在邏輯上是固定大小的不可變的值,就考慮定義成值類型;如果邏輯上是可引用的可變的對象,就考慮定義成引用類型。

(P041) 

一般不能將 null 值賦給值類型。這是因爲根據定義,值類型不能包含引用,即使是對 “無 (nothing)” 的引用。

爲了聲明可以存儲 null 的變量,要使用可空修飾符 (?) 。

將 null 賦給值類型,這在數據庫編程中尤其有用。

有可能造成大小變小或者引發異常 (因爲轉換失敗) 的任何轉換都需要執行顯式轉型 (explicit cast) 。相反,不會變小,而且不會引發異常 (無論操作數的類型是什麼) 的任何轉換都屬於隱式轉型 (implicit cast) 。

在 C# 中,可以使用轉型操作符執行轉型。通過在圓括號中指定希望變量轉換成的類型,表明你已認可在發生顯式轉型時可能丟失精度和數據,或者可能造成異常。

(P043) 

C# 還支持 unchecked 塊,它強制不進行溢出檢查,不會爲塊中溢出的賦值引發異常。

即使編譯時打開了 checked 選項,在執行期間, unchecked 關鍵字也會阻止 “運行時” 引發異常。

(P044) 

即使不要求顯式轉換操作符 (因爲允許隱式轉型) ,仍然可以強制添加轉型操作符。

每個數值數據類型都包含一個 Parse() 方法,它允許將字符串轉換成對應的數值類型。

可利用特殊類型 System.Convert 將一種類型轉換成另一種類型。

System.Convert 只支持小的數據類型,而且是不可擴展的。它允許從 bool 、 char 、 sbyte 、 short 、 int 、 long 、 ushort 、 uint 、 ulong 、 float 、 double 、 decimal 、 DateTime 和 string 類型中的任何一種類型轉換到另一種類型。

所有類型都支持 ToString() 方法,可以用它提供一個類型的字符串表示。

(P045) 

對於大多數類型, ToString() 方法只是返回數據類型的名稱,而不是數據的字符串表示。只有在類型顯式實現了 ToString() 的前提下才會返回字符串表示。

從 C# 2.0 (.NET 2.0) 開始,所有基元數值類型都包含靜態 TryParse() 方法。該方法與 Parse() 非常相似,只是轉換失敗的情況下,它不引發異常,而是返回 false 。

Parse() 和 TryParse() 的關鍵區別在於,假如轉換失敗, TryParse() 不會引發異常。

C# 中的數組是基於零的。

數組中每個數據項都使用名爲索引的整數值進行唯一性標識。 C# 數組中的第一個數據項使用索引 0 訪問。

程序員應確保指定的索引值小於數組的大小 (數組中的元素總數) 。

因爲 C# 數組是基於零的,所以數組最後一個元素的索引值要比數組元素的總數小 1 。

(P046) 

初學者可將索引想象成偏移量。第一項距離數組開頭的偏移量是 0 ,第二項的偏移量是 1 ,依次類推。

數組是幾乎每一種編程語言的基本組成部分,因此所有開發人員都要學會它。

在 C# 中,使用方括號聲明數組變量。首先要指定數組元素的類型,後跟一對方括號,再輸入變量名。

在 C# 中,作爲數組聲明一部分的方括號是緊跟在數據類型之後的,而不是出現在變量聲明之後。

(P047) 

使用更多的逗號,可以定義更多的維。數組總維數等於逗號數加 1 。

數組如果在聲明後賦值,則需要使用 new 關鍵字。

(P048) 

自 C# 3.0 起,不必在 new 後面指定數組的數據類型,只要編譯器能根據初始化列表中的數據類型推斷出數組元素的類型。但是,方括號仍然不可缺少。

只要將 new 關鍵字作爲數組賦值的一部分,就可以同時在方括號內指定數組的大小。

在初始化語句中指定的數組的大小必須和大括號中包含的元素數量相匹配。

從 C# 2.0 開始可以使用 default() 表達式判斷數據類型的默認值。 default() 獲取數據類型作爲參數。

由於數組大小不需要作爲變量聲明的一部分,所以可以在運行時指定數組大小。

(P050) 

多維數組的每一維的大小都必須一致。

交錯數組不使用逗號標識新的維。相反,交錯數組定義由數組構成的數組。

注意,交錯數組要求內部的每個數組都創建數組實例。

(P051) 

數組的長度是固定的,不能隨便更改,除非重新創建數組。

Length 成員返回數組中數據項的個數,而不是返回最高的索引值。

爲了將 Length 作爲索引來使用,有必要在它上面減 1 ,以避免越界錯誤。

(P052) 

Length 返回數組中元素的總數。

對於交錯數組, Length 返回的是外部數組的元素數。

(P053) 

使用 System.Array.BinarySearch() 方法前要對數組進行排序。

System.Array.Clear() 方法不刪除數組元素,而且不將長度設爲零。

System.Array.Clear() 方法將數組中的每個元素都設爲其默認值。

要獲取特定維的長度不是使用 Length 屬性,而是使用數組的 GetLength() 實例方法。

(P054) 

可以訪問數組的 Rank 成員來獲取整個數組的維數。

默認情況下,將一個數組變量賦值給另一個數組變量只會複製數組引用,而不是數組中單獨的元素。要創建數組的全新副本,需使用數組的 Clone() 方法。該方法返回數組的一個副本,更改這個新數組中的任何成員都不會影響原始數組的成員。

可以使用字符串的 ToCharArray() 方法,將整個字符串作爲字符數組返回。

(P055) 

用於聲明數組的方括號放在數據類型之後,而不是在變量標識符之後。

(P056) 

如果是在聲明之後再對數組進行賦值,需要使用 new 關鍵字,並可選擇指定數據類型。

不能在變量聲明中指定數組大小。

除非提供數組字面量,否則必須在初始化時指定數組大小。

數組的大小必須與數組字面量中的元素個數相符。

【第03章】 

(P058) 

通常將操作符劃分爲 3 大類 : 一元操作符、二元操作符和三元操作符,它們對應的操作數分別是 1 個、 2 個和 3 個。

使用負操作符 (-) 等價於從零減去操作數。

一元正操作數 (+) 對值幾乎沒有影響。它在 C# 語言中是多餘的,只是出於對稱性的考慮才加進來。

二元操作符要求兩個操作數。 C# 爲二元操作符使用中綴表示法 : 操作符在左、右操作數之間。每個二元表達式的結果要麼賦給一個變量,要麼以某種方式使用 (例如用作爲另一個表達式的操作數) 。

在 C# 中,只有調用、遞增、遞減和對象創建表達式才能作爲獨立的語句使用。

一元 (+) 操作符定義爲獲取 int 、 uint 、 long 、 ulong 、 float 、 double 和 decimal 類型 (及其可空版本) 的操作數。用於其他類型 (如 short ) 時,操作數會根據實際情況轉換爲上述某個類型。

算數操作符的每一邊都有一個操作數,計算結果賦給一個變量。

(P059) 

圓括號可以明確地將一個操作數與它所屬的操作符相關聯。

(P060) 

C# 的大多數操作符都是左結合的,賦值操作符右結合。

有時候,圓括號操作符並不改變表達式的求值結果。不過,使用圓括號來提高代碼的可讀性依然是一良好的編程的習慣。

[規範] 

1. 要使用圓括號增加代碼的易讀性,尤其是在操作符優先級不是讓人一目瞭然的時候;

在 C# 中,操作數總是從左向右求值。

操作符也可用於非數值類型。例如,可以使用加法操作符來拼接兩個或者更多字符串。

(P061) 

當必須進行本地化時,應該有節制地使用加法操作符,最好使用組合格式化。

[規範] 

1. 當必須進行本地化時,要用組合格式化而不是加法操作符來拼接字符串;

雖然 char 類型存儲的是字符而不是數字,但它是整型 (意味着它基於整數) ,可以和其他整型一起參與算數運算。然而,不是基於存儲的字符來解釋 char 類型的值,而是基於它的基礎值。

可以利用 char 類型的這個特點判斷兩個字符相距多遠。

(P062) 

二進制浮點類型實際存儲的是二進制分數而不是十進制分數。所以,一次簡單的賦值就可能引發精度問題。

[規範] 

1. 避免在需要準確的十進制算術運算時使用二進制浮點類型,而是使用 decimal 浮點類型;

比較兩個值是否相等的時候,浮點類型的不準確性可能造成非常嚴重的後果。

(P063) 

[規範] 

1. 避免將二進制浮點類型用於相等性條件式。要麼判斷兩個值之差是否在容差範圍之內,要麼使用 decimal 類型;

(P064) 

(+=) 操作符使左邊的變量遞增右邊的值。

(P065) 

賦值操作符還可以和減法、乘法、除法和取餘操作符結合。

C# 提供了特殊的一元操作符來實現計數器的遞增和遞減。遞增操作符 (++) 每次使一個變量遞增 1 。

可以使用遞減操作符 (--) 使變量遞減 1 。

遞增和遞減操作符在循環中經常用到。

(P066) 

遞增和遞減操作符用於控制特定操作的執行次數。

只要數據類型支持 “下一個值” 和 “上一個值” 的概念,就適合使用遞增和遞減操作符。

遞增或遞減操作符的位置決定了所賦的值是操作數計算之前還是之後的值。

(P067) 

遞增和遞減操作符相對於操作數的位置影響了表達式的結果。前綴操作符的結果是變量 遞增 / 遞減 之後的值,而後綴操作符的結果是變量 遞增 / 遞減 之前的值。

[規範] 

1. 避免混淆遞增和遞減操作符的用法;

(P068) 

常量表達式是 C# 編譯器能在編譯時完成求值的表達式 (而不是在程序運行時才能求值) ,因爲其完全由常量操作數構成。

const 關鍵字的作用就是聲明常量符號。由於常量和 “變量” 相反 —— “常” 意味着 “不可變” —— 以後在代碼中任何修改它的企圖都會造成編譯時錯誤。

[規範] 

1. 不要使用常量表示將來可能改變的任何值;

(P072) 

規範提倡除了單行語句之外都使用代碼塊。

使用大括號,可以將多個語句合併成代碼塊,允許在符合條件時執行多個語句。

(P074) 

事實上,設計規範規定除非是單行語句,否則不要省略大括號。

[規範] 

1. 避免在 if 語句中省略大括號,除非只有一行語句;

總的來說,作用域決定一個名稱引用什麼事物,而聲明空間決定同名的兩個事物是否衝突。

(P075) 

聲明空間中的每個局部變量名稱必須是唯一的。聲明空間覆蓋了包含在最初聲明局部變量的代碼塊中的所有子代碼塊。

(P076) 

相等性操作符使用兩個等號,賦值操作符使用一個等號。

(P077) 

關係和相等性操作符總是生成 bool 值。

邏輯操作符 (logic operator) 獲取布爾操作數並生成布爾結果。可以使用邏輯操作符合並多個布爾表達式來構成更復雜的布爾表達式。

(P078) 

^ 符號是異或 (exclusive OR , XOR) 操作符,若應用於兩個布爾操作數,那麼只有在兩個操作數中僅有一個爲 true 的前提下, XOR 操作符纔會返回 true 。

條件操作符是三元操作符,因爲它需要 3 個操作數,即 condition 、 consequence 和 alternative 。

作爲 C# 中唯一的三元操作符,條件操作符也經常被稱爲 “三元操作符” 。

(P079) 

和 if 語句不同,條件操作符的結果必須賦給某個變量 (或者作爲參數傳遞) 。它不能單獨作爲一個語句使用。

[規範] 

1. 考慮使用 if / else 語句,而不是使用過於複雜的條件表達式;

空接合操作符 (null coalescing operator) ?? 能簡單地表示 “如果這個值爲空,就使用另一個值” 。

?? 操作符支持短路求值。

(P080) 

空接合操作符能完美地 “鏈接” 。

空結合操作符是 C# 2.0 和可空值類型一起引入的,它的操作數既可以是可空值類型,也可以是引用類型。

C# 6.0 引入了一種更爲簡化的 null 條件操作符 (null-condition operator) ?. 。

(P083) 

兩個移位操作符是 >> 和 << ,分別稱爲右移位和左移位操作符。除此之外,還有複合移位和賦值操作符 <<= 和 >>= 。

AND 和 OR 操作符的按位版本不進行 “短路求值” 。

(P086) 

按位取反操作符 (~) 是對操作數的每一位取反,操作數可以是 int 、 uint 、 long 和 ulong 類型。

(P087) 

斐波那契數 (Fibonacci number) 是斐波那契數列 (Fibonacci series) 的成員,這個數列中的所有數都是數列中前兩個數之和。數列最開頭兩個數是 1 和 1 。

for 主要用於重複次數已知的循環,比如從 0 ~ n 的計數。 do / while 類似於 while 循環,區別在於它至少會循環一次。

do / while 循環與 while 循環非常相似,只是它最適合需要循環 1 ~ n 次的情況,而且 n 在循環開始前無法確定。 do / while 循環的一個典型應用就是反覆提醒用戶輸入。

(P088) 

由於遞增操作在循環語法中有一席之地,所以遞增和遞減操作符經常作爲 for 循環的一部分使用。

(P089) 

[規範] 

1. 如果發現正在寫的 for 循環包含了複雜條件和多個循環變量,要考慮重構方法,以使控制流更容易理解;

for 循環只不過是一種比寫 while 循環更方便的方法。 for 循環能改寫成 while 循環。

(P090) 

[規範] 

1. 假如事先知道循環次數,而且循環中需要用到控制循環次數的 “計數器” ,那麼要使用 for 循環;

2. 假如事先不知道循環次數,而且不需要計數器,那麼要使用 while 循環;

foreach 循環的特點是每一項只被遍歷一次 : 不會像其他循環那樣出現計數錯誤,也不可能越過集合邊界。

(P092) 

將一個值和許多不同的常量值比較時, switch 語句比 if 語句更容易理解。

switch 的 “主導類型” (governing type) 允許的主導數據類型包括 bool 、 sbyte 、 byte 、 short 、 ushort 、 int 、 uint 、 long 、 ulong 、 char 、 任何枚舉 (enum) 類型、上述所有值類型的可空類型以及 string 。

[規範] 

1. 不要使用 continue 作爲跳轉語句退出 switch 小節。如果 switch 語句是在一個循環中使用的,這樣寫是合法的。但是,這樣做很容易對之後的 switch 小節中出現的 break 語句的意義感到迷惑;

(P093) 

switch 語句至少要有一個 switch 小節。

雖然在之前的規範中提到,在一般情況下應該避免省略大括號,但有一個例外,就是要省略 case 和 break 語句的大括號,因爲它們的作用是指示一個塊的開始與結束。

(P094) 

switch 小節可以以任意順序出現, default 小節不一定非要出現在 switch 語句的最後。事實上, default 的 switch 小節完全可以省略;它是可選的。

C# 要求每個 switch 小節 (包括最後一個小節) 的結束點 “不可到達” 。這意味着 switch 小節通常以 break 、 return 、 throw 或 goto 結尾。

如果希望 switch 小節執行另一個 switch 小節中的語句,可以顯式使用 goto 語句來實現。

C# 使用 break 語句退出循環或者 switch 語句。任何時候遇到 break 語句,控制都會立即離開循環或 switch 。

(P097) 

一般都可以使用 if 語句代替 continue 語句,這樣做還能增強可讀性。

continue 語句的問題在於,它在一次循環中提供了多個出口,從而影響了可讀性。

C# 確實支持 goto ,而且只能利用 goto 在 switch 語句中實現貫穿。

(P098) 

C# 禁止通過 goto 跳轉到代碼塊內部。只能用 goto 在代碼塊內部跳轉,或者跳到一個封閉的代碼塊。

[規範] 

1. 避免使用 goto ;

控制流語句中的條件表達式在運行時求值。相反,C# 預處理器在編譯時調用。

每個預處理指令都以 # 開頭,而且必須在一行中寫完。換行符 (而不是分號) 標誌着預處理指令的結束。

(P102) 

C# 允許使用 #region 指令聲明代碼區域。 #region 和 #endregion 必須成對使用,兩個指令都可以選擇在指令後面跟隨一個描述性的字符串。除此之外,還可以將一個區域嵌套到另一個區域中。

【第04章】 

(P106) 

[規範] 

1. 要爲方法名使用動詞或動詞短語;

方法總是和類型 —— 通常是類 —— 關聯。類型將相關的方法分爲一組。

方法通過返回值將數據返回給調用者。

(P107) 

方法調用由方法名稱和實參列表和返回值構成。

命名空間是一種分類機制,用於組合功能相關的所有類型。

命名空間是分級的,級數可以任意,但是很少見到超過 6 級的命名空間。

命名空間主要用於按照功能領域組織類型,以便更容易地查找和理解它們。

[規範] 

1. 要爲命名空間使用 Pascal 大小寫風格;

2. 考慮將源代碼的文件目錄結構組織成與命名空間的層級結構相匹配的形式;

類型本質上是對方法及其相關數據進行組合的一種方式。

(P109) 

在方法名稱之後是圓括號中的實參列表,每個實參以逗號分隔,對應於聲明方法時指定的形參。

方法可接收任意數量的形參,每個形參都具有特定的數據類型。調用者爲形參提供的值稱爲實參;每個實參都要和一個形參對應。

可以將方法的返回值作爲另一個方法的實參使用。

[注意] 

1. 通常,開發者應側重於可讀性,而不是在寫出更短的代碼方面耗費心機。爲了使代碼一目瞭然,進而在長時間裏更容易維護,可讀性是關鍵;

(P111) 

C# 的每個方法都必須在某個類型中。

將一組相關語句轉移到一個方法中,而不是把它們留在一個較大的方法中,這是重構 (refactoring) 的一種形式。

與簡單地爲一個代碼塊加上註釋相比,重構的效果更好,因爲只需看方法名就可清楚地知道這個方法要做的事情。

(P112) 

1. 要爲參數名使用駝峯大小寫風格;

雖然方法可以指定多個參數,但返回類型只能有一個。

如果方法有返回類型,它的主體必須有 “不可到達的結束點” 。

換言之,一個具有返回類型的方法不允許在不返回任何值的情況下將控制貫穿到方法的末尾。

爲了保證這一點,最簡單的辦法就是將 return 語句作爲方法的最後一個語句。

(P113) 

注意, return 語句將控制轉移出 switch ,所以,在以 return 語句作爲方法最後一個語句的方法中,不需要用 break 語句防止非法 “貫穿” switch 小節。

雖然 C# 允許一個方法有多個返回語句,但爲了增強代碼的可讀性,以及使代碼更容易維護,應該儘可能地確定單一的退出位置,而不是在方法的多個代碼中散佈多個 return 語句。

爲了支持不帶方法主體的最簡單的方法聲明, C# 6.0 引入了表達式主體方法 (expression bodied method) ,使用表達式而不是一個完整的方法主體來聲明一個方法。

與使用大括號包含方法主體不同,表達式主體方法使用 Lambda 操作符 (=>) ,結果數據類型必須與方法的返回類型匹配。也就是說,儘管在表達式主體方法實現中並沒有顯式的返回語句,表達式的返回類型仍然必須與方法聲明的返回類型匹配。

表達式主體方法是完整方法主體聲明的語法簡化表示。因此,表達式主體方法的使用應限於最簡化的方法實現,通常用於單行可表示的方法。

和 C++ 不同, C# 類從來不將實現與聲明分開。 C# 不區分頭文件 (.h) 和實現文件 (.cpp) 。相反,聲明和實現總是出現在同一個文件中。

(P114) 

重名的兩個或更多類型只要在不同命名空間中,就沒有歧義。

using 指令不會導入任何嵌套命名空間 (nested namespace) 中的類型。嵌套命名空間 (由命名空間中的句點符號來標識) 必須顯式導入。

與 Java 相比, C# 不允許在 using 指令中使用通配符,每個命名空間都必須顯式地導入。

(P115) 

不僅可以在文件頂部使用 using 指令,還可以在命名空間聲明的頂部包含它們。

在文件頂部放置 using 指令和在命名空間聲明的頂部位置 using 指令的區別在於,後者的 using 指令只在聲明的命名空間內有效。

(P116) 

using static 指令允許省略規定類型的任何成員之前的命名空間和類型名稱。

別名的兩個最常見的用途是消除兩個同名類型的歧義和縮寫長名稱。

(P120) 

調用者中的變量名與被調用方法中的參數名相匹配。這種匹配純粹是爲了增強可讀性,名稱是否匹配與方法調用的行爲無關。被調用方法的參數和發出調用的方法的局部變量在不同聲明空間中,相互之間沒有任何關係。

(P123) 

out 參數在功能上和 ref 參數完全一致,唯一的區別是, C# 語言對別名變量的讀寫有不同的規定。

開發人員可以通過聲明一個或多個 out 參數來克服方法只有一個返回類型的限制。

[注意] 

1. 每個正常返回的代碼路徑都必須對所有 out 參數進行賦值;

(P124) 

參數數組不一定是方法的唯一參數,但必須是方法聲明中的最後一個參數。由於只有最後一個參數纔可能是參數數組,所以方法最多只能有一個參數數組。

(P125) 

[規範] 

1. 當一個方法需要處理任意數量 (包括零個) 額外實參時,要使用參數數組;

(P127) 

[注意] 

C# 依據方法名、參數數據類型或者參數數量的不同來定義方法的唯一性。

(P129) 

實現重載方法時經常採用的一種模式,它的基本思路是 : 開發者只需在一個方法中實現核心邏輯,其他所有重載版本都調用那個方法。如果核心實現發生了改變,那麼只需要在一個位置修改,而不必在每個實現中都進行修改。

[注意] 

1. 在一個方法中實現核心功能,所有其他重載的方法都調用這個方法。這意味着你可以只修改核心方法的實現,其他重載的方法就會自動地享受到修改;

從 C# 4.0 開始,語言的設計者增添了對可選參數 (optional parameters) 的支持。聲明方法時將常量值賦給參數,以後調用方法時就不必每個參數都指定。

(P130) 

可選參數一定放在所有必須的參數 (無默認值的參數) 後面。另外,默認值必須是常量,或者說必須是能在編譯時確定的值,這一點極大限制了 “可選參數” 的應用。

(P131) 

[規範] 

1. 要儘量爲所有參數提供好的默認值;

2. 要提供簡單的方法重載,其必需的參數的數量要少;

3. 考慮從最簡單到最複雜來組織重載;

C# 4.0 新增的另一個方法調用功能是命名參數 (named arguments) 。利用命名參數,調用者可顯式地爲一個參數賦值,而不是像以前那樣只能依據參數順序來決定哪個值賦給哪個參數。

添加了命名參數後,參數名就成爲方法接口的一部分。更改名稱會導致使用命名參數的代碼無法編譯。

[規範] 

1. 要將參數名視爲 API 的一部分。如果 API 之間的版本兼容性很重要,就要避免更改參數名;

(P135) 

try 關鍵字告訴編譯器 : 開發者認爲塊中的代碼有可能引發異常;如果真的引發了異常,那麼某個 catch 塊要嘗試處理這個異常。

try 塊之後必須緊跟着一個或多個 catch 塊 (或 / 和一個 finally 塊) 。 catch 塊可選擇指定異常的數據類型。只要數據類型與異常類型匹配,對應的 catch 塊就會執行。但是,假如一直找不到合適的 catch 塊,引發的異常就會變成一個未處理的異常,就好像沒有進行異常處理一樣。

(P136) 

處理異常的順序非常重要。 catch 塊必須按照從最具體到最不具體排列。

無論控制是正常地離開 try 塊還是由於 try 塊中的代碼引發異常而離開的,只要控制離開 try 塊, finally 塊就會執行。

finally 塊的作用是提供一個最終位置,在其中放入無論是否發生異常都要執行的代碼。

finally 塊最適合用來執行資源清理。

事實上,完全可以只寫一個 try 塊和一個 finally 塊,而不寫任何 catch 塊。

無論 try 塊是否引發異常,甚至無論是否寫了一個 catch 塊來處理異常, finally 塊都會執行。

(P137) 

[規範] 

1. 避免從 finally 塊顯式地引發異常 (因方法調用而隱式地引發的異常可以被接受) ;

2. 要優先使用 try / finally 而不是 try / catch 塊來實現資源清理代碼;

3. 要在拋出的異常中描述異常爲什麼發生。如有可能,還要說明如何防範;

(P138) 

可以指定一個不獲取任何參數的 catch 塊。

(P139) 

沒有指定數據類型的 catch 塊稱爲常規 catch 塊 (generic catch block) ,它等價於獲取 object 數據類型的 catch 塊。由於所有類最終都從 object 派生,所以沒有數據類型的 catch 塊必須放到最後。

常規 catch 塊很少使用,因爲沒有辦法捕獲有關異常的任何信息。

[規範] 

1. 避免使用常規 catch 塊,而應該使用捕獲 System.Exception 的 catch 塊來代替;

2. 避免捕獲無法獲知其正確行動的異常。對這種異常不進行處理比處理地不正確要好;

3. 避免在重新引發前捕獲和記錄異常。要允許異常逃脫,直至它被正確處理;

(P140) 

有時 catch 塊能捕獲到異常,但不能正確或者完整地處理它。在這種情況下,可以讓這個 catch 塊重新引發異常,具體的辦法是使用一個單獨的 throw 語句,不要在它後面指定任何異常。

(P141) 

[規範] 

1. 要在捕獲並重新引發異常時使用空的 throw 語句,以便保持調用棧;

2. 要通過引發異常而不是返回錯誤碼來報告執行失敗;

3. 不要讓公共成員將異常作爲返回值或者 out 參數。要通過異常來指明錯誤;不要通過它們作爲返回值來指明錯誤;

異常是專門爲了跟蹤例外的、事先沒有預料到的、而且可能造成嚴重後果的情況而設計的。爲預料之中的情況使用異常,會造成代碼難以閱讀、理解和維護。

[規範] 

1. 不要用異常來處理正常的、預期的情況;用異常處理異常的、非預期的情況;

(P142) 

從 .NET Framework 4 開始,枚舉類型也添加了 TryParse() 方法;

【第05章】 

(P144) 

面向對象編程的關鍵優勢之一是不需要完全從頭創建新的程序。而是可以將現有的一系列對象組裝到一起,並用新的功能擴展類,或者添加更多的類。

爲了支持封裝, C# 必須支持類、屬性、訪問修飾符以及方法。

開發人員一旦熟悉了面向對象編程,除非寫一些極爲簡單程序,否則很難回到結構化編程。

(P146) 

雖然並非必須,但一般應該將每個類都放到它自己的文件中,用類名對文件進行命名。這樣可以更容易地尋找定義了一個特定類的代碼。

[規範] 

1. 不要在一個源代碼文件中放置多個類;

2. 要用所含公共類型的名稱來命名源代碼文件;

定義好新類後,就可以像使用 .NET Framework 內置的類那樣使用它了。

換言之,可以聲明那個類型的變量,或者定義方法來接收新類型的參數。

類是模板,定義了對象在實例化的時候看起來像什麼樣子。所以,對象是類的實例。

從類創建對象的過程稱爲實例化 (instantiation) ,因爲對象是類的實例 (instance) 。

C# 使用 new 關鍵字實例化對象。

(P147) 

面向對象編程將方法和數據裝入對象。這提供了所有類成員 (類的數據和方法) 的一個分組,使它們不再需要單獨處理。

程序員應將 new 的作用理解成實例化對象而不是分配內存。在堆和棧上分配對象都支持 new 操作符,這進一步強調了 new 不是關於內存分配的,也不是關於是否有必要進行回收的。

和 C++ 不同, C# 不支持隱式確定性資源清理 (在編譯時確定的位置進行隱式對象析構) 。幸好, C# 通過 using 語句支持顯式確定性資源清理,通過終結器支持隱式非確定性資源清理。

(P148) 

面向對象設計的一個核心部分是對數據進行分組,以提供一個特定的結構。

在面向對象術語中,在類中存儲數據的變量稱爲成員變量。

實例字段是在類的級別上聲明的變量,用於存儲與對象關聯的數據。因此,關聯 (association) 是字段類型和包容類型之間的聯繫。

注意,字段不包含 static 修飾符,這意味着它是實例字段。只能從其包容類的實例 (對象) 中訪問實例字段,無法直接從類中訪問 (換言之,不創建實例就不能訪問) 。

(P150) 

靜態方法不能直接訪問類的實例字段,必須獲取類的實例才能調用實例成員 —— 無論該實例成員是方法還是字段。

在類的實例成員內部,可以獲取對這個類的引用。在 C# 中,爲了顯式指出當前訪問的字段或方法是包容類的實例成員,可以使用關鍵字 this 。調用任何實例成員時 this 都是隱式的,它返回對象本身的實例。

(P151) 

雖然可爲所有本地類成員引用添加 this 前綴,但規範的原則是,如果不會帶來更多的價值就不要在代碼中“添亂”。所以,只在必要時才使用 this 關鍵字。

(P152) 

C# 關鍵字 this 完全等價於 Visual Basic 關鍵字 Me 。

假如存在與字段同名的局部變量或參數,省略 this 將訪問局部變量或參數,而不是字段。所以,在這種情況下, this 是必須的。

還可使用 this 關鍵字顯式訪問類的方法。

有時需要使用 this 傳遞對當前正在執行的對象的引用。

(P156) 

在類的外部不可見的成員稱爲私有成員。

(P157) 

如果不爲類成員添加訪問修飾符,那麼默認使用的是 private 。也就是說,成員默認爲私有成員。公共成員必須顯式指定。

(P161) 

在 C# 6.0 之前的版本中,屬性初始化只能通過方法進行。但到了 C# 6.0 ,就可以使用類似字段初始化的語法,在聲明時自動初始化實現的屬性。

[規範] 

1. 要使用屬性簡化對簡單數據 (進行少量計算) 的訪問;

2. 避免從屬性的取值方法中引發異常;

3. 要在屬性引發異常時保留原始屬性值;

4. 如果沒有額外的實現邏輯,要優先使用自動實現的屬性,而不是帶有簡單支持字段的屬性;

[規範] 

1. 考慮爲支持字段和屬性使用相同的大小寫風格,爲支持字段附加 “_” 前綴。但不要使用雙下劃線,因爲以雙下劃線開頭的標識符是爲 C# 編譯器保留的;

2. 要使用名詞、名詞短語或形容詞來命名屬性;

3. 考慮讓屬性和它的類型同名;

4. 避免用駝峯大小寫風格命名字段;

5. 如果有用的話,要爲布爾屬性附加 “Is” “Can” 或 “Has” 前綴;

6. 不要聲明 public 或 protected 的實例字段 (而是通過屬性來公開字段) ;

7. 要用 Pascal 大小寫風格命名屬性;

8. 要優先使用自動實現的屬性而不是字段;

9. 如果沒有額外的實現邏輯,要優先使用自動實現的屬性,而不是自己編寫完整版本;

(P163) 

[規範] 

1. 避免從屬性外部 (即使是在包容屬性的類中) 訪問屬性的支持字段;

2. 調用 ArgumentException() 或 ArgumentNullException() 構造器時,要爲 paramName 參數傳遞 “value” (“value” 是屬性賦值方法隱含的參數名) ;

(P165) 

[規範] 

1. 如果不想調用者更改屬性的值,要創建只讀屬性;

2. 在 C# 6.0 (或以後的版本) 中,如果不想調用者更改屬性的值,要創建只讀的自動實現的屬性,而不是帶有後備字段的只讀屬性;

(P167) 

[規範] 

1. 要爲所有屬性的取值方法和賦值方法的實現應用適當的可訪問性修飾符;

2. 不要提供只寫屬性,也不要讓屬性的賦值方法的可訪問性比取值方法更寬鬆;

(P170) 

構造器是 “運行時” 用來初始化對象實例的方法。

(P171) 

假如類沒有顯式定義的構造器, C# 編譯器會在編譯時自動添加一個。該構造器不獲取參數,稱爲默認構造器。

一旦爲類顯式添加了構造器, C# 編譯器就不再自動提供默認構造器。

C# 3.0 新增了對象初始化器,用於初始化對象中所有可以訪問的字段和屬性。

總之,構造器退出時,所有屬性都應該初始化成合理的默認值。

(P172) 

[規範] 

1. 要爲所有屬性提供有意義的默認值,確保默認值不會造成安全漏洞或造成代碼效率大幅下降。對於自動實現的屬性,要通過構造器設置默認值;

2. 要允許以任意順序設置屬性,即使這會造成對象臨時處於無效狀態;

(P173) 

[規範] 

1. 如果使用構造器參數來設置屬性,構造器參數 (駝峯大小寫風格) 要使用和屬性 (Pascal 大小寫風格) 相同的名稱,區別僅僅是大小寫風格;

2. 要爲構造器提供可選參數,或者提供便利的重載構造器,用有意義的默認值初始化屬性;

3. 要允許以任意順序設置屬性,即使這會造成對象臨時處於無效狀態;

(P177) 

在 C# 中,與全局字段或函數等價的是靜態字段或方法。

(P178) 

實例字段,也就是非靜態字段,可以在聲明的同時進行初始化。靜態字段也可以。

和實例字段不同,未初始化的靜態字段將獲得默認值 (0 、 null 、 false 等) ,即 default(T) 的結果,其中 T 是類型名。所以,即使沒有顯式賦值的靜態字段也能被訪問。

靜態字段不從屬於實例,而是從屬於類。

(P180) 

由於靜態方法不通過實例引用,所以 this 關鍵字在靜態方法中無效。

靜態構造器不顯式調用,而是 “運行時” 在首次訪問類時自動調用靜態構造器。

由於靜態構造器不能顯式調用,所以不允許任何參數。

(P181) 

使用靜態構造器將類中的靜態數據初始化成特定的值,尤其是無法通過聲明時的一次簡單賦值來獲得初始值的時候。

在靜態構造器中進行的賦值,將優先於聲明時的賦值,這和實例字段的情況一樣。注意,沒有 “靜態終結器” 的說法。

[規範] 

1. 考慮以內聯方式初始化靜態字段,不要使用靜態構造器或者在聲明時賦值;

還可以將屬性聲明爲 static 。

(P182) 

使用靜態屬性幾乎肯定要比使用公共靜態字段好,因爲公共靜態字段在任何地方都能調用,而靜態屬性則至少提供了一定程度的封裝。

(P183) 

在聲明類時使用 static 關鍵字,具有兩個方面的意義。首先,它防止程序員寫代碼來實例化靜態類;其次,它防止在類的內部聲明任何實例字段或方法。

靜態類的另一個特點是 C# 編譯器自動在 CIL 代碼中把它標記爲 abstract 和 sealed 。這會將類指定爲不可擴展;換言之,不能從它派生出其他類。

(P184) 

如果擴展方法的簽名已經和被擴展類型中的簽名匹配,擴展方法永遠不會得到調用,除非是作爲一個普通的靜態方法。

(P185) 

擴展方法要慎用。

[規範] 

1. 避免輕率地定義擴展方法,尤其是要避免爲自己沒有所有權的類型定義擴展方法;

和 const 值一樣, const 字段 (稱爲常量字段) 包含在編譯時確定的值,它不可以在運行時改變。

常量字段自動成爲靜態字段,因爲不需要爲每個對象實例都生成新的字段實例。但是,將常量字段顯式聲明爲 static 會造成編譯錯誤。

[規範] 

1. 要爲永遠不變的值使用常量字段;

2. 不要爲將來會發生變化的值使用常量字段;

(P186) 

和 const 不同, readonly 修飾符只能用於字段 (不能用於局部變量) 。

它指出字段值只能從構造器中更改,或者在聲明時通過初始化器修改。

和 const 字段不一樣,每個實例的 readonly 字段都可以不同。

由於 readonly 字段必須從構造器中設置,所以編譯器要求這種字段能從其屬性外部訪問。

(P187) 

將 readonly 應用於數組不會凍結數組的內容,而是凍結數組實例 (也凍結了數組中的元素數量) ,這是因爲無法將值重新賦給新的實例。但數組中的元素仍然是可寫的。

[規範] 

1. 在 C# 6.0 (及之後版本) 中,要優先使用只讀的自動實現的屬性,而不是定義只讀字段;

2. 在 C# 6.0 之前的版本中,要爲預定義對象實例使用 public static readonly 字段;

3. 如果 API 版本的兼容性有要求,要避免將 C# 6.0 之前版本中的公共的 readonly 字段修改爲 C# 6.0 (及之後版本) 中的只讀的自動實現的屬性;

在類中除了定義方法和字段,還可以定義另一個類。這稱爲嵌套類 (nested class) 。假如一個類在它的包容類外部沒有多大意義,就適合把它設計成嵌套類。

(P188) 

嵌套類的獨特之處是可以爲類自身指定 private 訪問修飾符。

(P189) 

嵌套類中的 this 成員代表嵌套類而不是包容類的實例。嵌套類要想訪問包容類的實例,一個辦法是顯式傳遞包容類的實例,比如通過構造器或者方法參數。

嵌套類的另一個有趣的特點是它能訪問包容類的任何成員,其中包括私有成員。反之則不然,包容類不能訪問嵌套類的私有成員。

嵌套類用得很少。要從包容類型外部引用,就不能定義成嵌套類。另外要警惕 public 嵌套類,它們意味着不良的編碼風格,可能造成混淆和難以閱讀。

[規範] 

1. 避免聲明公共嵌套類型。唯一的例外是在這種類型的聲明沒有多大意義的時候,或者這種類型的聲明是與一種高級的自定義場景有關;

分部類主要用於將一個類的定義劃分到多個文件中。

分部類對代碼生成或修改工具來說意義重大。

C# 2.0 (和更高版本) 使用 class 前的上下文關鍵字 partial 來聲明分部類。

除了用於代碼生成器,分部類另一個常見的應用是將每個嵌套類都放到它們自己的文件中。這是爲了與編程規範 “將每個類定義都放到它自己的文件中” 保持一致。

(P190) 

分部類不允許對編譯好的類 (或其他程序集中的類) 進行擴展。分部類只是在同一個程序集中將一個類的實現拆分到多個文件中。

(P192) 

分部方法必須返回 void 。

【第06章】 

(P193) 

派生類型總是隱式地屬於基類型。

[注意] 

1. 代碼中的繼承用於定義 “屬於” 關係,派生類是對基類的特化;

(P195) 

每個派生類都擁有由其所有基類公開的全部成員。

[注意] 

1. 通過繼承,基類的每個成員都會出現在派生類的鏈條中;

所有類都隱式地派生於 object ,不管是否這樣指定。

[注意] 

1. 除非明確指定了基類,否則所有類都默認從 object 派生;

(P196) 

從基類型轉換爲派生類型,要求執行顯式轉型,而顯式轉型在運行時可能會失敗。

[注意] 

1. 派生對象可隱式轉型爲它的基類。相反,基類向派生類的轉換要求顯式的轉型操作符,因爲轉換可能會失敗。雖然編譯器允許可能有效的顯式轉型,但 “運行時” 會堅持進行檢查,如果在執行時出現非法的轉型,會引發異常;

(P197) 

派生類繼承了除構造器和析構器之外的所有基類成員。但是,繼承並不意味着一定能訪問。

(P198) 

根據封裝原則,派生類不能訪問基類的 private 成員。

[注意] 

1. 派生類不能訪問基類的私有成員;

(P199) 

[注意] 

1. 基類中的受保護成員只能從基類以及其派生鏈中的其他類訪問;

基本規則是,要從派生類中訪問受保護成員,必須在編譯時確定是從派生類 (或者它的某個子類) 的實例中訪問受保護成員。

由於每個派生類都可作爲它的任何基類的實例使用,所以對一個類型進行擴展的方法也可擴展它的任何派生類型。

如果擴展基類,所有擴展方法在派生類中也可以使用。

很少爲基類寫擴展方法。擴展方法的一個基本原則是,假如手上有基類的代碼,直接修改基類會更好。

(P201) 

密封類要求使用 sealed 修飾符,這樣做的結果是不能從它們派生出其他類。 string 類型就用 sealed 修飾符禁止了派生。

基類除構造器和析構器之外的所有成員都會在派生類中繼承。

(P202) 

C# 支持重寫實例方法和屬性,但不支持重寫字段或者任何靜態成員。

在基類中,必須將允許重寫的每個成員標記爲 virtual 。

默認情況下, Java 中的方法都是虛方法。假如希望方法具有非虛的行爲,就必須顯式密封它。相反, C# 的方法默認爲非虛方法。

C# 要求顯式使用 override 關鍵字來重寫方法。換句話說, virtual 標誌着方法或屬性可在派生類中被替換 (重寫) 。

(P203) 

爲了重寫方法,基類和派生類成員必須匹配,而且要有對應的 virtual 和 override 關鍵字。此外, override 關鍵字意味着派生類的實現會替換基類的實現。

對成員進行重載,會造成 “運行時” 調用最深的或者說派生得最遠的實現。

“運行時” 遇到虛方法時,它會調用虛成員派生得最遠的、重寫的實現。

創建類時必須謹慎選擇是否允許重寫方法,因爲控制不了派生的實現。虛方法不應包含關鍵代碼,因爲如果派生類重寫了它,那些代碼就永遠得不到調用。

(P204) 

虛方法只提供默認實現,這種實現可由派生類完全重寫。然而,由於繼承設計的複雜性,所以請事先想好是否需要虛方法。

(P205) 

最後要說的是,只有實例成員纔可以是 virtual 的。 CLR 根據具體化的類型 (在實例化期間指定) 來判斷將虛方法調用調度到哪裏。所以 static virtual 方法毫無意義,編譯器也不允許。

(P208) 

就 CIL 來說, new 修飾符對編譯器生成的代碼沒有任何影響。然而,一個 “新” 方法會生成方法的 newslot 元數據特性。從 C# 的角度看,它唯一的作用就是移除編譯器警告。

一般很少將整個類標記爲密封,除非是遇到迫切需要這種限制的情況。

(P209) 

爲了調用基類的實現,要使用 base 關鍵字。它的語法幾乎和 this 一樣,包括支持將 base 作爲構造器的一部分使用。

用 override 修飾的任何成員都自動成爲虛成員,其他子類能進一步 “特化” 它的實現。

[注意] 

1. 用 override 修飾的任何方法都自動成爲虛方法。只能對基類的虛方法進行重寫,所以重寫獲得的方法也是虛方法;

實例化一個派生類時, “運行時” 首先調用基類的構造器,以避免繞過對基類的初始化。

(P210) 

抽象類是僅供派生的類。無法實例化抽象類,只能實例化從它派生的類。不抽象、可直接實例化的類稱爲具體類。

抽象類代表抽象的實體。其抽象成員定義了從抽象實體派生的對象應包含什麼,但這種成員不包含實現。通常,抽象類中的大多數功能都沒有實現。一個類要從抽象類成功地派生,必須爲抽象基類中的抽象方法提供具體的實現。

(P211) 

不可實例化只是抽象類的一個較次要的特徵。其主要特徵是它包含抽象成員。抽象成員是沒有實現的方法或屬性,其作用是強制所有派生類提供實現。

(P212) 

由於抽象成員應當被重寫,所以自動成爲虛成員 (但不能用 virtual 關鍵字顯式地這樣聲明) 。除此之外,抽象成員不能聲明爲私有,否則派生類看不見它們。

[注意] 

1. 抽象成員必須被重寫,因此會自動成爲虛成員,但不能用 virtual 關鍵字顯式聲明;

(P213) 

抽象成員是實現多態性的一個手段。基類指定方法的簽名,而派生類提供具體的實現。

(P214) 

所有對象最終都從 object 派生 (不管是直接派生還是通過繼承鏈派生) 。

(P215) 

即使類定義沒有顯式地指明自己從 object 派生,也肯定是從 object 派生的。

C# 提供了 is 操作符來判斷基礎類型。

is 操作符的優點在於,它允許驗證一個數據項是否屬於特定類型。 as 操作符則更進一步,它會像一次轉型所做的那樣,嘗試將對象轉換爲特定數據類型。但和轉型不同的是,如果對象不能轉換, as 操作符會返回 null 。這一點相當重要,因爲它避免了可能因爲轉型而造成的異常。

(P216) 

使用 as 操作符可避免用額外的 try-catch 代碼處理轉換無效的情況,因爲 as 操作符提供了嘗試執行轉型但轉型失敗後不引發異常的一個辦法。

is 操作符相較於 as 操作符的一個優點是後者不能成功判斷基礎類型。 as 操作符能在繼承鏈中向上或向下隱式轉型,也支持提供了轉型操作符的類型。 as 不能判斷基礎類型而 is 能。

【第07章】 

(P218) 

接口是非常有用的,因爲和抽象類不同,接口能將實現細節和提供的服務完全隔離開。

(P219) 

接口只允許共享成員簽名,不允許共享實現。

接口訂立了契約,類必須履行這個契約,才能同實現該接口的其他類進行交互。

接口的關鍵特點是既不包含實現,也不包含數據。注意其中的方法聲明,它用一個分號取代了大括號。字段 (數據) 不能在接口聲明中出現。如果接口要求派生類包含特定數據,會聲明屬性而不是字段。由於沒有屬性的任何實現可以作爲接口聲明的一部分,所以屬性不引用支持字段。

接口聲明的成員描述了在實現該接口的類型中必須能夠訪問的成員。而所有非公共成員的目的是阻止其他代碼訪問成員。所以, C# 不允許爲接口成員使用訪問修飾符。所有成員都自動定義爲公共成員。

(P220) 

[規範] 

1. 接口名稱要使用 Pascal 大小寫風格,並以 “I” 作爲前綴;

(P223) 

聲明類以實現接口,類似於從基類派生 —— 要實現的接口和基類名稱以逗號分隔,基類 (如果有的話) 在前,接口順序任意。類可實現多個接口,但只能從一個基類直接派生。

(P224) 

一旦某個類聲明自己要實現接口,接口的所有成員都必須實現。抽象類允許提供接口成員的抽象實現。

接口的一個重要特徵是永遠不能實例化。

接口沒有構造器或終結器。

(P225) 

只有實例化實現接口的類型,才能使用接口實例。

顯式實現的方法只能通過接口本身調用。

聲明顯式接口成員實現要在成員名之前附加接口名前綴。

(P227) 

[規範] 

1. 避免顯式實現接口成員,除非有很好的理由。但如果不確定成員的用途,就先選擇顯式實現;

與派生類和基類的關係相似,從實現類型向它的已實現接口的轉換是隱式轉換,不需要轉型操作符。實現類型的實例總是包含接口的全部成員,所以對象總是能成功轉換爲接口類型。

從接口轉換爲它的某個實現類型,需要執行一次顯式的強制轉型。

一個接口可以從另一個接口派生,派生的接口將繼承 “基接口” 的所有成員。

(P230) 

擴展方法的一個重要特點是除了能作用於類,還能作用於接口。

(P231) 

C# 不僅允許爲特定類型的實例添加擴展方法,還允許爲那些對象的集合添加擴展方法。

(P232) 

[規範] 

1. 考慮通過定義接口來獲得和多繼承相似的效果;

(P233) 

接口在負責實現的類和使用接口的類之間訂立了契約,改動接口相當於改動契約,會使基於接口編寫的代碼失效。

實現接口的任何類都必須完整地實現,必須提供針對所有成員的實現。

[規範] 

1. 不要爲已交付的接口添加成員;

(P234) 

接口引入了另一個類別的數據類型 (是少數不對終極基類 System.Object 進行擴展的類型之一) 。但和類不同的是,接口永遠不能實例化。只能通過對實現接口的一個對象的引用來訪問接口實例。不能用 new 操作符創建接口實例,所以接口不能包含任何構造器或終結器。此外,接口中不允許靜態成員。

[規範] 

1. 一般要優先選擇類而不是接口。用抽象類將契約 (類型做什麼) 與實現細節 (類型怎麼做) 分離開;

2. 想在已從其他類型派生的類型上支持接口所定義的功能時,就考慮定義接口;

(P235) 

接口應該用於表示類型能提供的功能,而非陳述關於某個類型的事實。

[規範] 

1. 避免使用無成員的標記接口,而是使用特性;

【第08章】 

(P237) 

[規範] 

1. 不要創建消耗內存大於 16 字節的值類型;

值類型的值一般只是短時間存在。很多情況下,這樣的值只是作爲表達式的一部分,或用於激活方法。在這些情況下,值類型的變量和臨時值經常是存儲在稱爲棧的臨時存儲池中。

臨時池清理起來的代價低於需要進行垃圾回收的堆。不過,值類型要比引用類型更頻繁地複製,這種複製操作會增加性能的開銷。

引用類型的變量關聯了兩個存儲位置 : 直接和變量關聯的存儲位置,以及由變量中存儲的值引用的存儲位置。

(P238) 

複製引用類型的值時,複製的只是引用,這個引用非常小。 (一個引用的大小就是處理器的 “bit size” ; 32 位機器是 4 字節的引用, 64 位機器是 8 字節的引用,以此類推) 。

複製值類型的值會複製所有的數據,這些數據可能很大。

有時複製引用類型的效率更高,這正是編碼規範要求值類型不得大於 16 字節的原因。如果複製值類型的代價比作爲引用複製時高出 4 倍,就應該把它設計成引用類型了。

(P239) 

除了 string 和 object 是引用類型,所有 C# “內建” 類型都是值類型。

(P240) 

[注意] 

雖然語言本身未作要求,但對於使用值類型的一種良好的規範是確保值類型是不可變的。換言之,一旦實例化值類型,實例就不能修改。要修改,應該創建新實例。

[規範] 

1. 要創建不可變的值類型;

除了屬性和字段,結構還可包含方法和構造器。結構不允許包含用戶定義的默認 (無參) 構造器。在沒有提供默認的構造器時, C# 編譯器自動地產生一個默認的構造器將所有字段初始化爲各自的默認值。引用數據類型字段的默認值是 null ,數值類型字段的默認值是零,布爾類型字段的默認值是 false 。

爲了確保值類型的局部變量能被完全初始化,結構的每個構造器都必須初始化結構中的所有字段 (和只讀的自動實現的屬性) 。

(P241) 

[規範] 

1. 要確保結構的默認值有效,總是可以獲得結構默認的 “全零” 值;

(P242) 

所有值類型都有自動定義的無參構造器將值類型的實例初始化成默認狀態。所以,總是可以合法地使用 new 操作符創建值類型的實例。除此之外,還可使用 default 操作符生成結構的默認值。

表達式 default(int) 和 new int() 都生成一樣的值。

所有值類型都隱式密封。除此之外,除了枚舉之外的所有值類型都派生自 System.ValueType 。因此,結構的繼承鏈總是從 object 到 System.ValueType 到結構。

值類型也能實現接口。

(P243) 

[規範] 

1. 如果值類型的相等性有意義,要重載值類型的相等性操作符 (Equals() 、 == 和 !=) 。還要考慮實現 IEquatable<T> 接口;

裝箱和拆箱之所以重要,是因爲裝箱會影響性能和行爲。

(P245) 

每個裝箱操作都涉及內存分配和複製,每個拆箱操作都涉及類型檢查和複製。

不允許在 lock() 語句中使用值類型。

(P247) 

[規範] 

1. 避免可變的值類型;

(P249) 

枚舉是可由開發者聲明的值類型。枚舉的關鍵特徵是在編譯時聲明瞭一組可以通過名稱來引用的常量值,這使代碼更易讀。

[注意] 

1. 用枚舉替代布爾值能改善可讀性;

(P250) 

枚舉值實際是作爲整數常量實現的。默認第一個枚舉值是 0 ,後續每一項都遞增 1 。然而,可以顯式地爲枚舉賦值。

枚舉總是具有一個基礎類型,這可以是除了 char 之外的任意整型。事實上,枚舉類型的性能完全取決於基礎類型的性能。默認基礎類型是 int ,但可以使用繼承語法指定其他類型。

[規範] 

1. 考慮使用默認的 32 位整型作爲枚舉的基礎類型。只有出於互操作性或者性能方面的考慮才使用較小的類型,只有創建標誌 (flag) 數超過 32 個的標誌枚舉時才使用較大的類型;

(P251) 

[規範] 

1. 考慮在現有枚舉中添加新成員,但要注意兼容性風險;

2. 避免創建代表 “不完整” 值 (如版本號) 集合的枚舉;

3. 避免在枚舉中創建 “保留給將來使用” 的值;

4. 避免包含單個值的枚舉;

5. 要爲簡單枚舉提供 0 值 (代表無) 。若不顯式地進行初始化,就默認從 0 開始;

枚舉和其他值類型稍有不同,因爲枚舉類型派生自 System.Enum ,而 System.Enum 又是從 System.ValueType 派生的。

(P252) 

枚舉的一個好處是 ToString() 方法會輸出枚舉值標識符。

(P253) 

[規範] 

1. 如果字符串必須本地化成用戶語言,避免枚舉和字符串之間的直接轉換;

[注意] 

1. 位標誌枚舉名稱通常是複數,因爲它的值代表一組標誌;

使用按位 OR 操作符聯結枚舉值,使用按位 AND 操作符測試特定位是否存在。

(P255) 

[規範] 

1. 要用 FlagsAttribute 指出枚舉包含標誌;

2. 要爲所有標誌枚舉提供等於 0 的 None 值;

3. 避免標誌枚舉中的零值是除了 “所有標誌都未設置” 之外的其他意思;

4. 考慮爲常用標誌組合提供特殊值;

5. 不要包含 “哨兵” 值,這種值會使用戶感到困惑;

6. 要用 2 的冪確保所有標誌組合都不重複;

如果決定使用位標誌枚舉,枚舉的聲明應該用 FlagsAttribute 進行標記。這個特性應包含在一對方括號中,並放在枚舉聲明之前。

(P257) 

[規範] 

1. 除非它在邏輯上代表單個值,消耗 16 字節或更少的存儲空間,不可變,而且很少裝箱,否則不要定義結構;

【第09章】 

(P258) 

默認情況下,在任何對象上調用 ToString() 會返回類的完全限定名稱。

(P259) 

Console.WriteLine() 和 System.Diagnostics.Trace.Write() 等方法會調用對象的 ToString() 方法,所以可重寫 ToString() 輸出比默認實現更有意義的信息。

[規範] 

1. 要重寫 ToString() 以返回有用的、面向開發人員的診斷字符串;

2. 要使 ToString() 返回的字符串簡短;

3. 不要從 ToString() 返回空字符串代表 “空” (null) ;

4. 避免 ToString() 引發異常或造成可觀察到的副作用 (改變對象狀態) ;

5. 如果返回值與語言文化相關或需要格式化,就要重載 ToString(string format) 或實現 IFormattable ;

6. 考慮從 ToString() 返回獨一無二的字符串以標識對象實例;

(P261) 

引用的相等性並不是唯一 “相等性” 。兩個對象實例的成員值部分或全部相等,也可以說它們相等。

(P263) 

兩個同一的引用顯然是相等的,然而,兩個引用不相等的對象也可能是相等的對象。對象標識不同,不一定標識數據不同。

[注意] 

1. 爲值類型調用 ReferenceEquals() 將總是返回 false ;

(P264) 

判斷兩個對象是否相等 (即,它們包含相同的標識數據) 是使用對象的 Equals() 方法。在 object 中,這個虛方法只是用 ReferenceEquals() 判斷相等性。這顯然並不充分,所以一般都有必要用更恰當的實現重寫 Equals() 。

[注意] 

1. object.Equals() 的實現只是簡單地調用了一下 ReferenceEquals() ;

(P267) 

[規範] 

1. 要一起實現 GetHashCode() 、 Equals() 、 == 操作符和 != 操作符,缺一不可;

2. 要用相同的算法實現 Equals() 、 == 和 != ;

3. 避免在 GetHashCode() 、 Equals() 、 == 和 != 的實現中引發異常;

4. 避免重載可變的引用類型的相等性操作符,對於重載的實現速度過慢的相等性操作符,也要避免重載;

5. 要在實現 IComparable 時,實現與相等性有關的所有方法;

(P267) 

除非目的是使類型表現得像是一種基元類型 (如數值類型) ,否則就不要去重載操作符。

== 默認也是執行引用相等性檢查。

(P268) 

[注意] 

1. 在 == 操作符的重載實現中避免使用相等性比較操作符 (==) ;

(P269) 

+ 、 - 、 * 、 / 、 % 、 & 、 | 、 ^ 、 << 和 >> 操作符都被實現成二元靜態方法,其中至少有一個參數的類型是包容類型 (當前正在實現該操作符的類型) 。

(P271) 

從技術上說,實現顯式和隱式轉換操作符並不是對轉型操作符 (()) 進行重載。但由於效果一樣,所以一般都將 “實現顯式或隱式轉換” 說成 “定義轉型操作符” 。

(P272) 

[注意] 

1. 實現轉換操作符時,爲了保證封裝性,要麼返回值,要麼參數必須是封閉類型。 C# 不允許在被轉換類型的作用域之外指定轉換;

[規範] 

1. 不要爲有損轉換提供隱式轉換操作符;

2. 不要從隱式轉換中引發異常;

(P273) 

開發者可以將程序的不同部分轉移到單獨的編譯單元中,這些單元稱爲類庫,或者簡稱爲庫。然後,程序可以引用和依賴類庫來提供自己的一部分功能。這樣一來,兩個程序就可以依賴同一個類庫,從而在兩個程序中共享那個類庫的功能,並減少所需的編碼量。

省略 /target 或者指定 /target:exe 都將創建一個控制檯可執行程序。

要在多個應用程序中共享的程序集通常編譯成類庫。

爲了訪問不同程序集中的代碼, C# 編譯器允許開發者在命令行上引用程序集。這種情況下使用的選項是 /reference (/r 是縮寫) ,後跟一個引用列表。

(P274) 

類封裝了一系列相關的行爲和數據,程序集則封裝了一系列相關的類型。開發者可以將一個系統分解成多個程序集,然後在多個應用程序中共享那些程序集,或者將它們與第三方提供的程序集集成。

默認情況下,沒有任何訪問修飾符的類被定義成 internal 。結果是該類型無法從程序集外部訪問。即使另一個程序集引用了該類所在的程序集,被引用程序集中的所有 internal 類都是無法訪問的。

(P275) 

類似於爲類成員使用 private 和 protected 訪問修飾符來指定不同的封裝級別, C# 允許爲類使用訪問修飾符,從而控制類在程序集中的封裝級別。可用的訪問修飾符是 public 和 internal 。類要在程序集外部可見,必須標記成 public 。

internal 訪問修飾符並非僅適用於類型聲明,它還適用於類型的成員。

成員的可訪問性無法超過它所在的類型的可用性。

protected internal 是另一種類型成員訪問修飾符。這種成員可從包容程序集的任何位置以及類型的派生類中訪問 (即使派生類不在同一個程序集中) 。

由於默認是 private ,所以隨便指定別的一個訪問修飾符 (public 除外) ,成員的可見性都會稍微提高。

添加兩個修飾符,可訪問性會複合到一起,變得更大。

[注意] 

1. protected internal 成員可以從包容程序集的任何位置以及類型的派生類中訪問 (即使派生類不在同一個程序集中) ;

(P276) 

任何數據類型都用命名空間與名稱的組合來標識;

CLR 對 “命名空間” 一無所知, CLR 中的類型名稱都是完全限定的,包含命名空間。

(P277) 

命名空間大括號之間的所有內容都從屬於該命名空間。

[注意] 

1. CLR 中沒有 “命名空間” 這種東西。類型名稱必然完全限定;

和類相似,命名空間也可以嵌套。

(P278) 

由於命名空間是對類型進行組織的關鍵,所以使用命名空間來組織所有的類文件通常都是有益的。

有鑑於此,可以爲每個命名空間都創建一個文件夾。

[規範] 

1. 要爲命名空間名稱附加公司名前綴,防止不同公司的命名空間使用相同的名稱;

2. 要爲命名空間名稱中的二級名稱使用穩定的、不隨版本升級而變化的產品名稱;

3. 不要定義沒有明確放到一個命名空間中的類型;

4. 考慮創建與命名空間層次結構相匹配的文件夾結構;

(P282) 

[規範] 

1. 如果 API 簽名不能完全說明問題,要爲公共 API 提供 XML 註釋,其中包括成員說明、參數說明和 API 調用示例;

垃圾回收時是 “運行時” 的核心功能,作用是回收不再被引用的對象所佔用的內存。這句話的重點是 “內存” 和 “引用” 。垃圾回收器只回收內存,不處理其他資源。

垃圾回收器根據是否存在任何引用來決定要清理什麼。這暗示垃圾回收器處理的是引用對象,只回收堆上的內存。

爲了定位和移動所有可達對象,系統要在垃圾回收器運行期間維持狀態的一致性。爲此,進程中的所有託管線程都會在垃圾回收期間暫停。這顯然會造成應用程序出現短暫的停頓。不過,除非垃圾回收週期特別長,否則這個停頓是不太引人注意的。

(P284) 

終結器不能從代碼中顯式調用。

[注意] 

1. 編譯時不能確定終結器的確切執行時間;

(P285) 

終結器不允許傳遞任何參數,因此終結器不能重載。此外,終結器不能顯式調用。調用終結器的只能是垃圾回收器。因此,爲終結器添加訪問修飾符沒有意義 (也不支持) 。基類中的終結器作爲對象終結調用的一部分被自動調用。

[注意] 

1. 終結器不能顯式調用,只有垃圾回收器才能調用終結器;

由於垃圾回收器負責所有內存管理工作,所以終結器不負責回收內存。

終結器在自己的線程中執行,這使它們的執行變得更不確定。

終結器是對資源進行清理的備用機制。

很有必要提供進行確定性終結的方法,避免依賴終結器不確定的計時行爲。

(P287) 

using 語句只是提供了 try / finally 塊的語法快捷方式。

(P289) 

[規範] 

1. 要只爲使用了稀缺或昂貴資源的對象實現終結器方法,即使終結會推遲垃圾回收;

2. 要爲有終結器的類實現 IDisposable 接口以支持確定性終結;

3. 要爲實現了 IDisposable 的類實現終結器方法,以防 Dispose() 沒有被顯式調用;

4. 要重構終結方法來調用與 IDisposable 相同的代碼,可能就是調用一下 Dispose() 方法;

5. 不要在終結器方法中引發異常;

6. 要從 Dispose() 中調用 System.CC.SuppressFinalize() ,使垃圾回收更快地發生,並避免重複性的資源清理;

7. 要保證 Dispose() 具有冪等性 (可以被多次調用) ;

8. 要保證 Dispose() 的簡單性,把重點放在終結所要求的資源清理上;

9. 避免爲自己擁有的、帶終結器的對象調用 Dispose() 。相反,依賴終結隊列清理實例;

10. 避免在終結方法中引未被終結的其他對象;

11. 要在重寫 Dispose() 時調用基類的實現;

12. 考慮在調用 Dispose() 之後確保對象狀態變爲不可用。對象被 dispose 之後,調用除 Dispose() 之外的方法都應該引發 ObjectDisposedException 異常。 (Dispose() 應該能多次調用) ;

13. 要爲含有可 dispose 字段 (或屬性) 的類型實現 IDisposable 接口,並 dispose 這些實例;

【第10章】 

(P292) 

要引發異常,只需爲要引發的異常實例附加關鍵字 throw 作爲前綴。

(P293) 

C# 6.0 的總的規範是,對於參數類型異常中的參數名稱應該使用 nameof 操作符。

(P294) 

[規範] 

1. 要在向成員傳遞了錯誤參數時引發 ArgumentException 或者它的某個子類型。引發儘可能具體的異常 (如 ArgumentNullException) ;

2. 要在引發 ArgumentException 或者它的某個子類時設置 ParamName 屬性;

3. 要對傳入參數異常類型 (如 ArgumentException 、 ArgumentOutRangeException 和 ArgumentNullException) 的 ParamName 實參使用 nameof 操作符;

4. 要引發能說明問題的、最具體的異常 (派生得最遠的異常);

5. 不要引發 NullReferenceException 。相反,在值意外爲空時引發 ArgumentNullException ;

6. 不要引發 System.SystemException 或者從它派生的異常類型;

7. 不要引發 System.Exception 或者 System.ApplicationException ;

8. 考慮在程序繼續執行會變得不安全時調用 System.Environment.FailFast() 來終止進程;

(P297) 

C# 還支持常規 catch 塊,即 catch{} ,它在行爲上和 catch(System.Exception exception) 塊完全一致,只是沒有類型名或變量名。除此之外,常規 catch 塊必須是所有 catch 塊的最後一個。

C# 允許寫一個無參數的 catch 塊, C# 團隊將這個 catch 塊稱爲常規 catch 塊。

(P298) 

常規 catch 塊捕獲先前的 catch 塊沒有捕獲到的所有異常,無論它們是不是從 System.Exception 派生。

(P299) 

常規 catch 塊 (空 catch 塊) 不僅能捕獲非託管類型的異常,還能捕獲非 System.Exception 託管類型的異常。

[規範] 

1. 避免在調用棧較低的位置報告或記錄異常;

2. 不要捕獲不應該捕獲的異常。要允許異常在調用棧中向上傳播,除非能非常清楚地知道如何通過程序準確地定位棧中較低位置的錯誤;

3. 如果理解特定異常在給定的上下文中爲何引發,並能通過程序響應錯誤,就考慮捕獲該異常;

4. 避免捕獲 System.Exception 或 System.SystemException ,除非是在頂層異常處理程序中在重新引發異常之前執行最後的清理操作;

5. 要在 catch 塊中使用 throw ;而不是 throw <異常對象> 語句;

6. 重新引發不同的異常時要謹慎;

7. 不要引發 NullReferenceException 。相反,在值意外爲空時引發 ArgumentNullException ;

8. 避免通過異常條件引發異常;

9. 避免使用會經常改變的異常條件;

 (P303) 

 [規範] 

 1. 如果異常不以有別於現有 CLR 異常的方式進行處理,就不要創建新異常。相反,應該引發現有的框架異常;

 2. 要創建新異常類型來描述特別的程序錯誤。這種錯誤用現有的 CLR 異常無法描述,而且能通過程序以不同於現有 CLR 異常類型的方式進行處理;

 3. 要爲所有自定義異常類型提供無參構造器。還要提供獲取消息和內部異常作爲參數的構造器;

 4. 要爲異常類的名稱附加 “Exception” 後綴;

 5. 要使異常能由 “運行時” 序列化;

 6. 考慮提供異常屬性,以便通過程序訪問異常的額外信息;

 7. 避免使用過深的異常繼承層次結構;

(P304) 

[規範] 

1. 如果低層引發的異常在高層操作的上下文中沒有意義,考慮將低層異常封裝到更恰當的異常中;

2. 要在封裝異常時設置內部異常屬性;

3. 要將開發人員作爲異常的接收者,儘量說清楚問題和解決問題的機制;

4. 要在重新引發相同的異常時使用空的 throw 語句 (throw;) ,而不是向 throw 傳遞異常作爲參數;

【第11章】 

(P307) 

C# 通過泛型 (generics) 來促進代碼重用,尤其是算法的重用。

(P313) 

泛型允許開發人員把精力放在創建算法和模式上,並確保代碼能由不同數據類型重用。

在類名之後,需要在一對尖括號中指定類型參數。

可以向泛型提供類型實參,它將 “替換” 類中出現的每個 T 。

(P314) 

最核心的是,泛型允許寫代碼來實現模式,並在以後出現這種模式的時候重用那個實現。模式描述了在代碼中反覆出現的問題,而泛型類型爲這些反覆出現的模式提供了單一的實現。

(P315) 

[規範] 

1. 要爲類型參數選擇有意義的名稱,併爲名稱附加 “T” 前綴;

2. 考慮在類型名稱中指明約束;

C# 支持在語言中全面地使用泛型,其中包括接口和結構。

要聲明包含類型參數的接口,將類型參數放到接口名稱後面的一對尖括號中即可。

注意,一個泛型的類型實參可以成爲另一個泛型類型的類型參數。

相同泛型接口的不同構造被就看成是不同的類型,所以類或結構能多次實現 “同一個” 泛型接口。

(P316) 

[規範] 

1. 避免在類型中實現同一個泛型接口的多個構造;

泛型類或結構的構造器 (和終結器) 不要求類型參數。

(P317) 

default 操作符可提供任意類型的默認值,包括類型參數。

(P318) 

類型參數的數量 (或者稱爲元數,即 arity) 對類進行了唯一性的區別。

[規範] 

1. 要將只是類型參數數量不同的多個泛型類放到同一個文件中;

(P320) 

[規範] 

1. 避免在嵌套類型中用同名參數隱藏外層類型的類型參數;

(P323) 

對於任何給定的類型參數,都可以指定任意數量的接口約束,但類類型約束只能指定一個,因爲一個類可以實現任意數量的接口,但肯定只能從一個類繼承。每個新約束都在一個以逗號分隔的列表中聲明,約束列表跟在泛型類型名稱和一個冒號之後。如果有多個類型參數,每個類型參數前面都要使用 where 關鍵字。

(P324) 

注意,在兩個 where 字句之間,並不存在逗號。

(P329) 

泛型方法要使用泛型類型參數,這一點和泛型類型一樣。

在泛型或非泛型類型中都能聲明泛型方法。

如果在泛型類型中聲明泛型方法,其類型參數和泛型類型的類型參數是有區別的。

爲了聲明泛型方法,要按照與泛型類型一樣的方式指定泛型類型參數,也就是在方法名之後添加類型參數聲明。

使用泛型類型時,是在類型名之後提供類型實參。類似地,調用泛型方法時,是在方法的類型名之後提供類型實參。

(P330) 

爲了避免多餘的編碼,可以在調用時不指定類型實參。這就是所謂的類型推斷。

類型推斷要想成功,方法實參的類型必須與泛型方法的形參 “匹配” 以推斷出正確的類型實參。

泛型方法的類型參數也允許指定約束,其方式與在泛型類型中指定類型參數的方式相同。

約束在參數列表和方法主體之間指定。

(P332) 

[規範] 

1. 避免用表面上看是類型安全的但實際並不是類型安全的泛型方法誤導調用者;

(P333) 

從 C# 4 開始加入了對安全協變性的支持。爲了指出泛型接口應該對它的某個類型參數協變,就用 out 修飾符來修飾該類型參數。

(P334) 

用 out 修飾泛型接口的類型參數,會導致編譯器驗證 T 真的只用作 “輸出” ,即只用於方法的返回類型和只讀屬性的返回類型,永遠不用於形參或者屬性的賦值方法。

協變轉換有一些重要的限制 : 

1. 只有泛型接口和泛型委託纔可以是協變的。泛型類和結構永遠不是協變的;

2. 提供給 “來源” 和 “目標” 泛型類型的類型實參必須是引用類型,不能是值類型;

3. 接口或委託必須聲明爲支持協變,編譯器必須驗證協變所針對的類型參數確實只用在 “輸出” 位置;

(P335) 

與協變性相似,逆變性要求在聲明接口的類型參數時使用修飾符 in 。它指示編譯器覈實 T 從未在屬性的取值方法 (get 訪問器方法) 中出現,也沒有作爲方法的返回類型使用。如果檢查無誤,就啓用接口的逆變轉換。

逆變轉換存在與協變轉換相似的限制 : 只有泛型接口和委託類型才能是逆變的,發生變化的類型實參只能是引用類型,而且編譯器必須能驗證接口對於逆變轉換是安全的。

(P336) 

[規範] 

1. 避免不安全的數組協變。而是考慮將數組轉換成只讀接口 IEnumerable<T> ,以便通過協變轉換來安全地轉換;

(P337) 

泛型類編譯後與普通類並無區別。編譯的結果只有元數據和 CIL 。 CIL 是參數化的,接受在代碼中別的地方由用戶提供的類型。

除了在類的頭部包含元數和類型參數,並在代碼中用感嘆號指出類型參數之外,泛型類和非泛型類的 CIL 代碼並無多大區別。

【第12章】 

(P343) 

就像類能嵌套在其他類中一樣,委託也能嵌套在類中。假如委託聲明出現在另一個類的內部,委託類型就會成爲嵌套類型。

(P345) 

從 C# 2.0 開始,從方法組 (爲方法命名的表達式) 向委託類型的轉換會自動創建一個新的委託對象。

委託實際是特殊的類。

.NET 中的委託類型總是派生自 System.MulticastDelegate ,後者又從 System.Delegate 派生。

(P348) 

語句 Lambda 由形參列表,後跟 Lambda 操作符 (=>) ,然後跟一個代碼塊構成。

(P349) 

通常,只要編譯器能從 Lambda 表達式所轉換成的委託推斷出類型,所有 Lambda 表達式都不需要顯式聲明參數類型。然而,若指定類型能使代碼更易讀, C# 也允許這樣做。在不能推斷出類型的情況下, C# 要求顯式地指定 Lambda 參數類型。只要顯式指定了一個 Lambda 參數類型,所有參數類型都必須被顯式指定,而且必須和委託參數類型完全一致。

[規範] 

1. 考慮在 Lambda 形參列表中省略類型,只要類型對於讀者是顯而易見的,或者是無關緊要的細節;

當只有單個參數,而且類型可以推斷時,這種 Lambda 表達式可省略圍繞參數列表的圓括號。

如果 Lambda 沒有參數,或者有不止一個參數,或者顯式指定了類型的單個參數,那麼就必須將參數列表放到圓括號中。

(P350) 

空參數列表要求圓括號。

語句 Lambda 的語法比完整的方法聲明簡單得多,可以不指定方法名、可訪問性和返回類型,有時甚至可以不指定參數類型。

表達式 Lambda 只要返回的表達式,完全沒有語句塊。

(P351) 

不能對一個匿名方法使用 typeof() 操作符。

只有在將匿名方法轉換成一個特定類型後才能調用 GetType() 。

C# 2.0 不支持 Lambda 表達式,而是使用稱爲匿名方法的語法。

匿名方法很像語句 Lambda ,但缺少許多使 Lambda 變得簡潔的特性。

匿名方法必須顯式指定每個參數的類型,而且必須有一個語句塊。參數列表和代碼塊之間不使用 Lambda 操作符 (=>) ,而是在參數列表前面添加關鍵字 delegate ,以強調匿名方法必須轉換成一個委託類型。

[規範] 

1. 避免在新代碼中使用匿名方法語法,應該優先使用更簡潔的 Lambda 表達式語法;

有一個小特性是匿名方法支持而 Lambda 表達式不支持的,匿名方法在某些情況下可以徹底省略參數列表。

和 Lambda 表達式不同,匿名方法允許徹底省略參數列表,前提是主體中不使用任何參數,而且委託類型只要求 “值” 參數 (也就是說,不要求將參數標記爲 out 或 ref) 。

(P353) 

爲了減少自定義委託類型的必要, .NET 3.5 “運行時” 庫 (對應 C# 3.0) 包含了一組通用的委託,其中大多數都是泛型。

System.Func 系列委託代表有返回值的方法,而 System.Action 系列委託代表返回 void 的方法。

(P354) 

Func 委託的最後一個類型參數總是委託的返回類型。其他類型參數依次對應於委託參數的類型。

在許多情況下, .NET Framework 3.5 添加的 Func 委託都能完全避免定義自己的委託類型。然而,如果要想顯著增強代碼的可讀性,還是應該聲明自己的委託類型。

[規範] 

1. 考慮定義自己的委託類型對於可讀性的提升是否比使用預定義泛型委託類型所帶來的便利性來得重要;

(P355) 

實現泛型委託類型的引用轉換,這是 C# 4.0 添加協變和逆變轉換的關鍵原因之一。 (另一個原因是提供 IEnumerable<out T> 的協變性支持) 。

(P359) 

[規範] 

1. 避免在匿名函數中捕捉循環變量;

(P360) 

轉換成表達式樹的 Lambda 表達式對象代表的是對 Lambda 表達式進行描述的數據,而不是編譯好的、用於實現匿名函數的代碼。

表達式樹並非只能轉換成 SQL 語句;還可以構造一個表達式樹計算程序 (evaluator) ,將表達式轉換成任意查詢語言。

【第13章】 

(P366) 

委託本身又是一個更大的模式 (pattern) 的基本單元,這個模式稱爲 publish-subscribe (發佈-訂閱) 。

一個委託值是可以引用一系列方法的,這些方法將順序調用。這樣的委託稱爲多播委託 (multicast delegate) 。利用多播委託,單一事件的通知就可以發佈給多個訂閱者。

(P368) 

只需一個委託字段即可存儲所有訂閱者。

(P369) 

只需執行一個調用,即可向多個訂閱者發出通知 —— 這正是將委託更明確地稱爲 “多播委託” 的原因。

(P371) 

[規範] 

1. 要在調用委託前檢查它的值是不是 null 值;

2. 從 C# 6.0 開始,要在調用 Invoke() 之前使用 null 條件操作符;

(P373) 

無論 + 、 - 還是它們的複合賦值版本 (+= 和 -=) ,在內部都是使用靜態方法 System.Delegate.Combine() 和 System.Delegate.Remove() 來實現的。

(P380) 

event 關鍵字提供了必要的封裝來防止任何外部類發佈一個事件或者取消之前不是由其添加的訂閱者。

(P381) 

System.EventArgs 唯一重要的屬性是 Empty ,它用於指出不存在事件數據。

(P382) 

[規範] 

1. 要在調用委託前檢查它的值不爲 null (在 C# 6.0 中要使用 null 條件操作符) ;

2. 不要爲非靜態事件的 sender 傳遞 null 值;

3. 要爲靜態事件的 sender 傳遞 null 值;

4. 不要爲 eventArgs 參數傳遞 null 值;

5. 要爲事件使用 EventHandler<TEventArgs> 委託類型;

6. 要爲 TEventArgs 使用 System.EventArgs 類型或者它的派生類型;

7. 考慮使用 System.EventArgs 的子類作爲事件的實參類型 (TEventArgs) ,除非完全確定事件永遠不需要攜帶任何數據;

(P382) 

爲事件定義類型的規範是使用 EventHandler<TEventArgs> 委託類型。

(P383) 

通常應優先使用 EventHandler<TEventArgs> 。

在 C# 2.0 和之後使用事件的大多數情形中,都沒必要聲明自定義委託數據類型。

[規範] 

1. 要爲事件處理程序使用 System.EventHandler<T> 而非手動創建新的委託類型,除非自定義類型的參數名能提供有意義的說明;

事件限制外部類只能通過 “+=” 操作符向發佈者添加訂閱方法,並用 “-=” 操作符取消訂閱,除此之外的任何事情都不允許做。

【第14章】 

(P387) 

匿名類型是由編譯器聲明的數據類型。

(P388) 

匿名類型純粹是一個 C# 語言特性,不是 “運行時” 中的一種新類型。當編譯器遇到匿名類型語法時,自動生成一個 CIL 類,其屬性對應於在匿名類型聲明中命名的值和數據類型。

雖然 C# 匿名類型沒有名稱,但它仍然是強類型的。

(P389) 

[注意] 

1. 除非賦給變量的類型能一眼看出,否則應該只有在聲明匿名類型 (具體類型只有在編譯時才能確定) 時,才使用隱式類型的變量。不要不分青紅皁白地使用隱式類型的變量;

兩個匿名類型要在同一個程序集中做到類型兼容,屬性名稱、數據類型和屬性順序都必須完全匹配。

(P390) 

編譯器在生成匿名類型的代碼時,重寫了 ToString() 方法。

(P394) 

根據定義, .NET 中的集合本質上是一個類,它最起碼實現了 IEnumerbale<T> (或非泛型類型 IEnumerable) 。這個接口非常關鍵,因爲要想支持對集合執行的遍歷操作,最起碼的要求就是實現 IEnumerable<T> 規定的方法。

(P396) 

泛型集合的一個關鍵特徵就是將一種特定類型的對象全都收集到一個集合中。

集合類不直接支持 IEnumerator<T> 和 IEnumerator 接口。

(P398) 

IEnumerable<T> 上的每個方法都是一個標準查詢操作符 (standard query operator) ,用於爲所操作的集合提供查詢功能。

(P401) 

獲取一個實參並返回一個布爾值的委託表達式稱爲 “謂詞” 。

predicate 在 .NET Framework SDK 文檔中翻譯成 “謂詞” 。

(P403) 

使用 Select() 進行 “投射” ,這是非常強大的一個功能。

Where() 標準查詢操作符在 “垂直” 方向上篩選集合 (減少集合中項的數量) 。

Select() 標準查詢操作符在 “水平” 方向上減小集合的規模 (減少列的數量) 或者對數據進行徹底的轉換。

綜合運用 Where() 和 Select() ,可以獲得原始集合的一個子集,從而滿足當前算法的要求。

(P404) 

.NET Framework 4 引入了標準查詢操作符 AsParallel() ,它是靜態類 System.Linq.ParallelEnumerable 的成員。

對數據項集合執行的另一個常見的操作是獲取計數。爲了支持這種類型的查詢, LINQ 提供了 Count() 擴展方法。

(P405) 

如果計數的目的只是爲了看這個計數是否大於 0 ,那麼首選的做法是使用 Any() 操作符。 Any() 只嘗試遍歷集合中的一個項,如果成功就返回 true ,而不會遍歷整個序列。

[規範] 

1. 要在檢查集合中是否有項的時候使用 System.Linq.Enumerable.Any() 而不是調用 Count() 方法;

2. 要使用集合的 Count 屬性 (如果有的話) ,而不是調用 System.Linq.Enumerable.Count() 方法;

使用 LINQ 時,要記住的一個重要概念就是推遲執行。

(P406) 

通常,任何謂詞都只應做一件事情 : 對一個條件進行求值。它不應該有任何 “副作用” 。

Lambda 表達式在聲明時不執行。Lambda 表達式除非被調用,否則其中的代碼不會執行。

(P409) 

OrderBy() 獲取一個 Lambda 表達式,該表達式標識了要據此進行排序的鍵。

OrderBy() 只會獲取一個稱爲 keySelector 的參數來排序。要依據第 2 列來排序,需要使用一個不同的方法 ThenBy() 。類似地,更多的排序要使用更多的 ThenBy() 。

OrderBy() 返回的是一個 IOrderedEnumerable<T> 接口,而不是一個 IEnumerable<T> 。除此之外, IOrderedEnumerable<T> 是從 IEnumerable<T> 派生的,所以能爲 OrderBy() 的返回值使用全部標準查詢操作符 (包括 OrderBy()) 。但是,假如重複調用 OrderBy() ,會撤銷上一個 OrderBy() 的工作,只有最後一個 OrderBy() 的 keySelector 才真正起作用。所以,注意不要在上一個 OrderBy() 調用的基礎上再調用 OrderBy() 。

爲了指定額外的排序條件,應該使用 ThenBy() 。雖然 ThenBy() 是一個擴展方法,但它擴展的不是 IEnumerable<T> ,而是 IOrderedEnumerable<T> 。

總之,要先使用 OrderBy() ,再執行零個或者多個 ThenBy() 調用來提供額外的排序 “列” 。

(P410) 

[規範] 

1. 不要爲 OrderBy() 的結果再次調用 OrderBy() 。附加的排序依據用 ThenBy() 來指定;

(P416) 

GroupBy() 返回的是 IGrouping<TKey, TElement> 類型的數據項,該類型有一個屬性指定了作爲分組依據的鍵。

由於 IGrouping<TKey, TElement> 是從 IEnumerable<T> 派生的,所以可以用 foreach 語句枚舉組中的項,或者將數據聚合成像計數這樣的東西。

(P422) 

LINQ Provider 的作用是將表達式分解成各個組成部分。一經分解,表達式就可以轉換成另一種語言,可以序列化以便在遠程執行,可以通過一個異步執行模式來注入。

(P423) 

LINQ Provider 爲一個標準集合 API 提供了一種 “解釋” 機制。利用這種幾乎沒有任何限制的功能,可以注入與查詢和集合有關的行爲。

【第15章】 

(P426) 

查詢表達式總是以 “from 子句” 開始,以 “select 子句” 或者 “group by 子句” 結束。這些子句分別用上下文關鍵字 from 、 select 或 group 來標識。 from 子句中的標識符 word 稱爲範圍變量 (range variable) ,代表集合中的每一項,這就像是 foreach 循環中的循環變量代表集合中的每一項。

C# 查詢表達式的順序其實更接近各個操作在邏輯上的順序。對查詢進行求值時,首先指定集合 (from 子句) ,再篩選出想要的項 (where 子句) ,最後描述希望的結果 (select 子句) 。

查詢表達式的結果是 IEnumerable<T> 或 IQueryable<T> 類型的集合。 T 的實際類型是從 select 或 group by 子句推導的。

(P427) 

查詢表達式的 select 子句可以將 from 子句的表達式所收集到的東西投射到完全不同的數據類型中。

(P428) 

利用匿名類型,執行查詢時可以不必獲取全部數據,而是隻在集合中存儲和獲取需要的數據。

(P431) 

推遲執行通過委託和表達式樹來實現。委託允許創建和操縱方法的引用,方法中含有可在以後調用的表達式。類似地,利用表達式樹,可創建和操縱與表達式有關的信息,這種表達式能在以後檢查和處理。

篩選條件 (filter criteria) 用謂詞表示。所謂謂詞,本質上就是返回布爾值的 Lambda 表達式。

(P432) 

在查詢表達式中對數據項進行排序的是 orderby 子句。

多個排序條件以逗號分隔。

(P433) 

ascending 和 descending 是上下文關鍵字,分別指定以升序或降序排序。將排序順序指定爲升序或降序是可選的。如果沒有指定排序指定,就默認爲 ascending 。

let 子句引入了一個新的範圍變量,它容納的表達式值可以在查詢表達式剩餘的部分使用。可添加任意數量的 let 表達式,只需把它們每一個作爲一個附加的子句,放在第一個 from 子句之後、最後一個 select / group by 子句之前,加入查詢即可。

(P435) 

由於含有 group by 子句的查詢會產生一系列組合,所以對結果進行迭代的常用模式是創建嵌套的 foreach 循環。

(P436) 

group 子句使查詢返回由 IGrouping<TKey, TElement> 對象構成的集合。

into 子句引入的範圍變量成爲查詢剩餘部分的範圍變量;之前的任何範圍變量在邏輯上成爲之前查詢的一部分,不可在查詢延續中使用。

(P437) 

into 相當於一個管道操作符,它將第一個查詢的結果 “管道傳送” 給第二個查詢。用這種方式可以鏈接起任意數量的查詢。

(P439) 

每個查詢表達式都能 (而且必須能) 轉換成方法調用,但不是每一系列的方法調用都有對應的查詢表達式。

[規範] 

1. 要用查詢表達式語法使查詢更易讀,尤其是涉及複雜的 from 、 let 、 join 或 group 子句時;

2. 考慮在查詢所涉及的操作沒有查詢表達式語法時,使用標準查詢操作符 (方法調用形式) ;

【第16章】 

(P440) 

.NET Framework 中有許多非泛型集合類和接口,但它們主要是爲了向後兼容。

泛型集合類不僅更快 (因爲避免了裝箱開銷) ,還更加類型安全。所以,新代碼應該總是使用泛型集合類。

(P442) 

選擇集合類來解決數據存儲或者數據獲取問題時,首先要考慮的兩個接口就是 IList<T> 和 IDictionary<TKey, TValue> 。這兩個接口決定了集合類型是側重於通過位置索引來獲取值,還是側重於通過鍵來獲取值。

List<T> 類具有與數組相似的屬性。關鍵區別是隨着元素數量的增多,這種類會自動擴展 (與之相反,數組的長度是固定的) 。

(P444) 

如果元素類型實現了泛型 IComparable<T> 接口或者非泛型 IComparable 接口,排序算法默認就用它來決定排序順序。

IComparable<T> 和 IComparer<T> 的區別很細微,但卻很重要。前者說 “我知道如何將我自己和我的類型的另一個實例進行比較” ,後者說 “我知道如果比較給定類型的兩個實例” 。

(P446) 

[規範] 

1. 要確保自定義比較邏輯產生一致的 “全序” ;

集合類不要求集合中的所有元素都是唯一的。假如集合中有兩個或者更多的元素相同,則 IndexOf() 返回的是第一個索引, LastIndexOf() 返回的是最後一個索引。

BinarySearch() 採用的是快得多的二分搜索算法,但它要求元素已經排好序。

BinarySearch() 方法的一個有用的功能是假如元素沒有找到,會返回一個負整數。

(P447) 

鍵可爲任意數據類型,而非僅能爲字符串或數值。

(P449) 

Dictionary<TKey, TValue> 是作爲 “散列表” 實現的;這種數據結構在根據鍵來查找值時速度非常快 —— 無論字典中存儲了多少值。相反,檢查特定值是否在字典集合中相當花時間,性能和搜索無序列表一樣是 “線性” 的。

(P450) 

[規範] 

1. 不要對集合元素的順序進行任何假定。如果集合的說明文檔沒有指明它是按特定順序枚舉的,就不能保證以任何特定順序生成元素;

(P452) 

已排序集合類的元素是已經排好序的。對於 SortedDictionary<TKey, TValue> 元素是按照鍵排序的;對於 SortedList<T> ,元素則是按照值類排序的。

爲了在不修改棧的前提下訪問棧中的元素,要使用 Peek() 和 Contains() 方法。 Peek() 方法返回 Pop() 將獲取的下一個元素。

(P454) 

System.Collections.Generic 還支持鏈表集合,它允許正向和反向遍歷。

數組、字典和列表都提供了索引器 (indexer) 以便根據鍵或索引來獲取或者設置成員。

(P457) 

[規範] 

1. 不要用 null 引用表示空集合;

2. 考慮使用 Enumerable.Empty<T>() 方法生成空集合;

(P464) 

[規範] 

1. 考慮在迭代較深的數據結構時使用非遞歸算法;

(P467) 

yield 關鍵字是上下文關鍵字,不是保留的關鍵字。可以合法地聲明名爲 yield 的局部變量 (雖然這樣做有時會令人混淆) 。

只有在返回 IEnumerator<T> 或者 IEnumerable<T> 類型 (或者它們的非泛型版本) 的成員中,才能使用 yield return 語句。

【第17章】 

(P470) 

反射是指對程序集中的元數據進行檢查的過程。

通過 System.Type 的實例訪問類型的元數據,該對象包含了對類型實例的成員進行枚舉的方法。除此之外,可以調用被檢查類型的特定對象的成員。

讀取類型的元數據,關鍵在於獲得 System.Type 的一個實例,它代表了目標類型實例。

object 包含一個 GetType() 成員,因此,所有類型都包含該方法。調用 GetType() 可獲得與原始對象對應的 System.Type 實例。

(P471) 

獲得 Type 對象的另一個辦法是使用 typeof 表達式。 typeof 在編譯時綁定到特定的 Type 實例,並直接獲取類型作爲參數。

typeof 表達式在編譯時解析,這樣,類型比較 (也許是比較 GetType() 調用的返回類型) 就可以判斷一個對象是否是指定類型。

反射並非僅可以用來獲取元數據。下一步是獲取元數據,並動態調用它引用的成員。

(P475) 

MethodInfo 和 PropertyInfo 都是從 MemberInfo 繼承的 (雖然並非直接) 。

(P481) 

自定義特性很容易定義。特性是對象,定義特性就要定義類。

從 System.Attribute 派生之後,一個普通的類就成爲了特性。

[規範] 

1. 要爲自定義特性類添加 “Attribute” 後綴;

(P486) 

[規範] 

1. 要爲必需的參數提供只能取值的屬性 (提供私有賦值函數) ;

2. 要提供構造器參數來初始化與必需的參數對應的屬性。每個參數的名稱都應該和對應的屬性同名 (大小寫不同) ;

3. 避免提供構造器參數來初始化與可選參數對應的屬性 (因此,還要避免重載自定義屬性構造器) ;

(P487) 

[規範] 

1. 要對自定義特性應用 AttributeUsageAttribute 類;

(P492) 

爲了執行序列化,只需實例化一個 formatter ,然後爲合適的流對象調用 Serialization() 。爲了執行反序列化,只需調用 formatter 的 Deserialize() 方法,並指定包含了已序列化對象的流作爲參數。然而,由於 Deserialize() 返回的是 object 類型,所以還需要把它轉型爲最初的類型。

不可序列化的字段應使用 System.NonSerializable 特性來修飾。它告訴序列化框架忽略這些字段。不應持久化的字段也應使用這個特性來修飾。

(P497) 

dynamic 數據類型的幾個特徵 : 

1. dynamic 是告訴編譯器生成代碼的指令;

2. 任何類型都會轉換成 dynamic ;

3. 從 dynamic 到一個替代類型的成功轉換要依賴於基礎類型的支持;

4. dynamic 類型的基礎類型在每次賦值時都可能改變;

5. 驗證基礎類型上是否存在指定的簽名要推遲到運行時才進行 —— 但至少會進行;

6. 任何 dynamic 成員調用返回的都是一個 dynamic 對象;

7. 如果指定的成員在運行時不存在, “運行時” 會引發 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ;

8. 用 dynamic 實現的反射不支持擴展方法;

9. 究其根本, dynamic 是一個 System.Object ;

【第18章】 

(P505) 

爲了保證 UI 響應迅速,同時高效利用 CPU ,標準技術是編寫多線程程序, “並行” 執行多個計算。

(P506) 

進程是給定程序當前正在執行的實例;操作系統的一個基本功能就是管理進程。每個進程都包含一個或多個線程。進程由 System.Diagnostics 命名空間的 Process 類的實例表示。

C# 編程在語句和表達式的級別上根本就是在描述控制流。

線程由 System.Threading.Thread 類的實例表示。 Thread 類和操縱 Thread 的 API 都在 System.Threading 命名空間中。

任務和線程的區別是 : 任務代表需要執行的作業,而線程代表做這個作業的工作者。

任務由 Task 類的實例表示。

(P507) 

[規範] 

1. 不要以爲多線程總是會使代碼更快;

2. 要在通過多線程來更快解決處理器受限問題時,謹慎地衡量性能;

(P509) 

[規範] 

1. 不要無根據地以爲普通代碼中原子性的操作在多線程代碼中也是原子性的;

2. 不要以爲所有線程看到的都是一致的共享內存;

3. 要確保同時擁有多個鎖的代碼總是以相同的順序獲取它們;

4. 避免所有競態條件,即程序行爲不能受操作系統調度線程的方式的影響;

可以將線程想象成一名 “工作者” ,它獨立地按照你的程序指令工作。

(P511) 

調用 Thread.Start() 是告訴操作系統開始併發地執行一個新線程。

(P512) 

不要將 Thread.Sleep() 作爲高精度計時器使用,因爲它不是。

[規範] 

1. 避免在生產代碼中調用 Thread.Sleep() ;

(P513) 

[規範] 

1. 避免在生產代碼中終止線程,因爲可能發生不可預測的結果,使程序不穩定;

(P514) 

[規範] 

1. 要用線程池向處理器受限任務高效地分配處理器時間;

2. 避免將池中的工作者線程分配給 I / O 受限或者長時間運行的任務,而是改爲使用 TPL ;

(P515) 

任務是對象,其中封裝了以異步方式執行的工作。

委託是同步的,而任務是異步的。

任務將委託從同步執行模式轉變成異步。

(P518) 

C# 編程其實就是在延續的基礎上構造延續,直到整個程序的控制流結束。

(P519) 

異步任務使我們能將較小的任務合併成較大的任務,只需描述好異步延續就可以了。

可用 ContinueWith() “鏈接” 兩個任務,這樣當先驅任務完成後,第二個任務 (延續任務) 自動以異步方式開始。

(P520) 

由於 ContinueWith() 方法也返回一個 Task ,所以可以作爲另一個 Task 的先驅使用。以此類推,便可以建立起任意長度的連續任務鏈。

(P527) 

[規範] 

1. 避免程序在任何線程上產生未處理異常;

2. 考慮登記 “未處理異常” 事件處理程序以進行調試、記錄和緊急關閉;

3. 要取消未完成的任務而不要在程序關閉期間允許其運行;

(P529) 

在 .NET 4.0 中,獲取任務的一般方式是調用 Task.Factory.StratNew() 。

.NET 4.5 提供了更簡單的調用方式 Task.Run() 。

Task.Factory.StratNew() 用於調用一個要求創建額外線程的 CPU 密集型方法。而在 .NET 4.5 中,應該默認使用 Task.Run() ,除非它滿足不了一些特殊要求。

(P530) 

[規範] 

1. 要告訴任務工廠新建的任務可能長時間運行,使其能恰當地管理它;

2. 要儘量少用 TaskCreationOptions.LongRunning ;

(P536) 

用 async 關鍵字修飾的方法必須返回 Task 、 Task<T> 或 void 。

(P538) 

事實上, async 關鍵字最主要的作用有兩方面。其一,向閱讀代碼的人清楚說明,它所修飾的方法將自動由編譯器重寫;其二,告訴編譯器,方法中的上線問關鍵字 await 要被視爲異步控制流,不能當成普通的標識符。

(P541) 

async 方法的另一個重要特點是要求提供取消機制。

(P542) 

通常, await 關鍵字後面的表達式是 Task 類型或 Task<T> 類型。

從語法的角度看,作用於 Task 類型的 await 相當於返回 void 的表達式。

(P549) 

[規範] 

1. 要在很容易將一個計算分解成大量相互獨立的、處理器受限的小計算,而且這些小計算能在任何線程上以任何順序執行時,使用並行循環;

【第19章】 

(P563) 

爲了同步多個線程,阻止它們同時執行特定的代碼段,需要用監視器 (monitor) 阻止第二個線程進入受保護的代碼段,直到第一個線程退出那個代碼段。監視器功能由 System.Threading.Monitor 類提供。爲了標識受保護代碼段的開始和結束位置,需要分別調用靜態方法 Monitor.Enter() 和 Monitor.Exit() 。

要記住的一個重點是,在 Monitor.Enter() 和 Monitor.Exit() 這兩個調用之間,所有代碼都要用一個 try / finally 塊包圍起來。

(P566) 

同步是以犧牲性能爲代價的。

無論是使用 lock 關鍵字,還是顯式使用 Monitor 類,都必須小心地選擇 lock 對象。

同步對象不能是值類型,這一點很重要。

(P567) 

[規範] 

1. 避免鎖定 this 、 typeof() 或者字符串;

2. 要爲同步目標聲明 object 類型的一個單獨的只讀同步變量;

[規範] 

1. 避免使用 MethodImplAttribute 同步;

(P571) 

lock 關鍵字 (通過底層的 Monitor 類) 生成的代碼是可重入的。

[規範] 

1. 不要以不同的順序請求對相同兩個或更多同步目標的排他所有權;

2. 要確保同時持有多個鎖的代碼總是以相同的順序獲得這些鎖;

3. 要將可變的靜態數據封裝到具有同步邏輯的公共 API 中;

4. 避免對不大於本機 (指針大小) 整數的值的簡單讀寫操作進行同步,這種操作本來就是原子性的;

System.Threading.Mutex 在概念上和 System.Threading.Monitor 類幾乎完全一致 (沒有 Pulse() 方法支持) ,只是 lock 關鍵字用的不是它,而且可以命名不同的 Mutex 來支持多個進程之間的同步。可用 Mutex 類同步對文件或者其他跨進程資源的訪問。由於 Mutex 是一個跨進程資源,所以 .NET 2.0 開始允許通過一個 System.Security.AsscessControl.MutexSecurity 對象設置訪問控制。

Mutex 類的一個用處是限制應用程序不能同時運行多個實例。

【第20章】 

(P583) 

extern 方法永遠不包含任何主體,而且幾乎總是靜態方法。是由附加在方法聲明之前的 DllImport 特性 (而不是方法主體) 指向實現。該特性至少需要定義了函數的 DLL 的名稱。 “運行時” 根據方法名來判斷函數名。也可以用 EntryPoint 命名參數來重寫此默認行爲,明確地提供一個函數名。

(P588) 

[規範] 

1. 要圍繞非託管方法創建公共託管包裝器;這種非託管方法使用了託管代碼約定,比如結構化的異常處理;

(P592) 

[規範] 

1. 不要無謂地重複現有的、能執行非託管 API 功能的託管類;

2. 要將外部方法聲明爲私有或內部;

3. 要提供使用了託管約定的公共包裝器方法,包括結構化的異常處理、爲特殊值使用枚舉等;

4. 要爲不必要的參數選擇默認值來簡化包裝器方法;

5. 要用 SetLastErrorAttribute 將使用 SetlastError 錯誤碼的 API 轉換成引發異常 Win32Exception 的方法;

6. 要擴展 SafeHandle 或實現 IDisposable 並創建終結器來確保非託管資源被高效率地地清理;

7. 要在非託管 API 需要函數指針的時候,使用和所需方法的簽名匹配的委託類型;

8. 要儘量使用 ref 參數而不是指針類型;

可將 unsafe 用作類型或者類型內部的特定成員的修飾符。

(P593) 

[注意] 

1. 必須向編譯器顯式指明要支持不安全的代碼;

(P594) 

C# 總是把 * 和數據類型放在一塊兒。

指針是一種全新的類型。有別於結構、枚舉和類,指針的終極基類不是 System.Object ,甚至不能轉換成 System.Object 。相反,它們能轉換成 System.IntPtr (能轉換成 System.Object) 。

(P596) 

棧是一種寶貴的資源,耗盡棧空間會造成程序崩潰。

【第21章】 

(P610) 

[注意] 

1. 程序集是可以版本化和安裝的最小單元。構成程序集的單獨模塊則不是最小單元;

(P611) 

[注意] 

1. CLI 的一個強大功能是支持多種語言。這就允許使用多種語言來編寫一個程序,並允許用一種語言寫的代碼訪問用另一種語言寫的庫;
 

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