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

深刻理解协变和逆变传送门《逆变与协变详解

相关文章
相关标签/搜索