如何在protobuf 3中定义可选字段


111

我需要在protobuf(proto3语法)中指定一个带有可选字段的消息。用proto 2语法来说,我要表达的信息是这样的:

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

据我了解,“可选”概念已从语法原型3中删除(以及必需的概念)。尽管尚不清楚替代方案-使用默认值声明尚未从发送方指定字段,但是如果默认值属于有效值域(例如,考虑布尔类型),则仍会造成歧义。

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


这种方法是否合理?消息NoBaz {}消息Foo {int32 bar = 1; oneof baz {NoBaz undefined = 2; 定义的int32 = 3; }; }
MaxP

2
这个问题一个Proto 2版本,如果其他人找到了却使用了
Proto2。– chwarr

1
proto3基本上使所有字段都是可选的。但是,对于标量,它们使得无法区分“未设置字段”和“已设置为默认值的字段”。如果将标量包装在一个单例oneof中,例如-message blah {oneof v1 {int32 foo = 1; }},然后您可以再次检查是否实际上设置了foo。至少对于Python,您可以直接对foo进行操作,就好像它不在oneof内一样,并且可以询问HasField(“ foo”)。
jschultz410 '20

1
@MaxP也许您可以将接受的答案更改为stackoverflow.com/a/62566052/66465,因为新版本的protobuf 3现在具有optional
SebastianK,

Answers:


54

从protobuf版本3.12开始,proto3在实验上支持使用optional关键字(就像proto2中一样)给出标量字段存在信息。

syntax = "proto3";

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

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

Cyber​​Snoopy的回答所示,在底层protoc有效地将optional字段视为使用oneof包装器声明的字段:

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

如果您已经使用了这种方法,则proto3支持从实验状态毕业后,您就可以清理您的消息声明(从切换oneofoptionaloptional,因为有线格式是相同的。

您可以optional应用笔记:现场状态文档中找到有关现场状态的详细信息,并在proto3中找到。

--experimental_allow_proto3_optional标记传递给协议以使用版本3.12中的此功能。该功能公告称它将“通常有望在3.13中可用”。

2020年11月更新:该功能在3.14版中仍被视为实验性(需要标记)。有迹象表明正在取得进展。


3
您是否知道如何在C#中传递标志?
詹姆斯·汉考克

由于proto3添加了更好的语法,这是最好的答案。伟大的标注Jarad!
埃文·莫兰

只需添加以下内容optional int xyz:1)has_xyz检测是否设置了可选值2)clear_xyz将取消设置该值。这里更多的信息:github.com/protocolbuffers/protobuf/blob/master/docs/...
埃文·莫兰

@JamesHancock还是Java?
Tobi Akinyemi

1
@JónásBalázs将--experimental_allow_proto3_optional标志传递给协议以使用版本3.12中的此功能。
jaredjacobs

126

在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中删除,因此它们可能永远不会切换。因此,在可预见的将来继续使用它是安全的。


与您的解决方案类似,在我的评论中,我建议使用带有真实值和空类型(空消息)的oneof。这样,您就不必担心布尔值(这应该无关紧要,因为如果有布尔值,那么就没有baz_value了)对吗?
MaxP

2
@MaxP您的解决方案有效,但我建议对空消息使用布尔值。两者都会占用两个字节,但是空消息将占用更多的CPU,RAM和产生的代码膨胀。
Kenton Varda'3

13
我发现消息Foo {baof的一个{int32 baz_value = 1; }效果很好。
Cyber​​Snoopy

@Cyber​​Snoopy您可以将其发布为答案吗?您的解决方案完美而优雅。
Cheng Chen

@Cyber​​Snoopy在发送结构如下的响应消息时,是否偶然遇到任何问题:message FooList {repeating Foo foos = 1; }?您的解决方案很棒,但是现在我无法发送FooList作为服务器响应。
Caffeinate常

102

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

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

在您的.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()) { ... }


3
这是如何运作的?何时baz=null以及何时baz未通过,两种情况都hasBaz()说明false
mayankcpdixit

1
这个想法很简单:您可以使用包装器对象,也就是用户定义的类型。这些包装器对象被允许丢失。在使用gRPC时,我提供的Java示例非常适合我。
VM4

是的 我了解一般的想法,但我想看到它的实际效果。我不明白的是:(甚至在包装对象中)“如何识别缺少的包装值和空包装值?
mayankcpdixit

3
这是要走的路。使用C#,生成的代码产生Nullable <T>属性。
亚伦·休顿

6
比原始的awsner好!
Dev Aggarwal,

33

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

message Foo {
    oneof optional_baz { // "optional_" prefix here just serves as an indicator, not keyword in proto2
        int32 baz = 1;
    }
}

这如何体现可选字符?
JFFIGK

20
基本上,其中一个名字不好用。它的意思是“至多”。总会有一个空值。
ecl3ctic

如果未设置,则值的大小写将为None(在C#中)-请参见枚举类型以了解您选择的语言。
尼特尔

是的,这可能是在proto3中为这只猫换皮的最好方法-即使它确实使.proto变得难看。
jschultz410 '20

但是,它确实暗示您可以将字段的缺失解释为将其显式设置为null值。换句话说,“未指定可选字段”和“未有意指定字段表示其为空”之间存在歧义。如果您关心该级别的精度,则可以在其中一个字段中添加一个额外的google.protobuf.NullValue字段,以区分“未指定字段”,“指定为值X的字段”和“指定为null的字段” 。这有点笨拙,但这是因为proto3不像JSON那样直接支持null。
jschultz410

7

这里扩展@cybersnoopy的建议

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

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

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

因此,我们现在可以编写一些代码,如下所示:

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

if (optionNotSet.equals(optionCase)){
    // value not set
} else {
    // value set
}

在Python中,它甚至更简单。您可以只执行request.HasField(“ option_value”)。另外,如果您的消息中有很多这样的单例oneof,则可以像正常标量一样直接访问它们包含的标量。
jschultz410 '20


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)

-1

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

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

-2

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

GRPCContainer container = myGrpcResponseBean.getContainer();
if (container.getDefaultInstanceForType() != container) {
...
}

1
这不是一个好的通用方法,因为默认值通常是该字段的完全可接受的值,在这种情况下,您无法区分“缺少字段”和“存在字段但设置为默认值”。
jschultz410 '20
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.