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 чисел.

Если вы хотите сравнить двойники, проверьте, находятся ли они в очень небольшом количестве друг от друга.