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






4.97/5 (17投票s)
基于比较 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);
答案:False
(a
是 +∞,而 ∞ - ∞
是 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==y ) | y=1.0 | y=-0.0 | y=+0.0 | y=-∞ | y=+∞ | y=NaN |
---|---|---|---|---|---|---|
x=1.0 | True | 假 | 假 | 假 | 假 | 假 |
x=-0.0 | 假 | True | True | 假 | 假 | 假 |
x=+0.0 | 假 | True | True | 假 | 假 | 假 |
x=-∞ | 假 | 假 | 假 | True | 假 | 假 |
x=+∞ | 假 | 假 | 假 | 假 | True | 假 |
x=NaN | 假 | 假 | 假 | 假 | 假 | False |
不等性(x!=y ) | y=1.0 | y=-0.0 | y=+0.0 | y=-∞ | y=+∞ | y=NaN |
---|---|---|---|---|---|---|
x=1.0 | 假 | True | True | True | True | True |
x=-0.0 | True | 假 | 假 | True | True | True |
x=+0.0 | True | 假 | 假 | True | True | True |
x=-∞ | True | True | True | 假 | True | True |
x=+∞ | True | True | True | True | 假 | True |
x=NaN | True | True | True | True | True | True |
注释:
- 令人惊讶的相等性是针对
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<y ) | y=1.0 | y=-0.0 | y=+0.0 | y=-∞ | y=+∞ | y=NaN |
---|---|---|---|---|---|---|
x=1.0 | 假 | 假 | 假 | 假 | True | 假 |
x=-0.0 | True | 假 | 假 | 假 | True | 假 |
x=+0.0 | True | 假 | 假 | 假 | True | 假 |
x=-∞ | True | True | True | 假 | True | 假 |
x=+∞ | 假 | 假 | 假 | 假 | 假 | 假 |
x=NaN | 假 | 假 | 假 | 假 | 假 | False |
小于等于(x<=y ) | y=1.0 | y=-0.0 | y=+0.0 | y=-∞ | y=+∞ | y=NaN |
---|---|---|---|---|---|---|
x=1.0 | True | 假 | 假 | 假 | True | 假 |
x=-0.0 | True | True | True | 假 | True | 假 |
x=+0.0 | True | True | True | 假 | True | 假 |
x=-∞ | True | True | True | True | True | 假 |
x=+∞ | 假 | 假 | 假 | 假 | True | 假 |
x=NaN | 假 | 假 | 假 | 假 | 假 | False |
大于等于(x>=y ) | y=1.0 | y=-0.0 | y=+0.0 | y=-∞ | y=+∞ | y=NaN |
---|---|---|---|---|---|---|
x=1.0 | True | True | True | True | 假 | 假 |
x=-0.0 | 假 | True | True | True | 假 | 假 |
x=+0.0 | 假 | True | True | True | 假 | 假 |
x=-∞ | 假 | 假 | 假 | True | 假 | 假 |
x=+∞ | True | True | True | True | True | 假 |
x=NaN | 假 | 假 | 假 | 假 | 假 | False |
大于(x>y ) | y=1.0 | y=-0.0 | y=+0.0 | y=-∞ | y=+∞ | y=NaN |
---|---|---|---|---|---|---|
x=1.0 | 假 | True | True | True | 假 | 假 |
x=-0.0 | 假 | 假 | 假 | True | 假 | 假 |
x=+0.0 | 假 | 假 | 假 | True | 假 | 假 |
x=-∞ | 假 | 假 | 假 | 假 | 假 | 假 |
x=+∞ | True | True | True | True | 假 | 假 |
x=NaN | 假 | 假 | 假 | 假 | 假 | False |
注释:
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 值发明六种运算。必须特别注意 Infinity
和 NaN
等特殊值。
还有什么?
float
值类型也面临同样的问题。也就是说,该类型需要与 double
相同的解决方案,但要适应 float
。
decimal
的情况略有不同:它不被视为 C# 浮点类型,但存在类似的隐式舍入误差问题。也就是说,相同的解决方案(适用于 decimal
)也能在这里解决问题。
一些有用的链接
- 维基百科:IEEE-754[^]
- Steve Hollasch 的 IEEE 754 概述[^]
- C# double 类型(C# 参考)[^]
- 浮点类型表(C# 参考)[^]
- decimal(C# 参考)[^]
- Double.Equals 方法 (Double)[^]
- Double.CompareTo 方法 (Object)[^]
修订
版本 | 日期 | 描述 |
---|---|---|
V1.0 | 2012-05-13 | 初始版本 |
V1.1 | 2012-05-14 | 修复格式 |
V1.2 | 2012-05-14 | 添加了 double.IsNaN(x) 、double.IsPositiveInfinity(x) ,和 double.IsNegativeInfinity(x) |