注意:此网站已被弃用。该网站将在 2023 年 1 月 31 日后关闭,而流量将重定向到位于 https://protobuf.dev 的新网站。在此期间,我们仅会针对 protobuf.dev 进行更新。

Ruby 生成的代码

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

本页面介绍了协议缓冲区编译器为任何给定的协议定义生成的消息对象的 API。在阅读本文档之前,您应阅读 proto2proto3 的语言指南。

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.protosrc/bar/baz.proto 文件并生成两个输出文件:build/gen/foo_pb.rbbuild/gen/bar/baz_pb.rb。编译器会根据需要自动创建目录 build/gen/bar,但不会创建 buildbuild/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#dupMessage#clone:对此消息执行浅层复制并返回新副本。
  • Message#==:对两条消息执行深度等式比较。
  • Message#hash:计算消息值的浅层哈希值。
  • Message#to_hashMessage#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

每次设置字段时,都会对照该字段声明的类型对值进行类型检查。如果值的类型错误(或超出范围),则会引发异常。

单数字段

对于单数原始字段(数字、字符串和布尔值),分配给该字段的值应具有正确的类型,并且必须在适当的范围内:

  • 数字类型:值应为 FixnumBignumFloat。分配的值必须能准确代表目标类型。因此,可以将 1.0 分配给 int32 字段,但不允许分配 1.2
  • 布尔值字段:值必须为 truefalse。其他值不会隐式转换为 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 类将有名为 nameserial_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?