如何使用ConditionalWeakTable实现多引用作为键的函数记忆化?
用多个引用作为ConditionalWeakTable的键实现记忆化
因为ConditionalWeakTable仅支持单个引用类型作为键,要实现多输入引用的记忆化,有两种实用方案,推荐优先使用嵌套表的方式。
方案一:嵌套ConditionalWeakTable(推荐)
通过多层嵌套的ConditionalWeakTable,每一层对应一个输入引用,最终将计算结果绑定到最内层的键上。这种方式无需自定义类型,完全利用ConditionalWeakTable的原生特性,且能自动处理垃圾回收:当任意一个输入对象被回收时,对应层级的表条目会被移除,下层的缓存数据也会随之被回收。
代码示例
假设你有三个输入类型ObjA、ObjB、ObjC,以及返回类型Result,记忆化实现如下:
using System.Runtime.CompilerServices; public class MemoryCache { // 三层嵌套的ConditionalWeakTable,每层对应一个输入参数 private readonly ConditionalWeakTable<ObjA, ConditionalWeakTable<ObjB, ConditionalWeakTable<ObjC, Result>>> _cache = new(); public Result GetOrCompute(ObjA a, ObjB b, ObjC c) { // 获取或创建第二层表(对应第一个参数a) var level2Cache = _cache.GetValue(a, _ => new ConditionalWeakTable<ObjB, ConditionalWeakTable<ObjC, Result>>()); // 获取或创建第三层表(对应第二个参数b) var level3Cache = level2Cache.GetValue(b, _ => new ConditionalWeakTable<ObjC, Result>()); // 获取或计算最终结果(对应第三个参数c) return level3Cache.GetValue(c, _ => ComputeResult(a, b, c)); } // 实际计算逻辑的函数 private Result ComputeResult(ObjA a, ObjB b, ObjC c) { // 这里替换为你的业务计算代码 return new Result(); } } // 示例类型 public class ObjA { } public class ObjB { } public class ObjC { } public class Result { }
关键特性
- 线程安全:
ConditionalWeakTable的GetValue方法是原子操作,多线程调用不会出现重复计算的问题。 - 自动垃圾回收:只要任意一个输入对象被回收,对应层级的缓存条目会被自动清理,不会阻止原对象的回收。
- 高性能:直接使用原对象作为键,缓存命中率高,无需额外的相等性校验开销。
方案二:自定义组合键类型
如果嵌套表的层级过多(比如超过3个输入参数),可以将多个输入引用包装成一个自定义的组合键类型,作为ConditionalWeakTable的键。需要注意组合键必须基于引用相等性比较,且用弱引用持有输入对象,避免阻止原对象被回收。
代码示例
using System.Runtime.CompilerServices; public sealed class CombinedKey : IEquatable<CombinedKey> { private readonly WeakReference<ObjA> _aRef; private readonly WeakReference<ObjB> _bRef; private readonly WeakReference<ObjC> _cRef; private readonly int _cachedHashCode; public CombinedKey(ObjA a, ObjB b, ObjC c) { _aRef = new WeakReference<ObjA>(a); _bRef = new WeakReference<ObjB>(b); _cRef = new WeakReference<ObjC>(c); // 用RuntimeHelpers.GetHashCode获取对象的引用哈希,保证哈希值不随对象内容变化 _cachedHashCode = HashCode.Combine( RuntimeHelpers.GetHashCode(a), RuntimeHelpers.GetHashCode(b), RuntimeHelpers.GetHashCode(c)); } public bool Equals(CombinedKey? other) { if (other is null) return false; if (ReferenceEquals(this, other)) return true; // 尝试获取原对象,若任意对象已被回收,则不相等 if (!_aRef.TryGetTarget(out var a) || !other._aRef.TryGetTarget(out var otherA)) return false; if (!_bRef.TryGetTarget(out var b) || !other._bRef.TryGetTarget(out var otherB)) return false; if (!_cRef.TryGetTarget(out var c) || !other._cRef.TryGetTarget(out var otherC)) return false; // 基于引用相等性判断 return ReferenceEquals(a, otherA) && ReferenceEquals(b, otherB) && ReferenceEquals(c, otherC); } public override bool Equals(object? obj) => Equals(obj as CombinedKey); public override int GetHashCode() => _cachedHashCode; } public class MemoryCache { private readonly ConditionalWeakTable<CombinedKey, Result> _cache = new(); public Result GetOrCompute(ObjA a, ObjB b, ObjC c) { var key = new CombinedKey(a, b, c); return _cache.GetValue(key, _ => ComputeResult(a, b, c)); } private Result ComputeResult(ObjA a, ObjB b, ObjC c) { return new Result(); } }
注意事项
- 每次调用都会创建新的
CombinedKey实例,需要通过Equals方法匹配缓存中的键,性能略低于嵌套表方案。 - 必须使用
WeakReference持有输入对象,否则组合键会强引用输入对象,导致它们无法被垃圾回收。 - 哈希码需预先计算并缓存,避免每次调用
GetHashCode时尝试获取已回收的对象。
内容的提问来源于stack exchange,提问作者cmeeren
相关产品推荐
相关产品推荐

