應用筆記:欄位存在性
背景
欄位存在性是protobuf欄位是否具有值的概念。protobufs的存在性有兩種不同的表現形式:隱式存在性,其中生成的message API(僅)儲存欄位值;和顯式存在性,其中API還儲存欄位是否已設定。
注意
我們建議始終為proto3基本型別新增optional標籤。這為Editions提供了一條更平滑的路徑,Editions預設使用顯式存在性。存在性規範
存在性規範定義了在API表示和序列化表示之間進行轉換的語義。隱式存在性規範依賴於欄位值本身在(反)序列化時做出決策,而顯式存在性規範則依賴於顯式跟蹤狀態。
在標籤-值流(線格式)序列化中的存在性
線格式是標記化的、自定界值的流。根據定義,線格式表示一系列存在的值。換句話說,序列化中找到的每個值都表示一個存在的欄位;此外,序列化不包含有關不存在值的資訊。
proto訊息的生成API包括(反)序列化定義,這些定義在API型別和定義上存在的(標籤,值)對流之間進行轉換。此轉換旨在在訊息定義的更改中實現前向和後向相容;但是,當反序列化線格式訊息時,這種相容性引入了一些(可能令人驚訝的)考量。
- 序列化時,如果帶有隱式存在性的欄位包含其預設值,則不會被序列化。
- 對於數值型別,預設值為0。
- 對於列舉,預設值為零值列舉器。
- 對於字串、位元組和重複欄位,預設值為零長度值。
- “空”長度定界值(如空字串)可以在序列化值中有效表示:欄位是“存在的”,因為它出現線上格式中。但是,如果生成的API不跟蹤存在性,則這些值可能不會被重新序列化;即,空欄位在序列化往返後可能“不存在”。
- 反序列化時,重複欄位值的處理方式可能因欄位定義而異。
- 重複的
repeated欄位通常會附加到欄位的API表示中。(請注意,序列化packed重複欄位在標籤流中僅產生一個長度定界值。) - 重複的
optional欄位值遵循“最後一個獲勝”的規則。
- 重複的
oneof欄位公開了API級別的不變性,即一次只設置一個欄位。但是,線格式可能包含多個名義上屬於oneof的(標籤,值)對。類似於optional欄位,生成的API遵循“最後一個獲勝”的規則。- 對於生成的proto2 API中的列舉欄位,不會返回超出範圍的值。但是,超出範圍的值可能作為未知欄位儲存在API中,即使線格式標籤已被識別。
在命名欄位對映格式中的存在性
Protobuf可以以人類可讀的文字形式表示。兩種值得注意的格式是TextFormat(由生成的message DebugString方法產生的輸出格式)和JSON。
這些格式有其自身的正確性要求,通常比標籤值流格式更嚴格。然而,TextFormat更緊密地模仿了線格式的語義,並且在某些情況下提供了相似的語義(例如,將重複的名稱-值對映附加到重複欄位)。特別是,與線格式類似,TextFormat只包含存在的欄位。
然而,JSON是一種更嚴格的格式,不能有效表示線格式或TextFormat的一些語義。
- 值得注意的是,JSON元素在語義上是無序的,並且每個成員必須具有唯一的名稱。這與TextFormat關於重複欄位的規則不同。
- JSON可能包含“不存在”的欄位,這與用於其他格式的隱式存在性規範不同。
- JSON定義了一個
null值,可用於表示一個已定義但不存在的欄位。 - 重複欄位值可能包含在格式化輸出中,即使它們等於預設值(空列表)。
- JSON定義了一個
- 由於JSON元素是無序的,因此無法明確解釋“最後一個獲勝”規則。
- 在大多數情況下,這沒有問題:JSON元素必須具有唯一的名稱:重複欄位值不是有效的JSON,因此它們不需要像TextFormat那樣解決。
- 但是,這意味著可能無法明確解釋
oneof欄位:如果存在多個情況,它們是無序的。
理論上,JSON可以以語義保留的方式表示存在性。然而,在實踐中,存在性的正確性可能會因實現選擇而異,特別是如果選擇JSON作為與不使用protobuf的客戶端互操作的方式。
在Proto2 API中的存在性
此表概述了proto2 API中欄位(包括生成的API和使用動態反射)是否跟蹤存在性。
| 欄位型別 | 顯式存在性 |
|---|---|
| 單個數字(整數或浮點) | ✔️ |
| 單個列舉 | ✔️ |
| 單個字串或位元組 | ✔️ |
| 單個訊息 | ✔️ |
| 重複欄位 | |
| Oneof | ✔️ |
| 對映 |
單個欄位(所有型別)在生成的API中顯式跟蹤存在性。生成的message介面包含查詢欄位存在性的方法。例如,欄位foo有一個對應的has_foo方法。(特定名稱遵循與欄位訪問器相同的語言特定命名約定。)這些方法有時在protobuf實現中被稱為“hazzers”。
類似於單個欄位,oneof欄位顯式跟蹤其中哪個成員(如果有)包含值。例如,考慮這個oneof示例:
oneof foo {
int32 a = 1;
float b = 2;
}
根據目標語言,生成的API通常會包含幾種方法:
- 一個用於oneof的has_方法:
has_foo - 一個oneof case方法:
foo - 成員的has_方法:
has_a,has_b - 成員的getter方法:
a,b
重複欄位和對映不跟蹤存在性:空和不存在的重複欄位之間沒有區別。
在Proto3 API中的存在性
此表概述了proto3 API中欄位(包括生成的API和使用動態反射)是否跟蹤存在性。
| 欄位型別 | 可選 | 顯式存在性 |
|---|---|---|
| 單個數字(整數或浮點) | 否 | |
| 單個數字(整數或浮點) | 是 | ✔️ |
| 單個列舉 | 否 | |
| 單個列舉 | 是 | ✔️ |
| 單個字串或位元組 | 否 | |
| 單個字串或位元組 | 是 | ✔️ |
| 單個訊息 | 否 | ✔️ |
| 單個訊息 | 是 | ✔️ |
| 重複欄位 | 不適用 | |
| Oneof | 不適用 | ✔️ |
| 對映 | 不適用 |
類似於proto2 API,proto3不顯式跟蹤重複欄位的存在性。如果沒有optional標籤,proto3 API也不跟蹤基本型別(數字、字串、位元組和列舉)的存在性。Oneof欄位明確公開存在性,儘管可能不會像proto2 API那樣生成相同的has_方法集。
這種預設行為(不帶optional標籤不跟蹤存在性)與proto2的行為不同。除非您有特殊原因,我們建議在proto3中使用optional標籤。
在隱式存在性規範下,預設值與序列化目的的“不存在”同義。為了在概念上“清除”一個欄位(使其不會被序列化),API使用者會將其設定為預設值。
在隱式存在性下,列舉型別欄位的預設值是相應的0值列舉器。根據proto3語法規則,所有列舉型別都要求有一個對映到0的列舉器值。按照慣例,這是一個UNKNOWN或類似名稱的列舉器。如果零值在概念上超出了應用程式的有效值域,則此行為可以被認為是等同於顯式存在性。
在Editions API中的存在性
此表概述了Editions API中欄位(包括生成的API和使用動態反射)是否跟蹤存在性。
| 欄位型別 | 顯式存在性 |
|---|---|
| 單個數字(整數或浮點) | ✔️ |
| 單個列舉 | ✔️ |
| 單個字串或位元組 | ✔️ |
| 單個訊息† | ✔️ |
| 重複欄位 | |
| Oneofs† | ✔️ |
| 對映 |
† 訊息和oneofs從未有過隱式存在性,Editions不允許您設定field_presence = IMPLICIT。
基於Editions的API顯式跟蹤欄位存在性,類似於proto2,除非features.field_presence設定為IMPLICIT。類似於proto2 API,基於Editions的API不顯式跟蹤重複欄位的存在性。
語義差異
當設定了預設值時,隱式存在性序列化規範會導致與顯式存在性跟蹤規範明顯的差異。對於一個具有數字、列舉或字串型別的單個欄位:
- 隱式存在性規範
- 預設值不被序列化。
- 預設值不被合併。
- 要“清除”一個欄位,它被設定為其預設值。
- 預設值可能意味著
- 該欄位被顯式設定為其預設值,這在應用程式特定值域中是有效的;
- 該欄位透過設定其預設值在概念上被“清除”;或
- 該欄位從未被設定。
has_方法不被生成(但請參見此列表後的註釋)
- 顯式存在性規範
- 顯式設定的值總是被序列化,包括預設值。
- 未設定的欄位從不被合併。
- 顯式設定的欄位——包括預設值——被合併。
- 生成的
has_foo方法指示欄位foo是否已設定(且未清除)。 - 必須使用生成的
clear_foo方法來清除(即,取消設定)值。
注意
在大多數情況下,不會為隱式成員生成Has_方法。此行為的例外是Dart,它為proto3 proto schema檔案生成has_方法。合併考量
在隱式存在性規則下,目標欄位實際上不可能從其預設值合併(使用protobuf的API合併函式)。這是因為預設值被跳過,類似於隱式存在性序列化規範。合併僅使用更新(合併來源)訊息中的非跳過值來更新目標(合併至)訊息。
合併行為的差異對依賴部分“補丁”更新的協議有進一步的影響。如果未跟蹤欄位存在性,則僅靠更新補丁無法表示對預設值的更新,因為只有非預設值才會被合併。
在這種情況下,更新以設定預設值需要一些外部機制,例如FieldMask。但是,如果跟蹤了存在性,則所有顯式設定的值(甚至預設值)都將合併到目標中。
更改相容性考量
將欄位在顯式存在性和隱式存在性之間進行更改,對於線格式中的序列化值來說,是二進位制相容的更改。但是,訊息的序列化表示可能會有所不同,具體取決於用於序列化的訊息定義版本。具體來說,當“傳送方”將欄位顯式設定為其預設值時:
- 遵循隱式存在性規範的序列化值不包含預設值,即使它已顯式設定。
- 遵循顯式存在性規範的序列化值包含每個“存在”的欄位,即使它包含預設值。
這種更改可能安全也可能不安全,取決於應用程式的語義。例如,考慮兩個客戶端使用不同版本的訊息定義。
客戶端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欄位跟蹤支援的一般步驟:
- 在
.proto檔案中新增一個optional欄位。 - 執行
protoc(至少v3.15,或使用--experimental_allow_proto3_optional標誌的v3.12)。 - 在應用程式程式碼中使用生成的“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 中的欄位 | 是 |
| 重複欄位和對映 | 否 |
Proto3
是否跟蹤欄位存在性?
| 欄位型別 | 是否跟蹤? |
|---|---|
| 其他單個欄位 | 如果定義為optional |
| 單個訊息欄位 | 是 |
| oneof 中的欄位 | 是 |
| 重複欄位和對映 | 否 |
Edition 2023
是否跟蹤欄位存在性?
| 欄位型別(按優先順序降序) | 是否跟蹤? |
|---|---|
| 重複欄位和對映 | 否 |
| 訊息和Oneof欄位 | 是 |
如果features.field_presence設定為IMPLICIT,則為其他單個欄位 | 否 |
| 所有其他欄位 | 是 |