不支持可为 null 的 Setter/Getter
我们收到了一些反馈,一些人希望 Protobuf 能在他们选择的支持空值的语言中(特别是 Kotlin、C# 和 Rust)支持可空 getter/setter。虽然这对于使用这些语言的人来说似乎是一个有用的功能,但这一设计选择存在权衡,导致 Protobuf 团队决定不实现它们。
显式存在并不是一个直接映射到传统空值概念的概念。它很微妙,但显式存在哲学更接近于“字段不可空,但你可以检测字段是否被显式赋值。如果未赋值,则正常访问会看到一些默认值,但你可以在需要时检查字段是否被主动写入。”
不支持可空字段的最大原因是 .proto 文件中指定默认值的预期行为。按照设计,对未设置字段调用 getter 将返回该字段的默认值。
注意: C# 确实将消息字段视为可空的。这种与其他语言不一致之处源于缺少不可变消息,这使得无法创建共享的不可变默认实例。由于消息字段不能有默认值,因此这样做没有功能上的问题。
例如,考虑这个 .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)
如果存在可空 getter,它必然会忽略用户指定的默认值(转而返回 null),这将导致令人惊讶和不一致的行为。如果可空 getter 的用户想要访问字段的默认值,他们将不得不编写自己的自定义处理程序,在返回 null 时使用默认值,这消除了可空 getter 带来的更简洁/更容易代码的所谓好处。
同样,我们不提供可空 setter,因为其行为会不直观。执行一次 set 后再 get 不会总是返回相同的值,并且调用 set 只会在某些时候影响字段的 has-bit。
请注意,消息类型字段始终是显式存在字段(带有 hazzer)。Proto3 默认情况下,标量字段具有隐式存在(不带 hazzer),除非它们被显式标记为 optional,而 Proto2 不支持隐式存在。通过 Editions,显式存在是默认行为,除非使用了隐式存在功能。随着几乎所有字段都将具有显式存在的预期,可空 getter 带来的人体工程学问题预计将比 Proto3 用户更受关注。
由于这些问题,可空 setter/getter 将彻底改变默认值的使用方式。虽然我们理解其可能的实用性,但我们已决定其引入的不一致性和困难不值得。