JavaScript 到 C# 数值精度损失

JavaScript to C# Numeric Precision Loss

当使用 SignalR 和 MessagePack 对 JavaScript 和 C# 之间的值进行序列化和反序列化时,我发现 C# 在接收端有一点精度损失。

例如,我将值 0.005 从 JavaScript 发送到 C#。当反序列化值出现在 C# 端时,我得到值 0.004999999888241291,它很接近,但不完全是 0.005。 JavaScript 端的值为 Number 而在 C# 端我使用的是 double.

我读到 JavaScript 不能准确表示浮点数,这会导致像 0.1 + 0.2 == 0.30000000000000004 这样的结果。我怀疑我看到的问题与 JavaScript.

的这个功能有关

有趣的是,我没有看到相同的问题以相反的方式出现。从 C# 向 JavaScript 发送 0.005 会在 JavaScript 中产生值 0.005。

编辑:来自 C# 的值在 JS 调试器中被缩短了 window。正如@Pete 所提到的,它确实扩展到不完全是 0.5 的值 (0.005000000000000000104083408558)。这意味着至少双方都存在差异。

JSON 序列化没有相同的问题,因为我假设它通过字符串进行,这使得接收环境处于控制状态,将值解析为其本机数字类型。

我想知道是否有一种方法可以使用二进制序列化来使两边的值都匹配。

如果不是,这是否意味着无法在 JavaScript 和 C# 之间进行 100% 准确的二进制转换?

使用的技术:

我的代码基于this post。 唯一的区别是我使用的是 ContractlessStandardResolver.Instance.

请检查您发送的精确值,精度更高。语言通常会限制印刷精度以使其看起来更好。

var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...

更新

这已经 fixed in next release (5.0.0-preview4)

原答案

我测试了 floatdouble,有趣的是在这个特殊情况下,只有 double 有问题,而 float 似乎有效(即 0.005 是在服务器上读取)。

检查消息字节表明 0.005 作为类型 Float32Double 发送,这是一个 4 字节/32 位 IEEE 754 单精度浮点数,尽管 Number 是 64 位浮点数.

运行 控制台中的以下代码证实了以上内容:

msgpack5().encode(Number(0.005))

// Output
Uint8Array(5) [202, 59, 163, 215, 10]

mspack5 确实提供了强制 64 位浮点数的选项:

msgpack5({forceFloat64:true}).encode(Number(0.005))

// Output
Uint8Array(9) [203, 63, 116, 122, 225, 71, 174, 20, 123]

但是,forceFloat64 选项未被 signalr-protocol-msgpack 使用。

虽然这解释了为什么 float 在服务器端工作,but there isn't really a fix for that as of now. Let's wait what Microsoft says

可能的解决方法

  • 破解 msgpack5 选项?使用 forceFloat64 默认为 true 分叉并编译您自己的 msgpack5?我不知道。
  • 在服务器端切换到 float
  • 两边使用string
  • 在服务器端切换到 decimal 并编写自定义 IFormatterProviderdecimal不是原始类型,IFormatterProvider<decimal> is called for complex type properties
  • 提供方法来检索 double 属性 值并执行 double -> float -> decimal -> double 技巧
  • 您能想到的其他不切实际的解决方案

TL;DR

JS 客户端向 C# 后端发送单个浮点数的问题导致了一个已知的浮点问题:

// value = 0.00499999988824129, crazy C# :)
var value = (double)0.005f;

对于在方法中直接使用 double,问题可以通过自定义 MessagePack.IFormatterResolver:

来解决
public class MyDoubleFormatterResolver : IFormatterResolver
{
    public static MyDoubleFormatterResolver Instance = new MyDoubleFormatterResolver();

    private MyDoubleFormatterResolver()
    { }

    public IMessagePackFormatter<T> GetFormatter<T>()
    {
        return MyDoubleFormatter.Instance as IMessagePackFormatter<T>;
    }
}

public sealed class MyDoubleFormatter : IMessagePackFormatter<double>, IMessagePackFormatter
{
    public static readonly MyDoubleFormatter Instance = new MyDoubleFormatter();

    private MyDoubleFormatter()
    {
    }

    public int Serialize(
        ref byte[] bytes,
        int offset,
        double value,
        IFormatterResolver formatterResolver)
    {
        return MessagePackBinary.WriteDouble(ref bytes, offset, value);
    }

    public double Deserialize(
        byte[] bytes,
        int offset,
        IFormatterResolver formatterResolver,
        out int readSize)
    {
        double value;
        if (bytes[offset] == 0xca)
        {
            // 4 bytes single
            // cast to decimal then double will fix precision issue
            value = (double)(decimal)MessagePackBinary.ReadSingle(bytes, offset, out readSize);
            return value;
        }

        value = MessagePackBinary.ReadDouble(bytes, offset, out readSize);
        return value;
    }
}

并使用解析器:

services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
        {
            MyDoubleFormatterResolver.Instance,
            ContractlessStandardResolver.Instance,
        };
    });

解析器并不完美,因为转换为 decimal 然后转换为 double 会减慢处理速度并且 it could be dangerous.

不过

根据 OP 在评论中指出的,如果使用具有 double 返回属性的复杂类型,这 不能 解决问题。

进一步调查发现问题原因在MessagePack-CSharp:

// Type: MessagePack.MessagePackBinary
// Assembly: MessagePack, Version=1.9.0.0, Culture=neutral, PublicKeyToken=b4a0369545f0a1be
// MVID: B72E7BA0-FA95-4EB9-9083-858959938BCE
// Assembly location: ...\.nuget\packages\messagepack.9.11\lib\netstandard2.0\MessagePack.dll

namespace MessagePack.Decoders
{
  internal sealed class Float32Double : IDoubleDecoder
  {
    internal static readonly IDoubleDecoder Instance = (IDoubleDecoder) new Float32Double();

    private Float32Double()
    {
    }

    public double Read(byte[] bytes, int offset, out int readSize)
    {
      readSize = 5;
      // The problem is here
      // Cast a float value to double like this causes precision loss
      return (double) new Float32Bits(bytes, checked (offset + 1)).Value;
    }
  }
}

上面的解码器在需要将单个float数转换为double时使用:

// From MessagePackBinary class
MessagePackBinary.doubleDecoders[202] = Float32Double.Instance;

v2

此问题存在于 MessagePack-CSharp 的 v2 版本中。我已经提交 an issue on github, though the issue is not going to be fixed.