Преобразование десятичного числа для удвоения числа в C# приводит к различию

Я добавлю к рекомендациям выполнения, НАЧИНАЮТ TRAN перед Вашим ОБНОВЛЕНИЕМ, просто не забывайте на самом деле делать ФИКСАЦИЮ; можно нанести такой же ущерб при отъезде незафиксированной транзакции открытой. Не становитесь отвлеченными телефонами, коллегами, ланч и т.д., когда посреди обновлений или Вы найдете, что все остальные заблокированы вплоть до Вас ФИКСАЦИЯ или ОТКАТ.

43
задан Iravanchi 18 October 2009 в 08:00
поделиться

4 ответа

Интересно - хотя я обычно не доверяю обычным способам записи значений с плавающей запятой, когда вас интересуют точные результаты.

Вот несколько более простая демонстрация с использованием DoubleConverter.cs , который я использовал несколько раз раньше.

using System;

class Test
{
    static void Main()
    {
        decimal dcm1 = 8224055000.0000000000m;
        decimal dcm2 = 8224055000m;
        double dbl1 = (double) dcm1;
        double dbl2 = (double) dcm2;

        Console.WriteLine(DoubleConverter.ToExactString(dbl1));
        Console.WriteLine(DoubleConverter.ToExactString(dbl2));
    }
}

Результаты:

8224055000.00000095367431640625
8224055000

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

Он также нарушает раздел 6.2.1 спецификации C #:

Для преобразования десятичного числа в число с плавающей запятой или двойное десятичное значение округляется до ближайшее значение типа double или float. Хотя это преобразование может потерять точность, оно никогда не вызывает исключение, которое должно быть сгенерировано.

"Ближайшее двойное значение" явно всего 8224055000 ... так что это ошибка ИМО. Хотя я не ожидал, что это исправят в ближайшее время. (Между прочим, он дает те же результаты в .NET 4.0b1.)

Чтобы избежать ошибки, вы, вероятно, захотите сначала нормализовать десятичное значение, эффективно «удаляя» лишние нули после десятичной точки. Это несколько сложно, поскольку включает 96-битную целочисленную арифметику - класс .NET 4.0 BigInteger вполне может облегчить эту задачу, но это может не подойти вам.

49
ответ дан 26 November 2019 в 22:51
поделиться

Ответ заключается в том, что decimal пытается сохранить количество значащих цифр. Таким образом, 8224055000.0000000000m имеет 20 значащих цифр и хранится как 82240550000000000000E-10 , а 8224055000m имеет только 10 и сохраняется как 8224055000E + 0 . Мантисса double составляет (логически) 53 бита, т. Е. Не более 16 десятичных цифр. Это именно та точность, которую вы получаете при преобразовании в double , и действительно, случайный 1 в вашем примере находится в 16-м десятичном разряде. Преобразование выполняется не 1 в 1, потому что double использует основание 2.

Вот двоичные представления ваших чисел:

dcm:
00000000000010100000000000000000 00000000000000000000000000000100
01110101010100010010000001111110 11110010110000000110000000000000
dbl:
0.10000011111.1110101000110001000111101101100000000000000000000001
dcm2:
00000000000000000000000000000000 00000000000000000000000000000000
00000000000000000000000000000001 11101010001100010001111011011000
dbl2 (8224055000.0):
0.10000011111.1110101000110001000111101101100000000000000000000000

Для double я использовал точки для разделения знака, экспоненты и мантиссы. поля; для десятичного числа, см. MSDN для decimal.GetBits , но, по сути, последние 96 бит являются мантиссой. Обратите внимание, как биты мантиссы dcm2 и старшие биты dbl2 точно совпадают (не забывайте о неявном 1 бите в double ), и на самом деле эти биты представляют 8224055000. Биты мантиссы в dbl такие же, как в dcm2 и dbl2 , но для неприятного 1 в младшем бите. Показатель dcm равен 10, а мантисса - 82240550000000000000.

Обновление II: На самом деле очень легко отсечь конечные нули.

// There are 28 trailing zeros in this constant —
// no decimal can have more than 28 trailing zeros
const decimal PreciseOne = 1.000000000000000000000000000000000000000000000000m ;

// decimal.ToString() faithfully prints trailing zeroes
Assert ((8224055000.000000000m).ToString () == "8224055000.000000000") ;

// Let System.Decimal.Divide() do all the work
Assert ((8224055000.000000000m / PreciseOne).ToString () == "8224055000") ;
Assert ((8224055000.000010000m / PreciseOne).ToString () == "8224055000.00001") ;
25
ответ дан 26 November 2019 в 22:51
поделиться

Это старая проблема, и она была предметом множества аналогичных вопросов в StackOverflow.

Упрощенное объяснение состоит в том, что десятичные числа не могут быть точно представлены в двоичном формате

Эта ссылка представляет собой статью, которая может объяснить проблему.

1
ответ дан 26 November 2019 в 22:51
поделиться

Статья Что должен знать каждый компьютерный ученый об арифметике с плавающей запятой , была бы отличным местом для начала.

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

5
ответ дан 26 November 2019 в 22:51
поделиться
Другие вопросы по тегам:

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