JavaScript 到 C# 数值精度损失

TGH*_*TGH 16 javascript c# binary-serialization msgpack signalr

当使用 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 的这个特性有关。

有趣的部分是,我没有看到同样的问题反过来。将 0.005 从 C# 发送到 JavaScript 会导致 JavaScript 中的值 0.005。

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

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

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

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

使用的技术:

  • JavaScript
  • 带有 SignalR 和 msgpack5 的 .Net Core

我的代码基于这篇文章。唯一的区别是我正在使用ContractlessStandardResolver.Instance.

Pet*_*ete 13

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

var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...
Run Code Online (Sandbox Code Playgroud)


wei*_*hch 9

更新

这已在下一个版本 (5.0.0-preview4) 中修复。

原答案

我测试float和double,有趣在这种特殊情况下,只有double出现了问题,而float似乎是工作(即0.005读取服务器上)。

检查消息字节表明 0.005 被发送为Float32Double4 字节/32 位 IEEE 754 单精度浮点数类型,尽管Number是 64 位浮点数。

在控制台中运行以下代码确认上述内容:

msgpack5().encode(Number(0.005))

// Output
Uint8Array(5) [202, 59, 163, 215, 10]
Run Code Online (Sandbox Code Playgroud)

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

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

// Output
Uint8Array(9) [203, 63, 116, 122, 225, 71, 174, 20, 123]
Run Code Online (Sandbox Code Playgroud)

但是,signalr-protocol-msgpackforceFloat64不使用该选项。

虽然这解释了为什么float在服务器端有效,但到目前为止还没有真正的解决方案。让我们等待微软怎么说。

可能的解决方法

  • 破解 msgpack5 选项?Fork 并编译您自己的 msgpack5,forceFloat64默认为 true?我不知道。
  • 切换到float服务器端
  • string两边使用
  • 切换到decimal服务器端并编写自定义IFormatterProvider. decimal不是原始类型,并IFormatterProvider<decimal>为复杂类型属性调用
  • 提供检索double属性值的方法并执行double-> float-> decimal->double技巧
  • 您可以想到的其他不切实际的解决方案

TL; 博士

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

// value = 0.00499999988824129, crazy C# :)
var value = (double)0.005f;
Run Code Online (Sandbox Code Playgroud)

对于doublein 方法的直接使用,该问题可以通过自定义来解决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;
    }
}
Run Code Online (Sandbox Code Playgroud)

并使用解析器:

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

解析器并不完美,因为转换到decimalthen 会double减慢进程的速度并且可能很危险。

然而

按照在评论中指出的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\1.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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

当需要将单个float数字转换为double:

// From MessagePackBinary class
MessagePackBinary.doubleDecoders[202] = Float32Double.Instance;
Run Code Online (Sandbox Code Playgroud)

v2

此问题存在于 MessagePack-CSharp v2 版本中。我已经在 github 上提交了一个问题,但该问题不会被修复。