165

我需要在 protobuf(proto3 语法)中指定带有可选字段的消息。就 proto 2 语法而言,我想表达的信息是这样的:

message Foo {
    required int32 bar = 1;
    optional int32 baz = 2;
}

据我了解,“可选”概念已从语法 proto 3(以及必需的概念)中删除。虽然尚不清楚替代方案 - 使用默认值来说明尚未从发件人指定字段,但如果默认值属于有效值域(例如考虑布尔类型),则会留下歧义。

那么,我应该如何对上面的消息进行编码?谢谢你。

4

9 回答 9

159

在 proto3 中,所有字段都是“可选的”(如果发件人未能设置它们,这不是错误)。但是,字段不再是“可空的”,因为无法区分字段被显式设置为其默认值与根本没有被设置之间的区别。

如果您需要“空”状态(并且没有可用于此的超出范围的值),那么您需要将其编码为单独的字段。例如,您可以这样做:

message Foo {
  bool has_baz = 1;  // always set this to "true" when using baz
  int32 baz = 2;
}

或者,您可以使用oneof

message Foo {
  oneof baz {
    bool baz_null = 1;  // always set this to "true" when null
    int32 baz_value = 2;
  }
}

oneof版本在线上更明确、更高效,但需要了解oneof值的工作原理。

最后,另一个完全合理的选择是坚持使用 proto2。proto2 没有被弃用,事实上许多项目(包括 Google 内部)非常依赖 proto2 的特性,这些特性在 proto3 中被删除,因此它们可能永远不会切换。因此,在可预见的未来继续使用它是安全的。

于 2017-03-06T20:11:47.653 回答
139

从 protobuf 3.15 版开始,proto3 支持使用optional关键字(就像在 proto2 中一样)来提供标量字段存在信息。

syntax = "proto3";

message Foo {
    int32 bar = 1;
    optional int32 baz = 2;
}

为上面的字段生成一个has_baz()/hasBaz()方法optional,就像在 proto2 中一样。

正如Cyber​​Snoopy 的回答所建议的那样,在幕后,protoc有效地将optional字段视为使用oneof包装器声明的:

message Foo {
    int32 bar = 1;
    oneof optional_baz {
        int32 baz = 2;
    }
}

如果您已经使用过这种方法,您现在可以简化您的消息声明(从 切换oneofoptional)和代码,因为有线格式是相同的。

有关字段存在和optionalproto3 的详细信息可以在应用说明:字段存在文档中找到。

历史记录:对 proto3 的实验性支持optional于 2020 年 4 月 23 日在此评论中首次宣布。使用它需要--experimental_allow_proto3_optional在 3.12-3.14 版本中传递 protoc 标志。

于 2020-06-25T00:28:02.777 回答
131

一种方法是optional喜欢在接受的答案中描述:https ://stackoverflow.com/a/62566052/1803821

另一种是使用包装对象。您不需要自己编写它们,因为谷歌已经提供了它们:

在 .proto 文件的顶部添加这个导入:

import "google/protobuf/wrappers.proto";

现在您可以为每种简单类型使用特殊的包装器:

DoubleValue
FloatValue
Int64Value
UInt64Value
Int32Value
UInt32Value
BoolValue
StringValue
BytesValue

因此,要回答原始问题,这种包装器的用法可能是这样的:

message Foo {
    int32 bar = 1;
    google.protobuf.Int32Value baz = 2;
}

现在,例如在 Java 中,我可以执行以下操作:

if(foo.hasBaz()) { ... }

于 2018-04-30T11:49:23.937 回答
37

根据 Kenton 的回答,一个更简单但可行的解决方案如下所示:

message Foo {
    oneof optional_baz { // "optional_" prefix here just serves as an indicator, not keyword in proto2
        int32 baz = 1;
    }
}
于 2017-10-19T02:05:27.357 回答
8

在此处扩展 @cybersnoopy 的建议

如果您有一个带有如下消息的 .proto 文件:

message Request {
    oneof option {
        int64 option_value = 1;
    }
}

您可以使用提供的案例选项(java 生成的代码)

所以我们现在可以写一些代码如下:

Request.OptionCase optionCase = request.getOptionCase();
OptionCase optionNotSet = OPTION_NOT_SET;

if (optionNotSet.equals(optionCase)){
    // value not set
} else {
    // value set
}
于 2019-02-12T18:59:01.527 回答
1

对您想要的消息进行编码的另一种方法是添加另一个字段来跟踪“设置”字段:

syntax="proto3";

package qtprotobuf.examples;

message SparseMessage {
    repeated uint32 fieldsUsed = 1;
    bool   attendedParty = 2;
    uint32 numberOfKids  = 3;
    string nickName      = 4;
}

message ExplicitMessage {
    enum PARTY_STATUS {ATTENDED=0; DIDNT_ATTEND=1; DIDNT_ASK=2;};
    PARTY_STATUS attendedParty = 1;
    bool   indicatedKids = 2;
    uint32 numberOfKids  = 3;
    enum NO_NICK_STATUS {HAS_NO_NICKNAME=0; WOULD_NOT_ADMIT_TO_HAVING_HAD_NICKNAME=1;};
    NO_NICK_STATUS noNickStatus = 4;
    string nickName      = 5;
}

如果有大量字段并且只分配了少数字段,这尤其适用。

在 python 中,用法如下所示:

import field_enum_example_pb2
m = field_enum_example_pb2.SparseMessage()
m.attendedParty = True
m.fieldsUsed.append(field_enum_example_pb2.SparseMessages.ATTENDEDPARTY_FIELD_NUMBER)
于 2020-12-02T18:52:57.867 回答
0

只需使用:

syntax = "proto3";

message Hello {
    int64 required_id = 1;
    optional int64 optional_id = 2;
}

在 Go 中,它使用

type Hello struct {
   ...
   RequiredId int64 ...
   OptionalId *int64 ...
   ...
}

您可以轻松检查nil并区分默认值(零)和未设置值(零)。

这里的大多数答案都已过时且不必要地复杂。

于 2022-01-07T09:51:20.203 回答
-2

另一种方法是您可以为每个可选字段使用位掩码。如果设置了值,则设置这些位并重置未设置值的那些位

enum bitsV {
    baz_present = 1; // 0x01
    baz1_present = 2; // 0x02

}
message Foo {
    uint32 bitMask;
    required int32 bar = 1;
    optional int32 baz = 2;
    optional int32 baz1 = 3;
}

解析检查 bitMask 的值。

if (bitMask & baz_present)
    baz is present

if (bitMask & baz1_present)
    baz1 is present
于 2019-09-27T10:33:30.317 回答
-3

您可以通过将引用与默认实例进行比较来确定是否已初始化:

GRPCContainer container = myGrpcResponseBean.getContainer();
if (container.getDefaultInstanceForType() != container) {
...
}
于 2018-06-14T13:55:27.347 回答