1. 回调和异步编程
回调 (callback) — 把一个方法当作参数传入,在某个操作完成后调用它。经常用于异步操作、定时器、数据处理和用户界面。
示例 1: 带回调的异步操作
假设有个应用,用户输入请求,结果稍后才到(比如从网络)。拿到数据后我们想更新界面。
// 回调委托
public delegate void DataReceivedHandler(string result);
// 异步下载数据的机制(模拟)
public void DownloadDataAsync(DataReceivedHandler callback)
{
// 假设下载需要时间(通过定时器模拟)
Task.Delay(1000).ContinueWith(_ =>
{
string data = "搜索结果:<数据>";
callback(data); // 调用回调委托
});
}
// 使用:
DownloadDataAsync(result =>
{
Console.WriteLine("收到: " + result);
});
这个思路能让你把拿数据的机制和拿到数据后干啥完全分开,代码更灵活。
2. 把委托当作方法参数:策略和比较器
常见需求:允许用户把“逻辑”(函数)传进你的方法,这样就能自定义如何比较、过滤或转换元素。
示例 2: 通过委托实现 Strategy 模式
比如我们有一个排序实现,但想让用户能按名字、按日期、按大小等不同方式排序。
public delegate bool CompareFunc(int a, int b);
public void BubbleSort(int[] arr, CompareFunc compare)
{
for (int i = 0; i < arr.Length; i++)
{
for (int j = 0; j < arr.Length - 1; j++)
{
if (compare(arr[j], arr[j + 1]))
{
// 交换
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// 从大到小排序的比较函数
CompareFunc descending = (a, b) => a < b;
// 使用
int[] numbers = { 3, 1, 4, 2 };
BubbleSort(numbers, descending);
Console.WriteLine(string.Join(", ", numbers)); // 输出: 4, 3, 2, 1
这是在不改源代码的情况下把自己的“策略”注入到别人代码里的通用做法。
3. 匿名方法、lambda 表达式和委托
随着 C# 发展,为每个小功能都写一个类或方法很麻烦。幸运的是有匿名方法和 lambda,可以在“飞行中”创建委托。
示例 3: 把 lambda 当作委托
Func<int, int, int> operation = (x, y) => x * y;
int result = operation(3, 5); // 15
示例 4: 根据名字选择操作(switch + Delegates)
Func<int, int, int> op;
string userInput = "sum"; // "sub", "mul", "div"
switch (userInput)
{
case "sum": op = (a, b) => a + b; break;
case "sub": op = (a, b) => a - b; break;
case "mul": op = (a, b) => a * b; break;
case "div": op = (a, b) => a / b; break;
default: throw new Exception("未知操作!");
}
Console.WriteLine(op(6, 2));
别忘了验证输入——委托能提升灵活性和可读性。
4. 委托和处理链(“责任链”)
因为委托支持多路广播,你可以很容易地搭建处理链。
示例 5: 过滤器链
假设我们有一堆“过滤器”,要依次处理字符串。
public delegate string StringFilter(string input);
string RemoveDigits(string input) => new string(input.Where(ch => !char.IsDigit(ch)).ToArray());
string ToUpper(string input) => input.ToUpper();
StringFilter filters = RemoveDigits;
filters += ToUpper;
// 委托会让字符串通过所有过滤器
string text = "你好123";
foreach (StringFilter filter in filters.GetInvocationList())
{
text = filter(text);
}
Console.WriteLine(text); // 输出: "你好"
注意:如果直接调用 filters(text),只会返回最后一个处理器的值,而不是整个链的结果!如果你需要“传值”给后续处理器,像上面示例那样用 GetInvocationList() 逐个调用。
5. 委托用于动态绑定运行时行为
以前要替换行为常常需要额外的类和接口。用委托和 lambda,可以把很多“轻量级”的多态用函数表示。
示例 6: 带动态命令的机器人行为
public class Robot
{
public event Action<string>? OnCommandReceived;
public void ReceiveCommand(string command)
{
OnCommandReceived?.Invoke(command);
}
}
// 使用:
var robot = new Robot();
robot.OnCommandReceived += cmd => Console.WriteLine($"机器人执行: {cmd}");
robot.OnCommandReceived += cmd =>
{
if (cmd == "开机")
Console.WriteLine("系统加载中...");
};
// 试试
robot.ReceiveCommand("开机");
robot.ReceiveCommand("向前移动");
这种做法常用于测试、原型、DI(dependency injection)容器,以及通过参数传递业务逻辑。
6. 委托作为对状态的订阅
假设有个类保存某个状态,状态变化时想通知所有订阅者,用委托(和事件)很方便实现。
示例 7: 带变化订阅的类
public class Notifier<T>
{
private T _value = default!;
public event Action<T>? ValueChanged;
public T Value
{
get => _value;
set
{
if (!Equals(_value, value))
{
_value = value;
ValueChanged?.Invoke(_value);
}
}
}
}
// 使用:
var intValue = new Notifier<int>();
intValue.ValueChanged += v => Console.WriteLine($"新值: {v}");
intValue.Value = 5; // 触发事件
intValue.Value = 10;
这个思路就是“极简版响应式编程”,是 MVVM、data binding 和很多现代 UI 框架的基础。
7. 委托、闭包和词法作用域
lambda 和匿名方法可以捕获外部上下文的变量(closure)。这很方便,但也会带来一些意外的问题。
示例 8: 捕获循环变量的陷阱
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
foreach (var a in actions)
a(); // 会打印三次 3 (!)
为什么?因为闭包引用的是同一个变量 i,循环结束后它等于 3。如果你想记住当时的 0、1、2,可以这样写:
for (int i = 0; i < 3; i++)
{
int loopValue = i; // 保存当前值
actions[i] = () => Console.WriteLine(loopValue);
}
这样就按预期工作了。闭包陷阱是新手用 lambda 时最常犯的错误之一!
8. 组合委托
多路委托(multicast)内部维护一个方法列表,你可以用 (+=) 添加,(-=) 删除处理器。
特点:按引用和签名删除
void Handler1() => Console.WriteLine("1");
void Handler2() => Console.WriteLine("2");
Action a = Handler1;
a += Handler2;
a -= Handler1; // 会只留下 Handler2
a?.Invoke(); // 会输出 "2"
示例:动态管理处理器
Action a = Handler1;
a += Handler1;
a -= Handler1; // 现在列表中还剩下一个 Handler1!
9. 委托的可扩展性与控制反转(IoC)
在大型应用中,组件常需要“调用”外部代码但保持解耦。委托能把扩展、插件和回调接入进来而不引入 tight coupling。
示例:在构造函数中注入行为
public class Greeter
{
private readonly Func<string> _getName;
public Greeter(Func<string> getName)
{
_getName = getName;
}
public void Greet() => Console.WriteLine($"你好, {_getName()}!");
}
// 注入不同的行为:
var greeter1 = new Greeter(() => "Anya");
var greeter2 = new Greeter(() => DateTime.Now.ToShortTimeString());
greeter1.Greet(); // 输出: "你好, Anya!"
greeter2.Greet(); // 输出: "你好, 14:35!"
在真实项目中,这种方式有助于编写可测试且易维护的代码。
10. 实用细节
委托在标准接口和 LINQ 中
如果你用 LINQ、集合或异步,你一定会遇到委托。
- 很多方法比如 List<T>.Find, Array.Sort, Where, Select 都接受委托(Func<T, bool>, Comparison<T> 等)。
- LINQ 方法让你传入过滤、转换、聚合逻辑——不用额外创建类。
示例:对象排序的比较器
var people = new[] { "Ivan", "Maria", "Petr" };
Array.Sort(people, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", people)); // Ivan, Petr, Maria
委托与柯里化(部分应用参数)
用匿名方法/lambda,你可以“固定”部分参数,得到新函数。
示例:部分应用
Func<int, int, int> sum = (x, y) => x + y;
// 创建一个总是加 10 的函数
Func<int, int> add10 = y => sum(10, y);
Console.WriteLine(add10(5)); // 15
委托比较的特点
在 C# 中,如果两个委托的调用列表(invocation list)相同,就可以比较相等(==)。
void Handler1() { }
void Handler2() { }
Action a1 = Handler1;
Action a2 = Handler1;
Console.WriteLine(a1 == a2); // True
Action a3 = Handler1; a3 += Handler2;
Action a4 = Handler1; a4 += Handler2;
Console.WriteLine(a3 == a4); // True
但如果委托是基于匿名方法或 lambda 构建的,比较的是实例本身。
委托的序列化
委托可以序列化,但只有当它们指向的方法在可序列化的类中且所有类型可用时才行。从 .NET 8 开始,BinaryFormatter 默认被禁用且被视为过时,在未来版本可能被移除;在生产场景中很少使用委托序列化。
委托和事件的区别:什么时候用哪个?
- 委托是类型/变量,可以被显式调用。
- 事件(event)限制了对委托的访问:外部只能订阅/取消订阅(+=/-=),而只能在类内部触发调用。
- 事件总是某种委托类型,但不是所有委托都是事件。
如何选择?如果你希望逻辑能在类外定义,用委托。如果你要控制订阅/取消并保护内部变量,用事件。
11. 使用委托时的常见错误
错误 1:混淆委托和事件。
把公开的委托字段(public Action MyAction;)当作事件来用会破坏封装。外部代码可能意外或恶意地把所有订阅者覆盖掉(instance.MyAction = null;)或者直接调用它们,这会破坏类的逻辑。应使用 public event Action MyAction; 来保护接口。
错误 2:对多路委托返回值处理不当。
如果委托有返回值(比如 Func<string, int>),直接调用(myDelegate("test"))只会返回链中最后一个方法的结果。要获取所有订阅者的结果,需要遍历调用列表,使用 GetInvocationList()。
// 遍历所有订阅者并获取结果
var list = myDelegate.GetInvocationList();
foreach (var d in list)
{
var r = ((Func<string, int>)d)("test");
Console.WriteLine(r);
}
错误 3:在闭包中捕获循环变量。
新手常见陷阱:在 for 循环中创建的 lambda 会捕获迭代器变量本身,而不是当次的值。
// 错误示例:所有 action 都会打印最后的 i 值
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
// 正确示例:为每次迭代创建局部副本
for (int i = 0; i < 3; i++)
{
int copy = i;
actions[i] = () => Console.WriteLine(copy);
}
错误 4:引起内存泄漏。
如果实例方法订阅了一个长生命周期对象的委托但没有取消订阅,就会泄漏。长生命周期对象会持有对订阅者的引用,GC 无法回收它。尤其是在生命周期有限的类(比如 UI 组件)里,注意取消订阅。
GO TO FULL VERSION