前言

关于委托,我一开始同样也是没打算写的,但是考虑到我要写 lambda表达式的相关内容,于是不得不先说明委托的内容。对于初学者来说,第一次见到委托应该是在 Win 的事件模型中吧,反正我是这样的,这部分将对委托做详细说明。

委托

委托这一特性存在的目的是封装目标代码。封装好的代码可以在应用程序中进行传递,并根据需要执行(要保证参数和返回值的类型安全)。在C# 1时代,委托基本上用于事件处理和启动线程。即使在 2005 年后的C# 2推出之后,这一状况也没有太大变化。直到 2008 年 LINQ 问世,C# 开发人员才开始适应这种把函数传来传去的编程方式。

C# 2提供了 3 种 创建委托实例的新方式,同时支持声明泛型委托,比如EventHandler<TEventArgs>Action<T>

方法组转换

所谓方法组,就是一个或者多个同名方法。可以说,我们在每天不知不觉中使用方法组,因为每调用一次方法就是对方法组的一次使用,代码示例:

1
Console.WriteLine("Hello,World");

表达式Console.WriteLine就是一个方法组。之后编译器会根据该方法的调用实参从方法组中选择合适的重载方法进行调用。除了方法调用,**C# 1还将方法组用于委托创建表达式,作为创建委托实例的唯一方法**。假设存在如下方法:

1
private void HandleButtonClick(object sender, EventArgs e) { }

可以创建EventHandler实例:

1
EventHandler eventHandler = new EventHandler(HandleButtonClick);

C# 2通过方法组转换简化了委托实例的创建过程:只需要委托的签名与方法组中任何一个重载兼容,该方法组就可以隐式的转换为该委托类型。如下采用方法签名完全一致的委托举例:

1
EventHandler eventHandler = HandleButtonClick;

事件的订阅和取消也可以采用同样的方式:

1
button.click += HandleButtonClick;

简化版的代码和使用委托创建表达式代码最终会生成相同的中间代码,唯一区别是前者更加简洁。方法组转换简化了开发人员创建委托实例的工作,而对于匿名方法特性这方面表现更佳。

匿名方法

这部分不会深入说明匿名方法,因为匿名方法的继承者lambda表达式才是主角。lambda表达式由 C# 3推出,如果lambda表达式先问世,则不会存在匿名函数。

使用匿名方法,无须在创建委托实例前预先编写另一个实体方法,只需在委托中创建内联代码即可。大体过程是:使用delegate关键词,添加实参列表(可选),然后在大括号内编写需要的代码。

例如在事件触发时向控制台输出消息,代码示例:

1
2
3
4
EventHandler eventHandler = delegate
{
Console.WriteLine("Hello,World");
};

当然,我们也可以传入相关参数,来打印sender和事件参数这些信息:

1
2
3
4
EventHandler eventHandler = delegate(object sender,EventArgs args)
{
Console.WriteLine("sender = {0},args={0}",sender.GetType(),args.GetType());
};

然而匿名方法的正在威力,要等它用作闭包(closure)时才能发挥出来。闭包能够访问其声明作用域内的所有变量,即使当委托执行时这些变量已经不可访问。后面说明 lambda表达式的时候会对闭包做详细说明,此处只需要参考如下代码示例:

1
2
3
4
5
void AddClickLogger(Control control,string message){
control.click += delegate{
Console.WriteLine("Control Clicked:{0}",message);
}
}

message作为AddClickLogger方法的参数,是可以被匿名方法“捕获”的。AddClickLogger方法本身并不执行匿名方法,它只是将匿名方法添加到Click事件。当匿名方法真正开始执行时,AddClickLogger方法本身已经执行完成返回了。那么方法参数为何还可以访问呢?简而言之,是由编译器完成了枯燥的代码生成工作。

委托的兼容性

C# 1中创建委托实例时,创建实例的方法与委托的返回值类型和参数类型(包括ref/out修饰符)必须完全一致。假设有如下委托声明和方法:

1
2
3
4
5
public delegate void Printer(string message);
public void PrintAnything(object obj)
{
Console.WriteLine(obj);
}

之后创建一个Printer委托实例来把PrintAnything方法封装起来。看似没有声明问题,Printer传入的参数肯定是string引用,而string类型可以通过一致性转换变为object类型的引用,但是C# 1不允许这种方式,因为二者参数类型不匹配。到了C# 2,就可以在创建委托表达式和方法组中进行上述转换了。

1
2
Printer p1 = new Printer(PrintAnything);
Printer p2 = PrintAnything;

此外,还可以使用委托来创建另外一个委托,条件是两者前面要兼容。现在假设还有一个和PrintAnything兼容的委托,代码示例:

1
public delegate void GeneralPrinter(object obj);

之后可以使用GeneralPrinter实例来继续创建Printer委托的实例:

1
2
GeneralPrinter generalPrinter = PrintAnything;
Printer p1 = new Printer(GeneralPrinter);

编译器之所以允许以上写法,因为Printer的任何合法参数都可以安全的用作GeneralPrinter的实参,返回值也同理。

不过有时候上述规则并不能如我们所愿。参数或者返回值之间的兼容性必须满足一致性转换规则,这样才能保证执行期变量值不变,如下所示代码就不能通过编译:

1
2
3
4
5
public delegate void Int32Printrt(int x);
public delegate void Int64Printrt(long x);

Int64Printrt int64Printrt = ....; //完成方法委托
Int32Printrt int32Printrt = new Int32Printrt(int64Printrt);

这是因为两个委托签名不兼容,尽管从intlong类型存在隐式类型转换,但是它没有满足一致性转换的要求。

End

委托中封装的本质是创建了一个新的实例,而不是把已有委托看作不同类型的实例。

方法组转换如今广泛应用,兼容性特性已经融入日常编码;匿名方法的使用频率大幅度降低,因为lambda表达式几乎取代了匿名方法的所有功能。