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.