委托底层实现原理
大约 10 分钟约 3119 字
委托底层实现原理
简介
委托是 C# 中类型安全的方法引用机制,是事件、lambda 表达式、LINQ 和异步编程的基础。理解委托的内部结构(System.Delegate 和 System.MulticastDelegate)、调用链和编译器优化,有助于编写高性能的回调代码。
特点
委托内部结构
MulticastDelegate 字段
// 所有委托都继承自 System.MulticastDelegate(不能手动继承)
// MulticastDelegate 继承自 System.Delegate
// 内部关键字段:
// _target — 目标对象(实例方法为对象引用,静态方法为 null)
// _methodPtr — 方法指针(指向方法的入口地址)
// _invocationList — 多播委托的调用链(委托数组)
// _invocationCount — 调用链中的委托数量
// 使用反射查看委托内部
public class DelegateInspector
{
public void Inspect(Delegate del)
{
Console.WriteLine($"类型: {del.GetType().Name}");
Console.WriteLine($"目标: {del.Target}"); // 实例对象
Console.WriteLine($"方法: {del.Method.Name}"); // 方法信息
Console.WriteLine($"是否静态: {del.Method.IsStatic}");
if (del is MulticastDelegate multicast)
{
var list = multicast.GetInvocationList();
Console.WriteLine($"调用链长度: {list.Length}");
foreach (Delegate d in list)
{
Console.WriteLine($" → {d.Method.DeclaringType?.Name}.{d.Method.Name}");
}
}
}
}
// 实例方法委托
class Calculator
{
public int Add(int a, int b) => a + b;
}
var calc = new Calculator();
Func<int, int, int> add = calc.Add;
// add._target = calc(Calculator 实例)
// add._methodPtr = 指向 Calculator.Add 的指针
// 静态方法委托
Func<int, int, int> staticAdd = (a, b) => a + b;
// staticAdd._target = null(或闭包对象)
// staticAdd._methodPtr = 指向静态方法的指针委托创建的编译器行为
// C# 代码:Func<int, int, int> add = (a, b) => a + b;
// 编译器生成的代码:
// 1. 缓存委托实例(避免重复分配)
[CompilerGenerated]
private static Func<int, int, int>? <>9_CachedAnonymousMethodDelegate;
// 首次使用时创建,后续复用
Func<int, int, int> add = <>9_CachedAnonymousMethodDelegate
?? (<>9_CachedAnonymousMethodDelegate = new Func<int, int, int>(<Main>b__0));
[CompilerGenerated]
internal static int <Main>b__0(int a, int b) => a + b;
// 如果 lambda 捕获了变量,编译器不会缓存(每次创建新实例)
int factor = 10;
Func<int, int> multiply = x => x * factor;
// 每次执行这行都会创建新的委托+闭包对象
// 静态 lambda(C# 9+)不捕获变量,可以被缓存
Func<int, int, int> staticAdd = static (a, b) => a + b;
// 等价于:编译器生成静态方法 + 缓存委托多播委托深入
调用链机制
// += 合并委托(创建新的 InvocationList)
Action handler = () => Console.WriteLine("A");
handler += () => Console.WriteLine("B");
handler += () => Console.WriteLine("C");
// handler._invocationList = [delegateA, delegateB, delegateC]
// handler._invocationCount = 3
handler(); // 依次调用 A → B → C
// -= 移除委托
handler -= () => Console.WriteLine("B"); // 注意:这是新的 lambda,不会移除!
// 因为每次 lambda 创建新的委托实例
// 正确的移除方式
Action handlerA = () => Console.WriteLine("A");
Action handlerB = () => Console.WriteLine("B");
Action handlerC = () => Console.WriteLine("C");
Action handler2 = handlerA + handlerB + handlerC;
handler2 -= handlerB; // 移除 B
handler2(); // 只输出 A → C
// 多播委托的返回值问题
Func<int> func1 = () => 1;
Func<int> func2 = () => 2;
Func<int> func3 = () => 3;
Func<int> multi = func1 + func2 + func3;
int result = multi(); // 只返回 3!最后一个的返回值
// 获取所有返回值
foreach (Func<int> f in multi.GetInvocationList())
{
Console.WriteLine(f()); // 1, 2, 3
}异常处理
// 多播委托中一个处理器抛出异常,后续不会执行
Action<string> handlers = msg =>
{
Console.WriteLine($"Handler 1: {msg}");
};
handlers += msg =>
{
Console.WriteLine($"Handler 2: {msg}");
throw new InvalidOperationException("Handler 2 错误");
};
handlers += msg =>
{
Console.WriteLine($"Handler 3: {msg}"); // 不会执行
};
try
{
handlers("test");
}
catch
{
// Handler 3 不会执行!
}
// 解决:手动遍历调用链
void SafeInvokeAll(Delegate multi, params object[] args)
{
foreach (Delegate d in multi.GetInvocationList())
{
try
{
d.DynamicInvoke(args);
}
catch (Exception ex)
{
Console.WriteLine($"处理器 {d.Method.Name} 异常: {ex.InnerException?.Message}");
}
}
}委托性能
性能对比
// 不同调用方式的性能排序(快 → 慢)
// 1. 直接方法调用
// 2. 委托调用(非虚)
// 3. 委托调用(虚/接口)
// 4. 多播委托调用
// 5. DynamicInvoke
// 6. 反射 MethodInfo.Invoke
var sw = new Stopwatch();
const int iterations = 100_000_000;
// 直接调用
sw.Restart();
for (int i = 0; i < iterations; i++) Add(1, 2);
sw.Stop();
Console.WriteLine($"直接调用: {sw.ElapsedMilliseconds}ms"); // ~50ms
// 委托调用
Func<int, int, int> addDelegate = Add;
sw.Restart();
for (int i = 0; i < iterations; i++) addDelegate(1, 2);
sw.Stop();
Console.WriteLine($"委托调用: {sw.ElapsedMilliseconds}ms"); // ~80ms
// 反射调用
MethodInfo methodInfo = typeof(Program).GetMethod("Add", BindingFlags.Static | BindingFlags.NonPublic)!;
sw.Restart();
for (int i = 0; i < iterations; i++) methodInfo.Invoke(null, new object[] { 1, 2 });
sw.Stop();
Console.WriteLine($"反射调用: {sw.ElapsedMilliseconds}ms"); // ~10000ms
static int Add(int a, int b) => a + b;
// 优化:使用 Expression 编译委托替代反射
MethodInfo mi = typeof(string).GetMethod("Contains", new[] { typeof(string) })!;
var param = Expression.Parameter(typeof(string), "str");
var arg = Expression.Parameter(typeof(string), "value");
var call = Expression.Call(param, mi, arg);
var lambda = Expression.Lambda<Func<string, string, bool>>(call, param, arg);
Func<string, string, bool> compiled = lambda.Compile();
// 编译后的委托调用接近直接调用的性能减少委托分配
// 1. 缓存委托实例
// ❌ 每次创建
void ProcessBad()
{
items.Where(x => IsValid(x)); // 每次 new 委托(如果捕获了变量)
}
// ✅ 缓存
Func<Item, bool> predicate = x => IsValid(x);
void ProcessGood()
{
items.Where(predicate);
}
// 2. 使用方法组
// ❌
items.Where(x => IsValid(x));
// ✅(可能避免分配)
items.Where(IsValid);
// 3. static lambda(C# 9+)
// ✅ 编译器自动缓存
Func<int, int> square = static x => x * x;
// 4. 避免在热路径中创建闭包
// ❌ 每次迭代创建闭包
for (int i = 0; i < items.Length; i++)
{
Process(items[i], x => x.Id == targetId); // targetId 捕获 → 闭包
}
// ✅ 避免捕获
int localTarget = targetId;
Func<Item, bool> filter = x => x.Id == localTarget;
for (int i = 0; i < items.Length; i++)
{
Process(items[i], filter); // 复用委托
}优点
缺点
事件与委托的关系
event 关键字的本质
/// <summary>
/// event 是对委托的访问限制修饰符
/// </summary>
// 不使用 event — 外部可以随意操作委托
public class PublisherBad
{
public Action<string>? OnMessage; // 公开字段
public void SendMessage(string msg)
{
OnMessage?.Invoke(msg);
}
}
// 外部可以:
// publisher.OnMessage = null; // 清空所有订阅!
// publisher.OnMessage += myHandler; // 添加订阅
// publisher.OnMessage = myHandler; // 覆盖所有订阅!
// publisher.OnMessage?.Invoke("hack"); // 直接触发!
// 使用 event — 限制外部操作
public class PublisherGood
{
public event Action<string>? OnMessage; // 事件
public void SendMessage(string msg)
{
OnMessage?.Invoke(msg);
}
}
// 外部只能:
// publisher.OnMessage += myHandler; // 添加订阅 ✓
// publisher.OnMessage -= myHandler; // 移除订阅 ✓
// publisher.OnMessage = null; // 编译错误!只能在类内部
// publisher.OnMessage?.Invoke("hack"); // 编译错误!只能在类内部
// event 编译器生成的代码:
// 1. 私有委托字段
// 2. add_OnMessage 方法(+=)
// 3. remove_OnMessage 方法(-=)
// 外部的 += 和 -= 被编译为 add/remove 方法调用线程安全的事件调用
/// <summary>
/// 事件的线程安全调用模式
/// </summary>
// 模式 1:null 条件运算符(C# 6+,线程安全)
public class SafePublisher
{
public event Action<string>? OnMessage;
public void SendMessage(string msg)
{
OnMessage?.Invoke(msg);
// 编译器生成临时变量,防止在检查和调用之间被置空
}
}
// 模式 2:显式局部变量(等价于模式 1)
public void SendMessageOld(string msg)
{
var handler = OnMessage; // 复制到局部变量
if (handler != null)
handler(msg);
}
// 模式 3:取消订阅时的竞态条件
public class EventLeakExample
{
private event Action? _event;
// ❌ 非线程安全的取消订阅
public void UnsubscribeBad(Action handler)
{
_event -= handler;
// 如果另一个线程正在遍历 _event 的 InvocationList,
// 这里修改可能导致异常
}
// ✅ 使用 Interlocked 或锁
private readonly object _lock = new();
public void UnsubscribeSafe(Action handler)
{
lock (_lock)
{
_event -= handler;
}
}
}WeakEvent 模式
/// <summary>
/// WeakEvent — 避免事件导致的内存泄漏
/// </summary>
// 标准事件导致内存泄漏的例子:
// 订阅者被事件源持有强引用,即使不再使用也无法被 GC
public class EventSource
{
public event EventHandler? SomethingHappened;
public void Raise() => SomethingHappened?.Invoke(this, EventArgs.Empty);
}
// 解决方案 1:WeakReference 模式
public class WeakEventSource
{
private readonly List<WeakReference<EventHandler>> _handlers = new();
public void Subscribe(EventHandler handler)
{
_handlers.Add(new WeakReference<EventHandler>(handler));
}
public void Raise()
{
// 清理已死亡的引用
_handlers.RemoveAll(w => !w.TryGetTarget(out _));
foreach (var weakRef in _handlers)
{
if (weakRef.TryGetTarget(out var handler))
handler(this, EventArgs.Empty);
}
}
}
// 解决方案 2:实现 IDisposable 显式取消订阅
public class Subscriber : IDisposable
{
private readonly EventSource _source;
public Subscriber(EventSource source)
{
_source = source;
_source.SomethingHappened += OnEvent;
}
private void OnEvent(object? sender, EventArgs e) { /* ... */ }
public void Dispose()
{
_source.SomethingHappened -= OnEvent; // 关键:取消订阅
}
}表达式树与委托
Expression 与 Func 的转换
/// <summary>
/// 表达式树 — 委托的"可检查"形式
/// </summary>
// Func<T> — 编译后的可执行代码
Func<int, bool> isPositive = x => x > 0;
bool result = isPositive(5); // 直接执行
// Expression<Func<T>> — 表达式的数据结构
Expression<Func<int, bool>> isPositiveExpr = x => x > 0;
// 不会编译为可执行代码,而是构建表达式树
// 查看表达式树结构
void AnalyzeExpression(Expression<Func<int, bool>> expr)
{
// expr.Body 是 BinaryExpression: x > 0
var body = (BinaryExpression)expr.Body;
Console.WriteLine($"操作符: {body.NodeType}"); // GreaterThan
Console.WriteLine($"左操作数: {((ParameterExpression)body.Left).Name}"); // x
Console.WriteLine($"右操作数: {((ConstantExpression)body.Right).Value}"); // 0
}
// 表达式树 → 委托(编译执行)
Func<int, bool> compiled = isPositiveExpr.Compile();
bool result2 = compiled(-5); // false
// 表达式树的用途:
// 1. EF Core — 将 C# 表达式翻译为 SQL
// 2. Dynamic LINQ — 动态构建查询条件
// 3. 代码分析 — 静态分析工具检查代码
// 4. 序列化 — 跨进程传输查询逻辑总结
委托内部由 _target(目标对象)和 _methodPtr(方法指针)两个字段组成。多播委托通过 _invocationList 存储调用链,+= 创建新数组而非修改原委托。编译器会缓存无捕获的 lambda 委托实例。委托性能:直接调用 < 委托调用 < 反射调用。Expression.Compile() 可以将反射调用优化为委托调用级别。减少委托分配的策略:缓存实例、使用方法组、static lambda、避免热路径闭包。
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道“为什么这样写”和“在什么边界下不能这样写”。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
适用场景
- 当你准备把《委托底层实现原理》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了“高级”而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把《委托底层实现原理》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《委托底层实现原理》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《委托底层实现原理》最大的收益和代价分别是什么?
