語言指南 (版本)

說明如何在您的專案中使用 Protocol Buffers 語言的 2023 版修訂版。

本指南說明如何使用協定緩衝區語言來建構您的協定緩衝區資料,包括 .proto 檔案語法以及如何從您的 .proto 檔案產生資料存取類別。它涵蓋協定緩衝區語言的2023 版。如需有關版本在概念上如何與 proto2 和 proto3 不同的資訊,請參閱Protobuf 版本總覽

如需有關 proto2 語法的資訊,請參閱Proto2 語言指南

如需有關 proto3 語法的資訊,請參閱Proto3 語言指南

這是一份參考指南 – 如需使用本文件中描述的許多功能的逐步範例,請參閱您所選語言的教學

定義訊息類型

首先,讓我們來看一個非常簡單的範例。假設您想要定義一個搜尋請求訊息格式,其中每個搜尋請求都有一個查詢字串、您感興趣的特定結果頁面以及每頁的結果數。以下是您用來定義訊息類型的 .proto 檔案。

edition = "2023";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}
  • 檔案的第一行指定您正在使用 protobuf 語言規格的 2023 版。

    • edition (或 proto2/proto3 的 syntax) 必須是檔案的第一個非空白、非註解行。
    • 如果未指定 editionsyntax,協定緩衝區編譯器將假設您正在使用proto2
  • SearchRequest 訊息定義指定三個欄位 (名稱/值對),每個欄位對應您想要包含在此訊息類型中的一部分資料。每個欄位都有一個名稱和一個類型。

指定欄位類型

在先前的範例中,所有欄位都是純量類型:兩個整數 (page_numberresults_per_page) 和一個字串 (query)。您也可以為欄位指定列舉和複合類型,例如其他訊息類型。

指派欄位編號

您必須在您的訊息定義中為每個欄位指定一個介於 1536,870,911 之間的數字,並符合以下限制

  • 給定的數字在該訊息的所有欄位中必須是唯一的
  • 欄位編號 19,00019,999 保留給 Protocol Buffers 實作。如果您在訊息中使用其中一個保留的欄位編號,協定緩衝區編譯器將會抱怨。
  • 您不能使用任何先前保留的欄位編號,或任何已配置給擴充的欄位編號。

一旦您的訊息類型被使用,此數字就不能變更,因為它識別訊息連線格式中的欄位。「變更」欄位編號相當於刪除該欄位並建立一個具有相同類型但新編號的新欄位。請參閱刪除欄位,了解如何正確地執行此操作。

欄位編號永遠不應重複使用。永遠不要將欄位編號從保留清單中取出,以便重複使用於新的欄位定義。請參閱重複使用欄位編號的後果

您應該將欄位編號 1 到 15 用於最常設定的欄位。較低的欄位編號值在連線格式中佔用的空間較少。例如,範圍 1 到 15 中的欄位編號需要一個位元組來編碼。範圍 16 到 2047 中的欄位編號需要兩個位元組。您可以在協定緩衝區編碼中找到更多相關資訊。

重複使用欄位編號的後果

重複使用欄位編號會使解碼連線格式訊息時產生歧義。

protobuf 連線格式精簡,並且不提供一種方式來檢測使用一個定義編碼並使用另一個定義解碼的欄位。

使用一個定義編碼欄位,然後使用不同的定義解碼同一個欄位,可能會導致

  • 開發人員時間浪費於除錯
  • 剖析/合併錯誤 (最佳情況)
  • 洩漏 PII/SPII
  • 資料損毀

欄位編號重複使用的常見原因

  • 重新編號欄位 (有時是為了使欄位的數字順序更美觀)。重新編號會有效地刪除並重新新增所有參與重新編號的欄位,導致不相容的連線格式變更。

  • 刪除欄位且未保留該編號以防止未來重複使用。

    • 由於幾個原因,這對於擴充欄位來說是一個非常容易犯的錯誤。擴充宣告提供了一種保留擴充欄位的機制。

欄位編號限制為 29 位元,而不是 32 位元,因為有三個位元用於指定欄位的連線格式。如需更多相關資訊,請參閱編碼主題

指定欄位基數

訊息欄位可以是以下之一

  • 單數:

    單數欄位沒有明確的基數標籤。它有兩種可能的狀態

    • 欄位已設定,並且包含明確設定或從連線剖析的值。它將序列化到連線。
    • 欄位未設定,並且將傳回預設值。它不會序列化到連線。

    您可以檢查該值是否已明確設定。

    已遷移到版本的 Proto3 隱含欄位將使用 field_presence 特性集設定為 IMPLICIT 值。

    已遷移到版本的 Proto2 required 欄位也將使用 field_presence 特性,但設定為 LEGACY_REQUIRED

  • repeated:此欄位類型可以在格式正確的訊息中重複零次或多次。將會保留重複值的順序。

  • map:這是一種成對的鍵/值欄位類型。如需有關此欄位類型的更多資訊,請參閱Maps

重複欄位預設為壓縮

在 proto 的早期版本中,純量數值類型的 repeated 欄位預設使用 packed 編碼。

您可以在Protocol Buffer 編碼中找到更多關於 packed 編碼的資訊。

格式正確的訊息

當套用至 protobuf 訊息時,「格式正確」一詞指的是序列化/反序列化的位元組。protoc 解析器會驗證給定的 proto 定義檔是否可解析。

單數欄位可以在線路格式位元組中出現多次。解析器會接受輸入,但只有該欄位的最後一個實例可以透過產生的繫結存取。請參閱最後一個獲勝以了解更多關於此主題的資訊。

