Verse Wiki — 写给蓝图作者的 Verse 手册
拓展 · EXTRA

for 即列表推导:一行完成 map + filter

别的工具要「先筛一遍、再逐个改一遍」两步接力才能干完的活,Verse 一个 for 就顺手收工,还自动把结果攒成新数组。这一页把「供货、筛选、加工」三件套拆开看清楚,再和「手动开个数组、循环里一个个 Add」的老实写法打一场对照赛。

一、三件套:生成器供货、过滤器把关、主体加工

官方文档对 for 有一句关键定性:「循环求值为值的数组,因此可以基于现有数组快速创建新数组」。用大白话说:for 一边遍历一边把每轮的产出收集起来,跑完直接给你一个新数组——就像给蓝图 ForEach 自动接上了「每轮 Add 到一个新 Array」的出口。三件套是这么分工的:括号里打头的数组或范围负责供货,后面跟的筛选条件负责把关(像一道 Branch 拦着),大括号里的那串加工把每件货变成新形态,加工结果自动攒成一个新数组,全程不用你手动建数组、手动 Add。

report_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }

report_device := class(creative_device):

    Scores:[]int = array{55, 72, 91, 38, 87}

    OnBegin<override>()<suspends>:void =
        # 纯 filter:只留及格分 -> array{72, 91, 87}
        Passed := for (S : Scores, S >= 60) { S }

        # 纯 map:全员翻倍 -> array{110, 144, 182, 76, 174}
        Doubled := for (S : Scores) { S * 2 }

        # map + filter 一行流:及格分翻倍 -> array{144, 182, 174}
        Boosted := for (S : Scores, S >= 60) { S * 2 }

        Print("及格 {Passed.Length} 人,加成清单 {Boosted.Length} 条")

读图翻译:三行分别是纯筛选、纯加工、筛选加加工。Passed = ForEach 遍历 Scores、带一道「大于等于 60」的筛选,把通过的原样收进新数组;Doubled = 不筛选、每个乘 2 收集;Boosted = 先「大于等于 60」筛一道、通过的再乘 2 收集。相当于蓝图里 ForEach 接 Branch 接乘法节点,再把结果 Add 进一个新 Array——只是 Verse 一行就写完了。

注意 Boosted 那一行的信息密度:遍历、筛选、加工、建新数组,四件事一行写完,而且全程没有任何「会被改来改去的中间变量」。没通过筛选的那两轮(38 和 55)自动不进结果数组——这正是第 14 课讲过的「筛选一轮一判」在产出端的表现:这一轮没通过就没货,没货就不进新数组,连一个 Branch 都省了。所以筛选条件里那些「能通过 / 通不过」的判断——比较大小、按下标取数组元素 Arr[I] 等——都能直接往括号里堆。

括号里能放的不止筛选。规则是:打头必须是供货的数组或范围,后面可以跟任意多条筛选,或者顺手起个中间变量。也就是说,你可以先算个中间值、起个名字,再拿它来筛:for (S : Scores, D := S * 2, D >= 120) { D }——先把翻倍分算成 D,再用 D 把关,先起的名字后面都能接着用。这行和 Boosted 效果一样,但当加工逻辑变复杂时,拆出中间变量能让你不必把所有计算硬挤进一个式子里。

二、对照赛:命令式累加 vs 表达式一行流

不用 for 的自动收集,同样的活当然也能干,套路就是蓝图里最常见的那招:「循环外先建一个空 Array 变量,循环体里用 Add 一个个塞」:

imperative_style.verse
# 命令式写法:能跑,但每一行都是给 bug 留的门
var Collected:[]int = array{}
for (S : Scores):
    if (S >= 60):
        set Collected += array{S}

读图翻译:先建一个空数组变量 Collected(蓝图里 Make Array 出个空的、存进变量),ForEach 遍历 Scores,循环体里接一个 Branch 判「大于等于 60」,通过的用 set Collected += array{S}(相当于 Add 节点把 S 塞进 Collected)。四个节点串下来,才等于上面 for 一行的效果。

四行对一行,差距还不只是长短。手动版本多了一个「会被改来改去的半成品」Collected:循环没开始时它是空的,循环途中每一刻都只装了一半,别处的节点要是在错误的时机去读它、改它,bug 就冒出来了。for 版本没有这个空窗期——Passed 一出生就是完全体,而且是锁死的常量——压根没有 Set 节点可接,谁都改不动。这是 Verse 最舒服的一个瞬间:你只管说「我要什么」,不用一步步指挥「怎么一个个攒」。

什么时候还是该老老实实写循环?当每一轮要干的是「做了就算数」的动作(打印、开门、发奖励)而不是造数据时,就把动作放进 Loop Body;以及当筛选、加工逻辑复杂到一行塞不下时,拆成好几个中间变量、甚至退回「ForEach 里接 Branch」的普通写法,怎么好读怎么来。一行流是工具,不是信仰。

三、常见坑:一行流的三个翻车点

在括号筛选里打日志。筛选必须能「试完收回」,而 Print String 这类「做了就改屏幕、收不回」的节点不行——Compile 会亮红报「This invocation calls a function that has effects that are not allowed by its context」。想看中间值,把 Print String 挪进 Loop Body:那里没有这道限制,副作用随便放。

以为一轮没通过就全线停工。筛选没通过只是这一轮不产出、直接进下一轮(像 Branch 走 False),既不会中止整个 for,也不会让 for 本身失败。反过来,想「够数就提前收工」也办不到——break 只属于 loop,写进 for 会 Compile 报错;真要中途撤离,改用 loop 配自己维护的下标(第 15 课的主场),或把 for 挪进一个函数、靠函数整体结束退出。

担心空数组会炸。不会。供货的数组一件货都没有时,加工一次也不跑,for 安静地交出一个空数组,之后问它长度 .Length、再拿去遍历都照常工作——对空数组用一行流,不需要先判长度。

四、随堂小测

一个 for 遍历 array{1, 2, 3, 4}、带一道「X 大于 2」的筛选、通过的乘 10 收集,跑完 Result 是什么?

来源

本文整理自 Epic 官方文档:Control Flow in Verse ↗Verse Language Quick Reference ↗