简体   繁体   English

JavaScript 到 C# 数值精度损失

[英]JavaScript to C# Numeric Precision Loss

When serializing and deserializing values between JavaScript and C# using SignalR with MessagePack I am seeing a bit of precision loss in C# on the receiving end.当使用 SignalR 和 MessagePack 在 JavaScript 和 C# 之间序列化和反序列化值时,我在接收端看到 C# 中的一些精度损失。

As an example I am sending the value 0.005 from JavaScript to C#.例如,我将值 0.005 从 JavaScript 发送到 C#。 When the deserialized value appears on the C# side I am getting the value 0.004999999888241291 , which is close, but not 0.005 exactly.当反序列化的值出现在 C# 端时,我得到的值是0.004999999888241291 ,它很接近,但不是 0.005。 The value on the JavaScript side is Number and on the C# side I am using double . JavaScript 端的值是Number而在 C# 端我使用double

I have read that JavaScript can't represent floating point numbers exactly which can lead to results like 0.1 + 0.2 == 0.30000000000000004 .我读过 JavaScript 不能准确表示浮点数,这会导致像0.1 + 0.2 == 0.30000000000000004这样的结果。 I suspect the issue I am seeing is related to this feature of JavaScript.我怀疑我看到的问题与 JavaScript 的这个特性有关。

The interesting part is that I am not seeing the same issue going the other way.有趣的部分是,我没有看到同样的问题反过来。 Sending 0.005 from C# to JavaScript results in the value 0.005 in JavaScript.将 0.005 从 C# 发送到 JavaScript 会导致 JavaScript 中的值 0.005。

Edit : The value from C# is just shortened in the JS debugger window.编辑:来自 C# 的值只是在 JS 调试器窗口中缩短了。 As @Pete mentioned it does expand to something that is not 0.5 exactly (0.005000000000000000104083408558).正如@Pete 提到的,它确实扩展到不完全是 0.5 的值(0.005000000000000000104083408558)。 This means the discrepancy happens on both sides at least.这意味着差异至少发生在双方。

JSON serialization does not have the same issue since I am assuming it goes via string which leaves the receiving environment in control wrt parsing the value into its native numerical type. JSON 序列化没有相同的问题,因为我假设它通过字符串传递,这使得接收环境处于控制状态,并将值解析为其本机数字类型。

I am wondering if there is a way using binary serialization to have matching values on both sides.我想知道是否有一种方法可以使用二进制序列化在双方都有匹配的值。

If not, does this mean that there is no way to have 100% accurate binary conversions between JavaScript and C#?如果不是,这是否意味着无法在 JavaScript 和 C# 之间进行 100% 准确的二进制转换?

Technology used:使用的技术:

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

My code is based on this post .我的代码基于这篇文章 The only difference is that I am using ContractlessStandardResolver.Instance .唯一的区别是我使用的是ContractlessStandardResolver.Instance

Please check the precise value you are sending to a bigger precision.请检查您发送的精确值以达到更高的精度。 Languages typically limits the precision on print to make it look better.语言通常会限制打印的精度以使其看起来更好。

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

UPDATE更新

This has been fixed in next release (5.0.0-preview4) .这已在下一个版本 (5.0.0-preview4) 中修复

Original Answer原答案

I tested float and double , and interestingly in this particular case, only double had the problem, whereas float seems to be working (ie 0.005 is read on server).我测试了floatdouble ,有趣的是在这种特殊情况下,只有double有问题,而float似乎正在工作(即在服务器上读取 0.005 )。

Inspecting on the message bytes suggested that 0.005 is sent as type Float32Double which is a 4-byte / 32-bit IEEE 754 single precision floating point number despite Number is 64 bit floating point.检查消息字节表明 0.005 作为Float32Double类型发送,这是一个 4 字节/32 位 IEEE 754 单精度浮点数,尽管Number是 64 位浮点数。

Run the following code in console confirmed the above:在控制台中运行以下代码确认上述内容:

msgpack5().encode(Number(0.005))

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

mspack5 does provide an option to force 64 bit floating point: mspack5确实提供了强制 64 位浮点的选项:

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

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

However, the forceFloat64 option is not used by signalr-protocol-msgpack .但是, signalr-protocol-msgpack不使用forceFloat64选项。

Though that explains why float works on the server side, but there isn't really a fix for that as of now .虽然这解释了为什么float在服务器端工作, 但到目前为止还没有真正的解决方案 Let's wait what Microsoft says .让我们等待微软怎么

Possible workarounds可能的解决方法

  • Hack msgpack5 options?破解 msgpack5 选项? Fork and compile your own msgpack5 with forceFloat64 default to true?? fork 并编译您自己的 msgpack5, forceFloat64默认为 true? I don't know.我不知道。
  • Switch to float on server side切换到服务端float
  • Use string on both sides两边都用string
  • Switch to decimal on server side and write custom IFormatterProvider .在服务器端切换到decimal并编写自定义IFormatterProvider decimal is not primitive type, and IFormatterProvider<decimal> is called for complex type properties decimal不是原始类型,并且IFormatterProvider<decimal>被称为复杂类型属性
  • Provide method to retrieve double property value and do the double -> float -> decimal -> double trick提供检索double属性值的方法并执行double -> float -> decimal -> double技巧
  • Other unrealistic solutions you could think of您可以想到的其他不切实际的解决方案

TL;DR TL; 博士

The problem with JS client sending single floating point number to C# backend causes a known floating point issue: JS 客户端向 C# 后端发送单个浮点数的问题导致了一个已知的浮点问题:

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

For direct uses of double in methods, the issue could be solved by a custom MessagePack.IFormatterResolver :对于直接使用double in 方法,该问题可以通过自定义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;
    }
}

And use the resolver:并使用解析器:

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

The resolver is not perfect, as casting to decimal then to double slows the process down and it could be dangerous .解析器并不完美,因为转换为decimal然后double会减慢过程,并且可能很危险

However然而

As per the OP pointed out in the comments, this cannot solve the issue if using complex types having double returning properties.根据 OP 在评论中指出的那样,如果使用具有double返回属性的复杂类型,这无法解决问题。

Further investigation revealed the cause of the problem in MessagePack-CSharp:进一步的调查揭示了 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;
    }
  }
}

The above decoder is used when needing to convert a single float number to double :当需要将单个float转换为double时,使用上述解码器:

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

v2 v2

This issue exists in v2 versions of MessagePack-CSharp.此问题存在于 MessagePack-CSharp v2 版本中。 I have filed an issue on github , though the issue is not going to be fixed .我已经在 github 上提交了一个问题但该问题不会被修复

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM