ConditionalWeakTable 是 .NET 中用于为对象附加生命周期绑定元数据的线程安全集合,仅弱引用键、强引用值,键回收后条目自动移除;不支持枚举或 LINQ,适用于动态装饰对象,但需警惕值反向持有键导致的循环引用内存泄漏。

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









