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

rational:整数除法背后的第三种数字类型

int 除以 int,结果既不是 int 也不是 float,而是一位几乎从不露面的角色:rational。官方文档专门为它开了一页。这一篇讲清它是什么、为什么存在,以及它如何把两个隐患逼到了编译期在 Compile(编译)那一步就逼你处理掉

一、5 / 2 到底等于几

在 C、Java、C# 里,5 / 2 等于 2——余数被静默丢弃,朝零截断,不打招呼。在很多老语言、老引擎里,5 / 2 会直接等于 2——余数被悄悄丢掉、朝零抹平,不打招呼。这条规则简单高效,也贡献了数不清的 bug:平分奖励少了一份、进度条永远到不了 100%、负数一除结果方向不对……程序员往往要在运行时盯着一个「差了一点」的数值找半天,才想起来是整数除法背了刀。

Verse 的回答完全不同:5 / 2 等于……5/2。字面意思——它是一个精确的分数,类型叫 rational(有理数),分子 5,分母 2,一丝精度都没丢。官方文档为这个类型单独开了一页:它就是「int 除以 int 的结果类型」,除此之外你几乎不会在别处遇到它。

rational 有几条颇有性格的设定:没有字面量写法——你不能声明 X:rational = 5/2 之外凭空写一个有理数;它也不参与后续的加减乘除。实际上,它在语言里的用途基本只有一个:被送进 Floor()Ceil(),换回一个 int。Floor() 向负无穷方向取整,Ceil() 向正无穷方向取整——C 风格的截断除法,在 Verse 里写作 Floor(A / B),而且整件事因为除法可失败,必须住在 if 等失败上下文里。

rational 有几条颇有性格的设定:你没法直接把它写出来——除了像 X:rational = 5/2 这种由整数除法得来的写法,你不能凭空敲一个有理数出来;它也不参与后续的加减乘除。实际上,它在语言里的用途基本只有一个:被送进 Floor()Ceil(),换回一个 int。Floor() 向负无穷方向取整,Ceil() 向正无穷方向取整——某些老语言里那种「把小数直接砍掉」的整数除法,在 Verse 里得明写成 Floor(A / B),而且整件事因为除法可能走不通,必须接在 Branch 节点(if)上。

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

rational_lab_device := class(creative_device):

    OnBegin<override>()<suspends>:void =
        # 7 / 2 = rational 7/2;向下取整得 3,向上取整得 4
        if (Down := Floor(7 / 2), Up := Ceil(7 / 2)):
            Print("Floor: {Down}  Ceil: {Up}")

注意 if 里逗号连接的两个条件——第 5 课之后你会越来越习惯这种写法:逗号就是 and,任何一个绑定失败,整个 if 不成立。这里两次除法都成功,日志打出 Floor: 3 Ceil: 4

读图翻译:Event BeginPlay 里,一个 Branch(if)节点同时判断两件事——Floor(7 / 2) 向下取整、Ceil(7 / 2) 向上取整,用逗号(= and)连起来:两个都走通,才进 Print String。第 5 课之后你会越来越习惯这种写法。这里两次除法都走通,日志打出 Floor: 3 Ceil: 4

二、两个被逼到编译期的问题

把 rational 和「/ 可失败」放在一起看,你会发现这套设计逼着每一个写除法的人,在编译期就回答两个问题:

把 rational 和「除法可能走不通」放在一起看,你会发现这套设计逼着每一个写除法的人,在Compile(编译)时就回答两个问题:

问题一:除数会不会是 0?别的语言里,除零要么运行时崩溃(int),要么悄悄给你一个 Infinity(float),总之都是运行时才见分晓。Verse 把 int 除法定为可失败运算,不放进失败上下文就不给编译——「除数为零怎么办」从此不是「上线后祈祷」的问题,而是「不回答就无法提交代码」的问题。

问题一:除数会不会是 0?别处常见的做法是:除零要么当场崩溃(int),要么悄悄塞给你一个「无穷大」(float),反正都要等游戏跑起来才见分晓。Verse 把 int 除法定成「可能走不通」的一步,不接到 Branch 节点上就不让你 Compile 通过——「除数为零怎么办」从此不是「上线后祈祷」的问题,而是「不回答就交不了代码」的问题。

问题二:余数往哪边舍?C 的截断朝零,Python 的整除朝负无穷,两派程序员各自觉得对方的行为是 bug。Verse 干脆不选边:除法给你精确分数,舍入方向由你亲手写 Floor 或 Ceil 指定,白纸黑字留在代码里,读代码的人一眼看懂,不用背语言的默认规则。

问题二:余数往哪边舍?有的做法把小数朝零砍,有的朝负无穷取,两派人各自觉得对方是 bug。Verse 干脆不选边:除法给你精确分数,舍入方向由你亲手写 Floor 或 Ceil 指定,白纸黑字留在图里,看的人一眼就懂,不用去背某一套默认规则。

顺带一提,Floor 的「向负无穷」在负数上和 C 的「朝零截断」是真的会分道扬镳的:-7 / 2 在 C 里等于 -3,而 Verse 的 Floor(-7 / 2) 把 -3.5 向下舍到 -4。做倒计时、扣血分摊这类会出现负数的除法时,这一格之差就是一次事故。

顺带一提,Floor 的「向负无穷」在负数上,和「朝零把小数砍掉」的做法会真的分道扬镳:-7 / 2 按「朝零砍」是 -3,而 Verse 的 Floor(-7 / 2) 把 -3.5 向下舍到 -4。做倒计时、扣血分摊这类会出现负数的除法时,这一格之差就是一次事故。

那为什么不干脆返回 float?这是设计上最值得咀嚼的一步。float 是二进制小数,表示不了大多数十进制分数——1/3 存进 float 的那一刻,精度就永久损失了,后面再怎么取整都是在「已经不准的数」上做决定。rational 保存的是精确比值,精度损失只发生在你显式调用 Floor/Ceil 的那一行——损失多少、往哪边损失,全部由你签字画押。隐患不是被消灭了,而是被显式化了——这正是 Verse 类型系统一以贯之的哲学,你在 set、logic、可失败表达式上都见过同款。隐患不是被消灭了,而是被摆到明面上了——这正是 Verse 一以贯之的脾气:该你做的决定,它绝不偷偷替你做。你在 Set 节点、logic(真/假)、还有那些「可能走不通」的判断里,都见过同一副脾气。

最后一个工程小贴士:如果一个函数满篇都是除法,每个都包 if 嫌啰嗦,可以给整个函数标上 <decides> 效果,让函数本身成为可失败表达式,除法们就能直接写——代价是调用方要用方括号并接住失败。第 5 课拓展页一的 RoundToDecimalPlaces 用的正是这招。

最后一个工程小贴士:如果一个函数满篇都是除法,每个都单独接一个 Branch(if)嫌啰嗦,可以给整个函数标上 <decides> 效果,让函数本身变成「可能走不通」的一步,里面的除法就能直接写——代价是:谁调用它,谁就得用方括号,并在自己那边接一个 Branch(if)接住「走不通」。第 5 课拓展页一的 RoundToDecimalPlaces 用的正是这招。

if (N := Floor(7 / 2)): 成功后,N 的值和类型是?

为什么 Verse 让 int 除法返回 rational,而不是直接返回 float?

来源

本文整理自官方文档:Rational in Verse(Epic 官方文档)↗ · Int in Verse(Epic 官方文档)↗