循环引用PreserveReferencesHandling 属性(NewtonSoft)

76 阅读3分钟

以下通过具体代码示例和序列化结果,详细说明 PreserveReferencesHandling 各选项的行为差异。

示例背景

我们定义两个类,模拟对象间的循环引用和数组的重复引用:

using Newtonsoft.Json;

using System;

// 模拟对象循环引用:A 引用 B,B 引用 A

public class Person {

public string Name { get; set; }

public Person Friend { get; set; } // 指向另一个 Person 实例(可能形成循环)

}

// 模拟数组重复引用:两个 Team 实例共享同一个 Members 数组

public class Team {

public string TeamName { get; set; }

public Person[] Members { get; set; } // 可能被多个 Team 引用

}

1. PreserveReferencesHandling.None(默认)

行为:不保留引用信息,直接序列化对象内容。若存在循环引用,会抛出异常;若同一对象被多次引用,反序列化后生成独立实例。

示例代码

// 构造循环引用对象:A 的 Friend 是 B,B 的 Friend 是 A

Person personA = new Person { Name = "Alice" };

Person personB = new Person { Name = "Bob", Friend = personA };

personA.Friend = personB; // 形成循环

try {

string json = JsonConvert.SerializeObject(

personA,

Formatting.Indented,

new JsonSerializerSettings {

PreserveReferencesHandling = PreserveReferencesHandling.None

}

);

Console.WriteLine("序列化结果(无循环时):");

Console.WriteLine(json);

}

catch (JsonSerializationException ex) {

Console.WriteLine($"序列化失败(循环引用):{ex.Message}");

}

输出结果

序列化失败(循环引用):检测到循环引用对象图。可能的解决方法是在 JsonSerializerSettings 中设置 ReferenceLoopHandling。路径 'Friend.Friend'。

结论

  • 若对象图存在循环引用(如本例中的 personA 和 personB 互相引用),None 模式会直接抛出异常。
  • 若对象无循环但被多次引用(例如两个属性指向同一对象),序列化时会生成两个独立的 JSON 对象,反序列化后这两个属性会指向不同的实例(丢失原始引用关系)。

2. PreserveReferencesHandling.Objects

行为:保留普通对象(非数组 / 集合)的引用信息。首次出现的对象标记 id,后续引用标记id,后续引用标记 ref,反序列化时重建引用关系。

示例代码

// 构造循环引用对象(同上)

Person personA = new Person { Name = "Alice" };

Person personB = new Person { Name = "Bob", Friend = personA };

personA.Friend = personB;

string json = JsonConvert.SerializeObject(

personA,

Formatting.Indented,

new JsonSerializerSettings {

PreserveReferencesHandling = PreserveReferencesHandling.Objects

}

);

Console.WriteLine("序列化结果:");

Console.WriteLine(json);

// 反序列化验证引用关系

Person deserializedA = JsonConvert.DeserializeObject<Person>(json);

Console.WriteLine($"\n反序列化后,A 的 Friend 是否是 B? {deserializedA.Friend.Name == "Bob"}");

Console.WriteLine($"B 的 Friend 是否是 A? {deserializedA.Friend.Friend.Name == "Alice"}");

序列化结果

{

"$id": "1",

"Name": "Alice",

"Friend": {

"$id": "2",

"Name": "Bob",

"Friend": {

"$ref": "1"

}

}

}

反序列化输出

反序列化后,A 的 Friend 是否是 B? True

B 的 Friend 是否是 A? True

