3、Protobuf数据类型详解:标量类型、枚举、嵌套消息、Any类型、Oneof类型
好,咱们接着聊。上一章我们把 Protobuf 的基本语法和消息定义捋了一遍。这一章,我打算深入聊聊 Protobuf 里的各种数据类型。说白了,就是看看我们能往 .proto 文件里塞些什么东西。
我个人习惯,在开始一个新项目前,一定会先把数据模型想清楚。选对类型,后面能省不少事。你想想看,要是用错了,轻则浪费 Flash 空间,重则解析出错,那可就头疼了。
3.1 标量类型:最基础的积木
标量类型,就是那些最基础的数据单元。比如整数、浮点数、布尔值、字符串。Protobuf 里有一套自己的类型系统,跟 C 语言不完全一样。
我整理了一张表,你写代码时可以直接参考:
| .proto 类型 | C++ 类型 | 说明 | 典型字节数 |
|---|---|---|---|
| double | double | 64位浮点数 | 8 |
| float | float | 32位浮点数 | 4 |
| int32 | int32 | 使用可变长编码,负数效率低 | 1~5 |
| int64 | int64 | 同上,64位版本 | 1~10 |
| uint32 | uint32 | 无符号整数,可变长编码 | 1~5 |
| uint64 | uint64 | 无符号64位 | 1~10 |
| sint32 | int32 | 有符号整数,负数效率高 | 1~5 |
| sint64 | int64 | 有符号64位 | 1~10 |
| fixed32 | uint32 | 固定4字节,比uint32快 | 4 |
| fixed64 | uint64 | 固定8字节 | 8 |
| sfixed32 | int32 | 有符号,固定4字节 | 4 |
| sfixed64 | int64 | 有符号,固定8字节 | 8 |
| bool | bool | 布尔值 | 1 |
| string | string | UTF-8 编码字符串 | 可变 |
| bytes | string | 任意字节序列 | 可变 |
uint32 和 sint32。为什么?因为大部分传感器数据都是小整数,用可变长编码能省不少空间。比如一个 0~127 的值,只占 1 个字节。
这里有个坑,我得提醒你。int32 和 sint32 看起来差不多,但编码方式完全不同。int32 对负数用 64 位编码,效率很低。如果你确定数据可能为负,直接用 sint32。我曾经在一个项目里用 int32 传温度值,结果 -10 度编码出来占了 10 个字节,差点把缓冲区撑爆。
3.2 枚举:给数字起个名字
枚举,说白了就是给整数常量起个名字。在嵌入式里,枚举太常用了。比如设备状态、错误码、命令类型。
enum DeviceState {
DEVICE_UNKNOWN = 0;
DEVICE_IDLE = 1;
DEVICE_BUSY = 2;
DEVICE_ERROR = 3;
}
message StatusReport {
DeviceState state = 1;
uint32 error_code = 2;
}
嗯,这里要注意几点:
- 枚举值必须从 0 开始。0 是默认值,也是序列化时的默认值。
- 枚举值之间可以有空隙,比如 0, 1, 5, 10 都行。
- 枚举值可以重复,但我不建议这么干,容易混淆。
3.3 嵌套消息:把复杂变简单
嵌套消息,就是在一个消息里定义另一个消息。这玩意儿在构建复杂数据结构时特别好用。
message GPSData {
message Coordinate {
double latitude = 1;
double longitude = 2;
}
Coordinate current = 1;
repeated Coordinate track = 2;
float altitude = 3;
}
你想想看,如果没有嵌套,你得把 Coordinate 定义成全局的。在大型项目里,全局命名空间会变得很乱。嵌套消息把相关的类型收拢在一起,代码结构清晰多了。
我个人习惯,如果一个消息类型只被另一个消息使用,我就把它定义成嵌套的。这样别人看代码时,一眼就能看出依赖关系。
3.4 Any 类型:万能口袋
Any 类型,说白了就是一个万能口袋。它可以装任意类型的消息。这在处理不确定数据时特别有用。
import "google/protobuf/any.proto";
message Event {
string event_type = 1;
google.protobuf.Any payload = 2;
}
使用 Any 时,需要包含 google/protobuf/any.proto。序列化时,Any 会同时保存原始消息的二进制数据和类型信息。
3.5 Oneof 类型:互斥的选择
Oneof,就是一组互斥的字段。同一时刻,只能设置其中一个字段。这玩意儿在表示「多种可能」时特别好用。
message Command {
oneof cmd {
uint32 set_speed = 1;
bool stop = 2;
string reset = 3;
}
}
你看,一个命令要么是设置速度,要么是停止,要么是复位。不可能同时是两种。用 Oneof 就完美表达了这种互斥关系。
Oneof 的实现原理其实很简单:所有字段共享同一块内存。设置一个字段时,会自动清除其他字段的值。在 C++ 里,你可以用 has_xxx() 方法检查哪个字段被设置了。
3.6 实战选择建议
好了,类型都讲完了。最后我总结一下在 ESP32 上的选择策略:
- 整数:优先用 uint32 或 sint32。数据范围确定且不大时,用 fixed32 提高解析速度。
- 浮点数:用 float 就够了。double 精度高但占空间,除非 GPS 坐标这种高精度需求。
- 字符串:用 string。注意编码必须是 UTF-8。
- 枚举:状态机、错误码、命令类型,都用枚举。可读性比数字好太多。
- 嵌套消息:复杂数据结构用嵌套,保持代码整洁。
- Any:少用。只在确实需要动态类型时用。
- Oneof:互斥字段用 Oneof,节省内存,表达清晰。
嗯,这一章就到这里。下一章我们聊聊 Protobuf 的编码原理,看看那些可变长整数到底是怎么压缩的。到时候你会发现,原来 1 个字节能存 127 个数,还挺有意思的。