JavaScriptからC#への数値の桁落ち


16

MessageRで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のこの機能に関連していると思います。

興味深いのは、同じ問題が別の方向に進んでいるとは思えないことです。C#からJavaScriptに0.005を送信すると、JavaScriptの値は0.005になります。

編集:C#からの値は、JSデバッガーウィンドウで短縮されています。@Peteが述べたように、それは正確に0.5ではない何か(0.005000000000000000104083408558)に拡張されます。これは、矛盾が少なくとも両側で発生することを意味します。

JSONシリアライゼーションには同じ問題はありません。これは、値をネイティブの数値型に解析する際に受信環境を制御する文字列を経由すると想定しているためです。

バイナリシリアル化を使用して、両側で値を一致させる方法があるかどうか疑問に思っています。

そうでない場合、これはJavaScriptとC#の間で100%正確なバイナリ変換を行う方法がないことを意味しますか?

使用されるテクノロジー:

  • JavaScript
  • SignalRおよびmsgpack5を使用した.Netコア

私のコードはこの投稿に基づいています。唯一の違いは、私が使用していることですContractlessStandardResolver.Instance


C#での浮動小数点表現も、すべての値に対して正確ではありません。シリアル化されたデータを見てください。C#ではどのように解析しますか?
JeffRSon

C#ではどのタイプを使用しますか?Doubleにはそのような問題があることが知られています。
Poul Bak

シグナルャーに付属する組み込みのメッセージパックのシリアライズ/デシリアライズと、メッセージパックの統合を使用します。
TGH

浮動小数点値は正確ではありません。正確な値が必要な場合は、文字列(フォーマットの問題)または整数(1000を掛けることなど)を使用してください。
atmin

逆シリアル化されたメッセージを確認できますか?c#がオブジェクトに変換される前に、jsから取得したテキスト。
Jonny Piazzi

回答:


9

更新

これは次のリリース(5.0.0-preview4)で修正れました

元の回答

私はをテストfloatdouble、そして興味深いことに、この特定のケースでdoubleは、問題があるだけでしたが、機能しているfloatようです(つまり、サーバーで0.005が読み取られます)。

メッセージバイトを調べると、64ビットの浮動小数点であるFloat32Doubleにもかかわらず、0.005が4バイト/ 32ビットのIEEE 754単精度浮動小数点数であるタイプとして送信されることが示唆されましたNumber

上記で確認されたコンソールで次のコードを実行します。

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オプションはsignalr-protocol-msgpackでは使用されません。

それがfloatサーバー側で機能する理由を説明していますが、現時点では実際には修正されていませんマイクロソフトの発言を待ちましょう。

可能な回避策

  • msgpack5オプションをハックしますか?独自のmsgpack5をforkし、forceFloat64デフォルトでtrueにコンパイルしますか?知りません。
  • floatサーバー側に切り替え
  • string両面使用
  • decimalサーバー側に切り替え、カスタムを記述しますIFormatterProviderdecimalプリミティブ型ではなくIFormatterProvider<decimal>、複合型プロパティのために呼び出されます
  • doubleプロパティ値を取得してdouble-> float-> decimal-> doubleトリックを行うメソッドを提供する
  • あなたが考えることができる他の非現実的な解決策

TL; DR

単一の浮動小数点数をC#バックエンドに送信するJSクライアントの問題により、既知の浮動小数点の問題が発生します。

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

doubleinメソッドを直接使用する場合、問題はカスタムで解決できます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: ...\.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バージョンに存在します。私はgithub問題を提出しましが、問題は修正されません


興味深い発見。ここでの1つの課題は、問題が複雑なオブジェクトの任意の数のdoubleプロパティに当てはまるため、doubleを直接ターゲットに設定するのは難しいと思います。
TGH

@TGHええ、あなたは正しいです。MessagePack-CSharpのバグだと思います。詳細は私の更新を参照してください。現時点ではfloat、回避策として使用する必要があるかもしれません。彼らがv2でそれを修正したかどうかはわかりません。少し時間があれば見てみます。ただし、問題はv2がまだSignalRと互換性がないことです。SignalRのプレビューバージョン(5.0.0.0- *)のみがv2を使用できます。
weichch

これはv2でも機能しません。MessagePack-CSharpでバグを報告しました。
weichch

@TGH残念ながら、githubの問題での議論に従って、サーバー側に修正はありません。最善の修正は、クライアント側に32ビットではなく64ビットを送信させることです。私はそれを強制的に実行するオプションがあることに気づきましたが、Microsoftはそれを公開していません(私の理解から)。あなたが見てみたいなら、いくつかの厄介な回避策で答えを更新しました。そしてこの問題で頑張ってください。
weichch

それは興味深いリードのように聞こえます。それを見てみましょう。これであなたの助けをありがとう!
TGH

14

送信する正確な値をより高い精度で確認してください。言語は通常、印刷の精度を制限して見栄えをよくします。

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

はい、あなたはそれについて正しいです。
TGH
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.