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.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

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

Можливо, ви стикалися з відсутністю пропозицій автозаповнення після крапки:

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

Це пов'язано з динамічним кодом. Годо не може знати, який тип значення ви передаєте функції. Однак якщо ви напишете тип явно, ви отримаєте всі методи, властивості, константи тощо зі значення:

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

Порада

Якщо ви віддаєте перевагу статичному введенню, рекомендуємо ввімкнути параметр редактора Текстовий редактор > Завершення > Додати підказки типу. Також варто ввімкнути деякі попередження які вимкнено за замовчуванням.

Крім того, типізований GDScript покращує продуктивність, використовуючи оптимізовані коди операцій, коли типи операндів/аргументів відомі під час компіляції. У майбутньому планується більше оптимізацій GDScript, наприклад компіляція JIT/AOT.

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

Як використовувати статичну типізацію

Щоб визначити тип змінної, параметра або константи, напишіть двокрапку після назви, а потім її тип. наприклад перемінна здоров'я: 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. Вам не потрібно писати підказки типу для констант, оскільки Годо автоматично встановлює їх із призначеного значення. Але ви можете зробити це, щоб зробити зміст вашого коду зрозумілішим. Крім того, це корисно для типізованих масивів (наприклад, const A: Array[int] = [1, 2, 3]), оскільки нетипізовані масиви використовуються за замовчуванням.

Що може бути підказкою типу

Ось повний список того, що можна використовувати як підказку типу:

  1. Варіант. Будь-який тип. У більшості випадків це мало чим відрізняється від нетипової декларації, але покращує читабельність. Як тип повернення, змушує функцію явно повертати певне значення.

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

  3. Built-in types.

  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

Другий метод полягає у використанні ключового слова 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:
    # ...

Укажіть тип елемента Масиву

Щоб визначити тип «масиву», укладіть назву типу в «[]».

An array's type applies to for loop variables, as well as some operators like [], []=, and +. Array methods (such as push_back) and other operators (such as ==) are still untyped. Built-in types, native and custom classes, and enums may be used as element types. Nested array types (like Array[Array[int]]) are not supported.

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.

Лиття типу

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

Уявіть Ворога у вашій грі, який розширює Area2D. Ви хочете, щоб він зіткнувся з Player, CharacterBody2D із прикріпленим до нього сценарієм під назвою PlayerController. Ви використовуєте сигнал body_entered для виявлення зіткнення. За допомогою введеного коду тіло, яке ви виявляєте, буде загальним PhysicsBody2D, а не вашим PlayerController у зворотному виклику _on_body_entered.

Ви можете перевірити, чи цей PhysicsBody2D є вашим Програвачем за допомогою ключового слова 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()

You can also simplify the code by using the is not operator:

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

Alternatively, you can use the assert() statement:

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 ви також не піклуєтесь про тип вузла, якщо він має методи, необхідні для виклику.

Ви можете використовувати кастинг, щоб повідомити Годо тип, який ви очікуєте, коли отримаєте вузол: ($Timer як Timer), ($Player як CharacterBody2D) тощо. Годо переконається, що тип працює, і якщо так, номер рядка стане зеленим ліворуч від редактора сценаріїв.

Небезпечна проти безпечної лінії

Небезпечний рядок (рядок 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_entered(area: CollisionObject2D) -> void:
    pass

Система попередження

Примітка

Детальну документацію про систему попереджень GDScript переміщено до Система попередження GDScript.

Godot gives you warnings about your code as you write it. The engine identifies sections of your code that may lead to issues at runtime, but lets you decide whether or not you want to leave the code as it is.

У нас є низка попереджень, спрямованих саме на користувачів введеного GDScript. За замовчуванням ці попередження вимкнено. Ви можете ввімкнути їх у налаштуваннях проекту (Debug > GDScript, переконайтеся, що Advanced Settings увімкнено).

Ви можете ввімкнути попередження UNTYPED_DECLARATION, якщо хочете завжди використовувати статичні типи. Крім того, ви можете ввімкнути попередження INFERRED_DECLARATION, якщо ви віддаєте перевагу більш читабельному та надійному, але більш докладному синтаксису.

Попередження UNSAFE_* роблять небезпечні операції більш помітними, ніж небезпечні рядки. Наразі попередження UNSAFE_* не охоплюють усі випадки, які охоплюють небезпечні лінії.

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

Попередження 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,
    }
    
  2. Вкладені типи наразі не підтримуються:

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

Підсумок

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