constructor vs 原型:两种造对象的方式
第 16 课的原型语法 my_class{Field := X} 能应付八成场景,但当初始化本身带逻辑——一个等级要换算出三个属性——直填就开始露怯。Verse 为此准备了第二条生产线:<constructor> 函数。
第 16 课那种「类名后面跟花括号、字段用 := 填」的写法(叫原型 / archetype)能应付八成场景;可一旦初始化本身要「算」——比如给一个等级、要换算出名字、血量、攻击三样——直填就开始吃力。Verse 为此准备了第二条生产线:<constructor> 函数(构造函数)。
一、原型直填的极限在哪里
原型(archetype)实例化是「填配置单」:每个字段填什么,你在花括号里逐项写明。它简单、直观、编译器还替你检查漏项漏填了字段 Compile 还会报红提醒你——但它只能填值,不能算值的「派生关系」。设想一个 Boss 生成器:策划只想给一个参数「等级」,而名字、血量、攻击力都应该由等级换算出来。用原型写,调用方每次都得自己算三遍每个用到的地方都得自己把三样各算一遍:boss{Name := "巨龙 Lv.5", Health := 500, Attack := 50}——换算公式散落在每个调用点,改一处漏三处,这正是 bug 的温床。
你需要的是把「一个参数 → 多个字段」的换算逻辑封装成带名字的入口。这就是构造函数(constructor)的用武之地。
二、<constructor>:长得不像函数的函数
构造函数的写法有两处反直觉,盯紧了:不写返回类型,取而代之的是用 := 直接「指向」类名;函数体里不写逻辑语句,而是逐行给字段赋初值:
构造函数的写法有两处反直觉,盯紧了:第一,它不写「输出什么类型」,而是用 := 直接「指向」它要造的那个类名;第二,函数体里也不摆一般的逻辑节点,而是一行一行给各个字段填初始值:
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
boss := class:
Name:string
var Health:int = 100
var Attack:int = 10
# 构造函数:名字后挂 <constructor>,不写返回类型,:= 指向类名
MakeBoss<constructor>(Level:int) := boss:
Name := "巨龙 Lv.{Level}"
Health := 100 * Level
Attack := 10 * Level
spawn_device := class(creative_device):
OnBegin<override>()<suspends>:void =
# 原型:适合简单直填
Grunt := boss{Name := "小兵"}
# 构造函数:一个参数,派生出三个字段
Dragon := MakeBoss(5)
Print("{Dragon.Name} 登场,血量 {Dragon.Health}")
读图翻译:上半段先建一个 boss 蓝图,三个变量 Name / Health / Attack。中间 MakeBoss<constructor>(Level:int) := boss 是一个「造 boss 的专用入口」:喂它一个等级 Level,它内部就把名字、血量、攻击按等级算好、一次性填进去。下半段用起来对比很明显:boss{Name := "小兵"} 是老办法(花括号直填);MakeBoss(5) 是新入口——只给一个 5,三样属性自动换算出来。
调用方从此只写 MakeBoss(5)以后每个用到的地方只写 MakeBoss(5),换算公式集中在一处;要造 10 级 Boss,改个参数就行。而且构造函数不止能有一个——官方文档明确:一个类可以拥有多个构造函数。给不同场景各配一个命名入口,是非常顺手的模式:MakeEnemyGrunt()、MakeEnemyBoss(Level:int)……名字本身就是文档,调用方一眼看懂造的是什么。官方 Constructor in Verse 页同时强调:构造函数与原型表达式是两种不同的东西,构造函数封装可复用的初始化逻辑,原型则是就地直填的表达式——它们互补,不互斥。构造函数和原型是两种不同的东西:构造函数把「可复用的初始化算法」打包成一个入口,原型则是「就地拿花括号直填」——它们互补,不是二选一。
三、选哪个?以及,为什么 Verse 没有 new以及,为什么 Verse 造对象不用「new」这类关键字
选型口诀很短:字段直填用原型,派生逻辑用构造。展开说——字段值现成、每处初始化各不相同、就地一次性使用:原型;初始化包含换算或校验、同一套初始化要在多处复用、想给不同配置起可读的名字:构造函数。两者也可以共存:简单场合直填,复杂场合走 MakeXxx,互不干扰。
最后回答那个转语言必问的问题:为什么没有 new?因为在 Verse 的世界观里「一切皆表达式」——boss{Name := "小兵"} 本身就是一个求值后得到实例的表达式,和 1 + 1 得到 2 没有本质区别,不需要一个额外的关键字来宣告「我在分配对象」。连构造函数也顺应了这个逻辑:它「返回」的方式不是 return,而是整个函数体作为一张更聪明的配置单,描述出一个实例。理解了这一点,你也就理解了 Verse 造对象的全部哲学:对象不是被「new 出来」的,而是被「描述出来」的。
最后回答那个从代码转来的人必问的问题:为什么没有 new?因为在 Verse 里,boss{Name := "小兵"} 这一句写出来,本身就「等于」一个造好的实例——就跟 1 + 1 写出来就等于 2 一样自然,不需要再专门喊一声「我要 new 一个对象」。构造函数也一样:它交出实例的方式不是喊 return,而是把整个函数体当成一张更聪明的配置单,把实例「描述」出来。想通这点,你就懂了 Verse 造对象的全部哲学:对象不是被「new 出来」的,而是被「描述出来」的。
一个诚实的边界提示:子类构造函数如何链式调用父类构造函数,官方文档着墨不多,本站暂不展开——动手实验时以 Verse API Reference 与 编译器Compile 的实际结果为准。
四、随堂小测 ◇
下面哪一行是合法的 Verse 构造函数定义?
什么场景最该用构造函数而不是原型直填?
来源与延伸
本文整理自 Epic 官方文档: