Почему переменная типа double может иметь неожиданный результат? - PullRequest
1 голос
/ 01 марта 2011

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

double a = 1117.54 + 8561.64 + 13197.37;
double b = 22876.55;
Console.WriteLine("{0} == {1}: {2}", a, b, a == b);

Дает нам этот вывод:

22876.55 == 22876.55: False

Дальнейшая проверка показывает, что переменная a фактически содержит значение 22876.550000000003.

Это также воспроизводится в vb.net. я в здравом уме ? Что происходит?

Ответы [ 6 ]

3 голосов
/ 01 марта 2011

Типы с плавающей точкой не всегда способны точно представлять свои точные десятичные значения. Это либо «известная ошибка», либо «по замыслу», в зависимости от вашей перспективы. В любом случае, это является следствием внутреннего представления типов с плавающей запятой и общего источника ошибок.

Проблема также почти неизбежна, если не считать сложной системы компьютерной алгебры, которая представляет значения символически , а не числовые типы. Откройте Windows Calculator, определите квадратный корень из 4, а затем вычтите 2 из этого значения. Вы получите какое-то бессмысленное число с плавающей запятой, которое невероятно близко к 0, но не точно 0. Результат вашего вычисления квадратного корня не был сохранен как точно 2, поэтому, когда вы вычитаете ровно 2 из него, вы получаете «неожиданный» результат. Это неожиданно, если только вы не знаете маленький грязный секрет об арифметике с основанием 2.

Если вам интересно, есть несколько мест, где вы можете найти дополнительную информацию о том, почему это так. Джон Скит написал статью , объясняющую двоичные операции с плавающей запятой в контексте .NET Framework. Если у вас есть время, вы должны также внимательно изучить публикацию с точно названным названием Что должен знать каждый компьютерный специалист об арифметике с плавающей запятой .

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

1 голос
/ 01 марта 2011

Число с плавающей запятой сохраняет приближение к числу (около 6-7 десятичных знаков точности для числа с плавающей запятой).

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

Наиболее распространенная проблема с этим - использование ==, чтобы определить, точно ли два значения равны, потому что 2.999999 и 3.00000000 очень близки, но не равны. Из-за ошибки в представлении fp очень часто получаются числа, которые близки, но не равны, как вы нашли.

Поэтому, вместо того чтобы сказать «мой номер точно равен 3,0», мы должны сказать «достаточно ли мой номер до 3,0, чтобы я был доволен им?». Мы делаем это путем тестирования с допустимым значением, например: «Является ли мое значение больше 2,999 и меньше 3,001». При записи числа вы можете использовать строку формата, такую ​​как "{0: 0.000}", чтобы округлить его и убрать крошечную ошибку, которую он отображает.

чтобы вы могли достичь желаемого (с точностью до 3 десятичных знаков) с помощью чего-то вроде:

Console.WriteLine("{0:0.000} == {1:0.000}: {2}", a, b, Math.Abs(a - b) < 0.0001);
1 голос
/ 01 марта 2011

Ты в здравом уме. Вы просто имеете дело с числами, которые не могут быть идеально сохранены в произвольном количестве двоичных цифр. Вы увидите это на любом языке - «ошибки» округления присущи формату с плавающей запятой и, следовательно, аппаратному обеспечению.

Если вам действительно нужны идеальные сравнения, попробуйте использовать десятичный трюк: выберите наименьшую дробь, с которой вы будете иметь дело, и выразите все с точки зрения этого. Ваша личная постоянная Планка, если хотите. Например, ваш пример кода станет:

int a = 111754 + 856164 + 1319737;
int b = 2287655;
//Convert back to decimal format for human consumption:
Console.WriteLine("{0} == {1}: {2}", ((double)a)/100, ((double)b)/100, a == b);

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

1 голос
/ 01 марта 2011

Как сказал sharptooth, именно так переменные с плавающей запятой сохраняются в памяти. Вы можете прочитать больше здесь

Также, чтобы проверить, равны ли два числа с плавающей запятой, вы можете использовать что-то вроде этого:

double a = 1117.54 + 8561.64 + 13197.37;
double b = 22876.55;
Console.WriteLine("{0} == {1}: {2}", a, b, fabs(a-b) < 1e-9);

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

1 голос
/ 01 марта 2011

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

Также см. этот вопрос .

0 голосов
/ 01 октября 2013

Использовать тип данных Decimal вместо типа данных Double.Числовой вещественный литерал для обработки как десятичный, используйте суффикс m или M. Без суффикса m число обрабатывается как двойное число и генерирует ошибку компилятора.

decimal a = 1117.54M + 8561.64M + 13197.37M;
decimal b = 22876.55M;
Console.WriteLine("{0} == {1}: {2}", a, b, a == b);
...