Jak zdefiniować opcjonalne pole w protobuf 3

111

Muszę określić wiadomość z opcjonalnym polem w protobuf (składnia proto3). Jeśli chodzi o składnię Proto 2, komunikat, który chcę wyrazić, jest mniej więcej taki:

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

Z mojego rozumienia „opcjonalne” pojęcie zostało usunięte ze składni proto 3 (wraz z wymaganym pojęciem). Chociaż nie jest jasna alternatywa - użycie wartości domyślnej do stwierdzenia, że ​​pole nie zostało określone przez nadawcę, pozostawia niejednoznaczność, jeśli wartość domyślna należy do domeny prawidłowych wartości (na przykład typ boolowski).

Jak więc mam zakodować powyższą wiadomość? Dziękuję Ci.

MaxP
źródło
Czy poniższe podejście jest dobrym rozwiązaniem? wiadomość NoBaz {} wiadomość Foo {int32 bar = 1; oneof baz {NoBaz undefined = 2; int32 zdefiniowano = 3; }; }
MaxP
2
Istnieje wersja Proto 2 tego pytania , jeśli inni ją znajdują, ale używają Proto 2.
chwarr
1
Proto3 zasadniczo sprawia, że ​​wszystkie pola są opcjonalne. Jednak w przypadku skalarów uniemożliwiały one rozróżnienie między „polem nieustawionym” a „polem ustawionym, ale z wartością domyślną”. Jeśli zawiniesz swój skalar w singleton, np. - message blah {oneof v1 {int32 foo = 1; }}, możesz ponownie sprawdzić, czy foo zostało faktycznie ustawione, czy nie. Przynajmniej w przypadku Pythona możesz operować bezpośrednio na foo, tak jakby nie było wewnątrz oneof, i możesz zapytać HasField ("foo").
jschultz410
1
@MaxP Może mógłbyś zmienić zaakceptowaną odpowiedź na stackoverflow.com/a/62566052/66465, ponieważ ma teraz nowsza wersja protobuf 3optional
SebastianK

Odpowiedzi:

55

Od wersji 3.12 protobuf , proto3 ma eksperymentalne wsparcie dla użycia optionalsłowa kluczowego (tak jak w proto2) do podania skalarnej informacji o obecności pola.

syntax = "proto3";

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

has_baz()/ hasBaz()Metoda jest generowany na optionalobszarze powyżej, tak jak to było w proto2.

Pod maską Protoc skutecznie traktuje optionalpole tak, jakby zostało zadeklarowane przy użyciu oneofopakowania, jak sugeruje odpowiedź CyberSnoopy :

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

Jeśli już zastosowałeś to podejście, będziesz w stanie wyczyścić deklaracje wiadomości (przełączyć się z oneofna optional), gdy obsługa optionalprotokołu proto3 przejdzie ze statusu eksperymentalnego, ponieważ format połączenia jest taki sam.

Szczegółowe informacje dotyczące obecności optionalw terenie oraz w protokole Proto3 można znaleźć w Nota aplikacyjna: Obecność w terenie doc.

Przekaż --experimental_allow_proto3_optionalflagę protokołowi, aby użyć tej funkcji w wersji 3.12. Ogłoszenie funkcji mówi, że będzie „ogólnie dostępne miejmy nadzieję w 3.13”.

Aktualizacja z listopada 2020 r .: ta funkcja jest nadal uważana za eksperymentalną (wymagana flaga) w wersji 3.14 . Istnieją oznaki postępu.

jaredjacobs
źródło
3
Czy wiesz, jak przekazać flagę w C #?
James Hancock,
To najlepsza odpowiedź teraz, gdy proto3 dodał lepszą składnię. Świetne objaśnienie Jarad!
Evan Moran,
Wystarczy dodać dla optional int xyz: 1) has_xyzwykrywa, czy ustawiono opcjonalną wartość 2) clear_xyzusunie wartość. Więcej informacji tutaj: github.com/protocolbuffers/protobuf/blob/master/docs/…
Evan Moran
@JamesHancock czy Java?
Tobi Akinyemi
1
@ JónásBalázs Przekaż flagę --experimental_allow_proto3_optional do protokołu Protoc, aby korzystać z tej funkcji w wersji 3.12.
jaredjacobs
127

W proto3 wszystkie pola są „opcjonalne” (nie jest to błąd, jeśli nadawca ich nie ustawi). Ale pola nie są już „dopuszczalne do wartości null”, ponieważ nie ma sposobu, aby odróżnić pole, które jest jawnie ustawione na wartość domyślną, a tym, że w ogóle nie zostało ustawione.

Jeśli potrzebujesz stanu „null” (i nie ma wartości spoza zakresu, której możesz użyć do tego), zamiast tego musisz zakodować to jako oddzielne pole. Na przykład możesz zrobić:

message Foo {
  bool has_baz = 1;  // always set this to "true" when using baz
  int32 baz = 2;
}

Alternatywnie możesz użyć oneof:

message Foo {
  oneof baz {
    bool baz_null = 1;  // always set this to "true" when null
    int32 baz_value = 2;
  }
}

oneofWersja jest bardziej wyraźny i bardziej wydajny na drucie, ale wymaga zrozumienia, jak oneofdziała wartości.

Wreszcie inną całkowicie rozsądną opcją jest trzymanie się proto2. Proto2 nie jest przestarzałe, a w rzeczywistości wiele projektów (w tym wewnątrz Google) bardzo zależy od funkcji proto2, które zostały usunięte w proto3, dlatego prawdopodobnie nigdy się nie przełączą. Dlatego można bezpiecznie używać go w dającej się przewidzieć przyszłości.

Kenton Varda
źródło
Podobnie jak w twoim rozwiązaniu, w moim komentarzu zaproponowałem użycie oneof z wartością rzeczywistą i typem null (pusta wiadomość). W ten sposób nie zawracasz sobie głowy wartością logiczną (która nie powinna być istotna, ponieważ jeśli istnieje wartość logiczna, to nie ma wartości bazowej) Prawidłowo?
MaxP
2
@MaxP Twoje rozwiązanie działa, ale polecam wartość logiczną zamiast pustej wiadomości. Każda z nich zajmie dwa bajty na kablu, ale pusta wiadomość zajmie znacznie więcej procesora, pamięci RAM i wygenerowanego kodu.
Kenton Varda
13
Znajduję wiadomość Foo {oneof baz {int32 baz_value = 1; }} działa całkiem nieźle.
CyberSnoopy
@CyberSnoopy Czy możesz opublikować to jako odpowiedź? Twoje rozwiązanie działa idealnie i elegancko.
Cheng Chen
@CyberSnoopy Czy przypadkiem napotkałeś jakieś problemy podczas wysyłania wiadomości z odpowiedzią, która ma strukturę podobną do: message FooList {powtórzone Foo foos = 1; }? Twoje rozwiązanie jest świetne, ale mam teraz problem z wysłaniem FooList jako odpowiedzi serwera.
Kofeinat Często
102

Jednym ze sposobów jest optionalpolubienie opisane w zaakceptowanej odpowiedzi: https://stackoverflow.com/a/62566052/1803821

Innym jest użycie obiektów opakowujących. Nie musisz ich pisać samodzielnie, ponieważ Google już je udostępnia:

U góry pliku .proto dodaj ten import:

import "google/protobuf/wrappers.proto";

Teraz możesz użyć specjalnych opakowań dla każdego prostego typu:

DoubleValue
FloatValue
Int64Value
UInt64Value
Int32Value
UInt32Value
BoolValue
StringValue
BytesValue

Aby odpowiedzieć na pierwotne pytanie, użycie takiego opakowania mogłoby wyglądać tak:

message Foo {
    int32 bar = 1;
    google.protobuf.Int32Value baz = 2;
}

Teraz na przykład w Javie mogę robić takie rzeczy jak:

if(foo.hasBaz()) { ... }

