歡迎光臨
每天分享高質量文章

C#規範整理·泛型委托事件

基於泛型,我們得以將型別引數化,以便更大範圍地進行代碼復用。同時,它減少了泛型類及泛型方法中的轉型,確保了型別安全。委托本身是一種取用型別,它儲存的也是托管堆中物件的取用,只不過這個取用比較特殊,它是對方法的取用。事件本身也是委托,它是委托組,C#中提供了關鍵字event來對事件進行特別區分。
一旦我們開始編寫稍微複雜的C#代碼,就肯定離不開泛型、委托和事件。

1.總是優先考慮泛型

泛型的優點是多方面的,無論是泛型類還是泛型方法都同時具備可重用性、型別安全和高效率等特性,這都是非泛型類和非泛型方法無法具備的

2.避免在泛型型別中宣告靜態成員

  1. 實際上,隨著你為T指定不同的資料型別,MyList<T>相應地也變成了不同的資料型別,在它們之間是不共享靜態成員的。
  2. 但是若T所指定的資料型別是一致的,那麼兩個泛型物件間還是可以共享靜態成員的,如上文的list1和list2。但是,為了規避因此而引起的混淆,仍舊建議在實際的編碼工作中,儘量避免宣告泛型型別的靜態成員。
    非泛型型別中的泛型方法並不會在運行時的本地代碼中生成不同的型別。
    例如:

     


3.為泛型引數設定約束

在編碼過程中,應該始終考慮為泛型引數設定約束。約束使泛型引數成為一個實實在在的“物件”,讓它具有了我們想要的行為和屬性,而不僅僅是一個ob-ject。

指定約束示例:

  • 指定引數是值型別。(除Nullable外) where T:struct
  • 指定引數是取用型別 。 where T:class
  • 指定引數具有無引數的公共構造方法。 where T:new()

    註意,CLR目前只支持無參構造方法約束。

  • 指定引數必須是指定的基類,或者派生自指定的基類。
  • 指定引數必須是指定的接口,或者實現指定的接口。
  • 指定T提供的型別引數必須是為U提供的引數,或者派生自為U提供的引數。 where T:U
  • 可以對同一型別的引數應用多個約束,並且約束自身可以是泛型型別。

4.使用default為泛型型別變數指定初始值

有些演算法,比如泛型集合List<T>的Find演算法,所查找的物件有可能會是值型別,也有可能是取用型別。在這種演算法內部,我們常常會為這些值型別變數或取用型別變數指定預設值。於是,問題來了:值型別變數的預設初始值是0值,而取用型別變數的預設初始值是null值,顯然,這會導致下麵的代碼編譯出錯:


代碼”T t=null;”在Visual Studio編譯器中會警示:錯誤1不能將Null轉換為型別形參“T”,因為它可能是不可以為null值的型別。請考慮改用“default(T)”.
代碼”T t=0;”會警示:錯誤1無法將型別“int”隱式轉換為“T”。
改進


5.使用FCL中的委托宣告

  • 要註意FCL中存在三類這樣的委托宣告,它們分別是:Action、Func、Predicate。尤其是在它們的泛型版本出來以後,已經能夠滿足我們在實際編碼過程中的大部分需要。下麵是這三類委托宣告的簡要描述。
  • 我們應該習慣在代碼中使用這類委托來代替自己的委托宣告。
  • 除了Action、Func和Predicate外,FCL中還有用於表示特殊含義的委托宣告。


在FCL中每一類委托宣告都代表一類特殊的用途,雖然可以使用自己的委托宣告來代替,但是這樣做不僅沒有必要,而且會讓代碼失去簡潔性和標準性。在我們實現自己的委托宣告前,應該首先查看MSDN,確信有必要之後才這樣做。

6.使用Lambda運算式代替方法和匿名方法

在實際的編碼工作中熟練運用它,避免寫出煩瑣且不美觀的代碼。

7.小心閉包中的陷阱

如果匿名方法(Lambda運算式)取用了某個區域性變數,編譯器就會自動將該取用提升到該閉包物件中,即將for迴圈中的變數i 修改成了取用閉包物件(編譯器自動創建)的公共變數i。


以上結果全部輸出5;
另外一種實現方式;

Copy


這段代碼所演示的就是閉包物件。所謂閉包物件,指的是上面這種情形中的TempClass物件(在第一段代碼中,也就是編譯器為我們生成的“<>c__DisplayClass2”物件)。如果匿名方法(Lambda運算式)取用了某個區域性變數,編譯器就會自動將該取用提升到該閉包物件中,即將for迴圈中的變數i修改成了取用閉包物件的公共變數i。這樣一來,即使代碼執行後離開了原區域性變數i的作用域(如for迴圈),包含該閉包物件的作用域也還存在。理解了這一點,就能理解代碼的輸出了。

