【问题标题】:Can I force Json.NET to start tree deserialization from root我可以强制 Json.NET 从根目录开始树反序列化吗
【发布时间】:2020-07-13 17:39:26
【问题描述】:

这是我第一次尝试序列化/反序列化树。除了Path,它工作得很好。它必须从根开始构建,但反序列化从叶子开始。

    var root = new Node(null, "rootName");
    var tree = new Tree(root);
    root.AddChild("childName");
    var str = JsonConvert.SerializeObject(tree, Newtonsoft.Json.Formatting.Indented);
    var treeRestored = JsonConvert.DeserializeObject<Tree>(str);

...

class Node
{
    public IReadOnlyList<Node> Children => _children;

    [JsonIgnore]
    public string Path { get; } // needs parent

    [JsonProperty(ReferenceLoopHandling = ReferenceLoopHandling.Ignore)]
    public Node Parent { get; private set; }

    public string Name { get; }

    public Node(Node parent, string name)
    {
        Name = name;
        Parent = parent;
        Path = (parent == null ? "" : (parent.Name + ".")) + name;
        _children = new List<Node>();
    }

    [JsonConstructor]
    private Node(string Name, List<Node> Children)
    {
        this.Name = Name;
        _children = Children;
        foreach (var child in _children)
        {
            child.Parent = this;
        }
    }

    public void AddChild(string name)
    {
        _children.Add(new Node(this, name));
    }

    private readonly List<Node> _children;
}

class Tree
{
    public Node Root;

    public Tree(Node root)
    {
        Root = root;
    }
}

所以我尝试了以下方法。我从 Parent 属性中删除了 JsonProperty,更改了序列化命令,并更改了 JSON 构造函数。

    var str = JsonConvert.SerializeObject(tree, Newtonsoft.Json.Formatting.Indented,
        new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects });

    [JsonConstructor]
    private Node(string name, Node parent)
    {
        Name = name;
        Parent = parent;
        if (Parent != null) // Rebuild path
        {
            parent._children.Add(this);
            Path = parent.Name + "." + Name;
        }
        else
        {
            Path = Name;
        }
        _children = new List<Node>();
    }

进行这些更改后,序列化字符串如下所示:

{
  "$id": "1",
  "Root": {
    "$id": "2",
    "Children": [
      {
        "$id": "3",
        "Children": [],
        "Parent": {
          "$ref": "2"
        },
        "Name": "childName"
      }
    ],
    "Parent": null,
    "Name": "rootName"
  }
}

这个 Json 字符串包含足够的信息来首先创建根,然后是子。但是 JsonConverter 仍然从叶子开始。如何更改订单?

我知道我可以使用OnDeserialized,但这是我最后的选择。

【问题讨论】:

  • 好吧,我不明白你的问题是什么,我认为你应该审查并重新表述你的问题,以便更清楚。
  • @Aybe 我想从根目录反序列化树。我提供了一个示例问题。
  • 你的意思是你想从 X root 反序列化?如果是这样,那么根本不要使用Tree 类,而是直接序列化你的根节点,这就是我在这里所做的并且它有效。例如JsonConvert.Deserialize&lt;Node&gt;...
  • @Aybe 它对我不起作用。也许您可以以我的代码为基础创建实时示例?

标签: c# tree json.net tree-traversal


【解决方案1】:

我认为部分问题在于您尝试使用带有PerserveReferencesHandling.Objects 的参数化构造函数,而documented 不起作用:

注意:
当通过非默认构造函数设置值时,无法保留引用。对于非默认构造函数,必须在父值之前创建子值,以便将它们传递给构造函数,从而无法跟踪引用。 ISerializable 类型是一个类的示例,其值使用非默认构造函数填充,不适用于 PreserveReferencesHandling

我会这样做:

  1. 从私有[JsonConstructor] 中删除参数,并使该构造函数简单地将子项初始化为一个空列表。

    [JsonConstructor]
    private Node()
    {
        _children = new List<Node>();
    }
    
  2. NameParent 添加私有设置器并用[JsonProperty] 标记它们,以便Json.Net 可以使用私有设置器:

    [JsonProperty]  // allow Json.Net to use private setter
    public Node Parent { get; private set; }
    
    [JsonProperty]  // allow Json.Net to use private setter
    public string Name { get; private set; }
    
  3. 不要将Path 存储在您的对象中;而是按需计算。这只是一行代码,而且速度很快,因为它向上链。

    [JsonIgnore]
    public string Path
    {
        get { return Parent != null ? Parent.Path + "." + Name : Name; }
    }
    
  4. (可选,但推荐)让你的AddChild 方法返回它创建的Node——这将使它在创建树时更容易使用。否则,您总是必须搜索子节点列表才能找到刚刚添加的节点。

    public Node AddChild(string name)
    {
        Node node = new Node(this, name);
        _children.Add(node);
        return node;
    }
    

这是您的班级在进行所有更改后的样子:

class Node
{
    public IReadOnlyList<Node> Children => _children;

    [JsonIgnore]
    public string Path
    {
        get { return Parent != null ? Parent.Path + "." + Name : Name; }
    }

    [JsonProperty]  // allow Json.Net to use private setter
    public Node Parent { get; private set; }

    [JsonProperty]  // allow Json.Net to use private setter
    public string Name { get; private set; }

    public Node(Node parent, string name)
    {
        Name = name;
        Parent = parent;
        _children = new List<Node>();
    }

    [JsonConstructor]
    private Node()
    {
        _children = new List<Node>();
    }

    public Node AddChild(string name)
    {
        Node node = new Node(this, name);
        _children.Add(node);
        return node;
    }

    private readonly List<Node> _children;
}

如果即时计算Path 过于昂贵,您可以尝试以下想法:

  1. 使用惰性求值。第一次访问时计算路径,然后将结果缓存到本地,方便后续检索:

     private string _path;
    
     [JsonIgnore]
     public string Path
     {
         get 
         { 
             if (_path == null)
             {
                 _path = Parent != null ? Parent.Path + "." + Name : Name;
             }
             return _path; 
         }
     }
    
  2. 反序列化后立即遍历树并为每个节点请求Path,强制计算并缓存它。然后会为每个节点缓存。

     void PrecachePaths(Node node)
     {
         var path = node.Path;
         foreach (Node child in node.Children)
         {
             PrecachePaths(child);
         }
     }
    
     var treeRestored = JsonConvert.DeserializeObject<Tree>(str);
     PrecachePaths(treeRestored.Root);
    

【讨论】:

  • 我无法按需计算Path。在我的真实程序中计算属性非常昂贵。
  • 是什么让它如此昂贵?您真的对此进行了基准测试吗?
  • 太糟糕了,这个限制太糟糕了而且不合理。但这意味着无论如何我都不能以一种好的方式做到这一点。我试图避免私人二传手。感谢您向我指出这一点。
  • 你知道吗,我不确定你是否正确理解了这句话。对孩子来说,这可能意味着不同的东西。因为它保留了引用并愉快地调用了我的非默认缺点。
  • 但是如果结合缓存,另一种动态计算方法是可行的。
猜你喜欢
  • 2011-12-30
  • 2023-04-07
  • 2018-06-21
  • 2011-03-26
  • 1970-01-01
  • 2023-04-01
  • 2020-07-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多