JavaScript để mất chính xác số C #


16

Khi tuần tự hóa và giải tuần tự hóa các giá trị giữa JavaScript và C # bằng SignalR với MessagePack, tôi thấy một chút mất độ chính xác trong C # ở đầu nhận.

Ví dụ, tôi đang gửi giá trị 0,005 từ JavaScript đến C #. Khi giá trị khử lưu huỳnh xuất hiện ở phía C #, tôi sẽ nhận được giá trị 0.004999999888241291gần đúng, nhưng không chính xác 0,005. Giá trị ở phía JavaScript là Numbervà ở phía C # tôi đang sử dụng double.

Tôi đã đọc được rằng JavaScript không thể biểu thị chính xác các số dấu phẩy động có thể dẫn đến kết quả như thế nào 0.1 + 0.2 == 0.30000000000000004. Tôi nghi ngờ vấn đề tôi đang thấy có liên quan đến tính năng này của JavaScript.

Phần thú vị là tôi không thấy vấn đề tương tự đi theo hướng khác. Gửi 0,005 từ C # đến JavaScript dẫn đến giá trị 0,005 trong JavaScript.

Chỉnh sửa : Giá trị từ C # chỉ được rút ngắn trong cửa sổ trình gỡ lỗi JS. Như @Pete đã đề cập, nó mở rộng thành một thứ không chính xác 0,5 (0,005000000000000000104083408558). Điều này có nghĩa là sự khác biệt xảy ra ở cả hai bên ít nhất.

Tuần tự hóa JSON không có vấn đề tương tự vì tôi giả sử nó đi qua chuỗi, điều này khiến môi trường nhận trong điều khiển wrt phân tích giá trị thành kiểu số nguyên gốc của nó.

Tôi tự hỏi nếu có một cách sử dụng tuần tự nhị phân để có giá trị phù hợp ở cả hai bên.

Nếu không, điều này có nghĩa là không có cách nào để có chuyển đổi nhị phân chính xác 100% giữa JavaScript và C #?

Công nghệ sử dụng:

  • JavaScript
  • .Net Core với SignalR và dirpack5

Mã của tôi dựa trên bài đăng này . Sự khác biệt duy nhất là tôi đang sử dụng ContractlessStandardResolver.Instance.


Biểu diễn dấu phẩy động trong C # cũng không chính xác cho mọi giá trị. Có một cái nhìn vào dữ liệu nối tiếp. Làm thế nào để bạn phân tích nó trong C #?
JeffRSon

Bạn sử dụng loại nào trong C #? Double được biết là có vấn đề như vậy.
Poul Bak

Tôi sử dụng dịch vụ khử rung / giải nén gói tin đi kèm với signalr và đó là tích hợp gói tin.
TGH

Giá trị dấu phẩy động không bao giờ chính xác. Nếu bạn cần các giá trị chính xác, hãy sử dụng chuỗi (vấn đề định dạng) hoặc số nguyên (ví dụ: nhân với 1000).
tối

Bạn có thể kiểm tra tin nhắn khử lưu huỳnh? Văn bản bạn nhận được từ js, trước khi c # chuyển đổi trong một đối tượng.
Jonny Piazzi

Câu trả lời:


9

CẬP NHẬT

Điều này đã được sửa trong phiên bản tiếp theo (5.0.0-preview4) .

Câu trả lời gốc

Tôi đã thử nghiệm floatdouble, và thú vị trong trường hợp cụ thể này, chỉ doublecó vấn đề, trong khi floatdường như đang hoạt động (tức là 0,005 được đọc trên máy chủ).

Việc kiểm tra các byte thông báo cho thấy 0,005 được gửi dưới dạng loại Float32Doublelà số dấu phẩy động chính xác đơn 4 byte / 32 bit của IEEE 754 mặc dù Numberlà dấu phẩy động 64 bit.

Chạy đoạn mã sau trong bảng điều khiển đã xác nhận ở trên:

msgpack5().encode(Number(0.005))

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

mspack5 không cung cấp tùy chọn bắt buộc dấu phẩy động 64 bit:

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

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

Tuy nhiên, forceFloat64tùy chọn này không được sử dụng bởi signalr-Protocol-dirpack .

Mặc dù điều đó giải thích tại sao floathoạt động ở phía máy chủ, nhưng thực sự không có cách khắc phục nào cho đến bây giờ . Chúng ta hãy chờ những gì Microsoft nói .

Cách giải quyết có thể

  • Hack tùy chọn hackpack5? Ngã ba và biên dịch trình tin riêng của bạn với forceFloat64mặc định là đúng ?? Tôi không biết.
  • Chuyển sang floatphía máy chủ
  • Sử dụng stringcho cả hai bên
  • Chuyển sang decimalphía máy chủ và viết tùy chỉnh IFormatterProvider. decimalkhông phải là kiểu nguyên thủy và IFormatterProvider<decimal>được gọi cho các thuộc tính kiểu phức tạp
  • Cung cấp phương thức để lấy doublegiá trị tài sản và thực hiện double-> float-> decimal-> doublelừa
  • Các giải pháp phi thực tế khác mà bạn có thể nghĩ đến

TL; DR

Sự cố với máy khách JS gửi số dấu phẩy động đơn đến phụ trợ C # gây ra sự cố điểm nổi đã biết:

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

Để sử dụng trực tiếp các doublephương thức, vấn đề có thể được giải quyết bằng một tùy chỉnh 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;
    }
}

Và sử dụng trình giải quyết:

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

Bộ giải quyết không hoàn hảo, vì khi sử dụng decimalđể doublelàm chậm quá trình và nó có thể nguy hiểm .

Tuy nhiên

Theo OP chỉ ra trong các bình luận, điều này không thể giải quyết vấn đề nếu sử dụng các loại phức tạp có doublethuộc tính trả về.

Điều tra sâu hơn cho thấy nguyên nhân của vấn đề trong 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;
    }
  }
}

Bộ giải mã trên được sử dụng khi cần chuyển đổi một floatsố thành double:

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

v2

Vấn đề này tồn tại trong các phiên bản v2 của MessagePack-CSharp. Tôi đã gửi một vấn đề trên github , mặc dù vấn đề này sẽ không được khắc phục .


Những phát hiện thú vị. Một thách thức ở đây là vấn đề áp dụng cho bất kỳ số lượng thuộc tính kép nào trên một đối tượng phức tạp, do đó, sẽ rất khó để nhắm mục tiêu trực tiếp gấp đôi tôi nghĩ.
TGH

@TGH Vâng, bạn nói đúng. Tôi tin rằng đó là một lỗi trong MessagePack-CSharp. Xem cập nhật của tôi để biết chi tiết. Hiện tại, bạn có thể cần phải sử dụng floatnhư một cách giải quyết. Tôi không biết nếu họ đã sửa nó trong v2. Tôi sẽ xem xét một khi tôi có thời gian. Tuy nhiên, vấn đề là v2 chưa tương thích với SignalR. Chỉ các phiên bản xem trước (5.0.0.0- *) của SignalR mới có thể sử dụng v2.
weichch

Điều này cũng không hoạt động trong v2. Tôi đã đưa ra một lỗi với MessagePack-CSharp.
weichch

@TGH Thật không may, không có bất kỳ sửa chữa nào ở phía máy chủ theo cuộc thảo luận về vấn đề github. Cách khắc phục tốt nhất là yêu cầu phía khách hàng gửi 64 bit thay vì 32 bit. Tôi nhận thấy có một tùy chọn để buộc điều đó xảy ra, nhưng Microsoft không phơi bày điều đó (theo cách hiểu của tôi). Chỉ cần cập nhật câu trả lời với một số cách giải quyết khó chịu nếu bạn muốn xem qua. Và chúc may mắn về vấn đề này.
weichch

Nghe có vẻ như một dẫn thú vị. Tôi sẽ xem xét điều đó. Nhờ sự giúp đỡ của bạn với điều này!
TGH

14

Vui lòng kiểm tra giá trị chính xác mà bạn đang gửi đến độ chính xác lớn hơn. Các ngôn ngữ thường giới hạn độ chính xác trên bản in để làm cho nó trông đẹp hơn.

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

Vâng, bạn đúng về điều đó.
TGH
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.