Может ли кто-нибудь объяснить это странное поведение с подписанными поплавками в С#?
Вот пример с комментариями:
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
}
}
Итак, что вы думаете об этом?
Ответы
Ответ 1
Ошибка находится в следующих двух строках System.ValueType
: (я вступил в исходный источник)
if (CanCompareBits(this))
return FastEqualsCheck(thisObj, obj);
(Оба метода: [MethodImpl(MethodImplOptions.InternalCall)]
)
Когда все поля имеют ширину 8 байтов, CanCompareBits
ошибочно возвращает true, что приводит к поразрядному сравнению двух разных, но семантически идентичных значений.
Если по крайней мере одно поле не имеет ширины 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
Ответ 2
Я нашел ответ на http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx.
Ядром является исходный комментарий CanCompareBits
, который ValueType.Equals
использует, чтобы определить, следует ли использовать сравнение memcmp
:
Комментарий CanCompareBits говорит: "Вернуть true, если тип valuetype не содержать указатель и плотно упакованный". И использование FastEqualsCheck "memcmp", чтобы ускорить сравнение.
Далее автор формулирует проблему, описанную OP:
Представьте, что у вас есть структура, которая только содержит поплавок. Что произойдет если один содержит +0,0, а другой содержит -0,0? Они должны быть то же самое, но лежащий в основе двоичный представление различно. если ты вложить другую структуру, которая переопределяет метод Equals, что оптимизация также потерпит неудачу.
Ответ 3
Гипотеза Вилкса верна. Что такое "CanCompareBits", это проверка на то, что тип значения, о котором идет речь, "плотно упакован" в памяти. Плотно упакованную структуру сравнивают, просто сравнивая бинарные биты, составляющие структуру; слабо упакованную структуру сравнивают, вызывая Equals для всех членов.
Это объясняет наблюдение SLaks, что он воспроизводит структуры, которые все удваиваются; такие структуры всегда плотно упакованы.
К сожалению, как мы видели здесь, это вводит семантическую разницу, потому что побитное сравнение удвоений и сравнительное сравнение удвоений дает разные результаты.
Ответ 4
Половина ответа:
Отражатель сообщает нам, что 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()
(оба статических метода) являются extern ([MethodImpl(MethodImplOptions.InternalCall)]
) и не имеют источника.
Вернемся к предположению, почему один случай можно сравнивать по битам, а другой не может (проблемы с выравниванием, возможно?)
Ответ 5
Это верно для меня, с Mono gmcs 2.4.2.3.
Ответ 6
Простой тестовый пример:
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 байтов.
Ответ 7
Он должен быть связан с побитовым сравнением, поскольку 0.0
должен отличаться от -0.0
только битом сигнала.
Ответ 8
... что вы думаете об этом?
Всегда переопределять Equals и GetHashCode для типов значений. Он будет быстрым и правильным.
Ответ 9
Если вы делаете 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;
}
Он по-прежнему ошибочен.
кажется, что это false, если структура содержит только удвоения.
Ответ 10
Просто обновление для этой ошибки 10 лет: исправлено ( Отказ от ответственности: я автор этого PR) в .NET Core, который, вероятно, будет выпущен в .NET Core 2.1.0.
сообщение в блоге объяснил ошибку и как я ее исправил.
Ответ 11
Он должен быть равен нулю, так как изменение строки
d.d = -0.0
в
d.d = 0.0
приводит к тому, что сравнение истинно...