Форматирование удваивается для вывода в C#



запуск быстрого эксперимента, связанного с двойное умножение сломано в .NET? и прочитав пару статей о форматировании строк C#, я подумал, что это:



{
double i = 10 * 0.69;
Console.WriteLine(i);
Console.WriteLine(String.Format(" {0:F20}", i));
Console.WriteLine(String.Format("+ {0:F20}", 6.9 - i));
Console.WriteLine(String.Format("= {0:F20}", 6.9));
}


будет c# эквивалент этого кода C:



{
double i = 10 * 0.69;

printf ( "%fn", i );
printf ( " %.20fn", i );
printf ( "+ %.20fn", 6.9 - i );
printf ( "= %.20fn", 6.9 );
}


однако C# выдает результат:



6.9
6.90000000000000000000
+ 0.00000000000000088818
= 6.90000000000000000000


несмотря на то, что я показываю равное значение 6.89999999999999946709 (а не 6.9) в отладчике.



по сравнению с C который показывает точность, запрошенную форматом:



6.900000                          
6.89999999999999946709
+ 0.00000000000000088818
= 6.90000000000000035527


что происходит?



(Microsoft .NET Framework версии 3.51 SP1 / Visual Studio C# 2008 Express Edition)





у меня есть опыт в численных вычислениях и опыт реализации интервальной арифметики - метода оценки ошибок из - за пределов точности в сложных числовых системах-на различных платформах. Чтобы получить награду, не пытайтесь объяснить о хранении точность-в этом случае это разница одного ULP 64-битного двойника.



чтобы получить награду, я хочу знать, как (или может ли) .Net форматировать двойник с требуемой точностью, как видно в коде C.

727   10  

10 ответов:

проблема в том, что .NET всегда будет вокруг double до 15 знаков после запятой до применение форматирования, независимо от точности, запрошенной вашим форматом, и независимо от точного десятичного значения двоичного числа.

Я бы предположил, что отладчик Visual Studio имеет свои собственные процедуры форматирования/отображения, которые напрямую обращаются к внутреннему двоичному числу, следовательно, расхождения между вашим кодом C#, вашим кодом C и отладчик.

нет ничего встроенного, что позволит вам получить доступ к точным десятичным значением double, или чтобы вы могли форматировать double к определенному числу десятичных знаков, но вы можете сделать это самостоятельно, выделив внутреннее двоичное число и перестроив его как строковое представление десятичного значения.

в качестве альтернативы, вы можете использовать Джон Скит DoubleConverter класс (связано с его " двоичный с плавающей запятой и .NET" статья). Это имеет ToExactString метод, который возвращает точное десятичное значение double. Вы можете легко изменить это, чтобы включить округление выходных данных до определенной точности.

double i = 10 * 0.69;
Console.WriteLine(DoubleConverter.ToExactString(i));
Console.WriteLine(DoubleConverter.ToExactString(6.9 - i));
Console.WriteLine(DoubleConverter.ToExactString(6.9));

// 6.89999999999999946709294817992486059665679931640625
// 0.00000000000000088817841970012523233890533447265625
// 6.9000000000000003552713678800500929355621337890625
Digits after decimal point
// just two decimal places
String.Format("{0:0.00}", 123.4567);      // "123.46"
String.Format("{0:0.00}", 123.4);         // "123.40"
String.Format("{0:0.00}", 123.0);         // "123.00"

// max. two decimal places
String.Format("{0:0.##}", 123.4567);      // "123.46"
String.Format("{0:0.##}", 123.4);         // "123.4"
String.Format("{0:0.##}", 123.0);         // "123"
// at least two digits before decimal point
String.Format("{0:00.0}", 123.4567);      // "123.5"
String.Format("{0:00.0}", 23.4567);       // "23.5"
String.Format("{0:00.0}", 3.4567);        // "03.5"
String.Format("{0:00.0}", -3.4567);       // "-03.5"

Thousands separator
String.Format("{0:0,0.0}", 12345.67);     // "12,345.7"
String.Format("{0:0,0}", 12345.67);       // "12,346"

Zero
Following code shows how can be formatted a zero (of double type).

String.Format("{0:0.0}", 0.0);            // "0.0"
String.Format("{0:0.#}", 0.0);            // "0"
String.Format("{0:#.0}", 0.0);            // ".0"
String.Format("{0:#.#}", 0.0);            // ""

Align numbers with spaces
String.Format("{0,10:0.0}", 123.4567);    // "     123.5"
String.Format("{0,-10:0.0}", 123.4567);   // "123.5     "
String.Format("{0,10:0.0}", -123.4567);   // "    -123.5"
String.Format("{0,-10:0.0}", -123.4567);  // "-123.5    "

Custom formatting for negative numbers and zero
String.Format("{0:0.00;minus 0.00;zero}", 123.4567);   // "123.46"
String.Format("{0:0.00;minus 0.00;zero}", -123.4567);  // "minus 123.46"
String.Format("{0:0.00;minus 0.00;zero}", 0.0);        // "zero"

Some funny examples
String.Format("{0:my number is 0.0}", 12.3);   // "my number is 12.3"
String.Format("{0:0aaa.bbb0}", 12.3);

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

если вместо этого вы используете " {0:R} "он будет производить то, что называется" туда и обратно " значение, взгляните на это ссылка MSDN для получения дополнительной информации, вот мой код и на выход:

double d = 10 * 0.69;
Console.WriteLine("  {0:R}", d);
Console.WriteLine("+ {0:F20}", 6.9 - d);
Console.WriteLine("= {0:F20}", 6.9);

выход

  6.8999999999999995
+ 0.00000000000000088818
= 6.90000000000000000000

хотя этот вопрос пока закрыт, я считаю, что стоит упомянуть, как это зверство появилось на свет. В некотором смысле вы можете обвинить спецификацию C#, которая утверждает, что double должен иметь точность 15 или 16 цифр (результат IEEE-754). Немного дальше (раздел 4.1.6) указано, что реализации разрешено использовать выше точности. Заметьте:выше, не ниже. Им даже разрешено отклоняться от IEEE-754: выражения типа x * y / z здесь x * y даст +/-INF но будет в допустимом диапазоне после деления, не обязательно приведет к ошибке. Эта функция облегчает компиляторам использование более высокой точности в архитектурах, где это дает лучшую производительность.

но я обещал "разум". Вот цитата (Вы запросили ресурс в одном из ваших последних комментариев) от общий источник CLI, в clr/src/vm/comnumber.cpp:

"для того, чтобы дать цифры, которые находиться как дружелюбный для показа и круг-trippable, мы разбираем число через 15 цифр, а затем определить, если это туда и обратно к тому же значению. Если это так, мы преобразуем это число в a строка, в противном случае мы повторно используем 17 цифры и показать, что."

другими словами: команда разработчиков CLI MS решила быть как круговой, так и показывать красивые значения, которые не так больно читать. Хорошо это или плохо? Я бы хотел отказаться или уклоняться.

трюк, который он делает, чтобы узнать эту круглую отключаемость любого заданного числа? Преобразование в общую структуру чисел (которая имеет отдельные поля для свойств double) и обратно, а затем сравнить, отличается ли результат. Если он отличается, используется точное значение (как в вашем среднем значении с 6.9 - i) если это то же самое, используется" красивое значение".

как вы уже заметили в комментарии к Andyp,6.90...00 побитовое значение равно 6.89...9467. И теперь вы знаете, почему 0.0...8818 используется: он побитово отличается от 0.0.

этой 15 цифр барьер жестко закодирован и может быть изменен только путем перекомпиляции CLI, используя Mono или позвонив в Microsoft и убедив их добавить опцию для печати полной "точности" (это не совсем точность, но отсутствие лучшего слова). Вероятно, проще просто вычислить точность 52 бит самостоятельно или использовать библиотеку, упомянутую ранее.

изменить: если вы любите экспериментировать с iee-754 плавающими точками,рассмотрим этот онлайн-инструмент, который показывает вам все соответствующие части плавающей точкой.

использовать

Console.WriteLine(String.Format("  {0:G17}", i));

Это даст вам все 17 цифр, которые он имеет. По умолчанию двойное значение содержит 15 десятичных цифр точности, хотя внутри поддерживается максимум 17 цифр. {0: R} не всегда даст вам 17 цифр, он даст 15, если число может быть представлено с такой точностью.

который возвращает 15 цифр, если число может быть представлено с точностью или 17 цифр, если число может быть представлено с максимальной точностью. Там нет ничего, что вы можете сделать, чтобы сделать двойной возврат больше цифр, что является способом его реализации. Если вам это не нравится, сделайте новый двойной класс самостоятельно...

.NET двойной cant хранить больше цифр, чем 17, так что вы не можете увидеть 6.89999999999999946709 в отладчике вы увидите 6.899999999999999995. Пожалуйста, предоставьте изображение, чтобы доказать, что мы ошибаемся.

ответ на этот вопрос прост и может быть найден на MSDN

помните, что число с плавающей запятой может аппроксимировать только десятичное число, и что точность числа с плавающей запятой определяет, насколько точно это число аппроксимирует десятичное число. По умолчанию двойное значение содержит 15 десятичных цифр точности, хотя внутри поддерживается максимум 17 цифр.

в вашем примере значение i равно 6.89999999999999946709, которое имеет число 9 для всех позиций между 3-й и 16-й цифрами (не забудьте подсчитать целочисленную часть в цифрах). При преобразовании в строку фреймворк округляет число до 15-й цифры.

i     = 6.89999999999999 946709
digit =           111111 111122
        1 23456789012345 678901

Я попытался воспроизвести ваши выводы, но когда я смотрел " i "в отладчике, он появился как "6.899999999999999995", а не как "6.89999999999999946709", как вы написали в вопросе. Можете ли вы предоставить шаги, чтобы воспроизвести то, что вы видели?

чтобы увидеть, что отладчик показывает вам, вы можете использовать DoubleConverter как в следующей строке кода:

Console.WriteLine(TypeDescriptor.GetConverter(i).ConvertTo(i, typeof(string)));

надеюсь, что это помогает!

Edit: я думаю, что я больше устал, чем я думал, конечно, это то же самое, что форматирование значение и обратно (как упоминалось ранее).

ответ да, двойная печать нарушена в .NET, они печатают конечные цифры мусора.

вы можете прочитать, как это правильно реализовать здесь.

мне пришлось сделать то же самое для IronScheme.

> (* 10.0 0.69)
6.8999999999999995
> 6.89999999999999946709
6.8999999999999995
> (- 6.9 (* 10.0 0.69))
8.881784197001252e-16
> 6.9
6.9
> (- 6.9 8.881784197001252e-16)
6.8999999999999995

Примечание: и C и C# имеет правильное значение, просто сломанная печать.

Update: я все еще ищу разговор списка рассылки, который у меня был, который привел к этому открытию.

Я нашел это быстрое решение.

    double i = 10 * 0.69;
    System.Diagnostics.Debug.WriteLine(i);


    String s = String.Format("{0:F20}", i).Substring(0,20);
    System.Diagnostics.Debug.WriteLine(s + " " +s.Length );

Comments

    Ничего не найдено.