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

247

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

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

так что ты думаешь об этом?

Александр Ефимов
источник
2
Чтобы сделать вещи незнакомыми, c.d.Equals(d.d)оценивает trueкак делаетc.f.Equals(d.f)
Джастин Нисснер
2
Не сравнивайте поплавки с точным сравнением, как .Equals. Это просто плохая идея.
Торстен79
6
@ Thorsten79: Как это актуально здесь?
Бен М
2
Это самое странное. Использование long вместо double для f вводит то же поведение. И добавление еще одного короткого поля исправляет это снова ...
Jens
1
Странно - кажется, что это происходит только тогда, когда оба имеют одинаковый тип (float или double). Измените один на плавающий (или десятичный), и D2 работает так же, как D1.
tvanfosson

Ответы:

387

Ошибка в следующих двух строках 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
SLaks
источник
159
Шагая в System.ValueType? Это довольно хардкорный братан.
Pierreten
2
Вы не объясняете, каково значение "8 байтов в ширину". Разве структура со всеми 4-байтовыми полями не будет иметь тот же результат? Я предполагаю, что наличие одного 4-байтового поля и 8-байтовых полей просто срабатывает IsNotTightlyPacked.
Гейб
1
@ Gabe я писал ранее, чтоThe bug also happens with floats, but only happens if the fields in the struct add up to a multiple of 8 bytes.
SLaks
1
В настоящее время .NET является программным обеспечением с открытым исходным кодом, вот ссылка на реализацию ValueTypeHelper :: CanCompareBits в Core CLR . Не хотел обновлять ваш ответ, поскольку реализация немного изменилась по сравнению с исходным кодом, который вы опубликовали.
17
59

Я нашел ответ на http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx .

Основной частью является исходный комментарий CanCompareBits, который ValueType.Equalsиспользуется для определения, следует ли использовать memcmpсравнение в стиле:

Комментарий CanCompareBits гласит: «Верните истину, если тип значения не содержит указатель и плотно упакован». И FastEqualsCheck использует «memcmp» для ускорения сравнения.

Автор далее констатирует точно проблему, описанную ОП:

Представьте, что у вас есть структура, которая содержит только поплавок. Что произойдет, если один содержит +0.0, а другой содержит -0.0? Они должны быть одинаковыми, но базовое двоичное представление отличается. Если вы вложите другую структуру, которая переопределяет метод Equals, эта оптимизация также не будет выполнена.

Бен М
источник
Интересно , если поведение Equals(Object)для double, floatи Decimalизменились в течение ранних проектов .net; Я думаю , что это более важно иметь виртуальное X.Equals((Object)Y)только возвращение , trueкогда Xи Yнеразличимы, чем иметь этот метод соответствует поведению других перегрузок (особенно с учетом того, что из - за неявный тип принуждения, перегруженные Equalsметоды не даже определить отношение эквивалентности !, например, 1.0f.Equals(1.0)возвращает false, но 1.0.Equals(1.0f)возвращает true!) Реальная проблема ИМХО не в том, как сравниваются структуры ...
суперкат
1
... но с тем, что эти типы значений переопределяют, Equalsчтобы означать нечто иное, чем эквивалентность. Предположим, например, что кто-то хочет написать метод, который принимает неизменный объект и, если он еще не был кэширован, выполняет ToStringего и кэширует результат; если он был кеширован, просто верните кешированную строку. Это не слишком разумная вещь, но это может плохо сработать, Decimalпоскольку два значения могут сравниваться одинаково, но приводить к разным строкам.
Суперкат
52

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

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

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

Эрик Липперт
источник
3
Тогда почему это не ошибка? Хотя MS рекомендует всегда переопределять Equals для типов значений.
Александр Ефимов
14
Черт побери из меня. Я не эксперт по внутренним компонентам CLR.
Эрик Липперт
4
... нет? Несомненно, ваши знания внутренних компонентов C # приведут к значительным знаниям о том, как работает CLR.
CaptainCasey
37
@CaptainCasey: я провел пять лет, изучая внутренние компоненты компилятора C # и, возможно, всего пару часов изучая внутренние компоненты CLR. Помните, я являюсь потребителем CLR; Я достаточно хорошо понимаю его общественную поверхность, но его внутренности - для меня черный ящик.
Эрик Липперт
1
Моя ошибка, я думал, что компиляторы CLR и VB / C # были более тесно связаны ... поэтому C # / VB -> CIL -> CLR
CaptainCasey
22

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

Отражатель говорит нам, что 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)]) и не имеют доступного источника.

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

Vilx-
источник
17

Это действительно так для меня, с Mono gmcs 2.4.2.3.

Мэтью Флэшен
источник
5
Да, я тоже пробовал это в Mono, и это тоже дает мне правду. Похоже, что MS делает немного магии внутри :)
Александр Ефимов
3
интересно, мы все отправляемся в Моно?
WeNeedОтветы
14

Более простой контрольный пример:

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

SLaks
источник
Выглядит как правило оптимизатора, которое гласит: если все удваивается, чем сравнивать, то делайте двойные удвоения. Равные вызовы
Хенк
Я не думаю, что это тот же тестовый случай, в котором проблема, представленная здесь, состоит в том, что значение по умолчанию для Bad.f не равно 0, тогда как другой случай, похоже, является проблемой Int против Double.
Дрисс Зуак
6
@Driss: Значение по умолчанию double является 0 . Ты не прав.
Слакс
10

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

Жоао Анжело
источник
5

…что ты думаешь об этом?

Всегда переопределяйте Equals и GetHashCode для типов значений. Это будет быстро и правильно.

Вячеслав Иванов
источник
Помимо предостережения, что это необходимо только тогда, когда равенство имеет значение, это именно то, о чем я думал. Каким бы забавным ни был взгляд на особенности поведения равенства значений по умолчанию, как это делают ответы с наибольшим количеством голосов, существует причина, по которой существует CA1815 .
Джо Амента
@JoeAmenta Извините за поздний ответ. На мой взгляд (только на мой взгляд, конечно), равенство всегда ( ) актуально для типов значений. Реализация равенства по умолчанию неприемлема в общих случаях. ( ) За исключением очень особых случаев. Очень. Особый. Когда вы точно знаете, что вы делаете и почему.
Вячеслав Иванов
Я думаю, что мы согласны с тем, что переопределение проверок на равенство для типов значений практически всегда возможно и имеет смысл, за очень немногими исключениями, и обычно делает это строго более правильным. Пункт, который я пытался донести до слова «релевантный», заключался в том, что есть некоторые типы значений, экземпляры которых никогда не будут сравниваться с другими экземплярами на равенство, поэтому переопределение приведет к мертвому коду, который необходимо поддерживать. Те (и странные особые случаи, на которые вы намекаете) будут единственными местами, где я пропущу это.
Джо Амента
4

Просто обновление для этой 10-летней ошибки: она была исправлена ( Отказ от ответственности : я автор этого PR) в .NET Core, которая, вероятно, будет выпущена в .NET Core 2.1.0.

В блоге объясняется ошибка и как я ее исправил.

Джим Ма
источник
2

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

Это все еще ложь.

я т кажется, что это неверно , если структура содержит только двойник.

Мортен Андерсон
источник
1

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

дд = -0,0

чтобы:

дд = 0.0

результаты сравнения являются правдой ...

user243357
источник
И наоборот, NaN могут сравниваться друг с другом для изменения, когда они фактически используют одну и ту же битовую комбинацию.
Гарольд