Verse 效果系统全景:suspends 的坐标
suspends 不是孤零零的关键字,而是 Verse 效果系统里的一格。看清这张矩阵,你就能自己推导出一串「为什么」:为什么异步函数进不了失败上下文、为什么 suspends 与 decides 势不两立——而不必死记硬背。
一、效果:函数的「行为保证书」
第 18 课认识了说明符,第 19 课用上了 <suspends>。现在把镜头拉远:Verse 把「这个函数会对世界做什么」直接编码进类型系统,这类说明符叫函数效果(effects)。编译器像公证处一样核验每一张保证书——声明了什么效果,才允许做什么事;调用方所处的上下文不接受某效果,调用就无法通过编译。第 19 课那句经典报错「has the 'suspends' effect, which is not allowed by its context」,就是公证处在盖「拒绝」章。现在把镜头拉远:Verse 把「这个函数会对世界做什么」直接写进了类型信息里,这类说明符叫函数效果(effects)。Compile(蓝图左上角那个编译按钮)像公证处一样核验每一张保证书——声明了什么效果,才允许做什么事;如果调用它的那个地方不接受某种效果,一按 Compile 就亮红过不了。第 19 课那句经典报错「has the 'suspends' effect, which is not allowed by its context」,就是公证处在盖「拒绝」章。
官方文档《Specifiers and Attributes in Verse》把效果分成两族:
| 族 | 成员 | 规则 |
|---|---|---|
| 互斥效果(exclusive) | transacts、varies、computes、converges |
至多选一;都不写时默认 no_rollback |
| 可叠加效果(additive) | suspends、decides |
可与互斥效果组合使用 |
几个成员简介:transacts 表示函数体内的修改可以整体回滚——这是 Verse 失败语义的基石;varies、computes、converges 是对纯度与收敛性越来越严格的承诺,日常 UEFN 代码很少显式用到,细节以官方文档为准。最有趣的是那位「隐身的第五人」:什么互斥效果都不写时,函数默认拥有 no_rollback 效果——但这个默认效果无法显式书写,你在任何合法代码里都打不出它。suspends 表示函数可能跨模拟帧挂起,decides 表示函数可能失败(它让函数变成一个可以「掷骰失败」的表达式)。
几个成员简介:transacts 表示这个函数做的改动可以整体撤销回去——这是 Verse 失败语义的基石;varies、computes、converges 是对「纯不纯、会不会收敛」越来越严格的承诺,日常 UEFN 里很少显式用到,细节以官方文档为准。最有趣的是那位「隐身的第五人」:什么互斥效果都不写时,函数默认拥有 no_rollback 效果——但这个默认效果没法显式写出来,你在任何合法写法里都打不出它。suspends 表示函数可能跨模拟帧挂起(带小钟表),decides 表示函数可能失败——它让这个函数变成一个可以「掷骰失败」的判断(走不通就当没发生)。
二、用矩阵推导三个「为什么」
为什么 decides 目前必须搭配 transacts?Verse 对失败的承诺是「失败即如同什么都没发生」:一个 decides 函数半路失败时,它已经做过的修改必须全部撤销。谁提供撤销能力?transacts 的回滚机制。所以「可能失败」必须建立在「可以回滚」之上,两者是绑定出售的套餐。
为什么失败上下文不允许调用异步函数?失败上下文(if 的条件、decides 函数体)里的一切都要能回滚。而挂起意味着跨越模拟帧:真实时间流逝了、其他任务运行了、玩家开枪了——这些统统无法撤销。放一个 Sleep 进 if 条件,等于承诺「必要时把过去三秒退货」,编译器只能拒绝。
为什么「能不能走通」的判断里不允许放会等的节点?这类判断(比如 Branch 的条件、会掷骰失败的那种函数体)里的一切都要能撤销。而挂起意味着跨越模拟帧:真实时间流逝了、别的线跑过了、玩家开枪了——这些统统没法撤回。把一个 Sleep(Delay)塞进 Branch 的条件里,等于承诺「必要时把刚过去的三秒退货」,Compile 只能拒绝。
为什么 suspends 与 decides 不能同时标注?本质与上一条同源:decides 说「我可能失败,失败要回滚」,suspends 说「我会跨时间,跨过的时间没法回滚」——两张保证书互相矛盾,公证处不可能同时盖章。你可以写一个 <transacts><decides> 的函数,也可以写一个 <suspends> 的函数,但 <suspends><decides> 直接是编译错误。你可以给一个函数贴 <transacts><decides> 两张标签,也可以只贴 <suspends>,但 <suspends><decides> 一起贴,一按 Compile 就直接报错。
看,三条规则一条都不用背——全都是从「失败要能回滚,时间不能倒流」这一句话推出来的。这正是效果系统的价值:编译器在编译期就替你排除了整类「异步代码把失败语义搞坏」的 bug。Compile 在你按下编译的那一刻,就替你挡掉了整类「会等的节点把失败逻辑搞坏」的 bug。
三、为后面的章节埋一颗种子
本课程后面讲失败与回滚时,transacts 和 decides 会成为绝对主角:届时你会看到失败上下文如何撤销 set、为什么有些 API 的名字里带方括号(如 Items[0] 其实是个 decides 调用)。届时你会看到「走不通的判断」如何把已经跑过的 Set 撤销、为什么有些 API 名字里带方括号(比如 Items[0] 取数组元素,其实是个可能失败、走不通的操作)。现在只需带走一张地图:suspends 管时间,decides 管失败,transacts 管回滚;时间与失败不共戴天,失败与回滚焦不离孟。拿着它,再读官方 API 页上那些尖括号,你看到的就不再是咒语,而是一份份清清楚楚的行为保证书。
下面哪个效果是函数的默认互斥效果,却无法在代码里显式写出没法由你显式写出来?
为什么 <suspends> 与 <decides> 不能同时标注在一个函数上?
来源
本文整理自 Epic 官方文档: