Verse Wiki — 写给 UE6 时代的 Verse 互动手册
拓展 · EXTRA

open 与 closed enum:发布之后还能不能改

第 7 课定义的 enum 都是「默认款」。官方文档用了相当的篇幅回答另一个问题:岛屿发布之后,enum 还能不能加成员?答案写进了类型系统答案由枚举本身的类型规则替你管住——这一页讲 closed 与 open 两种约定,以及它们各自的代价。

一、为什么「发布之后」是个大问题

写单机小 demo 时,enum 想怎么改就怎么改——反正代码和数据都在你手里。可一张长期运营的地图不一样:v1 版本发布后,玩家的存档里可能已经躺着你的 enum 值;v2 版本的新代码和 v1 版本的旧数据要同台演出。这时「给 enum 加一个成员」就不再是随手的小事,而是一次版本兼容性事件:旧代码写的 case 分支,遇到新成员怎么办?老版本里那些按条目分流的 case,遇到一个它从没见过的新条目,该怎么办?

大多数语言把这个问题留给运行时炸给你看。Verse 的做法很有性格:在 enum 定义处就让你表态——这个枚举将来是「定死」还是「留口子」,然后用编译器把承诺执行到底。

很多做法是等游戏跑起来、真撞上了才崩给你看。Verse 的做法很有性格:在建这个枚举的时候就让你先表态——将来是「定死」还是「留口子」,然后靠 Compile 阶段的检查把这个承诺盯到底。

二、closed:定死,换一张穷尽匹配的安全网

enum 默认是 closed(封闭)的:成员集合一旦随岛屿发布就固定下来,后续版本不能再增删。听起来是限制,其实是买了一份大礼——穷尽匹配。既然编译器确切知道成员只有这几个,case 只要写全所有成员,就可以不写兜底分支;漏了哪个,编译不过:

enum 默认是 closed(封闭)的:条目清单一旦随岛屿发布就定死,后续版本不能再增删。听起来是限制,其实是白拿一份大礼——穷尽匹配。既然引擎确切知道就这么几个条目,case 只要把它们全写到,就可以不留兜底出口(Switch on Enum 的 Default 引脚);漏了哪个,Compile 直接不过:

chest_tier.verse
# 默认 closed:成员集合定死,case 可以穷尽匹配
chest_tier := enum{Common, Rare, Epic}

TierLabel(Tier:chest_tier)<computes>:string =
    case (Tier):
        chest_tier.Common => "普通"
        chest_tier.Rare => "稀有"
        chest_tier.Epic => "史诗"

▸ 读图翻译:一个默认 closed 的 chest_tier 枚举(Common / Rare / Epic),外加一个蓝图函数 TierLabel,用 case 把三个条目分别翻成「普通 / 稀有 / 史诗」。因为条目定死,这个 case 把三项写全就够了,不用留 Default 出口。

这张安全网的价值在维护期兑现:开发期你给 chest_tier 加了 Legendary,全项目里每一个漏改的 case 都会被编译器点名。它的反面是——一旦你给 case 加了兜底分支,新成员就会静悄悄地滑进兜底,安全网自动失效。所以对 closed enum 的风格建议是:能穷尽就穷尽,兜底分支慎写

这张安全网在维护期才真正兑现:开发期你给 chest_tier 加了 Legendary,全项目里每一个忘了更新的 case 都会被 Compile 点名。它的反面是——你要是给 case 补了个兜底出口(Default),新条目就会悄悄溜进兜底,安全网当场失效。所以对 closed 枚举的建议是:能写全就写全,Default 出口慎留

三、open:留口子,用纪律换灵活

如果你确定这个枚举将来一定会长——比如伤害类型,这个赛季只有火与冰,下个赛季八成要加毒——可以在定义时用 <open> 说明符把口子留好可以在定义时给它加上 <open> 这个标记,把口子留好:

damage_type.verse
# open:发布后允许在后续版本新增成员
damage_type := enum<open>{Fire, Ice}

▸ 读图翻译:跟建普通枚举一样,只是名字后面多挂了 <open> 这个标记,等于告诉引擎「这份清单以后还会加条目」。

代价立刻显现:对 open enum 的每一个 case,编译器都强制要求带默认(兜底)分支——因为旧版本的代码完全可能遇到新版本才出生的成员,必须有人负责接住它(兜底分支的具体写法以 官方 enum 文档 ↗ 为准)。也就是说,open 用「每处 case 多写一支」的纪律,换来了「发布后还能扩张」的自由;closed 反之。两种约定没有优劣,只有适不适合你的运营计划。

代价立刻显现:对 open 枚举的每一个 case,Compile 都强制你留一个默认(兜底)出口——因为老版本的代码完全可能撞上一个新版本才加进来的条目,总得有人接住它(兜底出口的具体写法以 官方 enum 文档 ↗ 为准)。也就是说,open 用「每处 case 都多留一手」的纪律,换来「发布后还能扩张」的自由;closed 反过来。两种约定没有优劣,只有适不适合你的运营计划。

拼图的最后一块是 <persistable>:带这个说明符定义的 enum 才能存进持久化数据(以模块级 weak_map 形式跨对局保存的玩家存档)。三者合起来看,你会发现一件在别的引擎里很少见的事——版本兼容性被写进了类型系统:什么能改、改了谁负责、旧数据还能不能读,全部在编译期说清楚。对打算把地图运营成「服务」而不是「作品」的创作者,这是 Verse 给出的最实在的工程承诺之一。

拼图的最后一块是 <persistable>:只有带上这个标记定义的 enum,才能被存进玩家存档(就是那种退出重进还在、每人一份的 SaveGame 数据)。三者合起来看,你会发现一件在别的引擎里很少见的事——「发布后什么能改」这件事被直接写进了枚举的类型规则:什么能改、改了谁负责、旧存档还读不读得动,全部在 Compile 阶段就说清楚。对打算把地图当成「长期服务」而不是「一锤子作品」来运营的创作者,这是 Verse 给出的最实在的工程承诺之一。

默认(closed)enum 换来的最大好处是什么?

来源

本文整理自 Epic 官方文档:Enum in Verse — Epic Games 官方文档 ↗