我不确定这种说明堆栈溢出问题的非标准方法是好是坏,但是这里有:

为什么最好的(数学或其他技术上的)解释代码是:

static void Main()
{
  decimal[] arr =
  {
    42m,
    42.0m,
    42.00m,
    42.000m,
    42.0000m,
    42.00000m,
    42.000000m,
    42.0000000m,
    42.00000000m,
    42.000000000m,
    42.0000000000m,
    42.00000000000m,
    42.000000000000m,
    42.0000000000000m,
    42.00000000000000m,
    42.000000000000000m,
    42.0000000000000000m,
    42.00000000000000000m,
    42.000000000000000000m,
    42.0000000000000000000m,
    42.00000000000000000000m,
    42.000000000000000000000m,
    42.0000000000000000000000m,
    42.00000000000000000000000m,
    42.000000000000000000000000m,
    42.0000000000000000000000000m,
    42.00000000000000000000000000m,
    42.000000000000000000000000000m,
  };

  foreach (var m in arr)
  {
    Console.WriteLine(string.Format(CultureInfo.InvariantCulture,
      "{0,-32}{1,-20:R}{2:X8}", m, (double)m, m.GetHashCode()
      ));
  }

  Console.WriteLine("Funny consequences:");
  var h1 = new HashSet<decimal>(arr);
  Console.WriteLine(h1.Count);
  var h2 = new HashSet<double>(arr.Select(m => (double)m));
  Console.WriteLine(h2.Count);
}

给出以下“有趣”(显然不正确)输出:

42 42 40450000
42.0 42 40450000
42.00 42 40450000
42.000 42 40450000
42.0000 42 40450000
42.00000 42 40450000
42.000000 42 40450000
42.0000000 42 40450000
42.00000000 42 40450000
42.000000000 42 40450000
42.0000000000 42 40450000
42.00000000000 42 40450000
42.000000000000 42 40450000
42.0000000000000 42 40450000
42.00000000000000 42 40450000
42.000000000000000 42 40450000
42.0000000000000000 42 40450000
42.00000000000000000 42 40450000
42.000000000000000000 42 40450000
42.0000000000000000000 42 40450000
42.00000000000000000000 42 40450000
42.000000000000000000000 41.999999999999993 BFBB000F
42.0000000000000000000000 42 40450000
42.00000000000000000000000 42.000000000000007 40450000
42.000000000000000000000000 42 40450000
42.0000000000000000000000000 42 40450000
42.00000000000000000000000000 42 40450000
42.000000000000000000000000000 42 40450000
有趣的后果:
2
3

在.NET 4.5.2。下进行了尝试。

最佳答案

Decimal.cs 中,我们可以看到GetHashCode()是作为 native 代码实现的。此外,我们可以看到对double的转换是作为对ToDouble()的调用实现的,而对GetHashCode()的调用又是作为 native 代码实现的。因此,从那里,我们看不到有关行为的合理解释。

在旧的Shared Source CLI中,我们可以找到这些方法的旧实现,如果它们的变化不大,希望可以对此有所启发。我们可以在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


FCIMPL1(double, COMDecimal::ToDouble, DECIMAL d)
{
    WRAPPER_CONTRACT;
    STATIC_CONTRACT_SO_TOLERANT;

    ENSURE_OLEAUT32_LOADED();

    double result;
    VarR8FromDec(&d, &result);
    return result;
}
FCIMPLEND

我们可以看到double实现是基于对double的转换:哈希码基于转换为decimal后得到的字节。它基于这样的假设:相等的double值转换为相等的VarR8FromDec值。

因此,让我们在.NET外部测试 FortyTwo 系统调用:

在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不支持任何浮点十进制类型的语言,因此我正在调用系统函数来执行计算。我首先将42设置为1。然后,我添加1并减去0.1。然后,我添加0.1并减去double。等等。这导致在.NET中以相同的方式扩展小数的精度。

这是(部分)输出:
...
20: ok
21: ok
22: not ok
23: ok
24: not ok
25: ok
26: ok
...

Thus showing that this is indeed a long-standing problem in Windows that merely happens to be exposed by .NET. It's system functions that are giving different results for equal decimal values, and either they should be fixed, or .NET should be changed to not use defective functions.

Now, in the new .NET Core, we can see in its decimal.cpp code to work around the problem:

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

基于错误的ojit_code值中的一个确实提供了相同的哈希码这一事实,这似乎也可以在当前的.NET Framework中实现。但这还不足以完全解决该问题。

10-06 05:01