Protobuf:复杂类型接口

Protobuf:复杂类型接口


本博客基于proto3语法,讲解protobuf中的复杂类型。

package

.proto文件中,支持导入其它.proto文件的内容,例如:

  • test.proto
cpp 复制代码
syntax = "proto3";
package test_pkg;

import "other.proto";

message Person {
    int32 id =1;
    string name = 2;
    other_pkg.Address addr = 3;
}
  • other.proto
cpp 复制代码
syntax="proto3";
package other_pkg;

message Address {
    string country = 1;
    string city = 2;
}

test.proto文件中,使用了other.proto的内容。想要实现多个文件的效果,需要通过improt导入文件:

cpp 复制代码
import 文件名.proto

随后在导入的文件中,使用包名.变量名的形式使用变量,比如other_pkg.Address


字段规则

如果想在protobuf中实现一个数组,它使用了一种字段规则的修饰方式来实现数组。

消息字段类型可以用以下规则修饰:

  • singular:消息中该字段可以出现0次或1次,字段默认使用该规则
  • repeated:消息中该字段可以出现任意次数,包括0

简单来说,对于singular变量只能存储一个值,如果输入新的值,后来的值会把原先的值覆盖。而对于repeated变量,其可以存储多个值,所以值都会被保留,其实也就是数组。

示例:

cpp 复制代码
syntax = "proto3";
package test_pkg;

message Person {
    int32 id = 1;
    string name = 2;
    repeated string phone = 3;
}

此处的phone字段被设置为了repeated,可以理解为一个string的数组,在pb.h文件中,该字段包含以下接口:

cpp 复制代码
// repeated string phone = 3;
int phone_size() const;
private:
int _internal_phone_size() const;
public:
void clear_phone();
const std::string& phone(int index) const;
std::string* mutable_phone(int index);
void set_phone(int index, const std::string& value);
void set_phone(int index, std::string&& value);
void set_phone(int index, const char* value);
void set_phone(int index, const char* value, size_t size);
std::string* add_phone();
void add_phone(const std::string& value);
void add_phone(std::string&& value);
void add_phone(const char* value);
void add_phone(const char* value, size_t size);

可以看到,相比于一般的string类型,被repeated修饰后,多出了很多下标操作,这里挑几个接口出来讲解:

  • int phone_size() const:获取当前数组的长度。
  • const std::string& phone(int index) const:通过下标获取值的操作,获取该变量的const引用。
  • std::string* mutable_phone(int index):获取指定下标元素的指针,可以通过指针修改该元素。
cpp 复制代码
void set_phone(int index, const std::string& value);
void set_phone(int index, std::string&& value);
void set_phone(int index, const char* value);
void set_phone(int index, const char* value, size_t size);

这四个接口用于设置指定下标的值,四种函数重载效果是一样的,只是传入字符串的形式不同。

  • std::string* add_phone():数组末尾增加一个空元素,并返回指向该元素的指针,可以通过指针修改该元素。
cpp 复制代码
void add_phone(const std::string& value);
void add_phone(std::string&& value);
void add_phone(const char* value);
void add_phone(const char* value, size_t size);

这四个接口直接插入一个指定元素到数组末尾,重载是为了兼容不同形式的字符串。

repeat修饰的变量接口总结:

接口 功能
xxx_size() 获取数组的长度
xxx(index) 获取指定下标的元素,不可修改
muteble_xxx(index) 获取指定下标元素的指针,可修改
set_xxx(index, value) 设置指定下标的元素为value
add_xxx() 尾插一个元素到数组,返回指向该元素的指针,可修改
add_xxx(value) 尾插value到数组

复杂类型

enum

enum是枚举类型,其列举多个常量,方便后续使用。

定义一个枚举,表示有哪些人物种类:

cpp 复制代码
enum Type{
    STU = 0;
    TEACHER = 1;
    OTHER = 2;
}

此处枚举了三个常量,分别表示学生,老师,其它。

枚举类型有以下要求:

  1. 0值必须存在,且要作为第一个元素
  2. 在语言中,定义枚举变量后,如果指定变量值,第一个元素0是默认值
  3. 枚举的常量值在32位整数内,但是负数无效

示例:

cpp 复制代码
syntax = "proto3";
package test_pkg;

enum Type{
    STU = 0;
    TEACHER = 1;
    OTHER = 2;
}

message Person {
    int32 id = 1;
    string name = 2;
    repeated string phone = 3;
    Type type = 4;
}

编译后,在.pb.h文件可以找到以下枚举类型:

cpp 复制代码
enum Type : int {
  STU = 0,
  TEACHER = 1,
  OTHER = 2,
  Type_INT_MIN_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::min(),
  Type_INT_MAX_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::max()
};

