Ruby 生成程式碼指南

描述 protocol buffer 編譯器為任何給定的協議定義所生成的訊息物件的 API。

在閱讀本文件之前,您應該先閱讀 proto2proto3editions 的語言指南。

編譯器呼叫

Protocol Buffer 編譯器在呼叫 --ruby_out= 命令列標誌時會生成 Ruby 輸出。--ruby_out= 選項的引數是編譯器希望您將 Ruby 輸出寫入的目錄。編譯器為每個 .proto 檔案輸入都會建立一個 .rb 檔案。輸出檔案的名稱是透過獲取 .proto 檔名並進行兩項更改來計算的。

  • 副檔名 (.proto) 被替換為 _pb.rb
  • Proto 路徑(透過 --proto_path=-I 命令列標誌指定)被替換為輸出路徑(透過 --ruby_out= 標誌指定)。

因此,舉例來說,假設您像下面這樣呼叫編譯器:

protoc --proto_path=src --ruby_out=build/gen src/foo.proto src/bar/baz.proto

編譯器將讀取檔案 src/foo.protosrc/bar/baz.proto 並生成兩個輸出檔案:build/gen/foo_pb.rbbuild/gen/bar/baz_pb.rb。如果需要,編譯器將自動建立目錄 build/gen/bar,但它不會建立 buildbuild/gen;它們必須已存在。

包(Packages)

.proto 檔案中定義的包名稱用於生成生成訊息的模組結構。給定一個檔案,例如

package foo_bar.baz;

message MyMessage {}

Protocol 編譯器將生成一個名為 FooBar::Baz::MyMessage 的輸出訊息。

但是,如果 .proto 檔案包含 ruby_package 選項,如下所示

option ruby_package = "Foo::Bar";

那麼生成的輸出將優先考慮 ruby_package 選項,並生成 Foo::Bar::MyMessage

訊息

給定一個簡單的訊息宣告:

message Foo {}

Protocol Buffer 編譯器會生成一個名為 Foo 的類。生成的類派生自 Ruby 的 Object 類(proto 沒有通用基類)。與 C++ 和 Java 不同,Ruby 生成的程式碼不受 .proto 檔案中 optimize_for 選項的影響;實際上,所有 Ruby 程式碼都針對程式碼大小進行了最佳化。

應該建立自己的 Foo 子類。生成的類並非設計為用於子類化,並可能導致“脆弱的基類”問題。

Ruby 訊息類為每個欄位定義了訪問器,並且還提供了以下標準方法:

  • Message#dup, Message#clone: 對此訊息執行淺複製並返回新副本。
  • Message#==: 對兩個訊息執行深度相等比較。
  • Message#hash: 計算訊息值的淺雜湊。
  • Message#to_hash, Message#to_h: 將物件轉換為 Ruby 的 Hash 物件。僅轉換頂層訊息。
  • Message#inspect: 返回表示此訊息的人類可讀字串。
  • Message#[], Message#[]=: 透過字串名稱獲取或設定欄位。將來這可能也用於獲取/設定擴充套件。

訊息類還定義了以下靜態方法。(通常我們偏愛靜態方法,因為常規方法可能會與您在 .proto 檔案中定義的欄位名稱衝突。)

  • Message.decode(str): 解碼此訊息的二進位制 protobuf 並將其作為新例項返回。
  • Message.encode(proto): 將此類的訊息物件序列化為二進位制字串。
  • Message.decode_json(str): 解碼此訊息的 JSON 文字字串並將其作為新例項返回。
  • Message.encode_json(proto): 將此類的訊息物件序列化為 JSON 文字字串。
  • Message.descriptor: 返回此訊息的 Google::Protobuf::Descriptor 物件。

建立訊息時,您可以在建構函式中方便地初始化欄位。以下是構造和使用訊息的示例:

message = MyMessage.new(int_field: 1,
                        string_field: "String",
                        repeated_int_field: [1, 2, 3, 4],
                        submessage_field: MyMessage::SubMessage.new(foo: 42))
serialized = MyMessage.encode(message)

message2 = MyMessage.decode(serialized)
raise unless message2.int_field == 1

巢狀型別

訊息可以宣告在另一個訊息內部。例如:

message Foo {
  message Bar { }
}

