Потеря точности 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 не имеет той же проблемы, так как я предполагаю, что она проходит через строку, которая оставляет принимающую среду под контролем, анализируя значение в его собственном числовом типе.

Мне интересно, есть ли способ использовать двоичную сериализацию, чтобы иметь совпадающие значения с обеих сторон.

Если нет, значит ли это, что нет никакого способа получить 100% точные двоичные преобразования между JavaScript и C #?

Используемая технология:

  • JavaScript
  • .Net Core с SignalR и msgpack5

Мой код основан на этом посте . Единственная разница в том, что я использую ContractlessStandardResolver.Instance.

TGH
источник
Представление с плавающей точкой в ​​C # также не является точным для каждого значения. Посмотрите на сериализованные данные. Как вы анализируете это в C #?
Джеффрсон
Какой тип вы используете в C #? У Double, как известно, есть такая проблема.
Пол Бак
Я использую встроенную сериализацию / десериализацию пакета сообщений, которая поставляется вместе с сигнализатором, и интеграцию пакета сообщений.
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->decimaldouble трюк ->
  • Другие нереальные решения, о которых вы могли подумать

TL; DR

Проблема с отправкой JS-клиентом одного числа с плавающей запятой в бэкэнд C # вызывает известную проблему с плавающей запятой:

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

Для прямого использования doubleметодов in, проблема может быть решена с помощью обычая 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

Эта проблема существует в версии v2 MessagePack-CSharp. Я подал проблему на GitHub , хотя проблема не будет решена .

weichch
источник
Интересные выводы. Одна из проблем здесь заключается в том, что эта проблема относится к любому числу двойных свойств сложного объекта, поэтому, на мой взгляд, будет непросто нацелить двойные объекты напрямую.
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