วิธีกำหนดฟิลด์ทางเลือกใน protobuf 3


111

ฉันต้องการระบุข้อความด้วยฟิลด์ทางเลือกใน protobuf (ไวยากรณ์ของ proto3) ในแง่ของไวยากรณ์ของโปรโต 2 ข้อความที่ฉันต้องการแสดงออกมีดังนี้:

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

จากความเข้าใจของฉันแนวคิด "ทางเลือก" ถูกลบออกจากไวยากรณ์โปรโต 3 (พร้อมกับแนวคิดที่จำเป็น) แม้ว่าจะไม่ชัดเจนทางเลือกอื่น - การใช้ค่าเริ่มต้นเพื่อระบุว่าเขตข้อมูลไม่ได้ถูกระบุจากผู้ส่ง แต่จะทำให้เกิดความไม่ชัดเจนหากค่าเริ่มต้นเป็นของโดเมนค่าที่ถูกต้อง (พิจารณาตัวอย่างเช่นประเภทบูลีน)

ฉันจะเข้ารหัสข้อความด้านบนได้อย่างไร? ขอขอบคุณ.


แนวทางด้านล่างเป็นวิธีแก้ปัญหาเสียงหรือไม่? ข้อความ NoBaz {} ข้อความ Foo {int32 bar = 1; oneof baz {NoBaz undefined = 2; int32 กำหนด = 3; }; }
MaxP

2
มีโปรโต 2 รุ่นของคำถามนี้ถ้าคนอื่น ๆ พบนี้ แต่จะมีการใช้โปร 2
chwarr

1
โดยทั่วไปแล้ว proto3 จะทำให้ฟิลด์ทั้งหมดเป็นทางเลือก อย่างไรก็ตามสำหรับสเกลาร์พวกเขาทำให้ไม่สามารถแยกความแตกต่างระหว่าง "field not set" และ "field set แต่เป็นค่าเริ่มต้น" ถ้าคุณรวมสเกลาร์ของคุณเป็นซิงเกิลตันเช่น - ข้อความ blah {oneof v1 {int32 foo = 1; }} จากนั้นคุณสามารถตรวจสอบอีกครั้งว่า foo ถูกตั้งค่าจริงหรือไม่ อย่างน้อยที่สุดสำหรับ Python คุณสามารถทำงานบน foo ได้โดยตรงราวกับว่ามันไม่ได้อยู่ในหนึ่งในนั้นและคุณสามารถถาม HasField ("foo") ได้
jschultz410

1
@MaxP บางทีคุณอาจเปลี่ยนคำตอบที่ยอมรับเป็นstackoverflow.com/a/62566052/66465เนื่องจากรุ่นที่ใหม่กว่าของ protobuf 3 ตอนนี้มีoptional
SebastianK

คำตอบ:


55

ตั้งแต่ protobuf รีลีส 3.12เป็นต้นมา proto3 มีการสนับสนุนการทดลองสำหรับการใช้optionalคีย์เวิร์ด (เช่นเดียวกับในโปรโต 2) เพื่อให้ข้อมูลสถานะฟิลด์สเกลาร์

syntax = "proto3";

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

A has_baz()/ hasBaz()method ถูกสร้างขึ้นสำหรับoptionalฟิลด์ด้านบนเช่นเดียวกับใน proto2

ภายใต้ฝากระโปรง Protoc จะปฏิบัติต่อoptionalสนามอย่างมีประสิทธิภาพราวกับว่ามีการประกาศโดยใช้oneofกระดาษห่อหุ้มตามที่คำตอบของ CyberSnoopyแนะนำ:

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

หากคุณเคยใช้แนวทางดังกล่าวแล้วคุณจะสามารถล้างการประกาศข้อความของคุณ (เปลี่ยนจากoneofเป็นoptional) ได้เมื่อ proto3 optionalสนับสนุนผู้สำเร็จการศึกษาจากสถานะการทดลองเนื่องจากรูปแบบลวดจะเหมือนกัน

คุณสามารถดูรายละเอียดที่สำคัญเกี่ยวกับการมีอยู่ของฟิลด์และoptionalใน proto3 ได้ในบันทึกการใช้งาน:เอกสารการแสดงตนของฟิลด์

ส่ง--experimental_allow_proto3_optionalแฟล็กไปยัง protoc เพื่อใช้ฟังก์ชันนี้ในรีลีส 3.12 ประกาศคุณลักษณะบอกว่ามันจะเป็น“ใช้ได้โดยทั่วไปหวังว่าใน 3.13”

พฤศจิกายน 2020 ปรับปรุง: คุณลักษณะนี้ถือว่ายังคงทดลอง (ธงจำเป็น) ในการเปิดตัว 3.14 มีสัญญาณของความคืบหน้ากำลังทำ


3
คุณบังเอิญรู้วิธีส่งค่าสถานะใน C # หรือไม่?
James Hancock

นี่คือคำตอบที่ดีที่สุดในตอนนี้ที่ proto3 เพิ่มไวยากรณ์ที่ดีขึ้น คำบรรยายภาพยอดเยี่ยม Jarad!
Evan Moran

เพียงเพื่อเพิ่มสำหรับoptional int xyz: 1) has_xyzตรวจพบว่ามีการตั้งค่าทางเลือก 2) clear_xyzจะยกเลิกการตั้งค่า ข้อมูลเพิ่มเติมที่นี่: github.com/protocolbuffers/protobuf/blob/master/docs/…
Evan Moran

@JamesHancock หรือ Java?
Tobi Akinyemi

1
@ JónásBalázsส่งแฟล็ก --experimental_allow_proto3_optional ไปยัง protoc เพื่อใช้ฟังก์ชันนี้ในรีลีส 3.12
jaredjacobs

127

ใน proto3 ช่องทั้งหมดเป็น "ทางเลือก" (ซึ่งจะไม่ใช่ข้อผิดพลาดหากผู้ส่งตั้งค่าไม่สำเร็จ) แต่ช่องนั้นไม่ "เป็นโมฆะ" อีกต่อไปเนื่องจากไม่มีวิธีใดที่จะบอกความแตกต่างระหว่างฟิลด์ที่ตั้งค่าเป็นค่าเริ่มต้นอย่างชัดเจนกับไม่ได้ตั้งค่าเลย

หากคุณต้องการสถานะ "ว่าง" (และไม่มีค่านอกช่วงที่คุณสามารถใช้สำหรับสิ่งนี้ได้) คุณจะต้องเข้ารหัสเป็นฟิลด์แยกแทน ตัวอย่างเช่นคุณสามารถทำได้:

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ค่าการทำงาน

ในที่สุดอีกทางเลือกหนึ่งที่เหมาะสมอย่างยิ่งคือติดกับ proto2 Proto2 ไม่ได้เลิกใช้งานและในความเป็นจริงหลายโครงการ (รวมถึงใน Google) ขึ้นอยู่กับคุณสมบัติของโปรโต 2 ซึ่งถูกลบออกใน proto3 ดังนั้นจึงไม่มีทางเปลี่ยน ดังนั้นจึงปลอดภัยที่จะใช้ต่อไปในอนาคตอันใกล้


เช่นเดียวกับวิธีแก้ปัญหาของคุณในความคิดเห็นของฉันฉันเสนอให้ใช้หนึ่งในค่าจริงและประเภท null (ข้อความว่างเปล่า) วิธีนี้จะทำให้คุณไม่ต้องกังวลกับค่าบูลีน (ซึ่งไม่น่าจะเกี่ยวข้องเพราะถ้ามีบูลีนแสดงว่าไม่มี baz_value) ถูกต้อง?
MaxP

2
@MaxP โซลูชันของคุณใช้งานได้ แต่ฉันอยากจะแนะนำบูลีนบนข้อความว่างเปล่า อาจใช้เวลาสองไบต์บนสาย แต่ข้อความว่างจะใช้ CPU, RAM และโค้ดที่ขยายตัวมากขึ้นเพื่อจัดการ
Kenton Varda

13
ฉันพบข้อความ Foo {oneof baz {int32 baz_value = 1; }} ใช้งานได้ดี
CyberSnoopy

@CyberSnoopy โพสต์เป็นคำตอบได้ไหม โซลูชันของคุณทำงานได้อย่างสมบูรณ์แบบและสวยงาม
Cheng Chen

@CyberSnoopy คุณบังเอิญประสบปัญหาใด ๆ เมื่อส่งข้อความตอบกลับที่มีโครงสร้างเช่น: ข้อความ 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

ดังนั้นเพื่อตอบคำถามเดิมการใช้ Wrapper อาจเป็นดังนี้:

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

ตัวอย่างเช่นใน Java ฉันสามารถทำสิ่งต่างๆเช่น:

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


3
วิธีนี้ทำงานอย่างไร? เมื่อbaz=nullไหร่bazก็ไม่ผ่านทั้ง 2 กรณีhasBaz()บอกfalseเลย!
mayankcpdixit

1
แนวคิดง่ายๆคือคุณใช้วัตถุห่อหุ้มหรืออีกนัยหนึ่งที่ผู้ใช้กำหนดประเภท วัตถุห่อหุ้มเหล่านี้ได้รับอนุญาตให้ขาดหายไป ตัวอย่าง Java ที่ฉันให้ไว้ใช้ได้ดีสำหรับฉันเมื่อทำงานกับ gRPC
VM4

ใช่ ฉันเข้าใจแนวคิดทั่วไป แต่ฉันอยากเห็นมันเป็นจริง สิ่งที่ฉันไม่เข้าใจคือ: (แม้ในออบเจ็กต์ wrapper) " จะระบุค่าของ wrapper ที่ขาดหายไปและเป็นค่าว่างได้อย่างไร "
mayankcpdixit

3
นี่คือวิธีที่จะไป ด้วย C # โค้ดที่สร้างขึ้นจะสร้างคุณสมบัติ Nullable <T>
Aaron Hudon

6
ดีกว่า awsner เดิม!
Dev Aggarwal

33

จากคำตอบของ Kenton โซลูชันที่เรียบง่าย แต่ใช้งานได้มีลักษณะดังนี้:

message Foo {
    oneof optional_baz { // "optional_" prefix here just serves as an indicator, not keyword in proto2
        int32 baz = 1;
    }
}

สิ่งนี้รวบรวมอักขระเสริมได้อย่างไร?
JFFIGK

20
โดยทั่วไปแล้ว oneof มีชื่อไม่ดี หมายความว่า "มากที่สุดหนึ่งใน" มีค่าว่างที่เป็นไปได้เสมอ
ecl3ctic

หากปล่อยไว้โดยไม่ได้ตั้งค่าตัวพิมพ์เล็กและใหญ่จะเป็นNone(ใน C #) - ดูประเภท enum สำหรับภาษาที่คุณเลือก
nitzel

ใช่นี่อาจเป็นวิธีที่ดีที่สุดในการสกินแมวตัวนี้ใน proto3 แม้ว่ามันจะทำให้. proto น่าเกลียดไปหน่อยก็ตาม
jschultz410

อย่างไรก็ตามมันค่อนข้างบอกเป็นนัยว่าคุณอาจตีความการไม่มีฟิลด์เป็นการตั้งค่าให้เป็นค่า null อย่างชัดเจน กล่าวอีกนัยหนึ่งก็คือมีความคลุมเครือระหว่างฟิลด์ที่เป็นทางเลือกไม่ได้ระบุและฟิลด์ 'ไม่ได้ระบุโดยเจตนาที่จะหมายความว่าเป็นโมฆะ หากคุณสนใจระดับความแม่นยำนั้นคุณสามารถเพิ่มช่อง 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

มีโพสต์ดีๆเกี่ยวกับเรื่องนี้: https://itnext.io/protobuf-and-null-support-1908a15311b6

วิธีแก้ปัญหาขึ้นอยู่กับกรณีการใช้งานจริงของคุณ:


ขอบคุณสำหรับ LInk: itnext.io/protobuf-and-null-support-1908a15311b6มีประโยชน์จริงๆ
abhilash_goyal

1

อีกวิธีหนึ่งในการเข้ารหัสข้อความที่คุณต้องการคือการเพิ่มฟิลด์อื่นเพื่อติดตามฟิลด์ "set":

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

อีกวิธีหนึ่งคือคุณสามารถใช้ bitmask สำหรับแต่ละฟิลด์ที่เลือกได้ และตั้งค่าบิตเหล่านั้นหากมีการตั้งค่าและรีเซ็ตบิตที่ไม่ได้ตั้งค่า

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.