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?