Union(联合体)
本文说明 Excel union 类型的各种特性。
原理
在 protoconf 中,union 类型是指带标签的联合体(tagged union):一种用于保存可以取多种不同但固定类型值的数据结构。任意时刻只有一种类型在使用,且一个 tag 字段明确指示当前使用的是哪种类型。更多详情请参考维基百科 Tagged union。
Tagged union 在不同编程语言中的对应:
- C++: std::variant
- Rust: Defining an Enum
Tableau 使用 protobuf message 将 enum 类型和 oneof 类型绑定在一起,以实现 tagged union。默认情况下,每个枚举值(>0)与 oneof 类型中具有相同 tag 编号的字段绑定。
Union 定义
例如,common.proto 中预定义的 union 类型 Target:
// Predefined union type.
message Target {
option (tableau.union) = {name:"Target"};
Type type = 9999 [(tableau.field) = { name: "Type" }];
oneof value {
option (tableau.oneof) = {
field: "Field"
};
Pvp pvp = 1; // Bound to enum value 1: TYPE_PVP.
Pve pve = 2; // Bound to enum value 2: TYPE_PVP.
Story story = 3; // Bound to enum value 3: TYPE_STORY.
Skill skill = 4; // Bound to enum value 4: TYPE_SKILL.
}
enum Type {
TYPE_NIL = 0;
TYPE_PVP = 1 [(tableau.evalue) = { name: "PVP" }];
TYPE_PVE = 2 [(tableau.evalue) = { name: "PVE" }];
TYPE_STORY = 3 [(tableau.evalue) = { name: "Story" }];
TYPE_SKILL = 4 [(tableau.evalue) = { name: "Skill" }];
}
message Pvp {
int32 type = 1; // scalar
int64 damage = 2; // scalar
repeated protoconf.FruitType types = 3; // incell enum list
}
message Pve {
Mission mission = 1; // incell struct
repeated int32 heros = 2; // incell list
map<int32, int64> dungeons = 3; // incell map
message Mission {
int32 id = 1;
uint32 level = 2;
int64 damage = 3;
}
}
message Story {
protoconf.Item cost = 1; // incell predefined struct
map<int32, protoconf.FruitType> fruits = 2; // incell map with value as enum type
map<int32, Flavor> flavors = 3; // incell map with key as enum type
message Flavor {
protoconf.FruitFlavor key = 1 [(tableau.field) = { name: "Key" }];
int32 value = 2 [(tableau.field) = { name: "Value" }];
}
}
message Skill {
int32 id = 1; // scalar
int64 damage = 2; // scalar
// no field tag 3
}
}
List 中的 predefined union
HelloWorld.xlsx 中的 worksheet TaskConf:
| ID | Target1Type | Target1Field1 | Target1Field2 | Target1Field3 | Target2Type | Target2Field1 | Target2Field2 | Target2Field3 |
|---|---|---|---|---|---|---|---|---|
| map<int32, Task> | [.Target]enum<.Target.Type> | union | union | union | enum<.Target.Type> | union | union | union |
| ID | Target1’s type | Target1’s field1 | Target1’s field2 | Target1’s field3 | Target2’s type | Target2’s field1 | Target2’s field2 | Target2’s field3 |
| 1 | PVP | 1 | 10 | Apple,Orange,Banana | PVE | 1,100,999 | 1,2,3 | 1:10,2:20,3:30 |
| 2 | Story | 1001,10 | 1:Apple,2:Orange | Fragrant:1,Sour:2 | Skill | 1 | 2 |
生成结果:
hello_world.proto
// --snip--
import "common.proto";
option (tableau.workbook) = {name:"HelloWorld.xlsx" namerow:1 typerow:2 noterow:3 datarow:4};
message TaskConf {
option (tableau.worksheet) = {name:"TaskConf"};
map<int32, Task> task_map = 1 [(tableau.field) = {key:"ID" layout:LAYOUT_VERTICAL}];
message Task {
int32 id = 1 [(tableau.field) = {name:"ID"}];
repeated protoconf.Target target_list = 2 [(tableau.field) = {name:"Target" layout:LAYOUT_HORIZONTAL}];
}
}
TaskConf.json
{
"taskMap": {
"1": {
"id": 1,
"targetList": [
{
"type": "TYPE_PVP",
"pvp": {"type": 1, "damage": "10", "types": ["FRUIT_TYPE_APPLE", "FRUIT_TYPE_ORANGE", "FRUIT_TYPE_BANANA"]}
},
{
"type": "TYPE_PVE",
"pve": {"mission": {"id": 1, "level": 100, "damage": "999"}, "heros": [1, 2, 3], "dungeons": {"1": "10", "2": "20", "3": "30"}}
}
]
},
"2": {
"id": 2,
"targetList": [
{
"type": "TYPE_STORY",
"story": {"cost": {"id": 1001, "num": 10}, "fruits": {"1": "FRUIT_TYPE_APPLE", "2": "FRUIT_TYPE_ORANGE"}, "flavors": {"1": {"key": "FRUIT_FLAVOR_FRAGRANT", "value": 1}, "2": {"key": "FRUIT_FLAVOR_SOUR", "value": 2}}}
},
{
"type": "TYPE_SKILL",
"skill": {"id": 1, "damage": "2"}
}
]
}
}
}
Map 中的 predefined union
HelloWorld.xlsx 中的 worksheet TaskConf:
| ID | TargetType | TargetField1 | TargetField2 | TargetField3 | Progress |
|---|---|---|---|---|---|
| map<int32, Task> | {.Target}enum<.Target.Type> | union | union | union | int32 |
| ID | Target’s type | Target’s field1 | Target’s field2 | Target’s field3 | Progress |
| 1 | PVP | 1 | 10 | Apple,Orange,Banana | 3 |
| 2 | PVE | 1,100,999 | 1,2,3 | 1:10,2:20,3:30 | 10 |
| 3 | Story | 1001,10 | 1:Apple,2:Orange | Fragrant:1,Sour:2 | 10 |
| 4 | Skill | 1 | 2 | 8 |
生成结果:
hello_world.proto
// --snip--
import "common.proto";
option (tableau.workbook) = {name:"HelloWorld.xlsx" namerow:1 typerow:2 noterow:3 datarow:4};
message TaskConf {
option (tableau.worksheet) = {name:"TaskConf"};
map<int32, Task> task_map = 1 [(tableau.field) = {key:"ID" layout:LAYOUT_VERTICAL}];
message Task {
int32 id = 1 [(tableau.field) = {name:"ID"}];
protoconf.Target target = 2 [(tableau.field) = {name:"Target"}];
int32 progress = 3 [(tableau.field) = {name:"Progress"}];
}
}
TaskConf.json
{
"taskMap": {
"1": {"id": 1, "target": {"type": "TYPE_PVP", "pvp": {"type": 1, "damage": "10", "types": ["FRUIT_TYPE_APPLE", "FRUIT_TYPE_ORANGE", "FRUIT_TYPE_BANANA"]}}, "progress": 3},
"2": {"id": 2, "target": {"type": "TYPE_PVE", "pve": {"mission": {"id": 1, "level": 100, "damage": "999"}, "heros": [1, 2, 3], "dungeons": {"1": "10", "2": "20", "3": "30"}}}, "progress": 10},
"3": {"id": 3, "target": {"type": "TYPE_STORY", "story": {"cost": {"id": 1001, "num": 10}, "fruits": {"1": "FRUIT_TYPE_APPLE", "2": "FRUIT_TYPE_ORANGE"}, "flavors": {"1": {"key": "FRUIT_FLAVOR_FRAGRANT", "value": 1}, "2": {"key": "FRUIT_FLAVOR_SOUR", "value": 2}}}}, "progress": 10},
"4": {"id": 4, "target": {"type": "TYPE_SKILL", "skill": {"id": 1, "damage": "2"}}, "progress": 8}
}
}
Map 中的 predefined incell union
HelloWorld.xlsx 中的 worksheet TaskConf:
| ID | Target1 | Target2 | Progress |
|---|---|---|---|
| map<int32, Task> | {.Target}|{form:FORM_TEXT} | {.Target}|{form:FORM_JSON} | int32 |
| ID | Target1 | Target2 | Progress |
| 1 | type:TYPE_PVP pvp:{type:1 damage:10 types:FRUIT_TYPE_APPLE types:FRUIT_TYPE_ORANGE types:FRUIT_TYPE_BANANA} | {“type”:“TYPE_PVP”,“pvp”:{“type”:1,“damage”:“10”,“types”:[“FRUIT_TYPE_APPLE”,“FRUIT_TYPE_ORANGE”,“FRUIT_TYPE_BANANA”]}} | 3 |
| 2 | type:TYPE_PVE pve:{mission:{id:1 level:100 damage:999} heros:1 heros:2 heros:3 dungeons:{key:1 value:10} dungeons:{key:2 value:20} dungeons:{key:3 value:30}} | {“type”:“TYPE_PVE”,“pve”:{“mission”:{“id”:1,“level”:100,“damage”:“999”},“heros”:[1,2,3],“dungeons”:{“1”:“10”,“2”:“20”,“3”:“30”}}} | 10 |
生成结果:
hello_world.proto
// --snip--
option (tableau.workbook) = {name:"HelloWorld.xlsx" namerow:1 typerow:2 noterow:3 datarow:4};
message TaskConf {
option (tableau.worksheet) = {name:"TaskConf"};
map<int32, Task> task_map = 1 [(tableau.field) = {key:"ID" layout:LAYOUT_VERTICAL}];
message Task {
int32 id = 1 [(tableau.field) = {name:"ID"}];
protoconf.Target target_1 = 2 [(tableau.field) = {name:"Target1" span:SPAN_INNER_CELL prop:{form:FORM_TEXT}}];
protoconf.Target target_2 = 3 [(tableau.field) = {name:"Target2" span:SPAN_INNER_CELL prop:{form:FORM_JSON}}];
int32 progress = 4 [(tableau.field) = {name:"Progress"}];
}
}
在 sheet 中定义 union 类型
在 metasheet @TABLEAU 中有两种 Mode 可用于在 sheet 中定义 union 类型:
MODE_UNION_TYPE:在一个 sheet 中定义单个 union 类型。MODE_UNION_TYPE_MULTI:在一个 sheet 中定义多个 union 类型。
每个 union 字段可以使用以下类型定义:
单个 union 类型
需要在 metasheet @TABLEAU 中将 Mode 选项设置为 MODE_UNION_TYPE。
例如,HelloWorld.xlsx 中的 worksheet Target:
| Name | Alias | Field1 | Field2 | Field3 |
|---|---|---|---|---|
| PVP | AliasPVP | ID uint32 Note | Damage int64 Note | Type enum<.FruitType> Note |
| PVE | AliasPVE | Hero []uint32 Note | Dungeon map<int32, int64> Note | |
| Skill | AliasSkill | StartTime datetime Note | Duration duration Note |
| Sheet | Mode |
|---|---|
| Target | MODE_UNION_TYPE |
生成结果:
hello_world.proto
// --snip--
option (tableau.workbook) = {name:"HelloWorld.xlsx"};
// Generated from sheet: Target.
message Target {
option (tableau.union) = {name:"Target"};
Type type = 9999 [(tableau.field) = { name: "Type" }];
oneof value {
option (tableau.oneof) = {field: "Field"};
PVP pvp = 1; // Bound to enum value: TYPE_PVP.
PVE pve = 2; // Bound to enum value: TYPE_PVE.
Skill skill = 3; // Bound to enum value: TYPE_SKILL.
}
enum Type {
TYPE_INVALID = 0;
TYPE_PVP = 1 [(tableau.evalue).name = "AliasPVP"];
TYPE_PVE = 2 [(tableau.evalue).name = "AliasPVE"];
TYPE_SKILL = 3 [(tableau.evalue).name = "AliasSkill"];
}
message PVP {
uint32 id = 1 [(tableau.field) = {name:"ID"}];
int64 damage = 2 [(tableau.field) = {name:"Damage"}];
protoconf.FruitType type = 3 [(tableau.field) = {name:"Type"}];
}
message PVE {
repeated uint32 hero_list = 1 [(tableau.field) = {name:"Hero" layout:LAYOUT_INCELL}];
map<int32, int64> dungeon_map = 2 [(tableau.field) = {name:"Dungeon" layout:LAYOUT_INCELL}];
}
message Skill {
google.protobuf.Timestamp start_time = 1 [(tableau.field) = {name:"StartTime"}];
google.protobuf.Duration duration = 2 [(tableau.field) = {name:"Duration"}];
}
}
多个 union 类型
需要在 metasheet @TABLEAU 中将 Mode 选项设置为 MODE_UNION_TYPE_MULTI。
例如,HelloWorld.xlsx 中的 worksheet Target:
| Sheet | Mode |
|---|---|
| Target | MODE_UNION_TYPE_MULTI |
生成结果:
hello_world.proto
// --snip--
option (tableau.workbook) = {name:"HelloWorld.xlsx"};
message WishTarget {
option (tableau.union) = {name:"UnionType" note:"WishTarget note"};
// ...
}
message HeroTarget {
option (tableau.union) = {name:"UnionType" note:"HeroTarget note"};
// ...
}
message BattleTarget {
option (tableau.union) = {name:"UnionType" note:"BattleTarget note"};
// ...
}
指定 Number 列
在 Number 列中,可以指定自定义的唯一字段编号和对应的枚举值编号。
例如,HelloWorld.xlsx 中的 worksheet Target:
| Number | Name | Alias | Field1 | Field2 | Field3 |
|---|---|---|---|---|---|
| 1 | PVP | AliasPVP | ID uint32 Note | Damage int64 Note | Type enum<.FruitType> Note |
| 20 | PVE | AliasPVE | Hero []uint32 Note | Dungeon map<int32, int64> Note | |
| 30 | Skill | AliasSkill | StartTime datetime Note | Duration duration Note |
| Sheet | Mode |
|---|---|
| Target | MODE_UNION_TYPE |
生成结果:
hello_world.proto
// --snip--
option (tableau.workbook) = {name:"HelloWorld.xlsx"};
// Generated from sheet: Target.
message Target {
option (tableau.union) = {name:"Target"};
Type type = 9999 [(tableau.field) = { name: "Type" }];
oneof value {
option (tableau.oneof) = {field: "Field"};
PVP pvp = 1; // Bound to enum value: TYPE_PVP.
PVE pve = 20; // Bound to enum value: TYPE_PVE.
Skill skill = 30; // Bound to enum value: TYPE_SKILL.
}
enum Type {
TYPE_INVALID = 0;
TYPE_PVP = 1 [(tableau.evalue).name = "AliasPVP"];
TYPE_PVE = 20 [(tableau.evalue).name = "AliasPVE"];
TYPE_SKILL = 30 [(tableau.evalue).name = "AliasSkill"];
}
// ...
}
指定 Type 列
默认情况下,每个 union 的 oneof 字段是一个以 Name 列指定名称的 message 类型。
现在,你可以添加 Type 列并指定自定义的 oneof 字段类型:
- scalar
- enum
- 全局 predefined struct
- 自定义命名 struct
- 同级的本地 predefined struct
例如,HelloWorld.xlsx 中的 worksheet Target:
| Name | Alias | Type | Field1 | Field2 | #Note |
|---|---|---|---|---|---|
| Fruit | Fruit | enum<.FruitType> | Bound to enum | ||
| Point | Point | int32 | Bound to scalar | ||
| Item | Item | .Item | Bound to global predefined struct | ||
| Player | Player | ID uint32 | Name string | Bound to local defined struct | |
| Friend | Friend | Player | Bound to local predefined in the same level | ||
| Monster | Monster | CustomMonster | Health uint32 | Attack int32 | Bound to local defined struct with custom type name |
| Boss | Boss | CustomMonster | Bound to local predefined struct in the same level |
| Sheet | Mode |
|---|---|
| Target | MODE_UNION_TYPE |
生成结果:
hello_world.proto
// --snip--
option (tableau.workbook) = {name:"HelloWorld.xlsx"};
// Generated from sheet: Target.
message Target {
option (tableau.union) = {name:"Target"};
Type type = 9999 [(tableau.field) = {name:"Type"}];
oneof value {
option (tableau.oneof) = {field:"Field"};
protoconf.FruitType fruit = 1; // Bound to enum value: TYPE_FRUIT.
int32 point = 2; // Bound to enum value: TYPE_POINT.
protoconf.Item item = 3; // Bound to enum value: TYPE_ITEM.
Player player = 4; // Bound to enum value: TYPE_PLAYER.
Player friend = 5; // Bound to enum value: TYPE_FRIEND.
CustomMonster monster = 6; // Bound to enum value: TYPE_MONSTER.
CustomMonster boss = 7; // Bound to enum value: TYPE_BOSS.
}
enum Type {
TYPE_INVALID = 0;
TYPE_FRUIT = 1 [(tableau.evalue).name = "Fruit"];
TYPE_POINT = 2 [(tableau.evalue).name = "Point"];
TYPE_ITEM = 3 [(tableau.evalue).name = "Item"];
TYPE_PLAYER = 4 [(tableau.evalue).name = "Player"];
TYPE_FRIEND = 5 [(tableau.evalue).name = "Friend"];
TYPE_MONSTER = 6 [(tableau.evalue).name = "Monster"];
TYPE_BOSS = 7 [(tableau.evalue).name = "Boss"];
}
message Player {
uint32 id = 1 [(tableau.field) = {name:"ID"}];
string name = 2 [(tableau.field) = {name:"Name"}];
}
message CustomMonster {
int32 health = 1 [(tableau.field) = {name:"Health"}];
int32 attack = 2 [(tableau.field) = {name:"Attack"}];
}
}
复杂 union 类型
例如,HelloWorld.xlsx 中的两个 worksheet Target 和 TaskConf:
| Name | Alias | Field1 | Field2 | Field3 |
|---|---|---|---|---|
| PVP | AliasPVP | ID uint32 Note | Damage int64 Note | Type []enum<.FruitType> Note |
| PVE | AliasPVE | Mission {uint32 ID, enum<.ItemType> Type}Mission Note | Hero []uint32 Note | Dungeon map<int32, int64> Note |
| Story | AliasStory | Cost {.Item} Note | Fruit map<int32, enum<.FruitType» Note | Flavor map<enum<.FruitFlavor>, enum<.FruitType» Note |
| Hobby | AliasHobby | Flavor map<enum<.FruitFlavor>, enum<.FruitType» Note | StartTime datetime Note | Duration duration Note |
| Skill | AliasSkill | ID uint32 Note | Damage int64 Note | |
| Empty | AliasEmpty |
| ID | TargetType | TargetField1 | TargetField2 | TargetField3 | Progress |
|---|---|---|---|---|---|
| map<int32, Task> | {.Target}enum<.Target.Type> | union | union | union | int32 |
| ID | Target’s type | Target’s field1 | Target’s field2 | Target’s field3 | Progress |
| 1 | AliasPVP | 1 | 10 | Apple,Orange,Banana | 3 |
| 2 | AliasPVE | 1,Equip | 1,2,3 | 1:10,2:20,3:30 | 10 |
| 3 | AliasStory | 1001,10 | 1:Apple,2:Orange | Fragrant:Apple,Sour:Orange | 10 |
| 4 | AliasHobby | Fragrant:Apple,Sour:Orange | 2023-06-01 10:00:00 | 22s | 12 |
| 5 | AliasSkill | 1 | 200 | 8 | |
| 6 | AliasEmpty |
| Sheet | Mode |
|---|---|
| Target | MODE_UNION_TYPE |
| Task |
生成结果:
hello_world.proto
// --snip--
option (tableau.workbook) = {name:"HelloWorld.xlsx" namerow:1 typerow:2 noterow:3 datarow:4};
// Generated from sheet: Target.
message Target {
option (tableau.union) = {name:"Target"};
Type type = 9999 [(tableau.field) = { name: "Type" }];
oneof value {
option (tableau.oneof) = {field: "Field"};
PVP pvp = 1;
PVE pve = 2;
Story story = 3;
Hobby hobby = 4;
Skill skill = 5;
Empty empty = 6;
}
enum Type {
TYPE_INVALID = 0;
TYPE_PVP = 1 [(tableau.evalue).name = "AliasPVP"];
TYPE_PVE = 2 [(tableau.evalue).name = "AliasPVE"];
TYPE_STORY = 3 [(tableau.evalue).name = "AliasStory"];
TYPE_HOBBY = 4 [(tableau.evalue).name = "AliasHobby"];
TYPE_SKILL = 5 [(tableau.evalue).name = "AliasSkill"];
TYPE_EMPTY = 6 [(tableau.evalue).name = "AliasEmpty"];
}
// ... message definitions omitted for brevity
}
message TaskConf {
option (tableau.worksheet) = {name:"TaskConf"};
map<int32, Task> task_map = 1 [(tableau.field) = {key:"ID" layout:LAYOUT_VERTICAL}];
message Task {
int32 id = 1 [(tableau.field) = {name:"ID"}];
protoconf.Target target = 2 [(tableau.field) = {name:"Target"}];
int32 progress = 3 [(tableau.field) = {name:"Progress"}];
}
}