GDScript 入门笔记(十二):类型系统进阶

前面在变量那篇里简单提过 GDScript 的类型系统,但没深入。用了一段时间后发现,类型系统如果掌握得好,写代码时能少踩很多坑,也能让编辑器更智能。这篇笔记就专门聊一聊 GDScript 类型系统的进阶用法,包括完整的类型注解写法、类型转换、类型检查,以及 GDScript 没有泛型时的替代思路。这些都是我在实际写项目时慢慢摸出来的经验,分享出来供参考。

一、GDScript 的类型哲学:动态为主,静态可选

GDScript 默认是动态类型的。你写 var a = 10a 的类型是 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_INTTYPE_STRING 等。

gdscript 复制代码
var value = 10
if typeof(value) == TYPE_INT:
    print("是整数")

typeof()is 的区别:is 检查对象是否是指定类型或其子类,typeof() 只返回精确的类型枚举,不包含继承关系。

在实际开发中,is 更常用,因为多态判断更方便。typeof() 偶尔用于需要精确区分类型的时候,比如区分 intfloat

3. get_class() 获取类名

get_class() 返回对象类名的字符串(内置类)或自定义类的脚本路径。但要注意,对于自定义类,get_class() 返回的是脚本资源路径,而不是 class_name。要获取 class_name 的字符串,可以用 get_script().get_global_name(),不过不常用。

四、类型转换

类型转换在 GDScript 里有几种方式,各有适用场景。

1. 基础类型转换函数

对于 intfloatString 等基础类型,使用内置的转换函数:

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. 基础类型之间的显式转换

例如把 floatintint(),把 intfloatfloat()。注意 int(3.9) 是 3,不是四舍五入,而是截断。要四舍五入用 round()

五、GDScript 的泛型限制与替代方案

GDScript 目前不支持真正的泛型编程。 你不能写一个像 C# 那样的 List<T> 或者自定义泛型类。但类型化数组提供了一部分泛型能力,你可以在 Array 后面加类型参数。

如果你需要更通用的容器,可以用 Array 普通数组或 Dictionary。但如果想要类型安全,通常的做法是:

  1. 使用类型化数组 Array[T]
  2. 自定义基类,让不同类型的对象都继承自同一个基类,然后用基类类型数组。
  3. 使用 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 里处理文本有哪些实用的方法。

相关推荐
LuminousCPP2 小时前
数据结构-排序(一):基础排序算法横向对比|冒泡、插入、希尔、堆与双向选择,详解二分 / Knuth 增量
c语言·数据结构·笔记·算法·排序算法
柳鲲鹏3 小时前
股票打新,中签为什么难?
笔记
亮工硬件3 小时前
1-8 简易串口通讯协议(UART + 自定义帧协议)
笔记·stm32·单片机·嵌入式硬件·学习
志尊宝3 小时前
Vue3 零基础每日笔记(004):模板语法初识——插值表达式、v-bind、v-on 一次学会
javascript·vue.js·笔记
SatanII4 小时前
OpenStack核心组件详解|Keystone+Glance+Nova+Cinder原理与实操笔记
网络·笔记·openstack
点心的游戏开发世界13 小时前
GDScript 入门笔记(十一):Lambda 与匿名函数
开发语言·笔记·游戏引擎·godot
摇滚侠13 小时前
《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 使用 JdbcTemplate 阅读笔记 32
spring boot·笔记·后端
鱼子星_15 小时前
【C++】继承和多态(上)
c++·笔记
xian_wwq15 小时前
【学习笔记】深度认知系列-第14讲 端侧AI崛起——为什么AI正在从云端走向本地
人工智能·笔记·学习