此处使用了C++11的强枚举类型语法,enum Type : int 指定底层类型为int。除去在.proto文件中指定的三个成员,还增加了两个其它成员,最后两个成员用于做边界值判断,限制成员的值只在int32范围内。

紧接着的是部分枚举类型的专有接口:

cpp 复制代码
bool Type_IsValid(int value);
constexpr Type Type_MIN = STU;
constexpr Type Type_MAX = OTHER;
constexpr int Type_ARRAYSIZE = Type_MAX + 1;

const ::PROTOBUF_NAMESPACE_ID::EnumDescriptor* Type_descriptor();
template<typename T>
inline const std::string& Type_Name(T enum_t_value)
inline bool Type_Parse(
    ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, Type* value);
  • Type_IsValid:判断整数是否是enum内部的成员之一
  • Type_Name:输入枚举值,返回枚举值对应的字符串名称
  • Type_parse:输入字符串和枚举值,判断两者是否匹配

再往后,在Person类中,定义了枚举的通用接口:

cpp 复制代码
// .test_pkg.Type type = 4;
void clear_type();
::test_pkg::Type type() const;
void set_type(::test_pkg::Type value);

可以看到,枚举类型的getset接口还是比较简单的。

enum接口总结:

接口 功能
xxx_IsValid() 判断整数是否是enum内部的成员之一
xxx_Name(value) 输入枚举值,返回枚举值对应的字符串名称
xxx_Parse(string, value) 输入字符串和枚举值,判断两者是否匹配
xxx() 获取当前枚举变量的值
set_xxx(value) 设置枚举的值

Any

假设现在要给Person类定义一个info字段,用于存储该人的具体信息。但是由于Person可以是学生,可以是老师,这两种人存储的info是不同的,如何让一个字段存储不同的值,成为一个泛型?

此处Any类型就排上用场了,Any可以存储任何其它类型的message,相当于一个泛型。

cpp 复制代码
syntax = "proto3";
package test_pkg;

import "google/protobuf/any.proto";

message test{
    google.protobuf.Any info = 1;
}

Any的本质是一个message,写在google/protobuf/any.proto中,使用Any类型需要import导包。在类内使用类型时,也需要指定包名google.protobuf.Any

引入泛型后,当前Person类如下:

cpp 复制代码
syntax = "proto3";
package test_pkg;

import "google/protobuf/any.proto";

enum Type{
    STU = 0;
    TEACHER = 1;
    OTHER = 2;
}

message stuInfo {
    string className = 1; // 班级
    int32 score = 2;      // 成绩
}

message teacherInfo {
    string subject = 1; // 科目
    int32 jobAge = 2;   // 工龄
}

message Person {
    int32 id = 1;
    string name = 2;
    repeated string phone = 3;
    Type type = 4;
    google.protobuf.Any info = 5;
}

Personinfo字段用于存储具体信息,对于学生来说,要存储班级和成绩。而对于老师来说,要存储所教的科目和工龄。此时将这些信息分别定义在stuInfoteacherInfo中,在Person内引入一个Any字段info,这个info就可以存储任意其它的message,自然也包括stuInfoteacherInfo

编译后,在.pb.hAny的接口如下:

cpp 复制代码
// .google.protobuf.Any info = 5;
bool has_info() const;
private:
bool _internal_has_info() const;
public:
void clear_info();
const ::PROTOBUF_NAMESPACE_ID::Any& info() const;
PROTOBUF_NODISCARD ::PROTOBUF_NAMESPACE_ID::Any* release_info();
::PROTOBUF_NAMESPACE_ID::Any* mutable_info();
void set_allocated_info(::PROTOBUF_NAMESPACE_ID::Any* info);
  • has_info():检查该字段是否为空
  • clear_info():清空该字段的值
  • info():获取该字段的值
  • release_info():获取字段内的值后,清空字段内的值
  • mutable_info():返回指向元素的指针,可通过指针修改元素

另外的,还有几个重要的接口,在google/protobuf/any.pb.h中:

cpp 复制代码
bool PackFrom(const ::PROTOBUF_NAMESPACE_ID::Message& message);

bool UnpackTo(::PROTOBUF_NAMESPACE_ID::Message* message);

template<typename T> 
bool Is();

这三个接口是Any最核心的接口:

  • PackFrom:将一个message类型转化为Any类型
  • UnpackTo:将一个Any类型转化回message
  • Is:传入模板参数,判断当前Any存储的类型是否和模板参数一样

这个用法有点复杂,示例:

cpp 复制代码
#include <iostream>
#include "test.pb.h"

using namespace std;
using namespace test_pkg;