结论

  • id标记首次出现的对象(如personAid 标记首次出现的对象(如 personA 的 id:1,personB 的 $id:2)。
  • ref标记对已有对象的引用(如personB.Friend指向ref 标记对已有对象的引用(如 personB.Friend 指向 ref:1,即 personA)。
  • 反序列化后,对象间的循环引用关系被完整保留。

3. PreserveReferencesHandling.Arrays

行为:保留数组 / 集合的引用信息。首次出现的数组标记 id,后续引用标记id,后续引用标记 ref,普通对象的引用不处理。

示例代码

// 构造重复引用的数组:两个 Team 共享同一个 Members 数组

Person[] members = new Person[] {

new Person { Name = "Alice" },

new Person { Name = "Bob" }

};

Team team1 = new Team { TeamName = "Team1", Members = members };

Team team2 = new Team { TeamName = "Team2", Members = members }; // 与 team1 共享 members 数组

string json = JsonConvert.SerializeObject(

new { team1, team2 }, // 序列化包含两个 Team 的匿名对象

Formatting.Indented,

new JsonSerializerSettings {

PreserveReferencesHandling = PreserveReferencesHandling.Arrays

}

);

Console.WriteLine("序列化结果:");

Console.WriteLine(json);

// 反序列化验证数组引用关系

var result = JsonConvert.DeserializeObject<dynamic>(json);

bool isSameArray = result.team1.Members == result.team2.Members; // 检查是否指向同一数组

Console.WriteLine($"\n反序列化后,两个 Team 的 Members 是否是同一数组? {isSameArray}");

序列化结果

{

"$id": "1",

"team1": {

"$id": "2",

"TeamName": "Team1",

"Members": {

"$id": "3",

"$values": [

{

"$id": "4",

"Name": "Alice",

"Friend": null

},

{

"$id": "5",

"Name": "Bob",

"Friend": null

}

]

}

},

"team2": {

"$id": "6",

"TeamName": "Team2",

"Members": {

"$ref": "3"

}

}

}

反序列化输出

反序列化后,两个 Team 的 Members 是否是同一数组? True

结论

  • 数组 members 被标记为 id:3team2.Members通过id:3,team2.Members 通过 ref:3 引用该数组。
  • 普通对象(如 Person 实例)的引用未被处理(id:4id:4 和 id:5 是独立对象)。

4. PreserveReferencesHandling.All

行为:同时保留对象和数组的引用信息(等价于 Objects | Arrays)。所有首次出现的对象(包括数组)标记 id,后续引用标记id,后续引用标记 ref。

示例代码

// 构造同时包含对象循环和数组重复引用的场景

Person personA = new Person { Name = "Alice" };

Person personB = new Person { Name = "Bob", Friend = personA };

personA.Friend = personB; // 对象循环

Person[] members = new Person[] { personA, personB }; // 数组包含循环对象

Team team1 = new Team { TeamName = "Team1", Members = members };

Team team2 = new Team { TeamName = "Team2", Members = members }; // 数组重复引用

string json = JsonConvert.SerializeObject(

new { team1, team2 },

Formatting.Indented,

new JsonSerializerSettings {

PreserveReferencesHandling = PreserveReferencesHandling.All

}

);

Console.WriteLine("序列化结果:");

Console.WriteLine(json);

// 反序列化验证所有引用关系

var result = JsonConvert.DeserializeObject<dynamic>(json);

bool isSameArray = result.team1.Members == result.team2.Members;

bool isFriendLoop = result.team1.Members[0].Friend.Name == "Bob"

&& result.team1.Members[1].Friend.Name == "Alice";

Console.WriteLine($"\n数组引用是否一致? {isSameArray}");

Console.WriteLine($"对象循环是否保留? {isFriendLoop}");

序列化结果(关键部分)

{

"$id": "1",

"team1": {

"$id": "2",

"TeamName": "Team1",

"Members": {

"$id": "3",

"$values": [

{

"$id": "4",

"Name": "Alice",

"Friend": {

"$id": "5",

"Name": "Bob",

"Friend": {

"$ref": "4"

}

}

},

{

"$ref": "5"

}

]

}

},

"team2": {

"$id": "6",

"TeamName": "Team2",

"Members": {

"$ref": "3"

}

}

}

反序列化输出

数组引用是否一致? True

对象循环是否保留? True

结论

  • 数组(id:3)和对象(personAid:3)和对象(personA 的 id:4,personB 的 $id:5)的引用均被保留。
  • 反序列化后,数组的重复引用和对象的循环引用均被完整重建。

总结表格

选项

处理对象类型

循环引用处理

重复引用处理

适用场景

None

抛出异常

生成独立实例(丢失引用)

简单对象图(无循环 / 重复)

Objects

普通对象

保留循环关系

保留对象引用(数组不处理)

对象间循环 / 重复引用

Arrays

数组 / 集合

不处理对象循环(仍抛异常)

保留数组引用(对象不处理)

数组 / 集合的重复引用

All

对象 + 数组 / 集合

保留循环关系

保留所有引用(对象 + 数组)

复杂对象图(混合循环 + 重复)

参考:AI豆包