新增更多訊息類型

可以在單一 .proto 檔案中定義多個訊息類型。如果您要定義多個相關訊息,這會很有用 – 例如,如果您想定義與您的 SearchResponse 訊息類型相對應的回覆訊息格式,您可以將它新增到同一個 .proto 檔案中。

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}

message SearchResponse {
 ...
}

合併訊息會導致膨脹 雖然可以在單一 .proto 檔案中定義多個訊息類型(例如訊息、列舉和服務),但當在單一檔案中定義大量具有不同依賴關係的訊息時,也可能導致依賴關係膨脹。建議每個 .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 119, 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、一個可為 null 的欄位存取器和一個複製函式。
  • Python 有點不同 – Python 編譯器會產生一個模組,其中包含 .proto 中每個訊息類型的靜態描述符,然後將該描述符與中繼類別一起使用,以在執行階段建立必要的 Python 資料存取類別。
  • 對於 Go,編譯器會產生一個 .pb.go 檔案,其中包含您檔案中每個訊息類型的類型。
  • 對於 Ruby,編譯器會產生一個 .rb 檔案,其中包含一個包含您的訊息類型的 Ruby 模組。
  • 對於 Objective-C,編譯器會從每個 .proto 產生 pbobjc.hpbobjc.m 檔案,每個檔案都包含一個用於您檔案中描述的每個訊息類型的類別。
  • 對於 C#,編譯器會從每個 .proto 產生一個 .cs 檔案,每個檔案都包含一個用於您檔案中描述的每個訊息類型的類別。
  • 對於 PHP,編譯器會為您檔案中描述的每個訊息類型產生一個 .php 訊息檔案,並為您編譯的每個 .proto 檔案產生一個 .php 中繼資料檔案。中繼資料檔案用於將有效的訊息類型載入到描述符池中。
  • 對於 Dart,編譯器會產生一個 .pb.dart 檔案,其中包含您檔案中每個訊息類型的類別。

您可以透過遵循您選擇的語言的教學課程,進一步了解如何使用每種語言的 API。如需更多 API 詳細資料,請參閱相關的API 參考

純量值類型

純量訊息欄位可以具有下列其中一種型別 – 此表格顯示在 .proto 檔案中指定的型別,以及在自動產生的類別中的對應型別

.proto 型別註解C++ 型別Java/Kotlin 型別[1]Python 型別[3]Go 型別Ruby 型別C# 型別PHP 型別Dart 型別Rust 型別
doubledoubledoublefloatfloat64Floatdoublefloatdoublef64
floatfloatfloatfloatfloat32Floatfloatfloatdoublef32
int32使用可變長度編碼。對於編碼負數效率較低 – 如果您的欄位可能具有負值,請改用 sint32。int32intintint32Fixnum 或 Bignum(視需要)intintegerinti32
int64使用可變長度編碼。對於編碼負數效率較低 – 如果您的欄位可能具有負值,請改用 sint64。int64longint/long[4]int64Bignumlonginteger/string[6]Int64i64
uint32使用可變長度編碼。uint32int[2]int/long[4]uint32Fixnum 或 Bignum(視需要)uintintegerintu32
uint64使用可變長度編碼。uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sint32使用可變長度編碼。帶正負號的 int 值。這些比一般的 int32 更有效率地編碼負數。int32intintint32Fixnum 或 Bignum(視需要)intintegerinti32
sint64使用可變長度編碼。帶正負號的 int 值。這些比一般的 int64 更有效率地編碼負數。int64longint/long[4]int64Bignumlonginteger/string[6]Int64i64
fixed32永遠為四個位元組。如果值通常大於 228,則比 uint32 更有效率。uint32int[2]int/long[4]uint32Fixnum 或 Bignum(視需要)uintintegerintu32
fixed64永遠為八個位元組。如果值通常大於 256,則比 uint64 更有效率。uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sfixed32永遠為四個位元組。int32intintint32Fixnum 或 Bignum(視需要)intintegerinti32
sfixed64永遠為八個位元組。int64longint/long[4]int64Bignumlonginteger/string[6]Int64i64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanboolbool
string字串必須始終包含 UTF-8 編碼或 7 位元 ASCII 文字,且不能超過 232stringStringstr/unicode[5]stringString (UTF-8)stringstringStringProtoString
bytes可能包含任何長度不超過 232 的任意位元組序列。stringByteStringstr (Python 2)
bytes (Python 3)
[]byteString (ASCII-8BIT)ByteStringstringListProtoBytes

[1] Kotlin 使用 Java 中的對應類型,即使對於不帶正負號的類型,也是如此,以確保在混合 Java/Kotlin 程式碼庫中的相容性。

[2] 在 Java 中,不帶正負號的 32 位元和 64 位元整數使用其帶正負號的對應項來表示,頂部位元只是儲存在符號位元中。

[3] 在所有情況下,將值設定為欄位都會執行類型檢查以確保其有效。

[4] 解碼時,64 位元或不帶正負號的 32 位元整數一律表示為 long,但如果設定欄位時給定 int,則可以是 int。在所有情況下,該值在設定時必須符合所表示的類型。請參閱 [2]。

[5] Python 字串在解碼時表示為 unicode,但如果給定 ASCII 字串,則可以是 str(這可能會變更)。

[6] 在 64 位元電腦上使用整數,在 32 位元電腦上使用字串。

您可以在Protocol Buffer 編碼中找到更多關於在序列化訊息時如何編碼這些類型的資訊。

