應用程式注意事項:欄位存在
背景
欄位存在是指 protobuf 欄位是否具有值的概念。protobuf 的存在有兩種不同的表現形式:隱含存在,其中產生的訊息 API 僅儲存欄位值;以及明確存在,其中 API 也儲存欄位是否已設定。
從歷史上看,proto2 主要遵循明確存在,而 proto3 僅公開隱含存在語意。使用 optional
標籤定義的基本類型(數值、字串、位元組和列舉)的單數 proto3 欄位具有明確存在,就像 proto2 一樣(此功能預設為啟用,自 3.15 版發行以來)。
注意
我們建議始終為 proto3 基本類型新增optional
標籤。這為版本提供了更順暢的路徑,版本預設使用明確存在。存在規範
存在規範定義了在API 表示法和序列化表示法之間轉換的語意。隱含存在規範依賴於欄位值本身來在(反)序列化時做出決策,而明確存在規範則依賴於明確追蹤狀態。
標籤值串流 (Wire Format) 序列化中的存在
Wire Format 是一種標記的、自我分隔值的串流。根據定義,Wire Format 表示一系列存在的值。換句話說,在序列化中找到的每個值都表示一個存在的欄位;此外,序列化不包含有關不存在的值的任何資訊。
proto 訊息的產生 API 包含(反)序列化定義,這些定義在 API 類型和定義上存在的 (標籤、值) 對串流之間進行轉換。此轉換旨在向前和向後相容訊息定義的變更;但是,這種相容性在還原序列化 Wire Format 訊息時引入了一些(可能令人驚訝的)考量。
- 在序列化時,如果具有隱含存在的欄位包含其預設值,則不會序列化這些欄位。
- 對於數值類型,預設值為 0。
- 對於列舉,預設值為零值列舉值。
- 對於字串、位元組和重複欄位,預設值為零長度值。
- 「空」長度分隔值(例如空字串)可以在序列化值中有效表示:該欄位是「存在的」,就表示它出現在 Wire Format 中。但是,如果產生的 API 不追蹤存在,則這些值可能不會重新序列化;即,空欄位在序列化往返之後可能「不存在」。
- 在還原序列化時,重複的欄位值可能會根據欄位定義以不同的方式處理。
- 重複的
repeated
欄位通常會附加到欄位的 API 表示法。(請注意,序列化封裝的重複欄位只會在標籤串流中產生一個長度分隔值。) - 重複的
optional
欄位值遵循「最後一個獲勝」的規則。
- 重複的
oneof
欄位公開了 API 層級的不變性,即一次只設定一個欄位。但是,Wire Format 可能包含多個 (標籤、值) 對,這些對概念上屬於oneof
。與optional
欄位類似,產生的 API 遵循「最後一個獲勝」的規則。- 對於產生的 proto2 API 中的列舉欄位,不會傳回超出範圍的值。但是,超出範圍的值可以作為 API 中的未知欄位儲存,即使 Wire Format 標籤已被識別。
具名字段映射 格式中的存在
Protobuf 可以以人類可讀的文字形式表示。兩種值得注意的格式是 TextFormat(由產生的訊息 DebugString
方法產生的輸出格式)和 JSON。
這些格式有自己的正確性要求,並且通常比標籤值串流格式更嚴格。但是,TextFormat 更接近於模仿 Wire Format 的語意,並且在某些情況下,確實提供了類似的語意(例如,將重複的名稱-值映射附加到重複欄位)。特別是,與 Wire Format 類似,TextFormat 僅包含存在的欄位。
但是,JSON 是一種嚴格得多的格式,並且無法有效表示 Wire Format 或 TextFormat 的某些語意。
- 值得注意的是,JSON 元素在語意上是無序的,並且每個成員都必須具有唯一的名稱。這與 TextFormat 對於重複欄位的規則不同。
- JSON 可能包含「不存在」的欄位,這與其他格式的隱含存在規範不同
- JSON 定義了一個
null
值,可用於表示已定義但不存在的欄位。 - 重複的欄位值可以包含在格式化輸出中,即使它們等於預設值(空清單)。
- JSON 定義了一個
- 由於 JSON 元素是無序的,因此無法明確地解釋「最後一個獲勝」規則。
- 在大多數情況下,這沒問題:JSON 元素必須具有唯一的名稱:重複的欄位值不是有效的 JSON,因此它們不需要像 TextFormat 那樣被解析。
- 但是,這表示可能無法明確地解釋
oneof
欄位:如果存在多個案例,它們是無序的。
從理論上講,JSON 可以以語意保留的方式表示存在。但是,在實務中,存在的正確性可能會因實作選擇而異,尤其是在選擇 JSON 作為與未使用 protobuf 的用戶端互通的方式時。
Proto2 API 中的存在
此表概述了在 proto2 API 中是否追蹤欄位的存在(對於產生的 API 和使用動態反射兩者)
欄位類型 | 明確存在 |
---|---|
單數數值(整數或浮點數) | ✔️ |
單數列舉 | ✔️ |
單數字串或位元組 | ✔️ |
單數訊息 | ✔️ |
重複 | |
Oneofs | ✔️ |
Maps |
單數欄位(所有類型)在產生的 API 中明確追蹤存在。產生的訊息介面包含查詢欄位是否存在的方法。例如,欄位 foo
具有對應的 has_foo
方法。(特定名稱遵循與欄位存取器相同的語言特定命名慣例。)這些方法有時在 protobuf 實作中被稱為「hazzers」。
與單數欄位類似,oneof
欄位明確追蹤哪個成員(如果有)包含值。例如,考慮以下 oneof
範例
oneof foo {
int32 a = 1;
float b = 2;
}
根據目標語言,產生的 API 通常會包含多種方法
- oneof 的 hazzer:
has_foo
- oneof case 方法:
foo
- 成員的 Hazzer:
has_a
、has_b
- 成員的 Getter:
a
、b
重複欄位和 Map 不追蹤存在:空的和不存在的重複欄位之間沒有區別。
Proto3 API 中的存在
此表概述了在 proto3 API 中是否追蹤欄位的存在(對於產生的 API 和使用動態反射兩者)
欄位類型 | optional | 明確存在 |
---|---|---|
單數數值(整數或浮點數) | 否 | |
單數數值(整數或浮點數) | 是 | ✔️ |
單數列舉 | 否 | |
單數列舉 | 是 | ✔️ |
單數字串或位元組 | 否 | |
單數字串或位元組 | 是 | ✔️ |
單數訊息 | 否 | ✔️ |
單數訊息 | 是 | ✔️ |
重複 | 不適用 | |
Oneofs | 不適用 | ✔️ |
Maps | 不適用 |
與 proto2 API 類似,proto3 不會為重複欄位明確追蹤存在。如果沒有 optional
標籤,proto3 API 也不會為基本類型(數值、字串、位元組和列舉)追蹤存在。Oneof 欄位肯定會公開存在,儘管可能不會產生與 proto2 API 中相同的一組 hazzer 方法。
這種在沒有 optional
標籤的情況下不追蹤存在的預設行為與 proto2 行為不同。我們重新引入了明確存在作為 2023 版中的預設值。我們建議將 optional
欄位與 proto3 一起使用,除非您有特定的理由不這樣做。
在隱含存在規範下,預設值在序列化方面與「不存在」同義。為了概念上「清除」欄位(使其不會被序列化),API 使用者會將其設定為預設值。
在隱含存在下的列舉類型欄位的預設值是相應的 0 值列舉值。在 proto3 語法規則下,所有列舉類型都必須具有映射到 0 的列舉值。按照慣例,這是一個 UNKNOWN
或類似命名的列舉值。如果零值在概念上超出應用程式的有效值範圍,則此行為可以被認為等同於明確存在。
語意差異
當設定預設值時,隱含存在序列化規範會導致與明確存在追蹤規範的明顯差異。對於具有數值、列舉或字串類型的單數欄位
- 隱含存在規範
- 預設值不會序列化。
- 預設值不會從中合併。
- 若要「清除」欄位,請將其設定為預設值。
- 預設值可能表示
- 欄位已明確設定為其預設值,這在應用程式特定值範圍內是有效的;
- 欄位透過設定其預設值而在概念上「清除」;或
- 欄位從未設定。
has_
方法未產生(但請參閱此清單後的注意事項)
- 明確存在規範
- 明確設定的值始終會序列化,包括預設值。
- 未設定的欄位永遠不會從中合併。
- 明確設定的欄位(包括預設值)會從中合併。
- 產生的
has_foo
方法指示欄位foo
是否已設定(且未清除)。 - 必須使用產生的
clear_foo
方法來清除(即,取消設定)值。
注意
在大多數情況下,不會為隱含成員產生Has_
方法。此行為的例外是 Dart,它使用 proto3 proto 綱要檔案產生 has_
方法。合併考量
在隱含存在規則下,目標欄位實際上不可能從其預設值合併(使用 protobuf 的 API 合併功能)。這是因為預設值會被跳過,類似於隱含存在序列化規範。合併只會使用來自更新(從中合併)訊息的未跳過值來更新目標(合併到)訊息。
合併行為的差異對依賴部分「修補程式」更新的協定具有進一步的影響。如果未追蹤欄位存在,則僅更新修補程式無法表示更新為預設值,因為僅會從中合併非預設值。
在這種情況下,更新以設定預設值需要一些外部機制,例如 FieldMask
。但是,如果已追蹤存在,則所有明確設定的值(即使是預設值)都將合併到目標中。
變更相容性考量
在明確存在和隱含存在之間變更欄位對於 Wire Format 中序列化的值來說是二進位相容的變更。但是,訊息的序列化表示法可能會有所不同,具體取決於用於序列化的訊息定義版本。具體來說,當「傳送者」明確將欄位設定為其預設值時
- 遵循隱含存在規範的序列化值不包含預設值,即使它已明確設定。
- 遵循明確存在規範的序列化值包含每個「存在」的欄位,即使它包含預設值。
此變更可能是安全或不安全的,具體取決於應用程式的語意。例如,考慮兩個具有不同訊息定義版本的用戶端。
用戶端 A 使用此訊息定義,該定義遵循欄位 foo
的明確存在序列化規範
syntax = "proto3";
message Msg {
optional int32 foo = 1;
}
用戶端 B 使用相同訊息的定義,但它遵循無存在規範
syntax = "proto3";
message Msg {
int32 foo = 1;
}
現在,考慮一個場景,其中用戶端 A 觀察到 foo
的存在,因為用戶端透過還原序列化和重新序列化來重複交換「相同」的訊息
// Client A:
Msg m_a;
m_a.set_foo(1); // non-default value
assert(m_a.has_foo()); // OK
Send(m_a.SerializeAsString()); // to client B
// Client B:
Msg m_b;
m_b.ParseFromString(Receive()); // from client A
assert(m_b.foo() == 1); // OK
Send(m_b.SerializeAsString()); // to client A
// Client A:
m_a.ParseFromString(Receive()); // from client B
assert(m_a.foo() == 1); // OK
assert(m_a.has_foo()); // OK
m_a.set_foo(0); // default value
Send(m_a.SerializeAsString()); // to client B
// Client B:
Msg m_b;
m_b.ParseFromString(Receive()); // from client A
assert(m_b.foo() == 0); // OK
Send(m_b.SerializeAsString()); // to client A
// Client A:
m_a.ParseFromString(Receive()); // from client B
assert(m_a.foo() == 0); // OK
assert(m_a.has_foo()); // FAIL
如果用戶端 A 依賴 foo
的明確存在,則透過用戶端 B 的「往返」將從用戶端 A 的角度來看是有損的。在此範例中,這不是安全的變更:用戶端 A 需要(透過 assert
)欄位存在;即使沒有透過 API 進行任何修改,該要求在值和對等相關案例中也會失敗。
如何在 Proto3 中啟用明確存在
以下是在 proto3 中使用欄位追蹤支援的一般步驟
- 將
optional
欄位新增至.proto
檔案。 - 執行
protoc
(至少 v3.15,或 v3.12 使用--experimental_allow_proto3_optional
旗標)。 - 在應用程式程式碼中使用產生的「hazzer」方法和「clear」方法,而不是比較或設定預設值。
.proto
檔案變更
這是一個 proto3 訊息範例,其欄位同時遵循無存在和明確存在語意
syntax = "proto3";
package example;
message MyMessage {
// implicit presence:
int32 not_tracked = 1;
// Explicit presence:
optional int32 tracked = 2;
}
protoc
叫用
proto3 訊息的存在追蹤預設為啟用,自 v3.15.0 版本起,直到 v3.12.0 為止,使用 protoc 進行存在追蹤時需要 --experimental_allow_proto3_optional
旗標。
使用產生的程式碼
具有明確存在(optional
標籤)的 proto3 欄位的產生程式碼將與 proto2 檔案中的程式碼相同。
這是以下「隱含存在」範例中使用的定義
syntax = "proto3";
package example;
message Msg {
int32 foo = 1;
}
這是以下「明確存在」範例中使用的定義
syntax = "proto3";
package example;
message Msg {
optional int32 foo = 1;
}
在範例中,函式 GetProto
建構並傳回類型為 Msg
且內容未指定的訊息。
C++ 範例
隱含存在
Msg m = GetProto();
if (m.foo() != 0) {
// "Clear" the field:
m.set_foo(0);
} else {
// Default value: field may not have been present.
m.set_foo(1);
}
明確存在
Msg m = GetProto();
if (m.has_foo()) {
// Clear the field:
m.clear_foo();
} else {
// Field is not present, so set it.
m.set_foo(1);
}
C# 範例
隱含存在
var m = GetProto();
if (m.Foo != 0) {
// "Clear" the field:
m.Foo = 0;
} else {
// Default value: field may not have been present.
m.Foo = 1;
}
明確存在
var m = GetProto();
if (m.HasFoo) {
// Clear the field:
m.ClearFoo();
} else {
// Field is not present, so set it.
m.Foo = 1;
}
Go 範例
隱含存在
m := GetProto()
if m.Foo != 0 {
// "Clear" the field:
m.Foo = 0
} else {
// Default value: field may not have been present.
m.Foo = 1
}
明確存在
m := GetProto()
if m.Foo != nil {
// Clear the field:
m.Foo = nil
} else {
// Field is not present, so set it.
m.Foo = proto.Int32(1)
}
Java 範例
這些範例使用 Builder
來示範清除。僅檢查存在並從 Builder
取得值遵循與訊息類型相同的 API。
隱含存在
Msg.Builder m = GetProto().toBuilder();
if (m.getFoo() != 0) {
// "Clear" the field:
m.setFoo(0);
} else {
// Default value: field may not have been present.
m.setFoo(1);
}
明確存在
Msg.Builder m = GetProto().toBuilder();
if (m.hasFoo()) {
// Clear the field:
m.clearFoo()
} else {
// Field is not present, so set it.
m.setFoo(1);
}
Python 範例
隱含存在
m = example.Msg()
if m.foo != 0:
# "Clear" the field:
m.foo = 0
else:
# Default value: field may not have been present.
m.foo = 1
明確存在
m = example.Msg()
if m.HasField('foo'):
# Clear the field:
m.ClearField('foo')
else:
# Field is not present, so set it.
m.foo = 1
Ruby 範例
隱含存在
m = Msg.new
if m.foo != 0
# "Clear" the field:
m.foo = 0
else
# Default value: field may not have been present.
m.foo = 1
end
明確存在
m = Msg.new
if m.has_foo?
# Clear the field:
m.clear_foo
else
# Field is not present, so set it.
m.foo = 1
end
Javascript 範例
隱含存在
var m = new Msg();
if (m.getFoo() != 0) {
// "Clear" the field:
m.setFoo(0);
} else {
// Default value: field may not have been present.
m.setFoo(1);
}
明確存在
var m = new Msg();
if (m.hasFoo()) {
// Clear the field:
m.clearFoo()
} else {
// Field is not present, so set it.
m.setFoo(1);
}
Objective-C 範例
隱含存在
Msg *m = [Msg message];
if (m.foo != 0) {
// "Clear" the field:
m.foo = 0;
} else {
// Default value: field may not have been present.
m.foo = 1;
}
明確存在
Msg *m = [Msg message];
if ([m hasFoo]) {
// Clear the field:
[m clearFoo];
} else {
// Field is not present, so set it.
m.foo = 1;
}
速查表
Proto2
是否追蹤欄位存在?
欄位類型 | 已追蹤? |
---|---|
單數欄位 | 是 |
單數訊息欄位 | 是 |
oneof 中的欄位 | 是 |
重複欄位 & map | 否 |
Proto3
是否追蹤欄位存在?
欄位類型 | 已追蹤? |
---|---|
其他單數欄位 | 如果定義為 optional |
單數訊息欄位 | 是 |
oneof 中的欄位 | 是 |
重複欄位 & map | 否 |
2023 版
是否追蹤欄位存在?
欄位類型(依遞減優先順序) | 已追蹤? |
---|---|
重複欄位 & map | 否 |
訊息和 Oneof 欄位 | 是 |
如果 features.field_presence 設定為 IMPLICIT ,則其他單數欄位 | 否 |
所有其他欄位 | 是 |