Неожиданное поведение в С# generic method на .Equals

Почему метод Equals возвращает другой результат из общего метода? Я думаю, что здесь есть какой-то автоматический бокс, который я не понимаю.

Вот пример, который воспроизводит поведение с .net 3.5 или 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;
}

Вывод:

False
True

Изменить: Этот код работает без каких-либо компромиссов:

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: я подал ошибку через MS Connect и был исправлен как исправленный, поэтому это возможно будут исправлены в следующей версии .NET Framework. Если они станут доступными, я обновляю их подробнее.

PS: это, по-видимому, исправлено в .net 4.0 и более поздних версиях (смотря на разборку TimeZoneInfo в mscorlib).

Ответы

Ответ 1

TimeZoneInfo не переопределяет метод Object Equals, поэтому он вызывает значение по умолчанию Object Equals, которое, по-видимому, не работает должным образом. Я считаю это ошибкой в ​​TimeZoneInfo. Это должно работать:

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

Вышеупомянутое вызовет вызов Equals<T>, который является методом, который вы вызывали выше (он неявно предпочел общий вызов, потому что он был более специфичен для типа параметра, чем Object Equals; однако внутри общего метода, он не мог быть уверенным, что такой общий Equals существует, поскольку не было никаких ограничений, гарантирующих это).

Ответ 2

FWIW, в mono 2.8+ оба возвращаемых значения False, выводя

False
False

Удивительно, csc.exe из VS2010 дает разные результаты, действительно выводящий:

False
True

Еще более интересно, проблема появляется не с созданным кодом IL, а с движком Framework/JIT;

  • Выполнение MS-скомпилированного изображения с помощью Mono VM приводит к False/False, например, с помощью скомпилированной версии
  • Выполнение Mono-скомпилированного изображения с помощью VM VM приводит к False/True, например, к скомпилированной версии MS

Для вашего интереса рассмотрим дизассемблирование компилятора Microsoft CSC.exe(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

и компилятор Mono gmcs.exe(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

Ответ 3

TimezoneInfo определяет его собственную перегрузку Equals (TimeZoneInfo). В методе сравнения используется объект equals (это вызов виртуального метода Object.Equals), тогда как в Console.WriteLine(tzOne.Equals(tzTwo)) вызывается перегруженный (новый) метод TimeZoneInfo.Equals.

TimeZoneInfo явно не переопределил метод Object.Equals правильно...