Кто-либо может объяснить это странное поведение с плаваниями со знаком в C#?

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

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        
    }
}

Так, что Вы думаете об этом?

247
задан Dan Dascalescu 11 November 2012 в 12:09
поделиться

9 ответов

Ошибка находится в следующих двух строках 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
386
ответ дан 23 November 2019 в 03:02
поделиться

Это действительно дает мне правду, с Mono's gmcs 2.4.2.3.

17
ответ дан 23 November 2019 в 03:02
поделиться

Более простой тестовый пример:

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 байтам.

14
ответ дан 23 November 2019 в 03:02
поделиться

Он должен быть связан с нулем, поскольку изменение строки

dd = -0.0

на:

dd = 0.0

приводит к если сравнение верно ...

1
ответ дан 23 November 2019 в 03:02
поделиться

Половина ответа:

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)] ) и не имеют доступного источника.

Вернемся к предположению, почему один случай можно сравнить по битам, а другой - нет (возможно, проблемы с выравниванием?)

22
ответ дан 23 November 2019 в 03:02
поделиться

Если вы сделаете 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.

2
ответ дан 23 November 2019 в 03:02
поделиться

Это должно быть связано с побитовым сравнением, поскольку 0,0 должно отличаться от -0,0 только сигнальным битом.

10
ответ дан 23 November 2019 в 03:02
поделиться

Гипотеза Вилкса верна. "CanCompareBits" проверяет, "плотно ли упакован" в памяти рассматриваемый тип значения. Плотно упакованная структура сравнивается путем простого сравнения двоичных битов, составляющих структуру; слабо упакованная структура сравнивается путем вызова Equals для всех членов.

Это объясняет наблюдение SLaks о том, что он воспроизводит двойные структуры; такие конструкции всегда плотно упакованы.

К сожалению, как мы видели здесь, это вносит семантическую разницу, потому что побитовое сравнение чисел типа double и сравнение чисел типа Equals дает разные результаты.

52
ответ дан 23 November 2019 в 03:02
поделиться

Я нашел ответ на 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, эта оптимизация также потерпит неудачу.

59
ответ дан 23 November 2019 в 03:02
поделиться
Другие вопросы по тегам:

Похожие вопросы: