Cómo definir un campo opcional en protobuf 3

105

Necesito especificar un mensaje con un campo opcional en protobuf (sintaxis proto3). En términos de sintaxis proto 2, el mensaje que quiero expresar es algo como:

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

Según tengo entendido, el concepto "opcional" se ha eliminado de la sintaxis proto 3 (junto con el concepto obligatorio). Aunque no está clara la alternativa: usar el valor predeterminado para indicar que un campo no ha sido especificado por el remitente, deja una ambigüedad si el valor predeterminado pertenece al dominio de valores válidos (considere, por ejemplo, un tipo booleano).

Entonces, ¿cómo se supone que debo codificar el mensaje anterior? Gracias.

MaxP
fuente
¿Es el enfoque siguiente una solución sólida? mensaje NoBaz {} mensaje Foo {int32 bar = 1; oneof baz {NoBaz undefined = 2; int32 definido = 3; }; }
MaxP
2
Hay una versión de Proto 2 de esta pregunta , si otros encuentran esto pero están usando Proto 2.
chwarr
1
proto3 básicamente hace que todos los campos sean opcionales. Sin embargo, para los escalares, hicieron imposible distinguir entre "campo no establecido" y "campo establecido pero al valor predeterminado". Si envuelve su escalar en un singleton oneof eg - message blah {oneof v1 {int32 foo = 1; }}, luego puede verificar nuevamente si foo se configuró o no. Para Python al menos, puede operar directamente en foo como si no estuviera dentro de oneof y puede preguntar a HasField ("foo").
jschultz410
1
@MaxP Tal vez podría cambiar la respuesta aceptada a stackoverflow.com/a/62566052/66465 ya que ahora tiene una versión más nueva de protobuf 3optional
SebastianK

Respuestas:

40

Desde la versión 3.12 de protobuf, proto3 ha admitido el uso de la optionalpalabra clave (al igual que en proto2) para proporcionar información de presencia de campo escalar.

syntax = "proto3";

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

Se genera un método has_baz()/ hasBaz()para el optionalcampo anterior, tal como estaba en proto2.

Debajo del capó, el protocolo trata de manera efectiva un optionalcampo como si fuera declarado usando un oneofcontenedor, como sugiere la respuesta de CyberSnoopy :

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

Si ya ha utilizado ese enfoque, puede limpiar las declaraciones de sus mensajes (cambiar de oneofa optional), ya que el formato de cable es el mismo.

Puede encontrar los detalles esenciales sobre la presencia de campo y optionalen proto3 en la Nota de aplicación: documento de presencia de campo .

En la versión 3.12, esta funcionalidad requiere pasar la --experimental_allow_proto3_optionalbandera al protocolo. El anuncio de la función dice que estará "generalmente disponible con suerte en 3.13".

Actualización de octubre de 2020: la función todavía se considera experimental (se requiere marca) en la versión 3.13 .

jaredjacobs
fuente
1
¿Sabe cómo pasar la bandera en C #?
James Hancock
Esta es la mejor respuesta ahora que proto3 agregó una mejor sintaxis. ¡Gran anuncio, Jarad!
Evan Moran
Solo para agregar para optional int xyz: 1) has_xyzdetecta si el valor opcional se estableció 2) clear_xyzanulará el valor. Más información aquí: github.com/protocolbuffers/protobuf/blob/master/docs/…
Evan Moran
@JamesHancock o Java?
Tobi Akinyemi
@TobiAkinyemi ??
James Hancock
123

En proto3, todos los campos son "opcionales" (en el sentido de que no es un error si el remitente no los configura). Pero los campos ya no son "anulables", ya que no hay forma de diferenciar entre un campo que se establece explícitamente en su valor predeterminado y que no se establece en absoluto.

Si necesita un estado "nulo" (y no hay un valor fuera de rango que pueda usar para esto), entonces deberá codificarlo como un campo separado. Por ejemplo, podrías hacer:

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

Alternativamente, puede usar oneof:

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

La oneofversión es más explícita y más eficiente en el cable, pero requiere comprender cómo oneoffuncionan los valores.

Finalmente, otra opción perfectamente razonable es seguir con proto2. Proto2 no está obsoleto y, de hecho, muchos proyectos (incluso dentro de Google) dependen en gran medida de las características de proto2 que se eliminan en proto3, por lo que es probable que nunca cambien. Por lo tanto, es seguro seguir usándolo en el futuro previsible.

Kenton Varda
fuente
De manera similar a su solución, en mi comentario, propuse usar el oneof con el valor real y un tipo nulo (un mensaje vacío). De esta manera, no se molesta con el valor booleano (que no debería ser relevante, porque si existe el booleano, entonces no hay baz_value) ¿Correcto?
MaxP
2
@MaxP Su solución funciona, pero recomendaría un booleano sobre un mensaje vacío. Cualquiera de las dos ocupará dos bytes en el cable, pero el mensaje vacío requerirá considerablemente más CPU, RAM y código generado para manejarlo.
Kenton Varda
13
Encuentro el mensaje Foo {oneof baz {int32 baz_value = 1; }} funciona bastante bien.
CyberSnoopy
@CyberSnoopy ¿Puedes publicarlo como respuesta? Tu solución funciona perfecta y elegante.
Cheng Chen
@CyberSnoopy ¿Se ha encontrado por casualidad con algún problema al enviar un mensaje de respuesta que está estructurado de la siguiente manera: message FooList {repetido Foo foos = 1; }? Su solución es excelente, pero ahora tengo problemas para enviar FooList como respuesta del servidor.
CaffeinateOften
95

Una forma es usar oneofcomo se sugiere en la respuesta aceptada.

Otro es usar objetos envoltorios. No es necesario que los escriba usted mismo, ya que Google ya los proporciona:

En la parte superior de su archivo .proto agregue esta importación:

import "google/protobuf/wrappers.proto";

Ahora puede usar envoltorios especiales para cada tipo simple:

DoubleValue
FloatValue
Int64Value
UInt64Value
Int32Value
UInt32Value
BoolValue
StringValue
BytesValue

Entonces, para responder a la pregunta original, el uso de dicho contenedor podría ser así:

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

Ahora, por ejemplo, en Java puedo hacer cosas como:

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

VM4
fuente
3
¿Como funciona esto? Cuándo baz=nully cuándo bazno se pasa, ¡ambos casos hasBaz()dicen false!
mayankcpdixit
La idea es simple: utiliza objetos de envoltura o, en otras palabras, tipos definidos por el usuario. Se permite que falten estos objetos de envoltura. El ejemplo de Java que proporcioné me funcionó bien cuando trabajé con gRPC.
VM4
¡Si! Entiendo la idea general, pero quería verla en acción. Lo que no entiendo es: (incluso en el objeto contenedor) " ¿Cómo identificar valores de contenedor nulos y faltantes? "
mayankcpdixit
2
Este es el camino a seguir. Con C #, el código generado produce propiedades Nullable <T>.
Aaron Hudon
5
¡Mejor que el awsner original!
Dev Aggarwal
32

Según la respuesta de Kenton, una solución más simple pero funcional se ve así:

message Foo {
    oneof optional_baz { // "optional_" prefix here just serves as an indicator, not keyword in proto2
        int32 baz = 1;
    }
}
CyberSnoopy
fuente
¿Cómo encarna esto el carácter opcional?
JFFIGK
19
Básicamente, oneof tiene un nombre deficiente. Significa "como máximo uno de". Siempre hay un posible valor nulo.
ecl3ctic
Si no se establece, el valor será None(en C #); consulte el tipo de enumeración para el idioma que elija.
nitzel
Sí, esta es probablemente la mejor manera de despellejar a este gato en proto3, incluso si hace que el .proto sea un poco feo.
jschultz410
Sin embargo, de alguna manera implica que puede interpretar la ausencia de un campo como establecerlo explícitamente en el valor nulo. En otras palabras, existe cierta ambigüedad entre 'campo opcional no especificado' y 'campo no se especificó intencionalmente para significar que es nulo'. Si le importa ese nivel de precisión, puede agregar un campo adicional google.protobuf.NullValue al que le permite distinguir entre 'campo no especificado', 'campo especificado como valor X' y 'campo especificado como nulo' . Es un poco extraño, pero eso se debe a que proto3 no admite nulos directamente como lo hace JSON.
jschultz410
7

Para ampliar la sugerencia de @cybersnoopy aquí

si tenía un archivo .proto con un mensaje como este:

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

Puede hacer uso de las opciones de caso proporcionadas (código generado por Java) :

Entonces ahora podemos escribir un código de la siguiente manera:

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

if (optionNotSet.equals(optionCase)){
    // value not set
} else {
    // value set
}
Benjamin Slabbert
fuente
En Python es aún más sencillo. Puede simplemente hacer request.HasField ("option_value"). Además, si tiene un montón de singleton oneof como este dentro de su mensaje, entonces puede acceder a sus escalares contenidos directamente como un escalar normal.
jschultz410
-1

Otra forma es que puede usar máscara de bits para cada campo opcional. y establecer esos bits si se establecen valores y restablecer aquellos bits cuyos valores no se establecen

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

Al analizar, compruebe el valor de bitMask.

if (bitMask & baz_present)
    baz is present

if (bitMask & baz1_present)
    baz1 is present
ChauhanTs
fuente
-2

puede encontrar si uno se ha inicializado comparando las referencias con la instancia predeterminada:

GRPCContainer container = myGrpcResponseBean.getContainer();
if (container.getDefaultInstanceForType() != container) {
...
}
eduyayo
fuente
1
Este no es un buen enfoque general porque muy a menudo el valor predeterminado es un valor perfectamente aceptable para el campo y en esa situación no se puede distinguir entre "campo ausente" y "campo presente pero configurado como predeterminado".
jschultz410