JavaScript 到 C# 的数值精度丢失问题

17
使用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#发送0.005到JavaScript会得到0.005的值。
编辑:来自C#的值仅在JS调试器窗口中缩短。正如@Pete所提到的,它会扩展到不是0.5的东西(0.005000000000000000104083408558)。这意味着至少两边都存在差异。
由于我假设JSON序列化通过字符串进行操作,这不会出现同样的问题,因为它使接收环境可以控制将值解析为其本机数字类型。
我想知道是否有一种使用二进制序列化的方法来使双方匹配。
如果没有,这是否意味着无法在JavaScript和C#之间进行100%准确的二进制转换?
使用的技术:
- JavaScript - .Net Core与SignalR和msgpack5
我的代码基于this post。唯一的区别是我使用ContractlessStandardResolver.Instance

C#中的浮点数表示并非对于每个值都是精确的。看一下序列化数据。你如何在C#中解析它? - JeffRSon
你在C#中使用什么类型?Double已知存在这样的问题。 - Poul Bak
我使用SignalR自带的消息包序列化/反序列化以及它的消息包集成。 - TGH
浮点数值从来都不是精确的。如果需要精确的值,可以使用字符串(格式化问题)或整数(例如通过乘以1000)。 - atmin
你能检查反序列化的消息吗?在C#将其转换为对象之前,你从js得到的文本。 - Jonny Piazzi
2个回答

13
请检查您要发送的值,确保以更大的精度进行发送。通常在打印时,语言会限制精度以使其看起来更好。
var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...

12

更新

此问题已在下一个版本(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,但是目前还没有真正的解决方法。让我们等待Microsoft的回应

可能的解决方法

  • 修改msgpack5选项?分支并编译自己的msgpack5,并将forceFloat64默认设置为true??我不知道。
  • 在服务器端切换到float
  • 在两端都使用string
  • 在服务器端切换到decimal并编写自定义IFormatterProviderdecimal不是原始类型,对于复杂类型属性会调用IFormatterProvider<decimal>
  • 提供检索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会减慢进程速度,并且可能存在危险

然而

正如评论中的OP指出的那样,如果使用具有返回double属性的复杂类型,则decimal转换为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;
    }
  }
}

当需要将单个float数转换为double时,使用上述解码器:

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

v2

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


@TGH 是的,你说得对。我相信这是MessagePack-CSharp中的一个bug。请查看我的更新以获取详细信息。目前,您可能需要使用“float”作为解决方法。我不知道他们是否在v2中修复了这个问题。一旦我有时间,我会去看看。但是,问题是v2还不兼容SignalR。只有预览版本(5.0.0.0-*)的SignalR可以使用v2。 - weichch
这在v2中也不起作用。我已经向MessagePack-CSharp报告了一个错误。 - weichch
@TGH 很遗憾,根据在 Github 问题中的讨论,服务器端没有任何修复方法。最好的解决方法是让客户端发送64位而不是32位。我注意到有一个选项可以强制执行这个操作,但据我所知,微软没有公开这个选项。如果您想看一下,我刚刚更新了答案,并提供了一些不好的解决方法。祝你好运! - weichch
那听起来像是一个有趣的线索。我会去看看。谢谢你的帮助! - TGH
我认为你已经找到了这个问题唯一可行的解决方案。我在signalR存储库中进行了快速的POC。我认为以下类似的东西可能会起作用:https://github.com/thelgevold/aspnetcore/commit/b8a105ed0763e87334c5f41a5dcc0f620fc112c1 - TGH
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接