区分 Protobuf 中缺失值和默认值

区分 Protobuf 中缺失值和默认值 - 知乎 https://zhuanlan.zhihu.com/p/46603988

Since protobuf release 3.15, proto3 supports using the optional keyword (just as in proto2) to give a scalar field presence information.

区分 Protobuf 中缺失值和默认值

背景

Protobuf 是目前非常主流的二进制序列化格式,GRPC 默认使用 Protobuf v3 格式,下面是 Protobuf 消息定义的例子:

# proto2
message Account {
	required string name = 1;    # 必需
    optional double profit_rate = 2 [default=-1.0];  # 可选,默认值修改成 -1.0,有 hasProfitRate()
}

# proto3
message Account {
	string name = 1;			# 可选,默认值为空字符串,无 hasName()
	double profit_rate = 2;		# 可选,默认值为 0.0,无 hasProfitRate()
}
  • 在 Protobuf 2 中,消息的字段可以加 required 和 optional 修饰符,也支持 default 修饰符指定默认值。默认配置下,一个 optional 字段如果没有设置,或者显式设置成了默认值,在序列化成二进制格式时,这个字段会被去掉,导致反序列化后,无法区分是当初没有设置还是设置成了默认值但序列化时被去掉了,即使 Protobuf 2 对于原始数据类型字段都有 hasXxx() 方法,在反序列化后,对于这个“缺失”字段,hasXxx() 总是 false——失去了其判定意义。
  • 在 Protobuf 3 中,更进一步,直接去掉了 required 和 optional 修饰符,所有字段都是 optional 的, 而且对于原始数据类型字段,压根不提供 hasXxx() 方法。来自 Google 的 GRPC 核心成员Eric Anderson 在 StackOverflow 网站很好的解释了这个设计决策的原因:Why required and optional is removed in Protocol Buffers 3

因此,在 Protobuf 3 中,同学们往往有一个疑问:比如收益率字段,怎么知道是收益率还没算出来(值为 NULL),还是收益率是 0.0 呢?两种情况下 getProfitRate() 都是返回 0.0。

Protobuf 2 中有个设置选项,可以让序列化时保留显式设置的默认值,但 GRPC 主流使用的 Protobuf 3,所以下面只讲述 Protobuf 3 中的解决方案,大部分来自伟大的 StackOverflow 网站:How to define an optional field in protobuf 3

方案一:用特殊值区分,尽量避免 null

Protobuf 3 为每个字段都提供默认值,除了 Eric 提到的考虑,这也是个极好的编程实践,与业界逐渐意识到 null 的危害而转向 Optional 类型相呼应。 原始数据类型保证不出现 null,这会极大的简化代码判断,提高健壮性。

绝大部份情况下,“没设置”跟默认 0 / 0.0 / false / "" 等价,是不会破坏业务逻辑的,比如“未收取手续费”跟“收取了 0.0 元手续费”是一个意思,如果业务逻辑一定要区分,比如收益率,可以考虑用特殊值区分,比如 -1.0,Double.MAX_VALUE 等,这跟大家习惯的函数返回值既表示错误也表示正常返回值的做法类似:open() 函数返回 -1 表示失败,否则表示成功。

另一个策略是把紧密相关的字段打包成消息类型,由于不再是原始数据类型,比如 profit_rate_with_date,就可以用 hasXxx() 判断了。注意不能用 getProfitRateWithDate() == null 判断,因为没有显式设置时,getProfitRateWithDate() 返回 default instance,而且 setProfitRateWithDate(null) 也是不允许的,这背后的设计考虑显而易见。

方案二:显式定义 boolean 字段(不建议)

message Account {
	string name = 1;
	double profit_rate = 2;
    bool  has_profit_rate = 3;
}

这个办法很直白,但浪费内存和网络带宽,而且每次设置 profit_rate 之后,要记得也设置 has_profit_rate 字段,麻烦。

方案三:使用 oneof 黑科技

message Account {
	string name = 1;
	oneof profit_rate {		# 可以加个 _present 后缀什么的
		double profit_rate = 2;
	}
}

oneof 的用意是达到 C 语言 union 数据类型的效果,但极富创造力的群众发现可以用 oneof 表达“缺失值”的概念。

  • 在 Java 中,依然对于原始数据类型没有 hasXxx(),需要用 XxxCase() == XxxCase.XXX_NOT_SET 或者 XxxCase().getNumber == 0 判断是否设置了。
  • 在 JavaScript 中,很鸡贼的对 oneof 类型生成了 hasXxx(),不清楚这种用法将来会不会一直支持下去。也可以用 Java 的类似语法判断。 注意不能用 getProfitRate() == null 判断,因为没设置的情况下,这个函数返回默认值 0.

在 JavaScript 里 msg.toObject() 虽然很方便转换成 plain object,但是对于 unset field,会转成默认值,失去 unset 语意。

方案四:使用 wrapper 类型

import "google/protobuf/wrappers.proto";

message Account {
	string name = 1;
	google.protobuf.DoubleValue profit_rate = 2;
}

这个方案利用了 Protobuf 3 只对原始数据类型不生成 hasXxx() 的特点,这跟编程语言中的 primitive type 和 boxed primitive type 之分是一致的,前者没有 null 一说。采用 wrapper 类型后,就可以用 hasXxx() 判断是否设置过了。

  • 在 Java 中,使用 setProfitRate(DoubleValue.of(1.0)) 设置,使用 getProfitRate().getValue() 获取
  • 在 JavaScript 中,使用 setProfitRate(new pb.google.protobuf.DoubleValue([1.0]) 设置 (我也不清楚为什么参数是数组,搞这么恶心),使用 getProfitRate().getValue() 或者使用 +getProfiltRate(),这里利用了 DoubleValue.toString() 。另外,在未设置时,JavaScript 里会鸡贼的让 getProfitRate() 返回 undefined,不清楚这种用法将来会不会一直支持下去。

Java 版的 Protobuf 中,com.google.protobuf.util.JsonFormat.printer().print(msg) 方法,对于 wrapper 类型有特殊处理(实际是 wrapper 类型自身实现的缘故),虽然 wrapper 类型的 protobuf 定义有个 value 字段,JsonFormat 序列化成 JSON 格式时,并不会输出 "xxx": { "value": 1.0 },而是直接输出 "xxx": 1.0,某些场合下可以利用这个特性。 JavaScript 版本 Protobuf 实现则没有这个功能,略微可见 Google 内部还是 Java 主流的多。

总结

  1. 尽量缩小需要区分缺失值的地方
  2. oneof 和 wrapper 两种办法各有便利,相比较下,更推荐符合正常思维的 wrapper 方案,不推荐奇技淫巧的 oneof 方案
  3. 坚持用 hasXxx() 判断是否缺失值,这是 Protobuf 通行方式,不要用 Xxx == null or undefined 来判断,不具备可移植性
编辑于 2018-10-12 17:13
protobuf
gRPC
Java
14 条评论
 
 

protobuf3中也可以使用optional修饰词了

2020-11-30
 
Since protobuf release 3.15, proto3 supports using the optional keyword (just as in proto2) to give a scalar field presence information.

 


文中的信息可能已经不适用了
06-11

 

posted @ 2022-11-30 16:14  papering  阅读(710)  评论(0编辑  收藏  举报