Статическая типизация в GDScript

В этом руководстве вы узнаете:

  • как использовать статическую типизацию в GDScript;

  • эти статические типы могут помочь вам избежать ошибки;

  • что статическая типизация улучшает работу с редактором.

Где и как использовать эту языковую функцию, решать только вам: вы можете использовать ее только в некоторых конфиденциальных файлах GDScript, использовать ее везде или не использовать ее вообще.

Статические типы могут использоваться для переменных, констант, функций, параметров и возвращаемых типов.

Краткий обзор статической типизации

Благодаря статической типизации GDScript может обнаруживать больше ошибок, даже не запуская код. Кроме того, подсказки типов предоставляют вам и вашим коллегам больше информации в процессе работы, поскольку типы аргументов отображаются при вызове метода. Статическая типизация улучшает автодополнение в редакторе и documentation ваших скриптов.

Представьте, что вы программируете систему управления инвентарём. Вы пишете класс Item, а затем Inventory. Чтобы добавить предметы в инвентарь, разработчики вашего кода должны всегда передавать Item методу Inventory.add(). С помощью типов вы можете реализовать это:

class_name Inventory


func add(reference: Item, amount: int = 1):
    var item := find_item(reference)
    if not item:
        item = _instance_item_from_db(reference)
    item.amount += amount

Статические типы также предоставляют улучшенные возможности автодополнения кода. Ниже показана разница между динамическими и статическими типизированными вариантами автодополнения.

Вероятно, вы сталкивались с отсутствием подсказок автодополнения после точки:

Варианты завершения для динамически типизированного кода.

Это происходит из-за динамического кода. Godot не может знать, какой тип значения вы передаёте функции. Однако, если вы укажете тип явно, вы получите все методы, свойства, константы и т. д. из значения:

Варианты завершения для статического типизированного кода.

Совет

Если вы предпочитаете статическую типизацию, рекомендуем включить настройку редактора Text Editor > Completion > Add Type Hints. Также рассмотрите возможность включить некоторые предупреждения, которые отключены по умолчанию.

Кроме того, типизированный GDScript повышает производительность за счёт использования оптимизированных опкодов, когда типы операндов/аргументов известны во время компиляции. В будущем планируется реализовать дополнительные оптимизации GDScript, такие как JIT/AOT-компиляция.

В целом, типизированное программирование дает вам более структурированный опыт. Это помогает предотвратить ошибки и улучшает аспект самодокументирования ваших скриптов. Это особенно полезно, когда вы работаете в команде или над долгосрочным проектом: исследования показали, что разработчики проводят большую часть своего времени за чтением чужого кода или скриптов, которые они написали в прошлом и о которых забыли. Чем яснее и структурированнее код, тем быстрее его понять, тем быстрее вы сможете двигаться вперед.

Как использовать статическую типизацию

Чтобы определить тип переменной, параметра или константы, поставьте двоеточие после имени, а затем укажите тип. Например, var health: int. Это гарантирует, что тип переменной всегда останется неизменным:

var damage: float = 10.5
const MOVE_SPEED: float = 50.0
func sum(a: float = 0.0, b: float = 0.0) -> float:
    return a + b

Godot попытается вывести типы, если вы напишете двоеточие, но опустите тип:

var damage := 10.5
const MOVE_SPEED := 50.0
func sum(a := 0.0, b := 0.0) -> float:
    return a + b

Примечание

  1. Для констант нет разницы между = и :=.

  2. Вам не нужно указывать тип констант, поскольку Godot автоматически устанавливает его на основе присвоенного значения. Но вы всё равно можете сделать это, чтобы сделать код более понятным. Кроме того, это полезно для типизированных массивов (например, const A: Array[int] = [1, 2, 3]), поскольку по умолчанию используются нетипизированные массивы.

Что может быть подсказкой типа

