为什么需要将委托作为参数?
将委托作为参数,本质上是将“行为”或“方法”作为参数传递给另一个方法,这允许接收方方法(被调用者)在执行过程中,可以调用由调用方传入的这个“行为”。

(图片来源网络,侵删)
这带来了几个关键好处:
- 回调:一个方法可以告诉另一个方法:“在你完成某个任务后,请调用这个方法(委托)来通知我结果或状态。”
- 代码复用与灵活性:你可以编写一个通用的算法或框架,而将具体的实现细节(比如如何比较两个对象、如何处理一个元素)留给调用者通过委托来定义。
- 事件处理:事件模型完全建立在委托的基础上,你订阅一个事件,本质上就是将一个方法(事件处理器)作为参数注册到事件源上。
实现步骤
将委托作为参数传递,通常遵循以下步骤:
- 定义委托类型:你需要声明一个委托类型,这个委托类型定义了它所能封装的方法的“签名”(返回类型和参数列表)。
- 创建委托实例:创建一个委托实例,并将其绑定到一个与委托签名匹配的具体方法上。
- 调用方法并传递委托:在一个方法调用中,将这个委托实例作为参数传递给另一个方法。
- 在目标方法内部执行委托:接收参数的方法内部,可以像调用普通方法一样调用传入的委托。
代码示例
下面我们通过几个从简单到复杂的例子来理解这个过程。
示例 1:最基础的回调
这个例子将展示一个经典的异步回调场景。

(图片来源网络,侵删)
步骤 1 & 2: 定义委托并创建实例
我们定义一个名为 TaskCompleteHandler 的委托,它接受一个 string 类型的消息作为参数,没有返回值,我们创建一个与它签名匹配的方法 OnTaskDone,并将其赋值给委托实例。
using System;
using System.Threading; // 为了演示耗时操作
// 1. 定义委托类型
// 它可以封装任何返回 void, 并接受一个 string 参数的方法。
public delegate void TaskCompleteHandler(string message);
public class Program
{
// 2. 一个与 TaskCompleteHandler 签名匹配的方法
public static void OnTaskDone(string result)
{
Console.WriteLine($"收到回调通知!任务已完成,结果是: {result}");
}
public static void Main(string[] args)
{
// 创建委托实例,并将其绑定到 OnTaskDone 方法
TaskCompleteHandler callback = OnTaskDone;
// 3. 调用另一个方法,并将委托作为参数传递
StartLongRunningProcess(callback);
Console.WriteLine("主线程继续执行其他工作...");
Console.ReadLine(); // 防止控制台窗口立即关闭
}
// 4. 这个方法接受一个 TaskCompleteHandler 类型的参数
public static void StartLongRunningProcess(TaskCompleteHandler handler)
{
Console.WriteLine("耗时任务开始...");
// 模拟一个耗时操作(比如网络请求、数据库查询)
Thread.Sleep(2000);
Console.WriteLine("耗时任务结束,准备通知调用者...");
// 在任务完成后,调用传入的委托(回调)
handler?.Invoke("数据处理成功!");
}
}
输出结果:
耗时任务开始...
主线程继续执行其他工作...
耗时任务结束,准备通知调用者...
收到回调通知!任务已完成,结果是: 数据处理成功!
分析:

