Assert.AreEqual() с System.Double, действительно запутывающий
Описание
Это не пример реального мира! Пожалуйста, не предлагайте использовать decimal
или что-то еще.
Я только спрашиваю об этом, потому что я действительно хочу знать, почему это происходит.
Недавно я снова увидел потрясающий веб-транслятор Tekpub Освоение С# 4.0 с помощью Jon Skeet.
В эпизоде 7 - Десятичные и плавающие точки это действительно странно и даже наше
Chuck Norris of Programming (aka Jon Skeet) не имеет реального ответа на мой вопрос.
Только может быть.
Вопрос: Почему сбой MyTestMethod()
и MyTestMethod2()
прошли?
Пример 1
[Test]
public void MyTestMethod()
{
double d = 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
Console.WriteLine("d = " + d);
Assert.AreEqual(d, 1.0d);
}
Это приводит к
d = 1
Ожидается: 0.99999999999999989d Но было: 1.0d
Пример 2
[Test]
public void MyTestMethod2()
{
double d = 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
Console.WriteLine("d = " + d);
Assert.AreEqual(d, 0.5d);
}
Это приводит к успеху
d = 0,5
Но почему?
Update
Почему Assert.AreEqual()
не покрывает это?
Ответы
Ответ 1
Хорошо, я не проверял, что делает Assert.AreEqual
, но я подозреваю, что по умолчанию он не применяет никакого допуска. Я не ожидал, что это за моей спиной. Так что давайте искать другое объяснение...
В основном вы видите совпадение - ответ после четырех добавлений оказывается точным значением, вероятно, потому, что самый низкий бит теряется где-то, когда изменяется величина - я не смотрел на используемые битовые шаблоны, но если вы используйте DoubleConverter.ToExactString
(мой собственный код), вы можете точно увидеть, что это значение в любой точке:
using System;
public class Test
{
public static void Main()
{
double d = 0.1d;
Console.WriteLine("d = " + DoubleConverter.ToExactString(d));
d += 0.1d;
Console.WriteLine("d = " + DoubleConverter.ToExactString(d));
d += 0.1d;
Console.WriteLine("d = " + DoubleConverter.ToExactString(d));
d += 0.1d;
Console.WriteLine("d = " + DoubleConverter.ToExactString(d));
d += 0.1d;
Console.WriteLine("d = " + DoubleConverter.ToExactString(d));
}
}
Результаты (в моем боксе):
d = 0.1000000000000000055511151231257827021181583404541015625
d = 0.200000000000000011102230246251565404236316680908203125
d = 0.3000000000000000444089209850062616169452667236328125
d = 0.40000000000000002220446049250313080847263336181640625
d = 0.5
Теперь, если вы начинаете с другого номера, он не работает сам по себе:
(Начиная с d = 10.1)
d = 10.0999999999999996447286321199499070644378662109375
d = 10.199999999999999289457264239899814128875732421875
d = 10.2999999999999989341858963598497211933135986328125
d = 10.39999999999999857891452847979962825775146484375
d = 10.4999999999999982236431605997495353221893310546875
Итак, в основном вам повезло или не повезло с вашим тестом - ошибки отменили себя.
Ответ 2
Assert.AreEqual()
делает, что; вы должны использовать перегрузку с третьим аргументом delta
:
Assert.AreEqual(0.1 + 0.1 + 0.1, 0.3, 0.00000001);
Ответ 3
Поскольку парные числа, как и все числа с плавающей запятой, являются приближениями, а не абсолютными значениями двоичных (base-2) представлений, которые, возможно, 10 (таким же образом, что base-10 не может отлично представлять 1/3). Таким образом, тот факт, что во втором случае округляется до правильного значения, когда вы выполняете сравнение равенства (и тот факт, что первый из них нет), просто удача, а не ошибка в структуре или что-то еще.
Также прочтите следующее: Выполнение результата для float в методе, возвращающем результат изменения с плавающей запятой
Assert.Equals не охватывает этот случай, потому что принцип наименьшего удивления утверждает, что, поскольку все другие встроенные числовые значения типа в .NET определяет .Equals() для выполнения эквивалентной операции ==, поэтому Double делает это тоже. Поскольку на самом деле два числа, которые вы генерируете в своем тесте (буквальный 0.5d и 5x сумма .1d), не равны ==
(фактические значения в регистрах процессоров различны). Equals() возвращает false.
Это не рамочное намерение нарушить общепринятые правила вычислений, чтобы сделать вашу жизнь удобной.
Наконец, я бы предложил, чтобы NUnit действительно осознал эту проблему, и согласно http://www.nunit.org/index.php?p=equalConstraint&r=2.5 предлагает следующий метод проверки с плавающей запятой равенство в пределах допуска:
Assert.That( 5.0, Is.EqualTo( 5 );
Assert.That( 5.5, Is.EqualTo( 5 ).Within(0.075);
Assert.That( 5.5, Is.EqualTo( 5 ).Within(1.5).Percent;
Ответ 4
Assert.AreEqual
учитывает это.
Но для этого вам нужно указать свой запас ошибки - дельта в пределах разницы между двумя значениями float считается равной для вашего приложения.
Есть две перегрузки для Assert.AreEqual
, которые принимают только два параметра - общий (T, T)
и не общий - (object, object)
. Они могут выполнять сравнения по умолчанию.
Используйте одну из перегрузок, которые принимают double
, а также параметр для дельта.
Ответ 5
Это особенность компьютерной арифметики с плавающей запятой
(http://www.eskimo.com/~scs/cclass/progintro/sx5.html)
Важно помнить, что точность с плавающей запятой числа обычно ограничены, и это может привести к неожиданным результатам. Результат такого разделения, как 1/3, не может быть представлен точно (это бесконечно повторяющаяся фракция, 0.333333...), поэтому вычисление (1 /3) x 3 имеет тенденцию давать результат, например, 0.999999... вместо 1.0. Кроме того, в основании 2 доля 1/10 или 0,1 в десятичной форме также бесконечно повторяющаяся фракция и не может быть точно представлена, либо (1/10) x 10 также может давать 0.999999.... По этим причинам и другие, вычисления с плавающей запятой редко точны. При работе с плавающей точкой компьютера, вы должны быть осторожны, чтобы не сравнивать два числа для точного равенства, и вы должны убедиться, что `` round off error '' не накапливается до тех пор, пока он серьезно не ухудшит результаты ваших расчетов.
Вы должны явно указать точность для Assert
Например:
double precision = 1e-6;
Assert.AreEqual(d, 1.0, precision);
Он работает для вас. Я часто использую этот путь в своем коде, но точность в зависимости от ситуации.
Ответ 6
Это связано с тем, что числа с плавающей запятой теряют точность. Лучшим способом сравнения равных является вычитание чисел и проверка того, что разные меньше определенного числа, например .001 (или любой точности, в которой вы нуждаетесь). Посмотрите http://msdn.microsoft.com/en-us/library/system.double%28v=VS.95%29.aspx, а именно раздел с плавающей запятой и потеря точности.
Ответ 7
0.1
не может быть представлен точно в двойном из-за его внутреннего формата.
Используйте десятичные числа, если вы хотите представить базовые 10 чисел.
Если вы хотите сравнить двойники, проверьте, находятся ли они в очень небольшом количестве друг от друга.