🔥个人主页:Milestone-里程碑

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

       <<Git>><<MySQL>>

🌟心向往之行必能至

目录

一、重复字段:repeated 关键字

1. 重复字段的定义格式

2. 实战:联系人的多个电话号码

二、嵌套消息体:定义复杂的子对象

1. 嵌套消息体的定义格式(两种方式)

方式 1:内部嵌套定义(适合仅当前消息体使用的子对象)

方式 2:独立定义(适合多个消息体共用的子对象)

2. 实战:电话号码包含号码和类型

三、枚举类型:enum 关键字

1. 枚举类型的定义规范

2. 枚举类型的使用场景

3. 实战:为电话号码添加类型枚举

四、实战:定义完整的通讯录消息体

五、编译复杂.proto 文件并生成 C++ 方法

1. 嵌套消息体的方法

2. 枚举类型的方法

3. 外层通讯录的方法

六、高级语法注意事项


在上一篇博客中,我们学习了 proto3 的基础语法,能定义简单的消息体和基础字段,但实际业务场景中,结构化数据往往更复杂 —— 例如一个联系人有多个电话号码、电话号码包含号码和类型、通讯录包含多个联系人等。

本文将讲解 proto3 的高级语法,包括重复字段(repeated)嵌套消息体枚举类型(enum),并结合通讯录场景实战,让大家能定义复杂的、贴合实际业务的 Protobuf 协议。

一、重复字段:repeated 关键字

实际业务中,经常需要一个字段包含多个值(如一个联系人有多个电话号码、一个订单有多个商品),proto3 通过repeated关键字定义重复字段,相当于编程语言中的数组 / 列表,重复值的顺序会被保留。

1. 重复字段的定义格式

proto

message 消息体名称 {
  // repeated + 字段类型 + 字段名 = 唯一编号;
  repeated 字段类型 字段名 = 字段唯一编号;
}
2. 实战:联系人的多个电话号码

在通讯录的PeopleInfo消息体中,添加多个电话号码字段,使用repeated string类型:

proto

syntax = "proto3";
package contacts;

// 联系人消息体
message PeopleInfo {
  string name = 1;    // 姓名
  sint32 age = 2;     // 年龄
  // 重复字段:多个电话号码,字符串类型,编号3
  repeated string phone_numbers = 3;
}
  • 编译后,C++ 会生成对应的列表操作方法(如add_phone_numbers()phone_numbers_size()phone_numbers(int index));
  • 重复字段的默认值为空列表,无值时不参与序列化。

二、嵌套消息体:定义复杂的子对象

当字段的结构化程度较高时(如电话号码不仅包含号码,还包含类型:移动 / 固定),单独的基础类型无法满足需求,此时可以在一个消息体中嵌套定义另一个消息体,形成父子消息体,也可以定义独立的消息体供其他消息体引用。

proto3 支持任意多层嵌套,且不同消息体的字段编号可以重复。

1. 嵌套消息体的定义格式(两种方式)
方式 1:内部嵌套定义(适合仅当前消息体使用的子对象)

proto

message 父消息体 {
  // 嵌套定义子消息体,仅父消息体能使用
  message 子消息体 {
    字段类型 字段名 = 唯一编号;
  }
  // 使用嵌套的子消息体作为字段类型
  子消息体 字段名 = 唯一编号;
  // 重复的子消息体(多个子对象)
  repeated 子消息体 字段名 = 唯一编号;
}
方式 2:独立定义(适合多个消息体共用的子对象)

proto

// 独立定义子消息体,所有消息体均可引用
message 子消息体 {
  字段类型 字段名 = 唯一编号;
}

message 父消息体1 {
  repeated 子消息体 字段名 = 唯一编号;
}

message 父消息体2 {
  子消息体 字段名 = 唯一编号;
}
2. 实战:电话号码包含号码和类型

将电话号码从简单的字符串,升级为包含号码(number)类型(type) 的复杂对象,使用内部嵌套消息体定义,并结合repeated实现多个电话号码:

proto

syntax = "proto3";
package contacts;

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

  // 嵌套消息体:电话号码(仅PeopleInfo使用)
  message Phone {
    string number = 1; // 电话号码
  }

  // 重复字段:多个电话号码对象,编号3
  repeated Phone phone = 3;
}

三、枚举类型:enum 关键字

当字段的取值为有限的固定值时(如电话号码类型:移动 / 固定、性别:男 / 女、订单状态:待支付 / 已支付 / 已取消),使用枚举类型(enum) 可以规范字段取值,避免无效值,提高代码的可读性和可维护性。

