C# 中的委托(上)

C# 的语言设计精髓之一,并与 C/C++ 中的函数指针做对比

由 Anawaert 于 2025-03-15 发布   

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)”:

No Zero-Param ctor

  这里就是一个很有意思的点了,我们需要为 TwoIntegersOperation 的构造函数中填入一个参数。我们填入什么呢,填入一个函数

  看到这里,学过 C/C++ 的读者肯定恍然大悟:这不相当于 C/C++ 中的函数指针,只要函数签名匹配,那么传入函数名,指针就能指向函数的地址并实现调用了。是,但又不完全是,但就目前而言,委托的的感觉的确与函数指针很像。

  所以,我们定义一个与 TwoIntegersOperation 委托“长得很像”的静态函数,就叫 Add 吧:

static int Add(int a, int b)
{
    return a + b;
}

  将 Add 作为参数填写在 TwoIntegersOperation 的构造函数中:

TwoIntegersOperation opt = new TwoIntegersOperation(Add);

  至此,我们已经成功声明了一个委托,并为该委托指定了对应执行的函数。

ctor With Add Function

  接下来,我们就可以像使用函数一样使用委托对象 opt 了:

int result = opt(10, 15);
Console.WriteLine(result);  // Output: 25

Got Added Res

  所以这就是 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 定义的委托都是多播委托。

  假设我们需要同时调用 AddMul 方法,那么对于先前我们定义的 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# 上的简单翻版。但是在使用委托的时候,需要注意几点:

  • 若我们将委托的调用函数全部减光,在调用的时候就会抛出异常,导致程序崩溃: Null Ref When No Func 解决方法:使用委托对象的 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 调用:

Call Multi Function

  • 若为委托添加了多个函数,而在执行到某个函数时抛出了异常,那么在这个函数后面的函数不会被执行。

  比如说在如下代码中,为 opt 添加了 AddBadMul 函数,但是 Bad 函数在执行过程中抛出了异常,那么 Mul 函数就不会被调用:

Bad Func Called

  哪怕是使用了 try...catch 语句对异常进行捕捉,依然不会执行后面的语句,除非抛出异常的函数是最后一个才被加入委托的:

Bad Func Called with Try Catch

Bad Func Last Called with Try Catch

总结

  本文非常简略地介绍了 C# 中的委托是什么,它的定义方法以及最简单的使用方法。由于 C# 中的委托的应用实际上是十分广泛且深入语言各个层面的,同时 .NET 官方为我们已经定义了一系列的内置委托,并且委托实际上与匿名方法还有事件关系密切。因此。碍于篇幅,本文仅能粗略地介绍一些委托的基础知识,在未来,将会在文章“C# 中的委托(下)”中介绍 .NET 的内置委托和“与其对应”的匿名函数,及 Lambda 表达式。若各位对这一节内容还有任何疑问或意见,欢迎在下方评论或提出。