Вот пример с комментариями:
class Program
{
// first version of structure
public struct D1
{
public double d;
public int f;
}
// during some changes in code then we got D2 from D1
// Field f type became double while it was int before
public struct D2
{
public double d;
public double f;
}
static void Main(string[] args)
{
// Scenario with the first version
D1 a = new D1();
D1 b = new D1();
a.f = b.f = 1;
a.d = 0.0;
b.d = -0.0;
bool r1 = a.Equals(b); // gives true, all is ok
// The same scenario with the new one
D2 c = new D2();
D2 d = new D2();
c.f = d.f = 1;
c.d = 0.0;
d.d = -0.0;
bool r2 = c.Equals(d); // false! this is not the expected result
}
}
Так, что Вы думаете об этом?
Ошибка находится в следующих двух строках System.ValueType
: (Я вошел в справочный источник)
if (CanCompareBits(this))
return FastEqualsCheck(thisObj, obj);
(Оба методы: [MethodImpl (MethodImplOptions.InternalCall)]
)
Когда все поля имеют ширину 8 байтов, CanCompareBits
ошибочно возвращает истину, что приводит к побитовому сравнению двух разных, но семантически идентичные значения.
Если хотя бы одно поле не имеет ширины 8 байт, CanCompareBits
возвращает false, и код переходит к использованию отражения для перебора полей и вызова Equals
для каждого значения, что правильно обрабатывает -0,0
как равное 0,0
.
Вот источник для CanCompareBits
из SSCLI:
FCIMPL1(FC_BOOL_RET, ValueTypeHelper::CanCompareBits, Object* obj)
{
WRAPPER_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;
_ASSERTE(obj != NULL);
MethodTable* mt = obj->GetMethodTable();
FC_RETURN_BOOL(!mt->ContainsPointers() && !mt->IsNotTightlyPacked());
}
FCIMPLEND
Это действительно дает мне правду, с Mono's gmcs 2.4.2.3.
Более простой тестовый пример:
Console.WriteLine("Good: " + new Good().Equals(new Good { d = -.0 }));
Console.WriteLine("Bad: " + new Bad().Equals(new Bad { d = -.0 }));
public struct Good {
public double d;
public int f;
}
public struct Bad {
public double d;
}
РЕДАКТИРОВАТЬ : ошибка также возникает с числами с плавающей запятой, но происходит только в том случае, если сумма полей в структуре кратна 8 байтам.
Он должен быть связан с нулем, поскольку изменение строки
dd = -0.0
на:
dd = 0.0
приводит к если сравнение верно ...
Половина ответа:
Reflector сообщает нам, что ValueType.Equals ()
делает что-то вроде этого:
if (CanCompareBits(this))
return FastEqualsCheck(this, obj);
else
// Use reflection to step through each member and call .Equals() on each one.
К сожалению, оба CanCompareBits ()
и FastEquals ()
(оба статических метода) являются внешними ( [MethodImpl (MethodImplOptions.InternalCall)]
) и не имеют доступного источника.
Вернемся к предположению, почему один случай можно сравнить по битам, а другой - нет (возможно, проблемы с выравниванием?)
Если вы сделаете D2 таким образом
public struct D2
{
public double d;
public double f;
public string s;
}
это правда.
если вы сделаете это так
public struct D2
{
public double d;
public double f;
public double u;
}
Это все равно ложь.
i Кажется, что это ложь, если структура содержит только числа типа double.
Это должно быть связано с побитовым сравнением, поскольку 0,0
должно отличаться от -0,0
только сигнальным битом.
Гипотеза Вилкса верна. "CanCompareBits" проверяет, "плотно ли упакован" в памяти рассматриваемый тип значения. Плотно упакованная структура сравнивается путем простого сравнения двоичных битов, составляющих структуру; слабо упакованная структура сравнивается путем вызова Equals для всех членов.
Это объясняет наблюдение SLaks о том, что он воспроизводит двойные структуры; такие конструкции всегда плотно упакованы.
К сожалению, как мы видели здесь, это вносит семантическую разницу, потому что побитовое сравнение чисел типа double и сравнение чисел типа Equals дает разные результаты.
Я нашел ответ на http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind- ValueType-равно.aspx .
Основной частью является комментарий источника к CanCompareBits
, который ValueType.Equals
использует для определения того, следует ли использовать memcmp
-стилевое сравнение:
в комментарии CanCompareBits говорится: "Вернуть истину, если тип значения не содержит указатель и плотно упакован". А FastEqualsCheck использует "memcmp" для ускорения сравнения.
Далее автор в точности формулирует проблему, описанную в OP:
Представьте, что у вас есть структура, которая содержит только число с плавающей запятой. Что произойдет , если один содержит +0,0, а другой содержит -0,0? Они должны быть одинаковыми , но лежащее в основе двоичное представление другое. Если вы вложите другую структуру, которая переопределяет метод Equals, эта оптимизация также потерпит неудачу.