null 許容のセッター/ゲッターはサポートされない

Protobufがnullableなセッターとゲッターをサポートしない理由について説明します

いくつかの人々から、protobufが、nullを許容する言語(特にKotlin、C#、Rust)において、nullableなゲッター/セッターをサポートしてほしいというフィードバックを受けています。これはこれらの言語を使用する人々にとって役立つ機能であると思われますが、設計上のトレードオフがあり、Protobufチームはそれらを実装しないという選択をしました。

明示的な存在は、nullabilityの伝統的な概念に直接マッピングされるものではありません。これは微妙ですが、明示的な存在の哲学は「フィールドはnullを許容しないが、フィールドが明示的に値を割り当てられたかどうかを検出できる。通常アクセスでは、値が割り当てられていない場合は何らかのデフォルト値が返されるが、必要に応じてフィールドがアクティブに書き込まれたかどうかを確認できる」という考え方に近いです。

nullableなフィールドを持たない最大の理由は、.protoファイルで指定されたデフォルト値の意図された動作です。設計上、設定されていないフィールドに対してゲッターを呼び出すと、そのフィールドのデフォルト値が返されます。

注: C#では、メッセージフィールドはnullableとして扱われます。この他の言語との矛盾は、不変なメッセージの欠如に起因しており、共有される不変なデフォルトインスタンスを作成することが不可能であるためです。メッセージフィールドはデフォルト値を持つことができないため、これに機能的な問題はありません。

例として、この.protoファイルを考えてみましょう。

message Msg { Child child = 1; }
message Child { Grandchild grandchild = 1; }
message Grandchild { int32 foo = 1 [default = 72]; }

そして対応するKotlinのゲッター

// 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のゲッター

// 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)

nullableなゲッターが存在する場合、必然的にユーザーが指定したデフォルト値は無視され(代わりにnullが返され)、驚くべき、一貫性のない動作につながります。nullableなゲッターのユーザーがフィールドのデフォルト値にアクセスしたい場合、nullが返されたときにデフォルトを使用するための独自のカスタム処理を記述する必要があり、これによりnullableなゲッターによるよりクリーンで簡単なコードという想定される利点が失われます。

同様に、動作が直感的ではないため、nullableなセッターは提供していません。セット操作の後にゲット操作を行っても常に同じ値が返されるとは限りませんし、セット操作がフィールドのhas-bitに影響を与えるのは時々だけです。

メッセージ型のフィールドは常に明示的な存在フィールド(has-er付き)であることに注意してください。Proto3では、スカラーフィールドは明示的にoptionalとマークされない限り、デフォルトで暗黙の存在(has-erなし)を持ちますが、Proto2は暗黙の存在をサポートしていません。エディションでは、暗黙の存在機能が使用されない限り、明示的な存在がデフォルトの動作です。ほとんどすべてのフィールドが明示的な存在を持つという将来の期待により、nullableなゲッターに伴う人間工学上の懸念は、Proto3ユーザーにとってよりも大きな懸念になると予想されます。

これらの問題により、nullableなセッター/ゲッターはデフォルト値の使用方法を根本的に変えることになります。その潜在的な有用性は理解していますが、それによってもたらされる不整合と複雑さに見合わないと判断しました。