ConditionalWeakTable 是什么,为什么不用 Dictionary
ConditionalWeakTable<k v></k>是 .NET 提供的一个特殊集合类型,核心用途是「给任意对象附加生命周期绑定的元数据」。它不阻止键对象被 GC 回收,一旦键被回收,对应条目自动消失——这点和
Dictionary<object t></object>有本质区别:
Dictionary会强引用键,导致本该回收的对象滞留。
典型场景包括:为第三方类型(比如
FileStream或用户自定义类)动态挂载上下文、诊断信息、AOP 行为钩子等,且不干预其生命周期。 键(
K)必须是引用类型,且内部用弱引用来跟踪 值(
V)是强引用,但键被回收后,整个条目从表中移除(即使值还活着) 不支持枚举(
foreach)、Count 属性或 LINQ 查询——它不是通用容器,而是“附着式存储”
ConditionalWeakTable 的线程安全性
ConditionalWeakTable<k v></k>的所有公开方法(
Add、
GetValue、
TryGetValue、
Remove)都是线程安全的。内部使用细粒度锁 + 无锁路径混合实现,.NET Core 2.1+ 还进一步优化了读多写少场景的性能。
但要注意:线程安全仅保证单个方法调用原子性,不保证复合操作的原子性。例如下面这段代码就有竞态风险:
var table = new ConditionalWeakTable<object, int>();
if (!table.TryGetValue(obj, out _))
{
table.Add(obj, ComputeValue()); // 可能被多个线程同时执行
}
正确做法是用
GetValue,它自带“首次调用初始化”语义:
var value = table.GetValue(obj, _ => ComputeValue());
GetValue内部确保:对同一键,最多只调用一次工厂函数,其余线程阻塞等待结果 工厂函数(
Func<k v></k>)内不能依赖外部可变状态,否则可能引发不可预期行为 如果工厂函数抛异常,该异常会被缓存并重抛;后续对该键的
GetValue调用仍会抛出同一异常
常见误用和内存泄漏隐患
最隐蔽的问题不是线程安全,而是误以为值的生命周期也受弱引用保护。实际上:
ConditionalWeakTable只弱引用键,值是强引用。如果值反过来持有键的引用(比如闭包捕获、事件订阅、内部字段赋值),就会形成循环引用,导致键无法被 GC —— 弱引用失效,内存泄漏发生。 避免在值类型中保存对键的强引用,尤其注意 lambda、匿名类、委托实例 若值需监听键的事件,务必在键释放前手动解绑(但键释放不可控,推荐改用
WeakEventManager或弱事件模式) 不要把它当缓存用:没有过期策略、不支持容量控制、不触发 GC 友好清理 调试时看不到条目?因为 Visual Studio 的调试器会临时强引用对象,干扰弱引用观察 —— 需用内存快照(dotMemory / VS Diagnostic Tools)验证实际存活情况
替代方案对比:WeakReference vs ConditionalWeakTable
如果你只是想“弱持有某个对象”,用
WeakReference<t></t>更轻量;但如果你想“给任意已有对象加字段”,
ConditionalWeakTable是唯一选择。
WeakReference<t></t>:你主动创建、持有、查询,适合缓存单个对象引用
ConditionalWeakTable<t u></t>:你把对象当键“贴标签”,框架帮你管理弱关联,适合装饰/扩展未知对象 两者都不解决“值引用键”的循环问题,这始终要靠设计规避
真正容易被忽略的是:它的存在本身就意味着你在做“运行时对象增强”,这种模式会让代码路径更难追踪、GC 行为更难预测——上线前务必用真实负载压测内存驻留曲线。
