很多人一看到 delegate 这个词就觉得很抽象。其实说白了:
委托就是"能被当作变量传来传去的方法"。
对刚接触 C# 的开发者来说,可以先把普通方法理解为“直接调用的代码”;而委托允许我们把一个方法保存起来,再把它传给其他方法。委托本身是一个“类型”,描述的是一种方法的签名(参数和返回值长什么样)。
举个生活化的类比:
一个最原始的自定义委托:
// 定义一个委托类型:描述"接受两个 int,返回 int"的方法长什么样
public delegate int MathOperation(int a, int b);
class Program
{
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
static void Main()
{
// 把方法本身(不是调用结果)赋值给委托变量
MathOperation op = Add;
Console.WriteLine(op(3, 4)); // 输出 7
op = Multiply;
Console.WriteLine(op(3, 4)); // 输出 12
}
}
看到了吗?op 这个变量里保存了一个可以被调用的方法。委托的核心作用就是:让方法可以被保存、传递,并在以后调用。
可以从另一个方面理解,C#里面有整型关键字int,字符串关键字string等数据类型。你定义了一个int变量之后,就可以把它传给函数了:
int count = 100;
Console.WriteLine(count);
而委托delegate的作用是,创建一个新的类型(类比int,string):
public delegate int MyNewType(int a);
现在,你可用的类型,除了int,string等C#语法里支持的之外,又多了个MyNewType。同样,你定义了一个MyNewType变量之后,就可以把它传给函数了:
// MyNewType 是类型;abs 是变量 - 只不过在使用abs时需要传一个参数
MyNewType abs = input => Math.Abs(input);
Console.WriteLine(abs(-1));
如果你自己每次都要写 delegate int MathOperation(int a, int b) 这种声明,会比较啰嗦。所以 .NET 提供了几个常用的内置委托类型,最常见的是 Action、Func、Predicate;事件场景中还经常使用 EventHandler。
Action 用于"只干活,不汇报结果"的方法——也就是返回值是 void 的方法。
// Action 不带参数
Action sayHello = () => Console.WriteLine("Hello!");
sayHello();
// Action<T> 带一个参数
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Rick");
Action<string, int> printInfo = (name, age) =>
Console.WriteLine($"{name} is {age} years old.");
printInfo("Rick", 30);
实际场景:日志记录、UI 按钮点击处理、遍历集合时对每个元素做点什么。
List<string> names = new() { "Alice", "Bob", "Charlie" };
// List.ForEach 接受一个 Action<T> 参数
names.ForEach(name => Console.WriteLine($"你好, {name}"));
一句话记忆:Action = "做点事,不用告诉我结果"。
Func 用于"干完活要汇报结果"的方法。语法规则是:最后一个泛型参数是返回值类型,前面的都是入参。
// Func<TResult>:无参数,返回 int
Func<int> getRandomNumber = () => new Random().Next(100);
Console.WriteLine(getRandomNumber());
// Func<T, TResult>:一个参数,返回 bool
Func<int, bool> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4)); // true
// Func<T1, T2, TResult>:两个参数,返回 int
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 4)); // 7
实际场景:LINQ 几乎全靠 Func 驱动。
List<int> numbers = new() { 1, 2, 3, 4, 5, 6 };
// Select 需要一个 Func<int, TResult>:把每个元素"转换"成新值
var squared = numbers.Select(n => n * n);
// Where 需要一个 Func<int, bool>:判断每个元素是否保留
var evens = numbers.Where(n => n % 2 == 0);
Console.WriteLine(string.Join(", ", squared)); // 1, 4, 9, 16, 25, 36
Console.WriteLine(string.Join(", ", evens)); // 2, 4, 6
一句话记忆:Func = "做点事,然后把结果还给我"。
Predicate<T> 其实就是 Func<T, bool> 的一个"专用马甲"——它只接受一个参数,永远返回 bool,专门用来表达"这个东西符不符合条件"。
Predicate<int> isNegative = n => n < 0;
List<int> numbers = new() { -3, -1, 0, 2, 5 };
// List<T>.Find 接受 Predicate<T>
int firstNegative = numbers.Find(isNegative);
Console.WriteLine(firstNegative); // -3
// FindAll、RemoveAll 也都用 Predicate<T>
List<int> negatives = numbers.FindAll(n => n < 0);
numbers.RemoveAll(n => n < 0);
为什么有了 Func<T, bool> 还要 Predicate<T>? 纯粹是历史原因——Predicate<T> 是 .NET Framework 早期(List<T> 的 Find/FindAll/RemoveAll 系列方法)就存在的类型,而 Func 家族是后来(.NET 3.5,配合 LINQ)才引入的。两者语义完全等价,能互相赋值:
Predicate<int> p = n => n > 0;
Func<int, bool> f = n => n > 0;
// 两者是不同的委托类型,需要通过 lambda 重新包装
Func<int, bool> converted = n => p(n);
现在写新代码时,大部分场景(尤其是 LINQ)直接用 Func<T, bool> 就够了;调用 List<T> 的 Find、FindAll、RemoveAll 等方法时,仍然会遇到 Predicate<T>。
前面三种委托解决的是"把方法当参数传递"的问题,而 EventHandler 解决的是另一个问题:一个对象发生了某件事,通知所有关心这件事的人。这就是"发布-订阅"模式,在 .NET 里靠 event 关键字 + EventHandler 委托实现。
public class Button
{
// 标准写法:EventHandler 委托 + event 关键字
public event EventHandler? Clicked;
public void SimulateClick()
{
Console.WriteLine("按钮被点击了...");
// 触发事件:通知所有订阅者
Clicked?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
var button = new Button();
// 订阅事件(多个订阅者都会被通知)
button.Clicked += (sender, e) => Console.WriteLine("订阅者A:我知道按钮被点了!");
button.Clicked += (sender, e) => Console.WriteLine("订阅者B:我也知道了!");
button.SimulateClick();
}
}
输出:
按钮被点击了...
订阅者A:我知道按钮被点了!
订阅者B:我也知道了!
EventHandler 的签名是固定的:void EventHandler(object? sender, EventArgs e)。如果你需要携带自定义数据,用泛型版本 EventHandler<TEventArgs>:
public class OrderEventArgs : EventArgs
{
public string OrderId { get; init; } = "";
public decimal Amount { get; init; }
}
public class OrderService
{
public event EventHandler<OrderEventArgs>? OrderCreated;
public void CreateOrder(string orderId, decimal amount)
{
// 处理订单逻辑...
OrderCreated?.Invoke(this, new OrderEventArgs { OrderId = orderId, Amount = amount });
}
}
// 订阅时能拿到强类型的数据
var service = new OrderService();
service.OrderCreated += (sender, e) =>
Console.WriteLine($"新订单:{e.OrderId},金额:{e.Amount}");
service.CreateOrder("ORD-001", 99.9m);
为什么不直接用 Action<object, EventArgs> 代替 EventHandler? 技术上可以做到功能类似,但 event 关键字配合 EventHandler 是 .NET 里的约定俗成。WinForms、WPF 以及许多 .NET 类库中的事件都遵循 sender + EventArgs 的签名规范,用标准类型能让你的 API 一眼就能被其他 .NET 开发者理解。
| 委托类型 | 有无返回值 | 典型场景 |
|---|---|---|
Action<T...> | 无(void) | 执行一个动作、回调、遍历处理 |
Func<T..., TResult> | 有 | 计算、转换、LINQ 查询 |
Predicate<T> | 有(固定 bool) | 条件判断(多见于 List<T> 老方法) |
EventHandler / EventHandler<T> | 无(void) | 对象内部状态变化,通知外部订阅者 |
大部分时候内置的 Action/Func 就够用了。但如果你想让签名有明确的业务语义,自定义委托能提升可读性:
// 语义清晰:一眼就知道这是"验证逻辑",而不是随便一个 Func<string, bool>
public delegate bool Validator(string input);
public class FormValidator
{
public List<Validator> Rules { get; } = new();
public bool IsValid(string input) => Rules.All(rule => rule(input));
}
var validator = new FormValidator();
validator.Rules.Add(input => !string.IsNullOrEmpty(input));
validator.Rules.Add(input => input.Length <= 20);
Console.WriteLine(validator.IsValid("Rick")); // true
另外,委托还支持多播(multicast)——一个委托变量可以同时挂载多个方法,这也是 event 常见的底层实现方式。多播委托最适合返回值为 void 的场景;如果多个方法都有返回值,调用者通常只能得到最后一个方法的返回值。
Action pipeline = () => Console.WriteLine("Step 1");
pipeline += () => Console.WriteLine("Step 2");
pipeline += () => Console.WriteLine("Step 3");
pipeline(); // 依次输出 Step 1、Step 2、Step 3
上面介绍的是最常见的几种委托类型。实际上,.NET 中还有很多 API 会把委托作为参数,用来表示“稍后要执行的代码”或“由调用方提供的处理逻辑”,例如:
Task.Run:接收需要执行的代码;CancellationToken.Register:接收取消发生时要执行的回调;List<T>.ForEach:接收遍历每个元素时要执行的操作;Where、Select:接收筛选条件或转换逻辑;ThreadStart:表示线程启动时要执行的方法。例如:
Task.Run(() => Console.WriteLine("在线程池中执行"));
cancellationToken.Register(() =>
Console.WriteLine("操作被取消"));
这些 API 的共同点都是:把一段代码作为参数传给另一个方法。这里不需要先理解线程池或异步状态机,只要先记住“委托可以传递一段待执行的代码”即可。
如果想继续了解更多委托类型和具体 API,可以参考 .NET 委托官方文档。
Func<T, bool>。event 关键字使用。Task.Run、CancellationToken.Register、List<T>.ForEach 和 LINQ)都会利用委托来传递一段待执行的代码。把这几个类型吃透,再回头看 LINQ、事件驱动 UI、异步编程这些"看起来很高级"的 API,你会发现它们全都是同一套思路的不同应用而已。
暂无评论,快来抢沙发吧!