擴充宣告

詳細說明擴充宣告是什麼、為什麼我們需要它們,以及我們如何使用它們。

簡介

本頁詳細說明擴充宣告是什麼、為什麼我們需要它們,以及我們如何使用它們。

注意: Proto3 不支援擴充(除了宣告自訂選項)。不過,proto2 和版本完全支援擴充。

如果您需要擴充的簡介,請閱讀此擴充指南

動機

擴充宣告旨在在常規欄位和擴充之間取得平衡。與擴充類似,它們避免對欄位的訊息類型產生依賴,因此可以在難以或不可能去除未使用的訊息的環境中產生更精簡的建置圖和更小的二進制檔。與常規欄位類似,欄位名稱/號碼會出現在封閉訊息中,這使得更容易避免衝突並查看宣告的欄位方便列表。

使用擴充宣告列出佔用的擴充號碼,可以讓使用者更容易選擇可用的擴充號碼並避免衝突。

用法

擴充宣告是擴充範圍的選項。就像 C++ 中的前向宣告一樣,您可以宣告擴充欄位的欄位類型、欄位名稱和基數(單數或重複),而無需匯入包含完整擴充定義的 .proto 檔案

syntax = "proto2";

message Foo {
  extensions 4 to 1000 [
    declaration = {
      number: 4,
      full_name: ".my.package.event_annotations",
      type: ".logs.proto.ValidationAnnotations",
      repeated: true },
    declaration = {
      number: 999,
      full_name: ".foo.package.bar",
      type: "int32"}];
}

此語法具有以下語義

  • 如果範圍大小允許,可以在單個擴充範圍中定義具有不同擴充號碼的多個 declaration
  • 如果擴充範圍有任何宣告,則範圍的所有擴充也必須宣告。這可以防止新增未宣告的擴充,並強制任何新的擴充都使用範圍的宣告。
  • 給定的訊息類型(.logs.proto.ValidationAnnotations)不需要事先定義或匯入。我們只檢查它是否是可以在另一個 .proto 檔案中定義的有效名稱。
  • 當此或另一個 .proto 檔案使用此名稱或號碼定義此訊息 (Foo) 的擴充時,我們會強制擴充的號碼、類型和完整名稱與此處預先宣告的相符。

警告: 避免對擴充範圍群組使用宣告,例如 extensions 4, 999。目前尚不清楚宣告適用於哪個擴充範圍,並且目前不支援。

擴充宣告預期兩個具有不同套件的擴充欄位

package my.package;
extend Foo {
  repeated logs.proto.ValidationAnnotations event_annotations = 4;
}
package foo.package;
extend Foo {
  optional int32 bar = 999;
}

保留宣告

擴充宣告可以標記為 reserved: true,表示它不再積極使用,並且擴充定義已刪除。請勿刪除擴充宣告或編輯其 typefull_name

reserved 標籤與常規欄位的保留關鍵字是分開的,並且不需要分割擴充範圍

syntax = "proto2";

message Foo {
  extensions 4 to 1000 [
    declaration = {
      number: 500,
      full_name: ".my.package.event_annotations",
      type: ".logs.proto.ValidationAnnotations",
      reserved: true }];
}

使用宣告中 reserved 的號碼的擴充欄位定義將無法編譯。

在 descriptor.proto 中的表示

擴充宣告在 descriptor.proto 中表示為 proto2.ExtensionRangeOptions 中的欄位

message ExtensionRangeOptions {
  message Declaration {
    optional int32 number = 1;
    optional string full_name = 2;
    optional string type = 3;
    optional bool reserved = 5;
    optional bool repeated = 6;
  }
  repeated Declaration declaration = 2;
}

反射欄位查找

擴充宣告不會從正常的欄位查找函式(如 Descriptor::FindFieldByName()Descriptor::FindFieldByNumber())傳回。與擴充類似,它們可以透過擴充查找常式(如 DescriptorPool::FindExtensionByName())探索。這是一個明確的選擇,反映了宣告不是定義,並且沒有足夠的資訊來傳回完整的 FieldDescriptor

從 TextFormat 和 JSON 的角度來看,宣告的擴充的行為仍與常規擴充類似。這也表示將現有欄位遷移到宣告的擴充將需要先遷移該欄位的任何反射使用。

使用擴充宣告來配置數字

擴充使用欄位號碼,就像普通欄位一樣,因此每個擴充都必須指定一個在其父訊息中唯一的號碼。我們建議使用擴充宣告來宣告父訊息中每個擴充的欄位號碼和類型。擴充宣告作為所有父訊息擴充的登錄表,而 protoc 將強制執行沒有欄位號碼衝突。當您新增新的擴充時,通常只需將先前新增的擴充號碼加一,即可選擇下一個可用的號碼。

提示: 對於 MessageSet特殊指南,其中提供腳本來協助選擇下一個可用的號碼。

每當您刪除擴充時,請務必將欄位號碼標記為 reserved,以消除意外重複使用它的風險。

此慣例只是一個建議 – protobuf 團隊沒有能力或意願強迫任何人在每個可擴充訊息中都遵循它。如果您身為可擴充 proto 的擁有者,不想透過擴充宣告協調擴充號碼,則可以選擇透過其他方式提供協調。不過,請務必小心,因為意外重複使用擴充號碼可能會導致嚴重問題。

一種避開這個問題的方法是完全避免擴充,而是使用google.protobuf.Any。對於前端儲存或直通系統,其中用戶端關心 proto 的內容,但接收它的系統不關心,這可能是一個不錯的選擇。

重複使用擴充號碼的後果

擴充是在容器訊息外部定義的欄位;通常在單獨的 .proto 檔案中。這種定義的分散讓兩個開發人員很容易意外地為同一個擴充欄位號碼建立不同的定義。

變更擴充定義的後果與擴充和標準欄位的後果相同。重複使用欄位號碼會導致如何從連線格式解碼 proto 的模糊性。protobuf 連線格式精簡,並且沒有提供好的方法來偵測使用一個定義編碼且使用另一個定義解碼的欄位。

這種模糊性可以在短時間內顯現,例如用戶端使用一個擴充定義,而伺服器使用另一個擴充定義進行通訊。

這種模糊性也可以在較長的時間內顯現,例如儲存使用一個擴充定義編碼的資料,然後稍後使用第二個擴充定義擷取和解碼。如果資料編碼和儲存後刪除了第一個擴充定義,則這種長期案例可能很難診斷。

其結果可能是

  1. 剖析錯誤(最佳情況)。
  2. 洩漏 PII / SPII – 如果 PII 或 SPII 是使用一個擴充定義寫入,並使用另一個擴充定義讀取。
  3. 資料損毀 – 如果資料是使用「錯誤」的定義讀取、修改和重新寫入。

資料定義模糊性幾乎肯定會至少讓某人花時間除錯。它也可能導致資料外洩或損毀,需要數個月才能清理。

使用提示

永遠不要刪除擴充宣告

刪除擴充宣告會在未來為意外重複使用敞開大門。如果不再處理擴充並且刪除了定義,則可以將擴充宣告標記為保留

永遠不要將 reserved 清單中的欄位名稱或號碼用於新的擴充宣告

保留號碼可能過去已用於欄位或其他擴充。

由於使用 textproto 時可能出現模糊性,因此不建議使用保留欄位的 full_name

永遠不要更改現有擴充宣告的類型

變更擴充欄位的類型可能會導致資料損毀。

如果擴充欄位是列舉或訊息類型,並且該列舉或訊息類型正在重新命名,則必須更新宣告名稱,並且是安全的。為避免損壞,類型、擴充欄位定義和擴充宣告的更新都應該在單個提交中進行。

重新命名擴充欄位時請小心

雖然重新命名擴充欄位對於連線格式來說沒有問題,但它可能會中斷 JSON 和 TextFormat 剖析。