int main()
{
    test_pkg::Person ps;
    ps.set_id(123456);
    ps.set_name("张三");
    // repeat字段,可以设置多个值
    ps.add_phone("123456");
    ps.add_phone("654321");
    ps.set_type(Type::STU); // 学生类型

    stuInfo stuinfo;
    stuinfo.set_classname("六年级一班");
    stuinfo.set_score(95);

    google::protobuf::Any any_message;
    if (any_message.PackFrom(stuinfo)) 
        *ps.mutable_info() = any_message;

    stuInfo getStuinfo;
    if (ps.info().Is<stuInfo>())
        ps.info().UnpackTo(&getStuinfo);

    return 0;
}

首先初始化了一个Person,随后初始化了该对象的初始信息,确定该对象是一个学生。所以他的info要存储一个stuInfo类型,用于表示详细信息。

cpp 复制代码
stuInfo stuinfo;
stuinfo.set_classname("六年级一班");
stuinfo.set_score(95);

这段代码初始化了一个stuinfo对象。

随后将这个stuInfo对象设置到ps.info成员中:

cpp 复制代码
google::protobuf::Any any_message;
if (any_message.PackFrom(stuinfo)) 
    *ps.mutable_info() = any_message;

设置Any成员时,首先要定义一个Any对象,然后通过PackFrom函数,把stuInfo对象转化为Any对象。如果PackFrom返回true,说明转化成功,此时any_message内部就已经是stuInfo的内容了。最后通过*ps.mutable_info() = any_message设置info字段为any_message对象。

如果后续想要把Any对象变回stuInfo对象:

cpp 复制代码
stuInfo getStuinfo;
if (ps.info().Is<stuInfo>())
    ps.info().UnpackTo(&getStuinfo);

定义一个getStuinfo接收返回值。随后执行ps.info().Is<stuInfo>(),判断当前的info内存储的变量类型是不是stuInfo,如果是,那么执行ps.info().UnpackTo(&getStuinfo),将info的内容解析到getStuinfo中。

不论用Any存储或提取任何一个message类型,都是这一套逻辑。

Any接口总结:

接口 功能
has_xxx() 检查该字段是否为空
clear_xxx() 清空该字段的值
xxx() 获取该字段的值
release_xxx() 获取字段内的值后,清空字段内的值
mutable_xxx() 返回指向元素的指针,可通过指针修改元素
PackFrom(message) 将一个message类型转化为Any类型
UnpackTo(&message) 将一个Any类型转化回message
Is<messageType>() 传入模板参数,判断当前Any存储的类型是否和模板参数一样

oneof

有时候,message中的字段在多个中选择一个,就可以使用oneof类型。

示例:

cpp 复制代码
message Person {
    int32 id = 1;
    string name = 2;
    oneof contact {
        int32 qq = 3;
        int32 tel = 4;
    }
}

此处的Person,增加了一个联系方式contact,用户可以在qqtel中二选一。

oneof内部的字段编号,不能与外部的message冲突,比如qqtel的字段编号就不可以是12

另外的,在oneof内部,不允许使用repeated修饰字段,比如这样:

cpp 复制代码
message Person {
    int32 id = 1;
    string name = 2;
    oneof contact {
        repeated int32 qq = 3;  // err
        repeated int32 tel = 4; // err
    }
}

如果设置了repeated,该修饰会失效,变量依然只能保存一个值。

对以下protoc代码编译:

cpp 复制代码
message Person {
    int32 id = 1;
    string name = 2;
    oneof contact {
        int32 qq = 3;
        int32 tel = 4;
    }
}

结果:

cpp 复制代码
// int32 qq = 3;
bool has_qq() const;
private:
bool _internal_has_qq() const;
public:
void clear_qq();
int32_t qq() const;
void set_qq(int32_t value);

public:
// int32 tel = 4;
bool has_tel() const;
private:
bool _internal_has_tel() const;
public:
void clear_tel();
int32_t tel() const;
void set_tel(int32_t value);

void clear_contact();

看起来这个oneof好像没有生效?此处就好像是单独定义了qqtel两个字段,也没有什么oneof的相关方法。

其实这个地方,qqtel确实是直接当作两个字段处理的,但是两个字段有点像C语言中的联合体,最后一次设置的是那个变量,那么存储的就是哪一个类型。

示例:

cpp 复制代码
test_pkg::Person ps;
ps.set_id(123456);
ps.set_name("张三");
ps.set_qq(111111);
ps.set_tel(222222);

if (ps.has_qq())
    cout << "qq: " << ps.qq() << endl;

if (ps.has_tel())
    cout << "tel: " << ps.tel() << endl;

定义了一个Person变量后,先后设置了qq(111111)tel(222222),随后通过has_xxx方法分别检测两个值,随后输出。

输出结果:

cpp 复制代码
tel: 222222

最后只输出了tel,因为后来的tel覆盖了qq,两者是二选一的关系。

当然,oneof类型也不是完全没有接口,比如这个接口:

cpp 复制代码
void clear_contact();

