range 为什么不是一等值
试着把 0..10 存进一个变量——编译器会当场拒绝。这一页从官方文档的几条限制出发,聊聊「语法糖」和「一等值」的区别,并给出倒数、跳步等所有官方没直接支持的写法的替代方案。
试着把 0..10 存进一个变量——Compile 会当场亮红拒绝。这一页从官方文档的几条限制出发,聊聊为什么范围只是一段「一次性的数数写法」,而不是一个能存、能传的正经值,并给出倒数、跳步这些官方没直接支持的写法的替代方案。
一、官方白纸黑字的几条限制
官方 Range in Verse 页面把范围表达式的边界画得非常清楚:范围只支持 int;步长固定为 1;只能升序;并且——重点来了——范围表达式只能出现在 for、sync、race、rush 这四种表达式里。换句话说,下面这些在 Python、Kotlin 里稀松平常的操作,在 Verse 里全部不成立:
官方 Range in Verse 页面把范围能干什么画得死死的:范围只能数整数;每次加 1;只能从小往大数;并且——重点来了——0..N 这种写法只能出现在 for、sync、race、rush 这四种地方(后三个是并发相关的,后面课程才讲)。换句话说,下面这些在别的语言里稀松平常的操作,在 Verse 里一概不成立:
using { /UnrealEngine.com/Temporary/Diagnostics }
# 合法:范围直接出现在 for 里
for (I := 0..10):
Print("第{I}轮")
# 以下全部是编译错误,范围不是值:
# MyRange := 0..10 # 不能存进变量
# TakeRange(0..10) # 不能当参数传递
# (0..10).Length # 不能对它调用方法
读图翻译:只有第一段是合法的——一个从 0 数到 10 的循环,每轮 Print String 打「第 __ 轮」。下面三行都被注释掉了,因为它们全是编译错误:范围不能存进变量、不能当参数喂给别的函数、也不能像数组那样去问它长度。范围不是一个「东西」,只是一段数数的动作。
也就是说,0..10 不是「一个范围对象」,而是一段生成器语法——它没有类型、没有身份、不能被指着说「就是它」,只在那四个特定语法位置里被编译器就地展开成迭代。编程语言把「能存进变量、能当参数、能当返回值」的东西叫一等值(first-class value);Verse 的 range 明确不是。
也就是说,0..10 不是「一个范围对象」,而是一段临时的数数写法——它没有类型、没有身份,你没法指着它说「就是这个值」,它只在那四个特定位置被就地摊开成「从头数到尾」。编程里把「能存进变量、能当参数传、能当返回值」的东西叫「一等值」(first-class value);Verse 的范围明确不算。
二、语法糖 vs 一等值:一次设计取舍
对比一下邻居们:Python 的 range(0, 11) 返回一个货真价实的 range 对象,能存、能传、能切片、能问长度;Kotlin 的 0..10 是 IntRange 实例,还能 step 2、downTo 倒着走。Verse 选了另一条路:把范围做成纯语法糖,只服务于「有界重复」这一个场景。
对比一下邻居们:在别的语言里,范围往往是个货真价实的对象,能存、能传、能切片、能问长度,甚至能设步长、能倒着走。Verse 选了另一条路:把范围做成纯粹的「一次性数数写法」,只服务于「按固定次数重复」这一个场景。
这笔买卖换来什么?简单。语言不用引入 range 类型、不用定义它和数组的转换规则、不用回答「浮点范围怎么办」「负步长语义是什么」这一串连环问题;for、sync、race、rush 四个接收方各自对「从 M 数到 N」做最直接的展开就够了。代价也明摆着:没有步长、没有倒序、没有惰性序列,想要这些,得自己动手。这类「砍掉一整类特性换取语言更小」的取舍,在 Verse 里你还会反复见到——它和「修改必须喊 set」「失败必须待在失败上下文」是同一种性格。
这笔买卖换来什么?简单。语言不用专门造一个「范围类型」、不用定义它和数组怎么互转、不用回答「小数范围怎么办」「倒着数是什么意思」这一串连环问题;for 和那三个并发场景各自把「从 M 数到 N」直接摊开就够了。代价也明摆着:没有步长、没有倒序、没有「用到才算」的懒序列,想要这些,得自己动手。这类「砍掉一整类花样、换语言更精简」的取舍,在 Verse 里你会反复见到——它和「改值必须走 Set 节点」「判断必须待在能走通 / 走不通的上下文里」是同一种性格。
三、替代写法清单:倒数、跳步与空范围
限制归限制,活还是要干。官方没直接支持的几种迭代,标准替代姿势如下——核心思路都是「让范围老老实实升序数数,数学换算交给循环体或子句」:
限制归限制,活还是要干。官方没直接支持的几种数法,标准替代姿势如下——核心思路都是「让范围老老实实从小往大数,换算交给循环体或括号里的中间变量」:
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
countdown_device := class(creative_device):
N:int = 5
OnBegin<override>()<suspends>:void =
# 倒序:10..0 一轮都不跑;正确姿势是升序 + N - I 换算
for (I := 0..N):
Countdown := N - I
Print("倒数:{Countdown}")
# 步长 2:没有 step 关键字,用乘法造等差数列
Evens := for (I := 0..5) { I * 2 }
Print("偶数共 {Evens.Length} 个")
# 空范围 0..-1:不报错,零轮执行——对空数组安全
Empty:[]int = array{}
for (I := 0..Empty.Length - 1):
Print("这行永远不会打印")
逐条拆解:倒数用 N - I,I 从 0 升到 N,Countdown 就从 N 降到 0;步长 2 用 I * 2,顺手还演示了第 10 课的 for 表达式产出数组;最后一条是范围「不是一等值」难得的温柔面——空范围(起点大于终点)不报错、不警告,循环体一轮都不执行,所以 0..Arr.Length - 1 对空数组永远安全。倒过来看,这份温柔也有阴暗面:手滑写出 10..0 时,编译器同样一声不吭,循环静默消失——遇到「循环怎么一次都没跑」的灵异现象,先检查范围两端的大小关系。
逐条拆解:倒数用 N - I,I 从 0 升到 N,Countdown 就从 N 降到 0;步长 2 用 I * 2,顺手还演示了第 10 课那招——for 跑完直接吐出一个数组;最后一条是范围「不是正经值」难得的温柔面——空范围(起点比终点大)不报错、不警告,循环体一轮都不跑,所以 0..数组长度 - 1 对空数组永远安全。倒过来看,这份温柔也有阴暗面:手滑写出 10..0 时,Compile 同样一声不吭,循环静默消失——遇到「循环怎么一次都没跑」的灵异现象,先检查范围两端谁大谁小。
四、随堂小测
想打印 10、9、8……一路倒数到 0,下面哪个写法可行?
来源
本文整理自 Epic 官方文档:Range in Verse ↗,并参考 Control Flow in Verse ↗。