Ruby 生成程式碼指南
在閱讀本文件之前,您應該先閱讀 proto2、proto3 或 editions 的語言指南。
編譯器呼叫
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.proto 和 src/bar/baz.proto 並生成兩個輸出檔案:build/gen/foo_pb.rb 和 build/gen/bar/baz_pb.rb。如果需要,編譯器將自動建立目錄 build/gen/bar,但它不會建立 build 或 build/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
每當您設定一個欄位時,該值都會根據該欄位的宣告型別進行型別檢查。如果值型別不正確(或超出範圍),則會引發異常。
單一欄位
對於單個原始欄位(數字、字串和布林值),分配給欄位的值應為正確型別,並且必須在適當的範圍內:
- 數字型別:值應為
Fixnum、Bignum或Float。分配的值必須在目標型別中精確表示。因此,將1.0分配給 int32 欄位是可以的,但分配1.2則不行。 - 布林欄位:值必須是
true或false。其他值不會隱式轉換為 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 Hash(Google::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 類將擁有名為 name 和 serial_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?