2021.4.14 追記
proto3で削除されたoptionalですがv3.15(experimentalオプションを利用する場合はv3.12)から正式に実装されたため、それ以降のバージョンを利用する場合は素直にoptionalを利用してもらうのがいいと思います!
https://github.com/protocolbuffers/protobuf/releases/tag/v3.15.0
Protocol Buffersはproto3でrequired
とoptional
が削除されました。
そもそも削除された経緯に関しては、@qsonaさんのエントリーにて、分かりやすくまとめて下さっています。
そこで課題になるのが、proto3において各フィールドは全てデフォルト値を持つため、デフォルト値が設定されたフィールドが利用側から
意図的にセットされたデフォルト値と同様の値
-
存在しないためセットされなかったデフォルト値
のどちらか判断できないということです。
つまりoptionalな値をどのように表現するかということです。
optionalの表現方法
選択肢はいくつかありますが、Stack Overflowの回答が参考になります。
1.別フィールドとして、明示的に有無を表現する
message User {
int32 id = 1;
bool has_name = 2; // nameの存在有無を表す
string name = 3; // 実際の値
}
User user1 = User.newBuilder()
.setHasName(true)
.setName("Tom")
.build();
User user2 = User.newBuilder()
.setHasName(false) // デフォルト値がfalseであるため、実際にはセットも不要
.build();
user1.getHasName(); // true
user1.getName(); // "Tom"
user2.getHasName(); // false
user2.getName(); // ""
2.oneofを利用する
message User {
int32 id = 1;
oneof name_optional {
string name = 2; // 扱い値をoneofでラップする
}
}
User user1 = User.newBuilder()
.setName("Tom")
.build();
User user2 = User.newBuilder()
.build();
user1.getNameOptionalCase(); // User.NameOptionalCase.NAME
user1.getName(); // "Tom"
user2.getNameOptionalCase(); // User.NameOptionalCase.NAMEOPTIONAL_NOT_SET
user2.getName(); // ""
3.google/protobuf/wrappers.proto
を利用する
message User {
int32 id = 1;
google.protobuf.StringValue name = 2;
}
User user1 = User.newBuilder()
.setName(StringValue.newBuilder().setValue("Tom").build())
.build();
User user2 = User.newBuilder()
.build();
user1.hasName(); // true
user1.getName().getValue(); // "Tom"
user2.hasName(); // false
user2.getName().getValue(); // ""
考察
まず共通して言えるのは、どの選択肢を選んでも、optionalな値であることを強制できないということです。*1
従ってどの選択肢でも、存在有無のチェック
→ 値に対する処理
を手続き的に行う必要があります。
1は、記法は非常にシンプルですが、2つのフィールドの関係性をコンパイラでは保証できず、コメント等でサポートする必要があります。
2は、1と比べコンパイラレベルでフィールドの有無を表現できるのがメリットですが、複数フィールドが想定されているoneofからすると少し違和感のある使い方です。
3は、optionalな値を扱う上で直感的で使いやすいですが、いくつかデメリットもあります。
- wrapper用のBuilderが必要だったり、値を取り出すために
getValue()
を呼び出したりと若干冗長 - プリミティブでない値をoptionalにしたい場合、1か2と併用する必要があり、optionalの表現方法が2通りになり統一できない
例:
message User {
int32 id = 1;
Fullname fullname = 2;
}
message Fullname {
string first_name = 1;
string last_name = 2;
}
例えばこのようなケースではwrapperは以下の型しか提供されていないため、fullnameごとoptionalにしたい場合には利用できません。
DoubleValue
FloatValue
Int64Value
UInt64Value
Int32Value
UInt32Value
BoolValue
StringValue
BytesValue
従って、optional表現を統一したい場合には、1か2で統一するのが分かりやすいと思います。
*1: 言語によって実装様々ですが、Maybe、Option、Optional、null安全のような存在しない可能性を強制するもの
まとめ
Protocol Buffersは仕様がコンパクトなスキーマ言語で、学習コストも低く可読性も良いのが特徴です。
しかしそれ故に、他ではよくあるような表現が取り除かれているものもあり、それらを適時カバーしながら使用することでより多くの表現ができると思います。