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 == as Object and do a type check.
  • If your class is public, remember: any instance of Foo may not be my implementation of Foo.
    • 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.