65.9K
CodeProject 正在变化。 阅读更多。
Home

揭秘 C# 浮点相等和关系运算

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (17投票s)

2012 年 5 月 13 日

CPOL

5分钟阅读

viewsIcon

60303

基于比较 epsilon 值,使浮点数的相等性和关系运算符更加可靠。

引言

我的介绍通常是这样的:为什么要费力气呢?C# 的 double 值类型对每个人来说都很熟悉。

我们真的了解这个类型吗?这会产生什么结果?

double a = 1.0 / 6.0;
Console.WriteLine(a + a + a + a + a + a == 1.0);
答案:False(六个六分之一相加不等于 1.0)!

或者这对我们会产生什么结果?

double a 1E300 / 1E-100;
Console.WriteLine(a - a == 0.0);
答案:Falsea 是 +∞,而 ∞ - ∞NaN,不等于 0.0)!

或者最后,我们从以下代码中得到什么?

Random r = new Random();
foreach (var n in Enumerable.Range(0, 10))
{
    double sum = 0.0;
    foreach (int count in Enumerable.Range(0, 100))
    {
        sum += Math.Round(r.NextDouble() / 50.0, 2);
    }
    Console.WriteLine(sum);
}
答案:当然,你会得到随机的和,但并非总是只有两位小数(舍入不是“精确”的,因为结果仍然是 double 值……)!

本文详细介绍了如何处理比较 double 值时出现的问。讨论了以下几点:

  • double 类型一览
  • double 相等性
  • double 相等性的怪异之处
  • 使 double 相等性容错
  • double 关系运算符
  • double 关系的怪异之处
  • 使 double 关系容错
  • 摘要

double 类型一览

C# 的 double 类型根据 IEEE-754 规范定义。这意味着 double

  • 是一种浮点类型
  • 范围约为 -10308 到 10308
  • 精度约为 15 位十进制数字
  • 最小的数(最接近 0.0)约为 +/- 10-308
  • 有两个零值:+/- 0.0
  • 有两个无穷值:+/- ∞
  • 有一个 NaN(Not a Number)“值”
  • 具有常用的算术运算符
  • 具有常用的相等性和关系运算符
  • 具有 Math 库支持,用于绝对值、舍入等更多函数。

double 相等性

正如我们在引言和概述中所见,double 具有一些特殊值

  • - 0.0
  • + 0.0
  • - ∞
  • + ∞
  • NaN

让我们看看所有这些值(加上表示“正常” double 值的 1.0)的相等性表格

