C++Protobufs::如何使用MergeFrom()擦除特定字段

C++ Protobufs :: How to erase particular field with MergeFrom()?

本文关键字:擦除 字段 何使用 MergeFrom C++Protobufs      更新时间:2023-10-16

首先:我不是protobuf方面的专家。

假设我有这样的消息结构:

package msg_RepAndOpt;
message RepAndOpt
{
    repeated string name = 1;
    optional string surname = 2;
    ...
    // there are lots of others.
}

我有两个组件有这个消息的副本:

// component1:
RepAndOpt A;
A.add_name("Name");
A.set_surname("Surname");
// component2:
RepAndOpt B;

在我的例子中,组件通过事务机制修改这些消息。这意味着,如果一个组件更改了某个字段,它也会将其发送到另一个组件以传播这些更改。组件接收器正在进行合并:

// Component2 modifies B and sends it to component1.
// Component1 perfoms merge:
A.MergeFrom(B);

现在,比如component2想要擦除字段"name"。如果它将发送清除B消息(默认结构),则:

  • MergeFrom()不会修改A
  • CopyFrom()还会擦除其他字段

另一种方法是用A的内容填充B,清除名称字段,component1将使用CopyFrom()。但这是不可接受的,因为系统负载非常高,而且可能还有很多其他字段。因此,清除名称字段所需的解决方案是:

  1. 组件2创建B消息
  2. 显式存储只想擦除名称字段的信息
  3. 组件1执行A.MergeFrom(B)
  4. 结果:A::name被清除,但其他字段保持不变

就我测试的情况而言,这适用于重复字段和可选字段。有现成的解决方案吗?或者我应该修改protobuf实现?

您的案例没有内置的protobuf解决方案。显而易见的解决方案是迭代消息A中的所有字段,并检查该字段是否存在于消息B中,如果不存在,则可以清除它

使用基本的MergeFrom()无法解决此问题,但您可能需要从protobuf库中查看这些:

https://github.com/google/protobuf/blob/master/src/google/protobuf/field_mask.protohttps://github.com/google/protobuf/blob/master/src/google/protobuf/util/field_mask_util.h

特别是FieldMaskUtil::MergeMessageTo()似乎可以随心所欲。您需要构造一个FieldMask,准确地指定您感兴趣的字段,这样其他字段就不会受到影响。

UPD:在Kenton Varda发表评论后更新(见下文)。

扩展前面的答案之一:

有一种方法可以通过在消息定义中添加新字段来解决这个问题(这适用于proto v2):

    repeated int32 fields_to_copy = 15;

此字段将由接收方将复制(而不是合并)的字段的ID填充。

我还实现了这个助手功能:

// CopiableProtoMsg.hpp
#pragma once
#include <google/protobuf/message.h>
template <typename T>
void CopyMessageFields(const T& from, T& to)
{
    const ::google::protobuf::Descriptor *desc = T::descriptor();
    const ::google::protobuf::Reflection *thisRefl = from.GetReflection();
    std::vector<const ::google::protobuf::FieldDescriptor*> fields;
    int size = from.fields_to_copy_size();
    for (int i = 0; i < size; ++i)
    {
      const ::google::protobuf::FieldDescriptor *field = desc->FindFieldByNumber(from.fields_to_copy(i));
      fields.push_back(field);
    }
    T msgCopy(from);
    thisRefl->SwapFields(&to, &msgCopy, fields);
    to.clear_fields_to_copy();
}

此函数检查fields_to_copy字段并执行复制(通过SwapFields())。

这是一个简单的测试:

RepAndOpt emulateSerialization(const RepAndOpt& B)
{
    RepAndOpt BB;
    std::string data;
    B.SerializeToString(&data);
    BB.ParseFromString(data);
    return BB;
}
TEST(RepAndOptTest, additional_field_do_the_job_with_serialization)
{
    RepAndOpt A;
    RepAndOpt B;
    A.add_name("1");
    A.add_name("2");
    A.add_name("3");
    A.set_surname("A");
    B.add_name("1");
    B.add_name("3");
    B.add_fields_to_copy(RepAndOpt::kNameFieldNumber);
    RepAndOpt recvB = emulateSerialization(B);
    A.MergeFrom(recvB);
    CopyMessageFields(recvB, A);
    EXPECT_EQ(2, A.name_size());
    EXPECT_STREQ("1", A.name(0).c_str());
    EXPECT_STREQ("3", A.name(1).c_str());
    EXPECT_TRUE(A.has_surname());
    EXPECT_EQ(0, A.fields_to_copy_size());
}

用字段掩码扩展方法(由Kenton Varda提出):

注意:这个解决方案需要proto3,但是原始消息可以用proto2语法声明。(链接到证明)

我们可以定义一个字段掩码字段:

import "google/protobuf/field_mask.proto";
message RepAndOpt
{
    repeated string name = 1;
    optional string surname = 2;
    optional google.protobuf.FieldMask field_mask = 3;    
}

下面是测试用法:

RepAndOpt emulateSerialization(const RepAndOpt& B)
{
    RepAndOpt BB;
    std::string data;
    B.SerializeToString(&data);
    BB.ParseFromString(data);
    return BB;
}
void mergeMessageTo(const RepAndOpt& src, RepAndOpt& dst)
{
    dst.MergeFrom(src);
    if (src.has_field_mask())
    {
        FieldMaskUtil::MergeOptions megreOpt;
        megreOpt.set_replace_message_fields(true);
        megreOpt.set_replace_repeated_fields(true);
        FieldMaskUtil::MergeMessageTo(src, src.field_mask(), megreOpt, &dst);
    }
}
TEST(RepAndOptTest, fix_merge_do_the_job_with_serialization_multiple_values)
{
    RepAndOpt A;
    A.add_name("A");
    A.add_name("B");
    A.add_name("C");
    A.set_surname("surname");
    RepAndOpt B;
    B.add_name("A");
    B.add_name("C");
    B.mutable_field_mask()->add_paths("name");
    mergeMessageTo(emulateSerialization(B), A);
    EXPECT_EQ(2, A.name_size());
    EXPECT_STREQ("A", A.name(0).c_str());
    EXPECT_STREQ("C", A.name(1).c_str());
    EXPECT_STREQ("surname", A.surname().c_str());
}

我有类似的用例,并最终基于@Denis的答案实现了我自己的混合。

尽管语言是Golang,它没有带MergeOptions的FieldMaskUtil。

            RepAndOpt.A.name RepAndOpt.B.name
 remove     ["A", "B", "C"]  ["A"]      => remove: A, keep: B, C
 add        ["A", "B", "C"]  ["D"]      => add:    D, keep: A, B, C
 add/remove ["A", "B", "C"]  ["A", "D"] => remove: A, add:  D, keep: B