site-www
site-www copied to clipboard
Best practice example for equals and hashCode (in inheritance)
What information needs to be added?
For a better understanding, the documentation should have information or rather an example about how to override operator == and hashCode. Especially in case of inheritance a small example would be great.
I've been researching this topic for the last two hours and could not find a good source for a best practice on this topic. There are several StackOverflow posts that differ a lot. Looking at Flutter, Linter-Rules or IntelliJ sources, you will find very different approaches to both operations.
From one Flutter source (MagnifierDecoration and its superclass ShapeDecoration) I've come up with the following but I'm still not sure if I've forgotten anything:
class Foo {
Foo(this.a, this.b);
final int a;
final int b;
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (runtimeType != other.runtimeType) return false;
return other is Foo && a == other.a && b == other.b;
}
@override
int get hashCode => Object.hash(a, b);
}
class Bar extends Foo {
Bar(super.a, super.b, this.c);
final int c;
@override
bool operator ==(Object other) {
return super == other && other is Bar && c == other.c;
}
@override
int get hashCode => Object.hash(super.hashCode, c);
}
class Baz extends Bar {
Baz(super.a, super.b, super.c, this.d);
final int d;
@override
bool operator ==(Object other) {
return super == other && other is Baz && d == other.d;
}
@override
int get hashCode => Object.hash(super.hashCode, d);
}
I've omitted calls to identical() and the comparison of runtimeType and moved them to the superclass. But maybe to minimize confusion they should be in every equals-override.
Where should this new content appear?
No response
I would like to fix this problem.
- [ ] I will try and fix this problem on dart.dev.
Thanks for opening an issue and providing the example snippets!
@lrhn Could you speak to any best practices here and what would make sense for us to highlight on the site? Thanks :)
There are different approaches because one size does not fit all. What you want to do very much depends on the class and class hierarchy that you're adding them to.
The runtimeType check in particular is often unncessary, or even counter-productive.
It makes sense in Flutter Widget hierarchies because those contain concrete super-classes and subclasses, whose instances should never be considered equal.
But it also prevents mocking, because a mock would have a different runtimeType than the type it mocks. (Unless the mock also mocks the runtimeType getter, which I don't expect them to do.)
In class hierarchies with only abstract superclasses, and concrete leaf-classes, I'd not include the runtimeType check.
I might not add == declarations to the superclasses at all, depending on how many fields they define (often such superinterfaces don't have fields either, just abstract interface getters).
So, for a stand-alone value class, I'd do:
final class Point {
final int x;
final int y;
const Point(this.x, this.y);
@override
int get hashCode => Object.hash(x, y);
@override
bool operator ==(Object other) =>
identical(this, other) || other is Point && x == other.x && y == other.y;
Anything more would be unnecessary overhead.
For a Widget-hierarchy, I might do something like the example above, but would probably rewrite hashCode in every class, as:
class Bar extends Foo {
...
@override
int get hashCode => Object.hash(a, b, c);
}
to not get two calls to Object.hash where only one is needed.
I'd also consider rewriting == for the same reasons, to not do repeated is-checks:
@override
bool operator ==(Object other) {
return identical(this, other) || this.runtimeType == other.runtimeType &&
other is Bar && c == other.a && b == other.b && c == other.c;
}
Doing both an is Foo and is Bar (very likely containing type arguments, which makes them more expensive) is unnecessary, since the latter implies the former.
(So yes, maybe the runtime-type check and is check should be in every ==, but then you definitely shouldn't do super == ....)
For other hierarchies, where subclasses might be different implementations of the same logical thing, which can be equal to each other, I'd not use runtimeType. I might implement the == in the superclass.
abstract interface class Color {
abstract final int red, green, blue;
int get hashCode => Object.hash(red, green, blue);
bool operator==(Object other) => identical(this, other) ||
other is Color && red == other.red && green == other.green && blue == other.blue;
}
final class RgbColor implements Color {
final int red, green, blue;
RgbColor(this.red, this.green, this.blue);
String toString() => "rgb($red, $green, $blue)";
}
final class WebColor implements Color {
final String _color
int get red => int.parse(_color.substring(1, 3), radix: 16);
int get green => int.parse(_color.substring(3, 5), radix: 16);
int get blue => int.parse(_color.substring(5, 7), radix: 16);
WebColor(String webColor) : _color = _validateInput(webColor); // checks format is #HHHHHH
String toString() => color;
}
In short: How to write == depends on your class and any other classes it expects to be compared to, and whether it should be possible to be equal to instances of other classes. It's a domain choice how to implement equals, not a general design choice. There is no simple answer which is always optimal.
The tradeoffs are:
- Check
runtimeType: Easy way to prevent the "colorpoint problem" (asymmetric equals). Costs extra (but not as much as it used to). Prevents mocking or any other class which wants to be equal. - Don't: Easier, shorter, let's others decide whether their class should be equal or not. Better suited for "value types" where equality is defined in terms of having the same public state.
- No
==at all, meaning falling back on identity only. Best choice for mutable classes, and classes which has no intrinsic equality. A database connection doesn't need==at all. (Maybe, unless it wants to be able to have different objects represent the same connection, but only the person designing the API can know for sure.)