Потеря точности 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.


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


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