Proto3 三大高级类型:Any、Oneof、Map 灵活解决复杂业务场景

🔥个人主页: Milestone-里程碑

❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>

<<Git>><<MySQL>>

🌟心向往之行必能至

目录

[一、Any 类型:泛型字段,存储任意消息体](#一、Any 类型:泛型字段,存储任意消息体)

[1. Any 类型的核心规则](#1. Any 类型的核心规则)

[2. Any 类型的使用步骤](#2. Any 类型的使用步骤)

[步骤 1:导入 any.proto 文件(必选)](#步骤 1:导入 any.proto 文件(必选))

[步骤 2:定义需要存储的任意消息体](#步骤 2:定义需要存储的任意消息体)

[步骤 3:定义 Any 类型字段](#步骤 3:定义 Any 类型字段)

[3. 实战:为联系人添加任意类型的地址信息](#3. 实战:为联系人添加任意类型的地址信息)

[4. Any 类型的核心方法(C++ 为例)](#4. Any 类型的核心方法(C++ 为例))

[二、Oneof 类型:多选一字段,仅一个字段生效](#二、Oneof 类型:多选一字段,仅一个字段生效)

[1. Oneof 类型的核心规则](#1. Oneof 类型的核心规则)

[2. Oneof 类型的定义格式](#2. Oneof 类型的定义格式)

[3. 实战:为联系人添加二选一的其他联系方式](#3. 实战:为联系人添加二选一的其他联系方式)

[4. Oneof 类型的核心方法(C++ 为例)](#4. Oneof 类型的核心方法(C++ 为例))

[三、Map 类型:键值对字段,存储关联数据](#三、Map 类型:键值对字段,存储关联数据)

[1. Map 类型的核心规则](#1. Map 类型的核心规则)

[2. Map 类型的定义格式](#2. Map 类型的定义格式)

[3. 实战:为联系人添加键值对格式的备注信息](#3. 实战:为联系人添加键值对格式的备注信息)

[4. Map 类型的核心方法(C++ 为例)](#4. Map 类型的核心方法(C++ 为例))

四、三大高级类型的综合实战:完整的通讯录协议

五、高级类型注意事项


在前两篇博客中,我们掌握了 proto3 的基础语法和高级语法,能定义包含基础字段、重复字段、嵌套消息和枚举的结构化数据,但实际业务中还有更复杂的需求:

  • 字段需要存储任意类型的对象(如联系人的附加信息可能是地址、身份证、银行卡等);
  • 多个字段只能有一个生效(如联系人的其他联系方式:QQ / 微信二选一);
  • 字段需要键值对格式(如联系人的备注信息:key = 备注标题,value = 备注内容)。

proto3 提供了AnyOneofMap三大高级类型,专门解决以上场景,让协议定义更灵活、更贴合实际业务。本文将详细讲解这三种类型的定义、使用规则和实战场景,结合通讯录案例完成协议升级。

一、Any 类型:泛型字段,存储任意消息体

Any 类型 相当于编程语言中的泛型 ,可以在字段中存储任意类型的 Protobuf 消息体 ,无需提前指定具体类型,实现了字段的 "动态类型",适合存储不确定类型的附加信息

1. Any 类型的核心规则
  1. Any 类型是 Google 预定义的类型,需要导入官方的 any.proto 文件才能使用;
  2. Any 类型的字段可以存储任意自定义的消息体,支持嵌套和复杂结构;
  3. 编译后,会生成PackFrom()UnpackTo() 方法,用于消息体与 Any 类型的互转;
  4. 提供Is<T>() 方法,用于判断 Any 字段中存储的消息体类型。
2. Any 类型的使用步骤
步骤 1:导入 any.proto 文件(必选)

proto

复制代码
// 导入Google官方的any.proto,必须写在包名声明后
import "google/protobuf/any.proto";
步骤 2:定义需要存储的任意消息体

proto

复制代码
// 示例:定义地址消息体,作为Any字段的存储内容
message Address {
  string home_address = 1; // 家庭地址
  string unit_address = 2; // 单位地址
}
步骤 3:定义 Any 类型字段

proto

复制代码
message 消息体名称 {
  // 使用google.protobuf.Any作为字段类型
  google.protobuf.Any 字段名 = 唯一编号;
}
3. 实战:为联系人添加任意类型的地址信息

在通讯录的PeopleInfo消息体中,添加 Any 类型的data字段,用于存储联系人的地址信息(Address 消息体),实现 "附加信息的动态存储":

proto

复制代码
syntax = "proto3";
package contacts;

// 导入Any类型的官方proto文件
import "google/protobuf/any.proto";

// 地址消息体:可作为Any字段的存储内容
message Address {
  string home_address = 1; // 家庭地址
  string unit_address = 2; // 单位地址
}

// 联系人消息体
message PeopleInfo {
  string name = 1;    // 姓名
  sint32 age = 2;     // 年龄

  // 嵌套:电话号码(含枚举)
  message Phone {
    enum PhoneType {
      MP = 0;  // 移动电话
      TEL = 1; // 固定电话
    }
    string number = 1;
    PhoneType type = 2;
  }
  repeated Phone phone = 3; // 多个电话号码

  // Any类型:存储任意消息体(此处存储Address),编号4
  google.protobuf.Any data = 4;
}

// 通讯录消息体
message Contacts {
  repeated PeopleInfo contacts = 1;
}
4. Any 类型的核心方法(C++ 为例)

编译后,C++ 会生成以下核心方法,实现消息体与 Any 类型的互转:

  1. PackFrom() :将自定义消息体(如 Address)打包为 Any 类型;

    cpp

    运行

    复制代码
    // 将Address对象打包到Any字段中
    Address address;
    people_info.mutable_data()->PackFrom(address);
  2. UnpackTo() :将 Any 类型解包为自定义消息体;

    cpp

    运行

    复制代码
    // 将Any字段解包为Address对象
    Address address;
    people_info.data().UnpackTo(&address);
  3. Is<T>() :判断 Any 字段中存储的是否为指定类型的消息体;

    cpp

    运行

    复制代码
    // 判断data字段是否存储的是Address对象
    if (people_info.data().Is<Address>()) {
      // 解包并处理
    }
  4. has_data():判断 Any 字段是否被赋值。

二、Oneof 类型:多选一字段,仅一个字段生效

Oneof 类型 用于定义一组互斥的字段 ,即这组字段中只能有一个字段被赋值,赋值新的字段会自动清除之前赋值的字段,适合 "多选一" 的业务场景(如联系方式:QQ / 微信二选一、支付方式:微信 / 支付宝 / 银行卡三选一)。

1. Oneof 类型的核心规则
  1. Oneof 字段的定义格式为oneof 字段名 { 子字段1; 子字段2; ... }
  2. 子字段的编号不可重复,且不可与消息体中的其他字段编号冲突;
  3. 不能在 Oneof 中使用 repeated 字段(重复字段与互斥规则冲突);
  4. 赋值 Oneof 中的一个子字段,会自动清除其他子字段的值
  5. 编译后,会生成**_case()** 方法,用于判断当前哪个子字段被赋值。
2. Oneof 类型的定义格式

proto

复制代码
message 消息体名称 {
  // 其他普通字段
  字段类型 字段名 = 唯一编号;

  // Oneof类型:定义互斥的子字段
  oneof oneof字段名 {
    字段类型 子字段1 = 唯一编号;
    字段类型 子字段2 = 唯一编号;
    // ... 更多子字段
  }
}
3. 实战:为联系人添加二选一的其他联系方式

PeopleInfo消息体中,添加 Oneof 类型的other_contact字段,包含qqweixin两个子字段,实现 "QQ / 微信二选一" 的联系方式:

proto

复制代码
syntax = "proto3";
package contacts;
import "google/protobuf/any.proto";

message Address {
  string home_address = 1;
  string unit_address = 2;
}

message PeopleInfo {
  string name = 1;
  sint32 age = 2;

  message Phone {
    enum PhoneType { MP = 0; TEL = 1; }
    string number = 1;
    PhoneType type = 2;
  }
  repeated Phone phone = 3;
  google.protobuf.Any data = 4;

  // Oneof类型:其他联系方式(QQ/微信二选一),编号5、6
  oneof other_contact {
    string qq = 5;
    string weixin = 6;
  }
}

message Contacts {
  repeated PeopleInfo contacts = 1;
}
4. Oneof 类型的核心方法(C++ 为例)

编译后,C++ 会生成以下核心方法,用于 Oneof 字段的操作和判断:

  1. 普通字段操作方法set_qq()set_weixin()qq()weixin()

  2. 判断赋值状态other_contact_case(),返回当前生效的子字段枚举值;

    cpp

    运行

    复制代码
    // 判断哪个联系方式被赋值
    switch (people_info.other_contact_case()) {
      case PeopleInfo::kQq: // QQ被赋值
        cout << "QQ: " << people_info.qq() << endl;
        break;
      case PeopleInfo::kWeixin: // 微信被赋值
        cout << "微信: " << people_info.weixin() << endl;
        break;
      case PeopleInfo::OTHER_CONTACT_NOT_SET: // 未赋值
        break;
    }
  3. 清空 Oneof 字段clear_other_contact(),清除所有子字段的值。

三、Map 类型:键值对字段,存储关联数据

Map 类型 用于定义键值对格式的字段 ,相当于编程语言中的Map/Dictionary,适合存储备注信息、扩展属性、键值对配置等场景,无需定义额外的消息体。

1. Map 类型的核心规则
  1. Map 类型的定义格式为map<key_type, value_type> 字段名 = 唯一编号;
  2. key_type(键类型) :只能是除floatbytes外的标量类型(如 int32、string、uint64 等);
  3. value_type(值类型) :可以是任意类型(标量类型、消息体、枚举、重复字段等);
  4. 不能用 repeated 修饰 Map 字段(Map 本身支持多组键值对);
  5. Map 中存储的元素是无序的,序列化和反序列化后的顺序可能不一致;
  6. 编译后,会生成与普通字段类似的操作方法,支持键值对的增删改查。
2. Map 类型的定义格式

proto

复制代码
message 消息体名称 {
  // 其他字段
  字段类型 字段名 = 唯一编号;

  // Map类型:键类型key_type,值类型value_type
  map<key_type, value_type> 字段名 = 唯一编号;
}
3. 实战:为联系人添加键值对格式的备注信息

PeopleInfo消息体中,添加 Map 类型的remark字段,键类型为string(备注标题),值类型为string(备注内容),实现联系人备注的灵活存储:

proto

复制代码
syntax = "proto3";
package contacts;
import "google/protobuf/any.proto";

message Address {
  string home_address = 1;
  string unit_address = 2;
}

message PeopleInfo {
  string name = 1;
  sint32 age = 2;

  message Phone {
    enum PhoneType { MP = 0; TEL = 1; }
    string number = 1;
    PhoneType type = 2;
  }
  repeated Phone phone = 3;
  google.protobuf.Any data = 4;

  oneof other_contact {
    string qq = 5;
    string weixin = 6;
  }

  // Map类型:备注信息(键:备注标题,值:备注内容),编号7
  map<string, string> remark = 7;
}

message Contacts {
  repeated PeopleInfo contacts = 1;
}
4. Map 类型的核心方法(C++ 为例)

编译后,C++ 会生成以下核心方法,用于 Map 字段的键值对操作:

  1. 获取 Map 对象mutable_remark(),返回 Map 对象的指针,用于增删改查;

    cpp

    运行

    复制代码
    // 向备注中添加键值对
    people_info.mutable_remark()->insert({"日程", "10月1日出游"});
    people_info.mutable_remark()->insert({"备注", "重要联系人"});
  2. 获取 Map 大小remark_size(),返回键值对的个数;

  3. 遍历 Map :通过迭代器遍历所有键值对;

    cpp

    运行

    复制代码
    for (auto it = people_info.remark().cbegin(); it != people_info.remark().cend(); ++it) {
      cout << it->first << ": " << it->second << endl;
    }
  4. 清空 Mapclear_remark(),清除所有键值对。

四、三大高级类型的综合实战:完整的通讯录协议

结合 Any、Oneof、Map 三大高级类型,定义一个完整的通讯录.proto 文件contacts.proto),包含联系人的所有核心信息:姓名、年龄、多个电话号码(含类型)、任意类型的地址、二选一的联系方式、键值对备注,以及多个联系人的通讯录:

proto

复制代码
syntax = "proto3";
package contacts;

// 导入Any类型依赖
import "google/protobuf/any.proto";

// 地址消息体:供Any类型存储
message Address {
  string home_address = 1; // 家庭地址
  string unit_address = 2; // 单位地址
}

// 联系人消息体
message PeopleInfo {
  string name = 1;    // 姓名
  sint32 age = 2;     // 年龄

  // 电话号码(嵌套+枚举)
  message Phone {
    enum PhoneType {
      MP = 0;  // 移动电话
      TEL = 1; // 固定电话
    }
    string number = 1; // 号码
    PhoneType type = 2; // 类型
  }
  repeated Phone phone = 3; // 多个电话号码

  google.protobuf.Any data = 4; // 任意类型附加信息(地址)
  oneof other_contact { // 二选一联系方式
    string qq = 5;
    string weixin = 6;
  }
  map<string, string> remark = 7; // 键值对备注
}

// 通讯录消息体:多个联系人
message Contacts {
  repeated PeopleInfo contacts = 1;
}

五、高级类型注意事项

  1. Any 类型必须导入 any.proto,否则编译报错,且仅能存储 Protobuf 消息体;
  2. Oneof 的子字段不可重复、不可用 repeated,赋值互斥,适合多选一场景;
  3. Map 的键类型不支持 float 和 bytes,元素无序,适合键值对存储;
  4. 三大高级类型的字段编号仍需遵循唯一规则,为后续扩展预留;
  5. 编译后,所有高级类型都会生成对应的操作方法,无需手动编写解析代码。

下一篇博客,我们将讲解 Protobuf 的默认值规则消息体更新规则 ,这是保证 Protobuf版本兼容性的核心,也是分布式系统中协议迭代的关键,让大家能在项目迭代中无缝扩展协议,不破坏旧版本程序。

相关推荐
蜜獾云2 小时前
DDD 架构分层,MQ消息要放到那一层处理?
java·jvm·架构
夫礼者2 小时前
【极简监控】核弹级排障利器:仿 Jenkins Script Console 打造免重启诊断“黑科技”
java·jenkins·监控·排错
小杍随笔2 小时前
【Rust Exercism 练习详解:Anagram + Space Age + Sublist(附完整代码与深度解读)】
开发语言·rust·c#
空空潍2 小时前
Spring AI 实战系列(四):Prompt工程深度实战
java·人工智能·spring·prompt
第二只羽毛2 小时前
IO代码解释3
java·大数据·开发语言
是娇娇公主~2 小时前
C++迭代器详解
开发语言·c++·stl
qq_148115372 小时前
C++网络编程(Boost.Asio)
开发语言·c++·算法
weisian1512 小时前
Java并发编程--24-死锁排查与性能调优:线上并发问题诊断指南(死锁,CPU飙升,内存溢出)
java·开发语言·arthas·死锁·火焰图·cpu飙升
CSCN新手听安2 小时前
【Qt】Qt概述(三)Qt初识,HelloWorld的创建,对象树
开发语言·qt