在Dart中覆盖hashcode的好方法是什么?

99

我发现自己想要覆盖对象的hashcode和==方法,想知道如何实现依赖于多个属性的hashcode的最佳实践,似乎有一些Dart特定的考虑。

最简单的答案是将所有属性的哈希值进行异或运算,这可能并不太糟糕。在Dart Up and Running中也有一个示例,链接为https://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html

  // Override hashCode using strategy from Effective Java, Chapter 11.
 int get hashCode {
   int result = 17;
   result = 37 * result + firstName.hashCode;
   result = 37 * result + lastName.hashCode;
   return result;
 }

但这似乎期望截断整数语义,在Dart中超出JS整数范围对于哈希来说似乎不好。

我们也可以这样做,在每次操作后将其截断为32位。

对于我的应用程序,集合的预期大小非常小,几乎任何方法都可以,但我很惊讶没有看到一般情况下的标准方法。 有谁有任何相关经验或强烈推荐吗?


1
我差点因为你说“食谱”而给你打-1分,但由于你的问题质量很好,所以我不会打-1分。当然,在你的标题中应该使用的术语是算法,甚至是“方法”(比如,一个好的方法或者一个好的算法),而不是“好的食谱”。我可以理解如果你认为算法这个词听起来像炫耀。你可以用“方法”,但是使用“食谱”这个词太过无聊了。 - barlop
也许可以在其他支持任意精度整数的编程语言中寻找示例。我认为Smalltalk有这些功能。 - Greg Lowe
1
Dart的整数很有趣,因为它们在VM上是任意的,但如果你编译到JavaScript,你会得到JavaScript的限制。所以你想要截断,有趣的问题是在哪里和如何最好地做到这一点。这可能是移位/乘法、XOR和截断的某种组合。这也是Smalltalk倾向于做的事情。魔鬼在细节中。 - Alan Knight
6个回答

78

quiver提供了一些辅助函数hash2hash3等,可以简化实现hashCode的任务,并确保在Dart VM下以及编译为JavaScript时都可以正常工作。

import 'package:quiver/core.dart';

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  bool operator ==(o) => o is Person && name == o.name && age == o.age;
  int get hashCode => hash2(name.hashCode, age.hashCode);
}

另外请参阅此帖子,其中讨论略微更长。


谢谢。正是我正在寻找的。 - Alan Knight
1
请记得将quiver添加为依赖项。在您的pubspec.yaml文件中:quiver: '>=0.18.0<0.19.0' - Joel Sjögren
3
hash2(name, age)就足够了吗? - Robin Dijkhof
hash2() 添加 import 'package:quiver/core.dart'; - Kirill Karmazin
7
自 Dart 2.14 开始,可以使用 Object.hash() 方法:https://dart.dev/guides/libraries/library-tour#implementing-map-keys。请参考 manikanta 的回答。 - BbL

72
自从2.14版本以来,Dart语言增加了对 Object.hash()Object.hashAll()Object.hashAllUnordered() 的支持。 hash() 文档:

为一些对象创建组合哈希码。

示例:
class SomeObject {
  final Object a, b, c;
  SomeObject(this.a, this.b, this.c);

  bool operator==(Object other) =>
      other is SomeObject && a == other.a && b == other.b && c == other.c;

  int get hashCode => Object.hash(a, b, c); // <----- here
}

实现说明:

此函数生成的哈希值不能保证在同一程序的不同运行或在同一程序的不同隔离中运行的代码之间稳定。所使用的确切算法可能因不同平台或平台库的不同版本而有所不同,并且它可能依赖于每次程序执行时更改的值。


8
应该给这个点赞。这是推荐的方法:https://dart.dev/guides/libraries/library-tour#implementing-map-keys。 - BbL
1
如果这个类有父类,那我能不能用Object.hash(a, b, c, super)呢? - nuynait
1
@nuynait 不可以单独使用 super 作为有效的表达式。我认为最好的选择是:Object.hash(super.hashCode, a, b, c) - Anakhand

42

为了尽量减少依赖,如果你已经依赖flutter但未依赖像quiver这样的东西,那么dart:ui库包含实用程序hashValueshashList用于创建和组合哈希值。如果组合列表值,则必须注意确保等号运算符和哈希码匹配行为。如果哈希码深度计算其哈希,则使用深层相等性;否则,使用浅层相等性。