預設欄位值

當解析訊息時,如果編碼的訊息位元組不包含特定的欄位,則在解析的物件中存取該欄位會傳回該欄位的預設值。預設值是特定於類型的

  • 對於字串,預設值為空字串。
  • 對於位元組,預設值為空位元組。
  • 對於布林值,預設值為 false。
  • 對於數值類型,預設值為零。
  • 對於訊息欄位,則不會設定該欄位。其確切值取決於語言。請參閱產生的程式碼指南以了解詳細資訊。
  • 對於列舉,預設值是第一個定義的列舉值,必須為 0。請參閱列舉預設值

重複欄位的預設值為空(通常是適當語言中的空清單)。

映射欄位的預設值為空(通常是適當語言中的空映射)。

覆寫預設純量值

在 protobuf 版本中,您可以為單數的非訊息欄位指定明確的預設值。例如,假設您想為 SearchRequest.result_per_page 欄位提供預設值 10

int32 result_per_page = 3 [default = 10];

如果發送者未指定 result_per_page,接收者將會觀察到以下狀態

  • result_per_page 欄位不存在。也就是說,has_result_per_page()(hazzer 方法)會傳回 false
  • result_per_page 的值(從「getter」傳回)是 10

如果發送者確實為 result_per_page 發送了一個值,則預設值 10 會被忽略,並且從「getter」傳回發送者的值。

請參閱您所選語言的程式碼產生指南,以取得有關預設值如何在產生的程式碼中運作的更多詳細資訊。

對於將 field_presence 功能設定為 IMPLICIT 的欄位,無法指定明確的預設值。

列舉

當您定義訊息類型時,您可能希望其欄位之一僅具有預定義的值列表中的一個值。例如,假設您想為每個 SearchRequest 新增一個 corpus 欄位,其中語料庫可以是 UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO。您可以透過在訊息定義中新增一個 enum,並為每個可能的值新增一個常數來非常簡單地完成此操作。

在以下範例中,我們新增了一個名為 Corpusenum,其中包含所有可能的值,以及一個 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;
}

列舉預設值

SearchRequest.corpus 欄位的預設值為 CORPUS_UNSPECIFIED,因為這是 enum 中定義的第一個值。

在 2023 版本中,enum 定義中定義的第一個值必須具有零值,並且應具有名稱 ENUM_TYPE_NAME_UNSPECIFIEDENUM_TYPE_NAME_UNKNOWN。這是因為

  • 零值需要是第一個元素,以便與 proto2 語意相容,其中第一個 enum 值是預設值,除非明確指定不同的值。
  • 必須存在一個零值,以便與 proto3 語意相容,其中零值用作使用此 enum 類型所有隱含存在欄位的預設值。

也建議此第一個預設值沒有除了「此值未指定」之外的語意含義。

可以像這樣明確覆寫 enum 欄位的預設值(例如 SearchRequest.corpus 欄位)

  Corpus corpus = 4 [default = CORPUS_UNIVERSAL];

如果 enum 類型已使用 option features.enum_type = CLOSED; 從 proto2 移轉,則對 enum 中的第一個值沒有限制。不建議變更這些類型 enum 的第一個值,因為它會變更任何使用該 enum 類型的欄位的預設值,而無需明確的欄位預設值。

列舉值別名

您可以透過將相同的值指派給不同的 enum 常數來定義別名。為此,您需要將 allow_alias 選項設定為 true。否則,當找到別名時,協定緩衝區編譯器會產生警告訊息。雖然所有別名值在還原序列化期間都有效,但在序列化時始終會使用第一個值。

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 上執行協定緩衝區編譯器時,產生的程式碼將具有 Java、Kotlin 或 C++ 的對應 enum,或 Python 的特殊 EnumDescriptor 類別,用於在執行階段產生的類別中建立一組具有整數值的符號常數。

在還原序列化期間,無法辨識的 enum 值將會保留在訊息中,儘管當訊息被還原序列化時如何表示是與語言相關的。在支援範圍超出指定符號的開放 enum 類型(例如 C++ 和 Go)的語言中,未知的 enum 值會簡單地儲存為其基礎整數表示法。在具有封閉 enum 類型(例如 Java)的語言中,會使用 enum 中的案例來表示無法辨識的值,並且可以使用特殊的存取器來存取基礎整數。無論如何,如果訊息被序列化,無法辨識的值仍會與訊息一起序列化。

如需更多有關如何在應用程式中使用訊息 enum 的資訊,請參閱您所選語言的程式碼產生指南

保留值

如果您透過完全移除 enum 項目或將其註解掉來更新 enum 類型,則未來的使用者可以在對類型進行自己的更新時重複使用數值。如果他們稍後載入相同 .proto 的舊實例,這可能會導致嚴重問題,包括資料損毀、隱私權錯誤等等。確保不會發生這種情況的一種方法是指定已刪除項目的數值(和/或名稱,這也可能導致 JSON 序列化的問題)是 reserved。如果未來的任何使用者嘗試使用這些識別碼,協定緩衝區編譯器會發出警告。您可以使用 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 "myproject/other_protos.proto";

依預設,您只能使用直接匯入的 .proto 檔案中的定義。但是,有時您可能需要將 .proto 檔案移動到新的位置。您可以將預留位置 .proto 檔案放在舊位置,以使用 import public 概念將所有匯入轉發到新位置,而不是直接移動 .proto 檔案並在單一變更中更新所有呼叫位置。

