Equality and Dart
A month ago I asked a question on my blog about a piece of Dart code.
For the below class, what are at least two things wrong with the implementation of operator ==
.
class Foo {
int _a;
int _b;
int get a => _a;
int get b => _b;
...
bool operator ==(Foo other) {
return other._a == this._a && other._b == this._b;
}
}
A number of guesses were posted to the G+ discussion.
Context: designing types for reuse is tough
If Foo
is only used within one library – so it should be named _Foo
– the existing implementation might be totally fine.
But if Foo
is…
- Used a lot within a single library or package
- Part of a code base with a lot of developers
- Part of a public package
- Serves as a base class
…then you have to think a lot harder about its implementation.
Special Equality
The parameter to operator ==
should always be Object
.
Don’t believe me? Paste the code below into try.dartlang.org. I’ll wait.
void main() {
var items = [0, 1, new Bar(5), 6, 7];
print(items.contains(6));
}
class Bar {
final int value;
Bar(this.value);
bool operator ==(Bar other) => value == other.value;
}
operator ==
is used in Iterable
for contains
, where
, and many other methods. Map
also uses ==
for its final check after finding matching hash codes. If there is any chance that an instance of your class will have equality checked with any other type – either directly or via very common Dart classes – you should not make assumptions about the argument to ==
except that it’s not null
. Dart takes care of the null
check for you.
This is easy to fix:
class Bar {
final int value;
Bar(this.value);
bool operator ==(Object other) =>
other is Bar && value == other.value;
}
General Equality
Just like relativistic physics, the special case is pretty straight forward. The general case is a bear.
I read that Special Relativity took Einstein 8 weeks where General Relativity took him 8 years, but I digress.
The special case is a black-and-white comparison: is the operand the same type as this
or not. The general case introduces a pile of gray.
Dart has implicit interfaces. This means anyone can implement your Foo
or Bar
class.
Let’s say we fixed our original example for the special case:
class Foo {
int _a;
int _b;
int get a => _a;
int get b => _b;
bool operator ==(Object other) {
return other is Foo && other._a == this._a && other._b == this._b;
}
}
What if someone implements Foo
in another library.
import 'package:foolib/foolib.dart';
class MyFoo implements Foo {
int get a => 41;
int get b => 42;
}
Try doing if (foo == myFoo)
.
You’ll get a runtime error along the lines of MyFoo does not have instance property _a
.
General Equality: simple fix
The simple fix is, well, simple. Avoid using private members of objects you don’t completely control. In the case of ==
that means any private members on other
.
This problem goes beyond operator ==
and extends to any method anywhere in your library.
Consider the static distance
method below.
class MyPoint {
num _x, num _y;
num get x => _x;
num get y => _y;
static double distance(MyPoint a, MyPoint b) {
// should you access _x or _y here?
}
}
See what I mean?
General Equality: simple mistakes
Avoiding private members is only part of the problem. Let’s look at MyPoint
again.
class MyPoint {
final num x, y;
MyPoint(this.x, this.y);
bool operator ==(Object other) =>
other is MyPoint && other.x == x && other.y == y;
}
No private members. No type assumptions. We’re home free! Right?
Try this in try.dartlang.org:
class MyPoint {
final num x, y;
MyPoint(this.x, this.y);
bool operator ==(Object other) =>
other is MyPoint && other.x == x && other.y == y;
}
class MyPoint3 extends MyPoint {
final num z;
MyPoint3(num x, num y, this.z) : super(x, y);
bool operator ==(Object other) =>
other is MyPoint3 && other.x == x && other.y == y && other.z == z;
}
void main() {
var mp = new MyPoint(1,2);
var mp3 = new MyPoint3(1,2,3);
print(mp == mp3); // true
print(mp3 == mp); // false
}
You’ve just broken one of the three laws of equality.
Cue mass hysteria.
There are solutions to this problem, but they are not simple. We’ll have to bother Florian Loitsch for details.
…and I haven’t even gotten into implementing hashCode
and handling mutable fields – here’s a taste.
Now what?
Keep calm, but be paranoid.
- Be careful when implementing
==
.- Make sure it’s something you want to sign up for.
- Read up on implementing
hashCode
correctly. - If you’re class has mutable state, be afraid!
- Always type the argument to
operator ==
asObject
and do a type check. - If your class is public, remember: any instance of
Foo
may not be my implementation ofFoo
.- Stick to the public interface.
- Document what you consider equality.
An alternative: public class, private implementation
If you’ve dug around the Dart source code, you’ve likely seen this pattern:
abstract class SomeClass {
int get a;
String get b;
bool someMethod();
factory SomeClass(int a, String b) => new _SomeClassImpl(a, b);
}
Besides allowing us to efficiently support both the Dart virtual machine and compiling to Javascript, this pattern also allows one to detect random implementations of a type.
class _SomeClassImpl implements SomeClass {
final int a;
final String b;
bool someMethod() ...
_SomeClassImpl(this.a, this.b);
// No impersonators allowed
bool operator ==(Object other) =>
other is _SomeClassImpl && other.a == a && other.b == b;
}
This pattern also allows you to lock down an object system where an arbitrary instance won’t fly.
Future
and Zone
are examples from the Dart SDK. You won’t get very far trying to implement or extend either of them – at least if you try to use them in place of their native implementations.
This post was edited and published using StackEdit. Freakin’ awesome. I may not ditch blogger after all.