【问题标题】:Is there a way to be recursive through different types of ProtoBuf fields?有没有办法通过不同类型的 ProtoBuf 字段进行递归?
【发布时间】:2021-07-15 17:05:30
【问题描述】:

我正在尝试在 protobuf 消息之间创建一个差异,该差异将适用于代码库中的所有 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 生成的类更改为 RepeatedFieldDiff 将无法转换对象。

我尝试使用IMessage 作为参数来实例化这个Diff 类,但它没有提供无参数构造函数。我现在正在考虑创建一个实现 IMessage 的类,该类将能够被 protoc 生成的类和 RepeatedField 强制转换,但这仍然需要一些时间,我不确定我是否能够提供必要的首先进行铸造。

最后,这一切都完成了,以便我可以返回两个结构之间的差异,包括原始字段和复杂的字段,例如列表,其想法是递归机制将遍历整个结构,直到它替换每个不同的字段和然后返回仅设置了相同字段的结构。

有没有办法解决强制转换问题,以便我可以继续使用递归机制?

【问题讨论】:

    标签: c# generics reflection protocol-buffers


    【解决方案1】:

    我怀疑您需要将重复的字段(和映射)简单地处理为通用 diff 方法中的单独分支。

    FieldDescriptorIsRepeated attribute 会“提前”告诉您某个字段是否重复。同样IsMap 用于地图。 GetValue 文档告诉您可以将返回的对象转换为:

    对于重复值,这将是一个 IList 实现。对于地图值,这将是一个 IDictionary 实现。

    确切的细节取决于您想如何定义两个重复字段(或映射)之间的差异。例如,您可能希望将递归 diff 过程应用于匹配对(列表索引或映射键),但对于替换中已删除的元素,delta 结果应该包含什么并不完全清楚(什么都没有?) ,或添加在其中(整个添加消息的副本?)。对于重复的字段,您可能还需要添加占位符项(默认实例?),即使是相等的元素,否则调用者将不知道哪些列表索引包含不相等的索引。

    最后,我认为您示例中的递归步骤不正确。它假定子消息也是T 类型,这不太可能。如果该字段具有某种其他消息类型,则递归调用需要返回该类型的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&lt;T&gt; 类中,以确保始终传入两个相同类型的消息,并指示输出也将是相同类型。

    您还可以删除out bool changed 输出参数并在递归步骤之前使用Equals 方法来确定是否需要将某个字段包含在传出的delta 消息中,就像您在示例中所做的那样。但这比必要的工作多一些,因为它涉及对递归的每个“级别”执行深度相等检查。

    【讨论】:

    • 你是对的,我最终不得不选择两种方法来区分列表和非列表元素。除此之外,我遇到了你描述的关于定义增量可能是什么或可能不是什么的问题。
    猜你喜欢
    • 2019-07-04
    • 1970-01-01
    • 2010-11-18
    • 2014-03-10
    • 1970-01-01
    • 2012-04-18
    • 2023-04-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多