8

I have a very strange situation I don't understand. Below is the case simplified:

double? d = 2;
int? i = 2;

Console.WriteLine(d.Equals((2))); // false
Console.WriteLine(i.Equals((2))); // true

I don't understand why one expression will net me true and another false. They seem identical.

Iteria
  • 421
  • 1
  • 5
  • 13
  • They may seem identical as text, but their binary representations are very different. See [this answer](https://stackoverflow.com/a/1650120/9091276) for more info – Frontear Sep 26 '18 at 18:06
  • 2
    This question is not a duplicate of the question about how you compare doubles to ints. The issue here is that comparing a **nullable** double to an int gives one answer and comparing the *same* double as non-nullable gives a different answer. – Eric Lippert Sep 26 '18 at 19:51
  • 2
    The downvotes on this question are unwarranted. The question is clear, and it is by no means obvious what rules of C# are causing this behaviour. This behaviour is an example of an egregious design error in C#; how C# handles equality is terrible. – Eric Lippert Sep 26 '18 at 19:56
  • int32.equals will return false if the other value is not int32 https://referencesource.microsoft.com/#mscorlib/system/int32.cs,225942ed7b7a3252 – pm100 Sep 26 '18 at 19:57

2 Answers2

14

You are completely right to find this confusing. It is a mess.

Let's start by clearly saying what happens by looking at more examples, and then we will deduce the correct rules that are being applied here. Let's extend your program to consider all these cases:

    double d = 2;
    double? nd = d;
    int i = 2;
    int? ni = i;
    Console.WriteLine(d == d);
    Console.WriteLine(d == nd);
    Console.WriteLine(d == i);
    Console.WriteLine(d == ni);
    Console.WriteLine(nd == d);
    Console.WriteLine(nd == nd);
    Console.WriteLine(nd == i);
    Console.WriteLine(nd == ni);
    Console.WriteLine(i == d);
    Console.WriteLine(i == nd);
    Console.WriteLine(i == i);
    Console.WriteLine(i == ni);
    Console.WriteLine(ni == d);
    Console.WriteLine(ni == nd);
    Console.WriteLine(ni == i);
    Console.WriteLine(ni == ni);
    Console.WriteLine(d.Equals(d));
    Console.WriteLine(d.Equals(nd));
    Console.WriteLine(d.Equals(i));
    Console.WriteLine(d.Equals(ni)); // False
    Console.WriteLine(nd.Equals(d));
    Console.WriteLine(nd.Equals(nd));
    Console.WriteLine(nd.Equals(i)); // False
    Console.WriteLine(nd.Equals(ni)); // False
    Console.WriteLine(i.Equals(d)); // False
    Console.WriteLine(i.Equals(nd)); // False
    Console.WriteLine(i.Equals(i)); 
    Console.WriteLine(i.Equals(ni));
    Console.WriteLine(ni.Equals(d)); // False
    Console.WriteLine(ni.Equals(nd)); // False
    Console.WriteLine(ni.Equals(i)); 
    Console.WriteLine(ni.Equals(ni));

All of those print True except the ones I have notated as printing false.

I'll now give an analysis of those cases.

The first thing to notice is that the == operator always says True. Why is that?

The semantics of non-nullable == are as follows:

int == int -- compare the integers
int == double -- convert the int to double, compare the doubles
double == int -- same
double == double -- compare the doubles

So in every non-nullable case, integer 2 is equal to double 2.0 because the int 2 is converted to double 2.0, and the comparison is true.

The semantics of nullable == are:

  • If both operands are null, they're equal
  • If one is null and the other is not, they're unequal
  • If both are not null, then fall back to the non-nullable case above.

So again, we see that for the nullable comparisons, int? == double?, int? == double, and so on, we always fall back to the non-nullable cases, convert the int? to double, and do the comparison in doubles. Thus these are also all true.

Now we come to Equals, which is where things get messed up.

There is a fundamental design problem here, which I wrote about in 2009: https://blogs.msdn.microsoft.com/ericlippert/2009/04/09/double-your-dispatch-double-your-fun/ -- the problem is that the meaning of == is resolved based on the compile time types of both operands. But Equals is resolved on the basis of the run time type of the left operand (the receiver), but the compile time type of the right operand (the argument), and that's why things go off the rails.

Let's begin by looking at what double.Equals(object) does. If the receiver of a call to Equals(object) is double then if the argument is not a boxed double, they are considered not equal. That is, Equals requires that the types match, whereas == requires that the types be convertible to a common type.

I'll say that again. double.Equals does not try to convert its argument to double, unlike ==. It just checks to see if it already is double, and if it is not, then it says they are not equal.

That then explains why d.Equals(i) is false... but... wait a minute, it is not false above! What explains this?

double.Equals is overloaded! Above we are actually calling double.Equals(double), which -- you guessed it -- converts the int to a double before doing the call! If we had said d.Equals((object)i)) then that would be false.

