为何我的Dictionary<ValidationTarget, List<ValidationDirective>>抛出KeyNotFoundException?
问题场景
遍历自定义字典ValidationDictionary的Keys集合,直接用遍历得到的key取值时抛出KeyNotFoundException,代码如下:
foreach (var key in this.Keys) { List<ValidationDirective> test = this[key]; }
ValidationDictionary继承自Dictionary<ValidationTarget, List<ValidationDirective>>,并使用自定义的ValidationTarget.EqualityComparer初始化。
核心问题分析
1. 自定义相等比较器的Equals逻辑完全错误
ValidationTarget.EqualityComparer.Equals方法通过比较两个对象的哈希码来判断相等,这是严重错误的:哈希码的设计目标是快速分组,不同对象完全可能产生相同的哈希码(哈希冲突),仅靠哈希码相等无法证明对象逻辑相等。这会导致字典在键匹配判断时出现误判。
2. ValidationTarget的Equals方法存在无限递归
ValidationTarget重写的Equals(object obj)方法中,当参数是ValidationTarget实例时,调用validationTargetObject.Equals(this),触发对方实例的Equals方法,进而形成无限递归。虽然这通常会导致栈溢出,但在你的场景中,哈希码不匹配可能导致查找过程在触发Equals之前就失败,从而抛出KeyNotFoundException。
3. 可变键对象的哈希码变化
ValidationTarget的GetHashCode方法依赖TargetId、TargetTag、ColumnName、RowIndex等字段,如果这些字段在对象被加入字典后被修改,对象的哈希码会发生变化。字典是根据对象加入时的哈希码分配哈希桶的,修改后查找时计算的哈希码与存储的哈希码不一致,导致字典无法在正确的桶中找到该键,即使key是从Keys集合中取出的。
修复方案
1. 修正自定义相等比较器的实现
确保Equals逻辑基于对象的实际字段,GetHashCode与Equals逻辑保持一致:
public class ValidationTarget { public class EqualityComparer : IEqualityComparer<ValidationTarget> { public bool Equals(ValidationTarget x, ValidationTarget y) { if (ReferenceEquals(x, y)) return true; if (x is null || y is null) return false; return x.TargetId == y.TargetId && x.TargetTag == y.TargetTag && x.ColumnName == y.ColumnName && x.RowIndex == y.RowIndex; } public int GetHashCode(ValidationTarget obj) { if (obj is null) return 0; unchecked { const int hashBase = 10376669; const int hashMultiplier = 126036629; int hash = hashBase; hash = (hash * hashMultiplier) ^ obj.TargetId.GetHashCode(); hash = (hash * hashMultiplier) ^ obj.TargetTag.GetHashCode(); hash = (hash * hashMultiplier) ^ obj.ColumnName.GetHashCode(); hash = (hash * hashMultiplier) ^ obj.RowIndex.GetHashCode(); return hash; } } } }
2. 修复ValidationTarget的Equals递归问题
实现正确的实例相等比较,同时实现IEquatable<ValidationTarget>接口提升性能:
public class ValidationTarget : IEquatable<ValidationTarget> { // ... 原有字段和方法 ... public bool Equals(ValidationTarget other) { if (other is null) return false; if (ReferenceEquals(this, other)) return true; return TargetId == other.TargetId && TargetTag == other.TargetTag && ColumnName == other.ColumnName && RowIndex == other.RowIndex; } public override bool Equals(object obj) { return Equals(obj as ValidationTarget); } public override int GetHashCode() { unchecked { const int hashBase = 10376669; const int hashMultiplier = 126036629; int hash = hashBase; hash = (hash * hashMultiplier) ^ this.TargetId.GetHashCode(); hash = (hash * hashMultiplier) ^ this.TargetTag.GetHashCode(); hash = (hash * hashMultiplier) ^ this.ColumnName.GetHashCode(); hash = (hash * hashMultiplier) ^ this.RowIndex.GetHashCode(); return hash; } } }
3. 确保键对象不可变(推荐)
将ValidationTarget中用于计算哈希码和判断相等的字段设置为readonly,避免对象加入字典后被修改,从根源上杜绝哈希码变化导致的查找问题。
内容的提问来源于stack exchange,提问作者Russ S.

