Ruby 生成代码指南
在阅读本文档之前,您应该阅读 proto2 或 proto3 的语言指南。
Ruby 的 Protocol Buffer 编译器会生成使用 DSL 定义消息模式的 Ruby 源文件。但是,该 DSL 仍可能发生变化。本指南仅描述生成的 message 的 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 名称用于为生成的 message 生成模块结构。给定一个像这样的文件:
package foo_bar.baz;
message MyMessage {}
Protocol Buffer 编译器将生成一个名为 FooBar::Baz::MyMessage
的输出消息。
但是,如果 .proto
文件包含 ruby_package
选项,如下所示:
option ruby_package = "Foo::Bar";
那么生成的输出将优先使用 ruby_package
选项,并生成 Foo::Bar::MyMessage
。
消息
给定一个简单的 message 声明:
message Foo {}
Protocol Buffer 编译器会生成一个名为 Foo
的类。生成的类继承自 Ruby 的 Object
类(proto 没有共同的基类)。与 C++ 和 Java 不同,Ruby 生成的代码不受 .proto
文件中 optimize_for
选项的影响;实际上,所有 Ruby 代码都针对代码大小进行了优化。
您 不应 创建自己的 Foo
子类。生成的类并非设计用于子类化,可能导致“脆弱的基类”问题。
Ruby message 类为每个字段定义了访问器,并提供了以下标准方法:
Message#dup
,Message#clone
: 对此 message 执行浅拷贝并返回新副本。Message#==
: 对两个 message 执行深度相等比较。Message#hash
: 计算 message 值的浅层哈希。Message#to_hash
,Message#to_h
: 将对象转换为 rubyHash
对象。仅转换顶级 message。Message#inspect
: 返回表示此 message 的人类可读字符串。Message#[]
,Message#[]=
: 按字符串名称获取或设置字段。将来可能也会用于获取/设置扩展。
message 类还定义了以下静态方法。(通常我们更喜欢静态方法,因为常规方法可能与您在 .proto 文件中定义的字段名冲突。)
Message.decode(str)
: 解码此 message 的二进制 protobuf 并返回一个新实例。Message.encode(proto)
: 将此类 message 对象序列化为二进制字符串。Message.decode_json(str)
: 解码此 message 的 JSON 文本字符串并返回一个新实例。Message.encode_json(proto)
: 将此类 message 对象序列化为 JSON 文本字符串。Message.descriptor
: 返回此 message 的Google::Protobuf::Descriptor
对象。
创建 message 时,可以在构造函数中方便地初始化字段。以下是构造和使用 message 的示例:
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 可以在另一个 message 内部声明。例如:
message Foo {
message Bar { }
}
在这种情况下,Bar
类被声明为 Foo
内部的一个类,因此您可以将其引用为 Foo::Bar
。
字段
对于 message 类型中的每个字段,都有用于设置和获取该字段的访问器方法。因此,给定一个字段 foo
,您可以编写:
message.foo = get_value()
print message.foo
每当您设置字段时,都会根据该字段的声明类型检查值类型。如果值类型错误(或超出范围),将引发异常。
单数(非重复)字段
对于单数(非重复)原始字段(数字、字符串和布尔值),您赋给字段的值应具有正确的类型,并且必须在适当的范围内:
- 数字类型:值应为
Fixnum
、Bignum
或Float
。您赋的值必须能在目标类型中精确表示。因此,将1.0
赋给 int32 字段是可以的,但赋1.2
是不允许的。 - 布尔字段:值必须是
true
或false
。其他值不会隐式转换为 true/false。 - Bytes 字段:赋的值必须是
String
对象。protobuf 库将复制字符串,将其转换为 ASCII-8BIT 编码,并冻结它。 - String 字段:赋的值必须是
String
对象。protobuf 库将复制字符串,将其转换为 UTF-8 编码,并冻结它。
不会发生自动的 #to_s
、#to_i
等调用来执行自动转换。如有必要,您应该先自己转换值。
检查存在性
使用 optional
字段时,通过调用生成的 has_...?
方法来检查字段存在性。设置任何值(即使是默认值)都会将字段标记为存在。可以通过调用另一个生成的 clear_...
方法来清除字段。例如,对于一个带有 int32 字段 foo
的 message 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
之外,生成的 message 还具有 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
当您赋一个子 message 时,它必须是正确类型的生成的 message 对象。
当您赋子 message 时,可能会创建 message 循环。例如:
// 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
是用特定类型构造的,并且期望所有数组成员都具有正确的类型。类型和范围的检查方式与 message 字段相同。
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?
对于包含 message 的重复字段,Google::Protobuf::RepeatedField
的构造函数支持一种带有三个参数的变体::message
、子 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 字段
Map 字段使用一个特殊类表示,该类类似于 Ruby 的 Hash
(Google::Protobuf::Map
)。与常规 Ruby 哈希不同,Map
是用特定类型的键和值构造的,并期望所有 map 的键和值都具有正确的类型。类型和范围的检查方式与 message 字段和 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
: 返回此枚举的 descriptor。
Oneof
给定一个带 oneof 的 message:
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 message,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?