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

tuple 展开(splatting):一包当一串传

第 14 课学会了把多个值打包成 tuple,这一页学它的逆操作:把一个 tuple 直接塞进函数调用,它会自动展开成一串实参。这个官方文档记载的语法糖背后,藏着 Verse 的一个深层设计——函数的参数列表,本质上就是一个 tuple。

第 14 课学会了把多个值打包成 tuple,这一页学它的反向操作:把一整包 tuple 直接塞进一次函数调用,它会自动摊开、填满好几个入参。这个官方文档记载的便利写法背后,藏着 Verse 的一个深层设计——函数的入参列表,本质上就是一个 tuple。

一、快递直通:包裹不用拆,直接签收

先复现一个第 14 课结尾留下的别扭场景。你有一个返回 tuple(float, float) 的函数,还有一个接受两个 float 参数的函数,想把前者的结果喂给后者,按基础课的写法得先接包、再逐格拆、再逐个传:

先复现一个第 14 课结尾留下的别扭场景。你有一个吐出 tuple(float, float)(两格小数包裹)的函数,还有一个需要两个小数入参的函数,想把前者的结果喂给后者。按基础课教的写法,得先把整包接住、再一格格拆、再一个个传:

Point := GetTarget(),然后 PrintPoint(Point(0), Point(1))——能跑,但啰嗦。官方 Tuple in Verse 文档记载了更漂亮的写法:把 tuple 作为单个实参传给函数时,调用的效果等同于把 tuple 的每个元素分别作为独立实参传入。这个特性叫 tuple 展开(tuple expansion),社区里也叫它 splatting(「啪」地一下拍开):

Point := GetTarget(),然后 PrintPoint(Point(0), Point(1))——能跑,但啰嗦。官方 Tuple in Verse 文档记载了更漂亮的写法:把一整包 tuple 当作一个参数传给函数时,效果等同于把包裹里每个元素分别当成一个独立入参传进去。这个特性叫 tuple 展开(tuple expansion),社区里也叫它 splatting(「啪」地一下拍开):

splat_demo.verse
using { /UnrealEngine.com/Temporary/Diagnostics }

# 接受两个参数的函数
PrintPoint(X:float, Y:float):void =
    Print("坐标:({X}, {Y})")

# 返回 tuple 的函数:多返回值打包出货
GetTarget():tuple(float, float) =
    (100.0, 200.0)

Demo():void =
    Target := GetTarget()
    # tuple 展开:一整包当作两个实参,自动拆开对号入座
    PrintPoint(Target)
    # 与逐个传参完全等价:
    PrintPoint(100.0, 200.0)
    # 甚至可以把返回值直接怼进调用,连中转变量都省了:
    PrintPoint(GetTarget())

读图翻译:PrintPoint 是个要「两个小数入口」的函数;GetTarget 吐出一个两格包裹。Demo 里三种喂法效果完全一样:直接把整包 Target 塞进去、把两个数 100.0, 200.0 分开塞、或者把 GetTarget() 的结果直接怼进调用。整包塞进去时,Verse 自动把包裹拍开、按位置塞进那两个入口——相当于把一个「多输出引脚」的节点,整束连到下一个「多输入引脚」的节点上。

PrintPoint(Target) 看起来像「只传了一个参数给双参数函数」,却能编译通过并正确运行——因为 Target 的类型 tuple(float, float) 和参数列表 (X:float, Y:float) 形状严丝合缝,编译器直接把包裹拍开,元素按位置对号入座。于是「多返回值直通多参数」的流水线成立了:上游函数打包出货,下游函数整包签收,中间一行拆包代码都不用写。

PrintPoint(Target) 看起来像「只递了一个值给一个要两个入口的函数」,却能顺利 Compile 并正确跑起来——因为 Target 的类型 tuple(float, float) 和入口列表 (X:float, Y:float) 形状严丝合缝,Verse 直接把包裹拍开,元素按位置对号入座。于是「一个函数的多个产出直通下一个函数的多个入口」这条流水线成立了:上游打包出货,下游整包签收,中间一行拆包代码都不用写。

二、背后的世界观:一切调用都是传一个 tuple

这不是偶然凑巧的语法糖,而是 Verse 深层设计的自然流露:函数的参数列表本质上就是一个 tuple。你写 PrintPoint(X:float, Y:float),在语言眼里约等于声明「这个函数接受一个 tuple(float, float)」;你写 PrintPoint(100.0, 200.0),约等于「传入 tuple 字面量 (100.0, 200.0)」。所以「把一个现成的 tuple 传进去」自然也成立——三种写法殊途同归,因为它们本来就是同一件事。

这不是偶然凑巧的便利写法,而是 Verse 深层设计的自然流露:函数的入参列表,本质上就是一个 tuple。你写 PrintPoint(X:float, Y:float),在语言眼里约等于说「这个函数收一个 tuple(float, float)」;你写 PrintPoint(100.0, 200.0),约等于「递给它一个两格包裹 (100.0, 200.0)」。所以「把一个现成的包裹整个递进去」自然也成立——三种写法殊途同归,因为它们本来就是同一件事。

这个视角还能解释第 14 课的两个「巧合」:为什么 tuple 字面量和函数调用长得一样,都是圆括号加逗号?为什么函数返回多个值的官方姿势偏偏是返回 tuple?答案统一了——在 Verse 的模型里,函数就是「吃进一个 tuple,吐出一个值(也可以是 tuple)」的机器。参数、返回值、打包、拆包,全是同一套 tuple 机制的不同侧脸。

这个视角还能解释第 14 课的两个「巧合」:为什么 tuple 的写法和函数调用长得一模一样,都是圆括号加逗号?为什么「一个函数吐好几个值」的官方姿势偏偏是吐一个 tuple?答案统一了——在 Verse 的模型里,函数就是一台「吃进一个 tuple,吐出一个值(这个值也可以是 tuple)」的机器。入参、返回值、打包、拆包,全是同一套 tuple 机制的不同侧脸。

实用场景两则:其一,参数转发。写一个包装函数时,把收到的参数原样转交给内层函数——参数列表本身可以当 tuple 接住再整包传出。其二,函数组合。让一批工具函数都以 tuple 进、tuple 出,它们就能像传送带一样首尾相接:StepB(StepA()),A 的多个产出直接灌进 B 的多个入参,不需要任何胶水代码。

实用场景两则:其一,参数转发。做一个包一层的函数时,把收到的入参原样转交给里层函数——那一串入参本身就能当一个 tuple 接住、再整包递出去。其二,函数串联。让一批工具函数都「吃 tuple、吐 tuple」,它们就能像传送带一样首尾相接:StepB(StepA()),A 吐出的好几个结果直接灌进 B 的好几个入口,中间一行拼接代码都不用写。

最后一个小提醒:展开发生在「形状匹配」的时候。如果函数本来就只有一个 tuple 类型的参数,那传入的 tuple 就是那个参数本身,不会被强行拆开——编译器按类型对号,不靠猜。

最后一个小提醒:自动摊开只在「形状对得上」的时候发生。如果一个函数本来就只要一个 tuple 类型的入参,那你递进去的 tuple 就是那个入参本身,不会被硬拆开——编译器是按类型对号入座的,不靠猜。

已有 F(X:int, Y:int):void 和 T:tuple(int, int) = (1, 2),调用 F(T) 会发生什么?已有一个要两个整数入参的函数 F(X:int, Y:int),和一个两格包裹 T:tuple(int, int) = (1, 2),把整包 T 递给它 F(T) 会发生什么?

来源与延伸

本文整理自 Epic 官方文档: