C# 中的委托(上)
前言
在 C#
中,有一种特殊的类型:委托。单看这个名字,相信聪明的读者们就已经大概已经知道委托可能是拿来做什么的了 —— 通过某种方式“告诉”这个受委托的对象,然后这个对象会代我们去“做”一些事情。是的,对于委托来说,要“做”的事情就是去调用函数,而决定委托“去不去做”这些事情的因素掌握在我们(用户定义的程序逻辑)手中。
当然,就这么单说可能还是非常不直观,完全无法体现委托在 C#
中的作用。因此在接下来的内容中,将简略地介绍 C#
中委托的定义与使用。
C#
中委托的定义
在 C#
中,我们使用 delegate
关键字来定义一个委托字段,定义的格式如下:
[修饰符] delegate [函数的返回值类型] [委托的名称]([函数的参数])
比如说:
public delegate int TwoIntegersOperation(int x, int y);
又比如说:
internal delegate void StringInfoOperation(string info);
乍一看,这和函数的定义长得好像,无非就是一个函数的返回值类型前多了个 delegate
嘛。所以,从委托定义的语法就能看出它与函数之间的关系密切。但是,需要注意的是,委托是一个引用类型,它派生自 System.MulticastDelegate
类型(而 System.MulticastDelegate
类型又派生自 System.Delegate
类型),所以它的使用与行为应该和类的使用类似。
委托的使用
在 C#
中使用委托进行函数调用
假设我们定义了一个名为 TwoIntegersOperation
的委托,定义如下:
public delegate int TwoIntegersOperation(int x, int y);
前面也提到了,委托是引用类型,所以使用方法与类相似,所以接下来我们就在 Main
函数中像类那样实例化一个委托对象吧:
TwoIntegersOperation opt = new TwoIntegersOperation();
好像不对,怎么编译器报错了,提示“‘Program.TwoIntegersOperation’不包含采用 0 个参数的构造函数(CS1729)”:
这里就是一个很有意思的点了,我们需要为 TwoIntegersOperation
的构造函数中填入一个参数。我们填入什么呢,填入一个函数。
看到这里,学过 C/C++
的读者肯定恍然大悟:这不相当于 C/C++
中的函数指针,只要函数签名匹配,那么传入函数名,指针就能指向函数的地址并实现调用了。是,但又不完全是,但就目前而言,委托的的感觉的确与函数指针很像。
所以,我们定义一个与 TwoIntegersOperation
委托“长得很像”的静态函数,就叫 Add
吧:
static int Add(int a, int b)
{
return a + b;
}
将 Add
作为参数填写在 TwoIntegersOperation
的构造函数中:
TwoIntegersOperation opt = new TwoIntegersOperation(Add);
至此,我们已经成功声明了一个委托,并为该委托指定了对应执行的函数。
接下来,我们就可以像使用函数一样使用委托对象 opt
了:
int result = opt(10, 15);
Console.WriteLine(result); // Output: 25
所以这就是 C#
中委托的使用,它就像 C/C++
中函数指针一样,……,所以本文就到这里,我们下一篇再见。
吗?
开玩笑,要是 C#
中的委托就这么短短 1000 字就能讲完,那笔者可就太愧对 C#
的语言设计者了。事实上,委托的设计在于它的多播性、简便性与安全性。我们先来看一下 C++
中函数指针长什么样:
复习 C++
中的函数指针
int (*twoIntFuncPtr)(int, int);
假设有这么一个 add
函数,它与 twoIntFuncPtr
匹配:
int add(int a, int b)
{
return a + b;
}
显然,我们可以为 twoIntFuncPtr
指定函数,在 main
函数中可以这样调用:
int main(int agrc, char* argv[])
{
twoIntFuncPtr = add;
int result = twoIntFuncPtr(10, 15);
std::cout << result << std::endl; // Output: 25
return 0;
}
从上例中我们发现,我们为 twoIntFuncPtr
指定了一个函数 add
,如果我们定义了另外一个函数 mul
用于执行两个 int
整数的乘法运算,同时想使用 twoIntFuncPtr
来进行调用来获取结果的话,就需要这样更改我们的代码了:
int mul(int a, int b)
{
return a * b;
}
int main(int agrc, char* argv[])
{
twoIntFuncPtr = mul; // 将给定的函数变更为 mul
int result = twoIntFuncPtr(10, 15);
std::cout << result << std::endl; // Output: 150
return 0;
}
也就是说,若我们想要使用 twoIntFuncPtr
来指向多个函数调用并获取结果时,需要多次变更函数指针指向的函数。虽然看起来很合理和正常,但是对于需要同时调用许多不同函数的时候,就显得不那么优雅了。
再看 C#
中的多播委托
回到 C#
,那么在委托中,是否有一些优雅的方式,使得我们使用一个委托就能同时调用多个函数呢?还真有,它叫多播委托。实际上,在 .NET Core 或未来版本的 C#
中,所有我们使用 delegate
定义的委托都是多播委托。
假设我们需要同时调用 Add
和 Mul
方法,那么对于先前我们定义的 TwoIntegersOperation
委托来说可以这样操作:
public delegate int TwoIntegersOperation(int x, int y);
static void Add(int a, int b)
{
return a + b;
}
static void Mul(int a, int b)
{
return a * b;
}
static void Main(string[] args)
{
TwoIntegersOperation opt = new TwoIntegersOperation(Add);
opt += Mul;
}
发现了什么,我们居然使用了 +=
运算符来连接了一个委托对象和函数。这看起来奇怪的语法实际上是很符合包括笔者在内的大部分 .NET 开发者的直觉的 —— 我们为一个委托对象新增了一个与 Add
函数连接的“通道”。那么既然是新增,那么就有减去,因此 -=
运算符和 +
运算符也可以用于委托对象:
// 将 Add 函数从调用列表中移除
opt -= Add;
// 假设 opt1 与 opt2 分别是两个委托,opt 则是集合了 opt1 和 opt2 的所有调用函数的委托
opt = opt1 + opt2
委托的注意事项
这么看来,委托的确是比函数指针强大,它的多播性决定了委托并不是函数指针在 C#
上的简单翻版。但是在使用委托的时候,需要注意几点:
- 若我们将委托的调用函数全部减光,在调用的时候就会抛出异常,导致程序崩溃:
解决方法:使用委托对象的
Invoke
方法,结合?
运算符与空合并运算符来避免出现空引用,比如在本例的Main
函数中,可以这样使用:
static void Main(string[] args)
{
TwoIntegersOperation opt = new TwoIntegersOperation(Add);
opt -= Add;
int result = opt?.Invoke(10, 20) ?? 0;
Console.WriteLine(result);
}
当然,若不想使用这么新的语法特性,也可以上述语句修改等价于:
static void Main(string[] args)
{
TwoIntegersOperation opt = new TwoIntegersOperation(Add);
opt -= Add;
int result;
if (opt != null)
{
result = opt.Invoke(10, 20);
}
else
{
result = 0;
}
Console.WriteLine(result);
}
- 使用
+=
、+
运算符添加的函数依然是存在调用顺序的,若先添加Add
,后添加Mul
,那么Add
会先于Mul
调用:
- 若为委托添加了多个函数,而在执行到某个函数时抛出了异常,那么在这个函数后面的函数不会被执行。
比如说在如下代码中,为 opt
添加了 Add
、Bad
和 Mul
函数,但是 Bad
函数在执行过程中抛出了异常,那么 Mul
函数就不会被调用:
哪怕是使用了 try...catch
语句对异常进行捕捉,依然不会执行后面的语句,除非抛出异常的函数是最后一个才被加入委托的:
总结
本文非常简略地介绍了 C#
中的委托是什么,它的定义方法以及最简单的使用方法。由于 C#
中的委托的应用实际上是十分广泛且深入语言各个层面的,同时 .NET 官方为我们已经定义了一系列的内置委托,并且委托实际上与匿名方法还有事件关系密切。因此。碍于篇幅,本文仅能粗略地介绍一些委托的基础知识,在未来,将会在文章“C# 中的委托(下)”中介绍 .NET 的内置委托和“与其对应”的匿名函数,及 Lambda 表达式。若各位对这一节内容还有任何疑问或意见,欢迎在下方评论或提出。