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

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

1.老是優先考慮泛型

泛型的優勢是多方面的,不管是泛型類仍是泛型方法都同時具有可重用性、類型安全和高效率等特性,這都是非泛型類和非泛型方法沒法具有的html

2.避免在泛型類型中聲明靜態成員

  1. 實際上,隨着你爲T指定不一樣的數據類型,MyList<T>相應地也變成了不一樣的數據類型,在它們之間是不共享靜態成員的。
  2. 可是若T所指定的數據類型是一致的,那麼兩個泛型對象間仍是能夠共享靜態成員的,如局部的List 和List 的變量。可是,爲了規避所以而引發的混淆,仍舊建議在實際的編碼工做中,儘可能避免聲明泛型類型的靜態成員。
    非泛型類型中的泛型方法並不會在運行時的本地代碼中生成不一樣的類型。
    例如:
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.爲泛型參數設定約束

在編碼過程當中,應該始終考慮爲泛型參數設定約束。約束使泛型參數成爲一個實實在在的「對象」,讓它具備了咱們想要的行爲和屬性,而不只僅是一個object。算法

指定約束示例:安全

  • 指定參數是值類型。(除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,確信有必要以後才這樣作。code

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

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

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》也能夠看看!

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

相關文章
相關標籤/搜索