构建多维查找表时,嵌套字典的更优替代方案是什么?
嘿,太懂你这种被嵌套字典的ContainsKey层层判断搞得代码又长又啰嗦的感觉了!之前我做多维查找表的时候也这么写过,每次加数据或者查数据都得反复检查每一层键是否存在,烦得不行。结合C#的特性,给你几个更优雅的实现方案,你可以根据自己的场景选:
TryGetValue简化嵌套判断 其实不用每次都写ContainsKey再取值,TryGetValue可以把“判断键是否存在”和“获取对应值”一步搞定,能省不少重复代码。比如你的AddItem和GetItems可以改成这样:
public class MyLookupTable<T1, T2, T3> { private readonly IDictionary<T1, IDictionary<T2, ISet<T3>>> _mapOfMapOfSequences = new Dictionary<T1, IDictionary<T2, ISet<T3>>>(); public void AddItem(T1 key1, T2 key2, T3 myItem) { // 检查第一层字典,不存在就创建 if (!_mapOfMapOfSequences.TryGetValue(key1, out var innerDict)) { innerDict = new Dictionary<T2, ISet<T3>>(); _mapOfMapOfSequences[key1] = innerDict; } // 检查第二层字典,不存在就创建集合 if (!innerDict.TryGetValue(key2, out var itemSet)) { itemSet = new HashSet<T3>(); innerDict[key2] = itemSet; } itemSet.Add(myItem); } public IEnumerable<T3> GetItems(T1 key1, T2 key2) { // 一层一层找,找不到就返回空序列 if (_mapOfMapOfSequences.TryGetValue(key1, out var innerDict) && innerDict.TryGetValue(key2, out var itemSet)) { return itemSet; } return Enumerable.Empty<T3>(); } }
这样代码比原来的ContainsKey写法清爽多了,还避免了重复查找键的开销。
如果这种多维查找的场景你经常用到,不如把所有嵌套逻辑都封装成一个独立的类,对外只暴露简单的API,以后用起来就不用再写重复的判断了。比如:
public class TwoKeyLookup<TKey1, TKey2, TValue> { private readonly Dictionary<TKey1, Dictionary<TKey2, HashSet<TValue>>> _innerLookup = new Dictionary<TKey1, Dictionary<TKey2, HashSet<TValue>>>(); // 添加元素 public void Add(TKey1 key1, TKey2 key2, TValue value) { if (!_innerLookup.TryGetValue(key1, out var innerDict)) { innerDict = new Dictionary<TKey2, HashSet<TValue>>(); _innerLookup[key1] = innerDict; } if (!innerDict.TryGetValue(key2, out var valueSet)) { valueSet = new HashSet<TValue>(); innerDict[key2] = valueSet; } valueSet.Add(value); } // 获取元素 public IEnumerable<TValue> Get(TKey1 key1, TKey2 key2) { return _innerLookup.TryGetValue(key1, out var innerDict) && innerDict.TryGetValue(key2, out var valueSet) ? valueSet : Enumerable.Empty<TValue>(); } // 还可以扩展其他方法,比如检查元素是否存在 public bool ContainsValue(TKey1 key1, TKey2 key2, TValue value) { return _innerLookup.TryGetValue(key1, out var innerDict) && innerDict.TryGetValue(key2, out var valueSet) && valueSet.Contains(value); } }
用的时候直接实例化这个类,调用Add和Get就行,完全不用关心内部的嵌套结构,代码会干净很多。而且以后要是需要扩展成三维键,修改这个类也很方便。
如果你的两个键T1和T2可以组合成一个元组,那直接用Dictionary<(T1, T2), ISet<T3>>代替嵌套字典是最省事的。把二维键变成一维,操作起来就跟普通字典一样简单:
public class MyLookupTable<T1, T2, T3> { private readonly Dictionary<(T1, T2), ISet<T3>> _lookup = new Dictionary<(T1, T2), ISet<T3>>(); public void AddItem(T1 key1, T2 key2, T3 myItem) { var compositeKey = (key1, key2); if (!_lookup.TryGetValue(compositeKey, out var itemSet)) { itemSet = new HashSet<T3>(); _lookup[compositeKey] = itemSet; } itemSet.Add(myItem); } public IEnumerable<T3> GetItems(T1 key1, T2 key2) { return _lookup.TryGetValue((key1, key2), out var itemSet) ? itemSet : Enumerable.Empty<T3>(); } }
这个方案的优势是结构扁平,代码最少。不过要注意元组的相等性:如果T1和T2是值类型或者已经正确重写了Equals和GetHashCode,那没问题;如果是自定义引用类型,要确保它们的相等判断符合你的业务预期。
Lookup(只读场景适用) 如果你的查找表是只读的,或者只需要一次性构建完成后查询,那可以试试System.Linq.Lookup。虽然它不直接支持多键,但可以把多个键组合成元组或者匿名类型作为键,用ToLookup方法构建:
// 假设你有一个数据源,比如 IEnumerable<(T1 Key1, T2 Key2, T3 Value)> dataItems var readOnlyLookup = dataItems.ToLookup(item => (item.Key1, item.Key2), item => item.Value); // 查询的时候直接用复合键 var results = readOnlyLookup[(targetKey1, targetKey2)]; // 返回IEnumerable<T3>,无匹配时返回空序列
不过Lookup是只读的,不能动态添加或删除元素,所以如果你的表需要频繁更新,这个方案就不适合了。
我个人比较推荐方案2或者方案3:如果以后可能扩展更多维度的键,方案2的自定义类扩展性更好;如果只是固定二维键,方案3的元组键字典最简洁。
内容的提问来源于stack exchange,提问作者Vesuvian

