С# Почему равные десятичные знаки производят неравные хэш-значения? - программирование
Подтвердить что ты не робот

С# Почему равные десятичные знаки производят неравные хэш-значения?

Мы столкнулись с магическим десятичным числом, которое нарушило нашу хэш-таблицу. Я отбросил его до следующего минимального случая:

decimal d0 = 295.50000000000000000000000000m;
decimal d1 = 295.5m;

Console.WriteLine("{0} == {1} : {2}", d0, d1, (d0 == d1));
Console.WriteLine("0x{0:X8} == 0x{1:X8} : {2}", d0.GetHashCode(), d1.GetHashCode()
                  , (d0.GetHashCode() == d1.GetHashCode()));

Вывод следующего результата:

295.50000000000000000000000000 == 295.5 : True
0xBF8D880F == 0x40727800 : False

Что действительно необычно: изменить, добавить или удалить любую из цифр в d0, и проблема исчезнет. Даже добавление или удаление одного из завершающих нулей! Знак, похоже, не имеет значения.

Наше решение состоит в том, чтобы разделить значение, чтобы избавиться от конечных нулей, например:

decimal d0 = 295.50000000000000000000000000m / 1.000000000000000000000000000000000m;

Но мой вопрос: как С# делает это неправильно?

4b9b3361

Ответ 1

Для начала, С# не делает ничего плохого. Это ошибка структуры.

Это действительно похоже на ошибку, но в любом случае нормализация в сравнении для равенства должна использоваться таким же образом для вычисления хеш-кода. Я проверил и могу воспроизвести его (используя .NET 4), включая проверку методов Equals(decimal) и Equals(object), а также оператора ==.

Это определенно выглядит как значение d0, которое является проблемой, так как добавление завершающего 0s к d1 не изменяет результаты (пока оно не будет таким же, как d0, конечно). Я подозреваю, что какой-то угловой случай споткнулся о точном представлении битов.

Я удивлен, что это не так (и, как вы говорите, он работает большую часть времени), но вы должны сообщить об ошибке на Connect.

Ответ 2

Еще одна ошибка (?), которая приводит к представлению разных байтов для одного и того же десятичного числа на разных компиляторах: попытайтесь скомпилировать следующий код на VS 2005, а затем в VS 2010. Или посмотрите на мой статья о проекте кода.

class Program
{
    static void Main(string[] args)
    {
        decimal one = 1m;

        PrintBytes(one);
        PrintBytes(one + 0.0m); // compare this on different compilers!
        PrintBytes(1m + 0.0m);

        Console.ReadKey();
    }

    public static void PrintBytes(decimal d)
    {
        MemoryStream memoryStream = new MemoryStream();
        BinaryWriter binaryWriter = new BinaryWriter(memoryStream);

        binaryWriter.Write(d);

        byte[] decimalBytes = memoryStream.ToArray();

        Console.WriteLine(BitConverter.ToString(decimalBytes) + " (" + d + ")");
    }
}

Некоторые люди используют следующий код нормализации d=d+0.0000m, который не работает должным образом на VS 2010. Ваш код нормализации (d=d/1.000000000000000000000000000000000m) выглядит хорошо - я использую тот же самый, чтобы получить один и тот же массив байтов для тех же десятичных знаков.

Ответ 3

Иди в эту ошибку...: - (

Тесты (см. ниже) показывают, что это зависит от максимальной точности, доступной для значения. Ошибочные коды хэша встречаются только вблизи максимальной точности для данного значения. Как показывают тесты, ошибка, похоже, зависит от цифр слева от десятичной точки. Иногда только хэш-код для maxDecimalDigits-1 неверен, иногда неверно значение для maxDecimalDigits.

var data = new decimal[] {
//    123456789012345678901234567890
    1.0m,
    1.00m,
    1.000m,
    1.0000m,
    1.00000m,
    1.000000m,
    1.0000000m,
    1.00000000m,
    1.000000000m,
    1.0000000000m,
    1.00000000000m,
    1.000000000000m,
    1.0000000000000m,
    1.00000000000000m,
    1.000000000000000m,
    1.0000000000000000m,
    1.00000000000000000m,
    1.000000000000000000m,
    1.0000000000000000000m,
    1.00000000000000000000m,
    1.000000000000000000000m,
    1.0000000000000000000000m,
    1.00000000000000000000000m,
    1.000000000000000000000000m,
    1.0000000000000000000000000m,
    1.00000000000000000000000000m,
    1.000000000000000000000000000m,
    1.0000000000000000000000000000m,
    1.00000000000000000000000000000m,
    1.000000000000000000000000000000m,
    1.0000000000000000000000000000000m,
    1.00000000000000000000000000000000m,
    1.000000000000000000000000000000000m,
    1.0000000000000000000000000000000000m,
};

for (int i = 0; i < 1000; ++i)
{
    var d0 = i * data[0];
    var d0Hash = d0.GetHashCode();
    foreach (var d in data)
    {
        var value = i * d;
        var hash = value.GetHashCode();
        Console.WriteLine("{0};{1};{2};{3};{4};{5}", d0, value, (d0 == value), d0Hash, hash, d0Hash == hash);
    }
}

Ответ 4

Это десятичная ошибка округления.

Требуется слишком высокая точность для установки d0 с .000000000000000, вследствие чего алгоритм, отвечающий за это, делает ошибку и заканчивает тем, что дает другой результат. В этом примере он может быть отнесен к ошибке, хотя следует отметить, что тип "десятичный" должен иметь точность 28 цифр, и здесь вам фактически требуется точность 29 цифр для d0.

Это можно протестировать, запросив полное шестнадцатеричное шестнадцатеричное представление d0 и d1.

Ответ 5

Я тестировал это в VB.NET(v3.5) и получил то же самое.

Интересная вещь о хэш-кодах:

A) 0x40727800 = 1081243648

B) 0xBF8D880F = -1081243648

Использование Decimal.GetBits() Я нашел

формат: Мантисса (hhhhhhhh hhhhhhhh hhhhhhhh) Показатель (seee0000) (h - значения, 's' - знак, 'e' - показатель степени, 0 должен быть нулями)

d1 == > 00000000 00000000 00000B8B - 00010000     = (2955/10 ^ 1) = 295,5

do == > 5F7B2FE5 D8EACD6E 2E000000 - 001A0000

... который преобразуется в 29550000000000000000000000000/10 ^ 26 = 295.5000000... и т.д.

** edit: ok, я написал 128-битный шестнадцатеричный десятичный калькулятор, и выше это точно верно

Он определенно выглядит как ошибка внутреннего преобразования. Microsoft явно заявляет, что они не гарантируют реализацию GetHashCode по умолчанию. Если вы используете его для чего-либо важного, то, вероятно, имеет смысл написать собственный GetHashCode для десятичного типа. Форматирование его до фиксированной десятичной строки с фиксированной шириной и хеширования, похоже, работает, например ( > 29 знаков после запятой, > ширина 58 - подходит для всех возможных десятичных знаков).

* edit: Я больше об этом не знаю. Он все равно должен быть ошибкой преобразования где-то, поскольку сохраненная точность принципиально меняет реальное значение в памяти. То, что хеш-коды заканчиваются как подписанные негативы друг другу, является большой подсказкой - для дальнейшего поиска нужно будет продолжить реализацию хэш-кода по умолчанию.

28 или 29 цифр не должны иметь значения, если не существует зависимого кода, который не правильно оценивает внешние экстенты. Максимальное доступное 96-битное целое число:

+79228162514264337593543950335

чтобы вы могли иметь 29 цифр, пока все это (без десятичной точки) меньше этого значения. Я не могу не думать, что это что-то гораздо более тонкое в вычислении хеш-кода.

Ответ 6

documetation предполагает, что из-за непредсказуемости GetHashCode() вы должны создать свой собственный. Он считался непредсказуемым, потому что каждый тип имеет свою собственную реализацию, и, поскольку мы не знаем его внутренних, мы должны создавать свои собственные в соответствии с тем, как мы оцениваем уникальность.

Однако, я думаю, что ответ заключается в том, что GetHashCode() не использует математическое десятичное значение для создания хэш-кода.

Математически мы видим, что 295.50000000 и 295.5 являются одинаковыми. Когда вы смотрите на десятичные объекты в среде IDE, это также верно. Однако, если вы делаете ToString() на обоих десятичных знаках, вы увидите, что компилятор видит их по-другому, т.е. Вы все равно увидите 295.50000000. GetHashCode(), очевидно, не использует математическое представление десятичного числа для создания хэш-кода.

Ваше исправление просто создает новый десятичный знак без всех конечных нулей, поэтому он работает.