Вот полный список того, что можно использовать в качестве подсказки типа:

  1. Variant. Любой тип. В большинстве случаев это не сильно отличается от нетипизированного объявления, но повышает читаемость. Как возвращаемый тип, заставляет функцию явно возвращать некоторое значение.

  2. (Только возвращаемый тип) void. Указывает, что функция не возвращает никакого значения.

  3. Встроенные типы.

  4. Родные классы (Object, Node, Area2D, Camera2D, и т.д.).

  5. Global classes.

  6. Inner classes.

  7. Глобальные, собственные и пользовательские именованные перечисления. Обратите внимание, что тип перечисления — это просто int, нет гарантии, что значение принадлежит множеству значений перечисления.

  8. Константы (включая локальные), если они содержат предварительно загруженный класс или перечисление.

В качестве типов можно использовать любой класс, включая ваши собственные. Существует два способа использования их в скриптах. Первый способ — предварительно загрузить скрипт, который вы хотите использовать в качестве типа, в константу:

const Rifle = preload("res://player/weapons/rifle.gd")
var my_rifle: Rifle

The second method is to use the class_name keyword when you create the script. For the example above, your rifle.gd would look like this:

class_name Rifle
extends Node2D

Если вы используете class_name, Godot регистрирует тип Rifle глобально в редакторе, и вы можете использовать его где угодно, без необходимости предварительной загрузки его в константу:

var my_rifle: Rifle

Укажите тип возвращаемого значения функции с помощью стрелки ->

Чтобы определить тип возвращаемого значения функции, напишите тире и правую угловую скобку -> после ее объявления, а затем укажите тип возвращаемого значения:

func _process(delta: float) -> void:
    pass

Тип void означает, что функция ничего не возвращает. Вы можете использовать любой тип, как и для переменных:

func hit(damage: float) -> bool:
    health_points -= damage
    return health_points <= 0

Вы также можете использовать собственные классы в качестве возвращаемых типов:

# Adds an item to the inventory and returns it.
func add(reference: Item, amount: int) -> Item:
    var item: Item = find_item(reference)
    if not item:
        item = ItemDatabase.get_instance(reference)

    item.amount += amount
    return item

Ковариация и контравариация (Covariance and contravariance)

При наследовании методов базового класса следует следовать принципу подстановки Лисков.

Covariance (Ковариантность): При наследовании метода можно указать тип возвращаемого значения, который является более конкретным (subtype (подтип)), чем родительский метод.

Contravariance (Контравариантность): при наследовании метода можно указать тип параметра, который менее специфичен (supertype (супертип)), чем родительский метод.

Пример:

class_name Parent


func get_property(param: Label) -> Node:
    # ...
class_name Child extends Parent


# `Control` is a supertype of `Label`.
# `Node2D` is a subtype of `Node`.
func get_property(param: Control) -> Node2D:
    # ...

Укажите тип элемента Array

Чтобы определить тип Array, заключите имя типа в [].

Тип массива применяется к переменным цикла for, а также к некоторым операторам, таким как [], [...] = (присваивание) и +. Методы массивов (например, push_back) и другие операторы (например, ==) по-прежнему нетипизированы. В качестве типов элементов могут использоваться встроенные типы, собственные и пользовательские классы, а также перечисления. Вложенные типы массивов (например, Array[Array[int]]) не поддерживаются.

var scores: Array[int] = [10, 20, 30]
var vehicles: Array[Node] = [$Car, $Plane]
var items: Array[Item] = [Item.new()]
var array_of_arrays: Array[Array] = [[], []]
# var arrays: Array[Array[int]] -- disallowed

for score in scores:
    # score has type `int`

# The following would be errors:
scores += vehicles
var s: String = scores[0]
scores[0] = "lots"

Начиная с Godot 4.2, вы также можете указать тип переменной цикла в цикле for. Например, можно написать:

var names = ["John", "Marta", "Samantha", "Jimmy"]
for name: String in names:
    pass

