【问题标题】:Why does this simple double assertion fail in C#为什么这个简单的双重断言在 C# 中失败
【发布时间】:2013-08-20 11:29:57
【问题描述】:

C# 中的以下测试将失败

Assert.AreEqual<double>(10.0d, 16.1d - 6.1d);

问题似乎是一个浮点错误。

16.1d - 6.1d == 10.000000000000002

这让我在为使用double 的代码编写单元测试时头疼。有没有办法解决这个问题?

【问题讨论】:

  • 是的,如果你需要精确,不要使用double。使用decimal。并且不要按 3, 7, 11, 13, 17, 19, 23, ... 或它们的倍数除以:-)

标签: c# floating-point floating-point-precision


【解决方案1】:

我同意安德斯·阿贝尔的观点。没有办法使用浮点数表示来做到这一点。
IEE 1985-754 的直接结果中,只有可以表示的数字

可以存储并精确计算(只要选择的位数允许)。

例如:
1024 * 1.75 * 183.375 / 1040.0675 10 / 1.1
如果您对有理数的精确表示几乎不感兴趣,您可以使用分数编写自己的数字实现。
这可以通过保存分子、分母和符号来完成。然后需要实现乘法、减法等操作(很难确保良好的性能)。 toString() 方法可能看起来像这样(我假设 cachedRepresentation、cachedDotIndex 和 cachedNumerator 是成员变量)

 public String getString(int digits) {
            if(this.cachedRepresentation == ""){
                this.cachedRepresentation += this.positiveSign ? "" : "-";  
                this.cachedRepresentation += this.numerator/this.denominator; 
                this.cachedNumerator = 10 * (this.numerator % this.denominator);
                this.cachedDotIndex = this.cachedRepresentation.Length;
                this.cachedRepresentation += ".";
            }

            if ((this.cachedDotIndex + digits) < this.cachedRepresentation.Length)
                return this.cachedRepresentation.Substring(0, this.cachedDotIndex + digits + 1);

            while((this.cachedDotIndex + digits) >= this.cachedRepresentation.Length){
                this.cachedRepresentation += this.cachedNumerator / this.denominator;
                this.cachedNumerator = 10 * (this.cachedNumerator % denominator);
            }
            return cachedRepresentation;
        }


这对我有用。在具有长数字的操作本身上,我遇到了一些数据类型太小的问题(通常我不使用 c#)。我认为对于一个有经验的 c# 开发者来说,在没有小数据类型问题的情况下实现这一点应该没有问题。

如果您想实现这一点,您应该在初始化时和使用 euclids 最大通用除法器的操作之前对分数进行缩小。

非有理数可以(在我知道的所有情况下)由一种算法指定,该算法尽可能接近您想要的精确表示(并且计算机允许)。

【讨论】:

    【解决方案2】:

    十进制系统和双精度数的二进制表示之间没有精确的转换(请参阅下面@PatriciaShanahan 的精彩评论,了解原因)。

    在这种情况下,数字的 .1 部分是问题所在,它不能被有限地表示为双精度数(比如 1/3 不能被有限地表示为十进制数)。

    一个代码 sn-p 来解释发生了什么:

    double larger = 16.1d; //Assign closest double representation of 16.1.
    double smaller = 6.1; //Assign closest double representation of 6.1.
    double diff = larger - smaller; //Assign closest diff between larger and  
                                    //smaller, but since a smaller value has a  
                                    //larger precision the result will have better  
                                    //precision than larger but worse than smaller. 
                                    //The difference shows up as the ...000002.
    

    在比较双精度时,始终使用 Assert.Equal overload,它采用 delta 参数。

    或者,如果您确实需要精确的十进制转换,请使用 decimal 数据类型,它具有另一种二进制表示形式,并且在您的示例中将完全返回 10

    【讨论】:

    • 稍微澄清一下:可以表示为二进制分数的每个值都有一个完全相同的十进制分数。那是因为二是十的因数。该问题特定于从十进制到二进制的转换。
    • 数字16.1 位于数学区间[16, 32) 中,而数字6.1 位于[4, 8) 中。这样做的意义在于,16.1 的整数部分比6.1 多使用了两位。因此16.1 有两位小数部分,十进制.1,再次与6.1 进行比较。因此,在这两种情况下,十分之一的无限二元展开在不同的“位置”处四舍五入。在进行以[8, 16) 结尾的减法16.1 - 6.1 时,小数部分的差异变得很重要。
    【解决方案3】:

    如果您使用 NUnit,请使用 Within 选项。您可以在此处找到更多信息:http://www.nunit.org/index.php?p=equalConstraint&r=2.6.2

    【讨论】:

    • 但Within 使得它的可读性大大降低:Assert.That(order.Products.Skip(1).First()._FinalDiscountedPrice, Is.EqualTo(12.7161).Within(0.001)。百分比);
    【解决方案4】:

    浮点数是基于指数对实际值的估计,因此测试正确失败。如果您需要两个十进制数完全等价,您可能需要检查十进制数据类型。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-12-30
      • 1970-01-01
      • 2019-01-19
      • 1970-01-01
      • 2013-11-22
      • 2016-09-23
      • 1970-01-01
      相关资源
      最近更新 更多