Answers to C# Equality quiz

Here are answers and commentary for the quiz that appeared here. The quiz is what do each of the C# expressions below evaluate to (in an unchecked context), given that x is a local  variable of type ‘int’.

 

With each case, the key is to understand what’s actually happening under the surface. This means identify any hidden operations from the C# compiler, and then identify what type of equality operator is actually being used. Looking at the IL for each case helps make these both more obvious.  I’ve also called out “extra stuff” to indicate additional issues in each case beyond just integer equality.

 

Note that lhs= “left hand side” and rhs = “right hand side”. For readability, I highlight the IL for the rhs in blue.

 

Answers:

  1. x == x TRUE

This is a the trivial example. Here’s the IL:

  IL_0002: ldloc.0

  IL_0003: ldloc.0

  IL_0004: ceq

The IL shows there’s no funny stuff going on here, as we’d expect given that x is just a variable (as opposed to say, a read-only property). Regardless of whether the equals is reference or value equality, this had better be true.

Extra stuff: None

 

  1. (object) x == (object) x FALSE

This is  probably the most interesting one. Casting from a value-type (‘int’ in this case) to a System.Object will invoke boxing, which will create a new instance on the fly. Here’s the IL which makes this more obvious:

  IL_000c: ldloc.0

  IL_000d: box [mscorlib]System.Int32

  IL_0012: ldloc.0

  IL_0013: box [mscorlib]System.Int32

  IL_0018: ceq

The IL-stack arguments to the ceq instruction are two instances of System.Object, a reference type, which means ceq will do a reference-equality check.  Each boxing instruction allocates its own separate instances, which just happens to have the same contents.  Thus this returns false.

Extra stuff: boxing.

 

  1. (System.Object) x == (System.Object) x FALSE

This is the same as #2. In C#, ‘object’ is just an alias for ‘System.Object’. Both compile to the same IL, and so this is false for the same reasons as #2.

Extra stuff: C# aliasing

 

  1. (int) (object) x == (int) (object) x TRUE

This will box and then unbox the arguments, but that’s a red-herring since the integer value obviously round trips through that. It was equal before, and so it’s equal afterwards. Here’s the IL:

  IL_0034: ldloc.0

  IL_0035: box [mscorlib]System.Int32

  IL_003a: unbox [mscorlib]System.Int32

  IL_003f: ldind.i4

  IL_0040: ldloc.0

  IL_0041: box [mscorlib]System.Int32

  IL_0046: unbox [mscorlib]System.Int32

  IL_004b: ldind.i4

  IL_004c: ceq

The ldind.i4 leaves an int on the IL stack, so the ceq instruction gets two ints and is thus doing a value-equality.

Extra stuff: boxing, unboxing

 

  1. (float) x == (float) x TRUE

This is true for the same reasons that #1 is true. Floats are value-types and so the comparison is a value-equality. The int-to-float conversion is deterministic, and so both the lhs and rhs will yield the same value. Whether the conversion loses precision is a red-herring because even if its wrong, it will be wrong on both sides. Here’s the IL:

  IL_0054: ldloc.0

  IL_0055: conv.r4

  IL_0056: ldloc.0

  IL_0057: conv.r4

  IL_0058: ceq

Extra stuff: none

 

  1. (int) x == (int) x TRUE

This is trivial. Since x is already of type int, the extra type casts truly do nothing. This compiles to the exact same IL as #1 (x==x).

Extra stuff: none

 

  1. (int) x == (float) x TRUE

Integer operands need to be converted to a common type before they can be compared. So either the int is promoted to a float, or the float is demoted to an int. Section 7.2.6.2 of the C# spec explains that in C#, the lhs int gets promoted to a float.

( This happens as a natural consequence of the overload rules described in 7.4.2. Section 6.1.2 lists an implicit conversion from an int to a float). The expression becomes “(float) x == (float) x”. This is case #5, which we can confirm by looking at the IL:

  IL_006a: ldloc.0

  IL_006b: conv.r4

  IL_006c: ldloc.0

  IL_006d: conv.r4

  IL_006e: ceq

