在C#开发中,委托(Delegate)是我每天都会用到的核心特性。它本质上是一个类型安全的函数指针,但比传统函数指针强大得多。我第一次真正理解委托的价值是在重构一个复杂的条件分支代码时——原本需要20多个if-else判断的逻辑,用委托重构后变得异常简洁。
委托是派生自System.MulticastDelegate的引用类型,具有三个关键特性:
csharp复制public delegate int MathOperation(int x, int y);
只能绑定到接受两个int参数并返回int的方法,这种编译期检查避免了运行时错误。
csharp复制MathOperation op;
if(useAddition)
op = Add;
else
op = Subtract;
在我早期的一个项目中,需要实现一个数据处理流水线,不同的数据需要经过不同的处理步骤。如果不使用委托,代码会是这样的:
csharp复制void ProcessData(Data data, ProcessingType type)
{
if(type == ProcessingType.A)
ProcessA(data);
else if(type == ProcessingType.B)
ProcessB(data);
// 更多条件分支...
}
使用委托重构后:
csharp复制delegate void DataProcessor(Data data);
void ProcessData(Data data, DataProcessor processor)
{
processor(data);
}
// 调用时
ProcessData(data, ProcessA);
这种解耦使得代码更容易维护和扩展。当新增处理类型时,不再需要修改ProcessData方法。
声明委托的语法看似简单,但有些细节需要注意:
csharp复制[访问修饰符] delegate 返回类型 委托名(参数列表);
最佳实践:
示例:
csharp复制namespace MyApplication
{
// 好的声明方式
public delegate void LogMessageHandler(string message, LogLevel level);
// 不推荐的声明方式
public delegate void Handler(string s, int i);
}
很多开发者只知道用new关键字实例化委托,其实C#提供了多种方式:
csharp复制delegate void MyDelegate(string s);
MyDelegate del = new MyDelegate(MethodName);
csharp复制MyDelegate del = MethodName;
csharp复制MyDelegate del = delegate(string s) { Console.WriteLine(s); };
csharp复制MyDelegate del = s => Console.WriteLine(s);
在实际项目中,我推荐优先使用Lambda表达式,除非方法体比较复杂。
调用委托有三种主要方式:
csharp复制del("Hello");
csharp复制del.Invoke("Hello");
csharp复制del?.Invoke("Hello");
重要提示:在调用委托前总是应该检查是否为null,否则会导致NullReferenceException。使用?.操作符是最安全的做法。
多播委托实际上是维护了一个方法调用列表。当使用+=添加方法时,实际上是创建了一个新的委托实例,它包含了原委托和方法列表的组合。
技术细节:
csharp复制foreach(Delegate handler in multicastDelegate.GetInvocationList())
{
try {
handler.DynamicInvoke(args);
}
catch(Exception ex) {
// 处理异常
}
}
性能优化:频繁操作多播委托会产生大量临时对象。在性能敏感场景,可以考虑缓存委托实例。
调试技巧:在Visual Studio中,可以通过查看委托的_target和_methodPtr字段来了解它绑定的具体方法。
我曾用多播委托实现过一个插件系统:
csharp复制public class PluginSystem
{
public delegate void MessageHandler(string message);
private MessageHandler _handlers;
public void AddHandler(MessageHandler handler) => _handlers += handler;
public void RemoveHandler(MessageHandler handler) => _handlers -= handler;
public void ProcessMessage(string message)
{
_handlers?.Invoke(message);
}
}
// 使用
var system = new PluginSystem();
system.AddHandler(msg => Console.WriteLine($"Logger: {msg}"));
system.AddHandler(msg => File.WriteAllText("log.txt", msg));
system.ProcessMessage("System started");
这个设计允许动态添加和移除处理逻辑,非常适合需要扩展性的场景。
事件实际上是委托的语法糖,它在委托基础上添加了封装层。关键区别:
典型的事件声明:
csharp复制public class Button
{
public event EventHandler Clicked;
protected virtual void OnClicked()
{
Clicked?.Invoke(this, EventArgs.Empty);
}
}
在大型项目中,事件设计需要考虑更多因素:
csharp复制public class OrderEventArgs : EventArgs
{
public int OrderId { get; }
public DateTime Timestamp { get; }
public OrderEventArgs(int orderId)
{
OrderId = orderId;
Timestamp = DateTime.Now;
}
}
csharp复制public event EventHandler<OrderEventArgs> OrderProcessed;
protected virtual void OnOrderProcessed(int orderId)
{
var handlers = OrderProcessed;
if(handlers != null)
{
foreach(EventHandler<OrderEventArgs> handler in handlers.GetInvocationList())
{
handler.BeginInvoke(this, new OrderEventArgs(orderId), null, null);
}
}
}
标准事件会导致发布者持有订阅者的引用,可能造成内存泄漏。解决方案是使用弱事件模式:
csharp复制public class WeakEventManager
{
private readonly List<WeakReference<EventHandler>> _handlers = new List<WeakReference<EventHandler>>();
public void AddHandler(EventHandler handler)
{
_handlers.Add(new WeakReference<EventHandler>(handler));
}
public void RaiseEvent(object sender, EventArgs args)
{
foreach(var weakRef in _handlers.ToArray())
{
if(weakRef.TryGetTarget(out var handler))
{
handler(sender, args);
}
else
{
_handlers.Remove(weakRef);
}
}
}
}
.NET提供了多种通用委托类型,可以避免重复声明:
csharp复制Action<string> print = Console.WriteLine;
Action<int, int> swap = (x, y) => { int temp = x; x = y; y = temp; };
csharp复制Func<int, int, int> add = (x, y) => x + y;
Func<string, int> parse = int.Parse;
csharp复制Predicate<string> isLong = s => s.Length > 10;
虽然通用委托很方便,但在性能关键路径上需要注意:
csharp复制private static readonly Func<string, int> _cachedParser = int.Parse;
void ProcessInput(string input)
{
int value = _cachedParser(input);
// ...
}
委托可以通过+运算符组合,这在中间件管道模式中很有用:
csharp复制public delegate Context RequestDelegate(Context context);
public static class PipelineBuilder
{
public static RequestDelegate Build(params RequestDelegate[] components)
{
RequestDelegate pipeline = context => context; // 初始传递委托
foreach(var component in components)
{
var next = pipeline;
pipeline = context => component(next(context));
}
return pipeline;
}
}
C#委托支持协变(covariance)和逆变(contravariance):
csharp复制// 协变示例
delegate object ObjectReturner();
ObjectReturner getString = () => "Hello";
// 字符串派生自object,所以这是合法的
// 逆变示例
delegate void StringConsumer(string s);
StringConsumer objectConsumer = (object o) => Console.WriteLine(o.ToString());
// 可以接受比string更通用的参数类型
这个特性在需要灵活性时非常有用,但过度使用会降低代码可读性。
委托调用比直接方法调用稍慢,因为:
优化建议:
虽然委托比直接调用慢,但比反射快得多。测试数据:
| 操作 | 耗时(纳秒/次) |
|---|---|
| 直接方法调用 | 1.2 |
| 委托调用 | 2.3 |
| MethodInfo.Invoke | 330 |
因此,在需要动态调用的场景,委托是比反射更好的选择。
在我设计的一个分布式任务系统中,委托发挥了核心作用:
csharp复制public class TaskRunner
{
private readonly Dictionary<string, Func<TaskRequest, TaskResult>> _handlers = new Dictionary<string, Func<TaskRequest, TaskResult>>();
public void RegisterHandler(string taskType, Func<TaskRequest, TaskResult> handler)
{
_handlers[taskType] = handler;
}
public async Task<TaskResult> ExecuteAsync(TaskRequest request)
{
if(_handlers.TryGetValue(request.TaskType, out var handler))
{
return await Task.Run(() => handler(request));
}
throw new NotSupportedException($"Task type {request.TaskType} is not supported");
}
}
这种设计允许动态添加新任务类型,而无需修改核心执行逻辑。
内存泄漏:长期存在的对象订阅事件而不取消订阅
多线程问题:在多线程环境下修改委托
性能问题:频繁创建和销毁委托
意外覆盖:使用=而不是+=
随着C#版本的演进,委托相关功能也在不断增强:
csharp复制void OuterMethod()
{
int LocalFunction(int x) => x * 2;
Func<int, int> func = LocalFunction;
}
csharp复制unsafe delegate*<int, int, int> pointer = &Add;
int result = pointer(3, 4);
不过对于大多数应用场景,传统的委托仍然是最佳选择,因为它提供了更好的安全性和灵活性。