VM4
źródło
3
Jak to działa? Kiedy baz=nulli kiedy baznie jest zdany, oba przypadki hasBaz()mówią false!
mayankcpdixit
1
Pomysł jest prosty: używasz obiektów opakowujących lub innymi słowy typów zdefiniowanych przez użytkownika. Tych obiektów opakowania może brakować. Podany przeze mnie przykład Java działał dobrze podczas pracy z gRPC.
VM4
Tak! Rozumiem ogólną ideę, ale chciałem zobaczyć to w akcji. To, czego nie rozumiem, to: (nawet w obiekcie otoki) „ Jak zidentyfikować brakujące i zerowe wartości opakowania?
mayankcpdixit
3
To jest odpowiednie rozwiązanie. W C # wygenerowany kod generuje właściwości Nullable <T>.
Aaron Hudon
6
Lepsze niż oryginalny awsner!
Dev Aggarwal
33

W oparciu o odpowiedź Kentona, prostsze, ale działające rozwiązanie wygląda następująco:

message Foo {
    oneof optional_baz { // "optional_" prefix here just serves as an indicator, not keyword in proto2
        int32 baz = 1;
    }
}
CyberSnoopy
źródło
jak to ucieleśnia opcjonalną postać?
JFFIGK
20
Zasadniczo oneof jest źle nazwany. To znaczy „co najwyżej jeden z”. Zawsze istnieje możliwa wartość zerowa.
ecl3ctic
Jeśli nie zostanie ustawiona, wielkość liter będzie None(w C #) - zobacz typ wyliczenia dla wybranego języka.
nitzel
Tak, to prawdopodobnie najlepszy sposób na oskórowanie tego kota w proto3 - nawet jeśli sprawia, że ​​.proto jest trochę brzydki.
jschultz410
Jednak w pewnym stopniu oznacza to, że brak pola można interpretować jako jawne ustawienie wartości null. Innymi słowy, istnieje pewna niejednoznaczność między „nie określono pola opcjonalnego” a „pole nie zostało określone celowo, co oznacza, że ​​jest puste”. Jeśli zależy Ci na tym poziomie dokładności, możesz dodać dodatkowe pole google.protobuf.NullValue do pola, które umożliwia rozróżnienie między „pole nieokreślone”, „pole określone jako wartość X” i „pole określone jako null” . To trochę dziwne, ale to dlatego, że proto3 nie obsługuje bezpośrednio wartości null, tak jak robi to JSON.
jschultz410
7

Aby rozwinąć sugestię @cybersnoopy tutaj

gdybyś miał plik .proto z taką wiadomością:

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

Możesz skorzystać z podanych opcji sprawy (kod wygenerowany w Javie) :

Więc możemy teraz napisać kod w następujący sposób:

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

if (optionNotSet.equals(optionCase)){
    // value not set
} else {
    // value set
}
Benjamin Slabbert
źródło
W Pythonie jest to jeszcze prostsze. Możesz po prostu zrobić request.HasField ("option_value"). Ponadto, jeśli masz w wiadomości kilka singletonów takich jak ten, możesz uzyskać bezpośredni dostęp do zawartych w nich skalarów, tak jak zwykły skalar.
jschultz410
1

Innym sposobem zakodowania zamierzonej wiadomości jest dodanie kolejnego pola do śledzenia pól „ustawionych”:

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;
}

Jest to szczególnie odpowiednie, jeśli istnieje duża liczba pól i tylko niewielka ich liczba została przypisana.

W Pythonie użycie wyglądałoby tak:

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)
user8819
źródło
-1

Innym sposobem jest użycie maski bitowej dla każdego pola opcjonalnego. i ustaw te bity, jeśli wartości są ustawione, i zresetuj te bity, których wartości nie są ustawione

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;
}

Podczas sprawdzania analizowania wartości parametru bitMask.

if (bitMask & baz_present)
    baz is present

if (bitMask & baz1_present)
    baz1 is present
ChauhanTs
źródło
-2

możesz sprawdzić, czy został zainicjowany, porównując odwołania z domyślną instancją:

GRPCContainer container = myGrpcResponseBean.getContainer();
if (container.getDefaultInstanceForType() != container) {
...
}
eduyayo
źródło
1
Nie jest to dobre podejście ogólne, ponieważ bardzo często wartość domyślna jest całkowicie akceptowalną wartością pola i w takiej sytuacji nie można rozróżnić między „brak pola” i „pole obecne, ale ustawione na wartość domyślną”.
jschultz410