Why integer zero does not equal long zero?

A strange piece of code I've just discovered in C# (should also be true for other CLI languages using .NET's structs).

using System;

public class Program
{
    public static void Main(string[] args)
    {
    int a;
    long b;

    a = 0;
    b = 0;

    Console.WriteLine(a.Equals(b)); // False
    Console.WriteLine(a.Equals(0L)); // False
    Console.WriteLine(a.Equals((long)0)); // False
    Console.WriteLine(a.Equals(0)); // True
    Console.WriteLine(a.Equals(a)); // True
    Console.WriteLine(a == b); // True
    Console.WriteLine(a == 0L); // True

    Console.WriteLine();

    Console.WriteLine(b.Equals(a)); // True
    Console.WriteLine(b.Equals(0)); // True
    Console.WriteLine(b.Equals((int)0)); // True
    Console.WriteLine(b.Equals(b)); // True
    Console.WriteLine(b == a); // True
    Console.WriteLine(b == 0); // True
    }
}

Two interesting points here (assuming that a is int and b is long):

  1. a != b, but b == a;
  2. (a.Equals(b)) != (a == b)

Is there any reason why comparison was implemented this way?

Note: .NET 4 was used if it makes any difference.

Answers


In general, Equals() methods are not supposed to return true for objects of different types.

a.Equals(b) calls int.Equals(object), which can only return true for boxed Int32s:

public override bool Equals(Object obj) { 
    if (!(obj is Int32)) {
        return false;
    }
    return m_value == ((Int32)obj).m_value; 
}  

b.Equals(a) calls long.Equals(long) after implicitly converting the int to a long. It therefore compares the two longs directly, returning true.

To understand more clearly, look at the IL generated by this simpler example (which prints True False True):

int a = 0;
long b = 0L;

Console.WriteLine(a == b);
Console.WriteLine(a.Equals(b));
Console.WriteLine(b.Equals(a));

IL_0000:  ldc.i4.0    
IL_0001:  stloc.0     
IL_0002:  ldc.i4.0    
IL_0003:  conv.i8     
IL_0004:  stloc.1     

IL_0005:  ldloc.0     //Load a
IL_0006:  conv.i8     //Cast to long
IL_0007:  ldloc.1     //Load b
IL_0008:  ceq         //Native long equality check
IL_000A:  call        System.Console.WriteLine    //True

IL_000F:  ldloca.s    00            //Load the address of a to call a method on it
IL_0011:  ldloc.1                   //Load b
IL_0012:  box         System.Int64  //Box b to an Int64 Reference
IL_0017:  call        System.Int32.Equals
IL_001C:  call        System.Console.WriteLine    //False

IL_0021:  ldloca.s    01  //Load the address of b to call a method on it
IL_0023:  ldloc.0         //Load a
IL_0024:  conv.i8         //Convert a to Int64
IL_0025:  call        System.Int64.Equals
IL_002A:  call        System.Console.WriteLine    //True

They are not the same because even simple types are inherited from System.Object - they are actually objects, and different object types, even with the same property values are not equal.

Example:

You could have a Co-Worker object with only one property: Name (string) and a partner object with only one property: Name (string)

Co-worker David is not the same as Parner David. The fact that they are different object types sets them apart.

In your case, using .Equals(), you're not comparing values, you're comparing objects. The object isn't "0" it's a System.Int32 with a Value of zero, and a System.Int64 with a value of zero.

Code sample based on question in comment below:

class CoWorker
{
   public string Name { get; set; }
}

class Partner
{
   public string Name { get; set; }
}

private void button1_Click(object sender, RoutedEventArgs e)
{
   CoWorker cw = new CoWorker();
   cw.Name = "David Stratton";
   Partner p = new Partner();
   p.Name = "David Stratton";

   label1.Content = cw.Equals(p).ToString();  // sets the Content to "false"
}

Operator and method overloads, as well as conversion operators, are evaluated at compile time, unlike virtual-method overrides which are evaluated at run-time. The expression someIntVar.Equals(someNumericQuantity) is completely unrelated to the expression someObjectVarThatHoldsAnInt.Equals(someNumericQuantity). If you were to pretend the virtual method Object.Equals has a different name (like IsEquivalentTo), and substitute that name every place the virtual method is used, this would be much clearer. An integer zero may be numerically equal to a long zero, but that does not mean they are semantically equivalent.

Such a separation in meaning between Equals and IsEquivalentTo, incidentally, would also have helped to avoid murkiness in the definition of the latter. It's possible to define a meaningful equivalence relation for arbitrary objects: storage location X should be considered equivalent to storage location Y if the behavior of all members of the former will always be equivalent to the corresponding members of the latter, and the only way to determine whether X and Y refer to the same object would be to use Reflection or ReferenceEquals. Even though 1.0m.Equals(1.00m) is and should be true, 1.0m.IsEquivalentTo(1.00m) should be false. Unfortunately, the use of the same name for the object equivalency-test method and the Decimal numerical-equality-test method led Microsoft to define the former to behave like the latter.


There is also the issue of narrowing or widening conversion. A long zero is always equal to an int zero, but not the other way around.

When a long is compared to an int, only the least significant 32-bits are compared and the rest are ignored, thus the int.Equals(long) operation cannot guarantee equality even if the lower bits match.

int a = 0;
long b = 0;

Trace.Assert(a.Equals((int)b));     // True   32bits compared to 32bits
Trace.Assert(a.Equals((long)b));    // False  32bits compared to 64bits (widening)
Trace.Assert(b.Equals((long)a));    // True   64bits compared to 64bits
Trace.Assert(b.Equals((int)a));     // True   64bits compared to 32bits (narrowing)

Also consider the case where the lower 32-bits are equal, but the upper ones are not.

uint a = 0;
ulong b = 0xFFFFFF000000;
Trace.Assert((uint)a == (uint)b);  // true because of a narrowing conversion
Trace.Assert((ulong)a == (ulong)b);  // false because of a widening conversion

because Equals compares objects and the a and b objects are different. They have the same value but are different as objects

This link may help you: http://msdn.microsoft.com/en-us/library/ms173147(v=vs.80).aspx


C# doesn't do automatic casting. Equals function compares types as well as values. Much like === in JS.


Need Your Help

Loading a local .kml file using google maps?

gis google-maps-api-3 kml kmz

I created a hello world program to load a local kml file (borrowed from google's docs):