使用 C# 中的 Newtonsoft 从 Json 高效手动反序列化嵌套对象

Efficient Manual Deserialization of Nested objects from Json using Newtonsoft in c#

我有这个网格,我想尽快从 json 读取。

[Serializable]
public class Mesh
{
    public int[] Faces { get; set; }
    public Vec3[] Vertices { get; set; }
}

[Serializable]
public class Vec3
{
    public double X { get; set; }
    public double Y { get; set; }
    public double Z { get; set; }
}

我读到创建手动序列化器和反序列化器是 much faster than using reflection。所以我尝试创建自己的:

public static Mesh FromJson(JsonTextReader reader)
{
    var mesh = new Mesh();
    var currentProperty = string.Empty;
    List<int> faces = new List<int>();
    List<Vec3> Vertices = new List<Vec3>();
    double X = 0, Y = 0, Z = 0;
    while (reader.Read())
    {
        if (reader.Value != null)
        {
            if (reader.TokenType == JsonToken.PropertyName)
                currentProperty = reader.Value.ToString();

            else if (reader.TokenType == JsonToken.Integer && currentProperty == "Faces")
                faces.Add(Int32.Parse(reader.Value.ToString()));

            else if (reader.TokenType == JsonToken.Float && currentProperty == "X")
            {
                X = float.Parse(reader.Value.ToString());
            }

            else if (reader.TokenType == JsonToken.Float && currentProperty == "Y")
            {
                Y = float.Parse(reader.Value.ToString());
            }

            else if (reader.TokenType == JsonToken.Float && currentProperty == "Z")
            {
                Z = float.Parse(reader.Value.ToString());
            }
        }
        else
        {
            //Console.WriteLine("Token: {0}", reader.TokenType);

            if (reader.TokenType == JsonToken.EndObject && reader.Path.Contains("Vertices"))
                Vertices.Add(new Vec3 { X = X, Y = Y, Z = Z });

        }
    }

    mesh.Faces = faces.ToArray();
    mesh.Vertices = Vertices.ToArray();
    return mesh;
}

尽管我在写作时取得了 30% 的进步(期待更多的 TBH),但阅读给我带来了麻烦。我觉得这与 Vec3 的嵌套有关,因为如果我 运行 有类似的逻辑但没有 Vec3 我会得到不错的结果 ~30%。我能找到的唯一例子是处理没有嵌套的简单数据结构,所以我觉得我在这里处理它有点简单。

将自动反序列化替换为手动反序列化时,提速 30% 并不意外。 Json.NET 所以反射的开销并不像您想象的那么糟糕。为给定类型构建合约会有一次性惩罚,但如果你反序列化一个大文件,那么惩罚会被摊销,由此产生的合约为快速获取和设置 属性 值提供委托。

也就是说,我发现您的代码存在以下问题(错误和可能的优化):

  1. 当您通读 JSON 时,您并没有跟踪解析状态。这要求您执行 reader.Path.Contains("Vertices") ,但性能不佳。当 JSON 数据与预期不符时,它还会使您的代码容易受到意外行为的影响。

  2. 您正在检查 currentProperty 的字符串相等性,但是如果您要将所有预期的 属性 名称添加到 DefaultJsonNameTable and set it at JsonTextReader.PropertyNameTable,您将能够用引用相等性检查替换这些检查,节省一些时间和一些内存分配。

    请注意,在 Json.NET 12.0.1 中添加了 PropertyNameTable。

  3. XYZ 是双精度数,但您将它们 解析为浮点数 :

    X = float.Parse(reader.Value.ToString());
    

    这是一个会导致精度损失的错误。更重要的是,您在 当前文化 (可能有本地化的小数点分隔符)而不是不变文化中解析它们,这是另一个错误。

  4. 在任何情况下都不需要将 reader.Value 解析为 double,因为它已经应该是 double。在整数值的情况下,它应该已经是 long。简单地转换为所需的原始类型应该就足够了,而且速度更快。

  5. 禁用 automatic date recognition 可能会节省您一些时间。

FromJson() 的以下版本解决了这些问题:

public static partial class MeshExtensions
{
    const string Vertices = "Vertices";
    const string Faces = "Faces";
    const string X = "X";
    const string Y = "Y";
    const string Z = "Z";

