【问题标题】:JavaScript to C# Numeric Precision LossJavaScript 到 C# 数值精度损失
【发布时间】:2024-10-12 19:55:02
【问题描述】:

在使用 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 序列化没有同样的问题,因为我假设它是通过字符串传递的,这使得接收环境可以控制 wrt 将值解析为其本机数字类型。

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

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

使用的技术:

  • JavaScript
  • .Net Core 与 SignalR 和 msgpack5

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

【问题讨论】:

  • C# 中的浮点表示对于每个值也不准确。看看序列化的数据。你如何在 C# 中解析它?
  • 你在 C# 中使用什么类型?已知 Double 存在此类问题。
  • 我使用了signalr自带的内置消息包序列化/反序列化和消息包集成。
  • 浮点值永远不会精确。如果您需要精确的值,请使用字符串(格式问题)或整数(例如乘以 1000)。
  • 你能检查反序列化的消息吗?在 c# 转换为对象之前从 js 得到的文本。

标签: javascript c# signalr msgpack binary-serialization


【解决方案1】:

更新

这是fixed in next release (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]

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

虽然这解释了为什么float 在服务器端工作,but there isn't really a fix for that as of now。让我们拭目以待Microsoft says

可能的解决方法

  • 破解 msgpack5 选项? Fork 并编译你自己的 msgpack5,forceFloat64 默认为 true??我不知道。
  • 在服务器端切换到float
  • 两边都使用string
  • 在服务器端切换到decimal 并编写自定义IFormatterProviderdecimal 不是原始类型,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,
        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 会减慢处理速度,而 it could be dangerous

但是

根据 cmets 中指出的 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 版本中存在此问题。我已经提交了an issue on githubthough the issue is not going to be fixed

【讨论】:

  • 有趣的发现。这里的一个挑战是该问题适用于复杂对象上任意数量的 double 属性,因此我认为直接定位 double 会很棘手。
  • @TGH 是的,你是对的。我相信这是 MessagePack-CSharp 中的一个错误。有关详细信息,请参阅我的更新。目前,您可能需要使用float 作为解决方法。我不知道他们是否在 v2 中解决了这个问题。有时间我会看看。但是,问题是 v2 与 SignalR 尚不兼容。只有 SignalR 的预览版 (5.0.0.0-*) 可以使用 v2。
  • 这在 v2 中也不起作用。我用 MessagePack-CSharp 提出了一个错误。
  • @TGH 不幸的是,根据 github 问题中的讨论,服务器端没有任何修复。最好的解决方法是让客户端发送 64 位而不是 32 位。我注意到有一个选项可以强制这种情况发生,但微软没有公开它(据我了解)。如果您想看一下,只需使用一些讨厌的解决方法更新答案。祝你在这个问题上好运。
  • 这听起来很有趣。我会看看那个。感谢您的帮助!
【解决方案2】:

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

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

【讨论】: