本页面介绍了协议缓冲区编译器为任何给定的协议定义生成的消息对象的 API。在阅读本文档之前,您应阅读 proto2 或 proto3 的语言指南。
Ruby 协议编译器会发出使用 DSL 定义消息架构的 Ruby 源文件。不过,DSL 仍可能会随时发生变化。在本指南中,我们仅介绍生成的消息的 API,而不是 DSL。
编译器调用
协议缓冲区编译器在使用 --ruby_out=
命令行 flag 调用时会生成 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
的输出消息。
信息
假设有一个简单的消息声明:
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
:将对象转换为红宝石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
每次设置字段时,都会对照该字段声明的类型对值进行类型检查。如果值的类型错误(或超出范围),则会引发异常。
单数字段
对于单数原始字段(数字、字符串和布尔值),分配给该字段的值应具有正确的类型,并且必须在适当的范围内:
- 数字类型:值应为
Fixnum
、Bignum
或Float
。分配的值必须能准确代表目标类型。因此,可以将1.0
分配给 int32 字段,但不允许分配1.2
。 - 布尔值字段:值必须为
true
或false
。其他值不会隐式转换为 true/false。 - 字节字段:分配的值必须是
String
对象。protobuf 库将复制该字符串,将其转换为 ASCII-8BIT 编码,然后将其冻结。 - 字符串字段:分配的值必须是
String
对象。protobuf 库将复制该字符串,将其转换为 UTF-8 编码,然后对其进行冻结。
系统不会自动执行 #to_s
、#to_i
等调用以执行自动转换。如有必要,您应首先自行转换值。
正在检查在家/外出状态
使用“可选”字段时,系统会通过调用生成的 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_...?
方法。
映射字段
映射字段使用类似于 Ruby Hash
(Google::Protobuf::Map
) 的特殊类表示。与常规 Ruby 哈希不同,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枚举模块还定义了以下实用程序方法:
Enum#lookup(number)
:查找给定数字并返回其名称,如果未找到则返回nil
。如果有多个名称具有此编号,则返回定义的第一个名称。Enum#resolve(symbol)
:返回此枚举名称的数值;如果未找到,则返回nil
。Enum#descriptor
:返回此枚举的描述符。
单曲
根据以下消息之一:
message Foo { oneof test_oneof { string name = 1; int32 serial_number = 2; } }
与 Foo
对应的 Ruby 类将有名为 name
和 serial_number
的成员,这些访问器方法与常规字段一样。但是,与常规字段不同的是,一次只能设置其中一个字段中的一个,因此设置其中一个字段会清除其他字段。
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 消息,其中一个成员也拥有单独的 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?