没有 optional 的世界¶
在很长的一段时间里,ProtoBuf3 不支持 ProtoBuf2 当中 optional 关键字。正如我在gRPC 与『面向扩展编程』 中提到的,我认为所有字段均为可选,有益于未来的扩展性,避免加入一个新的字段,导致调用方的代码类型出错。
比如在 TypeScript 当中,如果一个字段未修饰为 optional,那么初始化这个 object 就必须要完整的定义所有字段。如果有无数个调用方在使用这个 Request 类型,显然就很难在其中增加字段了。
一般来说,这个时候我们希望让一个字段 nullable 有两种方法:
- 使用 oneof 大法。
message Foo { int32 bar = 1; oneof optional_baz { int32 baz = 2; } }
- 使用 google.protobuf.wrappers ,作为基础类型的包装类,在基础数值上引入 null 的状态,就像 Java 当中 Integer 与 int 的关系。
message Foo { int32 bar = 1; google.protobuf.Int32Value baz = 2; }
上面的方法虽然不是很优雅,但可以 work!!!
重新引入 optional¶
不过最近 ProtoBuf v3.15.0 正式重新引入了 optional 关键字,着实令我有点 mind blowing。
Release Protocol Buffers v3.15.0 · protocolbuffers/protobuf
Protocol Compiler
Optional fields for proto3 are enabled by default, and no longer require
the --experimental_allow_proto3_optional flag.
C++
MessageDifferencer: fixed bug when using custom igno...
https://github.com/protocolbuffers/protobuf/releases/tag/v3.15.0
syntax = "proto3";
message Request {
string arg1 = 1;
optional string arg2 = 2;
}
❓
那么不带 optional 的字段到底是可选还是必选?
带着这个疑问,基于 TypeScript 和 Go 的 gRPC 做了一些实验,optional 和 non-optional 字段的不同表现,总结出如下的表现:
- 普通字段:
- Go 当中会使用基础类型表示,默认就具有了零值。
- TypeScript 当中也会将其作为基础类型表示,虽然 Node 中没有默认零值,但是 gRPC client 和 server 都会在接收到 proto message 的时候将所有未定义的字段初始化为零值。
- optional 字段:
- Go 当中会使用指针类型 field *T 表示,不再具有零值。
- TypeScript 当中也会将其作为可选类型 field?: T 表示,不需要去初始化。
这样就很清晰了,在没有 optional 的时候,所有字段并不是真正意义上的可选字段,只是对于调用方来说,可以选择性地初始化部分字段,但是对于被调用方来说,需要依赖框架或者语言本身的特性去补齐为定义字段,这样在使用的时候才不会产生空指针错误。
如果一个字段是可选字段,明确有一个空的状态,这个状态需要调用和被调用双方都能感知,就需要将其标记为 optional。