Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Статична типізація в 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 не може знати, який тип значення ви передаєте функції. Однак якщо ви напишете тип явно, ви отримаєте всі методи, властивості, константи тощо зі значення:

Параметри завершення для статичного коду.

Порада

Якщо ви надаєте перевагу статичній типізації, радимо ввімкнути параметр редактора Текстовий редактор > Автозаповнення > Додати підказки щодо типу. Також розгляньте можливість увімкнення параметра деякі попередження які вимкнено за замовчуванням.

Крім того, типізований 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. Варіант. Будь-який тип. У більшості випадків це мало чим відрізняється від нетипової декларації, але покращує читабельність. Як тип повернення, змушує функцію явно повертати певне значення.

  2. (Тільки тип повернення) void. Вказує, що функція не повертає жодного значення.

  3. Вбудовані типи.

  4. Нативні класи (Object, Node, Area2D, Camera2D тощо).

  5. Глобальні класи.

  6. Внутрішні класи.

  7. Глобальні, нативні та власні іменовані переліки. Зауважте, що тип переліку – це лише int, немає гарантії, що значення належить до набору значень переліку.

  8. Константи (включаючи локальні), якщо вони містять попередньо завантажений клас або перелік.

Ви можете використовувати будь-який клас, включаючи ваші власні класи, як типи. Існує два способи їх використання в скриптах. Перший метод полягає в попередньому завантаженні скрипта, який ви хочете використовувати як тип, у константу:

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

Другий метод полягає у використанні ключового слова class_name під час створення скрипта. Для наведеного вище прикладу ваш rifle.gd виглядатиме так:

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

Коваріація та контраваріація

Успадковуючи методи базового класу, слід дотримуватися принципу підстановки Ліскова.

Коваріація: Коли ви успадковуєте метод, ви можете вказати тип повернення, який є більш конкретним (підтип), ніж батьківський метод.

Контраваріантність: Коли ви успадковуєте метод, ви можете вказати тип параметра, який є менш конкретним (супертип), ніж батьківський метод.

Приклад:

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.

Вкажіть тип елемента a 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 fruit in fruit_costs:
    # `fruit` has type `String`

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

Лиття типу

Приведення типів є важливою концепцією в типізованих мовах. Приведення — це перетворення значення з одного типу в інший.

Уявіть Ворога у вашій грі, який розширює Area2D. Ви хочете, щоб він зіткнувся з Player, 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. Ми можемо використовувати це, щоб перевірити, чи є тіло гравцем чи ні. Ми також отримаємо повне автозавершення для змінної гравця завдяки цьому касту.

Примітка

Ключове слово 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 підтримує duck-typing, тому навіть якщо ваш таймер має тип Timer, він також є Node та Object, двома класами, які він розширює. З динамічним GDScript вам також не байдуже на тип вузла, якщо він має методи, які вам потрібно викликати.

Ви можете використовувати кастинг, щоб повідомити Godot тип, який ви очікуєте, коли отримаєте вузол: ($Timer як Timer), ($Player як 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_* не охоплюють усі випадки, які охоплюють небезпечні лінії.

Поширені небезпечні операції та їхні безпечні аналоги

Методи глобальної області застосування

Наведені нижче методи глобальної області видимості не є статично типізованими, але мають типізовані аналоги. Ці методи повертають статично типізовані значення:

Метод

Статично типізовані еквіваленти

abs()

Vector2.abs(), Vector2i.abs()
Vector3.abs(), Vector3i.abs()
Vector4.abs(), Vector4i.abs()

ceil()

Vector2.ceil()
Vector3.ceil()
Vector4.ceil()

clamp()

Vector2.clamp(), Vector2i.clamp()
Vector3.clamp(), Vector3i.clamp()
Vector4.clamp(), Vector4i.clamp()
(Нетипізований clamp() не працює з кольором)

floor()

Vector2.floor()
Vector3.floor()
Vector4.floor()

lerp()

Vector2.lerp()
Vector3.lerp()
Vector4.lerp()
Quaternion.slerp()
Transform2D.interpolate_with()
Transform3D.interpolate_with()

round()

Vector2.round()
Vector3.round()
Vector4.round()

sign()

Vector2.sign(), Vector2i.sign()
Vector3.sign(), Vector3i.sign()
Vector4.sign(), Vector4i.sign()

snapped()

Vector2.snapped(), Vector2i.snapped()
Vector3.snapped(), Vector3i.snapped()
Vector4.snapped(), Vector4i.snapped()

Під час використання статичної типізації використовуйте типізовані методи глобальної області видимості, коли це можливо. Це гарантує безпечні рядки та переваги типізованих інструкцій для кращої продуктивності.

Попередження UNSAFE_PROPERTY_ACCESS і UNSAFE_METHOD_ACCESS

У цьому прикладі ми маємо на меті встановити властивість та викликати метод для об'єкта, до якого приєднаний скрипт з назвою class_name MyScript, і який розширює 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, і на майбутнє планується більше оптимізацій.