You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
}

原因分析

  1. 对象存入HashSet时,集合会根据当时的哈希值计算存储位置;
  2. 修改age后,对象的哈希值随props变化而改变;
  3. 再次调用contains时,集合用新哈希值查找,无法匹配原存储位置的对象,导致查找失败;
  4. 重复添加时,新哈希值对应新存储位置,集合判定为新对象,最终造成重复存储。

内容的提问来源于stack exchange,提问作者Nikita Shadkov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 15:58:10