8.瞭解委托的本質

理解C#中的委托需要把握兩個要點:

  1. 委托是方法指標。
  2. 委托是一個類,當對其進行實體化的時候,要將取用方法作為它的構造方法的引數。

9.使用event關鍵字為委托施加保護

首先沒有event加持的委托,我們可以對它隨時進行修改賦值,以至於一個方法改動了另一個方法的委托鏈取用,比如賦值為null,另外一個方法中呼叫的時候將丟擲異常。
如果有event加持的時候,我們修改的時候,比如

fl.FileUploaded=null;
fl.FileUploaded=Progress;
fl.FileUploaded(10);

以上代碼編譯會出現錯誤警告:
事件 “ConsoleApplication1.FileUploader.FileUploaded ”
只能出現在+=或-=的左邊(從型別“ConsoleApplication1.FileUploader”中使用時除外)

10.實現標準的事件模型

有了上面的event加持,但是還不能夠規範。
EventHandler的原型宣告:

public delegate void EventHandler(object sender,EventArgs e);

微軟為事件模型設定的幾個規範:

  • 委托型別的名稱以EventHandler結束;
  • 委托原型傳回值為void;
  • 委托原型具有兩個引數:sender表示事件觸發者,e表示事件引數;
  • 事件引數的名稱以EventArgs結束。

11.使用泛型引數兼容泛型接口的不可變性

  • 讓傳回值型別傳回比宣告的型別派生程度更大的型別,就是“協變”。
  • 編譯器對於接口和委托型別引數的檢查是非常嚴格的,除非用關鍵字out特別宣告,不然這段代碼只會編譯失敗。比如下例
    例如:

報錯: 無法從“ConsoleApplication4.ISalary”轉換為“ConsoleApplication4.ISalary”

要讓PrintSalary完成需求,我們可以使用泛型型別引數:

static void PrintSalary(ISalarys)
{
s.Pay();
}

實際上,只要泛型型別引數在一個接口宣告中不被用來作為方法的輸入引數,我們都可姑且把它看成是“傳回值”型別的。所以,泛型型別引數這種樣式是滿足“協變”的定義的。但是,只要將T作為輸入引數,便不滿足“協變”的定義了。如:

interface ISalary
{
void Pay(T t);
}

編譯會提示:差異無效:型別引數“T”必須是在“ISalary.Pay(T)”上有效的逆變式。“T”為協變。

12.讓接口中的泛型引數支持協變

除了11中提到的使用泛型引數兼容泛型接口的不可變性外,還有一種辦法就是為接口中的泛型宣告加上out關鍵字來支持協變。
out關鍵字是FCL 4.0中新增的功能,它可以在泛型接口和委托中使用,用來讓型別引數支持協變性。通過協變,可以使用比宣告的引數派生型別更大的引數。通過下麵例子我們應該能理解這種應用。
比如:

Copy


FCL 4.0對多個接口進行了修改以支持協變,如IEnumerable<out T>、IEnumerator<out T>、IQuerable<out T>等。由於IEnumerable<out T>現在支持協變,所以上段代碼在FCL 4.0中能運行得很好。
在我們自己的代碼中,如果要編寫泛型接口,除非確定該接口中的泛型引數不涉及變體,否則都建議加上out關鍵字。協變增大了接口的使用範圍,而且幾乎不會帶來什麼副作用。

13.理解委托中的協變

委托中的泛型變數天然是部分支持協變的。
比如:


因為存在下麵這樣一種情況,所以編譯通不過:

要讓上面的代碼編譯通過,同樣需要為委托中的泛型引數指定out關鍵字:

public delegate T GetEmployeeHanlder(string name);

 

FCL 4.0中的一些委托宣告已經用out關鍵字來讓委托支持協變了,如我們常常會使用到的:

public delegate TResult Func()和
public delegate TOutput Converter(TInput input)

14.為泛型型別引數指定逆變

逆變是指方法的引數可以是委托或泛型接口的引數型別的基類。FCL 4.0中支持逆變的常用委托有:

Func
Predicate
//常用泛型接口有:
IComparer

舉例:

在上面的這個例子中,如果不為接口IMy-Comparable的泛型引數T指定in關鍵字,將會導致Test(p, m)編譯錯誤。由於引入了接口的逆變性,這讓方法Test支持了更多的應用場景。在FCL4.0之後版本的實際編碼中應該始終註意這一點。

總結

如有需要, 上一篇的《C#規範整理·集合和Linq》也可以看看!

深入理解協變和逆變傳送門《逆變與協變詳解》

赞(0)

分享創造快樂