Массив останется нетипизированным, но переменная name внутри цикла for всегда будет иметь тип String.

Укажите тип элемента Dictionary

Чтобы определить тип ключей и значений Dictionary, заключите имя типа в [] и разделите ключ и тип значения запятой.

Тип значения словаря применяется к переменным цикла for, а также к некоторым операторам, таким как [] и [...] = (присваивание). Методы словаря, возвращающие значения, и другие операторы (например, ==) по-прежнему нетипизированы. В качестве типов элементов могут использоваться встроенные типы, собственные и пользовательские классы, а также перечисления. Вложенные типизированные коллекции (например, Dictionary[String, Dictionary[String, int]]) не поддерживаются.

var fruit_costs: Dictionary[String, int] = { "apple": 5, "orange": 10 }
var vehicles: Dictionary[String, Node] = { "car": $Car, "plane": $Plane }
var item_tiles: Dictionary[Vector2i, Item] = { Vector2i(0, 0): Item.new(), Vector2i(0, 1): Item.new() }
var dictionary_of_dictionaries: Dictionary[String, Dictionary] = { { } }
# var dicts: Dictionary[String, Dictionary[String, int]] -- disallowed

for cost in fruit_costs:
    # cost has type `int`

# The following would be errors:
fruit_costs["pear"] += vehicles
var s: String = fruit_costs["apple"]
fruit_costs["orange"] = "lots"

Приведение типа

Приведение типов — важная концепция в типизированных языках. Приведение типов — это преобразование значения из одного типа в другой.

Представьте себе Enemy в вашей игре, который расширяет Area2D. Вы хотите, чтобы он столкнулся с Игроком, объектом CharacterBody2D, к которому прикреплён скрипт PlayerController. Для обнаружения столкновения используется сигнал body_entered. При использовании типизированного кода обнаруженным телом будет обобщённый PhysicsBody2D, а не ваш PlayerController в обратном вызове _on_body_entered.

Вы можете проверить, является ли этот PhysicsBody2D вашим Player, с помощью ключевого слова as и двоеточия :, чтобы принудительно использовать этот тип переменной. Это привязывает переменную к типу PlayerController:

func _on_body_entered(body: PhysicsBody2D) -> void:
    var player := body as PlayerController
    if not player:
        return

    player.damage()

Поскольку мы имеем дело с пользовательским типом, если body не расширяет PlayerController, переменная player будет установлена в null. Мы можем использовать это для проверки, является ли body игроком. Благодаря этому приведению мы также получим полное автодополнение для переменной player.

Примечание

Ключевое слово as автоматически приводит переменную к null в случае несоответствия типов во время выполнения, без выдачи ошибки/предупреждения. Хотя в некоторых случаях это может быть удобно, это также может привести к ошибкам. Используйте ключевое слово as только если это предусмотрено. Более безопасной альтернативой является использование ключевого слова is:

if not (body is PlayerController):
    push_error("Bug: body is not PlayerController.")

var player: PlayerController = body
if not player:
    return

player.damage()

Вы также можете упростить код, используя оператор is not:

if body is not PlayerController:
    push_error("Bug: body is not PlayerController")

В качестве альтернативы вы можете использовать оператор assert():

assert(body is PlayerController, "Bug: body is not PlayerController.")

var player: PlayerController = body
if not player:
    return

player.damage()

Примечание

Если вы попытаетесь выполнить приведение со встроенным типом, и это не удастся, Godot выдаст ошибку.

Безопасные строки

Вы также можете использовать приведение типов для обеспечения безопасности строк. Безопасные строки — это инструмент, который определяет, являются ли неоднозначные строки кода типобезопасными. Поскольку типизированный и динамический код могут комбинироваться, Godot иногда не имеет достаточной информации, чтобы определить, вызовет ли инструкция ошибку во время выполнения.

