Protocol Buffers 中 optional 关键字的发展史


Protocol Buffers 中 optional 关键字的发展史

optional 关键字在 Protocol Buffers (ProtoBuf) 中的演变历程,深刻地反映了其设计哲学从"显式严格"向"简洁高效"演进,最终又回归"务实灵活"的实践过程。这是一次经典的、由社区需求驱动的技术演进。

引言:什么是 Protocol Buffers?

在深入探讨 optional 关键字之前,我们有必要先了解它的载体------Protocol Buffers(通常简称为 Protobuf 或 Proto)。

Protocol Buffers 是 Google 开发的一种语言中立、平台无关、可扩展的序列化结构化数据的机制 。它类似于 XML 或 JSON,但更小、更快、更简单。你可以通过编写 .proto 文件来定义你的数据结构,然后使用 Protobuf 编译器 protoc 为各种编程语言(如 C++, Java, Python, Go, C# 等)生成源代码。这些生成的类提供了简单的方法来操作和序列化你的数据。

简单来说,Proto 是接口定义语言(IDL),而 Protobuf 是序列化格式。它的核心优势在于高效的二进制编码和强大的跨版本兼容性(向后和向前兼容),这使得它成为微服务之间通信和数据存储的理想选择。

发展历程概要

时间阶段 版本特征 关键技术事件
Proto2 时代 明确的字段规则 支持 requiredoptional(显式标记)、repeated
Proto3 早期 简化语法,移除显式规则 删除 requiredoptional,所有字段默认为 singular
社区探索期 开发者自行解决方案 采用 oneof 包装、使用 google/protobuf/wrappers.proto
官方实验性支持 Proto3.12 ~ 3.14 重新引入 optional(需 --experimental_allow_proto3_optional 标志)
正式支持 Proto3.15 (2021) 及以后 正式支持 optional 关键字,无需特殊标志

详细发展阶段

1. Proto2:明确的可选与必需

Proto2 中,每个字段的规则都必须被明确指定,语法显式而严格。

  • required: 字段必须被设置,否则消息被视为"未初始化",序列化时会引发错误。
  • optional: 字段可以设置,也可以不设置。这是真正的"可选"语义。
  • repeated: 字段可以重复多次(类似于列表或数组)。

关键特性 :对于 optional 的标量字段,编译器会生成 has_xxx() 方法(如 has_id()),用于在代码中明确区分"字段未设置"和"字段被设置为默认值(如 0"")"。

protobuf 复制代码
// Proto2 语法示例
syntax = "proto2";

message SearchRequest {
  required string query = 1;           // 必须字段
  optional int32 page_number = 2;      // 可选字段,可用 has_page_number() 检查
  optional int32 result_per_page = 3;  // 可选字段
  repeated string snippets = 4;        // 可重复字段
}

设计理念 :强调显式性和数据完整性,旨在减少歧义,确保关键数据在通信中不会丢失。


2. Proto3 早期:简化与"隐含"的可选

为了追求更简单的语法、更小的体积和更好的跨语言支持,Proto3 做出了颠覆性的改变:

  • 移除了 required 关键字:因为它带来了巨大的向后兼容性风险。
  • 移除了 optional 关键字 :所有单数字段(singular)本质上都是可选的,但无法再显式声明。
  • 移除了标量字段的现场存在(Presence)检测 :不再生成 has_xxx() 方法。
  • 移除了默认值设置 :无法再使用 [default = value] 语法。

设计理念的转变 : Proto3 认为,在网络通信中,"未设置"和"设置为类型零值"(如 0, false, "")在语义上通常是等价的。这样做带来了:

  • 更简洁的 API:开发者无需再纠结于字段规则。
  • 更小的编码体积:零值字段不会被序列化,节省了空间。
  • 更安全 :避免了因 required 字段缺失导致的运行时错误。

带来的问题 : 在许多业务场景中,区分"未设置"和"零值"至关重要。例如:

  • 在更新操作中,"不更新该字段"与"将该字段显式重置为零"是两种完全不同的意图。
  • 一个布尔值 false(否) 和 "未提供该信息" 代表不同的状态。

3. 社区的变通方案

为了解决 Proto3 无法区分缺失值与零值的问题,社区探索出几种方案:

方案一:使用包装类型 (Wrapper Types)

Google 提供了 google/protobuf/wrappers.proto,其中包含 Int32Value, BoolValue, StringValue 等包装类型。由于它们是消息(message),自然具有现场存在信息。

protobuf 复制代码
syntax = "proto3";
import "google/protobuf/wrappers.proto";

message Profile {
  string name = 1;
  google.protobuf.Int32Value age = 2;       // 可选的整型字段
  google.protobuf.BoolValue is_subscribed = 3; // 可选的布尔字段
}
  • 优点:官方支持,语义清晰。
  • 缺点:使用稍显繁琐,有微小的序列化开销。
方案二:巧用 oneof 结构

利用 oneof 会生成 case 检查方法的特性,来模拟可选字段。

protobuf 复制代码
syntax = "proto3";

message Profile {
  string name = 1;
  oneof optional_age {    // 使用 oneof 来包装
    int32 age = 2;
  }
}
// 生成代码后,可以通过检查 optional_age 的 case 来判断 age 是否被设置。
  • 优点:无需导入,序列化效率高。
  • 缺点 :是种 Hack,语法不直观,且不能用于 repeated 字段

4. 官方的回归:重新引入 optional

面对社区的强烈需求和实践反馈,Google 最终做出了务实的选择。 可以看看这个issue,关于optional的问题持续争论了几年:github.com/protocolbuf...

  • v3.12 (2020) :开始实验性支持 在 Proto3 中重新使用 optional 关键字,需要通过编译器标志 --experimental_allow_proto3_optional 启用。
  • v3.15 (2021)正式支持 在 Proto3 语法中使用 optional 关键字,无需任何特殊标志。 Protocol Buffers v3.15.0 Release Notes
protobuf 复制代码
syntax = "proto3";

message Profile {
  string name = 1;
  optional int32 age = 2;          // 正式归来!会生成 has_age() 方法
  optional bool is_subscribed = 3; // 可选的布尔字段
}

此时的实现 : 其底层实现机制类似于社区的 oneof 包装方案,但在语法上更加自然和直观,并且解决了 oneof 方案不能用于 repeated 字段的限制。


总结与建议

optional 关键字的"离去"与"归来",是一次典型的实践驱动设计。它说明了优秀的技术标准会根据开发者的真实需求不断演化。

给开发者的建议

  1. 新项目 :直接使用 Proto3 语法,并大胆使用正式的 optional 关键字(确保 Protobuf 编译器版本 ≥ 3.15)。
  2. 老项目迁移 :如果还在使用 oneof 或包装类型,可以计划性地迁移到 optional 关键字上,使代码更简洁、更易读。
  3. 兼容性 :Proto3 的 optional 字段与 Proto2 在线路格式上是兼容的,不同版本的消息定义可以互相解析。
  4. 设计哲学 :即使 optional 回归,Proto3 也永不回归 required 关键字 。这明确了其坚持的观点:required 在跨系统、跨版本的演化中弊大于利。

最终,optional 的回归让 Protocol Buffers 在保持 Proto3 核心优势(简洁、高效、安全)的同时,获得了应对复杂业务场景所需的灵活性,使其变得更加成熟和强大。

相关推荐
卡拉叽里呱啦14 分钟前
缓存-变更事件捕捉、更新策略、本地缓存和热key问题
分布式·后端·缓存
David爱编程19 分钟前
线程调度策略详解:时间片轮转 vs 优先级机制,面试常考!
java·后端
码事漫谈1 小时前
C++继承中的虚函数机制:从单继承到多继承的深度解析
后端
阿冲Runner1 小时前
创建一个生产可用的线程池
java·后端
写bug写bug1 小时前
你真的会用枚举吗
java·后端·设计模式
喵手2 小时前
如何利用Java的Stream API提高代码的简洁度和效率?
java·后端·java ee
掘金码甲哥2 小时前
全网最全的跨域资源共享CORS方案分析
后端
m0_480502642 小时前
Rust 入门 生命周期-next2 (十九)
开发语言·后端·rust
鹿鹿的布丁2 小时前
通过Lua脚本多个网关循环外呼
后端