Як визначити необов’язкове поле в protobuf 3


111

Мені потрібно вказати повідомлення з необов’язковим полем у протобуфі (синтаксис прото3). З точки зору синтаксису прото 2, повідомлення, яке я хочу висловити, є приблизно таким:

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

З мого розуміння, "необов'язкова" концепція була вилучена із синтаксису proto 3 (разом із необхідною концепцією). Хоча альтернатива незрозуміла - використання значення за замовчуванням, щоб зазначити, що поле не було вказано від відправника, залишає неоднозначність, якщо значення за замовчуванням належить до дійсних значень (розглянемо, наприклад, логічний тип).

Отже, як я повинен кодувати повідомлення вище? Дякую.


Чи є підхід нижче обґрунтованого рішення? повідомлення NoBaz {} повідомлення Foo {int32 bar = 1; oneof baz {NoBaz undefined = 2; int32 визначено = 3; }; }
MaxP

2
Існує версія Proto 2 цього питання , якщо інші знайдуть це, але використовують Proto 2.
chwarr

1
proto3 в основному робить усі поля необов’язковими. Однак для скалярів вони унеможливили розрізнення "не встановленого поля" та "набору полів, але значення за замовчуванням". Якщо ви обертаєте свій скаляр одним синглоном, наприклад - повідомлення blah {oneof v1 {int32 foo = 1; }}, тоді ви можете ще раз перевірити, чи дійсно foo було встановлено чи ні. Щонайменше для Python ви можете працювати безпосередньо з foo так, ніби він не знаходиться всередині одного з них, і ви можете запитати HasField ("foo").
jschultz410

1
@MaxP Можливо, ви могли б змінити прийняту відповідь на stackoverflow.com/a/62566052/66465, оскільки нова версія протобуфа 3 тепер маєoptional
SebastianK

Відповіді:


55

З часу випуску протобуфа 3.12 прото3 має експериментальну підтримку використання optionalключового слова (так само, як у прото2) для надання інформації про наявність скалярного поля.

syntax = "proto3";

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

A has_baz()/ hasBaz()метод генерується для optionalполя вище, так само, як це було в proto2.

Під капотом protoc ефективно обробляє optionalполе так, ніби воно було оголошено за допомогою oneofобгортки, як випливає з відповіді CyberSnoopy :

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

Якщо ви вже використовували такий підхід, ви зможете очистити свої декларації повідомлень (переключитися з oneofна optional) після того, як прото3 optionalпідтримає випускників із експериментального статусу, оскільки формат проводів однаковий.

Ви можете знайти деталізовану деталі щодо присутності полів та optionalпротокол 3 у примітці до програми: Документ про наявність полів

Передайте --experimental_allow_proto3_optionalпрапор протоку, щоб використовувати цю функцію у випуску 3.12. В анонсі функції зазначено, що вона буде "загальнодоступною, сподіваємося, через 3.13".

Оновлення від листопада 2020 р .: Функція все ще вважається експериментальною (потрібен прапор) у випуску 3.14 . Є ознаки прогресу.


3
Ви випадково знаєте, як передавати прапор на C #?
Джеймс Хенкок,

Це найкраща відповідь зараз, коли proto3 додав кращий синтаксис. Чудова виноска Джарад!
Еван Моран,

Просто додамо для optional int xyz: 1) has_xyzвиявляє, чи було встановлено необов’язкове значення 2) clear_xyzскасує значення. Більше інформації тут: github.com/protocolbuffers/protobuf/blob/master/docs/…
Еван Моран,

@JamesHancock чи Java?
Тобі Акіньємі

1
@ JónásBalázs Передайте прапор --experimental_allow_proto3_optional протоку, щоб використовувати цю функцію у випуску 3.12.
jaredjacobs

127

У протоці3 всі поля є "необов'язковими" (оскільки це не помилка, якщо відправник не може їх встановити). Але поля перестають бути "нульовими", оскільки немає можливості визначити різницю між полем, яке явно встановлено за значенням за замовчуванням, і не встановленим взагалі.

Якщо вам потрібен "нульовий" стан (і немає жодного значення поза діапазоном, яке ви можете використовувати для цього), тоді вам потрібно буде закодувати це як окреме поле. Наприклад, ви можете зробити:

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працюють значення.

Нарешті, ще один цілком розумний варіант - дотримуватися прото2. Proto2 не є застарілим, і насправді багато проектів (у тому числі всередині Google) дуже залежать від функцій proto2, які видаляються в proto3, отже, вони, швидше за все, ніколи не перейдуть. Отже, можна продовжувати використовувати його в найближчому майбутньому.


Подібно до вашого рішення, у своєму коментарі я запропонував використовувати oneof з дійсним значенням і нульовим типом (порожнє повідомлення). Таким чином, ви не турбуєтесь логічним значенням (яке не повинно бути актуальним, оскільки якщо є логічне значення, то немає і значення baz_value) Правильно?
MaxP 07.03.17

2
@MaxP Ваше рішення працює, але я б порекомендував логічне значення для порожнього повідомлення. Будь-яке з них займе два байти на дроті, але для порожнього повідомлення буде потрібно значно більше процесора, оперативної пам’яті та згенерованого здуття коду.
Кентон Варда,

13
Я дізнався повідомлення Foo {oneof baz {int32 baz_value = 1; }} працює досить добре.
CyberSnoopy

@CyberSnoopy Чи можете ви опублікувати це як відповідь? Ваше рішення працює ідеально та елегантно.
Cheng Chen

@CyberSnoopy Ви випадково стикалися з будь-якими проблемами під час надсилання повідомлення-відповіді, яке структуровано приблизно так: message FooList {повторне 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
Ідея проста: ви використовуєте обгорткові об'єкти або іншими словами визначені користувачем типи. Ці об'єкти-обгортки можуть бути відсутніми. Я надав приклад Java, який добре працював для мене під час роботи з gRPC.
VM4

Ага! Я розумію загальну ідею, але я хотів побачити це в дії. Чого я не розумію, так це: (навіть у об’єкті-обгортці) " Як визначити відсутні та нульові значення обгортки? "
mayankcpdixit

3
Це шлях. За допомогою C # згенерований код створює властивості Nullable <T>.
Аарон Гудон,

6
Краще, ніж оригінальний Awsner!
Dev Aggarwal

33

Виходячи з відповіді Кентона, виглядає більш просте, але робоче рішення:

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 #) - див. Тип переліку для вибраної вами мови.
нітцель

Так, це, мабуть, найкращий спосіб зняти шкіру з цієї кішки в прото3 - навіть якщо це робить .proto трохи потворним.
jschultz410

Однак це дещо передбачає, що ви можете інтерпретувати відсутність поля як явне встановлення для нього нульового значення. Іншими словами, існує певна двозначність між "необов'язковим полем не вказано" та "поле не було вказано навмисно, щоб означати, що воно є нульовим". Якщо ви піклуєтесь про такий рівень точності, тоді ви можете додати додаткове поле google.protobuf.NullValue до того, яке дозволяє розрізнити "поле не вказано", "поле вказано як значення X" і "поле вказано як нуль" . Це досить хитро, але це тому, що proto3 не підтримує null безпосередньо, як JSON.
jschultz410

7

Щоб розширити пропозицію @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
}

У Python це ще простіше. Ви можете просто зробити request.HasField ("option_value"). Крім того, якщо у вашому повідомленні є купа таких символів, то ви можете отримати доступ до вміщених скалярів безпосередньо, як звичайний скаляр.
jschultz410


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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.