变量、常量与枚举
掌握 var 的三种声明与类型推断 static var 类成员 const 常量表达式和 enum 枚举的完整用法
变量(var)、常量(const)与枚举(enum)是任何程序的基本 building block。GDScript 在这三者上都有明确而细致的规则:var 有三种声明形态与一套类型推断条件;成员变量的初始化遵循官方定义的七步顺序,顺序不对会产生非常隐蔽的 bug;static var 把状态挂在类而不是实例上;const 只接受编译期常量表达式;enum 本质是 int 的命名集合。本篇把这些规则一次讲清。
学习目标
- 使用 var 的三种声明形式,理解类型标注与
:=推断的规则与限制; - 掌握成员变量初始化的七步顺序,能识别并避开初始化顺序引发的 bug;
- 使用 static var 编写类级共享状态,了解它的限制与 @static_unload;
- 用 const 定义编译期常量,理解常量表达式的允许范围;
- 用 enum 定义匿名与具名枚举,掌握键访问、显式赋值与类型标注。
var:三种声明形式
var a # 不写类型也不赋值:Variant,默认值 null
var b = 5 # 不写类型但赋初值:动态
var c = 3.8
var typed_var: int # 类型标注、不赋初值:默认 0
var inferred_type := "String" # := 由右侧推断:类型为 String
三个要点:
第一,类型标注之后,赋一个不兼容的类型会直接报错(解析期错误):
var hp: int = 100
# hp = "many" # 报错:String 不能赋给 int
第二,浮点值赋给 int 是合法的,但会被隐式截断:
var level: int = 3.14 # 合法,level 的值是 3
小数部分被直接丢弃、不会四舍五入。这是”合法但有副作用”的行为,写代码时要心里有数:本意若是取整,请显式调用 round()、floor() 等函数,让意图可见。
第三,var a 这种”裸声明”得到的是 Variant 类型、默认值 null,等于放弃了静态类型的全部好处,只在确实需要任意类型时使用。
:= 类型推断的适用条件
:= 表示”类型由右侧表达式推断”。它只在所赋的值”有定义类型”时可用,典型场景包括:
- 内置类型的字面量:
var speed := 300.0; - 引擎类实例:
var dir := Vector2(1, 0); - preload 得到的脚本常量:
var Bullet := preload("res://bullet.gd"); - class_name 注册的全局类:
var enemy := Enemy.new(); - autoload 单例:
var audio := AudioPlayer。
Variant 没有定义类型,无法推断:
var a = 10 # a 是 Variant
# var b := a # 报错:Variant 无法推断
此时要么给 a 补上类型标注,要么显式写 var b: int = a。经验法则:能用 := 就用 :=——它既省去重复书写类型,又保住了静态类型。
成员变量初始化的七步顺序
创建实例时,成员变量按官方文档给出的固定顺序初始化:
- 依静态类型取默认值(int 是 0、float 是 0.0、引用类型是 null);
- 按脚本中的声明顺序,依次给成员赋初始值;
- (Node 派生类)@onready 变量被推迟,暂不初始化;
- 调用
_init(); - 实例化时赋导出值(场景与检查器里设置的 @export 值在此写入);
- (Node 派生类)初始化 @onready 变量;
- (Node 派生类)调用
_ready()。
flowchart TD
A["1. 依静态类型取默认值"] --> B["2. 按脚本顺序赋初始值"]
B --> C["3. @onready 推迟(仅 Node 派生)"]
C --> D["4. 调用 _init"]
D --> E["5. 实例化时赋导出值"]
E --> F["6. 初始化 @onready(仅 Node 派生)"]
F --> G["7. 调用 _ready(仅 Node 派生)"]
有两个非常典型的坑。
坑一:初始值严格按声明顺序执行,而初始值里允许包含函数调用。被依赖的成员必须声明在前面,否则函数读到的是默认值:
extends Node
var _cache: Dictionary = _make_cache() # 先执行
var _prefix: String = "player_" # 后执行
func _make_cache() -> Dictionary:
return {_prefix + "hp": 100} # 此时 _prefix 还是 String 的默认值 ""
func _ready() -> void:
print(_cache) # 输出 {"hp": 100},而不是 {"player_hp": 100}
这类”顺序错误导致数据残缺或互相覆盖”的问题(例如一份精心构造的 _data 字典被后续赋值清空覆盖)极其隐蔽,因为代码看起来完全正确。规则很简单:把被依赖的成员声明在前面;有复杂依赖关系的初始化不要塞进初始值,挪到 _init() 或 _ready() 里做。
坑二:_init() 在第 4 步执行,而导出值在第 5 步才写入。所以不要在 _init() 里读取 @export 变量并期待拿到场景里设置的值——那一刻读到的还是脚本里的初始值。
静态变量 static var
static var 属于类而非实例,所有实例共享同一份数据:
class_name Student
static var class_size: int = 0
var name: String
var id: int
func _init(student_name: String) -> void:
name = student_name
class_size += 1 # 所有实例共享的计数器
id = class_size
每 Student.new() 一次,class_size 加一,id 依次是 1、2、3……无论从哪个实例读 class_size,看到的都是同一个值。基类的静态变量也可以在子类中共享访问。
静态变量可以带类型,也可以带 setter/getter:
static var balance: int = 0:
set(value):
balance = maxi(value, 0) # 余额不允许为负
static var debt: int = 0
使用限制:
@export与@onready不能用于静态变量;- 局部变量(函数内部声明的变量)不能声明为 static。
默认情况下,含静态变量的脚本会常驻内存。如果确有需要,在 class_name/extends 之前写 @static_unload 注解,允许引擎卸载该脚本:
@static_unload
class_name Registry
extends RefCounted
常量 const:编译期常量表达式
const 声明编译期常量,其值必须在编译期可知:
const A = 5
const B = Vector2(20, 20)
const C = 10 + 20
const E = [1, 2, 3, 4][0]
const F = sin(20) # sin() 可用于常量表达式
要点:
- 允许的内容包括字面量、值类型构造、常量运算,甚至
sin(20)这类数学函数; - 可以显式标注类型:
const MAX_HEALTH: int = 100; - 常量数组是只读的,尝试修改会报错;
- 命名惯例是全大写 CONSTANT_CASE,如
MAX_HEALTH_POINT。
const 与 static var 的分工:永远不变的数学值、配置上限、路径模板用 const;需要类级可变状态(计数器、共享余额)用 static var。把可变的值误写成 const 会在赋值时报错,把不变的值写成 static var 则失去了编译期检查。
枚举 enum
枚举把一组相关的整型常量组织在一起,有两种形式:
enum {TILE_BRICK, TILE_FLOOR, TILE_SPIKE} # 匿名枚举
enum WeekDays { MONDAY, TUESDAY, WEDNESDAY } # 具名枚举
enum State { STATE_IDLE, STATE_JUMP = 5, STATE_SHOOT } # 显式赋值
匿名枚举的成员直接作为常量使用:TILE_BRICK 是 0、TILE_FLOOR 是 1、TILE_SPIKE 是 2。
具名枚举的键不是全局常量,必须通过枚举名访问:WeekDays.MONDAY。具名枚举还提供 keys() 与 values() 方法,分别返回键列表与值列表,适合做遍历或界面下拉框。
成员从 0 开始自动递增;显式赋值后,后续成员从该值继续递增:上面的 State 里,STATE_IDLE 是 0,STATE_JUMP 显式为 5,STATE_SHOOT 自动为 6。多个成员也可以赋同一个值,用来表达”同一含义的别名”。
枚举本质是 int:可以与整数比较、参与运算。因此可以用枚举名做类型标注:
var day: WeekDays = WeekDays.MONDAY
注意:这个标注只告诉解析器”day 应该是一个 WeekDays”,并不保证运行时的值真的属于这个集合——给 day 赋一个 99 这样的整数,并不会被这行声明拦下。想要严格的合法性校验,需要自己在逻辑里用 day in WeekDays.values() 之类的判断。
小结
- var 有三种形态:纯声明(Variant、默认 null)、赋值动态、类型标注或
:=推断;浮点赋给 int 会隐式截断。 :=只在右侧”有定义类型”时可用:内置类型字面量、引擎类、preload 脚本、class_name 全局类、autoload 单例;Variant 推断不了。- 成员初始化七步:类型默认值、按声明顺序赋初始值、@onready 推迟、_init、导出值、@onready 初始化、_ready;有依赖的初始化必须安排好声明顺序,或挪进 _init/_ready。
- static var 属于类、全实例共享、子类可访问;可带 setter/getter;@export/@onready 与局部 static 均不允许;@static_unload 允许卸载脚本。
- const 是编译期常量表达式,允许字面量、值类型构造、常量运算甚至 sin(20);常量数组只读;命名全大写。
- enum 有匿名与具名两种;具名枚举用 Name.KEY 访问并支持 keys()/values();显式赋值后自动递增;本质是 int,枚举类型标注不校验值域。