前面在变量那篇里简单提过 GDScript 的类型系统,但没深入。用了一段时间后发现,类型系统如果掌握得好,写代码时能少踩很多坑,也能让编辑器更智能。这篇笔记就专门聊一聊 GDScript 类型系统的进阶用法,包括完整的类型注解写法、类型转换、类型检查,以及 GDScript 没有泛型时的替代思路。这些都是我在实际写项目时慢慢摸出来的经验,分享出来供参考。
一、GDScript 的类型哲学:动态为主,静态可选
GDScript 默认是动态类型的。你写 var a = 10,a 的类型是 int,但这是推断出来的,不是强制。你也可以写 var a,不赋初值,那 a 就是 Variant 类型,可以装任何东西。
这种灵活性让快速原型开发很舒服,但项目一旦变大,缺点就暴露了:编辑器没法准确提示,类型错误要等运行时才暴露。所以 Godot 4 引入了更强的静态类型支持,你可以选择性地给变量、函数加上类型注解,把关键部分的类型固定下来。
我的习惯是:公开接口、成员变量、函数参数和返回值尽量写类型注解;局部临时变量可以偷懒省略。 这样既保留了灵活性,又能在重要边界上获得编译期检查。
二、类型注解的完整写法
1. 变量和常量
gdscript
var health: int = 100
var speed: float = 200.0
var player_name: String = "Tom"
var is_alive: bool = true
var spawn_position: Vector2 = Vector2.ZERO
常量也可以标注类型:
gdscript
const MAX_HEALTH: int = 100
注意:const 必须初始化,类型注解可以省略,但写上更明确。
2. 数组和字典的类型化
普通数组 var arr = [] 是 Array 类型,里面什么都能放。如果你想限制元素类型,可以这样写:
gdscript
var numbers: Array[int] = [1, 2, 3]
var names: Array[String] = ["Tom", "Jerry"]
var enemies: Array[Node2D] = []
这种类型化数组在编译时就会检查元素类型,编辑器补全也更准确。 不过要注意:类型化数组不能存放不兼容的类型,比如往 Array[int] 里塞字符串会直接报错。另外,类型化数组也不能存 null,如果可能为空,得用 Array[Node2D] 但元素为 null 时可能会警告或报错,具体要看版本。实际使用中,我通常用 Array[Node] 来存节点引用,并确保不会塞 null 进去,或者用 Array 普通数组。
字典的类型化写法类似,但 GDScript 4 对字典类型化的支持还不够完善,字典本身只能声明为 Dictionary,无法限制键和值的类型。这是和数组不同的地方,需要留意。
3. 函数参数和返回值
gdscript
func add(a: int, b: int) -> int:
return a + b
func get_player_name() -> String:
return player_name
func apply_damage(amount: float) -> void:
health -= amount
如果函数没有返回值,用 -> void。虽然不写也能运行,但写上后编辑器能帮你检查是否意外返回了值。
4. 变量声明时类型推断
即使不写类型注解,Godot 也会尽力推断。但要注意:如果变量声明时没有赋初值,类型就是 Variant。比如:
gdscript
var a # Variant
var b = 10 # int
var c := 10 # 使用 := 明确推断,等价于 var c = 10
:= 是"推断类型"的语法,和 = 类似,但强调类型由右侧决定,右侧是什么类型,变量就固定为什么类型。不过实际差别不大,我很少用 :=,习惯直接写 =。
三、类型检查:is 和 typeof()
1. is 判断对象类型
is 用于判断一个对象是否属于某个类或其子类。这是我们在代码中最常用的类型检查方式。
gdscript
var node = get_node("Player")
if node is Player:
node.take_damage(10)
is 对内置类型也可以:
gdscript
var value = 10
if value is int:
print("是整数")
注意:is 用于对象类型检查时,是基于继承关系的,子类对象也会通过父类的 is 检查。
2. typeof() 获取具体类型
typeof() 函数返回一个整数枚举,表示变量的具体类型。这个枚举定义在 @GlobalScope 中,比如 TYPE_INT、TYPE_STRING 等。
gdscript
var value = 10
if typeof(value) == TYPE_INT:
print("是整数")
typeof() 和 is 的区别:is 检查对象是否是指定类型或其子类,typeof() 只返回精确的类型枚举,不包含继承关系。
在实际开发中,is 更常用,因为多态判断更方便。typeof() 偶尔用于需要精确区分类型的时候,比如区分 int 和 float。
3. get_class() 获取类名
get_class() 返回对象类名的字符串(内置类)或自定义类的脚本路径。但要注意,对于自定义类,get_class() 返回的是脚本资源路径,而不是 class_name。要获取 class_name 的字符串,可以用 get_script().get_global_name(),不过不常用。
四、类型转换
类型转换在 GDScript 里有几种方式,各有适用场景。
1. 基础类型转换函数
对于 int、float、String 等基础类型,使用内置的转换函数:
gdscript
var text = "123"
var num = int(text) # 123
var pi = float("3.14") # 3.14
var s = str(123) # "123"
如果转换失败会报错并返回 null。所以在转换用户输入时最好先判断,比如用 text.is_valid_int()。
2. 对象类型转换:as 与强制转换
as 是安全转换,转换失败返回 null:
gdscript
var node = get_node("Player")
var player = node as Player
if player != null:
player.take_damage(10)
强制转换直接写目标类型,失败会报错:
gdscript
var player: Player = node # 如果 node 不是 Player 会报错
我基本都用 as 然后判空。
3. 基础类型之间的显式转换
例如把 float 转 int 用 int(),把 int 转 float 用 float()。注意 int(3.9) 是 3,不是四舍五入,而是截断。要四舍五入用 round()。
五、GDScript 的泛型限制与替代方案
GDScript 目前不支持真正的泛型编程。 你不能写一个像 C# 那样的 List<T> 或者自定义泛型类。但类型化数组提供了一部分泛型能力,你可以在 Array 后面加类型参数。
如果你需要更通用的容器,可以用 Array 普通数组或 Dictionary。但如果想要类型安全,通常的做法是:
- 使用类型化数组
Array[T]。 - 自定义基类,让不同类型的对象都继承自同一个基类,然后用基类类型数组。
- 使用
Variant类型,放弃类型检查,在需要时用is判断后转换。
我个人通常能用基类解决的问题就用基类,毕竟 Godot 本身就是面向节点的,多态用起来很自然。
六、特殊类型:Variant、Callable、Signal
GDScript 中有一些特殊的类型值得了解:
- Variant :所有类型的基类型。没有类型注解的变量就是
Variant。你可以显式声明var x: Variant = 10,表示这个变量可以放任何类型。 - Callable:函数引用类型,可以存 Lambda 或具名函数引用。
- Signal:信号类型,可以作为参数传递或存储,不过一般直接连接使用。
- NodePath :节点路径类型,用于引用场景树中的节点,通常用
$或get_node()获取。
这些类型在特定场合有用,但不必一开始就深究。
七、实战:把类型系统用起来
假设我们写一个道具系统,需要存储不同类型的道具,每种道具有不同的效果。我们可以这样设计:
gdscript
class_name Item
extends Resource
var item_name: String
var item_type: String
func use(player: Player) -> void:
pass
然后定义子类:
gdscript
class_name HealingItem
extends Item
var heal_amount: int = 10
func use(player: Player) -> void:
player.heal(heal_amount)
gdscript
class_name SpeedBoostItem
extends Item
var speed_multiplier: float = 1.5
var duration: float = 5.0
func use(player: Player) -> void:
player.apply_speed_boost(speed_multiplier, duration)
这样我们就可以用基类数组来存所有道具,并且安全地调用 use():
gdscript
var inventory: Array[Item] = []
func add_item(item: Item):
inventory.append(item)
func use_item(index: int, player: Player):
if index >= 0 and index < inventory.size():
inventory[index].use(player)
类型化数组 Array[Item] 确保只有 Item 及其子类能放入,调用 use() 时编辑器也知道这是个 Item,能给出补全。这就是类型系统的优势。
八、总结
GDScript 的类型系统在 Godot 4 中已经比较完善了,虽然没有泛型,但通过类型注解、类型化数组、继承多态,我们可以获得不错的类型安全性。我的建议是:
- 成员变量、函数参数和返回值尽量写类型注解。
- 数组尽量用类型化数组,特别是存同一类型对象时。
- 类型检查用
is,安全转换用as。 - 利用基类和继承实现类似泛型的效果。
这样写出来的代码更健壮,编辑器也更友好。下一篇我们进入字符串操作,看看 GDScript 里处理文本有哪些实用的方法。