How to define an optional field in protobuf 3
我需要在protobuf(proto3语法)中指定一个带有可选字段的消息。 用proto 2语法来说,我要表达的信息是这样的:
1 2 3 4
| message Foo {
required int32 bar = 1;
optional int32 baz = 2;
} |
据我了解,"可选"概念已从语法原型3中删除(以及必需的概念)。 尽管尚不清楚替代方案-使用默认值声明尚未从发送方指定字段,但如果默认值属于有效值域(例如,考虑布尔类型),则仍会造成歧义。
那么,我应该如何编码上面的消息? 谢谢。
-
这种方法是否合理? 消息NoBaz {}消息Foo {int32 bar = 1; oneof baz {NoBaz undefined = 2; 定义的int32 = 3; }; }
-
如果其他人找到了,但正在使用Proto 2,则可以找到该问题的Proto 2版本。
-
proto3基本上使所有字段都是可选的。 但是,对于标量,它们无法区分"未设置字段"和"但已设置为默认值"。 如果您将标量包装在单例中,例如 -消息等等{v1中的一个{int32 foo = 1; }},然后您可以再次检查是否实际设置了foo。 至少对于Python,您可以直接对foo进行操作,就好像它不在oneof中一样,并且可以询问HasField(" foo")。
在proto3中,所有字段均为"可选"(如果发件人未能设置它们,这不是错误)。但是,字段不再是"可为空的",因为无法分辨出明确设置为默认值的字段与根本没有设置默认值之间的区别。
如果需要"空"状态(并且没有可用于此的超出范围的值),则需要将其编码为单独的字段。例如,您可以执行以下操作:
1 2 3 4
| message Foo {
bool has_baz = 1; // always set this to"true" when using baz
int32 baz = 2;
} |
另外,您可以使用oneof:
1 2 3 4 5 6
| 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您的解决方案有效,但我建议对空消息使用布尔值。两者都会占用两个字节,但是空消息将占用更多的CPU,RAM和产生的代码膨胀。
-
我发现消息Foo {baof的一个{int32 baz_value = 1; }效果很好。
-
@Cyber??Snoopy您可以将其发布为答案吗?您的解决方案完美而优雅。
-
@Cyber??Snoopy在发送结构如下的响应消息时,是否偶然遇到任何问题:message FooList {repeating Foo foos = 1; }?您的解决方案很棒,但是我现在无法发送FooList作为服务器响应。
-
该答案给人的印象是无法区分丢失/默认消息字段。这仅在标量字段中适用。
-
布尔has_baz方法非常糟糕。它添加了一个额外的字段,该字段必须手动进行序列化,而不是自动维护,这很容易出错。 oneof方法更好,但是不需要baz_null。仅包含包含您要处理的标量的单例之一。大多数生成的代码(例如-Python)将允许您直接在标量上进行操作,就好像它不在oneof中一样,但是如果需要的话,它也允许您通过HasField显式检查是否存在/不存在。
-
经过进一步考虑,如果您不希望将字段缺失解释为null,而是希望具有将某些内容显式设置为null或忽略它(如果未指定)的能力,那么带有一个额外的显式字段来指示null的人就很有意义。在这种情况下,您可能要使用google.protobuf.NullValue而不是bool。该类型是一个只能有一个值(NULL_VALUE == 0)的枚举,因此您不会混淆true / false表示任何含义。此外,该类型是众所周知的类型,不同的语言可能使它更易于使用。
一种方法是使用oneof,如接受的答案中建议的那样。
另一个是使用包装对象。您不需要自己编写它们,因为Google已经提供了它们:
在您的.proto文件顶部,添加以下导入:
import"google/protobuf/wrappers.proto";
现在,您可以对每种简单类型使用特殊包装器:
1 2 3 4 5 6 7 8 9
| DoubleValue
FloatValue
Int64Value
UInt64Value
Int32Value
UInt32Value
BoolValue
StringValue
BytesValue |
因此,要回答原始问题,此类包装器的用法可能是这样的:
1 2 3 4
| message Foo {
int32 bar = 1;
google.protobuf.Int32Value baz = 2;
} |
现在以Java为例,我可以做类似的事情:
if(foo.hasBaz()) { ... }
-
这是如何运作的?当baz=null和baz未通过时,两种情况hasBaz()都表示false!
-
这个想法很简单:您可以使用包装器对象,也就是用户定义的类型。这些包装器对象被允许丢失。在使用gRPC时,我提供的Java示例非常适合我。
-
是的我了解一般的想法,但是我想看到它的实际效果。我不明白的是:(即使在包装对象中)"如何识别缺少的包装值和空包装值?"
-
因为如果原始消息中的值为null,则Int32Value会将值设置为默认实例。您可以尝试:如果您的值为null,则为TRUE:-if(request.getSomeInt32Value()== Int32Value.getDefaultInstance())
-
这是要走的路。使用C#,生成的代码将产生Nullable < T >属性。
-
比原来的awsner好!
-
公认答案中的一个不是很好。在oneof中不需要额外的baz_null字段。 WKT包装器很丑陋,因为现在您不能直接在标量上进行操作。相反,您必须跳过子消息。它还需要更多的序列化。此外,出于某些奇怪的原因,所有基本标量类型都没有现成的WKT包装器类型!!!单例oneof方法可以解决大多数语言中的大多数问题。对于Python,您可以直接在标量上进行操作,就好像它不是在oneof中一样,也可以询问HasField()。 .protos比较难看。
根据Kenton的回答,一个更简单但可行的解决方案如下所示:
1 2 3 4 5
| message Foo {
oneof optional_baz { //"optional_" prefix here just serves as an indicator, not keyword in proto2
int32 baz = 1;
}
} |
-
这如何体现可选字符?
-
基本上,其中一个名字不好用。它的意思是"至多"。总会有一个空值。
-
如果未设置,则值的大小写将为None(在C#中)-请参见枚举类型以了解您选择的语言。
-
是的,这可能是在proto3中为这只猫换皮的最好方法-即使它确实使.proto变得难看。
-
但是,它确实暗示您可以将字段的缺失解释为将其显式设置为null值。换句话说,在未指定可选字段和故意未指定字段(表示其为空)之间存在一些歧义。如果您关心该级别的精度,则可以在其中一个字段中添加一个额外的google.protobuf.NullValue字段,以区分未指定的字段,指定为值X的字段和指定为null的字段。它有点笨拙,但这就是因为proto3不像JSON那样直接支持null。
从protobuf 3.12版开始,proto3支持使用optional关键字(与proto2中一样)来提供标量字段存在信息。
1 2 3 4 5 6
| syntax ="proto3";
message Foo {
int32 bar = 1;
optional int32 baz = 2;
} |
就像在proto2中一样,上面的optional字段会生成一个has_baz() / hasBaz()方法。
正如Cyber??Snoopy的答案所暗示的那样,protoc可以有效地将optional字段视为使用oneof包装器声明的字段。如果您已经使用了这种方法,则可以清理消息声明(从oneof切换到optional),因为线路格式相同。
您可以在应用说明:字段存在文档中的proto3中找到有关字段存在和optional的详细信息。
在版本3.12中,此功能需要将--experimental_allow_proto3_optional标志传递给protoc。该功能公告称它将"通常有望在3.13中可用"。
在这里扩展@cybersnoopy的建议
如果您有一个带有如下消息的.proto文件:
1 2 3 4 5
| message Request {
oneof option {
int64 option_value = 1;
}
} |
您可以使用提供的案例选项(java生成的代码):
因此,我们现在可以编写一些代码,如下所示:
1 2 3 4 5 6 7 8
| 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,则可以像普通标量一样直接访问它们包含的标量。
关于此有一个很好的帖子:https://itnext.io/protobuf-and-null-support-1908a15311b6
解决方案取决于您的实际用例:
-
感谢您的提问:itnext.io/protobuf-and-null-support-1908a15311b6真的很有用。
另一种方法是可以对每个可选字段使用位掩码。并设置这些位(如果设置了值)并重置那些未设置值的位
1 2 3 4 5 6 7 8 9 10 11
| 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的值。
1 2 3 4 5
| if (bitMask & baz_present)
baz is present
if (bitMask & baz1_present)
baz1 is present |
您可以通过将引用与默认实例进行比较来查找是否已初始化:
1 2 3 4
| GRPCContainer container = myGrpcResponseBean.getContainer();
if (container.getDefaultInstanceForType() != container) {
...
} |
-
这不是一个好的通用方法,因为默认值通常是该字段的完全可接受的值,并且在那种情况下,您无法区分"缺少字段"和"存在但设置为默认值的字段"。