Cuando serializar y deserializar valores entre JavaScript y C # usando SignalR con MessagePack estoy viendo un poco de pérdida de precisión en C # en el extremo receptor.
Como ejemplo voy a enviar el valor de 0,005 JavaScript para C #. Cuando el valor deserializado aparece en el lado C # que estoy recibiendo el valor 0.004999999888241291
, que está cerca, pero no exactamente 0.005. El valor en el lado JavaScript es Number
y en el lado C # estoy usando double
.
He leído que JavaScript no puede representar números en coma flotante exactamente lo que puede conducir a resultados como 0.1 + 0.2 == 0.30000000000000004
. Sospecho que el problema que estoy viendo está relacionado con esta función de JavaScript.
La parte interesante es que no estoy viendo el mismo problema en la otra dirección. Envío de 0,005 de C # para JavaScript resulta en el valor de 0.005 en JavaScript.
Editar : El valor de C # es sólo acorta en la ventana del depurador JS. Como se mencionó @Pete que se expande a algo que no es exactamente 0,5 (,005000000000000000104083408558). Esto significa que la discrepancia ocurre en ambos lados por lo menos.
Serialización JSON no tiene el mismo problema ya que estoy asumiendo que va a través de cuerda que sale del medio receptor en WRT control de analizar el valor en su tipo numérico nativo.
Me pregunto si hay una manera de utilizar la serialización binaria tener valores coincidentes en ambos lados.
Si no es así, quiere decir esto que no hay manera de tener 100% de las conversiones binarias exactas entre JavaScript y C #?
Tecnología utilizada:
- JavaScript
- .Net Core con SignalR y msgpack5
Mi código se basa en este post . La única diferencia es que estoy usando ContractlessStandardResolver.Instance
.
I probado float
y double
, y de manera interesante en este caso particular, solamente double
tenido el problema, mientras que float
parece estar funcionando (es decir, 0.005 se lee en el servidor).
Inspección en el mensaje de bytes sugirió que 0.005 se envía como tipo Float32Double
que es un / 32 bits IEEE 754 de precisión simple número de coma flotante de 4 bytes a pesar de Number
que es de punto flotante de 64 bits.
Ejecute el código siguiente en la consola confirma lo anterior:
msgpack5().encode(Number(0.005))
// Output
Uint8Array(5) [202, 59, 163, 215, 10]
mspack5 proporciona una opción para forzar el punto flotante de 64 bits:
msgpack5({forceFloat64:true}).encode(Number(0.005))
// Output
Uint8Array(9) [203, 63, 116, 122, 225, 71, 174, 20, 123]
Sin embargo, la forceFloat64
opción no es utilizada por signalr-protocol-msgpack .
A pesar de eso explica por qué float
funciona en el lado del servidor, pero no es realmente una solución para que a partir de ahora . Vamos a esperar lo que dice Microsoft .
posibles soluciones
- Hack msgpack5 opciones? Tenedor y compilar sus propios msgpack5 con
forceFloat64
defecto a cierto ?? No lo sé. - Cambiar a
float
el lado del servidor - Utilizar
string
en ambos lados - Cambiar a
decimal
en el lado del servidor y de escritura personalizadoIFormatterProvider
.decimal
es de tipo no primitivo, yIFormatterProvider<decimal>
se llama para las propiedades de tipo complejo - Proporcionar un método para recuperar
double
valor de la propiedad y hacer eldouble
->float
->decimal
->double
truco - Otras soluciones poco realistas que usted podría pensar
TL; DR
El problema con el envío de cliente JS número único punto flotante para C # backend causa de un problema conocido en coma flotante:
// value = 0.00499999988824129, crazy C# :)
var value = (double)0.005f;
Para usos directos de double
los métodos, el problema podría ser resuelto por una costumbre 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;
}
}
Y utilizar el sistema de resolución:
services.AddSignalR()
.AddMessagePackProtocol(options =>
{
options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
{
MyDoubleFormatterResolver.Instance,
ContractlessStandardResolver.Instance,
};
});
La resolución no es perfecto, como la fundición a decimal
continuación, a double
ralentiza el proceso y podría ser peligroso .
sin embargo
De acuerdo con la OP señalado en los comentarios, esto no puede resolver el problema si el uso de tipos complejos que tienen double
propiedades que regresan.
Investigaciones posteriores revelaron la causa del problema en 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;
}
}
}
El decodificador anteriormente se utiliza cuando se necesita para convertir un solo float
número a double
:
// From MessagePackBinary class
MessagePackBinary.doubleDecoders[202] = Float32Double.Instance;
v2
Este problema existe en v2 versiones de MessagePack-CSharp. He presentado un problema en github , aunque el tema no va a ser fijo .