【本系列是我的 C# in Depth 第四版讀書筆記,背景故事在這裡

書的第二部分來到 C# 2-5,從現在的角度,這些已經老到不能再老,每個 C# 開發者幾乎都能信手拈來的基本技巧,不過閱讀過程仍有不少驚喜,學到一些過去沒注意的細節。

Generic 泛型

  • 沒寫過 C# 1.0/1.1 的人,應該很難想像在沒有泛型怎麼過日子,說泛型是 C# 2 最革命性的創新一點也不為過
  • C# 1.x 時代只有固定大小的 Array,只能放 object 無法檢核型別的 ArrayList、Hashtable 集合,或是只適用特定型別的 StringCollection 等集合,各有缺點
  • List<T> 帶來戲劇化改變,可動態增減元素、沒有 object 轉型別的程序及損耗、有強型別檢查,還一體適用所有型別,豈不美哉?
  • MethodName<T>(T arg) 也是高度共用程式碼並兼顧強型別的泛型應用
  • Generic Arity 指泛型型別參數的個數,同方法名稱配不同 Generic Arity 視為不同方法,但型別參數名稱不能做為區別,例如第四個會視跟第二筆重複:
public void Method() {} //Generic Arity = 0
public void Method<T>() {} //1
public void Method<T1, T2>() {} //2
public void Method<TAnotherName>() {} //錯誤,與 Method<T> 重複
public void Method<T3, T3>() {} //錯誤,型別參數名稱相同
  • 除了 enum 外,class、struct、interface、delegate 都可以加泛型
  • Type Inference 讓編譯器由傳入參數型別推斷 T,但無法由傳回參數型別逆推。(寫程式有發現過這樣也行,現在才知術語跟原理)
    這段有點複雜,有些邏輯要看語言規格書,但也不用太擔心型別判斷錯,編譯若出錯再找問題,型別不對加 (typeName) 強轉型(如:(object)"x")多半能解決
public static List<T> CopyAtMost<T>(List<T> input, int maxElements) //...

var numbers = new List<int>();
var x = CopyAtMost<int>(numbers, 2); //標準寫法
var y = CopyAtMost(numbers, 2); //不提供 <int>,用 List<int> 去推斷 T = int
//  Tuple 應用
new Tuple<int, string, int>(10, "x", 20);
// 靜態方法
Tuple.Create<int, string, int>(10, "x", 20);
// Type Inference
Tuple.Create(10, "x", 20)
  • Type Constraint 限制泛型可接受型別,class = 參考型別(Reference Type)、struct = 值型別(Value Type)、new() 有無參數建構式,若提供特定型別,寫法如下:
static void PrintItems<T>(List<T> items) where T : IFormattable
public void Sort(List<T> items) where T : IComparable<T>
TResult Method<TArg, TResult>(TArg input) where TArg : IComparable<TArg> where TResult : class, new()
  • 泛型取預設值 default(T)
  • 當 typeof 遇到泛型,List<T> 的 T 會置入實際型別,得到類似 System.Collections.Generic.List1\[System.String\] 的結果 List\1 代表 Generic Arity 為 1,有一個型別參數,[string] 表示。同理,Dictionary 應為 Dictionary`2:
    List<T> 中,T 稱為 Underlying Type
  • typeof(List<>)、typeof(Dictionary<,>)、typeof(Tuple<,,>) 取得泛型型別定義
  • 重要觀念,泛型型別會在每次使用新的 T 型別執行靜態建構式並擁有一組專屬靜態欄位,可想像成產生一個新型別。例如:
class GenericCounter<T> {
  private static int value;
  static GenericCounter() {
    //第一次存取 GenericCounter<int> 及 GenericCounter<string> 都會觸發
    //GenericCounter<int> 與 GenericCounter<string> 各有自己的 value,不會打架
  }
}
  • 若有多個型組參數,各種組合都有一份,例如下列組合,每個都是不同型別:
GenericType<int, string> 
GenericType<string, int>
Outter<string>.Inner<string>
Outter<string>.Inner<int>
Outter<int>.Inner<int>
Outter<int>.Inner<string>

Nullable Value Type

  • Tony Hoare 1965 在 Algol 語言中加入 Null Reference 的概念,號稱是個價值十億美元的錯誤。從此 NullReference (.NET)、NullPointerException (Java) 成為開發人員的惡夢。
  • 在 .NET 1.x 時代,Value Type 要賦與 Null 意義有兩種做法:使用某個極端值(如 int.MinValue、DateTime.MinValue)或加設一個 bool 旗標註明是否有值;但這需要在所有引用處加入檢查,否則一旦無效值被當成有意義資料混入流程(Silent Failure),常會造成災難。
  • Nullable<T> 實現了強制檢查,可用於 Primitive 型別(int、double, bool...)、列舉、系統內建 Struct 及自訂 Struct
    不能用在 string、int[]、Nullable<int>
  • 進步不只來自更容易寫出正確的程式碼,也在於很難寫出不對的程式碼或讓後果不要那麼嚴重,Nullable<T>是經典例子
  • Nullable<T> 方法:GetValueOrDefault()、GetValueOrDefault(T defaultValue)、Equal(object)、GetHashCode()、從 T 轉為 Nullable<T> 的隱含轉型(Implicit Conversion,保證成功)、Nullable<T> 轉為 T 的明確轉型(Explicit Conversion,當值為 null 時會抛 InvalidOperationException)
  • Nullable<T> Boxing (轉型為 object) 行為,若 HasValue == false,得到 null;若 HasValue == true,得到 Boxed T
  • Nullable<S> 轉 Nullable<T> 為隱含或明確轉型,依 S、T 決定;S 轉 Nullable<T> 為隱含或明確轉型,依 S、T 決定;Nullable<S> 轉 T 一律為明確轉換
  • C# 語言支援:
//int? 等同 Nullable<int>
//以下寫法意義相同
int? x = null;
int? x = new int?(); 

if (x!= null)...
if (x.HasValue)...
  • Nullable 運算子考量 null 處理稱為 Lifting,+、++、-、--、!、+、-、*、/、%、|、、<<、>>、==、!=、<、>、<=、>= 各有處理原則,Nullable<bool> 的 &、|、、! 遇到 null 的規則,書中有完整整理,但實務用到機會不多,有需要再查。
  • 以上處理由 Compiler 處理而非 Runtime,會在 IL 加入檢查及額外處理
  • 支援 as 運算子: object o; int? nullable = o as int?; 但效能不好,輸給 Cast 轉型跟 is
  • 術語 Null-Coalescing ?? 運算子,C# 2.0 加入。C# 6.0 再加入 ?.xxx (Null Condition Operator),建議節制使用,過度會影響可讀性。

委派建立簡化

  • Method Group - 一或多個相同名稱的方法,多載的方法是經典例子,如 void print(int x) 跟 void print(int x, int y)
  • C# 2.0 用 Method Group Conversion 簡化委派建立。延伸閱讀:Method Groups in C#
private void HandleButtonClick(sender object, EventArgs e) { ... }
EventHandler handler = new EventHandler(HandleButtonClick);
//簡化為
// Method Group Conversion: HandleButtonClick 是 Method Group,編譯器找 Signature 相同者自動轉為委派 
EventHandler handler = HandleButtonClick; 
//可寫成 += 加入 Method Group
button.Click += HandleButtonClick;
  • 用匿名方法簡化,省去宣告專屬實作具名方法
EventHandler handler = delegate(object sender, EventArgs e) { ... }
//如果用不到 sender 及 e
EventHandler handler = delegate { ... }
//應用 Closure
string message = "..."
button.Click += delegate 
{
  Console.WriteLine(message);
}
  • C# 2.0 增加 Singature 相容性
public delegate void Print(string message);
void PrintObject(object o) { ... }
new Print(PrintObject) // 2.0 OK,因為傳 string 可轉成 object,1.0 不行
//以下案例不行
public delegate void Int32Print(int x);
public delegate void Int64Print(long x);
public void PrintX(long x) { ... }
new Int32Print(PrintX) //不行,雖然 int 可陰含轉型 long,但這裡的行為並非 Generic Variance (未來再談)

My notes for C# in Depth part 2


Comments

# by Cash

//int? 等同 Nullable&lt;int&gt; <> 被 encoding 了

# by Jeffrey

to Cash, 感謝,已修正。

Post a comment


84 - 83 =