    public static Mesh FromJson(JsonTextReader reader)
    {
        var nameTable = new DefaultJsonNameTable();
        nameTable.Add(Vertices);
        nameTable.Add(Faces);
        nameTable.Add(X);
        nameTable.Add(Y);
        nameTable.Add(Z);
        reader.PropertyNameTable = nameTable;  // For performance
        reader.DateParseHandling = DateParseHandling.None;  // Possibly for performance.

        bool verticesFound = false;
        List<Vec3> vertices = null;
        bool facesFound = false;
        List<int> faces = null;

        while (reader.ReadToContent())
        {
            if (reader.TokenType == JsonToken.PropertyName && reader.Value == (object)Vertices)
            {
                if (verticesFound)
                    throw new JsonSerializationException("Multiple vertices");
                reader.ReadToContentAndAssert(); // Advance past the property name
                vertices = ReadVertices(reader); // Read the vertices array
                verticesFound = true;
            }
            else if (reader.TokenType == JsonToken.PropertyName && reader.Value == (object)Faces)
            {
                if (facesFound)
                    throw new JsonSerializationException("Multiple faces");
                reader.ReadToContentAndAssert(); // Advance past the property name
                faces = reader.ReadIntArray(); // Read the vertices array
                facesFound = true;
            }
        }

        return new Mesh
        {
            Vertices = vertices == null ? null : vertices.ToArray(),
            Faces = faces == null ? null : faces.ToArray(),
        };
    }

    static List<Vec3> ReadVertices(JsonTextReader reader)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;
        else if (reader.TokenType != JsonToken.StartArray)
            throw new JsonSerializationException(string.Format("Unexpected token type {0}", reader.TokenType));
        var vertices = new List<Vec3>();
        while (reader.ReadToContent())
        {
            switch (reader.TokenType)
            {
                case JsonToken.EndArray:
                    return vertices;

                case JsonToken.Null:
                    // Or throw an exception if you prefer.
                    //throw new JsonSerializationException(string.Format("Unexpected token type {0}", reader.TokenType));
                    vertices.Add(null);
                    break;

                case JsonToken.StartObject:
                    var vertex = ReadVertex(reader);
                    vertices.Add(vertex);
                    break;

                default:
                    // reader.Skip();
                    throw new JsonSerializationException(string.Format("Unexpected token type {0}", reader.TokenType));
            }
        }
        throw new JsonReaderException(); // Truncated file.
    }

    static Vec3 ReadVertex(JsonTextReader reader)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;
        else if (reader.TokenType != JsonToken.StartObject)
            throw new JsonException();
        var vec = new Vec3();
        while (reader.ReadToContent())
        {
            switch (reader.TokenType)
            {
                case JsonToken.EndObject:
                    return vec;

                case JsonToken.PropertyName:
                    if (reader.Value == (object)X)
                        vec.X = reader.ReadAsDouble().Value;
                    else if (reader.Value == (object)Y)
                        vec.Y = reader.ReadAsDouble().Value;
                    else if (reader.Value == (object)Z)
                        vec.Z = reader.ReadAsDouble().Value;
                    else // Skip unknown property names and values.
                        reader.ReadToContentAndAssert().Skip();
                    break;

                default:
                    throw new JsonSerializationException(string.Format("Unexpected token type {0}", reader.TokenType));
            }
        }
        throw new JsonReaderException(); // Truncated file.
    }
}

public static class JsonExtensions
{
    public static List<int> ReadIntArray(this JsonReader reader)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;
        else if (reader.TokenType != JsonToken.StartArray)
            throw new JsonReaderException(string.Format("Unexpected token type {0}", reader.TokenType));

        var list = new List<int>();
        // ReadAsInt32() reads the next token as an integer, skipping comments
        for (var value = reader.ReadAsInt32(); true; value = reader.ReadAsInt32())
        {
            if (value != null)
                list.Add(value.Value);
            else 
                // value can be null if we reached the end of the array, encountered a null value, or encountered the end of a truncated file.
                // JsonReader will throw an exception on most types of malformed file, but not on a truncated file.
                switch (reader.TokenType)
                {
                    case JsonToken.EndArray:
                        return list;
                    case JsonToken.Null:
                    default:
                        throw new JsonReaderException(string.Format("Unexpected token type {0}", reader.TokenType));
                }
        }
    }

    public static bool ReadToContent(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            return false;
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            if (!reader.Read())
                return false;
        return true;
    }

    public static JsonReader ReadToContentAndAssert(this JsonReader reader)
    {
        return reader.ReadAndAssert().MoveToContentAndAssert();
    }

    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

演示 fiddle here。如您所见,正确处理注释、意外属性和截断流等边界条件会使编写健壮的手动反序列化代码变得棘手。