Equatable是否需不可变?求非不可变致哈希错误的真实场景
Equatable类的不可变性与哈希错误场景
实现Equatable的类是否必须是不可变的?是否存在因不遵循不可变原则导致哈希结果错误的真实场景?我编写了测试用例测试非不可变类,未出现问题,HashSet也能正常工作,请展示一个简单的真实场景示例。
我提供的测试用例(无异常场景)
import 'dart:collection'; import 'package:equatable/equatable.dart'; import 'package:flutter_test/flutter_test.dart'; class _EquatableTest extends Equatable { final HashSet values; const _EquatableTest({required this.values}); @override List<Object> get props => [values]; _EquatableTest copyWith({ HashSet? values, }) { return _EquatableTest( values: values ?? this.values, ); } } class _EquatableTestNonImmutable extends Equatable { HashSet values; _EquatableTestNonImmutable({required this.values}); @override List<Object> get props => [values]; } class _AdditionalValue extends Equatable { HashSet<String> letters; _AdditionalValue({required this.letters}); void add(String letter) { letters.add(letter); } @override List<Object> get props => [letters]; } void main() { test('NonImmutable created same with _AdditionalValue value', () { _EquatableTest equatableImmune1; _EquatableTest equatableImmune2; equatableImmune1 = _EquatableTest(values: HashSet<_AdditionalValue>()); equatableImmune2 = _EquatableTest(values: HashSet<_AdditionalValue>()); equatableImmune1.values.add(_AdditionalValue(letters: HashSet.from(['a', 'b']))); equatableImmune1.values.add(_AdditionalValue(letters: HashSet.from(['c', 'd']))); equatableImmune2.values.add(_AdditionalValue(letters: HashSet.from(['a', 'b']))); equatableImmune2.values.add(_AdditionalValue(letters: HashSet.from(['c', 'd']))); expect(equatableImmune1, equatableImmune2); }); test('NonImmutable changed same with _AdditionalValue value', () { _EquatableTest equatableImmune1; _EquatableTest equatableImmune2; equatableImmune1 = _EquatableTest(values: HashSet<_AdditionalValue>()); equatableImmune2 = _EquatableTest(values: HashSet<_AdditionalValue>()); _AdditionalValue value1 = _AdditionalValue(letters: HashSet<String>()); _AdditionalValue value2 = _AdditionalValue(letters: HashSet<String>()); value1.add('a'); value2.add('a'); equatableImmune1.values.add(value1); equatableImmune2.values.add(value2); expect(equatableImmune1, equatableImmune2); if (equatableImmune1.values.first.runtimeType == _AdditionalValue && equatableImmune2.values.first.runtimeType == _AdditionalValue) { equatableImmune1.values.first.add('b'); equatableImmune2.values.first.add('b'); } expect(equatableImmune1, equatableImmune2); expect(equatableImmune1.hashCode, equatableImmune2.hashCode); }); }
问题解答
实现Equatable的类并非强制不可变,但强烈建议设计为不可变。Equatable依赖
props中的属性计算哈希值与判断相等性,若这些属性可变,当对象已存入依赖哈希的集合(如HashSet、HashMap)后修改属性,会直接导致哈希值变化,破坏集合内部结构,引发无法查找对象、重复存储等异常。你提供的测试用例未出现问题,是因为仅在修改后比较两个对象的相等性,未涉及对象存入集合后修改属性再执行查找/存在性判断的场景——这才是哈希错误高发的真实场景。
真实错误场景示例
以下代码展示了可变Equatable对象存入HashSet后,修改属性导致无法正确查找的问题:
import 'dart:collection'; import 'package:equatable/equatable.dart'; class MutableUser extends Equatable { String name; int age; MutableUser({required this.name, required this.age}); @override List<Object> get props => [name, age]; } void main() { final user = MutableUser(name: "Alice", age: 25); final hashSet = HashSet<MutableUser>(); // 将对象存入HashSet hashSet.add(user); print("存入后集合是否包含该用户:${hashSet.contains(user)}"); // 输出:true // 修改对象的props属性 user.age = 26; // 此时查找会失败,因为哈希值已变化 print("修改后集合是否包含该用户:${hashSet.contains(user)}"); // 输出:false // 甚至可以重复添加同一个对象,集合会认为是新元素 hashSet.add(user); print("修改后再次添加,集合大小:${hashSet.length}"); // 输出:2 }
原因分析
- 对象存入
HashSet时,集合会根据当时的哈希值计算存储位置; - 修改
age后,对象的哈希值随props变化而改变; - 再次调用
contains时,集合用新哈希值查找,无法匹配原存储位置的对象,导致查找失败; - 重复添加时,新哈希值对应新存储位置,集合判定为新对象,最终造成重复存储。
内容的提问来源于stack exchange,提问作者Nikita Shadkov
相关产品推荐
相关产品推荐

