Assert.AreEqual () с System.Double становится действительно запутанным - PullRequest
18 голосов
/ 12 января 2012

Описание

Это не пример из реальной жизни!Пожалуйста, не предлагайте использовать decimal или что-то еще.

Я спрашиваю об этом только потому, что очень хочу знать, почему это происходит.

Я недавно видел потрясающую трансляцию Tekpub Освоение C # 4.0 с Джоном Скитом .

В эпизоде ​​ 7 - десятичные дроби и числа с плавающей запятой все идет очень странно, и даже у нашего Чака Норриса по программированию (он же Джон Скит) нет реального ответа намой вопрос.Только может быть .

Вопрос: почему 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

Но почему?

Обновление

Почему Assert.AreEqual() не покрывает это?

Ответы [ 7 ]

57 голосов
/ 12 января 2012

Assert.AreEqual() охватывает ; Вы должны использовать перегрузку с третьим аргументом delta:

Assert.AreEqual(0.1 + 0.1 + 0.1, 0.3, 0.00000001);
11 голосов
/ 12 января 2012

Поскольку значения типа Double, как и все числа с плавающей запятой, представляют собой аппроксимации , а не абсолютные значения двоичные (base-2) представления, которые могут быть не в состоянии точно представить дроби base-10(так же, как base-10 не может идеально представить 1/3).Поэтому тот факт, что второй случай округляется до правильного значения, когда вы выполняете сравнение на равенство (и тот факт, что первый нет) - это просто удача, а не ошибка в фреймворке или что-то еще.

Кроме того, прочитайте это: Приведение результата к плавающей точке в методе, возвращающем результат изменения плавающей запятой

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;
9 голосов
/ 14 января 2012

Хорошо, я не проверял, что делает 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

Так что в основном вам повезло или не повезло с вашим тестом - ошибки исчезли сами собой.

5 голосов
/ 12 января 2012

Assert.AreEqual учитывает это.

Но для того, чтобы сделать это, вам необходимо указать свой предел погрешности - для вашего приложения дельта в пределах разницы между двумя значениями с плавающей запятой считается равной Существует две перегрузки для Assert.AreEqual, которые принимают только два параметра - общий (T, T) и неуниверсальный - (object, object). Они могут выполнять только сравнения по умолчанию.

Используйте одну из перегрузок, которые принимают double, и которая также имеет параметр для дельты.

3 голосов
/ 12 января 2012

Это особенность компьютерной арифметики с плавающей запятой (http://www.eskimo.com/~scs/cclass/progintro/sx5.html)

Важно помнить, что точность чисел с плавающей запятой обычно ограничена, и это может привести к неожиданным результатам.результат деления типа 1/3 не может быть представлен точно (это бесконечно повторяющаяся дробь, 0,333333 ...), поэтому вычисление (1/3) x 3 приводит к результату, подобному 0,9999999 ... вместо 1,0.в базе 2 дробь 1/10, или 0,1 в десятичной дроби, также является бесконечно повторяющейся дробью, и ее нельзя точно представить, поэтому (1/10) x 10 также может привести к 0,9999999 .... По этим причинами другие, вычисления с плавающей точкой редко бывают точными. При работе с компьютером с плавающей точкой вы должны быть осторожны, чтобы не сравнивать два числа для точного равенства, и вы должны убедиться, что «ошибка округления» не накапливается доэто серьезно ухудшает результаты ваших расчетов.

Вы должны явно установить точность дляr Утвердить

Например:

double precision = 1e-6;
Assert.AreEqual(d, 1.0, precision);

Это пример работы для вас.Я часто использую этот способ в своем коде, но точность зависит от ситуации

1 голос
/ 12 января 2012

Это потому, что числа с плавающей точкой теряют точность.Лучший способ сравнить «равные» - вычесть числа и убедиться, что они меньше определенного числа, например 0,001 (или с любой точностью, которая вам нужна).Посмотрите на http://msdn.microsoft.com/en-us/library/system.double%28v=VS.95%29.aspx, в частности, на значения с плавающей точкой и потерю точности.

0 голосов
/ 12 января 2012

0.1 не может быть точно представлен в виде двойного числа из-за его внутреннего формата.

Используйте десятичную дробь, если вы хотите представить базовые 10 чисел.

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

...