使用System.Double的Assert.AreEqual()变得非常混乱

时间:2012-01-11 20:13:23

标签: c# .net c#-4.0 floating-point clr

描述

这不是一个现实世界的例子!请不要建议使用decimal或其他内容。

我只是问这个,因为我真的想知道为什么会这样。

我最近再次看到了令人敬畏的Tekpub网络广播使用Jon Skeet 掌握C#4.0。

在剧集 7 - 小数点和浮点数中,这真的很奇怪,甚至我们的 编程的Chuck Norris(又名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

但为什么?

更新

为什么Assert.AreEqual()没有涵盖?

7 个答案:

答案 0 :(得分:57)

Assert.AreEqual() 确实涵盖了这一点;你必须使用带有第三个delta参数的重载:

Assert.AreEqual(0.1 + 0.1 + 0.1, 0.3, 0.00000001);

答案 1 :(得分:11)

因为双打与所有浮点数一样,是 近似值,而不是绝对值二进制(base-2)表示,它们可能无法完美地表示基数 - 10个分数(与基数10完全不能代表1/3的方式相同)。因此,当你执行相等比较时(第一个没有)事实上第二个碰巧会转到正确的值,这只是运气,而不是框架中的错误或其他任何错误。

另外,请阅读:Casting a result to float in method returning float changes result

Assert.Equals不包括这种情况,因为the principle of least astonishment表明由于.NET中的每个其他内置数值类型都定义.Equals()来执行==的等效操作,所以Double这样做好。实际上,您在测试中生成的两个数字(文字0.5d和.xd的5x总和)不是==相等(处理器寄存器中的实际值不同)Equals()返回假的。

为了让您的生活更方便,打破普遍接受的计算规则并不是框架的意图。

最后,我提出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;

答案 2 :(得分:9)

好的,我还没有检查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

所以基本上你碰巧遇到了你的考试很幸运或不幸 - 错误自行取消了。

答案 3 :(得分:5)

Assert.AreEqual确实考虑到了这一点。

但是为了做到这一点,你需要提供误差幅度 - 两个浮点值之间差值的增量被视为等于您应用的

Assert.AreEqual有两个重载只有两个参数 - 一个通用的(T, T)和一个非通用的 - (object, object)。这些只能进行默认比较。

使用其中一个带double的重载,并且还有一个delta参数。

答案 4 :(得分:3)

这是计算机浮点算术的特征 (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   关闭错误''不会累积,直到它严重降低结果   你的计算。

您应该明确设置Assert的精度

例如:

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

这适合你的样本。我经常在我的代码中使用这种方式,但精确度取决于具体情况

答案 5 :(得分:1)

这是因为浮点数会失去精度。比较equals的最佳方法是减去数字,并验证差异是否小于某个数字,如.001(或您需要的精度)。请特别注意http://msdn.microsoft.com/en-us/library/system.double%28v=VS.95%29.aspx浮点值和精度损失部分。

答案 6 :(得分:0)

0.1因为它的内部格式而无法在双精度中完全表示。

如果要表示基数为10的数字,请使用小数。

如果你想比较双打,请检查它们是否相互之间的数量非常小。