在C#中,负零(-0)和零(0)是否相等?

28

在C#中,负零(-0)是否等同于零(0)?


3
签名零是一种浮点数表示法,用于区分正零和负零。它被广泛使用在计算机科学领域中的浮点数运算中。签名零有两个表示形式:正零和负零,它们的二进制表示相同但符号位不同。当除以零时可能会出现负零值,而在某些情况下对于数学计算来说需要保留这种符号。 - Dan Bechard
3个回答

36

对于整数来说,不存在二进制表示法可以区分0和-0,所以按照定义它们是相等的。

对于IEEE浮点数,有负零和正零的区别。我进行了一些测试(使用.NET Framework 2.0的CLR,C# 3),结果表明它们被认为是相等的,这实际上符合IEEE 754标准的预期行为。

以下是我的测试代码:

    double minusOne = -1.0;
    double positiveZero = 0.0;
    double negativeZero = minusOne*positiveZero;
    Console.WriteLine("{0} == {1} -> {2}", positiveZero, negativeZero, positiveZero == negativeZero);
    Console.WriteLine("Binary representation is equal: {0}", BitConverter.DoubleToInt64Bits(positiveZero) == BitConverter.DoubleToInt64Bits(negativeZero));

返回:

0 == 0 -> True
Binary representation is equal: False

3
double negativeZero = -0.0 的结果与其他相同,这意味着常量 -0.0 被正确地解析为 double 类型的 "负零" 值。 - Lucero

13

听起来你想知道它们不能互换的边缘情况,这里有一些例子。

struct上的object.Equals

> struct SomeStruct { public double X; }
> var a = new SomeStruct { X = 0d };
> var b = new SomeStruct { X = -0d };
> a.Equals(b)
false
>

反转

> 1/0d
∞
> 1/-0d
->

显式字节转换

任何类型的显式按字节或按位进行分解,当然也包括类型判断。以下示例来自PC:

> BitConverter.GetBytes(0d)
byte[8] { 0, 0, 0, 0, 0, 0, 0, 0 }
> BitConverter.GetBytes(-0d)
byte[8] { 0, 0, 0, 0, 0, 0, 0, 128 }
> 

Math.Sign

尽管您可能期望的不同,Math.Sign并不能区分负零和正零。它只告诉您一个数字是否等于、大于或小于0。

> Math.Sign(-0f)
0 

Math.MinMath.Max

一些算术运算对于-0有边界情况。其中一个有趣的例子是Math.Min(以及相应的max),当比较带符号的零时,它返回第二个零。因此:

> BitConverter.GetBytes(Math.Min(0.0, -0.0))
byte[8] { 0, 0, 0, 0, 0, 0, 0, 128 }
> BitConverter.GetBytes(Math.Min(-0.0, 0.0))
byte[8] { 0, 0, 0, 0, 0, 0, 0, 0 }
> 

关于不规则的十进制表示法的说明

十进制类型本身没有负零,但它确实有多个二进制表示形式的零:

> new decimal(new int[4] { 0, 0, 0, 0 })
0
> new decimal(new int[4] { 0, 0, 0, -2147483648 })
0
> 

第二个示例可以被认为是负零,因为它在位上与常规零完全相同,只是否定位设置了。但就格式化程序而言,它只是一个零。实际上,对于不同的小数点位移,decimal 有几十种零表示方式,所有这些表示方式在算术上等效并显示为 0:

> new decimal(new int[4] { 0, 0, 0, 131072 })
0.00
> new decimal(new int[4] { 0, 0, 0, 1835008 })
0.0000000000000000000000000000
> new decimal(new int[4] { 0, 0, 0, 65536 })
0.0

那么,你只能通过二进制比较或二进制转换来区分它们。从实验中可以看出,上面的struct技巧对它们不起作用。Math.Min返回第二个给定的零。

奖励:次标准数

floatdouble类型中的某些位模式表示次标准(也称为非规格化)值。我不会详细介绍它们是什么 - 请参见链接 - 但需要知道的重要事情是CLI规范明确声明它们的操作是实现特定的。我不知道是否有将其视为0的平台,但可能存在。另一方面,《C#编程语言》表示它们被认为是“有效的非零值”。


1
只是一个有趣的小细节:我偶然发现了关于包含浮点数的结构体(根据您的第一个代码示例)的 object.Equals / ValueType.Equals 的不同行为,当我使用在线 .NET 沙盒时。(1/2) - user2819245
1
Ideone似乎使用Mono,不遵循MS文档中指定的ValueType.Equals行为(代码示例:https://ideone.com/7iWule)。当选择.NET Core 2.2编译器时,DotNetFiddle也会显示出不同的行为(https://dotnetfiddle.net/Se1U3n)。然而,在DotNetFiddle中切换编译器到.NET 4.7.2或Roslyn 2.0将产生符合ValueType.Equals文档行为的结果。 :-) - user2819245
从@Dono在另一个Stack Overflow评论中的说法来看,似乎这个问题已经在PR 13164中得到了解决。 - Matt Tsōnto

13

对于小数,至少有4种类型的零:

Decimal zero = Decimal.Zero;
Decimal negativeZero1 = new Decimal(0, 0, 0, true, 0);
Decimal negativeZero2 = -0.0m;
Decimal negativeZero3 = Decimal.Negate(Decimal.Zero);

虽然它们都相等并打印为"0",但它们具有不同的位表示:

zero:          {0x00000000 00000000 00000000 00000000 }
negativeZero1: {0x00000000 00000000 00000000 80000000 }
negativeZero2: {0x00000000 00000000 00000000 80010000 }
negativeZero3: {0x00000000 00000000 00000000 80000000 }

来源:十进制负零表示法


9
+1 - 但是:你的二进制变量 negativeZero1negativeZero3 在我的看法中看起来非常相似? - Lucero
这并不是整个故事:实际上,在十进制中,0有256种表示方法(或多或少),其中大约一半具有符号位设置。 - Matt Tsōnto
链接源代码中的一个有趣点:“顺便说一下,C#编译器不会将“-0m”视为负零,尽管它确实可以正确获取“-0.0m”的符号和比例。”我刚遇到了这个差异。 - Gabor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接