Extra stuff: C# numeric promotions

 

 

  1. (float) (int) x == (int) (float) x DEPENDS ON x!

The lhs and rhs are distinctly different here. The lhs converts from an int to a float. The rhs converts from an int to a float and then back to an int. In general, converting from an int to a float may lose precision. Thus if the given value of x round trips (e.g., does not lose precision when converted to a float and then back to an int), the expression will be true; else it will be false. Here’s the IL:

  IL_00d3: ldloc.1

  IL_00d4: conv.r4

  IL_00d5: ldloc.1

  IL_00d6: conv.r4

  IL_00d7: conv.i4

  IL_00d8: conv.r4

  IL_00d9: ceq

 

The extra conversion on the RHS is in italics.  Note that i4 (int) and r4 (float) are both 4-byte values, which is another hint that the conversion will lose precision.

 

Small values (such as 3) will round trip nicely. Large values (such as int.MaxValue) will lose precision. Section 4.1.6 of the C# spec explains that C# uses IEEE 754 formats for floating point numbers.

The following snippet demonstrates an integer not round-tripping:

        int max = int.MaxValue;

   Console.WriteLine("Int:{0}", max);

   Console.WriteLine("as float: {0}",(float) max);

   Console.WriteLine("back to int.{0}", (int) ((float) max));

It produces this output:

Int:2147483647

as float: 2.147484E+09

back to int.-2147483648

 

Extra stuff: precision in numeric conversions

           

  1. (System.Int32) x == (System.Int32) x TRUE

This is another free one. Section 4.1.4 of the C# spec explains that  System.Int32 is just an alias for ‘int’ (or vice-versa), so this is the same as 6. The spec does not make any reservations for non 32-bit processors here, so this should be true even for 64-bit.

The IL and reasoning are the same as #6.

One interesting thing here is that System.Int32 derives from System.Object. So casting to the base class breaks equality (as seen in case #2), yet casting to the derived class preserves equality! This may seem odd since normally, casting up and down the inheritance hierarchy doesn’t change equality.

The key is that the C# language uses casting syntax to do boxing under the covers. A language could avoid this potential confusion by forcing an explicit syntax for boxing. Thus you’d say something like “box(x)” instead of “(object) x”.

Extra stuff: C# aliasing

 

  1. x.ToString() == x.ToString() TRUE

Here we call System.Object.ToString() for both the lhs and rhs. Each invocation will actually return a different instance of the string, although the strings will have the same contents. However, the comparison here is  operator=(string,string), which does a value-equality comparison on strings (see 7.9.7 of the spec), so the expression will still be true.

This can be seen in the IL:

  IL_00f6: ldloca.s x

  IL_00f8: call instance string [mscorlib]System.Int32::ToString()

  IL_00fd: ldloca.s x

  IL_00ff: call instance string [mscorlib]System.Int32::ToString()

  IL_0104: call bool [mscorlib]System.String::op_Equality(string,

                                                                 string)

Extra stuff: value-equality for strings

 

  1. (object) x.ToString() == (object) x.ToString() FALSE *

This is very related to #10. The calls to ToString() will yield two separate instances with the same contents. (* = In theory, future implementations could change such that the same instance came back in certain scenarios. In practice, I can’t imagine this ever happening). However, since we cast to ‘object’, the comparison is now operator=(object,object), which does reference equality.  Here’s the IL:

  IL_011a: ldloca.s x

  IL_011c: call instance string [mscorlib]System.Int32::ToString()

  IL_0121: ldloca.s x

  IL_0123: call instance string [mscorlib]System.Int32::ToString()

  IL_0128: ceq

 

Notice that case #10 and #11 are identical except for the comparison operator.

Extra stuff: strings