語言指南 (proto 3)
本指南介紹瞭如何使用協議緩衝區語言來構建您的協議緩衝區資料,包括 .proto 檔案語法以及如何從您的 .proto 檔案生成資料訪問類。它涵蓋了協議緩衝區語言的 proto3 版本。
有關 editions 語法的詳細資訊,請參閱Protobuf Editions 語言指南。
有關 proto2 語法的詳細資訊,請參閱Proto2 語言指南。
這是一份參考指南——若想透過一個分步示例來了解本文件中描述的許多功能,請參閱你所選語言的教程。
定義訊息型別
首先,我們來看一個非常簡單的例子。假設您想定義一個搜尋請求訊息格式,其中每個搜尋請求都有一個查詢字串、您感興趣的特定結果頁面以及每頁的結果數量。這是您用來定義訊息型別的 .proto 檔案。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
檔案的第一行指定您使用的是 protobuf 語言規範的 proto3 版本。
edition(或 proto2/proto3 的syntax)必須是檔案的第一個非空、非註釋行。- 如果沒有指定
edition或syntax,協議緩衝區編譯器將假定您正在使用 proto2。
SearchRequest訊息定義指定了三個欄位(名稱/值對),每種你想包含在此類訊息中的資料都對應一個欄位。每個欄位都有一個名稱和一個型別。
指定欄位型別
在前面的例子中,所有欄位都是標量型別:兩個整數(page_number 和 results_per_page)和一個字串(query)。您還可以為您的欄位指定列舉和複合型別,例如其他訊息型別。
分配欄位編號
你必須為訊息定義中的每個欄位賦予一個介於 1 和 536,870,911 之間的編號,並遵守以下限制:
- 給定的編號必須在該訊息的所有欄位中是唯一的。
- 欄位編號
19,000到19,999為 Protocol Buffers 實現保留。如果你在訊息中使用這些保留的欄位編號,protocol buffer 編譯器會報錯。 - 您不能使用任何先前保留的欄位編號或已分配給擴充套件的任何欄位編號。
一旦您的訊息型別投入使用,這個編號就不能更改,因為它在訊息的有線格式中標識該欄位。“更改”欄位編號等同於刪除該欄位並建立一個具有相同型別但編號不同的新欄位。有關如何正確執行此操作,請參閱刪除欄位。
欄位編號永遠不應被重用。切勿將一個欄位編號從保留列表中移出,用於新的欄位定義。請參閱重用欄位編號的後果。
您應該為最常設定的欄位使用 1 到 15 的欄位編號。較小的欄位編號值在有線格式中佔用更少的空間。例如,範圍在 1 到 15 之間的欄位編號需要一個位元組進行編碼。範圍在 16 到 2047 之間的欄位編號需要兩個位元組。您可以在Protocol Buffer 編碼中找到更多相關資訊。
重用欄位編號的後果
重用欄位編號會使解碼線路格式的訊息變得模稜兩可。
protobuf 線路格式是精簡的,沒有提供檢測使用一種定義編碼的欄位並用另一種定義解碼的方法。
使用一種定義編碼一個欄位,然後用不同的定義解碼同一個欄位可能導致:
- 開發者浪費時間除錯
- 解析/合併錯誤(最好的情況)
- 個人身份資訊/敏感個人身份資訊洩露
- 資料損壞
欄位編號重用的常見原因
- 重編號欄位(有時為了實現欄位編號順序更美觀)。重編號實際上是刪除並重新新增所有涉及的欄位,導致不相容的線路格式變更。
- 刪除一個欄位並且沒有保留其編號以防止未來重用。
欄位編號限制為 29 位而不是 32 位,因為有三位用於指定欄位的線路格式。有關更多資訊,請參閱編碼主題。
指定欄位基數
訊息欄位可以是以下之一:
單一 (Singular):
在 proto3 中,有兩種型別的單一欄位
optional:(推薦)optional欄位處於兩種可能狀態之一- 該欄位已設定,幷包含一個被顯式設定或從線路中解析出的值。它將被序列化到線路中。
- 該欄位未設定,並將返回預設值。它將不會被序列化到線路中。
你可以檢查該值是否被顯式設定。
建議使用
optional而不是隱式欄位,以實現與 protobuf 版本和 proto2 的最大相容性。隱式:(不推薦)隱式欄位沒有明確的基數標籤,其行為如下
如果欄位是訊息型別,則其行為與
optional欄位完全相同。如果欄位不是訊息,它有兩種狀態
- 欄位設定為非預設(非零)值,該值已顯式設定或從資料線解析。它將被序列化到資料線。
- 欄位設定為預設(零)值。它不會被序列化到資料線。事實上,您無法確定預設(零)值是已設定還是從資料線解析,還是根本未提供。有關此主題的更多資訊,請參閱欄位存在。
repeated:此欄位型別在一個格式良好的訊息中可以重複零次或多次。重複值的順序將被保留。map:這是一個鍵值對欄位型別。有關此欄位型別的更多資訊,請參閱對映。
重複欄位預設為打包
在 proto3 中,標量數字型別的 repeated 欄位預設使用 packed 編碼。
你可以在 Protocol Buffer 編碼中找到更多關於 packed 編碼的資訊。
訊息型別欄位始終具有欄位存在
在 proto3 中,訊息型別欄位已經存在欄位。因此,新增 optional 修飾符不會改變欄位的欄位存在。
以下程式碼示例中 Message2 和 Message3 的定義為所有語言生成相同的程式碼,並且在二進位制、JSON 和 TextFormat 中表示沒有區別
syntax="proto3";
package foo.bar;
message Message1 {}
message Message2 {
Message1 foo = 1;
}
message Message3 {
optional Message1 bar = 1;
}
格式良好的訊息
術語“格式良好”在應用於 protobuf 訊息時,指的是序列化/反序列化的位元組。protoc 解析器會驗證給定的 proto 定義檔案是否可解析。
Singular 欄位可以在有線格式位元組中出現多次。解析器會接受輸入,但只有該欄位的最後一個例項可以透過生成的繫結訪問。有關此主題的更多資訊,請參閱後者為準。
新增更多訊息型別
可以在單個 .proto 檔案中定義多個訊息型別。如果您正在定義多個相關的訊息,這很有用——例如,如果您想定義與您的 SearchResponse 訊息型別相對應的回覆訊息格式,您可以將其新增到同一個 .proto 檔案中:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
message SearchResponse {
...
}
組合訊息會導致臃腫 雖然可以在單個 .proto 檔案中定義多種訊息型別(如 message、enum 和 service),但當在單個檔案中定義大量具有不同依賴關係的訊息時,也可能導致依賴臃腫。建議在每個 .proto 檔案中包含儘可能少的訊息型別。
添加註釋
要為你的 .proto 檔案添加註釋:
優先使用 C/C++/Java 風格的行尾註釋 '//',放在 .proto 程式碼元素的前一行。
也接受 C 風格的內聯/多行註釋
/* ... */。- 使用多行註釋時,推薦使用 '*' 作為邊距行。
/**
* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response.
*/
message SearchRequest {
string query = 1;
// Which page number do we want?
int32 page_number = 2;
// Number of results to return per page.
int32 results_per_page = 3;
}
刪除欄位
如果操作不當,刪除欄位可能會導致嚴重問題。
當您不再需要某個欄位且所有引用都已從客戶端程式碼中刪除時,您可以從訊息中刪除該欄位定義。但是,您必須保留已刪除的欄位編號。如果您不保留欄位編號,開發人員將來可能會重複使用該編號。
你也應該保留欄位名稱,以允許你的訊息的 JSON 和 TextFormat 編碼繼續解析。
保留欄位編號
如果您透過完全刪除欄位或將其註釋掉來更新訊息型別,未來的開發人員在對該型別進行自己的更新時可以重用該欄位編號。這可能會導致嚴重問題,如重用欄位編號的後果中所述。為確保這種情況不會發生,請將您刪除的欄位編號新增到 reserved 列表中。
如果未來的開發者試圖使用這些保留的欄位編號,protoc 編譯器將生成錯誤訊息。
message Foo {
reserved 2, 15, 9 to 11;
}
保留的欄位編號範圍是包含性的(9 to 11 與 9, 10, 11 相同)。
保留欄位名
之後重用舊欄位名通常是安全的,除非在使用 TextProto 或 JSON 編碼時,欄位名會被序列化。為避免這種風險,你可以將被刪除的欄位名新增到 reserved 列表中。
保留名稱僅影響 protoc 編譯器的行為,而不影響執行時行為,但有一個例外:TextProto 實現可能會在解析時丟棄具有保留名稱的未知欄位(而不會像其他未知欄位那樣引發錯誤)(目前只有 C++ 和 Go 實現這樣做)。執行時 JSON 解析不受保留名稱的影響。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
請注意,你不能在同一個 reserved 語句中混合使用欄位名和欄位編號。
從你的 .proto 檔案生成了什麼?
當您對 .proto 檔案執行protocol buffer 編譯器時,編譯器會以您選擇的語言生成您需要的程式碼,用於處理您在檔案中描述的訊息型別,包括獲取和設定欄位值、將訊息序列化到輸出流以及從輸入流解析訊息。
- 對於 C++,編譯器會從每個
.proto檔案生成一個.h和一個.cc檔案,其中為你檔案中描述的每種訊息型別都提供一個類。 - 對於 Java,編譯器會為每種訊息型別生成一個包含一個類的
.java檔案,以及一個用於建立訊息類例項的特殊Builder類。 - 對於 Kotlin,除了 Java 生成的程式碼外,編譯器還為每種訊息型別生成一個
.kt檔案,其中包含一個改進的 Kotlin API。這包括一個簡化訊息例項建立的 DSL、一個可空欄位訪問器和一個複製函式。 - Python 有點不同——Python 編譯器會生成一個模組,其中包含您
.proto檔案中每種訊息型別的靜態描述符,然後該描述符與一個*元類 (metaclass)* 一起使用,以在執行時建立必要的 Python 資料訪問類。 - 對於 Go,編譯器會生成一個
.pb.go檔案,其中包含你檔案中每種訊息型別的一個型別。 - 對於 Ruby,編譯器會生成一個
.rb檔案,其中包含一個 Ruby 模組,該模組包含你的訊息型別。 - 對於 Objective-C,編譯器會從每個
.proto檔案生成一個pbobjc.h和pbobjc.m檔案,其中為你檔案中描述的每種訊息型別都提供一個類。 - 對於 C#,編譯器會從每個
.proto檔案生成一個.cs檔案,其中為你檔案中描述的每種訊息型別都提供一個類。 - 對於 PHP,編譯器為您檔案中描述的每種訊息型別生成一個
.php訊息檔案,併為您編譯的每個.proto檔案生成一個.php元資料檔案。元資料檔案用於將有效的訊息型別載入到描述符池中。 - 對於 Dart,編譯器會生成一個
.pb.dart檔案,其中包含你檔案中每種訊息型別的一個類。
你可以透過按照你所選語言的教程來了解更多關於使用每種語言 API 的資訊。要了解更詳細的 API 資訊,請參閱相關的API 參考。
標量值型別
一個標量訊息欄位可以有以下型別之一——該表顯示了在 .proto 檔案中指定的型別,以及在自動生成的類中對應的型別:
| Proto 型別 | 說明 |
|---|---|
| double | 使用 IEEE 754 雙精度格式。 |
| float | 使用 IEEE 754 單精度格式。 |
| int32 | 使用可變長度編碼。對於編碼負數效率低下——如果你的欄位可能包含負值,請改用 sint32。 |
| int64 | 使用可變長度編碼。對於編碼負數效率低下——如果你的欄位可能包含負值,請改用 sint64。 |
| uint32 | 使用可變長度編碼。 |
| uint64 | 使用可變長度編碼。 |
| sint32 | 使用可變長度編碼。有符號整數值。這些比常規的 int32 更高效地編碼負數。 |
| sint64 | 使用可變長度編碼。有符號整數值。這些比常規的 int64 更高效地編碼負數。 |
| fixed32 | 總是四個位元組。如果值經常大於 228,比 uint32 更高效。 |
| fixed64 | 總是八個位元組。如果值經常大於 256,比 uint64 更高效。 |
| sfixed32 | 總是四個位元組。 |
| sfixed64 | 總是八個位元組。 |
| bool | |
| string | 字串必須始終包含 UTF-8 編碼或 7 位 ASCII 文字,且長度不能超過 232。 |
| bytes | 可包含任意位元組序列,長度不超過 232。 |
| Proto 型別 | C++ 型別 | Java/Kotlin 型別[1] | Python 型別[3] | Go 型別 | Ruby 型別 | C# 型別 | PHP 型別 | Dart 型別 | Rust 型別 |
|---|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | Float | double | float | double | f64 |
| float | float | float | float | float32 | Float | float | float | double | f32 |
| int32 | int32_t | int | int | int32 | Fixnum 或 Bignum (根據需要) | int | integer | int | i32 |
| int64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| uint32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum 或 Bignum (根據需要) | uint | integer | int | u32 |
| uint64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
| sint32 | int32_t | int | int | int32 | Fixnum 或 Bignum (根據需要) | int | integer | int | i32 |
| sint64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| fixed32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum 或 Bignum (根據需要) | uint | integer | int | u32 |
| fixed64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
| sfixed32 | int32_t | int | int | int32 | Fixnum 或 Bignum (根據需要) | int | integer | int | i32 |
| sfixed64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | bool |
| string | std::string | String | str/unicode[5] | string | String (UTF-8) | string | string | String | ProtoString |
| bytes | std::string | ByteString | str (Python 2), bytes (Python 3) | []byte | String (ASCII-8BIT) | ByteString | string | List | ProtoBytes |
[1] 為了確保在混合的 Java/Kotlin 程式碼庫中的相容性,Kotlin 即使對於無符號型別也使用 Java 對應的型別。
[2] 在 Java 中,無符號 32 位和 64 位整數使用其有符號的對應型別來表示,最高位僅儲存在符號位中。
[3] 在所有情況下,為欄位設定值都會進行型別檢查以確保其有效。
[4] 64 位或無符號 32 位整數在解碼時總是表示為 long,但在設定欄位時如果給定的是 int,也可以是 int。在所有情況下,設定的值必須符合所表示的型別。參見 [2]。
[5] Python 字串在解碼時表示為 unicode,但如果給定 ASCII 字串,則可以是 str(這可能會更改)。
[6] 在 64 位機器上使用 Integer,在 32 位機器上使用 string。
你可以在 Protocol Buffer 編碼中瞭解更多關於這些型別在序列化訊息時的編碼方式。
欄位預設值
當解析訊息時,如果編碼的訊息位元組中不包含某個特定欄位,那麼在解析的物件中訪問該欄位將返回該欄位的預設值。預設值是型別特定的:
- 對於字串,預設值是空字串。
- 對於位元組,預設值是空位元組。
- 對於布林值,預設值是 false。
- 對於數值型別,預設值是零。
- 對於訊息欄位,該欄位未設定。其確切值取決於語言。詳情請參閱生成的程式碼指南。
- 對於列舉,預設值是第一個定義的列舉值,它必須為 0。請參閱列舉預設值。
對於 repeated 欄位,預設值是空的(通常是相應語言中的空列表)。
對於 map 欄位,預設值是空的(通常是相應語言中的空 map)。
請注意,對於隱式存在的標量欄位,一旦訊息被解析,就無法判斷該欄位是明確設定為預設值(例如布林值是否設定為false),還是根本未設定:在定義訊息型別時應牢記這一點。例如,如果將布林值設定為false時會觸發某種行為,而您不希望該行為也預設發生,那麼就不要這樣做。另請注意,如果標量訊息欄位確實設定為其預設值,則該值不會在資料線上傳輸。如果浮點數或雙精度值設定為 +0,則不會被序列化,但 -0 被認為是不同的,將被序列化。
有關預設值在生成程式碼中如何工作的更多細節,請參閱你所選語言的生成程式碼指南。
列舉
當您定義訊息型別時,您可能希望它的某個欄位只能是預定義列表中的值之一。例如,假設您想為每個 SearchRequest 新增一個 corpus 欄位,其中 corpus 可以是 UNIVERSAL、WEB、IMAGES、LOCAL、NEWS、PRODUCTS 或 VIDEO。您可以透過在您的訊息定義中新增一個 enum 併為每個可能的值定義一個常量來非常簡單地實現這一點。
在下面的例子中,我們添加了一個名為 Corpus 的 enum,包含了所有可能的值,以及一個型別為 Corpus 的欄位:
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}
列舉值字首
在列舉值加字首時,剝離字首後的其餘名稱仍應是合法且符合樣式的列舉名稱。例如,避免以下情況:
enum DeviceTier {
DEVICE_TIER_UNKNOWN = 0;
DEVICE_TIER_1 = 1;
DEVICE_TIER_2 = 2;
}
相反,使用像 DEVICE_TIER_TIER1 這樣的值名稱,其中 DEVICE_TIER_ 部分被視為對列舉值進行作用域限定,而不是作為單個列舉值名稱的一部分。一些 Protobuf 實現會自動剝離與包含列舉名稱匹配的字首(在安全的情況下),但在本例中不能,因為裸露的 1 不是合法的列舉值名稱。
我們計劃在未來的版本中新增對作用域列舉的支援,這將消除手動為每個列舉值新增字首的需要,並能夠簡潔地寫成 TIER1 = 1。
列舉預設值
SearchRequest.corpus 欄位的預設值是 CORPUS_UNSPECIFIED,因為這是列舉中定義的第一個值。
在 proto3 中,列舉定義中定義的第一個值必須為零,並且應該命名為 ENUM_TYPE_NAME_UNSPECIFIED 或 ENUM_TYPE_NAME_UNKNOWN。這是因為
還建議這個第一個預設值除了“此值未指定”外,不具有任何語義含義。
列舉值別名
您可以透過為不同的列舉常量賦予相同的值來定義別名。為此,您需要將 `allow_alias` 選項設定為 `true`。否則,當發現別名時,protocol buffer 編譯器會生成一條警告訊息。雖然所有別名值在序列化時都有效,但在反序列化時只使用第一個值。
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // Uncommenting this line will cause a warning message.
ENAA_FINISHED = 2;
}
列舉器常量必須在 32 位整數的範圍內。由於 `enum` 值在傳輸時使用 varint 編碼,負值效率低下,因此不推薦使用。您可以在訊息定義中定義 `enum`,如前面的示例所示,也可以在外部定義——這些 `enum` 可以在您的 `.proto` 檔案中的任何訊息定義中重用。您還可以使用在一個訊息中宣告的 `enum` 型別作為另一個訊息中欄位的型別,使用語法 `_MessageType_._EnumType_`。
當您對使用 `enum` 的 `.proto` 檔案執行 protocol buffer 編譯器時,生成的程式碼將為 Java、Kotlin 或 C++ 提供相應的 `enum`,或者為 Python 提供一個特殊的 `EnumDescriptor` 類,用於在執行時生成的類中建立一組帶有整數值的符號常量。
重要
生成的程式碼可能會受到特定語言對列舉器數量的限制(對於某種語言來說是幾千個)。請檢視你計劃使用的語言的限制。在反序列化期間,無法識別的列舉值將保留在訊息中,儘管在反序列化訊息時如何表示這取決於語言。在支援超出指定符號範圍的值的開放列舉型別(例如 C++ 和 Go)的語言中,未知列舉值僅作為其底層整數表示儲存。在具有封閉列舉型別(例如 Java)的語言中,列舉中的一個案例用於表示無法識別的值,並且可以透過特殊訪問器訪問底層整數。在任何一種情況下,如果訊息被序列化,無法識別的值仍將與訊息一起序列化。
重要
有關列舉應如何工作與它們目前在不同語言中如何工作的對比資訊,請參閱列舉行為。有關如何在你的應用程式中使用訊息 enum 的更多資訊,請參閱你所選語言的生成程式碼指南。
保留值
如果您透過完全刪除一個列舉條目或將其註釋掉來更新一個列舉型別,未來的使用者在對該型別進行自己的更新時可以重用該數值。如果他們以後載入同一 .proto 的舊例項,這可能會導致嚴重問題,包括資料損壞、隱私漏洞等。確保這種情況不發生的一種方法是指定您刪除的條目的數值(和/或名稱,名稱也可能導致 JSON 序列化問題)是 reserved 的。如果任何未來的使用者試圖使用這些識別符號,protocol buffer 編譯器會報錯。您可以使用 max 關鍵字指定您的保留數值範圍一直到可能的最大值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
請注意,你不能在同一個 reserved 語句中混合使用欄位名和數值。
使用其他訊息型別
您可以使用其他訊息型別作為欄位型別。例如,假設您希望在每個 SearchResponse 訊息中包含 Result 訊息——為此,您可以在同一個 .proto 檔案中定義一個 Result 訊息型別,然後在 SearchResponse 中指定一個型別為 Result 的欄位:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
匯入定義
在前面的例子中,Result 訊息型別與 SearchResponse 在同一個檔案中定義——如果你想用作欄位型別的訊息型別已經定義在另一個 .proto 檔案中,該怎麼辦?
你可以透過匯入來使用其他 .proto 檔案中的定義。要匯入另一個 .proto 的定義,你在檔案頂部新增一個 import 語句:
import "myproject/other_protos.proto";
protobuf 編譯器會在使用 -I/--proto_path 標誌指定的一組目錄中搜索匯入的檔案,以解析 import 指令。import 語句中給定的路徑是相對於這些目錄解析的。有關使用編譯器的更多資訊,請參閱生成您的類。
例如,考慮以下目錄結構
my_project/
├── protos/
│ ├── main.proto
│ └── common/
│ └── timestamp.proto
要在 main.proto 中使用 timestamp.proto 中的定義,您需要從 my_project 目錄執行編譯器並設定 --proto_path=protos。然後 main.proto 中的 import 語句將是
// Located in my_project/protos/main.proto
import "common/timestamp.proto";
通常,您應該將 --proto_path 標誌設定為包含 .proto 檔案的最高階目錄。這通常是專案的根目錄,但在本例中它位於單獨的 /protos 目錄中。
預設情況下,您只能使用直接匯入的 .proto 檔案中的定義。然而,有時您可能需要將一個 .proto 檔案移動到新位置。您可以不在一次更改中直接移動 .proto 檔案並更新所有呼叫點,而是在舊位置放置一個佔位符 .proto 檔案,使用 import public 概念將所有匯入轉發到新位置。
注意:Java 中提供的公共匯入功能在移動整個 .proto 檔案或使用 java_multiple_files = true 時最有效。在這些情況下,生成的名稱保持穩定,避免了在程式碼中更新引用的需要。雖然在沒有 java_multiple_files = true 的情況下移動 .proto 檔案子集時在技術上功能正常,但這樣做需要同時更新許多引用,因此可能不會顯著簡化遷移。該功能在 Kotlin、TypeScript、JavaScript、GCL 或使用 protobuf 靜態反射的 C++ 目標中不可用。
import public 依賴可以被任何匯入包含 import public 語句的 proto 的程式碼傳遞性地依賴。例如:
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
使用 proto2 訊息型別
可以匯入 proto2 訊息型別並在 proto3 訊息中使用它們,反之亦然。但是,proto2 列舉不能直接在 proto3 語法中使用(如果匯入的 proto2 訊息使用它們,則沒關係)。
巢狀型別
你可以在其他訊息型別內部定義和使用訊息型別,如下例所示——這裡的 Result 訊息是在 SearchResponse 訊息內部定義的。
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果你想在其父訊息型別之外重用此訊息型別,你可以透過 _Parent_._Type_ 的方式引用它:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
你可以隨心所欲地巢狀訊息。在下面的例子中,請注意兩個名為 Inner 的巢狀型別是完全獨立的,因為它們是在不同的訊息中定義的:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
更新訊息型別
如果現有的訊息型別不再滿足您的所有需求——例如,您希望訊息格式有一個額外的欄位——但您仍然想使用舊格式建立的程式碼,別擔心!當您使用二進位制有線格式時,更新訊息型別非常簡單,不會破壞任何現有程式碼。
注意
如果您使用 ProtoJSON 或 proto 文字格式來儲存您的 protocol buffer 訊息,那麼您可以在 proto 定義中做的更改是不同的。ProtoJSON 有線格式的安全更改在這裡描述。請查閱 Proto 最佳實踐和以下規則:
二進位制線路非安全變更
有線不安全 (Wire-unsafe) 的更改是指,如果您使用新的 schema 解析器來解析使用舊 schema 序列化的資料(或反之),將會導致中斷的 schema 更改。只有在您知道資料的所有序列化器和反序列化器都使用新的 schema 時,才進行有線不安全的更改。
- 更改任何現有欄位的欄位編號是不安全的。
- 更改欄位編號等同於刪除該欄位並新增一個具有相同型別的新欄位。如果你想重編號一個欄位,請參閱刪除欄位的說明。
- 將欄位移入一個現有的
oneof是不安全的。
二進位制線路安全變更
線路安全的變更是指完全安全地演進模式,而不會有資料丟失或新的解析失敗的風險。
請注意,任何有線安全 (wire-safe) 的更改對於給定語言的應用程式碼來說都可能是破壞性更改。例如,向一個已有的列舉中新增一個值,對於任何對該列舉進行窮盡式 switch 的程式碼來說,都會導致編譯中斷。因此,Google 可能會避免在公共訊息上進行這類更改:AIPs 中包含了關於哪些更改是安全的指導意見。
- 新增新欄位是安全的。
- 移除欄位是安全的。
- 在您更新的訊息型別中,不得再次使用相同的欄位編號。您可能需要重新命名欄位,例如新增字首“OBSOLETE_”,或者將欄位編號設為保留,這樣您
.proto的未來使用者就不會意外地重用該編號。
- 在您更新的訊息型別中,不得再次使用相同的欄位編號。您可能需要重新命名欄位,例如新增字首“OBSOLETE_”,或者將欄位編號設為保留,這樣您
- 向列舉新增額外的值是安全的。
- 將一個單一的顯式存在欄位或擴充套件變更為一個新的
oneof的成員是安全的。 - 將一個只包含一個欄位的
oneof更改為顯式存在欄位是安全的。 - 將一個欄位更改為具有相同編號和型別的擴充套件是安全的。
二進位制線路相容變更(有條件安全)
與有線安全 (Wire-safe) 的更改不同,有線相容 (wire-compatible) 意味著相同的資料在給定更改前後都可以被解析。然而,在這種型別的更改下,資料的解析可能會是有損的。例如,將一個 int32 更改為 int64 是一個相容的更改,但是如果寫入了一個大於 INT32_MAX 的值,一個將其作為 int32 讀取的客戶端將丟棄該數字的高位位元。
只有在您仔細管理系統推出過程的情況下,才能對您的 schema 進行相容性更改。例如,您可以將 int32 更改為 int64,但要確保在新的 schema 部署到所有端點之前,您繼續只寫入合法的 int32 值,然後在之後才開始寫入更大的值。
如果你的模式是在組織外部發布的,通常不應該進行線路相容的更改,因為你無法管理新模式的部署,從而無法知道何時使用不同範圍的值是安全的。
int32、uint32、int64、uint64和bool都是相容的。- 如果從線路中解析出一個不適合相應型別的數字,你將得到與在 C++ 中將該數字強制轉換為該型別相同的效果(例如,如果一個 64 位數字被作為 int32 讀取,它將被截斷為 32 位)。
sint32和sint64彼此相容,但與其他整數型別不相容。- 如果寫入的值在 INT_MIN 和 INT_MAX(含)之間,那麼用任一型別解析都會得到相同的值。如果寫入了一個超出該範圍的 sint64 值,並被解析為 sint32,則 varint 會被截斷為 32 位,然後進行 zigzag 解碼(這將導致觀察到不同的值)。
- 只要位元組是有效的 UTF-8,
string和bytes就是相容的。 - 如果位元組包含訊息的編碼例項,則嵌入式訊息與
bytes相容。 fixed32與sfixed32相容,fixed64與sfixed64相容。- 對於
string、bytes和訊息欄位,singular 與repeated相容。- 對於重複欄位的序列化資料作為輸入,期望該欄位為 singular 的客戶端,如果它是原始型別欄位,將取最後一個輸入值;如果它是訊息型別欄位,將合併所有輸入元素。請注意,這對於數字型別(包括布林值和列舉)通常是不安全的。數字型別的重複欄位預設以打包格式序列化,當期望的是 singular 欄位時,將無法正確解析。
enum與int32、uint32、int64和uint64相容。- 請注意,當訊息被反序列化時,客戶端程式碼可能會以不同的方式處理它們:例如,未識別的 proto3 `enum` 值將保留在訊息中,但當訊息被反序列化時,其表示方式是依賴於語言的。
- 在
map<K, V>和相應的repeated訊息欄位之間更改欄位是二進位制相容的(有關訊息佈局和其他限制,請參閱下面的對映)。- 然而,更改的安全性取決於應用程式:在反序列化和重新序列化訊息時,使用 `repeated` 欄位定義的客戶端將產生語義上相同的結果;但是,使用 `map` 欄位定義的客戶端可能會重新排序條目並丟棄具有重複鍵的條目。
未知欄位
未知欄位是格式良好的 protocol buffer 序列化資料,表示解析器無法識別的欄位。例如,當一箇舊的二進位制檔案解析一個由新的二進位制檔案傳送的帶有新欄位的資料時,這些新欄位在舊的二進位制檔案中就成為未知欄位。
Proto3 訊息保留未知欄位並在解析和序列化輸出中包含它們,這與 proto2 的行為一致。
保留未知欄位
一些操作可能導致未知欄位丟失。例如,如果你執行以下操作之一,未知欄位將丟失:
- 將 proto 序列化為 JSON。
- 遍歷訊息中的所有欄位以填充一個新訊息。
為避免丟失未知欄位,請執行以下操作:
- 使用二進位制格式;避免使用文字格式進行資料交換。
- 使用面向訊息的 API,如
CopyFrom()和MergeFrom(),來複制資料,而不是逐個欄位複製。
TextFormat 是一個有點特殊的情況。序列化為 TextFormat 會使用欄位編號列印未知欄位。但如果存在使用欄位編號的條目,將 TextFormat 資料解析回二進位制 proto 會失敗。
Any
`Any` 訊息型別允許您將訊息用作嵌入型別,而無需它們的 .proto 定義。一個 `Any` 包含一個任意序列化的訊息(作為 `bytes`),以及一個作為該訊息型別的全域性唯一識別符號並解析到該型別的 URL。要使用 `Any` 型別,您需要匯入 `google/protobuf/any.proto`。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
給定訊息型別的預設型別 URL 是 type.googleapis.com/_packagename_._messagename_。
不同的語言實現將支援執行時庫幫助程式以型別安全的方式打包和解包 `Any` 值——例如,在 Java 中,`Any` 型別將有特殊的 `pack()` 和 `unpack()` 訪問器,而在 C++ 中則有 `PackFrom()` 和 `UnpackTo()` 方法:
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
Oneof
如果您的訊息有許多單一欄位,並且一次最多隻能設定一個欄位,您可以透過使用 oneof 功能來強制執行此行為並節省記憶體。
Oneof 欄位類似於可選欄位,但 oneof 中的所有欄位共享記憶體,並且一次最多隻能設定一個欄位。設定 oneof 的任何成員都會自動清除所有其他成員。您可以根據您選擇的語言,使用特殊的 case() 或 WhichOneof() 方法檢查 oneof 中設定了哪個值(如果有)。
請注意,如果*設定了多個值,則由 proto 中的順序決定的最後一個設定的值將覆蓋所有先前的值*。
oneof 欄位的欄位編號必須在封閉訊息中是唯一的。
使用 Oneof
要在你的 .proto 檔案中定義一個 oneof,你使用 oneof 關鍵字,後跟你 oneof 的名稱,在本例中是 test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然後將 oneof 欄位新增到 oneof 定義中。您可以新增任何型別的欄位,除了 map 欄位和 repeated 欄位。如果需要將重複欄位新增到 oneof 中,可以使用包含重複欄位的訊息。
在您生成的程式碼中,oneof 欄位具有與常規欄位相同的 getter 和 setter。您還可以獲得一個特殊方法,用於檢查 oneof 中設定了哪個值(如果有)。您可以在相關API 參考中找到有關您所選語言的 oneof API 的更多資訊。
Oneof 特性
設定一個 oneof 欄位將自動清除 oneof 的所有其他成員。所以如果你設定了幾個 oneof 欄位,只有你設定的*最後一個*欄位仍然有值。
SampleMessage message; message.set_name("name"); CHECK_EQ(message.name(), "name"); // Calling mutable_sub_message() will clear the name field and will set // sub_message to a new instance of SubMessage with none of its fields set. message.mutable_sub_message(); CHECK(message.name().empty());如果解析器在傳輸線上遇到同一個 oneof 的多個成員,則在解析後的訊息中只使用看到的最後一個成員。在解析傳輸線上的資料時,從位元組的開頭開始,評估下一個值,並應用以下解析規則:
首先,檢查同一個 oneof 中的*另一個*欄位當前是否已設定,如果是,則清除它。
然後像該欄位不在 oneof 中一樣應用內容:
- 一個基本型別將覆蓋任何已設定的值。
- 一個訊息將合併到任何已設定的值中。
一個 oneof 不能是
repeated。反射 API 適用於 oneof 欄位。
如果你將一個 oneof 欄位設定為預設值(例如將一個 int32 oneof 欄位設定為 0),該 oneof 欄位的“case”將被設定,並且該值將被序列化到線路上。
如果你正在使用 C++,請確保你的程式碼不會導致記憶體崩潰。下面的示例程式碼會崩潰,因為
sub_message已經透過呼叫set_name()方法被刪除了。SampleMessage message; SubMessage* sub_message = message.mutable_sub_message(); message.set_name("name"); // Will delete sub_message sub_message->set_... // Crashes here同樣在 C++ 中,如果你
Swap()兩個帶有 oneofs 的訊息,每個訊息最終會得到對方的 oneof case:在下面的例子中,msg1將有一個sub_message,而msg2將有一個name。SampleMessage msg1; msg1.set_name("name"); SampleMessage msg2; msg2.mutable_sub_message(); msg1.swap(&msg2); CHECK(msg1.has_sub_message()); CHECK_EQ(msg2.name(), "name");
向後相容性問題
在新增或刪除 oneof 欄位時要小心。如果檢查 oneof 的值返回 `None`/`NOT_SET`,這可能意味著 oneof 尚未設定,或者它被設定為 oneof 不同版本中的一個欄位。無法區分這兩種情況,因為無法知道傳輸線上的未知欄位是否是 oneof 的成員。
標籤重用問題
- 將單一欄位移入或移出 oneof:在訊息序列化和解析後,您可能會丟失一些資訊(某些欄位將被清除)。但是,您可以安全地將單個欄位移入新的 oneof,並且如果已知只有一個欄位被設定,則可以移動多個欄位。有關更多詳細資訊,請參閱更新訊息型別。
- 刪除一個 oneof 欄位再加回來:這可能會在訊息被序列化和解析後清除你當前設定的 oneof 欄位。
- 拆分或合併 oneof:這與移動單一欄位有類似的問題。
對映
如果你想在資料定義中建立關聯對映,protocol buffers 提供了一種方便的快捷語法:
map<key_type, value_type> map_field = N;
...其中 `key_type` 可以是任何整數或字串型別(即,除浮點型別和 `bytes` 之外的任何標量型別)。請注意,列舉和 proto 訊息都不能作為 `key_type`。`value_type` 可以是除另一個 map 之外的任何型別。
因此,舉例來說,如果你想建立一個專案對映,其中每個 Project 訊息都與一個字串鍵相關聯,你可以這樣定義它:
map<string, Project> projects = 3;
對映特性
- Map 欄位不能是
repeated。 - 對映值的線路格式順序和對映迭代順序是未定義的,所以你不能依賴你的對映項會以特定的順序排列。
- 為
.proto生成文字格式時,對映按鍵排序。數字鍵按數值排序。 - 從線路解析或合併時,如果存在重複的對映鍵,則使用最後看到的鍵。從文字格式解析對映時,如果存在重複的鍵,解析可能會失敗。
- 如果你為一個 map 欄位提供了鍵但沒有提供值,該欄位被序列化時的行為取決於語言。在 C++、Java、Kotlin 和 Python 中,會序列化該型別的預設值,而在其他語言中則什麼也不序列化。
- 符號
FooEntry不能與 mapfoo存在於同一作用域,因為FooEntry已經被 map 的實現使用了。
生成的 map API 目前可用於所有支援的語言。你可以在相關的 API 參考中找到更多關於你所選語言的 map API 的資訊。
向後相容性
map 語法線上路上等同於以下內容,因此不支援 map 的 protocol buffers 實現仍然可以處理你的資料:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支援對映的 protocol buffers 實現都必須既能生成也能接受可被先前定義接受的資料。
包(Packages)
你可以向 .proto 檔案新增一個可選的 package 說明符,以防止協議訊息型別之間的名稱衝突。
package foo.bar;
message Open { ... }
然後你可以在定義你的訊息型別的欄位時使用包說明符:
message Foo {
...
foo.bar.Open open = 1;
...
}
包說明符影響生成程式碼的方式取決於你選擇的語言:
- 在 C++ 中,生成的類被包裹在一個 C++ 名稱空間內。例如,
Open會在名稱空間foo::bar中。 - 在 Java 和 Kotlin 中,除非你在你的
.proto檔案中明確提供一個option java_package,否則該包將用作 Java 包。 - 在 Python 中,
package指令被忽略,因為 Python 模組是根據它們在檔案系統中的位置來組織的。 - 在 Go 中,`package` 指令被忽略,生成的 `.pb.go` 檔案位於以相應 `go_proto_library` Bazel 規則命名的包中。對於開源專案,您**必須**提供一個 `go_package` 選項或設定 Bazel `-M` 標誌。
- 在 Ruby 中,生成的類被包裝在巢狀的 Ruby 名稱空間內,並轉換為所需的 Ruby 大寫風格(首字母大寫;如果第一個字元不是字母,則字首為 `PB_`)。例如,`Open` 將位於 `Foo::Bar` 名稱空間中。
- 在 PHP 中,除非您在
.proto檔案中明確提供了 `option php_namespace`,否則 package 在轉換為 PascalCase 後被用作名稱空間。例如,`Open` 將位於 `Foo\Bar` 名稱空間中。 - 在 C# 中,除非您在 `.proto` 檔案中明確提供 `option csharp_namespace`,否則 package 在轉換為 PascalCase 後將用作名稱空間。例如,`Open` 將位於 `Foo.Bar` 名稱空間中。
請注意,即使 `package` 指令不直接影響生成的程式碼(例如在 Python 中),仍然強烈建議為 .proto 檔案指定包,否則可能導致描述符中的命名衝突,並使 proto 對其他語言不可移植。
包和名稱解析
protocol buffer 語言中的型別名稱解析方式類似於 C++:首先搜尋最內層的作用域,然後是次內層,以此類推,每個包都被視為其父包的“內層”。前導的點“.”(例如,`.foo.bar.Baz`)表示從最外層作用域開始。
protocol buffer 編譯器透過解析匯入的 .proto 檔案來解析所有型別名稱。每種語言的程式碼生成器都知道如何在該語言中引用每種型別,即使它有不同的作用域規則。
定義服務
如果您想將您的訊息型別與 RPC(遠端過程呼叫)系統一起使用,您可以在 .proto 檔案中定義一個 RPC 服務介面,protocol buffer 編譯器將以您選擇的語言生成服務介面程式碼和存根 (stub)。因此,舉例來說,如果您想定義一個 RPC 服務,其方法接受您的 SearchRequest 並返回一個 SearchResponse,您可以在您的 .proto 檔案中這樣定義:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
與協議緩衝區一起使用的最直接的 RPC 系統是 gRPC:一個由 Google 開發的與語言和平臺無關的開源 RPC 系統。gRPC 與協議緩衝區配合得特別好,並且允許您使用特殊的協議緩衝區編譯器外掛直接從 .proto 檔案生成相關的 RPC 程式碼。
如果您不想使用 gRPC,也可以將協議緩衝區與您自己的 RPC 實現一起使用。您可以在Proto2 語言指南中找到有關此內容的更多資訊。
還有許多正在進行的第三方專案開發 Protocol Buffers 的 RPC 實現。有關我們所知專案的連結列表,請參閱第三方附加元件 Wiki 頁面。
JSON 對映
標準 protobuf 二進位制線格式是使用 protobuf 的兩個系統之間通訊的首選序列化格式。為了與使用 JSON 而不是 protobuf 線格式的系統進行通訊,Protobuf 支援 JSON 中的規範編碼。
選項
.proto 檔案中的單個宣告可以用許多*選項*來註解。選項不會改變宣告的整體含義,但可能會影響它在特定上下文中的處理方式。可用選項的完整列表定義在 /google/protobuf/descriptor.proto 中。
一些選項是檔案級選項,意味著它們應該寫在頂層作用域,而不是任何訊息、列舉或服務定義內部。一些選項是訊息級選項,意味著它們應該寫在訊息定義內部。一些選項是欄位級選項,意味著它們應該寫在欄位定義內部。選項也可以寫在列舉型別、列舉值、oneof 欄位、服務型別和服務方法上;然而,目前對於這些都沒有任何有用的選項。
以下是一些最常用的選項:
java_package(檔案選項): 您希望用於生成的 Java/Kotlin 類的包名。如果在.proto檔案中沒有明確給出java_package選項,那麼預設情況下將使用 proto 包(在.proto檔案中使用“package”關鍵字指定)。然而,proto 包通常不是好的 Java 包,因為 proto 包不要求以反向域名開頭。如果不生成 Java 或 Kotlin 程式碼,此選項無效。option java_package = "com.example.foo";java_outer_classname(檔案選項): 您想要生成的包裝器 Java 類的類名(因此也是檔名)。如果在.proto檔案中沒有明確指定java_outer_classname,類名將透過將.proto檔名轉換為駝峰式來構造(所以foo_bar.proto變為FooBar.java)。如果java_multiple_files選項被停用,那麼為.proto檔案生成的所有其他類/列舉等都將作為巢狀類/列舉等生成在這個外部包裝器 Java 類*內部*。如果不生成 Java 程式碼,此選項無效。option java_outer_classname = "Ponycopter";java_multiple_files(檔案選項): 如果為 false,則此.proto檔案只生成一個.java檔案,並且為頂級訊息、服務和列舉生成的所有 Java 類/列舉等將巢狀在一個外部類中 (請參閱java_outer_classname)。如果為 true,則為頂級訊息、服務和列舉生成的每個 Java 類/列舉等將生成單獨的.java檔案,並且為本.proto檔案生成的包裝 Java 類將不包含任何巢狀類/列舉等。這是一個布林選項,預設為false。如果不生成 Java 程式碼,此選項無效。option java_multiple_files = true;optimize_for(檔案選項):可以設定為SPEED、CODE_SIZE或LITE_RUNTIME。這會以下列方式影響 C++ 和 Java 程式碼生成器(以及可能的第三方生成器):SPEED(預設): protocol buffer 編譯器將生成用於序列化、解析以及對你的訊息型別執行其他常見操作的程式碼。此程式碼經過高度最佳化。CODE_SIZE:protocol buffer 編譯器將生成最小化的類,並依賴於共享的、基於反射的程式碼來實現序列化、解析和各種其他操作。因此,生成的程式碼將比使用 `SPEED` 時小得多,但操作會更慢。類仍然會實現與 `SPEED` 模式下完全相同的公共 API。此模式在包含大量.proto檔案且不需要所有檔案都快如閃電的應用中最為有用。LITE_RUNTIME:protocol buffer 編譯器將生成僅依賴於“lite”執行時庫(libprotobuf-lite而不是libprotobuf)的類。lite 執行時比完整庫小得多(大約小一個數量級),但省略了某些功能,如描述符和反射。這對於在受限平臺(如手機)上執行的應用特別有用。編譯器仍將像在 `SPEED` 模式下一樣生成所有方法的快速實現。生成的類在每種語言中只會實現 `MessageLite` 介面,該介面僅提供完整 `Message` 介面方法的一個子集。
option optimize_for = CODE_SIZE;cc_generic_services,java_generic_services,py_generic_services(檔案選項): 通用服務已被棄用。 protocol buffer 編譯器是否應分別基於 C++、Java 和 Python 中的服務定義生成抽象服務程式碼。由於歷史原因,這些預設為 `true`。然而,自 2.3.0 版本(2010年1月)起,RPC 實現更傾向於提供程式碼生成器外掛來生成更特定於每個系統的程式碼,而不是依賴於“抽象”服務。// This file relies on plugins to generate service code. option cc_generic_services = false; option java_generic_services = false; option py_generic_services = false;cc_enable_arenas(檔案選項): 為 C++ 生成的程式碼啟用 arena 分配。objc_class_prefix(檔案選項): 設定 Objective-C 類字首,該字首會新增到此 .proto 生成的所有 Objective-C 類和列舉之前。沒有預設值。您應該使用 3-5 個大寫字元的字首,正如 Apple 推薦的那樣。請注意,所有 2 個字母的字首都由 Apple 保留。packed(欄位選項): 對於基本數字型別的重複欄位,預設為true,導致使用更緊湊的編碼。要使用未打包的線格式,可以將其設定為false。這提供了與 2.3.0 版之前(很少需要)解析器的相容性,如以下示例所示repeated int32 samples = 4 [packed = false];deprecated(欄位選項): 如果設定為 `true`,表示該欄位已棄用,新程式碼不應使用。在大多數語言中,這沒有實際效果。在 Java 中,它會變成一個 `@Deprecated` 註解。對於 C++,clang-tidy 將在使用已棄用欄位時生成警告。將來,其他特定語言的程式碼生成器可能會在該欄位的訪問器上生成棄用註解,這反過來又會在編譯試圖使用該欄位的程式碼時發出警告。如果該欄位無人使用,並且您想阻止新使用者使用它,請考慮用保留語句替換該欄位宣告。int32 old_field = 6 [deprecated = true];
列舉值選項
支援列舉值選項。你可以使用 deprecated 選項來指示某個值不應再使用。你還可以使用擴充套件建立自定義選項。
下面的例子展示了新增這些選項的語法:
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
optional string string_name = 123456789;
}
enum Data {
DATA_UNSPECIFIED = 0;
DATA_SEARCH = 1 [deprecated = true];
DATA_DISPLAY = 2 [
(string_name) = "display_value"
];
}
讀取 string_name 選項的 C++ 程式碼可能看起來像這樣:
const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);
請參閱自定義選項,瞭解如何將自定義選項應用於列舉值和欄位。
自定義選項
Protocol Buffers 還允許您定義和使用自己的選項。請注意,這是一項高階功能,大多數人不需要。如果您確實認為需要建立自己的選項,請參閱Proto2 語言指南瞭解詳細資訊。請注意,建立自定義選項使用擴充套件,這在 proto3 中僅允許用於自定義選項。
選項保留
選項有一個*保留期 (retention)* 的概念,它控制一個選項是否在生成的程式碼中被保留。選項預設具有*執行時保留期*,意味著它們在生成的程式碼中被保留,因此在執行時在生成的描述符池中是可見的。但是,您可以設定 `retention = RETENTION_SOURCE` 來指定一個選項(或選項中的欄位)在執行時不得被保留。這被稱為*原始碼保留期*。
選項保留是一個高階功能,大多數使用者不需要擔心,但如果您想使用某些選項而不想為在二進位制檔案中保留它們付出程式碼大小的代價,它可能很有用。具有源保留的選項對於 `protoc` 和 `protoc` 外掛仍然可見,因此程式碼生成器可以使用它們來自定義其行為。
保留可以直接在選項上設定,如下所示:
extend google.protobuf.FileOptions {
optional int32 source_retention_option = 1234
[retention = RETENTION_SOURCE];
}
它也可以設定在一個普通欄位上,在這種情況下,它僅在該欄位出現在選項內部時生效:
message OptionsMessage {
int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}
您可以設定 `retention = RETENTION_RUNTIME`,但這沒有效果,因為這是預設行為。當一個訊息欄位被標記為 `RETENTION_SOURCE` 時,它的全部內容都會被丟棄;它內部的欄位不能透過嘗試設定 `RETENTION_RUNTIME` 來覆蓋這一點。
注意
截至 Protocol Buffers 22.0,選項保留的支援仍在進行中,目前只支援 C++ 和 Java。Go 從 1.29.0 版本開始支援。Python 的支援已完成,但尚未釋出。選專案標
欄位有一個 `targets` 選項,它控制該欄位在用作選項時可以應用於的實體型別。例如,如果一個欄位有 `targets = TARGET_TYPE_MESSAGE`,那麼該欄位就不能在列舉(或任何其他非訊息實體)的自定義選項中設定。Protoc 會強制執行這一點,並且如果違反目標約束,會引發錯誤。
乍一看,這個功能似乎沒有必要,因為每個自定義選項都是特定實體選項訊息的擴充套件,這已經將選項限制在該一個實體上。然而,在您有一個應用於多種實體型別的共享選項訊息,並且您想控制該訊息中各個欄位的用法時,選專案標就很有用了。例如:
message MyOptions {
string file_only_option = 1 [targets = TARGET_TYPE_FILE];
int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_ENUM];
}
extend google.protobuf.FileOptions {
optional MyOptions file_options = 50000;
}
extend google.protobuf.MessageOptions {
optional MyOptions message_options = 50000;
}
extend google.protobuf.EnumOptions {
optional MyOptions enum_options = 50000;
}
// OK: this field is allowed on file options
option (file_options).file_only_option = "abc";
message MyMessage {
// OK: this field is allowed on both message and enum options
option (message_options).message_and_enum_option = 42;
}
enum MyEnum {
MY_ENUM_UNSPECIFIED = 0;
// Error: file_only_option cannot be set on an enum.
option (enum_options).file_only_option = "xyz";
}
生成你的類
要生成您需要使用的 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 程式碼來處理在 .proto 檔案中定義的訊息型別,您需要在 .proto 檔案上執行 protocol buffer 編譯器 protoc。如果您還沒有安裝編譯器,下載軟體包並按照 README 中的說明進行操作。對於 Go,您還需要為編譯器安裝一個特殊的程式碼生成器外掛;您可以在 GitHub 的 golang/protobuf 倉庫中找到它和安裝說明。
protobuf 編譯器呼叫如下
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
IMPORT_PATH指定一個目錄,用於在解析import指令時查詢.proto檔案。如果省略,則使用當前目錄。可以透過多次傳遞--proto_path選項來指定多個匯入目錄。-I=_IMPORT_PATH_可以作為--proto_path的簡寫形式。
注意: 相對於其 `proto_path` 的檔案路徑在給定的二進位制檔案中必須是全域性唯一的。例如,如果您有 `proto/lib1/data.proto` 和 `proto/lib2/data.proto`,這兩個檔案不能與 `-I=proto/lib1 -I=proto/lib2` 一起使用,因為 `import "data.proto"` 的含義將不明確。相反,應該使用 `-Iproto/`,全域性名稱將是 `lib1/data.proto` 和 `lib2/data.proto`。
如果您正在釋出一個庫,並且其他使用者可能會直接使用您的訊息,您應該在他們預期使用的路徑中包含一個唯一的庫名,以避免檔名衝突。如果您在一個專案中有多個目錄,最好的做法是傾向於將一個 `-I` 設定為專案的頂層目錄。
你可以提供一個或多個輸出指令:
--cpp_out在DST_DIR中生成 C++ 程式碼。更多資訊請參閱 C++ 生成程式碼參考。--java_out在DST_DIR中生成 Java 程式碼。更多資訊請參閱 Java 生成程式碼參考。--kotlin_out在DST_DIR中生成額外的 Kotlin 程式碼。更多資訊請參閱Kotlin 生成程式碼參考。--python_out在DST_DIR中生成 Python 程式碼。更多資訊請參閱 Python 生成程式碼參考。--go_out在DST_DIR中生成 Go 程式碼。更多資訊請參閱Go 生成程式碼參考。--ruby_out在DST_DIR中生成 Ruby 程式碼。更多資訊請參閱 Ruby 生成程式碼參考。--objc_out在DST_DIR中生成 Objective-C 程式碼。更多資訊請參閱 Objective-C 生成程式碼參考。--csharp_out在DST_DIR中生成 C# 程式碼。更多資訊請參閱 C# 生成程式碼參考。--php_out在DST_DIR中生成 PHP 程式碼。更多資訊請參閱 PHP 生成程式碼參考。
為了方便起見,如果 `DST_DIR` 以 `.zip` 或 `.jar` 結尾,編譯器會將輸出寫入一個具有給定名稱的 ZIP 格式的存檔檔案中。`.jar` 輸出也會被賦予一個 Java JAR 規範所要求的清單檔案。請注意,如果輸出存檔已經存在,它將被覆蓋。
您必須提供一個或多個
.proto檔案作為輸入。可以一次指定多個.proto檔案。儘管檔案是相對於當前目錄命名的,但每個檔案必須位於其中一個IMPORT_PATH中,以便編譯器可以確定其規範名稱。
檔案位置
最好不要將 .proto 檔案與其他語言的原始檔放在同一個目錄中。考慮在你的專案根包下為 .proto 檔案建立一個子包 proto。
位置應與語言無關
在處理 Java 程式碼時,將相關的 `.proto` 檔案放在與 Java 原始碼相同的目錄中很方便。但是,如果任何非 Java 程式碼使用相同的 proto,路徑字首將不再有意義。因此,通常應將 proto 放在與語言無關的相關目錄中,例如 `//myteam/mypackage`。
這條規則的例外是當很明顯 protos 只會在 Java 上下文中使用時,例如用於測試。
支援的平臺
有關資訊
- 支援的作業系統、編譯器、構建系統和 C++ 版本,請參閱 基礎 C++ 支援政策。
- 支援的 PHP 版本,請參閱支援的 PHP 版本。