Why is this happening?
Because the rules of the language speak.
You have provided an operator with this signature:
public static bool operator ==(Foo f1, Foo f2)
and then - wherever this happens in the code - you get this expression:
f1 == null
where f1
has the compile time type Foo
. Now null
implicitly converted to Foo
, so why not use your operator? And if you have the first line of your statement unconditionally calling itself, you should expect a stack overflow ...
To prevent this from happening, you will need one of two changes in the language:
- The language should have a special case, which meant
==
when it was used in the declaration for ==
. Hk. - The language would have to decide that any
==
expression with one null
operand always meant a comparison of references.
Nothing special, IMO. Avoiding this by simply avoiding redundancy and adding optimization:
public static bool operator ==(Foo f1, Foo f2) { if (object.ReferenceEquals(f1, f2)) { return true; } if (object.ReferenceEquals(f1, null) || object.ReferenceEquals(f2, null)) { return false; } return f1.Equals(f2); }
However, then you need to fix your Equals
method, because then it will return to your ==
, which will lead to another stack overflow. You never really talked about how you want to define equality ...
I usually had something like this:
// Where possible, define equality on sealed types. // It gets messier otherwise... public sealed class Foo : IEquatable<Foo> { public static bool operator ==(Foo f1, Foo f2) { if (object.ReferenceEquals(f1, f2)) { return true; } if (object.ReferenceEquals(f1, null) || object.ReferenceEquals(f2, null)) { return false; } // Perform actual equality check here } public override bool Equals(object other) { return this == (other as Foo); } public bool Equals(Foo other) { return this == other; } public static bool operator !=(Foo f1, Foo f2) { return !(f1 == f2); } public override int GetHashCode() { // Compute hash code here } }
Please note that this only allows you to worry about validation checks in one place. To avoid over comparing f1
to null when it was called using the Equals
instance method to start, you can delegate ==
to Equals
after checking for the invalidity of f1
, but I'd probably stick with this.