Software Testing Blog

C#: Inconsistent equality

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 ints and shorts, 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);      // true
Console.WriteLine(myShort == myInt);      // true
Console.WriteLine(myInt.Equals(myShort)); // true
Console.WriteLine(myShort.Equals(myInt)); // false!

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);          // scenario 1 true
Console.WriteLine(myShort == myInt);          // scenario 2 true
Console.WriteLine(myInt.Equals(myShort));     // scenario 3 true
Console.WriteLine(myShort.Equals(myInt));     // scenario 4 false!
Console.WriteLine(objInt1 == objInt1);        // scenario 5 true
Console.WriteLine(objInt1 == objShort);       // scenario 6 false!!
Console.WriteLine(objInt1 == objInt2);        // scenario 7 false!!!
Console.WriteLine(Equals(objInt1, objInt2));  // scenario 8 true
Console.WriteLine(Equals(objInt1, objShort)); // scenario 9 false!?!

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) // static method from object
Equals(object)         // virtual method from object
Equals(int)            // Implements IEquatable<int>.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) // static method from object
Equals(object)         // virtual method from object
Equals(short)          // Implements IEquatable<short>.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.


As always, if you have questions about a bug you’ve found in a C, C++, C# or Java program that you think would make a good episode of ATBG, please send your question along with a small reproducer of the problem to TheBugGuys@Coverity.com. We cannot promise to answer every question or solve every problem, but we’ll take a selection of the best questions that we can answer and address them on the dev testing blog every couple of weeks.

  1. Scenario 7 surprises me, coming from Java. When boxing integers in Java, pools of boxed primitives are maintained to cheapen the cost of boxing.

    ISTR the language spec actually requires this, although it provides a lot of freedom about how big the pool might be.

    Anyway, the integer 1 would certainly belong to the pool in any real-world compiler, so both objInt1 and objInt2 would be references aliased to the same object, taken from the pool.

    I take it that C# does not do the same thing. Nothing in the language spec requires it that I can see, but nothing seems to prohibit it either, so I would expect that scenario 7 could be true for a conformant C# compiler.

    Am I understanding the spec correctly?

    1. As far as I understand scenario #7 should always produce false. Specification states that boxing is equivalent to creating a new object. So, primitive caching, if implemented, shouldn’t influence boxed objects.

    2. @Joshua,

      IIRC, in Java only integers between -128 and 127 are in the pool. Does that mean that comparing boxed integers would give different results depending on whether or not the values are in that range? That would be even more confusing…

      1. Yes indeed it does. But I’d argue that if your program depends on that fact to perform correctly, it’s a badly written program and only accidentally correct. You most likely meant to compare by value not reference, or less likely are using boxed primitives in an unusual way that could be rewritten to not just guarantee correctness but also be a lot clearly about its intent.

        I have used this bit of wierdness in interview questions. Not as a filter to cut people out, because you can not know this and still be a perfectly competent Java dev. Rather, as the extra points question that demonstrates that the interviewee really does understand the difference between value and reference, does read the language spec or else is experienced enough to have been bitten by it, and should maybe be offered a job title with “Senior” in it.

    1. The specification strongly implies that boxed value types are all new allocations and therefore reference-unequal. Strings however are permitted to be “interned” — that is, two value-identical strings become reference-identical as well — at the whim of the runtime.

      In general the CLR is much less aggressive about interning than the JVM. The interning optimization in practice saves a small amount of memory but at the cost of having to search all living strings for a potential intern on every string allocation. The corresponding increase in time for the allocation is too expensive, and the savings in memory and time on string comparisons isn’t large enough to justify it, or so the CLR team believes.

      1. The interning optimization in practice saves a small amount of memory but at the cost of having to search all living strings for a potential intern on every string allocation.

        Without being able to read the minds of the JVM authors, I expect the goal of reducing load on the garbage collector may be just as important or more so than the goal of reducing memory usage per se.

        The JVM, just like the CLR, is free to intern or not as it wishes I think – the only times it is required is for string literals and explicit calls to String.intern(). So each team is equally free to do a cost-benefit analysis on their string allocation strategy.

        The corresponding increase in time for the allocation is too expensive, and the savings in memory and time on string comparisons isn’t large enough to justify it, or so the CLR team believes.

        As I said above about boxing, relying on interning and using == where you meant String.equals() is a bug. I doubt the JVM team would be trying to support that behaviour. I’ve been bitten by that one myself, actually. My code worked on the Sun JVM and then failed when we ported to some obscure third-party JVM that didn’t intern as much. Modern IDEs can throw a warning about that, which I started using religiously at that point.

  2. Seems like short should be defining a Equals(int) or whatever the biggest integer type is and return false if the value is larger than short.max() and return Equal(short) with the passed in argument converted to short otherwise.

  3. Ugh, equality, best avoided in my opinion :)

    And here you’re only talking about using it. What about implementing it?! Sooo much to think about: ==, !=, Equals, GetHashCode, IEquatable.

    Think I’ll go and lie down…

  4. Joshua:

    the problem then of course is:
    ((object)(int)127) == ((object)(int)127) // true
    ((object)(int)128) == ((object)(int)128) // false

    now, that is confusing!

    Better to be consistently wrong than being right only some of the time.

  5. Third-to-last paragraph: “Since the argument is of type int it is unboxed and compared by reference.”

    should be: “Since the argument is of type int it is unboxed and compared by value”.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Current day month ye@r *