In Decimal.cs http://referencesource.microsoft.com/#mscorlib/system/decimal.cs,我们可以看到GetHashCode()
作为本机代码实现。此外,我们还可以看到演员阵容double
是作为调用来实现的ToDouble()
,这又作为本机代码实现。因此,从那里我们看不到这种行为的逻辑解释。
在旧的共享源 CLI http://www.microsoft.com/en-us/download/details.aspx?id=4917,我们可以找到这些方法的旧实现,如果它们没有改变太多的话,希望能提供一些启示。我们可以在comdecimal.cpp中找到:
FCIMPL1(INT32, COMDecimal::GetHashCode, DECIMAL *d)
{
WRAPPER_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;
ENSURE_OLEAUT32_LOADED();
_ASSERTE(d != NULL);
double dbl;
VarR8FromDec(d, &dbl);
if (dbl == 0.0) {
// Ensure 0 and -0 have the same hash code
return 0;
}
return ((int *)&dbl)[0] ^ ((int *)&dbl)[1];
}
FCIMPLEND
and
FCIMPL1(double, COMDecimal::ToDouble, DECIMAL d)
{
WRAPPER_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;
ENSURE_OLEAUT32_LOADED();
double result;
VarR8FromDec(&d, &result);
return result;
}
FCIMPLEND
我们可以看到,GetHashCode()
实现是基于转换为double
:哈希码基于转换为后生成的字节double
。它基于以下假设:decimal
值转换为相等double
values.
那么让我们测试一下VarR8FromDec https://msdn.microsoft.com/en-us/library/windows/desktop/ms221523%28v=vs.85%29.aspx.NET 之外的系统调用:
在 Delphi 中(我实际上使用的是 FreePascal),这里有一个简短的程序,可以直接调用系统函数来测试它们的行为:
{$MODE Delphi}
program Test;
uses
Windows,
SysUtils,
Variants;
type
Decimal = TVarData;
function VarDecFromStr(const strIn: WideString; lcid: LCID; dwFlags: ULONG): Decimal; safecall; external 'oleaut32.dll';
function VarDecAdd(const decLeft, decRight: Decimal): Decimal; safecall; external 'oleaut32.dll';
function VarDecSub(const decLeft, decRight: Decimal): Decimal; safecall; external 'oleaut32.dll';
function VarDecDiv(const decLeft, decRight: Decimal): Decimal; safecall; external 'oleaut32.dll';
function VarBstrFromDec(const decIn: Decimal; lcid: LCID; dwFlags: ULONG): WideString; safecall; external 'oleaut32.dll';
function VarR8FromDec(const decIn: Decimal): Double; safecall; external 'oleaut32.dll';
var
Zero, One, Ten, FortyTwo, Fraction: Decimal;
I: Integer;
begin
try
Zero := VarDecFromStr('0', 0, 0);
One := VarDecFromStr('1', 0, 0);
Ten := VarDecFromStr('10', 0, 0);
FortyTwo := VarDecFromStr('42', 0, 0);
Fraction := One;
for I := 1 to 40 do
begin
FortyTwo := VarDecSub(VarDecAdd(FortyTwo, Fraction), Fraction);
Fraction := VarDecDiv(Fraction, Ten);
Write(I: 2, ': ');
if VarR8FromDec(FortyTwo) = 42 then WriteLn('ok') else WriteLn('not ok');
end;
except on E: Exception do
WriteLn(E.Message);
end;
end.
请注意,由于 Delphi 和 FreePascal 没有对任何浮点十进制类型的语言支持,因此我调用系统函数来执行计算。我正在设置FortyTwo
首先42
。然后我添加1
并减去1
。然后我添加0.1
并减去0.1
。等等。这会导致小数点的精度在 .NET 中以相同的方式扩展。
这是输出(部分):
...
20: ok
21: ok
22: not ok
23: ok
24: not ok
25: ok
26: ok
...
这表明这确实是 Windows 中长期存在的问题,只是碰巧被 .NET 暴露出来。系统函数对于相同的十进制值给出不同的结果,要么应该修复它们,要么应该更改 .NET 以不使用有缺陷的函数。
现在,在新的 .NET Core 中,我们可以看到小数.cpp https://github.com/dotnet/coreclr/blob/master/src/classlibnative/bcltype/decimal.cpp解决该问题的代码:
FCIMPL1(INT32, COMDecimal::GetHashCode, DECIMAL *d)
{
FCALL_CONTRACT;
ENSURE_OLEAUT32_LOADED();
_ASSERTE(d != NULL);
double dbl;
VarR8FromDec(d, &dbl);
if (dbl == 0.0) {
// Ensure 0 and -0 have the same hash code
return 0;
}
// conversion to double is lossy and produces rounding errors so we mask off the lowest 4 bits
//
// For example these two numerically equal decimals with different internal representations produce
// slightly different results when converted to double:
//
// decimal a = new decimal(new int[] { 0x76969696, 0x2fdd49fa, 0x409783ff, 0x00160000 });
// => (decimal)1999021.176470588235294117647000000000 => (double)1999021.176470588
// decimal b = new decimal(new int[] { 0x3f0f0f0f, 0x1e62edcc, 0x06758d33, 0x00150000 });
// => (decimal)1999021.176470588235294117647000000000 => (double)1999021.1764705882
//
return ((((int *)&dbl)[0]) & 0xFFFFFFF0) ^ ((int *)&dbl)[1];
}
FCIMPLEND
这似乎也在当前的 .NET Framework 中实现,基于以下事实:错误之一double
值确实给出了相同的哈希码,但这还不足以完全解决问题。