如何使用 Utf8JsonWriter 在“.0”完好无损的情况下编写像“82.0”这样的浮点数?

Pau*_*gar 1 .net c# system.text.json

我一直在努力编写一个 double,例如82.0使用 Utf8JsonWriter。

默认情况下,methodWriteNumberValue方法接受一个 double 并为我格式化它,并且格式(这是标准的“G”格式)省略了“.0”后缀。我找不到控制它的方法。

按照设计,我似乎不能只将原始字符串写入 Utf8JsonWriter,但我找到了一种解决方法:创建一个 JsonElement 并调用 JsonElement.WriteTo`。这会调用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)
}
Run Code Online (Sandbox Code Playgroud)

无论如何我都需要格式化一个double,这样很好,但是解析它,创建一个jsonDocument和一个JsonElement,只是为了能够找到一种调用受保护方法的方法,似乎真的很浪费。但是,它确实有效(我用 F# 编写并转换为 C#,如果我在语法中犯了错误,请道歉)。

有没有更好的办法?想到的一些潜在解决方案(我是 dotnet 的新手,所以我不确定这里有什么可能):

  • 有没有办法直接访问私有API?我认为子类化 Utf8Writer 可能会起作用,但它是一个密封类。
  • 我可以直接实例化一个 JsonElement 而不用整个解析繁琐吗?

至于为什么这是必要的:我需要强制写入整数值,.0因为我需要与之交互的非常具体的格式,它区分整数和浮点 JSON 值。(我对指数格式没意见,因为这显然是一个浮点数)。

dbc*_*dbc 5

您的要求是创建一个JsonConverter<double>满足以下条件的:

  • double以固定格式格式化值时,当值是整数时,.0必须附加小数部分。

  • 以指数格式格式化时没有变化。

  • 格式化非有限双精度时没有变化,例如double.PositiveInfinity.

  • 不需要支持JsonNumberHandling选项WriteAsStringAllowReadingFromString.

  • 没有中间解析到JsonDocument.

在这种情况下,按照mjwillscomments 中的建议,您可以将 转换doubledecimal具有所需小数部分的 a,然后将其写入 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  /sf/ask/521773031/
                // 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();
}
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 之所以有效,是因为decimal(与 不同double)保留了尾随零,如文档备注中所述

  • 无条件地将 a 强制转换double为 adecimal可能会失去大值的精度,因此只需执行

    writer.WriteNumberValue(0.0m + (decimal)value);
    
    Run Code Online (Sandbox Code Playgroud)

    不建议强制使用最少的数字。(例如,序列化9999999999999992将导致9999999999999990.0而不是9999999999999992.0.)

    但是,根据维基百科页面双精度浮点格式:整数值的精度限制,从 ?2^53 到 2^53 的整数可以精确表示为double,因此可以强制转换为十进制并强制使用最少的位数用于该范围内的值。

  • 除此之外,decimal除了从一些文本表示中解析它之外,没有办法在运行时直接设置 .Net 的位数。为了提高性能,我使用Utf8FormatterUtf8Parser,但是在 .NET Core 3.0 之前的框架中,这可能会失去精度,因此string应改用常规格式和解析。有关详细信息,请参阅的代码注释Utf8JsonWriter.WriteValues.Double.cs

  • 你问, 有没有办法直接访问私有API?

    您始终可以使用反射来调用私有方法,如如何使用反射来调用私有方法中所示?,但是不建议这样做,因为内部方法可以随时更改,从而破坏您的实现。除此之外,除了将其解析为JsonDocument然后写入之外,没有公共 API 可以直接编写“原始”JSON 。我必须在使用 System.Text.JsonSerialising BigInteger 的回答中使用相同的技巧

  • 你问, 我可以直接实例化一个 JsonElement 而不需要整个解析繁琐吗?

    这在 .NET 5 中是不可能的。如其源代码所示,JsonElement结构仅包含对其父级的引用JsonDocument _parent以及指示元素在文档中的位置的位置索引。

    实际上,在 .NET 5 中,当您反序列化为JsonElementusing 时JsonSerializer.Deserialize<JsonElement>(string)JsonElementConverter会在内部将传入的 JSON 读入临时JsonDocument,克隆其RootElement,然后处理文档并返回克隆。

  • 在您的原始转换器中,f.ToString("0.0################")在使用逗号作为小数点分隔符的语言环境中将无法正常工作。您需要使用不变的语言环境:

    f.ToString("0.0################", NumberFormatInfo.InvariantInfo);
    
    Run Code Online (Sandbox Code Playgroud)
  • else所述的块double.IsFinite(value)检查的目的是序列化等非有限值double.PositiveInfinity正确。经过实验,我发现Utf8JsonWriter.WriteNumberValue(value)对于这些类型的值无条件抛出,因此必须调用序列化程序以在JsonNumberHandling.AllowNamedFloatingPointLiterals启用时正确处理它们 。

  • 的特殊情况value < MaxPreciselyRepresentedIntValue旨在通过尽可能避免对文本表示的任何往返来最大化性能。

    但是,我实际上还没有进行概要分析以确认这比执行文本往返更快。

演示小提琴here,其中包括一些单元测试,断言转换器为各种整double数值生成与 Json.NET 相同的输出,因为 Json.NET.0在序列化这些值时总是附加 a 。