有没有办法通过不同类型的 ProtoBuf 字段进行递归?
Is there a way to be recursive through different types of ProtoBuf fields?
我正在尝试在代码库中的 protobuf messages that would work along all the IMessage 个对象之间创建差异(据我所知,每个 protobuf 实现的接口)。
为此,我制作了一个通用方法,该方法将采用任何 IMessage
实现(提供无参数构造函数)并尝试创建增量。
public class ProtocolDiffer<T> where T : IMessage, new()
{
public T Diff(T original, T replacement)
{
var delta = new T();
foreach (var fieldDescriptor in replacement.Descriptor.Fields.InFieldNumberOrder())
{
// Obtain the pair of values to compare
var originalFieldValue = fieldDescriptor.Accessor.GetValue(original);
var replacementFieldValue = fieldDescriptor.Accessor.GetValue(replacement);
Console.WriteLine(
$"Is the field {fieldDescriptor.Name.ToUpper()} equal between Original and Replacement? " +
$"{originalFieldValue.Equals(replacementFieldValue)}");
// These fields are equal, jump to the next pair
if (originalFieldValue.Equals(replacementFieldValue)) continue;
// They were not equal
// Is it a simple field?
if (fieldDescriptor.FieldType == FieldType.Message)
{
// Non-basic fields need to be evaluated instead of simply replaced
fieldDescriptor.Accessor.SetValue(delta,
Diff((T)(IMessage) fieldDescriptor.Accessor.GetValue(original),
(T)(IMessage) fieldDescriptor.Accessor.GetValue(replacement)));
}
else
{
fieldDescriptor.Accessor.SetValue(delta, fieldDescriptor.Accessor.GetValue(replacement));
}
}
return delta;
}
...
}
我遇到的问题是递归机制失败,因为最终被处理的对象从通过 protoc 生成的 class 变为 RepeatedField
并且 Diff
将无法投射物体。
我尝试使用 IMessage
作为参数来实例化此 Diff
class 但它不提供无参数构造函数。我现在正在考虑创建一个实现 IMessage
的 class,它可以被生成的协议转换为 classes 和 RepeatedField,但这仍然需要一些时间和我不确定我是否能够首先提供必要的铸造。
最后,这一切都在完成,这样我就可以 return 两种结构之间的区别,包括原始字段和复杂字段,例如列表,其想法是递归机制将遍历整个结构,直到它替换了每个不同的字段,然后 return 结构只设置了相同的字段。
有没有办法解决转换问题,以便我可以继续使用递归机制?
我怀疑您需要将重复的字段(和映射)简单地作为通用 diff 方法中的单独分支来处理。
IsRepeated attribute of a FieldDescriptor
will tell you "in advance" whether a field is repeated or not. Likewise IsMap for maps. The GetValue 文档告诉您可以将 returned 对象转换为:
For repeated values, this will be an IList implementation. For map values, this will be an IDictionary implementation.
具体细节取决于您要如何定义两个重复字段(或映射)之间的差异。例如,您可能希望将递归 diff 过程应用于匹配对(列表索引或映射键),但 delta
结果应包含替换中已删除的元素的内容并不完全明显(无?),或在其中添加(整个添加消息的副本?)。对于重复的字段,您可能还需要添加占位符项(默认实例?),即使对于相等的元素也是如此,否则调用者将不知道哪些列表索引包含不相等的元素。
最后,我认为你的例子中的递归步骤不正确。它假定子消息也是 T
类型,这不太可能。如果该字段具有某些 other 消息类型,则递归调用将需要 return 该类型的 delta
,而不是 T
。
一种可能性是编写一个完全基于反射的方法,具有以下一般形状(不是完整的示例):
public IMessage Diff(IMessage original, IMessage replacement, out bool changed) {
// new, empty message of the same type; not sure if there's a cleaner way
IMessage delta = replacement.Descriptor.Parser.ParseFrom(ByteString.Empty);
changed = false;
foreach (var fieldDescriptor in replacement.Descriptor.Fields.InFieldNumberOrder()) {
if (fieldDescriptor.IsMap) {
var originalMap = (IDictionary<object, IMessage>) fieldDescriptor.Accessor.GetValue(original);
var replacementMap = (IDictionary<object, IMessage>) fieldDescriptor.Accessor.GetValue(replacement);
// ...your desired logic for comparing two maps...
} else if (fieldDescriptor.IsRepeated) {
var originalList = (IList<IMessage> fieldDescriptor.Accessor.GetValue(original);
var replacementList = (IList<IMessage> fieldDescriptor.Accessor.GetValue(replacement);
// ...your desired logic for comparing two lists...
} else if (fieldDescriptor.FieldType == FieldType.Message) {
var originalMessage = (IMessage) fieldDescriptor.Accessor.GetValue(original);
var replacementMessage = (IMessage) fieldDescriptor.Accessor.GetValue(replacement);
bool fieldChanged;
var fieldDelta = Diff(originalMessage, replacementMessage, out fieldChanged);
if (fieldChanged) {
changed = true;
fieldDescriptor.Accessor.SetValue(delta, fieldDelta);
}
} else {
var originalValue = fieldDescriptor.Accessor.GetValue(original);
var replacementValue = fieldDescriptor.Accessor.GetValue(replacement);
if (!originalValue.Equals(replacementValue)) {
changed = true;
fieldDescriptor.Accessor.SetValue(delta, replacementValue);
}
}
}
return delta;
}
为了更好的类型安全,您可以将其包装在您建议的 ProtocolDiffer<T>
class 中,以确保始终传入相同类型的两条消息,并指示输出也将属于同一类型。
您还可以删除 out bool changed
out 参数并在递归步骤之前使用 Equals
方法来确定是否需要在传出的 delta
消息中包含一个字段,例如你在做你的例子。但这比必要的工作多了一点,因为它涉及对递归的每个“级别”执行深度相等性检查。
我正在尝试在代码库中的 protobuf messages that would work along all the IMessage 个对象之间创建差异(据我所知,每个 protobuf 实现的接口)。
为此,我制作了一个通用方法,该方法将采用任何 IMessage
实现(提供无参数构造函数)并尝试创建增量。
public class ProtocolDiffer<T> where T : IMessage, new()
{
public T Diff(T original, T replacement)
{
var delta = new T();
foreach (var fieldDescriptor in replacement.Descriptor.Fields.InFieldNumberOrder())
{
// Obtain the pair of values to compare
var originalFieldValue = fieldDescriptor.Accessor.GetValue(original);
var replacementFieldValue = fieldDescriptor.Accessor.GetValue(replacement);
Console.WriteLine(
$"Is the field {fieldDescriptor.Name.ToUpper()} equal between Original and Replacement? " +
$"{originalFieldValue.Equals(replacementFieldValue)}");
// These fields are equal, jump to the next pair
if (originalFieldValue.Equals(replacementFieldValue)) continue;
// They were not equal
// Is it a simple field?
if (fieldDescriptor.FieldType == FieldType.Message)
{
// Non-basic fields need to be evaluated instead of simply replaced
fieldDescriptor.Accessor.SetValue(delta,
Diff((T)(IMessage) fieldDescriptor.Accessor.GetValue(original),
(T)(IMessage) fieldDescriptor.Accessor.GetValue(replacement)));
}
else
{
fieldDescriptor.Accessor.SetValue(delta, fieldDescriptor.Accessor.GetValue(replacement));
}
}
return delta;
}
...
}
我遇到的问题是递归机制失败,因为最终被处理的对象从通过 protoc 生成的 class 变为 RepeatedField
并且 Diff
将无法投射物体。
我尝试使用 IMessage
作为参数来实例化此 Diff
class 但它不提供无参数构造函数。我现在正在考虑创建一个实现 IMessage
的 class,它可以被生成的协议转换为 classes 和 RepeatedField,但这仍然需要一些时间和我不确定我是否能够首先提供必要的铸造。
最后,这一切都在完成,这样我就可以 return 两种结构之间的区别,包括原始字段和复杂字段,例如列表,其想法是递归机制将遍历整个结构,直到它替换了每个不同的字段,然后 return 结构只设置了相同的字段。
有没有办法解决转换问题,以便我可以继续使用递归机制?
我怀疑您需要将重复的字段(和映射)简单地作为通用 diff 方法中的单独分支来处理。
IsRepeated attribute of a FieldDescriptor
will tell you "in advance" whether a field is repeated or not. Likewise IsMap for maps. The GetValue 文档告诉您可以将 returned 对象转换为:
For repeated values, this will be an IList implementation. For map values, this will be an IDictionary implementation.
具体细节取决于您要如何定义两个重复字段(或映射)之间的差异。例如,您可能希望将递归 diff 过程应用于匹配对(列表索引或映射键),但 delta
结果应包含替换中已删除的元素的内容并不完全明显(无?),或在其中添加(整个添加消息的副本?)。对于重复的字段,您可能还需要添加占位符项(默认实例?),即使对于相等的元素也是如此,否则调用者将不知道哪些列表索引包含不相等的元素。
最后,我认为你的例子中的递归步骤不正确。它假定子消息也是 T
类型,这不太可能。如果该字段具有某些 other 消息类型,则递归调用将需要 return 该类型的 delta
,而不是 T
。
一种可能性是编写一个完全基于反射的方法,具有以下一般形状(不是完整的示例):
public IMessage Diff(IMessage original, IMessage replacement, out bool changed) {
// new, empty message of the same type; not sure if there's a cleaner way
IMessage delta = replacement.Descriptor.Parser.ParseFrom(ByteString.Empty);
changed = false;
foreach (var fieldDescriptor in replacement.Descriptor.Fields.InFieldNumberOrder()) {
if (fieldDescriptor.IsMap) {
var originalMap = (IDictionary<object, IMessage>) fieldDescriptor.Accessor.GetValue(original);
var replacementMap = (IDictionary<object, IMessage>) fieldDescriptor.Accessor.GetValue(replacement);
// ...your desired logic for comparing two maps...
} else if (fieldDescriptor.IsRepeated) {
var originalList = (IList<IMessage> fieldDescriptor.Accessor.GetValue(original);
var replacementList = (IList<IMessage> fieldDescriptor.Accessor.GetValue(replacement);
// ...your desired logic for comparing two lists...
} else if (fieldDescriptor.FieldType == FieldType.Message) {
var originalMessage = (IMessage) fieldDescriptor.Accessor.GetValue(original);
var replacementMessage = (IMessage) fieldDescriptor.Accessor.GetValue(replacement);
bool fieldChanged;
var fieldDelta = Diff(originalMessage, replacementMessage, out fieldChanged);
if (fieldChanged) {
changed = true;
fieldDescriptor.Accessor.SetValue(delta, fieldDelta);
}
} else {
var originalValue = fieldDescriptor.Accessor.GetValue(original);
var replacementValue = fieldDescriptor.Accessor.GetValue(replacement);
if (!originalValue.Equals(replacementValue)) {
changed = true;
fieldDescriptor.Accessor.SetValue(delta, replacementValue);
}
}
}
return delta;
}
为了更好的类型安全,您可以将其包装在您建议的 ProtocolDiffer<T>
class 中,以确保始终传入相同类型的两条消息,并指示输出也将属于同一类型。
您还可以删除 out bool changed
out 参数并在递归步骤之前使用 Equals
方法来确定是否需要在传出的 delta
消息中包含一个字段,例如你在做你的例子。但这比必要的工作多了一点,因为它涉及对递归的每个“级别”执行深度相等性检查。