請注意,公開匯入功能在 Java、Kotlin、TypeScript、JavaScript、GCL 以及使用 protobuf 靜態反射的 C++ 目標中不可用。

任何匯入包含 import public 陳述式的 proto 的程式碼都可以依賴 import public 相依性。

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

協定編譯器會使用 -I/--proto_path 旗標在協定編譯器命令列上指定的一組目錄中搜尋匯入的檔案。如果未提供任何旗標,則它會在呼叫編譯器的目錄中搜尋。一般來說,您應該將 --proto_path 旗標設定為專案的根目錄,並對所有匯入使用完整限定名稱。

使用 proto2 和 proto3 訊息類型

可以匯入 proto2proto3 訊息類型,並將它們用於您 2023 版的訊息中,反之亦然。

巢狀類型

您可以在其他訊息類型內定義和使用訊息類型,如下列範例所示 – 此處的 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;
    }
  }
}

更新訊息類型

如果現有的訊息類型不再滿足您的所有需求 – 例如,您希望訊息格式具有額外的欄位 – 但您仍然想使用使用舊格式建立的程式碼,請別擔心!當您使用二進位有線格式時,更新訊息類型而不會破壞任何現有程式碼非常簡單。

請查看Proto 最佳實務和以下規則

  • 請勿變更任何現有欄位的欄位編號。「變更」欄位編號相當於刪除該欄位並新增一個具有相同類型的新欄位。如果您想重新編號欄位,請參閱刪除欄位的指示。
  • 如果您新增新欄位,則使用您的「舊」訊息格式的程式碼序列化的任何訊息仍然可以由您新產生的程式碼剖析。您應該記住這些元素的預設值,以便新程式碼可以與舊程式碼產生的訊息正確互動。同樣地,您的新程式碼建立的訊息可以由您的舊程式碼剖析:舊的二進位檔會在剖析時簡單地忽略新欄位。請參閱未知欄位章節,以取得詳細資訊。
  • 可以移除欄位,前提是在您更新的訊息類型中不再使用該欄位編號。您可能想要改為重新命名該欄位,也許新增字首「OBSOLETE_」,或讓欄位編號保留,以便您 .proto 的未來使用者不會意外地重複使用該編號。
  • int32uint32int64uint64bool 全部相容 – 這表示您可以將欄位從這些類型之一變更為另一個類型,而不會中斷向前或向後相容性。如果從網路上剖析的數字不適合對應的類型,您將獲得與在 C++ 中將數字轉換為該類型相同的效果(例如,如果將 64 位元數字讀取為 int32,則會截斷為 32 位元)。
  • sint32sint64 彼此相容,但與其他整數類型相容。
  • 只要位元組是有效的 UTF-8,stringbytes 就相容。
  • 如果位元組包含訊息的編碼實例,則嵌入的訊息與 bytes 相容。
  • fixed32sfixed32 相容,而 fixed64sfixed64 相容。
  • 對於 stringbytes 和訊息欄位,單數形式與repeated相容。如果輸入的是重複欄位的序列化資料,當客戶端預期此欄位為單數形式時,若此欄位為原始型別,則會採用最後一個輸入值;若此欄位為訊息型別,則會合併所有輸入元素。請注意,這對於數值型別(包含布林值和列舉)通常不安全。數值型別的重複欄位預設會以封裝格式序列化,當預期為單數欄位時,將無法正確解析。
  • 就線路格式而言,enumint32uint32int64uint64 相容(請注意,若數值不符合,則會被截斷)。但是,請注意,當訊息反序列化時,客戶端程式碼可能會以不同方式處理它們:例如,無法辨識的 enum 值將會保留在訊息中,但訊息反序列化時的表示方式會因語言而異。整數欄位總是只保留其數值。
  • 將單個 optional 欄位或擴充功能變更為新的 oneof 的成員在二進位層級上是相容的,但是對於某些語言(特別是 Go),產生的程式碼的 API 會以不相容的方式變更。因此,Google 不會在公開的 API 中進行此類變更,如 AIP-180 中所述。在原始碼相容性的相同警告下,如果您確定沒有程式碼會同時設定多個欄位,則將多個欄位移至新的 oneof 可能會是安全的。將欄位移至現有的 oneof 是不安全的。同樣地,將單個欄位 oneof 變更為 optional 欄位或擴充功能是安全的。
  • map<K, V> 和對應的 repeated 訊息欄位之間變更欄位在二進位層級上是相容的(請參閱下方的 映射,瞭解訊息佈局和其他限制)。但是,變更的安全性取決於應用程式:在反序列化和重新序列化訊息時,使用 repeated 欄位定義的客戶端將會產生語意上相同的結果;但是,使用 map 欄位定義的客戶端可能會重新排序條目並刪除具有重複鍵的條目。

未知欄位

未知欄位是格式正確的 Protocol Buffer 序列化資料,表示剖析器無法辨識的欄位。例如,當舊的二進位檔剖析由具有新欄位的新二進位檔傳送的資料時,這些新欄位在舊的二進位檔中會變成未知欄位。

版本訊息會保留未知欄位,並在剖析期間和序列化輸出中包含它們,這與 proto2 和 proto3 的行為一致。

保留未知欄位

某些動作可能會導致未知欄位遺失。例如,如果您執行下列其中一項操作,則會遺失未知欄位

  • 將 proto 序列化為 JSON。
  • 反覆查看訊息中的所有欄位以填入新的訊息。

為了避免遺失未知欄位,請執行下列操作

  • 使用二進位;避免使用文字格式進行資料交換。
  • 使用面向訊息的 API,例如 CopyFrom()MergeFrom(),以複製資料,而不是逐欄位複製

