Форматирование удваивается для вывода в 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 ( "%f\n", i ); printf ( " %.20f\n", i ); printf ( "+ %.20f\n", 6.9 - i ); printf ( "= %.20f\n", 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 с пакетом обновления 1 (SP1) / Visual Studio C # 2008 Express Edition)


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

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

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

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

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

Кроме того, вы можете использовать class DoubleConverter Jon Skeet (связанный с его статьей «Binary floating point и .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}», он произведет так называемое значение «round-trip», взгляните на эту ссылку 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 :

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

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

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

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

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

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

использование

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

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

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

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

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

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

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

 i = 6.89999999999999 946709 digit = 111111 111122 1 23456789012345 678901 

я попытался воспроизвести ваши результаты, но когда я наблюдал «я» в отладчике, он появился как «6.8999999999999995», а не «6.89999999999999946709», как вы писали в вопросе. Можете ли вы представить шаги по воспроизведению того, что вы видели?

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

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

Надеюсь это поможет!

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

Ответ: да, двойная печать нарушена в .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 # имеют правильное значение, только сломанная печать.

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

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

  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 ); 

Console.WriteLine (string.Format («Стоимость курса: {0: 0.00}», + cfees));

 internal void DisplaycourseDetails() { Console.WriteLine("Course Code : " + cid); Console.WriteLine("Couse Name : " + cname); //Console.WriteLine("Couse Name : " + string.Format("{0:0.00}"+ cfees)); // string s = string.Format("Course Fees is {0:0.00}", +cfees); // Console.WriteLine(s); Console.WriteLine( string.Format("Course Fees is {0:0.00}", +cfees)); } static void Main(string[] args) { Course obj1 = new Course(101, "C# .net", 1100.00); obj1.DisplaycourseDetails(); Course obj2 = new Course(102, "Angular", 7000.00); obj2.DisplaycourseDetails(); Course obj3 = new Course(103, "MVC", 1100.00); obj3.DisplaycourseDetails(); Console.ReadLine(); } } 
  • Является ли плавающая точка == когда-либо ОК?
  • Как выполнить поразрядную операцию с числами с плавающей запятой
  • Почему арифметика с плавающей запятой в C # неточна?
  • самое большое целое число, которое может быть сохранено в двойном
  • Почему бы не использовать Double или Float для представления валюты?
  • Функции сравнения с плавающей запятой для C #
  • Решение проблем точности чисел с плавающей запятой
  • Почему целочисленное деление на -1 (отрицательное) приводит к FPE?
  • Равновесие и допуски с плавающей точкой
  • Значение синуса 180 выходит как 1.22465e-16
  • Формат Float до n знаков после запятой
  • Давайте будем гением компьютера.