class Example {
    final String value1;
    final Object value2;
    final List<Object> deep;
    final List<Object> shallow;

    Example({this.value1, this.value2, this.deep, this.shallow});

    @override
    operator ==(o) =>
        o is Example &&
        o.value1 == value1 &&
        o.value2 == value2 &&
        listEquals(o.deep, deep) &&
        o.shallow == shallow;

    @override
    int get hashCode => hashValues(value1, value2, hashList(deep), shallow);
}

Flutter 的 hashValues API 文档

Flutter 的 hashList API 文档


3
太棒了,尽量减少依赖一直是我的兴趣所在! - WSBT
1
真的,你的回答救了我,非常感谢。 - poonam kalra
1
hashValues 保证对于两个不同的参数返回不同的哈希值吗?或者它是否在极大程度上减少了这种风险? - SametSahin
不能保证hashValues会为两个不同的参数返回不同的哈希值,因为键空间通常比对象的值空间小,没有哈希函数可以做到这一点。但据我所知,该函数旨在最大程度地减少碰撞的可能性,同时最小化计算复杂度。 - Daniel Brotherston
3
请注意,现在hashValues和hashList都已被弃用。请改用Object.hashAll()和Object.hashAllUnordered():https://api.flutter.dev/flutter/dart-ui/hashList.html - TarHalda

19

这个equatable软件包可以提供帮助

import 'package:equatable/equatable.dart';

class Person extends Equatable {
  final String name;
  final int age;

  Person(this.name, this.age);

  @override
  List<Object> get props => [name, age];
}

现在Person将使用从Equatable继承的==hashCode,它需要您提供的props列表


很遗憾,当您使用provider StateManagement时,这无法与继承自ChangeNotifier的模型一起使用。 - NiklasLehnfeld
2
@NiklasLehnfeld 如果你需要扩展另一个类,你可以使用mixin。例如:class MyModel extends ChangeNotifier with EquatableMixin - Pavel
1
这仅适用于不可变对象,因此所有成员变量必须是 final。如果您的类对象随时间而变化,则不是很有用。 - Teh Sunn Liu
1
@NiklasLehnfeld,你不应该从ChangeNotifier扩展模型。你做错了什么。模型应该是不可变的。如果你想要响应式字段,请考虑使用MobX或Get。 - Sergey Molchanovsky

12

我推荐使用 "equatable" 插件。

https://pub.dev/packages/equatable

示例:

原始模式:

class Person {
  final String name;

  const Person(this.name);

  @override
  bool operator ==(Object other) =>
    identical(this, other) ||
    other is Person &&
    runtimeType == other.runtimeType &&
    name == other.name;

  @override
  int get hashCode => name.hashCode;
}

使用Equatable:

import 'package:equatable/equatable.dart';

class Person extends Equatable {
  final String name;

  Person(this.name);

  @override
  List<Object> get props => [name];
}

-1
由于Dart与Java非常相似,因此您肯定可以找到适用于Dart的好的Java散列码参考资料。 通过一些谷歌搜索,我找到了Java的的Wikipedia页面。它提供了一个简单对象的哈希码的基本示例。一种流行的方法是使用质数(不同的质数)进行乘法,并为对象的每个属性添加一些值。 例如这个问题解释了为什么在String.hashCode()方法中选择数字31。 通过谷歌可以轻松找到更详细的哈希码实现示例。

3
我不知道Dart与Java如此相似。特别是在处理整数类型方面有所不同,如果将Java的常用哈希函数应用于大量内容,则可能导致超出JavaScript整数范围,这将是不好的。 - Alan Knight
Java的整数也会溢出。您可能希望使用dart:typed_data包中的int32或int64,以使您的哈希码保持一致。 - Steven Roose
1
Java中的整数会通过包装来溢出。Javascript中的整数(以及编译为Javascript的Dart整数)会通过转换为双精度浮点数来溢出,这对于哈希而言更加糟糕。使用int32数据类型可以解决问题,但是dart:typed_data没有单个int类型,只有集合类型。 - Alan Knight

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,