如果你学过 C 语言,大概对函数指针有些印象------那个既强大又危险的东西,稍不留神就会踩进类型错误的坑里。C# 的委托,可以理解为函数指针的"文明化改造":同样是对方法的引用,但编译器全程把关类型安全,还顺手附赠了多播、事件等一整套能力。
本质上,委托是一个类型。它定义了一份"签名契约"------规定方法必须接受什么参数、返回什么类型。任何符合这份契约的方法,都可以被委托实例持有、传递、随时调用。把行为当数据来对待,这正是函数式编程思想悄悄渗入 C# 的地方。
🧭 三个核心特性,记住就够了
委托的魅力集中在三点上。
类型安全。 绑定方法时,编译器会检查签名是否匹配,不符合就直接报错,彻底杜绝了 C 函数指针那种"运行时才爆炸"的惊喜。
一等公民。 委托实例可以赋值给变量、作为参数传入函数、作为返回值带出来------方法,从此可以像整数、字符串一样自由流动。
多播能力。 一个委托实例可以用 += 挂载多个方法,调用时按顺序逐一执行;用 -= 可以随时摘掉某一个。这是 .NET 事件机制的底层基础。
🔍 委托的三个演化层次
.NET 中的委托并非一成不变,它经历了从"原始声明"到"内置泛型"再到"Lambda 语法糖"的演化过程。理解这个层次,能让你读懂各个年代的代码,也能在合适的场景选对写法。
| 层次 | 语法形式 | 特点 |
|---|---|---|
| 原始委托 | delegate 关键字声明类型 |
最基础,显式声明类型 |
| 泛型委托 | Func<> / Action<> / Predicate<> |
BCL 内置,无需自定义类型 |
| Lambda 表达式 | x => x + 1 |
最简洁,现代 C# 主流写法 |
三者本质完全相同,Lambda 只是委托的语法糖,编译器最终都会将其转化为委托实例。
💻 代码说话
原始委托:从零开始绑定方法
csharp
// 声明委托类型------定义"签名契约"
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
}
}
多播委托:一次调用,多方响应
csharp
delegate void Logger(string message);
class Program
{
static void LogToConsole(string msg) => Console.WriteLine($"[Console] {msg}");
static void LogToFile(string msg) => Console.WriteLine($"[File] {msg}");
static void LogToCloud(string msg) => Console.WriteLine($"[Cloud] {msg}");
static void Main()
{
Logger logger = LogToConsole;
logger += LogToFile;
logger += LogToCloud;
logger("系统启动");
// [Console] 系统启动
// [File] 系统启动
// [Cloud] 系统启动
logger -= LogToFile; // 摘掉文件日志
logger("第二条日志");
// [Console] 第二条日志
// [Cloud] 第二条日志
}
}
内置泛型委托:告别重复声明
.NET BCL 提供了三个开箱即用的泛型委托,覆盖了绝大多数场景,日常开发几乎不需要再手动 delegate 声明类型。
csharp
// Func<TInput, TOutput> --- 有返回值
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(5, 3)); // 8
// Action<T> --- 无返回值(void)
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("World"); // Hello, World!
// Predicate<T> --- 返回 bool,专为过滤而生
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4)); // True
Console.WriteLine(isEven(7)); // False
委托作为参数:策略模式的精髓
这是委托最让人着迷的用法------把行为本身作为参数传进去,同一套逻辑框架,注入不同的策略,产生截然不同的结果。解耦做到这个程度,代码的弹性会大幅提升。
csharp
class DataProcessor
{
public static List<int> Filter(List<int> data, Predicate<int> condition)
=> data.Where(x => condition(x)).ToList();
public static List<T> Transform<T>(List<int> data, Func<int, T> converter)
=> data.Select(converter).ToList();
}
class Program
{
static void Main()
{
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evens = DataProcessor.Filter(numbers, n => n % 2 == 0);
var bigOnes = DataProcessor.Filter(numbers, n => n > 6);
var squares = DataProcessor.Transform(numbers, n => n * n);
Console.WriteLine(string.Join(", ", evens)); // 2, 4, 6, 8, 10
Console.WriteLine(string.Join(", ", bigOnes)); // 7, 8, 9, 10
Console.WriteLine(string.Join(", ", squares)); // 1, 4, 9, 16, 25, 36, 49, 64, 81, 100
}
}
事件:委托的"受保护封装"
event 关键字是对委托的进一步约束。外部代码只能 += 订阅或 -= 退订,无法直接赋值覆盖、也无法从外部触发调用------这道护栏,让发布-订阅模式变得既灵活又安全。
csharp
class Button
{
public event Action<string> Clicked;
public void SimulateClick()
{
Console.WriteLine("按钮被点击...");
Clicked?.Invoke("ButtonA"); // ?. 防止没有订阅者时的空引用
}
}
class Program
{
static void Main()
{
var btn = new Button();
btn.Clicked += name => Console.WriteLine($"处理器1: {name} 被点击");
btn.Clicked += name => Console.WriteLine($"处理器2: 记录日志 - {name}");
btn.SimulateClick();
// 按钮被点击...
// 处理器1: ButtonA 被点击
// 处理器2: 记录日志 - ButtonA
}
}
💡 一张表收尾
| 概念 | 一句话理解 |
|---|---|
| 委托类型 | 方法签名的"合同书",规定参数与返回值 |
| 委托实例 | 持有具体方法引用的变量 |
| 多播委托 | 一条链上绑定多个方法,顺序执行 |
Func / Action |
内置泛型委托,告别重复声明 |
| Lambda | 委托的匿名内联写法,现代 C# 的主流姿势 |
event |
委托的受保护封装,专为发布-订阅模式设计 |
委托的核心哲学只有一句话:把行为当数据来传递。一旦建立起这个直觉,你会发现 LINQ、异步回调、依赖注入背后,都藏着同一个影子。