UPDATE from 2019: This post was originally on Coverity’s “Ask The Bug Guys” developer blog but that was taken down; an archived version is here. The formatting is still a bit messed up; I’ll fix it later.
Today’s episode of Ask The Bug Guys features a C# question from reader Jan:
Hi Bug Guys! We recently had a bug in our code comparing int
s and short
s, where calling Equals()
produced different results from using the ==
operator. Also, the behaviour of Equals()
was not symmetric. Here’s a code snippet that reproduces the behaviour we observed:
int myInt = 1;
short myShort = 1;
Console.WriteLine(myInt == myShort);
Console.WriteLine(myShort == myInt);
Console.WriteLine(myInt.Equals(myShort));
Console.WriteLine(myShort.Equals(myInt));
|
We were quite surprised when we found this. What explains this difference? Is it better to use ==
instead of Equals()
when comparing integer types?
Hi Jan! Thanks for the great question.
C# was designed to be a “pit of success” language: that is, a language where the obvious technique and the correct technique are the same. And for the most part, that’s true. Unfortunately, equality is one of the areas where there are significant pits of failure, and you’ve fallen right into one of them.
I’m going to add some additional cases to your program to illustrate a number of different equalities.
int myInt = 1;
short myShort = 1;
object objInt1 = myInt;
object objInt2 = myInt;
object objShort = myShort;
Console.WriteLine(myInt == myShort);
Console.WriteLine(myShort == myInt);
Console.WriteLine(myInt.Equals(myShort));
Console.WriteLine(myShort.Equals(myInt));
Console.WriteLine(objInt1 == objInt1);
Console.WriteLine(objInt1 == objShort);
Console.WriteLine(objInt1 == objInt2);
Console.WriteLine(Equals(objInt1, objInt2));
Console.WriteLine(Equals(objInt1, objShort));
|
What the heck? As it turns out, we have many different kinds of equality demonstrated here.
In scenarios one and two we must first determine what the ==
operator means. C# defines over a dozen different built-in ==
operators:
object == object
string == string
int == int
uint == uint
long == long
ulong == ulong
...
|
There is no int == short
or short == int
operators, so the unique best match on the list of built-in operators must be determined. It turns out that the best match is int == int
. So the short
is converted to int
and then the two values are compared as numbers. They are therefore equal.
In scenario three we must first solve an overload resolution problem to determine what Equals
means. The receiver is of type int
and it has three methods named Equals
:
Equals( object , object )
Equals( object )
Equals( int )
|
The first one we can eliminate because there are not enough arguments. Of the other two, the unique best method is the one that takes an int
; it is always better to convert the short
argument to int
than to object
. Therefore we call Equals(int)
, which then compares the two integers again using value equality, so this is true.
In scenario four we again must determine what Equals
means. The receiver is of type short
which again has three methods named Equals
Equals( object , object )
Equals( object )
Equals( short )
|
Overload resolution eliminates the first because there are too few arguments and eliminates the third because there is no implicit conversion from int
to short
. That leaves short.Equals(object)
, which has the moral equivalent of this implementation:
bool Equals( object z)
{
return z is short && ( short )z == this ;
}
|
That is, for this method to return true the argument passed in must be a boxed short
, and when unboxed it must be equal to the receiver. Since the argument is a boxed int
, this returns false. There is no special gear in this implementation that says “well, what if I were to convert myself to the type of the argument and then compare?”
In scenarios five, six and seven operator overload resolution chooses the object == object
form, which is equivalent to a call to Object.ReferenceEquals
. Clearly the two references are equal in case five and unequal in cases six and seven. Whether the values of the objects when interpreted as numbers are equal does not come into play at all; only reference equality is relevant.
In scenarios eight and nine operator overload resolution chooses the static method Object.Equals
, which you can think of as being implemented like this:
public static bool Equals( object x, object y)
{
if (ReferenceEquals(x, y)) return true ;
if (ReferenceEquals(x, null )) return false ;
if (ReferenceEquals(y, null )) return false ;
return x.Equals(y);
}
|
In scenario eight we have two references that are unequal and not null; therefore we call int.Equals(object)
, which as you would expect from our previous discussion of short.Equals(object)
is implemented as the moral equivalent of:
bool Equals( object z)
{
return z is int && ( int )z == this ;
}
|
Since the argument is of type int
it is unboxed and compared by value. In scenario nine the argument is a boxed short
and so the type check fails and this is false.
Summing up: I’ve shown nine different ways that two things can be compared for equality; despite the fact that clearly in every case we have the number one on both sides of the equality, equality is true in only half the cases. If you think this is crazy and confusing, you’re right! Equality is tricky in C#.
I’ve been looking a lot at confusing cases for equality over my last year at Coverity; two of our checkers are specifically designed to find situations where you used the wrong kind of equality. But this article is long enough already and I’ve answered your question (I hope!), so I’ll discuss the specific cases that our checkers look for in another posting.