JavaScript导致C#数值精度损失 可能的解决方法 TL; DR v2

当使用带MessagePack的SignalR在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调试器窗口中缩短了。正如@Pete所提到的,它确实扩展为不完全为0.5的值(0.005000000000000000104083408558)。这意味着差异至少发生在双方。

JSON序列化没有相同的问题,因为我假设它通过字符串传递,从而将接收环境留在控件中,从而将值解析为其原始数字类型。

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

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

使用的技术:

  • JavaScript
  • .Net Core与SignalR和msgpack5

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

poee223 回答:JavaScript导致C#数值精度损失 可能的解决方法 TL; DR v2

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

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发送,尽管Number是64位浮点,它还是4字节/ 32位IEEE 754单精度浮点数。

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

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]

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

尽管这解释了float在服务器端but there isn't really a fix for that as of now上起作用的原因。让我们等Microsoft says

可能的解决方法

  • 没有msgpack5选项?分叉并使用forceFloat64默认编译为true编译自己的msgpack5?我不知道。
  • 在服务器端切换到float
  • 在两侧都使用string
  • 在服务器端切换到decimal并编写自定义IFormatterProvider decimal不是原始类型,而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,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,out readSize);
            return value;
        }

        value = MessagePackBinary.ReadDouble(bytes,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\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,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 githubthough the issue is not going to be fixed

本文链接:https://www.f2er.com/2566134.html

大家都在问