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

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

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

1.總是優先考慮泛型

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

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

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

class MyList
{    
    static int count;   
   public static int Func<T>()   
   {       
       return count++;  
  }}
 輸出 0 ;1;2

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值,顯然,這會導致下面的程式碼編譯出錯:

public T Func<T>()
{    
    T t=null;    
    T t=0;    
    return t;
}

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

public T Func<T>()
{   
  T t=default(T); 
   return t;
}

5.使用FCL中的委託宣告

  • 要注意FCL中存在三類這樣的委託宣告,它們分別是:Action、Func、Predicate。尤其是在它們的泛型版本出來以後,已經能夠滿足我們在實際編碼過程中的大部分需要。下面是這三類委託宣告的簡要描述。
  • 我們應該習慣在程式碼中使用這類委託來代替自己的委託宣告。
  • 除了Action、Func和Predicate外,FCL中還有用於表示特殊含義的委託宣告。
//如用於表示註冊事件方法的委託宣告:
public delegate void EventHandler(object sender,EventArgs e);
public delegate void EventHandler<TEventArgs>(object sender,TEventArgs e);

//表示執行緒方法的委託宣告:
public delegate void ThreadStart();
public delegate void ParameterizedThreadStart(object obj);

//表示非同步回撥的委託宣告:
public delegate void AsyncCallback(IAsyncResult ar);

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

6.使用Lambda表示式代替方法和匿名方法

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

7.小心閉包中的陷阱

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

static void Main(string[]args)
{    

    List<Action>lists=new List<Action>();   
    for(int i=0;i<5;i++)   
 {       
     Action t=()=>    
     {           
     Console.WriteLine(i.ToString());     
      };       
     lists.Add(t);   
 }    

    foreach(Action t in lists)   
     {    
        t();   
     }
}

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

static void Main(string[]args)
{   
   List<Action>lists=new List<Action>();
  TempClass tempClass=new TempClass(); 
  for(tempClass.i=0;tempClass.i<5;tempClass.i++) 
   {       
     Action t=tempClass.TempFuc;    
     lists.Add(t);  
  }   
 foreach(Action t in lists)  
  {      
        t(); 
   }
}
class TempClass
{    
    public int i;  
    public void TempFuc()   
 {        
    Console.WriteLine(i.ToString());  
  }
}

這段程式碼所演示的就是閉包物件。所謂閉包物件,指的是上面這種情形中的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特別宣告,不然這段程式碼只會編譯失敗。比如下例
    例如:
class Program{  
  static void Main(string[]args) 
   {       
        ISalary<Programmer>s=new BaseSalaryCounter<Programmer>();   
        PrintSalary(s);    
}    

static void PrintSalary(ISalary<Employee>s)
    { 
       s.Pay(); 
   }
}

interface ISalary<T>
{   
 void Pay();
}

class BaseSalaryCounter<T>:ISalary<T>
{  

  public void Pay() 
   {        
Console.WriteLine("Pay base salary"); 
   }

}

class Employee
{   
 public string Name{get;set;}
}

class Programmer:Employee{}

class Manager:Employee{}

報錯: 無法從“ConsoleApplication4.ISalary<ConsoleApplication4.Programmer>”轉換為“ConsoleApplication4.ISalary<ConsoleApplication4.Employee>”
要讓PrintSalary完成需求,我們可以使用泛型型別引數:

static void PrintSalary<T>(ISalary<T>s)
{  
 s.Pay();
}

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

interface ISalary<out T>
{    
void Pay(T t);
}

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

12.讓介面中的泛型引數支援協變

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

static void Main(string[]args)   
 {       
     ISalary<Programmer>s=new BaseSalaryCounter<Programmer>(); 
     ISalary<Manager>t=new BaseSalaryCounter<Manager>();   
     PrintSalary(s);      
     PrintSalary(t);
}

 static void PrintSalary(ISalary<Employee>s)//用法正確 
   {        
       s.Pay();   
   }
}

interface ISalary<out T>  //使用了out關鍵字
{    
     void Pay();
}

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

13.理解委託中的協變

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

public delegate T GetEmployeeHanlder<T>(string name);

static void Main(){
     GetEmployeeHanlder<Employee>getAEmployee=GetAManager;  
     Employee e=getAEmployee("Mike");
}

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

GetEmployeeHanlder<Manager>getAManager=GetAManager;GetEmployeeHanlder<Employee>getAEmployee=getAManager;
static Manager GetAManager(string name)
{  
    Console.WriteLine("我是經理:"+name); 
     return new Manager(){Name=name};
}

static Employee GetAEmployee(string name)
{ 
  Console.WriteLine("我是僱員:"+name);
  return new Employee(){Name=name};
}

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

public delegate T GetEmployeeHanlder<out T>(string name);

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

public delegate TResult Func<out TResult>()和
public delegate TOutput Converter<in TInput,out TOutput>(TInput input)

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

逆變是指方法的引數可以是委託或泛型介面的引數型別的基類。FCL 4.0中支援逆變的常用委託有:

Func<in T,out TResult>
Predicate<in T>
//常用泛型介面有:
IComparer<in T>

舉例:

class Program
{    
static void Main()   
 {      
  Programmer p=new Programmer{Name="Mike"};     
  Manager m=new Manager{Name="Steve"};        
  Test(p,m);    
}   

static void Test<T>(IMyComparable<T>t1,T t2)   
 {        //省略    }}

public interface IMyComparable<in T>
{    
    int Compare(T other);
}

public class Employee:IMyComparable<Employee>
{    
public string Name{get;set;}    
public int Compare(Employee other)   
 {       
 return Name.CompareTo(other.Name);  
  }
}

public class Programmer:Employee,IMyComparable<Programmer>
{    
public int Compare(Programmer other)  
  {       
 return Name.CompareTo(other.Name);  
  }
}

public class Manager:Employee{
}

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

總結

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

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

相關文章