Это происходит, когда вы получаете дочерний узел. Возьмем, к примеру, таймер: с динамическим кодом вы можете получить узел с помощью $Timer. GDScript поддерживает Утиную типизацию, поэтому даже если ваш таймер имеет тип Timer, он также является Node и Object, два класса которые он расширяет. В динамическом GDScript вы также не заботитесь о типе узла, если у него есть необходимые вам методы для вызова.

Вы можете использовать приведение типов, чтобы сообщить Godot, какой тип вы ожидаете при получении узла: ($Timer as Timer), ($Player as CharacterBody2D) и т. д. Godot проверит, работает ли тип, и если это так, номер строки в левой части редактора скриптов станет зеленым.

Небезопасная и Безопасная Линия

Небезопасная строка (строка 7) против Безопасных Строк (строки 6 и 8)

Примечание

Безопасные строки не всегда означают лучший или более надёжный код. См. примечание выше о ключевом слове as. Например:

@onready var node_1 := $Node1 as Type1 # Safe line.
@onready var node_2: Type2 = $Node2 # Unsafe line.

Несмотря на то, что объявление node_2 отмечено как небезопасное, оно надёжнее объявления node_1. Если вы измените тип узла в сцене и случайно забудете изменить его в скрипте, ошибка будет обнаружена сразу при загрузке сцены. В отличие от node_1, которое будет автоматически преобразовано в null, и ошибка будет обнаружена позже.

Примечание

Вы можете отключить безопасные строки или изменить их цвет в настройках редактора.

Типизированный или динамичный: придерживайтесь одного стиля

Типизированный GDScript и динамический GDScript могут сосуществовать в одном проекте. Но рекомендуется придерживаться единого стиля написания вашего кода. Всем будет проще понимать код, если все будет использоваться единый стиль написания.

Типизированный код требует немного больше времени на написание, но вы получаете те же преимущества, о которых мы говорили выше. Вот пример того же пустого скрипта в динамическом стиле:

extends Node


func _ready():
    pass


func _process(delta):
    pass

И со статической типизацией:

extends Node


func _ready() -> void:
    pass


func _process(delta: float) -> void:
    pass

Как видите, вы также можете использовать типы с виртуальными методами движка. Обратные вызовы сигналов, как и любые методы, также могут использовать типы. Вот сигнал body_entered в динамическом стиле:

func _on_area_2d_body_entered(body):
    pass

И тот же обратный вызов, с подсказками типов:

func _on_area_2d_body_entered(body: PhysicsBody2D) -> void:
    pass

Система предупреждений

Примечание

Подробная документация о системе предупреждений GDScript перенесена в Система предупреждений GDScript.

Godot выдаёт предупреждения о вашем коде по мере его написания. Движок выявляет фрагменты кода, которые могут привести к проблемам во время выполнения, но позволяет вам решить, оставить код как есть или нет.

У нас есть ряд предупреждений, предназначенных специально для пользователей типизированного GDScript. По умолчанию эти предупреждения отключены. Вы можете включить их в настройках проекта (Debug > GDScript, убедитесь, что опция Advanced Settings включена).

Вы можете включить предупреждение UNTYPED_DECLARATION, если хотите всегда использовать статические типы. Кроме того, вы можете включить предупреждение INFERRED_DECLARATION, если предпочитаете более читабельный и надёжный, но более многословный синтаксис.

Предупреждения UNSAFE_* делают небезопасные операции более заметными, чем небезопасные линии. В настоящее время предупреждения UNSAFE_* не охватывают все случаи, которые покрывают небезопасные линии.

Распространенные небезопасные операции и их безопасные аналоги

Global scope methods

The following global scope methods are not statically typed, but they have typed counterparts available. These methods return statically typed values:

Method

Statically typed equivalents

abs()

ceil()

clamp()

floor()

lerp()

round()

sign()

snapped()

When using static typing, use the typed global scope methods whenever possible. This ensures you have safe lines and benefit from typed instructions for better performance.

