JavaScript до C # Числова втрата точності


16

Під час серіалізації та десеріалізації значень між JavaScript та C # за допомогою SignalR з MessagePack я бачу трохи втрати точності в C # на приймальному кінці.

Як приклад я надсилаю значення 0,005 з JavaScript на C #. Коли деріаріалізоване значення з’являється на стороні C #, я отримую значення 0.004999999888241291, яке близько, але точно не 0,005. Значення на стороні JavaScript є Numberі на стороні C #, яку я використовую double.

Я читав, що JavaScript не може точно представляти числа з плаваючою комою, що може призвести до таких результатів 0.1 + 0.2 == 0.30000000000000004. Я підозрюю, що проблема, яку я бачу, пов'язана з цією особливістю JavaScript.

Цікавою є те, що я не бачу того самого питання, що йде іншим шляхом. Відправка 0,005 з C # на JavaScript призводить до значення 0,005 у JavaScript.

Редагувати : Значення від C # просто скорочується у вікні налагодження JS. Як згадував @Pete, він розширюється до того, що точно не становить 0,5 (0,005000000000000000104083408558). Це означає, що розбіжність трапляється принаймні з обох сторін.

Серіалізація JSON не має тієї ж проблеми, оскільки я припускаю, що вона проходить через рядок, який залишає приймаюче середовище в контрольному wrt, розбираючи значення на свій рідний числовий тип.

Мені цікаво, чи існує спосіб використання двійкової серіалізації, щоб мати відповідні значення з обох сторін.

Якщо ні, чи означає це, що немає можливості здійснити 100% точну бінарну конверсію між JavaScript та C #?

Використовувана технологія:

  • JavaScript
  • .Net Core із SignalR та msgpack5

Мій код заснований на цій публікації . Різниця лише в тому, що я використовую ContractlessStandardResolver.Instance.


Представлення з плаваючою комою в C # також не є точним для кожного значення. Погляньте на серіалізовані дані. Як ти розбираєш його в C #?
JeffRSon

Який тип ви використовуєте в C #? Як відомо, у "Double" є таке питання.
Poul Bak

Я використовую вбудовану серіялізацію / десеріалізацію пакета повідомлень, що постачається разом із сигналізатором, і це інтеграція пакета повідомлень.
TGH

Значення з плаваючою комою ніколи не є точними. Якщо вам потрібні точні значення, використовуйте рядки (випуск форматування) або цілі числа (наприклад, множення на 1000).
atmin

Чи можете ви перевірити дезаріалізоване повідомлення? Текст, який ви отримали від js, перш ніж c # перетворюється в об’єкт.
Джоні Піацці

Відповіді:


9

ОНОВЛЕННЯ

Це було виправлено у наступному випуску (5.0.0-preview4) .

Оригінальний відповідь

Я тестував floatі double, що цікаво в цьому конкретному випадку, тільки doubleмав проблему, тоді як, floatздається, працює (тобто 0,005 читається на сервері).

Перевіряючи байти повідомлень, можна припустити, що 0,005 надсилається як тип, Float32Doubleякий є 4-байтним / 32-бітовим IEEE 754 з одноточним числом з плаваючою точкою, незважаючи 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-Protocol-msgpack не використовується .

Хоча це і пояснює, чому floatпрацює на серверній стороні, але насправді це не виправлено на даний момент . Зачекаємо, що скаже Microsoft .

Можливі обхідні шляхи

  • Параметри злому msgpack5? Розкладіть і компілюйте свій власний msgpack5 за forceFloat64замовчуванням до true ?? Не знаю.
  • Перехід floatна сторону сервера
  • Використовуйте stringз обох сторін
  • Увімкніть decimalна стороні сервера і напишіть на замовлення IFormatterProvider. decimalне є примітивним типом і IFormatterProvider<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уповільнити процес, і це може бути небезпечно .

Однак

Як зазначено в коментарі ОП, це не може вирішити проблему, якщо використовувати складні типи, що мають 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

Ця проблема існує у версії v2 версії MessagePack-CSharp. Я подав проблему на 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.