JObject失去了引用。

3

我有两个场景来展示这个问题。

场景一

using System;
using Newtonsoft.Json.Linq;

public class Program
{
    public static void Main()
    {
        var arr = new JArray();
        arr.Add("apple");
        var obj = new JObject();
        obj["arr"] = arr;
        obj["arr"] = arr;       
        arr.Add("mango");
        
        foreach(var a in obj["arr"]){
            Console.WriteLine(a);
        }
        
                            
    }
}

这里的 obj["array"] 应该引用之前初始化的 arr。因此输出应该是:

apple
mango

但是输出结果是

apple

方案二

using System;
using Newtonsoft.Json.Linq;

public class Program
{
    public static void Main()
    {
        var arr = new JArray();
        arr.Add("apple");
        var obj = new JObject();
        obj["arr"] = arr;
        
        var obj2 = new JObject();
        obj2["arr"] = arr;      
        
        arr.Add("mango");
        
        foreach(var a in obj2["arr"]){
            Console.WriteLine(a);
        }
    }
}

同样地,obj2["arr"]应该引用arr,但它没有。因此,期望的输出为:
apple
mango

但是输出结果是什么?
apple

我不太擅长csharp,请告诉我是否漏掉了什么。

编辑

根据@Wyck在评论中提到的另一个场景,添加如下情况。

情景3

using System;
using Newtonsoft.Json.Linq;

public class Program
{
    public static void Main()
    {
        var arr = new JArray();
        Console.WriteLine(arr.GetHashCode());
        arr.Add("apple");
        var obj = new JObject();
        obj["arr"] = arr;
        Console.WriteLine(obj["arr"].GetHashCode());
        obj["arr"] = arr;   
        Console.WriteLine(obj["arr"].GetHashCode());
        obj["arr"] = arr;   
        Console.WriteLine(obj["arr"].GetHashCode());
        
        arr.Add("mango");
        
        foreach(var a in obj["arr"]){
            Console.WriteLine(a);
        }
        
                            
    }
}

重复执行赋值 obj["arr"] = arr 奇数次会得到原始引用,但偶数次不会。
输出结果将是:
10465620
10465620
1190878
10465620
apple
mango

观察到偶数赋值后哈希码发生了变化,但是奇数赋值后哈希码又恢复原样。


@Wyck 是的,有点奇怪,但是为什么它失败了让我感到困惑,对我来说似乎是个bug!请看我在Krik答案下的评论。 - Ghasan غسان
1
是的,这是正确的。关于原因,请参见nested json objects dont update / inherit using Json.NETJArray.Remove(JToken) does not delete - dbc
可能是“正确”的,但肯定是“令人惊讶的”! - Wyck
@Wyck - 我相信LINQ-to-XML的工作方式也类似,因为父/子图形在那里也是双向连接的。 - dbc
@dbc - 我已更新我的问题,以展示奇数和偶数数字分配的情况。 - ashutosh
显示剩余3条评论
2个回答

2
如果您查看Newtonsoft.Json的源代码,您会发现在将数组分配给属性时,它会创建一个副本:
public JProperty(string name, object? content)
{
    ...
    Value = IsMultiContent(content)
        ? new JArray(content)
        : CreateFromContent(content);
}

JObject中相关的部分在这里

您可以通过获取obj2["arr"]arr(均为类型JArray)的哈希码(.GetHashCode()),并观察它们的值会不同,从而轻松地在代码中进行测试。

因此,为了能够添加到数组中,您需要在属性被分配后通过访问JObject实例来访问它,或者每次添加元素时重新将数组分配给该属性。


我也这么想,但是有点不对劲。如果你只分配一次,它会通过引用原始数组(而不是通过 JObject)工作,但是,如果你分配两次(无论出于什么原因),它似乎会创建一个副本。然而,如果你分配其他东西,比如 arr2,然后再次分配,它就可以工作了!可以工作obj["arr"] = arr; obj["arr"] = arr2; obj["arr"] = arr; 可以工作obj["arr"] = arr; 不能工作!obj["arr"] = arr; obj["arr"] = arr; - Ghasan غسان
更奇怪的是,如果您执行 obj["arr"] = arr 奇数次,它会起作用。如果您执行偶数次,则会得到其他对象。(尝试执行3、4、5或6次!) - Wyck
1
@Ghasanغسان - 你的观察是正确的。一个JToken只能有一个父级,因此只有在尝试添加已经有父级的令牌时才需要克隆。 - dbc

0
这个答案完全基于 @dbc 的评论。这种令人惊讶的行为是由 Json.net 实现方式造成的。原始答案在 这里
所有的 JToken 都需要有一个 Parent 属性,而且只能有一个 Parent
obj["arr"] = arr;

在设置arr之前,首先验证arr是否已经有了Parent。如果Parentnull,那么arr将被分配未修改的值,否则将克隆arr并将克隆的arr分配给Parent。这就是场景1和场景2行为的原因。

using System;
using Newtonsoft.Json.Linq;

public class Program
{
    public static void Main()
    {
        var arr = new JArray();
        
        arr.Add("apple");
        var obj = new JObject();
        obj["arr"] = arr;
        Console.WriteLine(arr.Parent == null ? "null" : arr.Parent.GetHashCode().ToString());
        obj["arr"] = arr;
        Console.WriteLine(arr.Parent == null ? "null" : arr.Parent.GetHashCode().ToString());
        obj["arr"] = arr;   
        Console.WriteLine(arr.Parent == null ? "null" : arr.Parent.GetHashCode().ToString());
        
        arr.Add("mango");
        
        foreach(var a in obj["arr"]){
            Console.WriteLine(a);
        }
        
                            
    }
}

这段代码块的输出将会是:

10566368
null
10566368
apple
mango

可以看到,在偶数次赋值后,arrParent被设置为null
在第一次赋值后,arr.Parent被设置(包含Name和其ValueJProperty,属于JOject)。在第二次赋值时,由于arr已经有了一个Parent,因此将克隆arr并将克隆值设置为obj["arr"]。由于设置了新的JToken,前一个JTokenParent将被设置为null,即arr.Parent将变为null
再次进行第三次赋值时,arr.Parentnull。因此,它将像第一次赋值一样被设置。
对于情况2,arr已经有了一个Parent,因此在第二次赋值时,即obj2["arr"] = arr;,将克隆并设置arr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接