基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用。同时,它减少了泛型类及泛型方法中的转型,确保了类型安全。委托本身是一种引用类型,它保存的也是托管堆中对象的引用,只不过这个引用比较特殊,它是对方法的引用。事件本身也是委托,它是委托组,C#中提供了关键字event来对事件进行特别区分。

  一旦我们开始编写稍微复杂的C#代码,就肯定离不开泛型、委托和事件。

![](https://img2018.cnblogs.com/blog/710776/201906/710776-20190613112138058-2026075746.png)

1.总是优先考虑泛型

泛型的优点是多方面的,无论是泛型类还是泛型方法都同时具备可重用性、类型安全和高效率等特性,这都是非泛型类和非泛型方法无法具备的

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,确信有必要之后才这样做。

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》也可以看看!

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

C#规范整理·泛型委托事件的更多相关文章

  1. C# 《编写高质量代码改善建议》整理&笔记 --(三)泛型&委托&事件

    1.泛型 基于泛型,我们可以将类型参数化,以便更大范围地进行代码复用.同时,它减少了泛型类及泛型方法中的转型, 确保类型安全. 1)总是优先考虑泛型 优点:可重用性,类型安全,高效率. 2)避免在泛型 ...

  2. C#规范整理·资源管理和序列化

    资源管理(尤其是内存回收)曾经是程序员的噩梦,不过在.NET平台上这个噩梦似乎已经不复存在.CLR在后台为垃圾回收做了很多事情,使得我们现在谈起在.NET上进行开发时,都会说还是new一个对象吧!回收 ...

  3. 委托-异步调用-泛型委托-匿名方法-Lambda表达式-事件【转】

    1. 委托 From: http://www.cnblogs.com/daxnet/archive/2008/11/08/1687014.html 类是对象的抽象,而委托则可以看成是函数的抽象.一个委 ...

  4. 第十节:委托和事件(2)(泛型委托、Func和Action、事件及与委托的比较)

    一. 泛型委托 所谓的泛型委托,即自定义委托的参数可以用泛型约束,同时内置委托Func和Action本身就是泛型委托. 将上一个章节中的Calculator类中的方法用自定义泛型委托重新实现一下. p ...

  5. 整理之DOM事件阶段、冒泡与捕获、事件委托、ie事件和dom模型事件、鼠标事件

    整理之DOM事件阶段 本文主要解决的问题: 事件流 DOM事件流的三个阶段 先理解流的概念 在现今的JavaScript中随处可见.比如说React中的单向数据流,Node中的流,又或是今天本文所讲的 ...

  6. 委托、泛型委托、多播委托、匿名函数、lamda表达式、事件

    1.为什么要使用委托 将一个方法作为参数传递给另一个方法 2.委托概念 public delegate int 委托名(int a, int b); 声明一个委托类型,可以用访问修饰符修饰,deleg ...

  7. .NET代码编写规范 整理

    .NET代码编写规范 整理 .NET代码编写规范 - [ASP.NET] 2009-02-26 | Tag: 版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明 http://lensp ...

  8. 我自己总结的C#开发命名规范整理了一份

    我自己总结的C#开发命名规范整理了一份 标签: 开发规范文档标准语言 2014-06-27 22:58 3165人阅读 评论(1) 收藏 举报  分类: C#(39)  版权声明:本文为博主原创文章, ...

  9. C#规范整理·集合和Linq

    LINQ(Language Integrated Query,语言集成查询)提供了类似于SQL的语法,能对集合进行遍历.筛选和投影.一旦掌握了LINQ,你就会发现在开发中再也离不开它.   开始! 前 ...

随机推荐

  1. Asp.net Mvc 页面静态化

    http://www.cnblogs.com/gowhy/archive/2013/01/01/2841472.html

  2. CSU-1908 The Big Escape

    CSU-1908 The Big Escape Description There is a tree-like prison. Expect the root node, each node has ...

  3. 课堂笔记II

  4. 3.创建应用服务(Application Services)

    在Application项目中定义应用服务.首先定义Task的应用服务层的接口:

  5. Python的生成器Generator小结

    一. 生成器的介绍 在介绍生成器(Generator)之前,我们首先需要熟悉列表生成式,列表生成式是Python内置的简单又强大的用来创建列表的生成式. 举个例子, 如果我们想生成[1*1,2*2,3 ...

  6. ZigBee学习二 LED点对点通信

    ZigBee学习二 LED点对点通信 终端上电后,自动连接协调器进行组网,成功后,开始发送LED字符数据.当协调器接收到数据后,终端和协调器的LED1都开始闪烁. 工程搭建和文件添加 步骤这里就省了. ...

  7. hdu 1158 dp

    /* 题目大意:给n个月工作需要的人数,雇佣一个需要花hire 每个月的薪水是salary,解雇一个需要fire 求完成所有工作的最小费用 dp(i,j)表示第i个月雇佣j员工的最小费用 */ #in ...

  8. [AGC004E] Salvage Robots (DP)

    Description 蛤蟆国的领土我们可以抽象为H*W的笼子,在这片蛤土上,有若干个机器人和一个出口,其余都是空地,每次蛤蟆会要求让所有的机器人向某个方向移动一步,当机器人移动到出口时会被蛤蟆活摘出 ...

  9. [Oracle] Redo&Undo梳理

    Oracle Redo&undo Oracle中的redo和undo是关键技术的核心, 诸如实例恢复, 介质恢复, DataGuard, 闪回机制等都是给予redo和undo的, 所以很有必要 ...

  10. Python 多核 多线程 调度

    参考: http://www.oschina.net/translate/pythons-hardest-problem https://news.ycombinator.com/item?id=58 ...