不支援可為 Null 的 Setter/Getter

說明為何 Protobuf 不支援可為 Null 的 Setter 和 Getter

我們已收到使用者反應,希望 protobuf 在他們選擇的 Null-friendly 語言(特別是 Kotlin、C# 和 Rust)中支援可為 Null 的 getter/setter。雖然這對於使用這些語言的使用者來說似乎是一個有用的功能,但此設計選擇存在權衡考量,這導致 Protobuf 團隊選擇不實作它們。

不使用可為 Null 欄位的最大原因是 .proto 檔案中指定的預設值的預期行為。根據設計,呼叫未設定欄位的 getter 將會傳回該欄位的預設值。

注意: C# 確實將訊息欄位視為可為 Null。與其他語言的這種不一致性源於缺乏不可變的訊息,這使得建立共用的不可變預設實例變得不可能。由於訊息欄位不能有預設值,因此這不存在功能性問題。

例如,考慮這個 .proto 檔案

message Msg { optional Child child = 1; }
message Child { optional Grandchild grandchild = 1; }
message Grandchild { optional 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。

請注意,訊息類型欄位始終是顯式存在欄位(具有 hazzers)。Proto3 預設為純量欄位具有隱式存在性(沒有 hazzers),除非它們被明確標記為 optional,而 Proto2 不支援隱式存在性。透過 版本,顯式存在性是預設行為,除非使用了隱式存在性功能。由於預期幾乎所有欄位都將具有顯式存在性,因此與可為 Null getter 相關的人體工學問題預計將比 Proto3 使用者可能遇到的問題更令人擔憂。

由於這些問題,可為 Null 的 setter/getter 將徹底改變預設值的使用方式。雖然我們理解其可能的實用性,但我們已決定不值得為此引入不一致性和困難。