不支持可为 null 的 Setter/Getter
我们收到的反馈是,一些人希望 protobuf 能在他们选择的 null-friendly 语言(特别是 Kotlin、C# 和 Rust)中支持可为 null 的 getter/setter。虽然对于使用这些语言的人来说,这确实是一个有用的功能,但这一设计选择存在一些权衡,导致 Protobuf 团队决定不予实现。
显式存在性(Explicit presence)这一概念,并不能直接映射到传统的可为 null (nullability) 的概念。这是一个微妙之处,显式存在性哲学更接近于“字段不可为 null,但你可以检测字段是否被显式赋值。如果字段未被赋值,正常访问会看到某个默认值,但你可以在需要时检查该字段是否被主动写入过”。
不设置可为 null 字段的最大原因是 .proto 文件中指定的默认值的预期行为。按照设计,对一个未设置的字段调用 getter 将返回该字段的默认值。
注意: C# 确实将消息字段视为可为 null。这种与其他语言的不一致性源于其缺少不可变消息(immutable messages),这使得创建共享的不可变默认实例成为不可能。因为消息字段不能有默认值,所以这样做在功能上没有问题。
举个例子,考虑这个 .proto 文件
message Msg { Child child = 1; }
message Child { Grandchild grandchild = 1; }
message Grandchild { int32 foo = 1 [default = 72]; }
以及相应的 Kotlin getter
// With our API where getters are always non-nullable:
msg.child.grandchild.foo == 72
// With nullable submessages the ?. operator fails to get the default value:
msg?.child?.grandchild?.foo == null
// Or verbosely duplicating the default value at the usage site:
(msg?.child?.grandchild?.foo ?: 72)
以及相应的 Rust getter
// With our API:
msg.child().grandchild().foo() // == 72
// Where every getter is an Option<T>, verbose and no default observed
msg.child().map(|c| c.grandchild()).map(|gc| gc.foo()) // == Option::None
// For the rare situations where code may want to observe both the presence and
// value of a field, the _opt() accessor which returns a custom Optional type
// can also be used here (the Optional type is similar to Option except can also
// be aware of the default value):
msg.child().grandchild().foo_opt() // Optional::Unset(72)
如果存在可为 null 的 getter,它必然会忽略用户指定的默认值(以便返回 null),这会导致出人意料且不一致的行为。如果可为 null getter 的用户想要访问字段的默认值,他们将不得不编写自己的自定义处理逻辑,以便在返回 null 时使用默认值,这反而消除了使用 null getter 本应带来的更简洁/更易用代码的好处。
同样,我们不提供可为 null 的 setter,因为其行为会不直观。执行 set 操作后再执行 get 操作,可能不会得到相同的值;而调用 set 操作也只会偶尔影响字段的 has-bit。
请注意,消息类型的字段始终是显式存在性字段(带有 has-bit)。Proto3 默认标量字段为隐式存在性(没有 has-bit),除非它们被显式标记为 optional,而 Proto2 不支持隐式存在性。在 Editions 中,除非使用隐式存在性特性,否则显式存在性是默认行为。我们预期未来几乎所有字段都将具有显式存在性,因此,与可为 null getter 相关的易用性问题,预计会比 Proto3 用户之前可能遇到的更为突出。
由于这些问题,可为 null 的 setter/getter 会从根本上改变默认值的使用方式。虽然我们理解其潜在的用处,但我们认为它所引入的不一致性和复杂性得不偿失。