Union(联合体)

本文说明 Excel union 类型的各种特性。

原理

在 protoconf 中,union 类型是指带标签的联合体(tagged union):一种用于保存可以取多种不同但固定类型值的数据结构。任意时刻只有一种类型在使用,且一个 tag 字段明确指示当前使用的是哪种类型。更多详情请参考维基百科 Tagged union

Tagged union 在不同编程语言中的对应:

Tableau 使用 protobuf messageenum 类型和 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

基于 predefined union 类型 Target

HelloWorld.xlsx 中的 worksheet TaskConf

IDTarget1TypeTarget1Field1Target1Field2Target1Field3Target2TypeTarget2Field1Target2Field2Target2Field3
map<int32, Task>[.Target]enum<.Target.Type>unionunionunionenum<.Target.Type>unionunionunion
IDTarget1’s typeTarget1’s field1Target1’s field2Target1’s field3Target2’s typeTarget2’s field1Target2’s field2Target2’s field3
1PVP110Apple,Orange,BananaPVE1,100,9991,2,31:10,2:20,3:30
2Story1001,101:Apple,2:OrangeFragrant:1,Sour:2Skill12

生成结果:

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

基于 predefined union 类型 Target

HelloWorld.xlsx 中的 worksheet TaskConf

IDTargetTypeTargetField1TargetField2TargetField3Progress
map<int32, Task>{.Target}enum<.Target.Type>unionunionunionint32
IDTarget’s typeTarget’s field1Target’s field2Target’s field3Progress
1PVP110Apple,Orange,Banana3
2PVE1,100,9991,2,31:10,2:20,3:3010
3Story1001,101:Apple,2:OrangeFragrant:1,Sour:210
4Skill128

生成结果:

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

基于 predefined union 类型 Target

HelloWorld.xlsx 中的 worksheet TaskConf

IDTarget1Target2Progress
map<int32, Task>{.Target}|{form:FORM_TEXT}{.Target}|{form:FORM_JSON}int32
IDTarget1Target2Progress
1type: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
2type: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

NameAliasField1Field2Field3
PVPAliasPVPID
uint32
Note
Damage
int64
Note
Type
enum<.FruitType>
Note
PVEAliasPVEHero
[]uint32
Note
Dungeon
map<int32, int64>
Note
SkillAliasSkillStartTime
datetime
Note
Duration
duration
Note
SheetMode
TargetMODE_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

SheetMode
TargetMODE_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

NumberNameAliasField1Field2Field3
1PVPAliasPVPID
uint32
Note
Damage
int64
Note
Type
enum<.FruitType>
Note
20PVEAliasPVEHero
[]uint32
Note
Dungeon
map<int32, int64>
Note
30SkillAliasSkillStartTime
datetime
Note
Duration
duration
Note
SheetMode
TargetMODE_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

NameAliasTypeField1Field2#Note
FruitFruitenum<.FruitType>Bound to enum
PointPointint32Bound to scalar
ItemItem.ItemBound to global predefined struct
PlayerPlayerID
uint32
Name
string
Bound to local defined struct
FriendFriendPlayerBound to local predefined in the same level
MonsterMonsterCustomMonsterHealth
uint32
Attack
int32
Bound to local defined struct with custom type name
BossBossCustomMonsterBound to local predefined struct in the same level
SheetMode
TargetMODE_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 TargetTaskConf

NameAliasField1Field2Field3
PVPAliasPVPID
uint32
Note
Damage
int64
Note
Type
[]enum<.FruitType>
Note
PVEAliasPVEMission
{uint32 ID, enum<.ItemType> Type}Mission
Note
Hero
[]uint32
Note
Dungeon
map<int32, int64>
Note
StoryAliasStoryCost
{.Item}
Note
Fruit
map<int32, enum<.FruitType»
Note
Flavor
map<enum<.FruitFlavor>, enum<.FruitType»
Note
HobbyAliasHobbyFlavor
map<enum<.FruitFlavor>, enum<.FruitType»
Note
StartTime
datetime
Note
Duration
duration
Note
SkillAliasSkillID
uint32
Note
Damage
int64
Note
EmptyAliasEmpty
IDTargetTypeTargetField1TargetField2TargetField3Progress
map<int32, Task>{.Target}enum<.Target.Type>unionunionunionint32
IDTarget’s typeTarget’s field1Target’s field2Target’s field3Progress
1AliasPVP110Apple,Orange,Banana3
2AliasPVE1,Equip1,2,31:10,2:20,3:3010
3AliasStory1001,101:Apple,2:OrangeFragrant:Apple,Sour:Orange10
4AliasHobbyFragrant:Apple,Sour:Orange2023-06-01 10:00:0022s12
5AliasSkill12008
6AliasEmpty
SheetMode
TargetMODE_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"}];
  }
}