当我尝试序列化具有类似结构的对象时,我的C#程序正在运行StackOverflowException:
对象具有互相引用的成员
无法尝试捕获(idk为什么)
如果计数设置为6500以下(可能因计算机而异),则表示已成功序列化
下面的示例代码:
class Chacha { public Chacha NextChacha { get; set; } } public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All, PreserveReferencesHandling = PreserveReferencesHandling.Objects, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; static void Main(string[] args) { int count = 15000; Chacha[] steps = new Chacha[count]; steps[0] = new Chacha(); for (int i = 1; i < count; i++) { steps[i] = new Chacha(); steps[i-1].NextChacha = steps[i]; } string serSteps = JsonConvert.SerializeObject(steps, Settings); }
JSON.NET版本是:9.0.1
.NET Framework:4.5.2有什么
解决方案如何序列化此结构?
欢迎任何帮助或建议。谢谢
出现stackoverflow异常的原因是Json.NET是一个递归的单遍树或图序列化程序,PreserveReferencesHandling.Objects
启用该功能后,它将始终序列化每个对象的第一次出现。您已经构造了15,000个元素Chacha []
数组,以便第一个条目是包含顺序连接的所有其他项目的链接列表的开头。Json.NET将尝试通过15,000个递归级别将其序列化为嵌套的JSON对象(深度为15,000个级别),从而使过程中的堆栈溢出。
因此,您需要做的是将整个链接表仅写在列表的开头,作为JSON数组。但是,不幸的是,Json.NET还是基于契约的序列化器,这意味着无论嵌套深度是多少,只要遇到给定类型的对象,Json.NET都将尝试写入相同的属性。因此,向对象添加Chacha[] NextChachaList
属性Chacha
无济于事,因为它将在每个级别写入。取而代之的是,有必要创建一个相当复杂的定制JsonConverter
,该定制以线程安全的方式跟踪序列化深度,并且仅在顶层写入链接列表。以下是窍门:
class ChachaConverter : LinkedListItemConverter{ protected override bool IsNextItemProperty(JsonProperty member) { return member.UnderlyingName == "NextChacha"; // Use nameof(Chacha.NextChacha) in latest c# } } public abstract class LinkedListItemConverter : JsonConverter where T : class { const string refProperty = "$ref"; const string idProperty = "$id"; const string NextItemListProperty = "nextItemList"; [ThreadStatic] static int level; // Increments the nesting level in a thread-safe manner. int Level { get { return level; } set { level = value; } } public override bool CanConvert(Type objectType) { return typeof(T).IsAssignableFrom(objectType); } protected abstract bool IsNextItemProperty(JsonProperty member); List GetNextItemList(object value, JsonObjectContract contract) { var property = contract.Properties.Where(p => IsNextItemProperty(p)).Single(); List list = null; for (var item = (T)property.ValueProvider.GetValue(value); item != null; item = (T)property.ValueProvider.GetValue(item)) { if (list == null) list = new List (); list.Add(item); } return list; } void SetNextItemLinks(object value, List list, JsonObjectContract contract) { var property = contract.Properties.Where(p => IsNextItemProperty(p)).Single(); if (list == null || list.Count == 0) return; var previous = value; foreach (var next in list) { if (next == null) continue; property.ValueProvider.SetValue(previous, next); previous = next; } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { using (new PushValue (Level + 1, () => Level, (old) => Level = old)) { writer.WriteStartObject(); if (serializer.ReferenceResolver.IsReferenced(serializer, value)) { writer.WritePropertyName(refProperty); writer.WriteValue(serializer.ReferenceResolver.GetReference(serializer, value)); } else { writer.WritePropertyName(idProperty); writer.WriteValue(serializer.ReferenceResolver.GetReference(serializer, value)); var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType()); // Write the data properties (if any). foreach (var property in contract.Properties .Where(p => p.Readable && !p.Ignored && (p.ShouldSerialize == null || p.ShouldSerialize(value)))) { if (IsNextItemProperty(property)) continue; var propertyValue = property.ValueProvider.GetValue(value); if (propertyValue == null && serializer.NullValueHandling == NullValueHandling.Ignore) continue; writer.WritePropertyName(property.PropertyName); serializer.Serialize(writer, propertyValue); } if (Level == 1) { // Write the NextItemList ONLY AT THE TOP LEVEL var nextItems = GetNextItemList(value, contract); if (nextItems != null) { writer.WritePropertyName(NextItemListProperty); serializer.Serialize(writer, nextItems); } } } writer.WriteEndObject(); } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; var jObject = JObject.Load(reader); // Detach and process $ref var refValue = (string)jObject[refProperty].RemoveFromLowestPossibleParent(); if (refValue != null) { var reference = serializer.ReferenceResolver.ResolveReference(serializer, refValue); if (reference != null) return reference; } // Construct the value var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(existingValue == null ? typeof(T) : existingValue.GetType()); T value = (existingValue as T ?? (T)contract.DefaultCreator()); // Detach and process $id var idValue = (string)jObject[idProperty].RemoveFromLowestPossibleParent(); if (idValue != null) { serializer.ReferenceResolver.AddReference(serializer, idValue, value); } // Detach the (possibly large) list of next items. var nextItemList = jObject[NextItemListProperty].RemoveFromLowestPossibleParent(); // populate the data properties (if any) serializer.Populate(jObject.CreateReader(), value); // Set the next item references if (nextItemList != null) { var list = nextItemList.ToObject >(serializer); SetNextItemLinks(value, list, contract); } return value; } } public struct PushValue
: IDisposable { Action setValue; T oldValue; public PushValue(T value, Func getValue, Action setValue) { if (getValue == null || setValue == null) throw new ArgumentNullException(); this.setValue = setValue; this.oldValue = getValue(); setValue(value); } #region IDisposable Members // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class. public void Dispose() { if (setValue != null) setValue(oldValue); } #endregion } public static class JsonExtensions { public static JToken RemoveFromLowestPossibleParent(this JToken node) { if (node == null) return null; var contained = node.AncestorsAndSelf().Where(t => t.Parent is JContainer && t.Parent.Type != JTokenType.Property).FirstOrDefault(); if (contained != null) contained.Remove(); // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should if (node.Parent is JProperty) ((JProperty)node.Parent).Value = null; return node; } }
然后,给定稍微修改的类Chacha
:
class Chacha { public Chacha NextChacha { get; set; } public long Data { get; set; } }
为3个项目的数组生成以下JSON:
{ "$type": "Question41828014.Chacha[], Tile", "$values": [ { "$id": "1", "Data": 0, "nextItemList": { "$type": "System.Collections.Generic.List`1[[Question41828014.Chacha, Tile]], mscorlib", "$values": [ { "$id": "2", "Data": 1 }, { "$id": "3", "Data": 2 } ] } }, { "$ref": "2" }, { "$ref": "3" } ] }
注意,JSON深度现在受到严格限制。小提琴的例子。
请注意,为类型指定自定义转换器后,它需要手动完成所有操作。如果您的类型Chacha
是多态的,并且需要读取和写入"$type"
属性,则需要自己将该逻辑添加到转换器中。
顺便说一句,我建议使用TypeNameHandling.Objects
而不是TypeNameHandling.All
。可以在JSON中合理地指定对象类型(只要正确清理了类型),但是应该在代码中指定集合类型。这样做可以从数组切换到,List
而不必重新读取旧版JSON文件。