首先让我展示一个问题的简单测试用例以及如何触发它
class ProtoRecurseTest
{
private int nextPayload = 1;
public int Payload { get; private set; } = 0;
public ProtoRecurseTest Back { get; private set; } = null;
public List<ProtoRecurseTest> Children { get; set; } = new List<ProtoRecurseTest>();
public ProtoRecurseTest Add()
{
ProtoRecurseTest result = new ProtoRecurseTest(this, nextPayload++);
Children.Add(result);
return result;
}
public ProtoRecurseTest()
{
}
private ProtoRecurseTest(ProtoRecurseTest parent, int payload)
{
Back = parent;
this.Payload = payload;
nextPayload = payload + 1;
}
private static void ToStringHelper(ProtoRecurseTest proto, StringBuilder sb)
{
sb.Append(proto.Payload + " -> ");
// another little hassle of protobuf due to empty list -> null deserialization
if (proto.Children != null)
{
foreach (var child in proto.Children)
ToStringHelper(child, sb);
}
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
ToStringHelper(this, sb);
return sb.ToString();
}
}
没有protobuf注释,因为这是通过编程处理的。我已经手动确保将该类以及Back和Children都添加到架构中。AsReferenceDefault=true。
当一个实例被填充到至少8个奇怪的深度时,就会发生递归触发。7就可以了。人口编码是直接的:
ProtoRecurseTest recurseTest = new ProtoRecurseTest();
ProtoRecurseTest recurseItem = recurseTest;
for (int i = 0; i < 8; i++)
recurseItem = recurseItem.Add();
然后序列化recurseTest。这种行为只在子项在列表中时发生,但在列表中,即使每个列表只有一个子项,也会发生这种行为,正如您在示例填充代码中看到的那样。当我用一个引用替换子代时,所有内容都很好地序列化了。
这是使用ProtoBuf.NET 2.1.0.0。
这里有一个解决方案,但我并不特别喜欢。这是基于@marcgravell对这个问题的回答。
class ProtoRecurseTest : ISerializationManagementCallbacks
{
private int nextPayload = 1;
public int Payload { get; private set; } = 0;
public ProtoRecurseTest Back { get; private set; } = null;
public List<ProtoRecurseTest> Children { get; set; } = new List<ProtoRecurseTest>();
public ProtoRecurseTest Add()
{
ProtoRecurseTest result = new ProtoRecurseTest(this, nextPayload++);
Children.Add(result);
return result;
}
public ProtoRecurseTest()
{
}
private ProtoRecurseTest(ProtoRecurseTest parent, int payload)
{
Back = parent;
this.Payload = payload;
nextPayload = payload + 1;
}
private static void ToStringHelper(ProtoRecurseTest proto, StringBuilder sb)
{
sb.Append(proto.Payload + " -> ");
// another little hassle of protobuf due to empty list -> null deserialization
if (proto.Children != null)
{
foreach (var child in proto.Children)
ToStringHelper(child, sb);
}
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
ToStringHelper(this, sb);
return sb.ToString();
}
static void PreSerializationHelper(ProtoRecurseTest instance)
{
instance.Back = null;
foreach (var child in instance.Children)
PreSerializationHelper(child);
}
public void BeforeSerialization()
{
PreSerializationHelper(this);
}
static void PostDeserializationHelper(ProtoRecurseTest instance, ProtoRecurseTest parent)
{
if (instance.Children == null)
instance.Children = new List<ProtoRecurseTest>();
instance.Back = parent;
foreach (var child in instance.Children)
PostDeserializationHelper(child, instance);
}
public void AfterDeserialization()
{
PostDeserializationHelper(this, null);
}
}
现在,对序列化/反序列化的调用只需在执行业务之前检查是否可以将该类型强制转换为ISerializationManagementCallbacks(它只是提供了BeforeSerialization和AfterDeserialization方法),然后调用适当的方法(如果是的话)。它运行良好。
然而,我并不喜欢在我的代码库中混合更多的序列化问题——这就是为什么模式是通过编程生成的。理想情况下,我想完全分离序列化,但空列表->null问题(这并不是说它本身是一个"问题",而是protobuf标准中不受欢迎的部分)已经让这变得不可能了,但这将是另一个更深奥的问题,我确实认为这可能确实是一个问题。