JavaScript เป็น C # การสูญเสียความแม่นยำเชิงตัวเลข


16

เมื่อซีเรียลไลซ์และค่าดีซีเรียลไลซ์ระหว่าง JavaScript และ C # โดยใช้ SignalR กับ MessagePack ฉันเห็นการสูญเสียความแม่นยำเล็กน้อยใน C # เมื่อสิ้นสุดการรับ

ตัวอย่างเช่นฉันส่งค่า 0.005 จาก JavaScript ไปยัง C # เมื่อค่า deserialized ปรากฏขึ้นในด้าน C # ฉันได้รับค่า0.004999999888241291ซึ่งอยู่ใกล้ แต่ไม่ใช่ 0.005 คุ้มค่าในด้าน JavaScript เป็นNumberและที่ด้านข้าง C # doubleฉันใช้

ฉันได้อ่านแล้วว่าจาวาสคริปต์ไม่สามารถแสดงตัวเลขทศนิยมได้อย่างแม่นยำซึ่งสามารถนำไปสู่ผลลัพธ์เช่น0.1 + 0.2 == 0.30000000000000004นั้น ฉันสงสัยว่าปัญหาที่ฉันเห็นเกี่ยวข้องกับคุณสมบัติของ JavaScript นี้

ส่วนที่น่าสนใจคือฉันไม่ได้เห็นปัญหาแบบเดิม ๆ การส่ง 0.005 จาก C # ถึง JavaScript จะทำให้ได้ค่า 0.005 ใน JavaScript

แก้ไข : ค่าจาก C # สั้นลงในหน้าต่างดีบักเกอร์ JS ดังที่ @Pete ได้กล่าวถึงมันจะขยายไปสู่สิ่งที่ไม่ใช่ 0.5 อย่างแน่นอน (0.005000000000000000104083408558) ซึ่งหมายถึงความคลาดเคลื่อนเกิดขึ้นกับทั้งสองฝ่ายเป็นอย่างน้อย

การทำให้เป็นอันดับ JSON ไม่ได้มีปัญหาเดียวกันตั้งแต่ฉันสมมติว่ามันจะผ่านสตริงที่ออกจากสภาพแวดล้อมที่ได้รับในการควบคุมการแยกวิเคราะห์ค่าลงในประเภทตัวเลขพื้นเมืองของมัน

ฉันสงสัยว่ามีวิธีใช้ binary serialization เพื่อให้มีการจับคู่ค่าทั้งสองด้าน

ถ้าไม่นี่หมายความว่าไม่มีวิธีใดที่จะมีการแปลงไบนารีที่ถูกต้อง 100% ระหว่าง JavaScript และ C # ใช่ไหม

เทคโนโลยีที่ใช้:

  • JavaScript
  • .Net Core พร้อม SignalR และ msgpack5

รหัสของฉันอ้างอิงจากโพสต์นี้ ContractlessStandardResolver.Instanceแตกต่างเพียงว่าฉันใช้


การแสดงค่าทศนิยมใน C # นั้นไม่ถูกต้องสำหรับทุกค่าเช่นกัน ดูที่ข้อมูลที่ต่อเนื่องกัน คุณแยกวิเคราะห์มันใน C # ได้อย่างไร
JeffRSon

คุณใช้ C # ในประเภทใด เป็นที่ทราบกันว่า Double มีปัญหาดังกล่าว
พอลบาก

ฉันใช้การแพ็คข้อความ / การดีซีเรียลไลเซชันในตัวที่มาพร้อมกับสัญญาณและการรวมชุดข้อความ
TGH

ค่าจุดลอยตัวจะไม่แม่นยำ หากคุณต้องการค่าที่แม่นยำให้ใช้สตริง (ปัญหาการจัดรูปแบบ) หรือจำนวนเต็ม (เช่นคูณด้วย 1,000)
atmin

คุณสามารถตรวจสอบข้อความที่ดีซีเรียลไลซ์ได้หรือไม่? ข้อความที่คุณได้รับจาก js ก่อนที่ c # จะแปลงเป็นวัตถุ
Jonny Piazzi