TextFormat 是一種特殊情況。序列化為 TextFormat 會使用其欄位編號列印未知欄位。但是,如果存在使用欄位編號的條目,則將 TextFormat 資料剖析回二進位 proto 會失敗。

擴充

擴充功能是在其容器訊息外部定義的欄位;通常在與容器訊息的 .proto 檔案不同的 .proto 檔案中。

為何使用擴充?

使用擴充功能主要有兩個原因

  • 容器訊息的 .proto 檔案將具有較少的匯入/相依性。這可以改善建置時間、打破循環相依性,並促進鬆散耦合。擴充功能非常適合用於此目的。
  • 允許系統以最少的相依性和協調將資料附加到容器訊息。由於欄位編號空間有限以及重複使用欄位編號的後果,擴充功能並非此目的的絕佳解決方案。如果您的使用案例需要對大量擴充功能進行非常低的協調,請考慮改用 Any 訊息型別

擴充範例

讓我們看一個擴充功能的範例

// file kittens/video_ext.proto

import "kittens/video.proto";
import "media/user_content.proto";

package kittens;

// This extension allows kitten videos in a media.UserContent message.
extend media.UserContent {
  // Video is a message imported from kittens/video.proto
  repeated Video kitten_videos = 126;
}

請注意,定義擴充功能的檔案 (kittens/video_ext.proto) 會匯入容器訊息的檔案 (media/user_content.proto)。

容器訊息必須為擴充功能保留其欄位編號的子集。

// file media/user_content.proto

package media;

// A container message to hold stuff that a user has created.
message UserContent {
  // Set verification to `DECLARATION` to enforce extension declarations for all
  // extensions in this range.
  extensions 100 to 199 [verification = DECLARATION];
}

容器訊息的檔案 (media/user_content.proto) 定義訊息 UserContent,該訊息為擴充功能保留欄位編號 [100 至 199]。建議為範圍設定 verification = DECLARATION,以要求宣告所有擴充功能。

新增新的擴充功能 (kittens/video_ext.proto) 時,應在 UserContent 中新增對應的宣告,並應移除 verification

// A container message to hold stuff that a user has created.
message UserContent {
  extensions 100 to 199 [
    declaration = {
      number: 126,
      full_name: ".kittens.kitten_videos",
      type: ".kittens.Video",
      repeated: true
    },
    // Ensures all field numbers in this extension range are declarations.
    verification = DECLARATION
  ];
}

UserContent 宣告欄位編號 126 將由完整名稱為 .kittens.kitten_videos 和完整型別為 .kittens.Videorepeated 擴充欄位使用。若要深入瞭解擴充功能宣告,請參閱擴充功能宣告

請注意,容器訊息的檔案 (media/user_content.proto) 不會匯入 kitten_video 擴充功能定義 (kittens/video_ext.proto)

與具有相同欄位編號、型別和基數的標準欄位相比,擴充功能欄位的線路格式編碼沒有差異。因此,只要欄位編號、型別和基數保持不變,將標準欄位移出其容器以作為擴充功能,或將擴充功能欄位移入其容器訊息以作為標準欄位是安全的。

但是,由於擴充功能是在容器訊息外部定義的,因此不會產生用於取得和設定特定擴充功能欄位的專用存取器。在我們的範例中,Protocol Buffer 編譯器不會產生 AddKittenVideos()GetKittenVideos() 存取器。相反地,擴充功能是透過參數化函式來存取,例如:HasExtension()ClearExtension()GetExtension()MutableExtension()AddExtension()

在 C++ 中,它會如下所示

UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetExtensionCount(kittens::kitten_videos));
user_content.GetExtension(kittens::kitten_videos, 0);

定義擴充範圍

如果您是容器訊息的所有者,則需要為訊息的擴充功能定義擴充範圍。

分配給擴充功能欄位的欄位編號不能重複用於標準欄位。

在定義擴充範圍後,擴充範圍是可以安全擴充的。一個好的預設值是分配 1000 個相對較小的數字,並使用擴充功能宣告密集填入該空間

message ModernExtendableMessage {
  // All extensions in this range should use extension declarations.
  extensions 1000 to 2000 [verification = DECLARATION];
}

在新增擴充功能宣告的範圍之前,您應該新增 verification = DECLARATION 以強制要求此新範圍使用宣告。一旦新增實際的宣告,就可以移除此預留位置。

將現有的擴充範圍分割為涵蓋相同總範圍的單獨範圍是安全的。這對於將舊的訊息型別遷移到擴充功能宣告可能是必要的。例如,在遷移之前,範圍可能會定義為

message LegacyMessage {
  extensions 1000 to max;
}

在遷移之後(分割範圍),它可以是

message LegacyMessage {
  // Legacy range that was using an unverified allocation scheme.
  extensions 1000 to 524999999 [verification = UNVERIFIED];
  // Current range that uses extension declarations.
  extensions 525000000 to max  [verification = DECLARATION];
}

增加起始欄位編號或減少結束欄位編號以移動或縮小擴充範圍是不安全的。這些變更可能會使現有的擴充功能失效。

建議對大多數 proto 執行個體中填入的標準欄位使用欄位編號 1 到 15。不建議將這些數字用於擴充功能。

如果您的編號慣例可能會導致擴充功能具有非常大的欄位編號,您可以使用 max 關鍵字指定擴充範圍達到最大可能的欄位編號

message Foo {
  extensions 1000 to max;
}

