JavaScript 到 C# 数值精度损失



当使用带有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 的这个功能有关。

有趣的是,我没有看到同样的问题走向另一个方向。将 0.005 从 C# 发送到 JavaScript 会导致 JavaScript 中的值为 0.005。

编辑:C#中的值只是在JS调试器窗口中缩短。正如@Pete提到的,它确实扩展到不完全是 0.5 的东西(0.00500000000000000000104083408558)。这意味着差异至少发生在双方

。JSON 序列化没有同样的问题,因为我假设它通过字符串进行,这使得接收环境处于控制状态,将值解析为其本机数字类型。

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

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

使用的技术:

  • JavaScript
  • .Net Core with SignalR and msgpack5

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

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

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

更新

此问题已在下一版本(5.0.0-预览版4)中修复。

原始答案

我测试了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选项不由信令协议-msgpack使用。

虽然这解释了为什么float在服务器端工作,但到目前为止还没有真正的修复方法。让我们等待Microsoft怎么说。

可能的解决方法

  • 破解味精包5选项?分叉并编译您自己的 msgpack5,forceFloat64默认值为 true??我不知道。
  • 切换到服务器端的float
  • 在两侧使用string
  • 切换到服务器端的decimal并编写自定义IFormatterProviderdecimal不是基元类型,IFormatterProvider<decimal>是为复杂类型属性调用
  • 提供检索double属性值并执行double->float->decimal->double技巧
  • 的方法
  • 您能想到的其他不切实际的解决方案

TL;博士

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返回属性的复杂类型,则无法解决问题。

进一步的调查揭示了MessagePack-CSharp中问题的原因:

// Type: MessagePack.MessagePackBinary
// Assembly: MessagePack, Version=1.9.0.0, Culture=neutral, PublicKeyToken=b4a0369545f0a1be
// MVID: B72E7BA0-FA95-4EB9-9083-858959938BCE
// Assembly location: ....nugetpackagesmessagepack1.9.11libnetstandard2.0MessagePack.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 上提出了一个问题,尽管这个问题不会得到解决。

最新更新