以Func为键缓存IEnumerable结果:哈希码重复及键选型难题求助
Func<T, bool>作为字典键的缓存问题 首先得搞清楚你遇到的核心问题:默认情况下,Func<T, bool>这类委托是引用类型,它的GetHashCode()和相等性判断都是基于对象引用的。这意味着:
- 即使两个谓词逻辑完全一样,只要是不同的委托实例(比如两个不同的lambda表达式),它们的哈希码就会不同;
- 但如果你发现不管什么谓词哈希码都相同,大概率是你不小心复用了同一个委托实例,或者在获取哈希码的方式上有问题(比如误把委托类型的哈希码当成了实例的?)
不过不管怎样,要实现“按谓词逻辑缓存结果”的需求,默认的字典键处理肯定不适用,得自定义相等比较器。下面给你两种可行的方案:
方案一:自定义相等比较器,基于表达式树解析谓词逻辑
如果你的谓词是可以转换成表达式树的(比如用Expression<Func<T, bool>>代替Func<T, bool>),那我们可以解析表达式树的结构来判断两个谓词是否逻辑等价,同时生成对应的哈希码。
代码实现步骤:
- 把缓存字典的键类型改成
Expression<Func<T, bool>> - 实现一个
ExpressionEqualityComparer<T>,用来比较表达式树的逻辑是否相同,并生成对应的哈希码
public class ExpressionEqualityComparer<T> : IEqualityComparer<Expression<Func<T, bool>>> { public bool Equals(Expression<Func<T, bool>> x, Expression<Func<T, bool>> y) { if (x == null && y == null) return true; if (x == null || y == null) return false; // 用表达式访问器遍历比较两个表达式的结构 return new ExpressionComparerVisitor().Visit(x.Body).Equals(new ExpressionComparerVisitor().Visit(y.Body)); } public int GetHashCode(Expression<Func<T, bool>> obj) { if (obj == null) return 0; // 基于表达式结构生成哈希码 return new ExpressionHashCodeVisitor().Visit(obj.Body).GetHashCode(); } // 自定义访问器,用于比较表达式结构 private class ExpressionComparerVisitor : ExpressionVisitor { private readonly List<object> _comparisonParts = new List<object>(); protected override Expression VisitConstant(ConstantExpression node) { _comparisonParts.Add(node.Value); return base.VisitConstant(node); } protected override Expression VisitBinary(BinaryExpression node) { _comparisonParts.Add(node.NodeType); base.Visit(node.Left); base.Visit(node.Right); return node; } protected override Expression VisitMember(MemberExpression node) { _comparisonParts.Add(node.Member.Name); _comparisonParts.Add(node.Member.DeclaringType); return base.VisitMember(node); } // 可以根据你的谓词场景,覆盖其他必要的Expression类型(比如MethodCall、Unary等) public override bool Equals(object obj) { if (obj is not ExpressionComparerVisitor other) return false; return _comparisonParts.SequenceEqual(other._comparisonParts); } public override int GetHashCode() { return HashCode.Combine(_comparisonParts); } } // 自定义访问器,用于生成哈希码 private class ExpressionHashCodeVisitor : ExpressionVisitor { private readonly HashCode _hashCode = new HashCode(); protected override Expression VisitConstant(ConstantExpression node) { _hashCode.Add(node.Value); return base.VisitConstant(node); } protected override Expression VisitBinary(BinaryExpression node) { _hashCode.Add(node.NodeType); base.Visit(node.Left); base.Visit(node.Right); return node; } protected override Expression VisitMember(MemberExpression node) { _hashCode.Add(node.Member.Name); _hashCode.Add(node.Member.DeclaringType); return base.VisitMember(node); } // 同样补充其他必要的Expression类型 public new int GetHashCode() { return _hashCode.ToHashCode(); } } }
然后你的缓存字典就可以这么定义:
IDictionary<Expression<Func<T, bool>>, IEnumerable<T>> Cache = new Dictionary<Expression<Func<T, bool>>, IEnumerable<T>>(new ExpressionEqualityComparer<T>());
这个方案的好处是能真正识别逻辑相同的谓词,哪怕是不同的委托实例;缺点是需要处理各种表达式类型,实现起来有点繁琐,而且解析表达式树会有一定性能开销。
方案二:给谓词分配唯一标识,用标识作为字典键
如果你的谓词是预先定义好的(不是动态生成的),可以给每个谓词关联一个唯一的字符串或枚举值,用这个标识作为字典的键。比如:
代码示例:
// 先定义所有可能的谓词标识 public enum PredicateKey { ActiveUsers, InactiveUsers, UsersWithOrders } // 然后维护一个谓词和标识的映射 private readonly Dictionary<PredicateKey, Func<User, bool>> _predicateMap = new() { { PredicateKey.ActiveUsers, u => u.IsActive }, { PredicateKey.InactiveUsers, u => !u.IsActive }, { PredicateKey.UsersWithOrders, u => u.Orders.Count > 0 } }; // 缓存字典用标识作为键 private readonly Dictionary<PredicateKey, IEnumerable<User>> _cache = new(); // 使用的时候 public IEnumerable<User> GetUsers(PredicateKey key) { if (_cache.TryGetValue(key, out var result)) { return result; } var predicate = _predicateMap[key]; var users = // 从数据库获取数据并过滤的逻辑 _cache[key] = users; return users; }
这个方案简单高效,适合谓词数量固定、可预先定义的场景;但如果你的谓词是动态生成的(比如用户输入的条件转换的),这个方案就不适用了。
补充:为什么默认的Func<T, bool>哈希码会有问题?
再回头说下你遇到的“无论传入何种谓词哈希码都相同”的问题——如果你的代码里是每次创建新的Func实例(比如每次调用方法时都写u => u.IsActive),那默认的GetHashCode()应该是不同的,因为每个lambda都是新的委托实例。如果出现哈希码相同的情况,可能是:
- 你不小心把同一个委托实例传给了不同的调用;
- 你错误地获取了
typeof(Func<T, bool>).GetHashCode()而不是实例的GetHashCode(); - 某些特殊场景下,编译器对相同的lambda做了缓存(比如在同一个方法里的相同lambda可能被复用),但这种情况很少见。
如果想验证,可以在获取哈希码的时候打印委托实例的ReferenceEquals结果,看看是不是同一个实例。
内容的提问来源于stack exchange,提问作者Francis Ducharme

