Я изо всех сил пытался написать двойной, такой как 82.0, используя Utf8JsonWriter.

По умолчанию метод method WriteNumberValue принимает двойное значение и форматирует его для меня, а формат (который является стандартным форматом «G») опускает суффикс «.0». Я не могу найти способ контролировать это.

По дизайну кажется, что я не могу просто написать необработанную строку в Utf8JsonWriter, но я нашел обходной путь: создать JsonElement и вызвать JsonElement.WriteTo`. Это вызывает a закрытый метод в Utf8JsonWriter и записывает строку прямо в него.

С этим открытием я сделал то, что выглядело очень хакерским и неэффективным.

open System.Text.Json

void writeFloat(Utf8JsonWriter w, double d) {
  String floatStr = f.ToString("0.0################")
  JsonElement jse = JsonDocument.Parse(floatStr).RootElement
  jse.WriteTo(w)
}

Мне все равно нужно отформатировать двойник, так что это нормально, но его разбор, создание jsonDocument и JsonElement, просто чтобы найти способ вызвать защищенный метод, кажется действительно расточительным. Однако он работает (я написал его на F # и перевел на C #, извиняюсь, если допустил ошибку в синтаксисе).

Есть ли способ лучше? Некоторые потенциальные решения, которые приходят на ум (я новичок в dotnet, поэтому не уверен, что здесь возможно):

  • есть ли способ получить доступ к частный API напрямую? Я думал, что подкласс Utf8Writer может сработать, но это закрытый класс.
  • Могу ли я создать экземпляр JsonElement напрямую, без всякой чуши синтаксического анализа?

Что касается того, почему это необходимо: мне нужно принудительно записывать целочисленные значения с добавлением .0, потому что есть чрезвычайно конкретный формат, с которым мне нужно взаимодействовать, который различает значения JSON с целыми числами и с плавающей запятой. (Я согласен с экспоненциальным форматом, поскольку это явно плавающее число).

1
Paul Biggar 22 Фев 2021 в 05:05

1 ответ

Лучший ответ

Вам необходимо создать JsonConverter<double>, удовлетворяющие следующему:

  • При форматировании значений double в фиксированном формате, когда значение является целым числом, необходимо добавить дробную часть .0.

  • Без изменений при форматировании в экспоненциальном формате.

  • Без изменений при форматировании не конечных чисел двойной точности, таких как double.PositiveInfinity.

  • Нет необходимости поддерживать JsonNumberHandling варианты WriteAsString или AllowReadingFromString.

  • Никакого промежуточного анализа для JsonDocument.

В этом случае, как предлагает mjwills в comments, вы можете преобразовать double в decimal с требуемым дробным компонентом, затем запишите его в JSON следующим образом:

public class DoubleConverter : JsonConverter<double>
{
    // 2^49 is the largest power of 2 with fewer than 15 decimal digits.  
    // From experimentation casting to decimal does not lose precision for these values.
    const double MaxPreciselyRepresentedIntValue = (1L<<49);

    public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
    {
        bool written = false;
        // For performance check to see that the incoming double is an integer
        if ((value % 1) == 0)
        {
            if (value < MaxPreciselyRepresentedIntValue && value > -MaxPreciselyRepresentedIntValue)
            {
                writer.WriteNumberValue(0.0m + (decimal)value);
                written = true;
            }
            else
            {
                // Directly casting these larger values from double to decimal seems to result in precision loss, as noted in  https://stackoverflow.com/q/7453900/3744182
                // And also: https://docs.microsoft.com/en-us/dotnet/api/system.convert.todecimal?redirectedfrom=MSDN&view=net-5.0#System_Convert_ToDecimal_System_Double_
                // > The Decimal value returned by Convert.ToDecimal(Double) contains a maximum of 15 significant digits.
                // So if we want the full G17 precision we have to format and parse ourselves.
                //
                // Utf8Formatter and Utf8Parser should give the best performance for this, but, according to MSFT, 
                // on frameworks earlier than .NET Core 3.0 Utf8Formatter does not produce roundtrippable strings.  For details see
                // https://github.com/dotnet/runtime/blob/eb03e0f7bc396736c7ac59cf8f135d7c632860dd/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs#L103
                // You may want format to string and parse in earlier frameworks -- or just use JsonDocument on these earlier versions.
                Span<byte> utf8bytes = stackalloc byte[32];
                if (Utf8Formatter.TryFormat(value, utf8bytes.Slice(0, utf8bytes.Length-2), out var bytesWritten)
                    && IsInteger(utf8bytes, bytesWritten))
                {
                    utf8bytes[bytesWritten++] = (byte)'.';
                    utf8bytes[bytesWritten++] = (byte)'0';
                    if (Utf8Parser.TryParse(utf8bytes.Slice(0, bytesWritten), out decimal d, out var _))
                    {
                        writer.WriteNumberValue(d);
                        written = true;
                    }   
                }
            }
        }
        if (!written)
        {
            if (double.IsFinite(value))
                writer.WriteNumberValue(value);
            else
                // Utf8JsonWriter does not take into account JsonSerializerOptions.NumberHandling so we have to make a recursive call to serialize
                JsonSerializer.Serialize(writer, value, new JsonSerializerOptions { NumberHandling = options.NumberHandling });
        }
    }
    
    static bool IsInteger(Span<byte> utf8bytes, int bytesWritten)
    {
        if (bytesWritten <= 0)
            return false;
        var start = utf8bytes[0] == '-' ? 1 : 0;
        for (var i = start; i < bytesWritten; i++)
            if (!(utf8bytes[i] >= '0' && utf8bytes[i] <= '9'))
                return false;
        return start < bytesWritten;
    }
    
    public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => 
        // TODO: Handle "NaN", "Infinity", "-Infinity"
        reader.GetDouble();
}

Примечания:

  • Это работает, потому что decimal (в отличие от double) сохраняет конечные нули, как указано в примечания к документации.

  • Безусловное преобразование double в decimal может потерять точность для больших значений, поэтому просто выполните

    writer.WriteNumberValue(0.0m + (decimal)value);
    

    заставлять минимальное количество цифр не рекомендуется. (Например, сериализация 9999999999999992 приведет к 9999999999999990.0, а не 9999999999999992.0.)

    Однако, согласно странице Википедии, Формат с плавающей запятой двойной точности: ограничения точности для целочисленные значения, целые числа от −2 ^ 53 до 2 ^ 53 могут быть точно представлены как double, поэтому для значений в этом диапазоне можно использовать преобразование в десятичное число и принудительное использование минимального количества цифр.

  • Кроме этого, нет никакого способа напрямую установить количество цифр .Net decimal во время выполнения, кроме синтаксического анализа его из некоторого текстового представления. Для повышения производительности я использую Utf8Formatter и Utf8Parser, однако в средах, предшествующих .NET Core 3.0, точность может быть снижена, и вместо этого следует использовать обычное форматирование и синтаксический анализ string. Подробности см. На комментарии к коду для Utf8JsonWriter.WriteValues.Double.cs.

  • Вы спросили, есть ли способ получить прямой доступ к частному API?

    Вы всегда можете использовать отражение для вызова частного метода, как показано в Как использовать отражение для вызова частного метода? , однако это не рекомендуется, поскольку внутренние методы могут быть изменены в любое время, что нарушит вашу реализацию. Кроме того, нет общедоступного API для прямой записи «сырого» JSON, кроме его синтаксического анализа до JsonDocument и последующего написания этого. Мне пришлось использовать тот же трюк в моем ответе на Сериализация BigInteger с использованием System.Text.Json .

  • Вы спросили, могу ли я создать экземпляр JsonElement напрямую, без всякой чуши синтаксического анализа?

    Это невозможно в .NET 5. Как показано в его исходный код, структура JsonElement просто содержит ссылку на свой родительский JsonDocument _parent вместе с индексом местоположения, указывающим где элемент расположен в документе.

    Фактически, в .NET 5 при десериализации в JsonElement с использованием JsonSerializer.Deserialize<JsonElement>(string) внутренне JsonElementConverter считывает входящий JSON во временный JsonDocument, клонирует свой RootElement, затем удаляет документ и возвращает клон.

  • В вашем исходном конвертере f.ToString("0.0################") не будет правильно работать в локали, в которых в качестве десятичного разделителя используется запятая. Вместо этого вам нужно использовать инвариантный языковой стандарт:

    f.ToString("0.0################", NumberFormatInfo.InvariantInfo);
    
  • Блок else проверки double.IsFinite(value) предназначен для правильной сериализации неконечных значений, таких как double.PositiveInfinity. Экспериментируя, я обнаружил, что Utf8JsonWriter.WriteNumberValue(value) безоговорочно генерирует значения для этих типов значений, поэтому сериализатор должен быть вызван для правильной обработки их, когда JsonNumberHandling.AllowNamedFloatingPointLiterals включен.

  • Особый случай для value < MaxPreciselyRepresentedIntValue предназначен для максимизации производительности за счет исключения любого обращения к текстовому представлению, когда это возможно.

    Однако я на самом деле не профилировал, чтобы подтвердить, что это быстрее, чем выполнение текстового возврата.

Демо-скрипт здесь, который включает некоторые модульные тесты, утверждающие, что конвертер генерирует тот же вывод, что и Json.NET для широкого диапазона целых значений double, поскольку Json.NET всегда добавляет .0 при их сериализации.

3
dbc 25 Фев 2021 в 16:10