Ruby 程式碼產生指南
在閱讀本文檔之前,您應該先閱讀 proto2 或 proto3 的語言指南。
Ruby 的通訊協定編譯器會發出 Ruby 原始碼檔案,這些檔案使用 DSL 來定義訊息結構描述。但是,DSL 仍然可能會變更。在本指南中,我們僅說明產生的訊息的 API,而不說明 DSL。
編譯器調用
當使用 --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
;它們必須已存在。
套件
在 .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 {}
通訊協定緩衝區編譯器會產生一個名為 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
:將物件轉換為 RubyHash
物件。僅轉換最上層訊息。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
每當您設定欄位時,都會根據該欄位的宣告類型檢查值類型。如果值類型錯誤 (或超出範圍),則會引發例外狀況。
單數欄位
對於單數基本類型欄位 (數字、字串和布林值),您指派給欄位的值應該是正確的類型,並且必須在適當的範圍內
- 數字類型:值應該是
Fixnum
、Bignum
或Float
。您指派的值必須在目標類型中完全可表示。因此,將1.0
指派給 int32 欄位是可以的,但指派1.2
則不行。 - 布林值欄位:值必須是
true
或false
。沒有其他值會隱含地轉換為 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 陣列。
與單數欄位不同,永遠不會為重複欄位產生 has_...?
方法。
Map 欄位
Map 欄位使用特殊類別表示,該類別的作用類似於 Ruby Hash
(Google::Protobuf::Map
)。與常規 Ruby 雜湊不同,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 類別將具有名為 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?