1. 枚举类型的定义规范

proto

// 枚举类型命名:驼峰命名法(大驼峰),首字母大写
enum 枚举类型名称 {
  // 枚举常量命名:全大写字母,多个单词用下划线连接
  常量名1 = 0; // 必须包含0值常量,且作为第一个元素(proto3规范)
  常量名2 = 1;
  常量名3 = 2;
  // ...
}

核心规则

  1. 枚举类型必须包含0 值常量,且作为第一个元素(为了兼容 proto2 的默认值语义,0 为默认值);
  2. 枚举常量的取值为32 位整数不建议使用负数(与编码规则冲突);
  3. 枚举类型可以嵌套在消息体中(仅当前消息体使用),也可以独立定义(多个消息体共用);
  4. 同级枚举的常量名不可重复(不同级、不同包名的枚举常量名可重复)。
2. 枚举类型的使用场景
  • 嵌套使用:枚举仅为某个消息体的字段服务,如电话号码类型仅为Phone消息体服务;
  • 独立使用:枚举为多个消息体服务,如OrderStatus(订单状态)供OrderOrderDetail等消息体使用。
3. 实战:为电话号码添加类型枚举

在嵌套的Phone消息体中,添加电话号码类型枚举(移动:MP、固定:TEL),并将其作为Phone的字段:

proto

syntax = "proto3";
package contacts;

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

  // 嵌套消息体:电话号码
  message Phone {
    // 枚举类型:电话号码类型(嵌套定义,仅Phone使用)
    enum PhoneType {
      MP = 0;  // 移动电话(默认值,必须为第一个元素)
      TEL = 1; // 固定电话
    }

    string number = 1; // 电话号码
    PhoneType type = 2; // 电话号码类型,使用枚举类型
  }

  // 重复字段:多个电话号码对象,编号3
  repeated Phone phone = 3;
}
  • 编译后,C++ 会生成对应的枚举类和字段操作方法(如set_type()type()PhoneType_Name()(获取枚举常量名));
  • 枚举字段的默认值为第一个枚举常量(0 值)

四、实战:定义完整的通讯录消息体

结合重复字段嵌套消息体枚举类型,定义一个完整的通讯录消息体 —— 包含多个联系人,每个联系人包含姓名、年龄、多个电话号码(含类型)

proto

syntax = "proto3";
package contacts;

// 联系人消息体
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; // 多个电话号码
}

// 通讯录消息体:包含多个联系人
message Contacts {
  // 重复字段:多个联系人对象,编号1
  repeated PeopleInfo contacts = 1;
}

五、编译复杂.proto 文件并生成 C++ 方法

使用以下命令编译上述contacts.proto文件:

bash

运行

protoc --cpp_out=. contacts.proto

编译后生成contacts.pb.hcontacts.pb.cc,其中会为复杂字段生成专属操作方法(C++ 为例),核心方法如下:

1. 嵌套消息体的方法
  • 创建子对象:PeopleInfo_Phone* add_phone();(为重复的 phone 字段添加一个 Phone 对象);
  • 获取子对象:const PeopleInfo_Phone& phone(int index) const;(根据索引获取 Phone 对象);
  • 获取子对象数量:int phone_size() const;(获取电话号码的个数)。
2. 枚举类型的方法
  • 设置枚举值:void set_type(PeopleInfo_Phone_PhoneType value);
  • 获取枚举值:PeopleInfo_Phone_PhoneType type() const;
  • 获取枚举常量名:static const std::string& PhoneType_Name(PhoneType value);(如将 MP 转为字符串 "MP")。
3. 外层通讯录的方法
  • 添加联系人:PeopleInfo* add_contacts();
  • 获取联系人数量:int contacts_size() const;
  • 获取指定联系人:const PeopleInfo& contacts(int index) const;

六、高级语法注意事项

  1. repeated字段的顺序会被严格保留,序列化和反序列化后的顺序一致;
  2. 嵌套消息体的作用域仅限于父消息体,外部消息体无法直接引用(如需引用,改为独立定义);
  3. 枚举类型必须以 0 值常量开头,否则编译报错;
  4. 同级枚举的常量名不可重复,不同级枚举无此限制;
  5. 复杂字段的编号仍需遵循唯一编号规则,为后续扩展预留编号。

下一篇博客,我们将讲解 proto3 的高级特性 ——Any 类型、Oneof 类型和 Map 类型,这三种类型能解决更复杂的业务场景,如泛型字段、多选一字段、键值对字段等,让 Protobuf 的协议定义更灵活。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