Может ли кто-нибудь объяснить это странное поведение с подписанными поплавками в С#?

Вот пример с комментариями:

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

приводит к тому, что сравнение истинно...