Коммутативное свойство сложения с числами двойной точности
Рассмотрим следующий модульный тест:
// Works (sum 0.1 to 0.4)
float f1 = 0.1F + 0.2F + 0.3F + 0.4F;
Assert.AreEqual(1F, f1);
// Works too (sum 0.4 to 0.1)
float f2 = 0.4F + 0.3F + 0.2F + 0.1F;
Assert.AreEqual(1F, f2);
// Works (sum 0.1 to 0.4)
double d1 = 0.1D + 0.2D + 0.3D + 0.4D;
Assert.AreEqual(1D, d1);
// Fails! (sum 0.4 to 0.1)
double d2 = 0.4D + 0.3D + 0.2D + 0.1D;
Assert.AreEqual(1D, d2);
Все работает, как и ожидалось для типа с плавающей точкой (сумма равна 1 в обоих случаях), но при использовании double коммутативность сложения не учитывается. Действительно, сумма первого равна 1, но за второе я получаю 0,999999999.....
Я понимаю, почему результат один раз, а один раз нет (потому что некоторые числа не могут быть представлены без потери точности в базе 2), но это не объясняет, почему это работает для float, а не для double...
Может кто-нибудь объяснить это?
4 ответа
float f11 = 0;
f11 += 0.1F;//0.1
f11 += 0.2F;//0.3
f11 += 0.3F;//0.6
f11 += 0.4F;//1.0
float f2 = 0.4F + 0.3F + 0.2F + 0.1F;
float f22 = 0;
f22 += 0.4F;//0.4
f22 += 0.3F;//0.700000048
f22 += 0.2F;//0.900000036
f22 += 0.1F;//1.0
Чтобы добавить ответ astander- это то, как значения выглядят для чисел с плавающей точкой. Из-за более низкой точности (7 цифр для чисел с плавающей запятой, 14-15 для двойных) значения в конечном итоге отображаются по-разному и случайно совпадают с ожидаемыми.
Но это все - это просто совпадение! Никогда не зависи от этого. Операции с плавающей запятой являются ассоциативными и не точными. Никогда не сравнивайте поплавки или удвоенные, используя ==
, всегда рассматривайте возможность использования некоторого значения маржи. Этот образец работает для 1
, но для другой ценности это потерпит неудачу.
Посмотрите на ниже
// This works (sum 0.1 to 0.4)
double d1 = 0.1D + 0.2D + 0.3D + 0.4D;
double d11 = 0;
d11 += 0.1D;//0.1
d11 += 0.2D;//0.30000000000000004
d11 += 0.3D;//0.60000000000000009
d11 += 0.4D;//1.0
// This does NOT work! (sum 0.4 to 0.1)
double d2 = 0.4D + 0.3D + 0.2D + 0.1D;
double d22 = 0;
d22 += 0.4D;//0.4
d22 += 0.3D;//0.7
d22 += 0.2D;//0.89999999999999991
d22 += 0.1D;//0.99999999999999989
И отладка, посмотрите на отдельные шаги.
Вам нужно помнить, что
double d2 = 0.4D + 0.3D + 0.2D + 0.1D;
также можно рассматривать как
double d2 = (((0.4D + 0.3D) + 0.2D) + 0.1D);
Кажется, что проблема не в 2-х представлениях числа 1, а в более двух путях того, как он туда попал.
В следующих:
float f = 0.3F + 0.3F + 0.2F + 0.1F;
double d = 0.3D + 0.3D + 0.2D + 0.1D;
Результатом будет:
float f = 0.900000036f;
double d = 0.9;
Так что для разных чисел ошибки округления могут возникать в числах с плавающей запятой, где их нет в двойных числах, и наоборот - это потому, что они имеют разное количество цифр, поэтому место, где могут возникать ошибки округления, отличается.
Это известная проблема при сравнении чисел с плавающей запятой, потому что в соответствии со спецификацией C# они реализованы на основе неприятного стандарта IEEE, который вызывает такое поведение.
Так что вы никогда не должны сравнивать 2 числа с плавающей запятой или двойные в C#. Вместо этого вы должны увидеть, меньше ли их разница, чем конкретное значение дельты.