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

subtype、castable 与「类型为什么不能比较」

Verse 允许你把「类型本身」当值存进变量——听起来像开挂,直到你想用它当 map 的键:编译器冷冷回一句 not comparable,Epic 还把工单标成 won't fix。这堵墙背后,站着 Verse 类型系统的理论根基和一条数学定理。

一、把类型当值:subtype 家族

正课里我们把「实例」存进变量;进阶玩法是把「类型」也存进变量。Verse 提供了一组类型宏来表达「某类型的子类型」:subtype(t) 表示「t 的任意子类型」;castable_subtype(t) 表示「可以作为转型目标的子类型」;concrete_subtype(t) 表示「具体的(可实例化的)子类型」。它们可以出现在字段、变量、参数的类型标注里——于是「生成哪种战利品」这件事,可以直接用一个存类型的字段来配置:

正课里我们把「实例」存进变量;进阶玩法是把「类型」本身也存进变量——有点像在蓝图里放一个「类引用(Class Reference)」变量,里面装的不是某个具体实例,而是「要用哪一种蓝图」。Verse 提供了一组写法来表达「某个类型的子类型」(子类型 ≈ 它的子蓝图 Child Blueprint):subtype(t) 是「t 的任意子类型」;castable_subtype(t) 是「能当 Cast 目标的子类型」;concrete_subtype(t) 是「能实际造出实例的子类型」。它们能出现在字段、变量、参数的类型上——于是「生成哪种战利品」这件事,可以直接拿一个「装类型的字段」来配置:

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

loot := class:
    Value:int = 0

coin := class(loot):
    Value<override>:int = 10

gem := class(loot):
    Value<override>:int = 100

# 字段里存的不是实例,而是「类型本身」
loot_spawner := class:
    LootType:concrete_subtype(loot) = coin

    # 用存下来的类型直接实例化(论坛验证过的范式)
    MakeOne():loot =
        LootType{}

读图翻译:这段先建一个基类 loot 和它的两个子蓝图 coingem(各自把 Value 改成 10 和 100)。关键在 loot_spawner:它有个字段 LootType,装的不是某个实例,而是「哪一种 loot 类型」,这里先填成 coinMakeOne() 就直接拿这个存下来的类型去造一只实例(LootType{})——相当于蓝图里 Spawn 一个「由变量指定的类」。

换一行 loot_spawner{LootType := gem},同一台生成器就改吐宝石——不用继承、不用分支判断,类型本身成了可配置的数据。配合正课学过的转型,还能反向验货:if (G := gem[Something]): 检查一个 loot 到底是不是宝石。(顺带一提:接口要作为运行时转型目标,与 <castable> specifier 有关,细节以 Verse API Reference 为准。)

把那行换成 loot_spawner{LootType := gem},同一台生成器就改吐宝石了——不用另开子蓝图、也不用一堆 Branch 判断,「用哪种类型」本身成了可配置的数据。配合正课学过的 Cast,还能反向验货:if (G := gem[Something]): 就是「Cast To gem」,看一个 loot 到底是不是宝石。(顺带一提:接口要能当运行时 Cast 的目标,和 <castable> 这个标记有关,细节以 Verse API Reference 为准。)

二、撞墙现场:类型不可比较

存都能存了,下一步自然想建一张「类型 → 处理逻辑」的查找表:遇到 coin 走这条路,遇到 gem 走那条路,注册进一个 map,多优雅。论坛帖「[Major] Verse types are not comparable」替所有人试过了——此路不通:

存都能存了,下一步自然想建一张「类型 → 处理逻辑」的查找表:遇到 coin 走这条路,遇到 gem 走那条路,全登记进一个 Map(储物柜),多优雅。论坛帖「[Major] Verse types are not comparable」替所有人试过了——此路不通:

type_map_fail.verse
# ✗ 编译错误:
# 'subtype(loot)' cannot be used as the type of map keys
# because it is not comparable for equality
# var Handlers:[subtype(loot)]int = map{}

不只是 map 键:两个类型值之间连 = 都不能用。也就是说,你可以把类型搬来搬去、用它实例化、用它转型,但永远无法问出「这两个类型是不是同一个」——在第 13 课我们讲过 map 的键必须 comparable,类型恰恰不在名单上,而且官方明确表示:不会修

上面注释里那行就是想拿类型当 Map 的键,结果编译直接拒绝。而且不只是当键:两个类型值之间连一个 =(判相等)都不给用。也就是说,你能把类型搬来搬去、拿它造实例、拿它做 Cast,却永远问不出「这两个类型是不是同一个」——第 13 课讲过 Map 的键必须是「能判相等」的类型,类型恰恰不在名单上,而且官方明确表示:不会修

三、Epic 的解释:类型即函数,与 Rice 定理

为什么态度这么坚决?Epic 工程师 Saam 在帖子里给出了理论层面的答案:在 Verse 的类型系统中,类型本质上是函数——你可以把一个类型理解成「判定某个值是否属于它」的谓词。这套设计让 Verse 的类型系统极其灵活(subtype 家族就是产物),但也带来一个数学上的死胡同:要判断两个类型「相等」,等价于判断两个函数行为等价;而计算理论中的 Rice 定理告诉我们,关于程序行为的任何非平凡性质都是不可判定的——没有任何算法能对所有情况给出正确答案。

为什么态度这么坚决?Epic 工程师 Saam 给了个理论层面的答案:在 Verse 里,一个「类型」本质上是一段判断——你可以把它理解成「拿任何一个值来问『你算不算这一类』、给出是或否」的小程序。这套设计让类型玩法极其灵活(前面那组 subtype 写法就是产物),但也撞上一个数学死胡同:要判断「两个类型相等」,就等于判断「两段判断程序是不是永远给一样的结果」;而计算理论里的 Rice 定理说了,关于「程序跑起来会怎样」的任何有意义的问题,都是没法用算法判定的——不存在一个万能算法能对所有情况都答对。

所以「类型不可比较」不是 Epic 偷懒,而是数学在划线:与其提供一个在某些情况下必然出错或挂起的 =,不如从一开始就不提供。工单被标为 won't fix,理由正是如此。这也是一扇难得的窗口:Verse 自我定位为 functional logic language,类型与函数、命题在底层是一家人——你在第 8 课感受过的「失败即控制流」,和这里的「类型即函数」,同出一脉。

所以「类型不能比较」不是 Epic 偷懒,而是数学在划线:与其给一个在某些情况下必然出错或卡死的 =,不如干脆不给。工单被标成 won't fix,正是这个理由。这也是一扇难得的窗口:Verse 把自己定位成「函数式逻辑语言」,类型、函数、命题在底层本是一家——你在第 8 课体会过的「一条线走不走得通就是控制流」,和这里的「类型其实是一段判断」,同出一脉。

四、绕行方案:查表梦想的三种替身

▢ 替身一:给类型挂「名牌」。键不能是类型,但可以是 string 或 enum:给每个类挂一个 Kind:string 之类的常量字段,用名牌当 map 键,查表照做不误——代价是名牌要自己保证不重名。

▢ 替身一:给类型挂一张「名牌」。键不能是类型,但可以是 string 或 enum(枚举资产):给每个蓝图挂一个 Kind:string 之类的固定字段,拿这张名牌去当 Map 的键,查表照样跑——代价是名牌得自己保证不重名。

▢ 替身二:逐个转型分派。castable_subtype(t) 存类型、按顺序 if (C := some_type[Ref]): 逐个试——本质是把查表换成了 if 链,类型少的时候完全够用。

▢ 替身二:逐个 Cast 分派。castable_subtype(t) 把类型存下来,再按顺序 if (C := some_type[Ref]): 一个个试着 Cast——本质是把「查表」换成了一串 Branch,类型不多的时候完全够用。

▢ 替身三:接口多态。回到第 17 课的主场:与其「根据类型查处理器」,不如把处理逻辑作为方法写进各个类型,签同一份 interface,让运行时多态自动分发。绝大多数「类型 → 行为」的查表需求,其实是接口还没抽出来的信号。

▢ 替身三:接口多态(最推荐)。回到第 17 课的主场:与其「按类型查处理器」,不如把各自的处理逻辑写进各个蓝图,让它们签同一份接口(BPI),交给运行时多态自动分发。绝大多数「类型 → 行为」的查表需求,其实都是「接口还没抽出来」的信号。

为什么 subtype(...) 类型的值不能当 map 的键?为什么「装着一个类型」的值(subtype(...))不能拿来当 Map 的键?

来源与延伸

本文整理自 Epic 开发者论坛与官方文档:

论坛:[Major] Verse types are not comparable(含 Epic 工程师 Saam 的回复)↗

官方文档:Comparable in Verse ↗

官方文档:Verse Glossary(type cast 条目)↗