没有运算符重载怎么办:自定义比较器与 transacts 效果
第 9 课说过,普通类实例不能用 = 直接比较,想给自定义类型「教会」大于小于就更没门了——Verse 目前不开放运算符重载。这一页从论坛上一条真实的报错出发,看社区怎么绕过这堵墙,顺便认识一个新朋友:transacts 效果标注。
第 9 课说过,普通蓝图实例不能用 = 直接比较,想给自己做的类型「教会」大于小于就更没门了——Verse 目前不开放「运算符重载」(就是给自定义类型自己定义 +、< 这些符号该怎么算)。这一页从论坛上一条真实的报错出发,看社区怎么绕过这堵墙,顺便认识一个新朋友:transacts 效果标注。
一、论坛现场:一条报错引出的两个问题
Epic 开发者论坛上,有位开发者想给自己的 CustomPlayer 类型写一个比较函数,用来给玩家排行榜排序。函数本身逻辑简单,可一编译就撞墙,报错信息是本课读者迟早会见到的一位「熟人」:
Epic 开发者论坛上,有位开发者想给自己的 CustomPlayer 类型写一个比较用的函数,用来给玩家排行榜排序。函数本身逻辑简单,可一按 Compile 就撞墙,报红信息是本课读者迟早会见到的一位「熟人」:
This invocation calls a function that has effects that are not allowed by its context: transacts no_rollback
他顺势追问:Verse 能不能像 C++ 那样重载运算符,直接给 CustomPlayer 定义 <?这个帖子的价值就在这里——它一次踩中了两个坑:一个是语言边界(有没有运算符重载),一个是效果系统(为什么报 transacts no_rollback)。
他顺势追问:Verse 能不能像有些语言那样「重载运算符」,直接给 CustomPlayer 定义 <(让「小于」对两个玩家也有意义)?这个帖子的价值就在这里——它一次踩中了两个坑:一个是语言边界(有没有运算符重载),一个是效果系统(为什么报 transacts no_rollback)。
二、答案一:UEFN Verse 目前没有运算符重载
先回答语言边界问题:不能。你在官方 API 文档里确实能看到 operator'+=' 这样的条目,但那是内建运算符在标准库里的「户口登记」,不代表用户可以给自定义类型上新户口——UEFN Verse 现阶段不支持为自定义类型定义运算符。
先回答语言边界问题:不能。你在官方 API 文档里确实能看到 operator'+=' 这样的条目,但那只是内建运算符在官方库里的「户口登记」,不代表你可以给自己做的类型上新户口——UEFN Verse 现阶段不支持为自定义类型定义运算符。
所以,想让自定义类型「可比较」,路只有两条:要么比较它内部本来就 comparable 的字段(int、string 等),要么按第 9 课讲过的办法给类标 <unique>,用 = 做「是不是同一个实例」的身份比较。至于大小排序,就得写普通函数当比较器。
所以,想让自定义类型「可比较」,路只有两条:要么比较它内部本来就能比的字段(整数、字符串等),要么按第 9 课讲过的办法给类标 <unique>,用 = 做「是不是同一个实例」的身份比较。至于大小排序,就得自己写个普通函数来当比较器(一个负责「谁大谁小」的小函数)。
三、答案二:被采纳的重构——比较器只收分数,并标上 transacts
再看效果系统问题。帖子里被采纳的答案没有硬刚运算符重载,而是做了两步重构:第一,让比较函数不接收整个 CustomPlayer,只接收提前提取好的 int 分数——比较逻辑越纯粹越好;第二,给函数显式标注 <transacts> 效果:
再看效果系统问题。帖子里被采纳的答案没有硬刚运算符重载,而是改了两处:第一,让比较函数别接收整个 CustomPlayer,只收提前取好的那个整数分数——比较逻辑越纯粹越好;第二,给函数显式挂上 <transacts> 这个效果标注:
# 被采纳的做法:比较器只接收分数,并标注 <transacts>
CompareScores<public>(A:int, B:int)<transacts>:int =
if (A < B) then -1 else if (A > B) then 1 else 0
读图翻译:这是一个自定义函数 CompareScores,收两个整数 A、B,回一个整数。函数体是一串 Branch 判断:A 比 B 小就回 -1,A 比 B 大就回 1,否则(相等)回 0——正好对应蓝图里拿两个 Integer 比大小、按结果输出 -1 / 1 / 0 的一串节点。名字后面的 <transacts> 是给这个函数贴的一张「我可以被回滚」的标签。
为什么 <transacts> 是关键?回想第 8 课:失败上下文里的代码是事务性的,失败就整体回滚。当你把一个函数作为「零件」传进这类可回滚的上下文(比如排序算法内部要在可失败位置反复调用你的比较器)时,这个零件自己也必须承诺「我可以被回滚」——这个承诺就是 <transacts>。不标,编译器就认为它默认带着不可回滚的效果,于是甩出上面那条 transacts no_rollback 报错。
为什么 <transacts> 是关键?回想第 8 课:Branch 这类「能走不通」的判断里,代码是可回滚的——走不通就把做过的改动整体撤销、当没发生过。当你把一个函数当「零件」塞进这类可回滚的场合(比如排序时要在「能走不通」的位置反复调用你的比较器)时,这个零件自己也得承诺「我可以被回滚」——这个承诺就是 <transacts>。不标,编译器就当它默认带着「不可回滚」的效果,于是一按 Compile 就甩出上面那条 transacts no_rollback 报红。
顺带注意函数体:if (A < B) then -1 else if (A > B) then 1 else 0 是单行 if 表达式的链式写法,整个 if 直接作为返回值——正好复习第 8 课「if 是表达式」和第 9 课「没有三元运算符就用 if then else」两个知识点。至于把比较器具体传给哪个排序 API,各版本签名不同,以 Verse API Reference 为准。
顺带注意函数体:if (A < B) then -1 else if (A > B) then 1 else 0 把好几个 if…then…else 串成一行,整串直接当成函数的输出值——正好复习第 8 课「if 能当值用」和第 9 课「没有三元简写就用 if then else(像蓝图的 Select 节点)」两个知识点。至于把比较器具体传给哪个排序功能,各版本不一样,以 Verse API Reference 为准。
四、随堂小测 ★
把一个比较函数传给需要「可回滚」上下文的 API一个要求「能被回滚」的场合(比如排序),却报 transacts no_rollback,最可能的原因是?
本文整理自 Epic 开发者论坛的问答帖:How to operator overloading in Verse — Unreal Engine Forums ↗