Ruby 程式碼產生指南

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

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

Ruby 的協定編譯器會發出使用 DSL 來定義訊息綱要的 Ruby 原始檔。但是,DSL 仍然可能會變更。在本指南中,我們只描述產生的訊息的 API,而不描述 DSL。

編譯器呼叫

當使用 --ruby_out= 命令列旗標呼叫時,protocol buffer 編譯器會產生 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;它們必須已經存在。

套件

.proto 檔案中定義的套件名稱用於為產生的訊息產生模組結構。給定一個類似如下的檔案

package foo_bar.baz;

message MyMessage {}

協定編譯器會產生一個名稱為 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#dupMessage#clone:執行此訊息的淺層複製並傳回新的複本。
  • Message#==:執行兩個訊息之間的深度相等比較。
  • Message#hash:計算訊息值的淺層雜湊。
  • Message#to_hashMessage#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 => 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 等呼叫來執行自動轉換。如有必要,您應該先自行轉換值。

檢查存在性

使用 optional 欄位時,欄位存在性會透過呼叫產生的 has_...? 方法來檢查。設定任何值 (即使是預設值) 也會將欄位標示為存在。欄位可以透過呼叫另一個產生的 clear_... 方法來清除。例如,對於具有 int32 欄位 foo 的訊息 MyMessage

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

單數訊息欄位

對於子訊息,未設定的欄位將傳回 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?
  raise unless message.submessage_field == nil
  puts "Submessage field is unset."
else
  raise unless message.submessage_field != nil
  message.clear_submessage_field
  raise unless message.submessage_field == nil
  puts "Cleared submessage field."
end

當您指派子訊息時,它必須是正確類型的產生訊息物件。

當您指派子訊息時,可能會建立訊息週期。例如

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

# test.rb

require 'foo'

message = RecursiveSubmessage.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?

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

與單數欄位不同,系統永遠不會為重複欄位產生 has_...? 方法。

Map 欄位

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;
  }
  optional SomeEnum bar = 1;
}

您可以像這樣引用枚舉值

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

您可以將數字或符號指派給枚舉欄位。當讀回該值時,如果枚舉值已知,則它將是一個符號,如果未知,則它將是一個數字。由於 proto3 使用開放枚舉語意,因此任何數字都可以指派給枚舉欄位,即使它未在枚舉中定義。

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?