Ruby 程式碼產生指南
在閱讀本文檔之前,您應該先閱讀 proto2 或 proto3 的語言指南。
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.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 {}
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
:將物件轉換為 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 Array。
與單數欄位不同,系統永遠不會為重複欄位產生 has_...?
方法。
Map 欄位
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;
}
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?