คำตอบ:


9

UPDATE

นี้ได้รับการแก้ไขในรุ่นถัดไป (5.0.0-preview4)

คำตอบเดิม

ฉันทดสอบfloatและdoubleและน่าสนใจในกรณีนี้โดยเฉพาะdoubleมีปัญหาในขณะที่floatดูเหมือนว่าจะทำงาน (เช่น 0.005 อ่านบนเซิร์ฟเวอร์)

การตรวจสอบไบต์ของข้อความแนะนำว่า 0.005 ถูกส่งเป็นชนิดFloat32Doubleซึ่งเป็นเลขทศนิยม 4 จุดความแม่นยำเดี่ยว IEEE 754 4 ไบต์ / 32- บิตแม้จะ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ตัวเลือกที่จะไม่ใช้signalr โปรโตคอล msgpack

แม้ว่าที่อธิบายว่าทำไมfloatทำงานบนฝั่งเซิร์ฟเวอร์แต่มีไม่ได้จริงๆแก้ไขสำหรับว่า ณ ตอนนี้ รอ Let 's สิ่งที่ไมโครซอฟท์กล่าวว่า

วิธีแก้ไขที่เป็นไปได้

  • แฮก msgpack5 ตัวเลือก? แยกและรวบรวม msgpack5 ของคุณเองด้วยforceFloat64ค่าเริ่มต้นเป็นจริง ฉันไม่รู้
  • สลับไปfloatที่ฝั่งเซิร์ฟเวอร์
  • ใช้stringทั้งสองด้าน
  • สลับไปในฝั่งเซิร์ฟเวอร์และการเขียนที่กำหนดเองdecimal ไม่ใช่ชนิดดั้งเดิมและถูกเรียกใช้สำหรับคุณสมบัติชนิดที่ซับซ้อนIFormatterProviderdecimalIFormatterProvider<decimal>
  • จัดเตรียมวิธีในการเรียกคืน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กระบวนการช้าลงและอาจเป็นอันตรายได้

อย่างไรก็ตาม

ตาม 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 , แม้ว่าปัญหาจะไม่ได้รับการแก้ไข


ผลการวิจัยที่น่าสนใจ ความท้าทายอย่างหนึ่งที่นี่ก็คือปัญหานี้นำไปใช้กับคุณสมบัติสองจำนวนใด ๆ บนวัตถุที่ซับซ้อนดังนั้นมันจึงยากที่จะกำหนดเป้าหมายเป็นสองเท่าโดยตรงที่ฉันคิด
TGH

@TGH ใช่คุณพูดถูก ฉันเชื่อว่ามันเป็นข้อผิดพลาดใน MessagePack-CSharp ดูการปรับปรุงของฉันสำหรับรายละเอียด สำหรับตอนนี้คุณอาจต้องใช้floatวิธีแก้ปัญหา ฉันไม่รู้ว่าพวกเขาได้แก้ไขใน v2 หรือไม่ ฉันจะดูเมื่อฉันมีเวลา อย่างไรก็ตามปัญหาคือ v2 ยังเข้ากันไม่ได้กับ SignalR เฉพาะรุ่นพรีวิว (5.0.0.0- *) ของ SignalR เท่านั้นที่สามารถใช้ v2
weichch

มันไม่ทำงานใน v2 เช่นกัน ฉันยกข้อผิดพลาดกับ MessagePack-CSharp
weichch

@TGH แต่น่าเสียดายที่ไม่มีการแก้ไขใด ๆ ในฝั่งเซิร์ฟเวอร์ตามการสนทนาในปัญหา Github การแก้ไขที่ดีที่สุดคือการรับฝั่งไคลเอ็นต์ให้ส่ง 64 บิตมากกว่า 32 บิต ฉันสังเกตเห็นว่ามีตัวเลือกในการบังคับให้สิ่งนั้นเกิดขึ้น แต่ 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.