相等性(x==yy=1.0y=-0.0y=+0.0y=-∞y=+∞y=NaN
x=1.0True
x=-0.0TrueTrue
x=+0.0TrueTrue
x=-∞True
x=+∞True
x=NaNFalse

不等性(x!=yy=1.0y=-0.0y=+0.0y=-∞y=+∞y=NaN
x=1.0TrueTrueTrueTrueTrue
x=-0.0TrueTrueTrueTrue
x=+0.0TrueTrueTrueTrue
x=-∞TrueTrueTrueTrueTrue
x=+∞TrueTrueTrueTrueTrue
x=NaNTrueTrueTrueTrueTrueTrue

注释:

  • 令人惊讶的相等性是针对 NaN 的:两个 NaN 值是**不**相等的!
  • +0.0-0.0 是相等的——谢天谢地

奖励

如果你想知道一个值是否为 NaN,该怎么办?
相等运算符总是返回 False,对于 a == double.NaN...

答案:使用特定函数 double.IsNaN(a)。同样,通过特定函数 double.IsPositiveInfinity(a)double.IsNegativeInfinity(a) 分别检查无穷大也是明智的。

double 相等性的怪异之处

浮点算术的本质是它们会进行隐式舍入。并非所有值都能以无限精度存储,只能存储这些值的近似值。因此,这种近似值可能会导致微小的“舍入误差”。

对带有舍入误差的值进行相加可能会导致更大的误差。

double a = 1.0/6.0; // has a small rounding error
Console.WriteLine("equal = {0}", a+a+a+a+a+a == 1.0); // False --> *not* equal
Console.WriteLine("delta = {0}", a+a+a+a+a+a - 1.0);  // delta = -1.11022302462516E-16 --> *not* 0.0

这意味着,您**不能信任** double 值上的相等运算符 ==!=。结论是,我们需要一个允许“微小”差值的相等性比较。

注意:正如引言中所展示的,舍入对此没有帮助。需要一种可靠的比较。

使 double 相等性容错

为了再次获得对相等运算符的信任,我们必须提供一个允许容错比较的相等函数。为此,我们必须确保相等/不等性表格得到满足。

一个可能的解决方案

public static class RealExtensions
{
    public struct Epsilon
    {
        public Epsilon(double value) { _value = value; }
        private double _value;
        internal bool IsEqual   (double a, double b) { return (a == b) ||  (Math.Abs(a - b) < _value); }
        internal bool IsNotEqual(double a, double b) { return (a != b) && !(Math.Abs(a - b) < _value); }
    }
    public static bool EQ(this double a, double b, Epsilon e) { return e.IsEqual   (a, b); }
    public static bool NE(this double a, double b, Epsilon e) { return e.IsNotEqual(a, b); }
}
用法
var epsilon = new RealExtension.Epsilon(1E-3);
...
double x = 1.0 / 6.0;
double y = x+x+x+x+x+x;
if (y.EQ(1.0, epsilon)) ...

注释:

  • 这些相等/不等性函数也适用于特殊值。
  • 仔细选择 Epsilon 值:例如,角度 epsilon 通常与长度 epsilon 非常不同。
  • Epsilon 值预计在 1E-1 到 1E-15 的范围内(有意义的限制是 1E-15,因为 double 的精度约为 15 位十进制数字)。
  • double.Epsilon 对于 epsilon 没有用——它只是可能存在的最小的。

就这样!嗯,还没完……

关系运算符呢:<<=>=>。它们也涉及相等性和不等性运算。

double 关系运算符

首先,让我们再次列出操作表

小于(x<yy=1.0y=-0.0y=+0.0y=-∞y=+∞y=NaN
x=1.0True
x=-0.0TrueTrue
x=+0.0TrueTrue
x=-∞TrueTrueTrueTrue
x=+∞
x=NaNFalse

小于等于(x<=yy=1.0y=-0.0y=+0.0y=-∞y=+∞y=NaN
x=1.0TrueTrue
x=-0.0TrueTrueTrueTrue
x=+0.0TrueTrueTrueTrue
x=-∞TrueTrueTrueTrueTrue
x=+∞True
x=NaNFalse

大于等于(x>=yy=1.0y=-0.0y=+0.0y=-∞y=+∞y=NaN
x=1.0TrueTrueTrueTrue
x=-0.0TrueTrueTrue
x=+0.0TrueTrueTrue
x=-∞True
x=+∞TrueTrueTrueTrueTrue
x=NaNFalse

大于(x>yy=1.0y=-0.0y=+0.0y=-∞y=+∞y=NaN
x=1.0TrueTrueTrue
x=-0.0True
x=+0.0True
x=-∞
x=+∞TrueTrueTrueTrue
x=NaNFalse

注释:

  • double.NaN 总是导致 False(即 a < b 不是 a >= b 的反面)。
  • +0.0-0.0 再次相等。

相等性/不等性函数所存在的问题与关系运算类似。让我们在下一节中对此进行探讨。

double 关系的怪异之处

由于不能信任原生的相等性/不等性运算符,关系运算符也不能信任。

double a = 1.0 / 6.0;
double b = a + a + a + a + a + a;
...
if (1.0 < b) ... // delta = +1.11022302462516E-16 --> False (delta is close to zero but not exactly)
...
if (b < 1.0) ... // delta = -1.11022302462516E-16 --> True (delta is close to zero but not exactly)
同样适用于所有关系运算符。

这需要像上面用于相等性/不等性函数的特定基于 epsilon 的关系函数。

使 double 关系容错

让我们先从一个可能的解决方案开始,它基于上面的 EQ/NE。

public static class RealExtensions
{
    public struct Epsilon
    {
        public Epsilon(double value) { _value = value; }
        private double _value;
        internal bool IsEqual   (double a, double b) { return (a == b) ||  (Math.Abs(a - b) < _value); }
        internal bool IsNotEqual(double a, double b) { return (a != b) && !(Math.Abs(a - b) < _value); }
    }
    public static bool EQ(this double a, double b, Epsilon e) { return e.IsEqual   (a, b); }
    public static bool LE(this double a, double b, Epsilon e) { return e.IsEqual   (a, b) || (a < b); }
    public static bool GE(this double a, double b, Epsilon e) { return e.IsEqual   (a, b) || (a > b); }

    public static bool NE(this double a, double b, Epsilon e) { return e.IsNotEqual(a, b); }
    public static bool LT(this double a, double b, Epsilon e) { return e.IsNotEqual(a, b) && (a < b); }
    public static bool GT(this double a, double b, Epsilon e) { return e.IsNotEqual(a, b) && (a > b); }
}
用法
var epsilon = new RealExtension.Epsilon(1E-3);
...
double x = 1.0 / 6.0;
double y = x+x+x+x+x+x;
if (y.LT(1.0, epsilon)) ...

注释:

  • 这些关系函数也适用于特殊值。

*************** 现在,这就可以了!终于。 *******************

摘要

对浮点值的原生相等性和关系运算符不可信。一种可能的解决方案是基于某个 epsilon 值发明六种运算。必须特别注意 InfinityNaN 等特殊值。

还有什么?

float 值类型也面临同样的问题。也就是说,该类型需要与 double 相同的解决方案,但要适应 float

decimal 的情况略有不同:它不被视为 C# 浮点类型,但存在类似的隐式舍入误差问题。也就是说,相同的解决方案(适用于 decimal)也能在这里解决问题。

一些有用的链接

修订

版本日期描述
V1.02012-05-13初始版本
V1.12012-05-14修复格式
V1.22012-05-14添加了 double.IsNaN(x)
double.IsPositiveInfinity(x),
double.IsNegativeInfinity(x)

© . All rights reserved.