执行线 vs 表达式:为什么 Verse 里没有「白线」
主课说「执行线在 Verse 里没有对应物」,那是从结构上说的。这一页从更深的地方回答同一个问题:蓝图用两种颜色的线把世界切成了「做事」和「算值」两半,而 Verse 干脆认为这两件事是同一件事。
一、蓝图把世界切成了两半
你可能从没这么想过,但蓝图图表其实是两张图叠在一起:
- 白线那张图:上面挂的是「做一件事」的节点——Set、Print String、Destroy Actor、Delay。它们有先后,必须排队,一个执行完才轮到下一个。
- 彩线那张图:上面挂的是「算一个值」的节点——Add、Get、Make Vector、Greater Than。它们没有先后,谁需要谁就去拉一次。
这条界线在编程语言里有个正式名字:语句(statement)与表达式(expression)。语句是「做一件事」,它没有值,你没法把一条语句塞进另一个东西的输入引脚;表达式是「算一个值」,它有值,可以被接到别处。C++、C#、Java 这些语言都保留了这条界线,蓝图只是把它画成了两种颜色的线而已。
这条界线带来一个非常具体的麻烦。设想一个再普通不过的需求:根据弹药有没有,给一个字符串变量填不同的内容。在蓝图里你只能这么干:先在变量面板新建一个 String 变量 Label,然后放一个 Branch,True 那条线上接一个 Set Label 节点填「有弹药」,False 那条线上接另一个 Set Label 节点填「空仓」,两条线汇合之后再 Get Label 用。三个节点、一个变量、四根线,只为了表达一个「二选一的值」。为什么这么费劲?因为 Branch 是语句——它挂在白线上,它没有值,你没法把 Branch 的输出直接接到别的输入引脚上。变量在这里纯粹是个中转站。
二、Verse 里一切都是表达式
Verse 把这条界线整个删掉了:每一段代码都是表达式,每个表达式都有值。if 有值、代码块有值、循环有值、函数调用当然也有值。有些表达式的值是 void(意思是「没有有用的值」),你忽略它就行——但它在语法上依然是个值,这就够了。
先看 if。同样那个「二选一的字符串」,在 Verse 里是一行:
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
turret_device := class(creative_device):
var Ammo:int = 1
OnBegin<override>()<suspends>:void =
# 整个 if 的值,直接落成一块名字牌
Label := if (Ammo > 0) { "有弹药" } else { "空仓" }
Print(Label)
数一数省掉了什么:那个 String 变量没了(Label 是个常量,连 var 都不用),两个 Set 节点没了,「两条线汇合」这件事也不用管了。if 走通就产出 "有弹药",走不通就产出 "空仓",整个 if 表达式的值就是产出的那个。
分支里要写好几行也没关系,换成缩进式排版即可,值仍然是每个块里最后一个表达式的值:
Label := if (Ammo > 0):
Print("检查弹匣")
"有弹药"
else:
Print("检查弹匣")
"空仓"
再看代码块。Verse 有一个 block 表达式,专门用来把几行圈成一块;按官方文档,除非中途提前退出,块的值就是块里最后一个被执行的表达式的值。于是「算一个中间结果」可以就地圈起来,不用把临时名字漏到外面:
Total := block:
Base := 10
Bonus := 20
Base + Bonus
# Base 和 Bonus 出了块就作废,外面只剩下 Total = 30
Print("{Total}")
这在蓝图里对应什么?对应「用一堆 Pure 节点算出一个值,再拉一根线出来」——只不过蓝图里那堆中间节点会永远散落在图上,谁都能再拉一根线走;block 则把它们关在门里,门一关只剩结果。
顺带一提,这也是 Verse 函数不需要 return 节点的原因:函数体本身就是一个块,块的值就是函数的返回值,所以函数体最后一个表达式的值自动成为返回值(叫「隐式返回」,第 19 课细讲)。蓝图里那个孤零零的 Return Node 在 Verse 里没有对应物——因为函数体已经自带值了。
三、这对蓝图作者意味着什么
第一,大量「中转变量」可以消失。回想你写过的蓝图:有多少变量的唯一用途,是把 Branch 或 ForEach 算出来的东西接到下游?这类变量在 Verse 里基本都能删掉,直接把表达式的值接过去。变量少一个,「谁在什么时候改了它」这个问题就少一份。这不只是省事:主课讲过 Verse 的默认是不可变,而中转变量恰恰是被迫要写 var 的那一类;删掉它们,你的代码里 var 会明显变少,而每一个剩下的 var 都真的代表「这里有会变的状态」。
第二,「做事」和「算值」可以混在一起写。这是把双刃剑。好处是 if (Item := Inventory[0]) { Print("{Item}") } 这种「一边判断一边取值」的写法极其顺手;坏处是嵌套一深就没法读了。所以 Verse 社区的排版惯例才格外重要——什么时候该拆开、什么时候可以串成一行,那是技巧页 x3 的话题。
第三,「这个东西有没有副作用」不再靠线的颜色区分。蓝图里你一眼能看出某个节点挂没挂在白线上,也就一眼知道它会不会改状态。Verse 里所有东西长得都一样,那这个信息去哪了?——被搬进了函数签名的效果说明符,而且由编译器强制检查,比看线颜色更靠谱。那是拓展页 x2 的话题。
最后回答标题里那个问题:Verse 里没有「白线」,不是因为 Epic 忘了画,而是因为白线所区分的那两类东西,在 Verse 里根本就是同一类。一张图上只剩一种线的时候,那种线也就不必画了。
四、小测验
在 Verse 里,if 是什么?
来源
本文整理自 Epic 官方文档:block expression(官方文档)↗ · Verse Language Quick Reference(官方文档)↗ · Book of Verse(官方语言手册)↗