Dart中使用Set作为Map键的取值问题及内置解决方案咨询
使用Set作为Dart Map键的问题与解决方案
你碰到的这个问题其实挺典型的——用Set当Dart Map的键时存进去取不出来,核心原因不是Set的==判断出了问题,而是它的hashCode计算逻辑!
先给你扒清楚问题根源:Dart里默认的Set(也就是HashSet),虽然两个包含相同元素的Set用==比较会返回true,但它们的hashCode却不一定相同——因为这个值是基于集合的内部存储结构计算的,而不是集合里的元素。而Map查找键的时候,会先匹配hashCode,再验证==,这就导致你用新创建的、元素完全一样的Set去查,根本找不到之前存的条目。
你可以自己跑个小例子验证下:
void main() { final set1 = {0}; final set2 = {0}; print(set1 == set2); // 输出 true,元素相同所以相等 print(set1.hashCode == set2.hashCode); // 输出 false!这就是问题的关键 }
那有没有内置方法能解决这个问题?当然有!Dart标准库提供了UnmodifiableSetView,它是一个不可变的Set视图,它的hashCode是基于内部元素计算的,==也会正确比较元素内容。
直接改你的代码就行:
// 先确保你的自定义类型重写了==和hashCode,否则元素相等性判断会出错 class MyType1 { final int id; MyType1(this.id); @override bool operator ==(Object other) => identical(this, other) || other is MyType1 && runtimeType == other.runtimeType && id == other.id; @override int get hashCode => id.hashCode; } class MyType2 { final String content; MyType2(this.content); } void main() { // 把Map的键类型改成UnmodifiableSetView<MyType1> final myObject = <UnmodifiableSetView<MyType1>, MyType2>{}; final myType1 = MyType1(100); final myType2 = MyType2("hello world"); // 插入时用UnmodifiableSetView包装你的Set myObject[UnmodifiableSetView({myType1})] = myType2; // 查找时同样用UnmodifiableSetView包装相同元素的Set final retrievedValue = myObject[UnmodifiableSetView({myType1})]; print(retrievedValue?.content); // 输出 "hello world",成功拿到值! }
如果你的场景里需要键是可变的Set(比如存进去之后还要修改Set里的元素),那UnmodifiableSetView就不适用了,这时候你得自己写一个自定义的Set子类,手动重写==和hashCode,让它们基于集合内的元素来计算。比如:
class HashableMutableSet<T> extends Set<T> { final Set<T> _inner = <T>{}; HashableMutableSet([Iterable<T>? elements]) { if (elements != null) _inner.addAll(elements); } // 委托所有Set的操作给_inner集合 @override bool add(T value) => _inner.add(value); @override bool contains(Object? element) => _inner.contains(element); @override bool remove(Object? value) => _inner.remove(value); @override Iterator<T> get iterator => _inner.iterator; @override T? lookup(Object? element) => _inner.lookup(element); @override Set<T> toSet() => _inner.toSet(); // 重写==,比较两个集合的元素是否完全一致 @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! HashableMutableSet) return false; return _inner.containsAll(other._inner) && other._inner.containsAll(_inner); } // 重写hashCode,基于所有元素的hashCode组合计算 @override int get hashCode { int result = 0; for (final element in _inner) { result ^= element.hashCode; } return result; } }
用这个自定义类的话,就可以直接把它当作Map的键,而且还能修改Set里的元素(不过要注意:修改键的元素会导致它的hashCode变化,这时候Map里的键就失效了,所以修改之后得重新存一遍):
void main() { final myObject = <HashableMutableSet<MyType1>, MyType2>{}; final keySet = HashableMutableSet([MyType1(100)]); myObject[keySet] = MyType2("test"); print(myObject[keySet]?.content); // 输出 "test" // 修改键的元素(注意:这会改变hashCode,之后原键在Map里找不到了) keySet.add(MyType1(200)); print(myObject[keySet]); // 输出 null // 所以修改后需要重新插入 myObject[keySet] = MyType2("updated"); print(myObject[keySet]?.content); // 输出 "updated" }
最后给你划个重点:
- 如果不需要修改作为键的Set,优先用内置的
UnmodifiableSetView,简单又安全 - 如果必须用可变Set当键,自定义Set子类重写
==和hashCode是唯一的办法,但要注意修改键元素后的风险
内容的提问来源于stack exchange,提问作者Valentin Vignal
相关产品推荐
相关产品推荐