max 為 229 - 1,或 536,870,911。

選擇擴充編號

擴充功能只是可以在其容器訊息外部指定的欄位。分配欄位編號的所有相同規則都適用於擴充功能欄位編號。重複使用欄位編號的後果也適用於重複使用擴充功能欄位編號。

如果容器訊息使用擴充功能宣告,則選擇唯一的擴充功能欄位編號很簡單。在定義新的擴充功能時,請選擇容器訊息中定義的最高擴充範圍中所有其他宣告以上的最低欄位編號。例如,如果容器訊息定義如下

message Container {
  // Legacy range that was using an unverified allocation scheme
  extensions 1000 to 524999999;
  // Current range that uses extension declarations. (highest extension range)
  extensions 525000000 to max  [
    declaration = {
      number: 525000001,
      full_name: ".bar.baz_ext",
      type: ".bar.Baz"
    }
    // 525,000,002 is the lowest field number above all other declarations
  ];
}

Container 的下一個擴充功能應新增一個具有編號 525000002 的新宣告。

未驗證的擴充功能編號分配(不建議使用)

容器訊息的所有者可以選擇放棄擴充功能宣告,而選擇使用他們自己的未驗證擴充功能編號分配策略。

未驗證的分配方案會使用 Protocol Buffer 生態系統外部的機制,在選定的擴充範圍內分配擴充功能欄位編號。一個範例是使用單一儲存庫的提交編號。從 Protocol Buffer 編譯器的角度來看,由於無法檢查擴充功能是否使用正確取得的擴充功能欄位編號,因此此系統是「未驗證的」。

與擴充功能宣告等已驗證系統相比,未驗證系統的優點是能夠在無需與容器訊息所有者協調的情況下定義擴充功能。

未驗證系統的缺點是 Protocol Buffer 編譯器無法保護參與者免於重複使用擴充功能欄位編號。

不建議使用未驗證的擴充功能欄位編號分配策略,因為重複使用欄位編號的後果會落在訊息的所有擴充者身上(而不僅僅是不遵守建議的開發人員)。如果您的使用案例需要非常低的協調,請考慮改用 Any 訊息

未驗證的擴充功能欄位編號分配策略僅限於 1 到 524,999,999 的範圍。欄位編號 525,000,000 及以上只能與擴充功能宣告一起使用。

指定擴充類型

擴充功能可以是除了 oneofmap 以外的任何欄位型別。

巢狀擴充 (不建議)

您可以在另一個訊息的範圍中宣告擴充功能

import "common/user_profile.proto";

package puppies;

message Photo {
  extend common.UserProfile {
    int32 likes_count = 111;
  }
  ...
}

在這種情況下,用於存取此擴充功能的 C++ 程式碼為

UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);

換句話說,唯一的效果是 likes_countpuppies.Photo 的範圍內定義。

這是常見的混淆來源:在訊息型別內部巢狀宣告 extend 區塊並未暗示外部型別與擴充型別之間有任何關係。特別是,先前的範例並非表示 PhotoUserProfile 的任何子類別。它只是表示符號 likes_countPhoto 的範圍內宣告;它只是一個靜態成員。

一個常見的模式是在擴充功能的欄位型別的範圍內定義擴充功能 - 例如,以下是 puppies.Photo 型別的 media.UserContent 的擴充功能,其中擴充功能定義為 Photo 的一部分

import "media/user_content.proto";

package puppies;

message Photo {
  extend media.UserContent {
    Photo puppy_photo = 127;
  }
  ...
}

但是,沒有要求具有訊息型別的擴充功能必須在該型別內定義。您也可以使用標準定義模式

import "media/user_content.proto";

package puppies;

message Photo {
  ...
}

// This can even be in a different file.
extend media.UserContent {
  Photo puppy_photo = 127;
}

為了避免混淆,**建議使用此標準(檔案層級)語法**。巢狀語法經常被不熟悉擴充功能的用戶誤認為是子類別化。

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 ...
  }
}

如果您想將包含的訊息限制為少量類型,並在將新類型加入列表之前要求許可,請考慮使用擴充功能搭配擴充功能宣告,而不是 Any 訊息類型。

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(message.has_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.has_name());
    
  • 如果剖析器在傳輸線上遇到同一個 oneof 的多個成員,則只有最後看到的成員會用於已剖析的訊息中。

  • Oneof 不支援擴充功能。

  • Oneof 不能是 repeated

  • Reflection 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() 兩個具有 oneof 的訊息,每個訊息最終都會具有另一個訊息的 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(msg2.has_name());
    

向後相容性問題

加入或移除 oneof 欄位時請小心。如果檢查 oneof 的值傳回 None/NOT_SET,則可能表示 oneof 尚未設定,或是已設定為不同版本的 oneof 中的欄位。無法分辨差異,因為無法知道傳輸線上未知的欄位是否為 oneof 的成員。

標籤重複使用問題

  • **將單數欄位移入或移出 oneof**:在訊息序列化和剖析後,您可能會遺失一些資訊(某些欄位將會清除)。然而,您可以安全地將單一欄位移入**新的** oneof,並且如果已知只設定一個欄位,則可以移動多個欄位。請參閱更新訊息類型以取得更多詳細資訊。
  • **刪除 oneof 欄位並將其加回**:這可能會在訊息序列化和剖析後清除您目前設定的 oneof 欄位。
  • **分割或合併 oneof**:這與移動單數欄位有類似的問題。

Maps

如果您想要建立關聯式地圖作為資料定義的一部分,則協定緩衝區提供了一個方便的速記語法