在需要清除oneof内部的值的时候,如果不确定具体是哪一个类型,就无法确定是caler_qq还是clear_tel,如果一个个通过has判断还是有点麻烦的,就可以通过clear_contact一键清除。

所有的oneof的字段都被保存在一个枚举中:

cpp 复制代码
enum ContactCase {
  kQq = 3,
  kTel = 4,
  CONTACT_NOT_SET = 0,
};

ContactCase contact_case() const;

此处的kQqkTel分别就是之前设置的qqtel,而CONTACT_NOT_SET表示未设置参数。

其实在判断当前oneof存储的是哪一个字段时,不用一个个进行has_xxx的判断。通过contact_case函数,会返回一个枚举类型,只需要判断枚举类型是哪一个变量即可:

cpp 复制代码
switch (ps.contact_case()) 
{
	case Person::ContactCase::kQq:
	    // 处理qq字段
	    break;
	case Person::ContactCase::kTel:
	    // 处理tel字段
	    break;
	case Person::ContactCase::CONTACT_NOT_SET:
	    // 没有字段被设置
	    break;
}

oneof接口总结:

接口 功能
clear_xxx() 不论字段存储的是什么,清空该字段的值
xxx_case() 返回当前存储的类型的枚举值

map

map用于创建一个键值对的映射关系,语法:

cpp 复制代码
map<key_tyep, value_type> map_name = N;

其实语法和C++std::map是一样的。

map有以下注意点:

  • key_type:可以是处理floatbytes之外的任意标量类型
  • value_type:可以是任意类型
  • map中的元素是无序的

示例:

cpp 复制代码
message score {
    map<string, int32> chinese = 1;
    map<string, int32> english = 2;
    map<string, int32> math = 3;
}

这是一个记录学生成绩的表格,三个成员分别代表不同科目的成绩。

编译结果以chinese为例:

cpp 复制代码
// map<string, int32> chinese = 1;
int chinese_size() const;
private:
int _internal_chinese_size() const;
public:
void clear_chinese();
private:
const ::PROTOBUF_NAMESPACE_ID::Map< std::string, int32_t >&
    _internal_chinese() const;
::PROTOBUF_NAMESPACE_ID::Map< std::string, int32_t >*
    _internal_mutable_chinese();
public:
const ::PROTOBUF_NAMESPACE_ID::Map< std::string, int32_t >&
    chinese() const;
::PROTOBUF_NAMESPACE_ID::Map< std::string, int32_t >*
    mutable_chinese();
  • chinese_size():获取map内部元素的个数
  • clear_chinese():清空map的元素
  • chinese():获取mapconst引用
  • mutable_chinese():获取map的指针,可通过指针修改map

此处map的类型不是std::map,而是protobuf自己封装的::PROTOBUF_NAMESPACE_ID::Map,但是其用法和std::map没有很大区别。

::PROTOBUF_NAMESPACE_ID::Map也重载了operator[],拿到变量后,可以直接通过[]进行访问。

示例:

cpp 复制代码
test_pkg::Score sc;

auto chinese_mp = *sc.mutable_chinese();
auto english_mp = *sc.mutable_english();
auto math_mp = *sc.mutable_math();

chinese_mp["张三"] = 66;
chinese_mp["李四"] = 88;
chinese_mp["王五"] = 98;

english_mp["张三"] = 97;
english_mp["李四"] = 77;
english_mp["王五"] = 82;

math_mp["张三"] = 46;
math_mp["李四"] = 25;
math_mp["王五"] = 79;

cout << "张三-chinese: " << chinese_mp["张三"] << endl;
cout << "张三-english: " << english_mp["张三"] << endl;
cout << "张三-math: " << math_mp["张三"] << endl;

通过auto chinese_mp = *sc.mutable_chinese(),获取到map,由于mutable_chinese返回的是一个指针,所以还要进行解引用。

随后通过chinese_mp[] 设置与获取元素了。

输出结果:

cpp 复制代码
张三-chinese: 66
张三-english: 97
张三-math: 46

map接口总结:

接口 功能
xxx_size() 获取map内部元素的个数
clear_xxx() 清空map的元素
xxx() 获取mapconst引用
mutable_xxx() 获取map的指针,可通过指针修改map
map.operator[] 直接通过[]获取与设置元素

相关推荐
cwj&xyp23 分钟前
Python(二)str、list、tuple、dict、set
前端·python·算法
无 证明33 分钟前
new 分配空间;引用
数据结构·c++
xiaoshiguang35 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡5 小时前
【C语言】判断回文
c语言·学习·算法
别NULL5 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇5 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
CYBEREXP20086 小时前
MacOS M3源代码编译Qt6.8.1
c++·qt·macos
yuanbenshidiaos6 小时前
c++------------------函数
开发语言·c++
yuanbenshidiaos6 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习6 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法