JavaScript转换C#数值精度损失


16

当使用带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
  • 带有SignalR和msgpack5的.Net Core

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


C#中的浮点表示也不完全适用于每个值。看一下序列化的数据。您如何在C#中解析它?
JeffRSon

您在C#中使用哪种类型?Double被认为有这样的问题。
Poul Bak

我使用signalr及其消息包集成附带的内置消息包序列化/反序列化。
TGH

浮点值从来都不是精确的。如果需要精确值,请使用字符串(格式问题)或整数(例如,乘以1000)。
atmin

您可以检查反序列化的消息吗?在c#转换为对象之前,您从js获得的文本。
Jonny Piazzi

Answers:


9

更新

此问题已在下一版本(5.0.0-preview4)中修复

原始答案

我测试floatdouble,有趣在这种特殊情况下,只有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]

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

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

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

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

尽管这解释了为什么float可以在服务器端工作,但是到目前为止,还没有真正的解决方案。让我们等一下微软怎么

可能的解决方法

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

TL; DR

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

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

对于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;
    }
}

并使用解析器:

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

解析器不健全,如铸造decimal然后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;
    }
  }
}

当需要将一个float数字转换为以下代码时,可以使用上述解码器double

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

v2

在MessagePack-CSharp的v2版本中存在此问题。我已经在github上提出了一个问题尽管这个问题不会得到解决


有趣的发现。这里的一个挑战是该问题适用于复杂对象上的任意数量的double属性,因此我认为直接定位double会很棘手。
TGH

@TGH是的,你是对的。我相信这是MessagePack-CSharp中的错误。详情请参阅我的更新。目前,您可能需要将其float用作解决方法。我不知道他们是否在v2中修复了该问题。有空的时候我会看看。但是,问题是v2尚未与SignalR兼容。只有SignalR的预览版本(5.0.0.0- *)可以使用v2。
weichch

这在v2中也不起作用。我在MessagePack-CSharp中提出了一个错误。
weichch

@TGH不幸的是,根据github问题的讨论,服务器端没有任何修复。最好的解决办法是让客户端发送64位而不是32位。我注意到有强制执行此操作的选项,但Microsoft并没有公开(据我了解)。如果您想看看,请使用一些讨厌的解决方法来更新答案。在这个问题上祝你好运。
weichch

听起来很有趣。我会看一下。感谢您的帮助!
TGH

14

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

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

是的,您对此是正确的。
TGH
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.