All right, so we know why double.Equals(int) is true -- because the int is converted to double.

We also know why double.Equals(int?) is false. int? is not convertible to double, but it is to object. So we call double.Equals(object) and box the int, and now its not equal.

What about nd.Equals(object) ? The semantics of that is:

  • If the receiver is null and the argument is null, they are equal
  • If the receiver is not null then defer to the non-nullable semantics of d.Equals(object)

So now we know why nd.Equals(x) works if x is a double or double? but not if it is int or int?. (Though interestingly, of course (default(double?)).Equals(default(int?)) would be true since they are both null!)

Finally, by similar logic we see why int.Equals(object) gives the behaviour it has. It checks to see if its argument is a boxed int, and if it is not, then it returns false. Thus i.Equals(d) is false. The i cannot be converted to double, and the d cannot be converted to int.

This is a huge mess. We would like equality to be an equivalence relation, and it is not! An equality relationship should have these properties:

  • Reflexivity: a thing is equal to itself. That is usually true in C# though there are a few exceptions.
  • Symmetry: If A is equal to B then B is equal to A. That is true of == in C# but not of A.Equals(B), as we've seen.
  • Transitivity: If A equals B and B equals C then A also equals C. That is absolutely not the case in C#.

So, its a mess on all levels. == and Equals have different dispatch mechanisms and give different results, neither of them are equivalence relations, and it's all confusing all the time. Apologies for getting you into this mess, but it was a mess when I arrived.

For a slightly different take on why equality is terrible in C#, see item number nine on my list of regrettable language decisions, here: http://www.informit.com/articles/article.aspx?p=2425867

BONUS EXERCISE: Repeat the above analysis, but for x?.Equals(y) for the cases where x is nullable. When do you get the same results as for non-nullable receivers, and when do you get different results?

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
1

It looks like the answer is in the source for the Equals method on each of the types. If the types don't match then they are not equal.

https://referencesource.microsoft.com/#mscorlib/system/double.cs,147

// True if obj is another Double with the same value as the current instance.  This is
// a method of object equality, that only returns true if obj is also a double.
public override bool Equals(Object obj) {
    if (!(obj is Double)) {
        return false;
    }
    double temp = ((Double)obj).m_value;
    // This code below is written this way for performance reasons i.e the != and == check is intentional.
    if (temp == m_value) {
        return true;
    }
    return IsNaN(temp) && IsNaN(m_value);
}

https://referencesource.microsoft.com/#mscorlib/system/int32.cs,72

public override bool Equals(Object obj) {
    if (!(obj is Int32)) {
        return false;
    }
    return m_value == ((Int32)obj).m_value;
}
John Boker
  • 82,559
  • 17
  • 97
  • 130
  • Thanks, that's such a clear answer. Also, extremely unexpected. EDIT: actually I have to take that back, I'm not sure this answer applies to nullable – Iteria Sep 26 '18 at 20:13
  • @Iteria: This answer gets most of the way there. The other two things you have to know are: (1) `Nullable.Equals(object)` checks for nullity, and then defers to `object.Equals(object)` on the underlying type `T`, and (2) `double.Equals()` has overloads. – Eric Lippert Sep 26 '18 at 20:29
  • 1
    Ah, I see. Between this answer and your answer above. I think I have it. – Iteria Sep 27 '18 at 11:27