(图片来源网络,侵删)
StartLongRunningProcess方法本身不关心任务完成后具体要做什么,它只负责在完成时调用handler这个委托。- 调用者(
Main方法)通过传入OnTaskDone方法,定义了任务完成后的具体行为,这就是解耦和灵活性的体现。
示例 2:使用 Action 和 Func (现代 C# 推荐)
在 C# 2.0 之后,微软引入了几个内置的泛型委托类型,如 Action 和 Func,我们通常不需要自己定义委托类型,可以直接使用它们,使代码更简洁。
Action:用于封装没有返回值的方法。Action<T>表示接受一个 T 类型参数,返回void的方法。Func:用于封装有返回值的方法。Func<T, TResult>表示接受一个 T 类型参数,返回TResult类型的方法。
使用 Action 的例子:
我们将上面的例子改写,使用 Action<string> 代替自定义的 TaskCompleteHandler。
using System;
using System.Threading;
public class Program
{
// 这个方法签名与 Action<string> 完全匹配
public static void OnTaskDone(string result)
{
Console.WriteLine($"收到回调通知!任务已完成,结果是: {result}");
}
public static void Main(string[] args)
{
// 直接使用 Action<string>,无需定义委托类型
Action<string> callback = OnTaskDone;
// 调用时,传递 Action 委托
StartLongRunningProcess(callback);
Console.WriteLine("主线程继续执行其他工作...");
Console.ReadLine();
}
// 方法签名改为接受 Action<string>
public static void StartLongRunningProcess(Action<string> handler)
{
Console.WriteLine("耗时任务开始...");
Thread.Sleep(2000);
Console.WriteLine("耗时任务结束,准备通知调用者...");
// 调用方式不变
handler?.Invoke("数据处理成功!");
}
}
输出结果与之前完全相同,可以看到,代码更简洁了。
示例 3:作为“策略”传递(更高级的用法)
假设我们有一个方法,需要对一个整数列表进行某种操作,但具体是什么操作(比如求和、求最大值、打印所有值)由调用者决定。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main(string[] args)
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 场景1:我们想对每个数字进行平方操作
Console.WriteLine("对每个数字进行平方操作:");
ProcessList(numbers, Square);
Console.WriteLine(); // 换行
// 场景2:我们想判断每个数字是否为偶数
Console.WriteLine("判断每个数字是否为偶数:");
ProcessList(numbers, IsEven);
Console.WriteLine();
// 场景3:使用 Lambda 表达式直接定义行为,更简洁
Console.WriteLine("打印所有大于5的数字:");
ProcessList(numbers, num => Console.WriteLine($"数字 {num} 大于5"));
}
// 定义委托类型,接受一个 int,返回 void
public delegate void NumberProcessor(int number);
// 核心方法:接受一个委托作为参数
public static void ProcessList(List<int> list, NumberProcessor processor)
{
if (list == null || processor == null) return;
foreach (var num in list)
{
// 对列表中的每个元素,调用传入的“处理器”
processor(num);
}
}
// 具体的处理方法1
public static void Square(int num)
{
Console.WriteLine($"平方: {num} * {num} = {num * num}");
}
// 具体的处理方法2
public static void IsEven(int num)
{
Console.WriteLine($"{num} 是 {(num % 2 == 0 ? "偶数" : "奇数")}");
}
}
输出结果:
对每个数字进行平方操作:
平方: 1 * 1 = 1
平方: 2 * 2 = 4
平方: 3 * 3 = 9
平方: 4 * 4 = 16
平方: 5 * 5 = 25
平方: 6 * 6 = 36
平方: 7 * 7 = 49
平方: 8 * 8 = 64
平方: 9 * 9 = 81
平方: 10 * 10 = 100
判断每个数字是否为偶数:
1 是 奇数
2 是 偶数
3 是 奇数
4 是 偶数
5 是 奇数
6 是 偶数
7 是 奇数
8 是 偶数
9 是 奇数
10 是 偶数
打印所有大于5的数字:
数字 6 大于5
数字 7 大于5
数字 8 大于5
数字 9 大于5
数字 10 大于5
分析:
ProcessList是一个通用的列表处理方法。NumberProcessor委托作为“策略”参数,传递了具体的处理逻辑。- 调用者可以通过传递不同的方法(如
Square,IsEven)或 Lambda 表达式(如num => Console.WriteLine(...))来改变ProcessList的行为,而无需修改ProcessList的任何代码,这正是“开闭原则”(对扩展开放,对修改关闭)的体现。
| 特性 | 说明 |
|---|---|
| 核心思想 | 将方法作为参数传递,实现行为注入。 |
| 关键步骤 | 定义委托 (或使用 Action/Func) 2. 创建委托实例并绑定方法 3. 将委托作为参数传递 4. 在目标方法中调用委托。 |
| 主要用途 | 回调、事件、策略模式、自定义排序/查找 (如 List<T>.Sort 的 Comparison<T 参数)。 |
| 现代实践 | 优先使用 Action (无返回值) 和 Func (有返回值) 等泛型委托,它们更灵活且类型安全,Lambda 表达式是创建委托实例的绝佳方式,能让代码更简洁。 |
掌握将委托作为参数传递,是迈向 C# 高级编程的重要一步,它极大地增强了代码的灵活性、可读性和可维护性。
