JavaScript zu C # Numeric Precision Loss

16

Beim Serialisieren und Deserialisieren von Werten zwischen JavaScript und C # mithilfe von SignalR mit MessagePack tritt auf der Empfangsseite ein gewisser Genauigkeitsverlust in C # auf.

Als Beispiel sende ich den Wert 0,005 von JavaScript an C #. Wenn der deserialisierte Wert auf der C # -Seite angezeigt wird, erhalte ich den Wert 0.004999999888241291, der nahe beieinander liegt, aber nicht genau 0,005. Der Wert auf der JavaScript-Seite ist Numberund auf der C # -Seite, die ich verwende double.

Ich habe gelesen, dass JavaScript Gleitkommazahlen nicht genau darstellen kann, was zu Ergebnissen wie führen kann 0.1 + 0.2 == 0.30000000000000004. Ich vermute, dass das Problem, das ich sehe, mit dieser Funktion von JavaScript zusammenhängt.

Der interessante Teil ist, dass ich nicht sehe, dass das gleiche Problem in die andere Richtung geht. Das Senden von 0,005 von C # an JavaScript führt zu dem Wert 0,005 in JavaScript.

Bearbeiten : Der Wert von C # wird im JS-Debugger-Fenster nur gekürzt. Wie @Pete erwähnt hat, wird es auf etwas erweitert, das nicht genau 0,5 ist (0,005000000000000000104083408558). Dies bedeutet, dass die Diskrepanz zumindest auf beiden Seiten auftritt.

Die JSON-Serialisierung hat nicht das gleiche Problem, da ich davon ausgehe, dass sie über eine Zeichenfolge erfolgt, wodurch die empfangende Umgebung die Kontrolle über das Parsen des Werts in seinen nativen numerischen Typ behält.

Ich frage mich, ob es eine Möglichkeit gibt, mithilfe der binären Serialisierung auf beiden Seiten übereinstimmende Werte zu erhalten.

Wenn nicht, bedeutet dies, dass es keine Möglichkeit gibt, 100% genaue binäre Konvertierungen zwischen JavaScript und C # durchzuführen?

Verwendete Technologie:

  • JavaScript
  • .Net Core mit SignalR und msgpack5

Mein Code basiert auf diesem Beitrag . Der einzige Unterschied ist, dass ich benutze ContractlessStandardResolver.Instance.

TGH
quelle
Die Gleitkommadarstellung in C # ist nicht für jeden Wert genau. Schauen Sie sich die serialisierten Daten an. Wie analysiert man es in C #?
JeffRSon
Welchen Typ verwenden Sie in C #? Es ist bekannt, dass Double ein solches Problem hat.
Poul Bak
Ich verwende die integrierte Serilisierung / Deserialisierung von Nachrichtenpaketen, die mit signalr und der Integration von Nachrichtenpaketen geliefert wird.
TGH
Gleitkommawerte sind niemals präzise. Wenn Sie genaue Werte benötigen, verwenden Sie Zeichenfolgen (Formatierungsproblem) oder Ganzzahlen (z. B. durch Multiplizieren mit 1000).
am
Können Sie die deserialisierte Nachricht überprüfen? Der Text, den Sie von js erhalten haben, bevor c # in ein Objekt konvertiert wurde.
Jonny Piazzi

Antworten:

9

AKTUALISIEREN

Dies wurde in der nächsten Version (5.0.0-Vorschau4) behoben .

Ursprüngliche Antwort

Ich habe getestet floatund double, und interessanterweise in diesem speziellen Fall, nur doubledas Problem gehabt, während floates zu funktionieren scheint (dh 0,005 wird auf dem Server gelesen).

Die Überprüfung der Nachrichtenbytes ergab, dass 0,005 als Typ gesendet wird, bei Float32Doubledem es sich um eine 4-Byte / 32-Bit-Gleitkommazahl mit einfacher Genauigkeit nach IEEE 754 handelt, obwohl Numberes sich um 64-Bit-Gleitkommazahlen handelt.

Führen Sie den folgenden Code in der Konsole aus, um Folgendes zu bestätigen:

msgpack5().encode(Number(0.005))

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