map<key_type, value_type> map_field = N;

…其中 key_type 可以是任何整數或字串類型(因此,除了浮點類型和 bytes 之外的任何純量類型)。請注意,列舉或 proto 訊息都無法用於 key_typevalue_type 可以是任何類型,但不能是另一個地圖。

因此,舉例來說,如果您想要建立專案的地圖,其中每個 Project 訊息都與字串索引鍵關聯,則您可以像這樣定義它

map<string, Project> projects = 3;

Maps 功能

  • 地圖不支援擴充功能。
  • 地圖欄位不能是 repeated
  • 地圖值的線路格式順序和地圖迭代順序是未定義的,因此您無法依賴您的地圖項目按特定順序排列。
  • 當為 .proto 產生文字格式時,地圖會依索引鍵排序。數字索引鍵會依數值排序。
  • 當從傳輸線剖析或合併時,如果有重複的地圖索引鍵,則會使用最後看到的索引鍵。當從文字格式剖析地圖時,如果有重複的索引鍵,剖析可能會失敗。
  • 如果您為地圖欄位提供索引鍵但沒有值,則欄位序列化時的行為取決於語言。在 C++、Java、Kotlin 和 Python 中,會序列化該類型的預設值,而在其他語言中則不會序列化任何內容。
  • 在與地圖 foo 相同的範圍中,不能存在符號 FooEntry,因為 FooEntry 已被地圖的實作所使用。

目前所有支援的語言都可使用產生的地圖 API。您可以在相關的API 參考資料中找到更多關於您選擇的語言的地圖 API 資訊。

向後相容性

地圖語法在傳輸線上等同於以下語法,因此不支援地圖的協定緩衝區實作仍然可以處理您的資料

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

任何支援地圖的協定緩衝區實作都必須產生和接受可被較早定義接受的資料。

套件

您可以將選用的 package 指定符加入 .proto 檔案中,以防止協定訊息類型之間的名稱衝突。

package foo.bar;
message Open { ... }

然後,您可以在定義訊息類型的欄位時使用 package 指定符

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

package 指定符影響產生程式碼的方式取決於您選擇的語言

  • 在 **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** 中,套件會用作轉換為 PascalCase 後的命名空間,除非您在 .proto 檔案中明確提供 option php_namespace。例如,Open 會在命名空間 Foo\Bar 中。
  • 在 **C#** 中,套件會用作轉換為 PascalCase 後的命名空間,除非您在 .proto 檔案中明確提供 option csharp_namespace。例如,Open 會在命名空間 Foo.Bar 中。

請注意,即使 package 指示詞不會直接影響產生的程式碼,例如在 Python 中,仍然強烈建議為 .proto 檔案指定套件,否則可能會導致描述符中的命名衝突,並使 proto 無法在其他語言中移植。

套件與名稱解析

協定緩衝區語言中的類型名稱解析方式與 C++ 類似:首先會搜尋最內層的範圍,然後搜尋次內層,依此類推,每個套件都被視為其父套件的「內部」。前導 '.'(例如,.foo.bar.Baz)表示從最外層範圍開始。

協定緩衝區編譯器會透過剖析匯入的 .proto 檔案來解析所有類型名稱。每種語言的程式碼產生器都知道如何在該語言中參照每個類型,即使它具有不同的範圍規則。

定義服務

如果您想要在 RPC(遠端程序呼叫)系統中使用訊息類型,則可以在 .proto 檔案中定義 RPC 服務介面,而協定緩衝區編譯器會在您選擇的語言中產生服務介面程式碼和存根。因此,舉例來說,如果您想要定義一個 RPC 服務,該服務具有一個採用 SearchRequest 並傳回 SearchResponse 的方法,則可以在您的 .proto 檔案中將其定義如下

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

與協定緩衝區搭配使用的最直接 RPC 系統是 gRPC:一種在 Google 開發的語言和平台中立的開放原始碼 RPC 系統。gRPC 與協定緩衝區搭配使用效果特別好,並且可讓您使用特殊的協定緩衝區編譯器外掛程式直接從您的 .proto 檔案產生相關的 RPC 程式碼。

如果您不想使用 gRPC,也可以將協定緩衝區與您自己的 RPC 實作搭配使用。您可以在Proto2 語言指南中找到更多相關資訊。

目前也有許多進行中的第三方專案在為協定緩衝區開發 RPC 實作。如需我們知道的專案連結清單,請參閱第三方附加元件 Wiki 頁面

JSON 對應

標準 protobuf 二進位線路格式是兩個使用 protobuf 的系統之間進行通訊時的首選序列化格式。為了與使用 JSON 而非 protobuf 線路格式的系統進行通訊,Protobuf 支援 ProtoJSON 中的標準編碼。

選項

.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 (檔案選項): 可以設定為 SPEEDCODE_SIZELITE_RUNTIME。這會以以下方式影響 C++ 和 Java 程式碼產生器(以及可能的第三方產生器)

    • SPEED (預設值): 通訊協定緩衝區編譯器會產生程式碼,用於序列化、剖析及對訊息類型執行其他常見操作。此程式碼經過高度最佳化。
    • CODE_SIZE: 通訊協定緩衝區編譯器會產生最精簡的類別,並依賴共用的、基於反射的程式碼來實作序列化、剖析及各種其他操作。因此,產生的程式碼會比使用 SPEED 時小得多,但操作速度會較慢。類別仍然會實作與 SPEED 模式下完全相同的公開 API。此模式最適用於包含大量 .proto 檔案,且並非所有檔案都需要極快速度的應用程式。
    • LITE_RUNTIME: 通訊協定緩衝區編譯器會產生僅依賴「輕量」執行階段程式庫 (libprotobuf-lite 而非 libprotobuf) 的類別。輕量執行階段比完整程式庫小得多 (約小一個數量級),但省略了描述元和反射等特定功能。這對於在行動電話等受限平台上執行的應用程式特別有用。編譯器仍會像在 SPEED 模式下那樣產生所有方法的快速實作。產生的類別在每種語言中只會實作 MessageLite 介面,該介面僅提供完整 Message 介面的部分方法。
    option optimize_for = CODE_SIZE;
    
  • cc_generic_servicesjava_generic_servicespy_generic_services (檔案選項): 通用服務已淘汰。 通訊協定緩衝區編譯器是否應分別根據 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++ 產生的程式碼啟用競技場配置

  • objc_class_prefix (檔案選項): 設定 Objective-C 類別前綴,該前綴會附加到由此 .proto 產生的所有 Objective-C 類別和列舉。沒有預設值。您應該使用介於 3 到 5 個大寫字元之間的前綴,如 Apple 建議的那樣。請注意,所有 2 個字母的前綴都由 Apple 保留。

  • packed (欄位選項): 在 protobuf 版本中,此選項鎖定為 true。若要使用未封裝的有線格式,您可以使用版本功能來覆寫此選項。這提供了與 2.3.0 版之前的剖析器相容性(很少需要),如下列範例所示

    repeated int32 samples = 4 [features.repeated_field_encoding = EXPANDED];
    
  • deprecated (欄位選項): 如果設定為 true,則表示欄位已淘汰,不應由新程式碼使用。在大多數語言中,這實際上沒有任何影響。在 Java 中,這會變成 @Deprecated 註解。對於 C++,clang-tidy 會在每次使用已淘汰的欄位時產生警告。未來,其他特定於語言的程式碼產生器可能會在欄位的存取子產生淘汰註解,這反過來會在編譯嘗試使用該欄位的程式碼時發出警告。如果沒有人使用該欄位,而您想要防止新使用者使用它,請考慮將欄位宣告替換為 reserved 陳述式。

    int32 old_field = 6 [deprecated = true];
    

列舉值選項

支援列舉值選項。您可以使用 deprecated 選項來表示不再應該使用值。您也可以使用擴充功能建立自訂選項。

以下範例顯示新增這些選項的語法

import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  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);

請參閱自訂選項,以瞭解如何將自訂選項套用至列舉值和欄位。

自訂選項

通訊協定緩衝區也允許您定義和使用自己的選項。請注意,這是一個進階功能,大多數人不需要使用。如果您認為確實需要建立自己的選項,請參閱Proto2 語言指南以取得詳細資訊。請注意,建立自訂選項會使用擴充功能

選項保留

選項具有保留的概念,這會控制是否在產生的程式碼中保留選項。預設情況下,選項具有執行階段保留,這表示它們會保留在產生的程式碼中,因此在產生的描述元集區中在執行階段是可見的。不過,您可以設定 retention = RETENTION_SOURCE 來指定選項 (或選項中的欄位) 不得在執行階段保留。這稱為來源保留

選項保留是一個進階功能,大多數使用者不需要擔心,但如果您想使用某些選項而不必支付將其保留在二進位檔中的程式碼大小成本,這會很有用。具有來源保留的選項對於 protocprotoc 外掛程式仍然可見,因此程式碼產生器可以使用它們來自訂其行為。

保留可以直接在選項上設定,如下所示

extend google.protobuf.FileOptions {
  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 來覆寫該設定。

選項目標

欄位具有 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 {
  MyOptions file_options = 50000;
}

extend google.protobuf.MessageOptions {
  MyOptions message_options = 50000;
}

extend google.protobuf.EnumOptions {
  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 檔案上執行通訊協定緩衝區編譯器 protoc。如果您尚未安裝編譯器,請下載套件,並按照 README 中的指示進行操作。對於 Go,您還需要為編譯器安裝特殊的程式碼產生器外掛程式;您可以在 GitHub 上的golang/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 的簡短形式。

  • 您可以提供一或多個輸出指示

    為了額外的便利性,如果 DST_DIR.zip.jar 結尾,編譯器會將輸出寫入一個以指定名稱命名的 ZIP 格式壓縮檔。.jar 輸出也會按照 Java JAR 規範的要求,包含一份 manifest 檔案。請注意,如果輸出壓縮檔已存在,將會被覆寫。

  • 您必須提供一個或多個 .proto 檔案作為輸入。可以一次指定多個 .proto 檔案。儘管這些檔案的名稱是相對於目前目錄,每個檔案都必須位於其中一個 IMPORT_PATH 中,以便編譯器可以確定其規範名稱。

檔案位置

最好不要將 .proto 檔案與其他語言的原始碼放在同一個目錄中。考慮在專案的根目錄下,為 .proto 檔案建立一個子套件 proto

位置應與語言無關

在使用 Java 程式碼時,將相關的 .proto 檔案放在與 Java 原始碼相同的目錄中很方便。然而,如果任何非 Java 程式碼使用相同的 protos,路徑前綴將不再有意義。因此,一般來說,請將 protos 放在與語言無關的相關目錄中,例如 //myteam/mypackage

這個規則的例外情況是,當清楚地知道這些 protos 只會在 Java 環境中使用時,例如用於測試。

支援的平台

有關資訊,請參閱