14

Why does the Equals method return a different result from within the generic method? I think that there's some automatic boxing here that I don't understand.

Here's an example that reproduces the behavior with .net 3.5 or 4.0:

static void Main(string[] args)
{
    TimeZoneInfo tzOne = TimeZoneInfo.Local;
    TimeZoneInfo tzTwo = TimeZoneInfo.FindSystemTimeZoneById(tzOne.StandardName);
    Console.WriteLine(Compare(tzOne, tzTwo));
    Console.WriteLine(tzOne.Equals(tzTwo));
}

private static Boolean Compare<T>(T x, T y)
{
    if (x != null)
    {
        return x.Equals(y);
    }
    return y == null;
}

Output:

False
True

Edit: This code works as desired without many compromises:

private static Boolean Compare<T>(T x, T y)
{
    if (x != null)
    {
        if (x is IEquatable<T>)
        {
            return (x as IEquatable<T>).Equals(y);
        }
        return x.Equals(y);
    }
    return y == null;
}

Followup: I filed a bug via MS Connect and it has been resolved as fixed, so it's possible this will be fixed in the next version of the .net framework. I'll update with more details if they become available.

PS: This appears to be fixed in .net 4.0 and later (by looking at the disassembly of TimeZoneInfo in mscorlib).

Wedge
  • 19,513
  • 7
  • 48
  • 71
  • Even if it is boxing, I would expect the proper `Equals` to be called. – Gabe Nov 21 '11 at 21:28
  • When I debug with a breakpoint on the .Equals line I can see that x and y are the expected types, and if I run '(x as TimeZoneInfo).Equals(y as TimeZoneInfo)' in the immediate window I get the expected result. It's very odd. – Wedge Nov 21 '11 at 21:35
  • 2
    There is no boxing, TimeZoneInfo is a class. Plus since your method is generic, structs would not be boxed either. – Rich O'Kelly Nov 21 '11 at 21:35
  • TimeZoneInfo is a class anyway, so you'd think it would be the same result. – James Michael Hare Nov 21 '11 at 21:36
  • 2
    Incidentally, your Compare() method should return `true` if x == null && y == null, I'd adapt the return line at the end to be `return y == null` – James Michael Hare Nov 21 '11 at 21:37
  • 1
    Wow, the fact TimeZoneInfo implements IEquatable but does't override object Equals() does seem to be the culprit (see @Dan's answer). Very bizarre, also explains why your unconstrained generic fails to work (unconstrained generics act like `object` in terms of methods they can `see` from type `T`). – James Michael Hare Nov 21 '11 at 21:43

3 Answers3

13

TimeZoneInfo does not override the Object Equals method, so it calls the default Object Equals, which apparently does not work as expected. I would consider this a bug in TimeZoneInfo. This should work:

private static Boolean Compare<T>(T x, T y)
        where T: IEquatable<T>
{
    if (x != null)
    {
        return x.Equals(y);
    }
    return false;
}

The above will cause it to call Equals<T>, which is the method you were calling above (it implicitly preferred the generic call because it was more specific to the parameter type than the Object Equals; inside the generic method, however, it had no way to be sure that such a generic Equals existed, since there was no constraint guaranteeing this).

Dan Bryant
  • 27,329
  • 4
  • 56
  • 102
  • 1
    @James, my guess is that the developer wanted to retain reference equality via the == operator (for whatever reason); when that's the case, it's strongly recommended that Object Equals has the same behavior as the == operator. At the same time, they wanted to provide IEquatable support (likely for generic algorithms requiring that constraint), so now there is the unfortunate side effect that `.Equals` and `==` provide different results. – Dan Bryant Nov 21 '11 at 21:48
  • @Dan Bryant, Now it makes sense. And constraining to IEquatable seems to work. Although I wish there was a way to also allow enums to be used as arguments. – Wedge Nov 21 '11 at 22:11
  • 1
    @Wedge, if you want to support types that don't implement `IEquatable`, but still use `IEquatable` if it exists, you simply check 'if (x is IEquatable)` and use that interface, if it is supported. If not, just fall back to the Object Equals method, like it uses now. – Dan Bryant Nov 21 '11 at 22:14
9

FWIW, on mono 2.8+ both return values are False, outputting

False
False

Amazingly, csc.exe from VS2010 produces different results, indeed, outputting:

False
True

Even more interestingly, the problem appears not with the generated IL code, but with the Framework implementation/JIT engine;

  • executing the MS-compiled image with the Mono VM results in False/False, like the mono compiled version
  • executing the Mono-compiled image with the MS VM results in False/True, like the MS compiled version

For your interest, here are the disassemblies of Microsoft's CSC.exe compiler (csc.exe /optimize+ test.cs):

.method private static hidebysig 
       default bool Compare<T> (!!T x, !!T y)  cil managed 
{
    // Method begins at RVA 0x2087
// Code size 30 (0x1e)
.maxstack 8
IL_0000:  ldarg.0 
IL_0001:  box !!0
IL_0006:  brfalse.s IL_001c

IL_0008:  ldarga.s 0
IL_000a:  ldarg.1 
IL_000b:  box !!0
IL_0010:  constrained. !!0
IL_0016:  callvirt instance bool object::Equals(object)
IL_001b:  ret 
IL_001c:  ldc.i4.0 
IL_001d:  ret 
} // end of method Program::Compare

and Mono's gmcs.exe compiler (dmcs -optimize+ test.cs):

.method private static hidebysig 
       default bool Compare<T> (!!T x, !!T y)  cil managed 
{
    // Method begins at RVA 0x212c
// Code size 33 (0x21)
.maxstack 4
IL_0000:  ldarg.0 
IL_0001:  box !!0
IL_0006:  brfalse IL_001f

IL_000b:  ldarga.s 0
IL_000d:  ldarg.1 
IL_000e:  box !!0
IL_0013:  constrained. !!0
IL_0019:  callvirt instance bool object::Equals(object)
IL_001e:  ret 
IL_001f:  ldc.i4.0 
IL_0020:  ret 
} // end of method Program::Compare
sehe
  • 374,641
  • 47
  • 450
  • 633
  • FYI (though not really relevant to the question), the CSC IL is padded and structured for use with the debugger; if you emit a release build, it will probably look more like the GMCS output. – Dan Bryant Nov 21 '11 at 21:55
  • @DanBryant: spot on; the mono compiler made no difference with `-optimize+`, replacing the MS emitted IL code for `/optimize+` - it is identical to the mono emitted IL now – sehe Nov 21 '11 at 21:58
  • "FWIW, on mono 2.8+ the result is identical, outputting" is confusing and sounds like you mean identical to the OP's findings. I take it you mean "FWIW, on mono 2.8+ both return values are `False`"? – Kev Nov 21 '11 at 22:43
  • @Kev: That's why I spelled out `outputting` with the exact output? I knew it was confusing, which is why I disambiguate. I'll adopt your phrase though – sehe Nov 21 '11 at 22:45
3

TimezoneInfo defines it's own overload of Equals(TimeZoneInfo). In the Compare method, the object equals is used (it's a virtual method call of Object.Equals), whereas in Console.WriteLine(tzOne.Equals(tzTwo)) the overloaded (new) TimeZoneInfo.Equals method is called.

TimeZoneInfo obviously hasn't overridden the Object.Equals method correctly...

Rich O'Kelly
  • 41,274
  • 9
  • 83
  • 114