前置知识: GDScript

风格指南与静态类型实践

9 min入门

按官方风格指南统一命名与代码顺序,用静态类型获得更快执行与更早的错误发现

010 篇讲过:GDScript 是渐进类型语言,编译期已知类型的操作会使用优化后的 opcode,执行更快。本篇把这个收益落地成可执行的实践:什么场合该写类型标注、怎么写才正确。另一条主线是官方风格指南(style guide):命名怎么定、成员按什么顺序排、格式遵循哪些规则。风格指南的目标很朴素——让同一个项目的代码读起来像一个人写的。

两件事看似不相关,其实互相支撑:统一的类型标注与统一的代码结构,都会让”读别人的代码”这件事变得容易。建议对照自己正在写的项目边读边改。

学习目标

  • 说出静态类型的两大收益,并按官方最佳实践书写类型标注;
  • 理解安全行(safe line)的概念及其局限;
  • 按官方命名规范命名文件、类、函数、变量、信号、常量与枚举;
  • 按官方模板组织脚本成员顺序,并理解背后的三条原则;
  • 应用格式规则:缩进、行宽、引号、浮点写法、运算符选择等;
  • 在项目设置中配置警告系统,并用 @warning_ignore 精准忽略;
  • 编写进入脚本文档的文档注释。

静态类型的收益

在写具体的最佳实践之前,先明确为什么要写类型标注,两大理由:

  1. 执行更快:编译期已知类型的操作会使用优化后的 opcode(操作码),静态类型的代码不但不慢,反而更快;
  2. 更早发现错误:类型错误在解析期就报错,而不是等到运行时才崩溃——错误暴露得越早,修复成本越低。

此外类型标注本身就是最可靠的文档:var speed: float 比 var speed 传达的意图清晰得多。

静态类型最佳实践

官方静态类型教程给出一组明确的正反例:

# 好:值类型显式标注
var health: int = 0

# 好:右侧是有定义类型的表达式,用 := 推断
var direction := Vector3(1, 2, 3)

推断很方便,但有三类情况要特别小心:

第一,冗余标注是坏例子。右侧表达式已经有明确类型时,再手写一遍类型属于冗余,徒增噪音。

第二,get_node 这类返回值类型不具体的调用必须显式标注:

# 显式标注类型
@onready var health_bar: ProgressBar = get_node("UI/LifeBar")
# 或用 as 转换
@onready var health_bar := get_node("UI/LifeBar") as ProgressBar

两种写法各有取舍。显式标注更”空安全”:场景里节点类型不对,解析期直接报错。as 更类型安全但欠空安全:转换失配不会报错,而是静默得到 null,问题被推迟到第一次使用这个变量时才爆发。

第三,歧义推断要显式标注。var health := 0 推断为 int;如果你本意是 float,就应显式写 var health: float = 0,否则后续的小数运算会悄悄出错。

最后一条是工程纪律:Typed(静态)与动态两种风格不要混用,项目内保持一致。写原型时可以动态,定稿后统一补类型,不要让两种风格在同一个文件里反复切换。

安全行:绿色不等于可靠

脚本编辑器会把”类型可证”的代码行显示为绿色,称为安全行(safe line)。绿色是很好的即时反馈,但要记住:安全行不等于更可靠的行。官方给过一个经典反例:

@onready var node := $Node1 as Type1

这一行是安全行——as 转换”总能成功”,失败也只会得到 null,解析器确实可以证明它不出类型错误。但如果你后来在场景里把 Node1 的类型改了,这行会静默得到 null,错误一直潜伏到运行时使用 node 的那一刻。

反倒是显式类型标注的写法更早暴露问题:

@onready var node: Type1 = $Node1

类型失配会在解析期直接报错,而不是静默 null。结论:绿色提示值得追求,但显式标注比 as 更能提前暴露错误,别把”绿色”当成”正确”的同义词。

命名规范

官方风格指南的命名规范一表看完:

对象约定示例
文件名snake_caseyaml_parser.gd
类名 / 节点名PascalCaseclass_name YAMLParser
函数名 / 变量名snake_casefunc load_level():
信号名过去时 snake_casesignal door_opened、signal score_changed
常量 / 枚举成员CONSTANT_CASEconst MAX_SPEED = 200
枚举名PascalCase 单数enum Element
私有成员前置下划线var _counter = 0

几条约定背后的理由值得体会:信号用过去时,因为它广播的是”已经发生的事件”——door_opened(门开了)、score_changed(分数变了)都比动词原形读起来自然;枚举名单数(Element 而不是 Elements),因为枚举值描述的是单个种类;前置下划线只是社区约定,GDScript 语言层面没有强制私有,它靠约定与工具(如脚本文档不再显示私有成员)共同生效。

代码顺序

官方模板规定了脚本成员的书写顺序:

  1. @tool / @icon 注解;
  2. class_name;
  3. extends;
  4. 文档注释;
  5. signal 信号;
  6. enum 枚举;
  7. const 常量;
  8. static 变量;
  9. @export 变量;
  10. 普通 var 变量;
  11. @onready 变量;
  12. _static_init();
  13. static 方法;
  14. 虚方法(_init、_enter_tree、_ready、_process、_physics_process);
  15. 重写的其他方法;
  16. 其余方法;
  17. 内部类。