在這種情況下,Bar 類被宣告為 Foo 內部的一個類,因此您可以將其稱為 Foo::Bar

欄位

對於訊息中的每個欄位,都有設定和獲取欄位的訪問器方法。因此,對於欄位 foo,您可以編寫:

message.foo = get_value()
print message.foo

每當您設定一個欄位時,該值都會根據該欄位的宣告型別進行型別檢查。如果值型別不正確(或超出範圍),則會引發異常。

單一欄位

對於單個原始欄位(數字、字串和布林值),分配給欄位的值應為正確型別,並且必須在適當的範圍內:

  • 數字型別:值應為 FixnumBignumFloat。分配的值必須在目標型別中精確表示。因此,將 1.0 分配給 int32 欄位是可以的,但分配 1.2 則不行。
  • 布林欄位:值必須是 truefalse。其他值不會隱式轉換為 true/false。
  • 位元組欄位:分配的值必須是 String 物件。Protobuf 庫將複製該字串,將其轉換為 ASCII-8BIT 編碼,並凍結它。
  • 字串欄位:分配的值必須是 String 物件。Protobuf 庫將複製該字串,將其轉換為 UTF-8 編碼,並凍結它。

不會進行自動的 #to_s#to_i 等呼叫來執行自動轉換。如果需要,您應該自己先轉換值。

檢查存在性

顯式欄位存在性由 field_presence 功能(在 editions 中)、optional 關鍵字(在 proto2/proto3 中)和欄位型別(訊息欄位和 oneof 欄位始終具有顯式存在性)決定。當欄位存在時,您可以透過呼叫生成的 has_...? 方法來檢查該欄位是否在訊息中設定。設定任何值—即使是預設值—都會將欄位標記為已存在。欄位可以透過呼叫不同的生成的 clear_... 方法來清除。

例如,對於具有 int32 欄位 foo 的訊息 MyMessage

message MyMessage {
  int32 foo = 1;
}

foo 的存在性可以如下檢查:

m = MyMessage.new
raise if m.has_foo?
m.foo = 0
raise unless m.has_foo?
m.clear_foo
raise if m.has_foo?

奇異訊息欄位

子訊息欄位始終存在,無論它們是否被標記為 optional。未設定的子訊息欄位返回 nil,因此您始終可以判斷訊息是否被顯式設定。要清除子訊息欄位,請將其值顯式設定為 nil

if message.submessage_field.nil?
  puts "Submessage field is unset."
else
  message.submessage_field = nil
  puts "Cleared submessage field."
end

除了比較和分配 nil 之外,生成的類還具有 has_...clear_... 方法,其行為與基本型別相同。

if !message.has_submessage_field?
  puts "Submessage field is unset."
else
  message.clear_submessage_field
  raise if message.has_submessage_field?
  puts "Cleared submessage field."
end

分配子訊息時,它必須是正確型別的已生成訊息物件。

在分配子訊息時,可能會建立訊息迴圈。例如:

// foo.proto
message RecursiveMessage {
  RecursiveMessage submessage = 1;
}

# test.rb
require 'foo'

message = RecursiveMessage.new
message.submessage = message

如果您嘗試序列化此物件,庫將檢測到迴圈並無法序列化。

重複欄位

重複欄位使用自定義類 Google::Protobuf::RepeatedField 表示。此類行為類似於 Ruby 的 Array 並混入了 Enumerable。與常規 Ruby 陣列不同,RepeatedField 是使用特定型別構造的,並且期望所有陣列成員都具有正確的型別。型別和範圍的檢查與訊息欄位類似。

int_repeatedfield = Google::Protobuf::RepeatedField.new(:int32, [1, 2, 3])

raise unless !int_repeatedfield.empty?

# Raises TypeError.
int_repeatedfield[2] = "not an int32"

# Raises RangeError
int_repeatedfield[2] = 2**33

message.int32_repeated_field = int_repeatedfield

# This isn't allowed; the regular Ruby array doesn't enforce types like we need.
message.int32_repeated_field = [1, 2, 3, 4]

# This is fine, since the elements are copied into the type-safe array.
message.int32_repeated_field += [1, 2, 3, 4]

# The elements can be cleared without reassigning.
int_repeatedfield.clear
raise unless int_repeatedfield.empty?

對於包含訊息的重複欄位,Google::Protobuf::RepeatedField 的建構函式支援帶三個引數的變體::message、子訊息的類以及要設定的值。

first_message = MySubMessage.new(foo: 42)
second_message = MySubMessage.new(foo: 79)

repeated_field = Google::Protobuf::RepeatedField.new(
    :message,
    MySubMessage,
    [first_message, second_message]
)
message.sub_message_repeated_field = repeated_field

RepeatedField 型別支援與常規 Ruby Array 相同的所有方法。您可以使用 repeated_field.to_a 將其轉換為常規 Ruby Array。

與單個欄位不同,從不為重複欄位生成 has_...? 方法。

對映欄位

Map 欄位使用充當 Ruby HashGoogle::Protobuf::Map)的特殊類表示。與常規 Ruby hash 不同,Map 是使用鍵和值的特定型別構造的,並且期望所有 map 的鍵和值都具有正確的型別。型別和範圍的檢查與訊息欄位和 RepeatedField 元素類似。

int_string_map = Google::Protobuf::Map.new(:int32, :string)

# Returns nil; items is not in the map.
print int_string_map[5]

# Raises TypeError, value should be a string
int_string_map[11] = 200

# Ok.
int_string_map[123] = "abc"

message.int32_string_map_field = int_string_map

列舉

由於 Ruby 沒有原生列舉,我們為每個列舉建立一個帶有常量來定義值的模組。給定 .proto 檔案:

message Foo {
  enum SomeEnum {
    VALUE_A = 0;
    VALUE_B = 5;
    VALUE_C = 1234;
  }
  SomeEnum bar = 1;
}

您可以這樣引用列舉值:

print Foo::SomeEnum::VALUE_A  # => 0
message.bar = Foo::SomeEnum::VALUE_A

您可以為列舉欄位分配數字或符號。讀取值時,如果列舉值已知,它將是符號;如果未知,則為數字。

對於 proto3 使用的 OPEN 列舉,可以為列舉分配任何整數值,即使該值未在列舉中定義。

message.bar = 0
puts message.bar.inspect  # => :VALUE_A
message.bar = :VALUE_B
puts message.bar.inspect  # => :VALUE_B
message.bar = 999
puts message.bar.inspect  # => 999

# Raises: RangeError: Unknown symbol value for enum field.
message.bar = :UNDEFINED_VALUE

# Switching on an enum value is convenient.
case message.bar
when :VALUE_A
  # ...
when :VALUE_B
  # ...
when :VALUE_C
  # ...
else
  # ...
end

列舉模組還定義了以下實用方法:

  • Foo::SomeEnum.lookup(number): 查詢給定的數字並返回其名稱,如果未找到則返回 nil。如果多個名稱具有此數字,則返回定義的第一個。
  • Foo::SomeEnum.resolve(symbol): 返回此列舉名稱的數字,如果未找到則返回 nil
  • Foo::SomeEnum.descriptor: 返回此列舉的描述符。

Oneof

給定一個包含 oneof 的訊息:

message Foo {
  oneof test_oneof {
     string name = 1;
     int32 serial_number = 2;
  }
}

Foo 對應的 Ruby 類將擁有名為 nameserial_number 的成員,並像常規 欄位一樣具有訪問器方法。但是,與常規欄位不同的是,一個 oneof 中的欄位最多隻能同時設定一個,因此設定一個欄位會清除其他欄位。

message = Foo.new

# Fields have their defaults.
raise unless message.name == ""
raise unless message.serial_number == 0
raise unless message.test_oneof == nil

message.name = "Bender"
raise unless message.name == "Bender"
raise unless message.serial_number == 0
raise unless message.test_oneof == :name

# Setting serial_number clears name.
message.serial_number = 2716057
raise unless message.name == ""
raise unless message.test_oneof == :serial_number

# Setting serial_number to nil clears the oneof.
message.serial_number = nil
raise unless message.test_oneof == nil

對於 proto2 訊息,oneof 成員也具有單獨的 has_...? 方法:

message = Foo.new

raise unless !message.has_test_oneof?
raise unless !message.has_name?
raise unless !message.has_serial_number?
raise unless !message.has_test_oneof?

message.name = "Bender"
raise unless message.has_test_oneof?
raise unless message.has_name?
raise unless !message.has_serial_number?
raise unless !message.has_test_oneof?