《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 的一個強大功能是支持多種語言,這就允許使用多種語言來撰寫一個程式,并允許用一種語言寫的代碼訪問用另一種語言寫的庫;
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/73212.html
標籤:C#
