JavaScript до C# Нумери c Точность потери - PullRequest
16 голосов
/ 27 марта 2020

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

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

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

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

  • JavaScript
  • . Net Ядро с SignalR и msgpack5

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

Ответы [ 2 ]

14 голосов
/ 27 марта 2020

Пожалуйста, проверьте точное значение, которое вы посылаете с большей точностью. Языки обычно ограничивают точность печати, чтобы она выглядела лучше.

var n = Number(0.005);
console.log(n);
0.005
console.log(n.toPrecision(100));
0.00500000000000000010408340855860842566471546888351440429687500000000...
9 голосов
/ 30 марта 2020

ОБНОВЛЕНИЕ

Это было исправлено в следующем выпуске (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 трюк
  • Другие нереалистичные c решения, о которых вы могли подумать

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

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

...