mspack5 bietet eine Option zum Erzwingen eines 64-Bit-Gleitkommas:

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

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

Die forceFloat64Option wird jedoch nicht von signalr-protocol-msgpack verwendet .

Das erklärt zwar, warum es floatauf der Serverseite funktioniert, aber es gibt derzeit keine wirkliche Lösung dafür . Warten wir, was Microsoft sagt .

Mögliche Problemumgehungen

  • Msgpack5-Optionen hacken? Fork und kompiliere dein eigenes msgpack5 mit dem forceFloat64Standardwert true ?? Ich weiß es nicht.
  • Wechseln Sie floatauf die Serverseite
  • Verwenden Sie stringauf beiden Seiten
  • Wechseln Sie decimalauf die Serverseite und schreiben Sie benutzerdefiniert IFormatterProvider. decimalist kein primitiver Typ und IFormatterProvider<decimal>wird für komplexe Typeneigenschaften aufgerufen
  • Geben Sie eine Methode zum Abrufen des doubleEigenschaftswerts an und führen Sie den Trick double-> float-> decimal-> ausdouble
  • Andere unrealistische Lösungen, die Sie sich vorstellen können

TL; DR

Das Problem, dass der JS-Client eine einzelne Gleitkommazahl an das C # -Backend sendet, verursacht ein bekanntes Gleitkommaproblem:

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

Für die direkte Verwendung von doublein-Methoden könnte das Problem durch eine benutzerdefinierte Lösung gelöst werden 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;
    }
}

Und benutze den Resolver:

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

Der Resolver ist nicht perfekt, da das Gießen bis decimaldahin doubleden Prozess verlangsamt und gefährlich sein kann .

jedoch

Gemäß dem in den Kommentaren genannten OP kann dies das Problem nicht lösen, wenn komplexe Typen mit doublezurückgegebenen Eigenschaften verwendet werden.

Weitere Untersuchungen ergaben die Ursache des Problems in 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;
    }
  }
}

Der obige Decoder wird verwendet, wenn eine einzelne floatZahl konvertiert werden muss in double:

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

v2

Dieses Problem tritt in Version 2 von MessagePack-CSharp auf. Ich habe ein Problem bei Github eingereicht , das Problem wird jedoch nicht behoben .

weichch
quelle
Interessante Ergebnisse. Eine Herausforderung besteht darin, dass das Problem auf eine beliebige Anzahl von Doppeleigenschaften für ein komplexes Objekt zutrifft. Daher wird es meiner Meinung nach schwierig sein, Doppel direkt anzuvisieren.
TGH
@TGH Ja, du hast recht. Ich glaube, es ist ein Fehler in MessagePack-CSharp. Siehe meine aktualisierten für Details. Im Moment müssen Sie möglicherweise floateine Problemumgehung verwenden. Ich weiß nicht, ob sie das in v2 behoben haben. Ich werde einen Blick darauf werfen, sobald ich etwas Zeit habe. Das Problem ist jedoch, dass v2 noch nicht mit SignalR kompatibel ist. Nur Vorschauversionen (5.0.0.0- *) von SignalR können v2 verwenden.
weichch
Dies funktioniert auch in Version 2 nicht. Ich habe einen Fehler mit MessagePack-CSharp ausgelöst.
weichch
@TGH Leider gibt es auf der Serverseite keine Korrektur gemäß der Diskussion im Github-Problem. Die beste Lösung wäre, die Clientseite dazu zu bringen, 64 Bit anstelle von 32 Bit zu senden. Ich habe festgestellt, dass es eine Option gibt, um dies zu erzwingen, aber Microsoft legt dies nicht offen (nach meinem Verständnis). Gerade aktualisierte Antwort mit einigen bösen Problemumgehungen, wenn Sie einen Blick darauf werfen möchten. Und viel Glück in dieser Angelegenheit.
weichch
Das klingt nach einer interessanten Spur. Ich werde mir das ansehen. Vielen Dank für Ihre Hilfe!
TGH
14

Bitte überprüfen Sie den genauen Wert, den Sie senden, genauer. Sprachen beschränken normalerweise die Genauigkeit des Drucks, damit er besser aussieht.

var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...
Pete
quelle
Ja, da hast du recht.
TGH