有人能解释一下c#中有符号浮点的这种奇怪行为吗?

下面是带有注释的例子:

class Program
{
// first version of structure
public struct D1
{
public double d;
public int f;
}


// during some changes in code then we got D2 from D1
// Field f type became double while it was int before
public struct D2
{
public double d;
public double f;
}


static void Main(string[] args)
{
// Scenario with the first version
D1 a = new D1();
D1 b = new D1();
a.f = b.f = 1;
a.d = 0.0;
b.d = -0.0;
bool r1 = a.Equals(b); // gives true, all is ok


// The same scenario with the new one
D2 c = new D2();
D2 d = new D2();
c.f = d.f = 1;
c.d = 0.0;
d.d = -0.0;
bool r2 = c.Equals(d); // false! this is not the expected result
}
}

你觉得这个怎么样?

15961 次浏览

给我真实的,与Mono的gmcs 2.4.2.3。

简单的测试用例:

Console.WriteLine("Good: " + new Good().Equals(new Good { d = -.0 }));
Console.WriteLine("Bad: " + new Bad().Equals(new Bad { d = -.0 }));


public struct Good {
public double d;
public int f;
}


public struct Bad {
public double d;
}

编辑:该错误也发生在浮点数上,但仅当结构体中的字段加起来是8字节的倍数时才会发生。

它必须是零相关的,因为改变了直线

D.d = -0.0

:

D.d = 0.0

结果比较是正确的…

它必须与逐位比较相关,因为0.0应该只与-0.0的信号位不同。

一半答案:

反射器告诉我们ValueType.Equals()做这样的事情:

if (CanCompareBits(this))
return FastEqualsCheck(this, obj);
else
// Use reflection to step through each member and call .Equals() on each one.

不幸的是,CanCompareBits()FastEquals()(都是静态方法)都是extern ([MethodImpl(MethodImplOptions.InternalCall)]),并且没有可用的源代码。

回到猜测为什么一种情况可以通过比特进行比较,而另一种情况不能(可能是对齐问题?)

如果D2像这样

public struct D2
{
public double d;
public double f;
public string s;
}

这是真的。

如果你这样做

public struct D2
{
public double d;
public double f;
public double u;
}

它仍然是假的。

如果结构体只包含双精度变量,t似乎是假的。

错误出现在System.ValueType的下面两行代码中:(我进入了引用源代码)

if (CanCompareBits(this))
return FastEqualsCheck(thisObj, obj);

(两个方法都是[MethodImpl(MethodImplOptions.InternalCall)])

当所有字段都是8字节宽时,CanCompareBits错误地返回true,导致两个不同但语义相同的值按位比较。

当至少有一个字段不是8字节宽时,CanCompareBits返回false,代码继续使用反射遍历字段并为每个值调用Equals,这将正确地将-0.0视为等于0.0

下面是来自SSCLI的CanCompareBits的源代码:

FCIMPL1(FC_BOOL_RET, ValueTypeHelper::CanCompareBits, Object* obj)
{
WRAPPER_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;


_ASSERTE(obj != NULL);
MethodTable* mt = obj->GetMethodTable();
FC_RETURN_BOOL(!mt->ContainsPointers() && !mt->IsNotTightlyPacked());
}
FCIMPLEND

我在http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx找到了答案。

核心部分是CanCompareBits上的源注释,ValueType.Equals使用它来确定是否使用__abc2风格的比较:

CanCompareBits的注释说 如果valuetype不是,则返回true 包含指针,是紧密的 包装”。和fastqualscheck使用

. "memcmp"加速比较

作者继续阐述了OP所描述的问题:

假设你有一个结构 只包含一个浮点数。会发生什么 如果一个包含+0.0,另一个包含+0.0 包含-0.0 ?他们应该是 相同,但基本的二进制 表现方式不同。如果你 嵌套其他覆盖的结构 Equals方法,即优化

维尔克斯的猜想是正确的。“CanCompareBits”所做的是检查所讨论的值类型是否在内存中“紧密地打包”。一个紧凑的结构是通过简单地比较组成结构的二进制位来比较的;松散包装的结构通过对所有成员调用Equals来进行比较。

这解释了SLaks的观察,即它用全是双精度的结构体进行还原;这样的结构总是紧凑的。

不幸的是,正如我们在这里看到的,这引入了语义上的差异,因为双精度对象的位比较和双精度对象的等号比较给出了不同的结果。

你觉得这个怎么样?

总是重写值类型上的Equals和GetHashCode。它将是快速和正确的。

只是对这个10年前的bug的更新:它在。net Core中已被修复 (免责声明:我是这篇PR的作者),这可能会在。net Core 2.1.0中发布。

博客解释了错误以及我如何修复它。