从这份顺序可以读出三条原则:属性与信号排在最前——先声明数据与事件,再写行为;公开成员先于私有成员——读代码的人最先看到对外接口;虚回调先于类自身的其他方法——生命周期入口一目了然。

一个符合规范的片段:

signal player_spawned(position)

enum Job {
    KNIGHT,
    WIZARD,
}

const MAX_LIVES = 3

@export var max_health = 50

var _speed = 300.0

@onready var sword = get_node("Sword")

注意枚举最后一个成员后面的逗号——这正是下一节格式规则的要求。

格式规则

官方风格指南的格式约定汇总如下:

  • 缩进使用 Tab(编辑器默认),空格与 Tab 不能混用;
  • 一行一条语句,唯一的例外是三元表达式可以留在行内;
  • 函数与类之间空两行;
  • 行宽小于 100 字符,理想控制在 80 以内;
  • 多行集合(数组、字典、枚举)末尾加逗号;
  • 运算符优先使用英文单词 and、or、not,而不是符号形式的 &&、|| 与 !;
  • 字符串默认使用双引号;
  • 浮点数不省略首尾的零:写 0.234 而不是 .234,写 13.0 而不是 13.;
  • 避免多余括号。
# 三元表达式:一行一条语句的唯一例外
y += 3 if y < 10 else -1

# 浮点写全首尾的零
var rate := 0.234
var duration := 13.0

这些规则大多能被编辑器自动维护(如 Tab 缩进),养成习惯后基本不再需要刻意思考。

警告系统

GDScript 为可疑代码提供了一套警告系统。配置入口在项目设置的 GDScript 区(需要先在设置界面启用”高级设置”才能看到)。每一类警告都可以单独设为 Ignore、Warn 或 Error——设为 Error 后,出现该警告的脚本将无法编译。团队项目常把几类高频警告直接升级为错误,让问题在编译阶段就被拦下。

某个位置确实不想处理警告时,可以用注解精准忽略:

@warning_ignore("unused_variable")
var unused = 5

几个值得认识的警告:

  • ONREADY_WITH_EXPORT:@onready 与 @export 同时用于一个变量是冲突用法,默认按错误处理;
  • CONFUSABLE_CAPTURE_REASSIGNMENT:lambda 按值捕获一次局部变量,捕获之后再重新赋值原变量会产生”改了值却没生效”的混淆,该警告专门提示这类写法;
  • unused_variable:声明了却没使用的变量,最常见也最该随手清理。

文档注释

双井号 ## 写在成员正上方就是文档注释,内容会进入脚本文档;@export 变量的说明还会显示在检查器提示里。它支持三个特殊标记:

  • @tutorial: URL:给成员挂一个教程链接;
  • @deprecated:标记已弃用的成员;
  • @experimental:标记实验性的成员。

文档注释支持 BBCode 排版:[b] 加粗、[code] 行内代码、[codeblock] 代码块;还可以直接链接引擎成员:[member Node2D.scale]、[method Node2D.look_at]、[signal SceneTree.process_frame];用 [param a] 引用当前成员的参数 a,用 [br] 换行。

## 玩家的一件装备。
## @tutorial: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html
## @experimental: 附魔系统尚未定型。
class_name Gear

## 装备提供的防御力。
@export var defense: int = 0

## 内部调试计数,不会出现在脚本文档中。
var _debug_hits: int = 0

最后一条规则与命名规范呼应:以下划线开头的成员默认被视为私有,不会显示在脚本文档里。想公开文档,就别给成员加下划线前缀。

小结

  • 静态类型有两大收益:编译期已知类型的操作使用优化后的 opcode 执行更快;类型错误在解析期报错而不是运行时崩溃;
  • 最佳实践:值类型显式标注或 := 推断;冗余标注是坏例子;get_node 必须显式标注类型或用 as;歧义推断(想要 float 却写出 var health := 0)要显式标注;Typed 与动态风格不混用;
  • 安全行是类型可证的绿色代码行,但安全行不等于更可靠:$Node1 as Type1 是安全行却可能静默 null,显式类型标注反而更早暴露错误;
  • 命名:文件 snake_case、类与节点 PascalCase、函数与变量 snake_case、信号过去时 snake_case、常量与枚举成员 CONSTANT_CASE、枚举名 PascalCase 单数、私有成员前置下划线;
  • 代码顺序遵循官方模板:注解、class_name、extends、文档注释、signal、enum、const、static 变量、@export、普通 var、@onready、_static_init、static 方法、虚方法、重写方法、其余方法、内部类;
  • 格式:Tab 缩进、一行一条语句(三元表达式例外)、函数与类之间空两行、行宽小于 100(理想 80)、多行集合末尾逗号、优先 and/or/not、字符串默认双引号、浮点写全首尾零、避免多余括号;
  • 警告系统在项目设置 GDScript 区配置,任一警告可设为 Error 使编译失败;单处忽略用 @warning_ignore;
  • 文档注释用 ##,支持 @tutorial、@deprecated、@experimental 与 BBCode;下划线开头的成员默认视为私有,不进入脚本文档。

参考链接