Предупреждения UNSAFE_PROPERTY_ACCESS и UNSAFE_METHOD_ACCESS

В этом примере мы хотим задать свойство и вызвать метод объекта, к которому прикреплён скрипт с именем class_name MyScript, и который extends Node2D. Если у нас есть ссылка на объект как Node2D (например, переданная нам системой физики), мы можем сначала проверить, существуют ли свойство и метод, а затем задать и вызвать их, если они есть:

if "some_property" in node_2d:
    node_2d.some_property = 20  # Produces UNSAFE_PROPERTY_ACCESS warning.

if node_2d.has_method("some_function"):
    node_2d.some_function()  # Produces UNSAFE_METHOD_ACCESS warning.

Однако этот код выдаст предупреждения UNSAFE_PROPERTY_ACCESS и UNSAFE_METHOD_ACCESS, поскольку свойство и метод отсутствуют в указанном типе — в данном случае Node2D. Чтобы сделать эти операции безопасными, можно сначала проверить, принадлежит ли объект типу MyScript, с помощью ключевого слова is, а затем объявить переменную типа MyScript, для которой можно задать её свойства и вызвать её методы:

if node_2d is MyScript:
    var my_script: MyScript = node_2d
    my_script.some_property = 20
    my_script.some_function()

В качестве альтернативы вы можете объявить переменную и использовать оператор as, чтобы попытаться привести объект к типу. Затем вам нужно будет проверить, успешно ли произошло приведение, убедившись, что переменная была назначена:

var my_script := node_2d as MyScript
if my_script != null:
    my_script.some_property = 20
    my_script.some_function()

Предупреждение UNSAFE_CAST

В этом примере мы хотим, чтобы метка, прикреплённая к объекту, входящему в область столкновения, отображала название этой области. Как только объект попадает в область столкновения, физическая система отправляет сигнал с объектом Node2D, и наиболее простое (но не статически типизированное) решение для достижения нашей цели можно реализовать следующим образом:

func _on_body_entered(body: Node2D) -> void:
    body.label.text = name  # Produces UNSAFE_PROPERTY_ACCESS warning.

Этот фрагмент кода выдаёт предупреждение UNSAFE_PROPERTY_ACCESS, поскольку label не определено в Node2D. Чтобы решить эту проблему, можно сначала проверить, существует ли свойство label, и привести его к типу Label, прежде чем задавать его свойство text, например:

func _on_body_entered(body: Node2D) -> void:
    if "label" in body:
        (body.label as Label).text = name  # Produces UNSAFE_CAST warning.

Однако это приводит к предупреждению UNSAFE_CAST, поскольку body.label имеет тип Variant. Чтобы безопасно получить свойство нужного типа, можно использовать метод Object.get(), который возвращает объект как значение Variant или null, если свойство не существует. Затем можно определить, содержит ли свойство объект нужного типа, с помощью ключевого слова is, и, наконец, объявить статически типизированную переменную с этим объектом:

func _on_body_entered(body: Node2D) -> void:
    var label_variant: Variant = body.get("label")
    if label_variant is Label:
        var label: Label = label_variant
        label.text = name

Случаи, когда невозможно указать типы

В заключение этого введения рассмотрим случаи, когда использовать подсказки типов нельзя. Это приведёт к синтаксической ошибке.

  1. Вы не можете указать тип отдельных элементов в массиве или словаре:

var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy]
var character: Dictionary = {
    name: String = "Richard",
    money: int = 1000,
    inventory: Inventory = $Inventory,
}
  1. Вложенные типы в настоящее время не поддерживаются:

var teams: Array[Array[Character]] = []

Подведение итогов

Типизированный GDScript — мощный инструмент. Он помогает писать более структурированный код, избегать распространённых ошибок и создавать масштабируемые и надёжные системы. Статические типы повышают производительность GDScript, и в будущем планируется дальнейшая оптимизация.