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.

Tipizzazione statica in GDScript

In questa guida imparerai:

  • come utilizzare la tipizzazione statica in GDScript;

  • che la tipizzazione statica può aiutare a evitare bug;

  • che la tipizzazione statica migliora la tua esperienza con l'editor.

Dove e come usare questa nuova funzionalità del linguaggio dipende completamente da te: puoi usarla solo in certi file GDScript sensibili, dappertutto o non usarla affatto.

La tipizzazione statica si può utilizzare su variabili, costanti, funzioni, parametri e tipi restituiti.

Un breve sguardo alla tipizzazione statica

Grazie alla tipizzazione statica, GDScript può rilevare più errori senza nemmeno eseguire il codice. Inoltre, i suggerimenti sui tipi forniscono a te e ai tuoi colleghi più informazioni mentre programmate, poiché i tipi degli argomenti appaiono quando chiamate un metodo. La tipizzazione statica migliora il completamento automatico dell'editor e la documentazione dei tuoi script.

Immagina di programmare un sistema di inventario. Programmi una classe Item, poi un Inventory. Per aggiungere elementi all'inventario, le persone che lavorano con il tuo codice devono sempre passare un oggetto di tipo Item al metodo Inventory.add(). Con la tipizzazione, lo si può imporre:

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

La tipizzazione statica inoltre fornisce migliori opzioni di completamento del codice. Di seguito, puoi vedere la differenza tra le opzioni di completamento di tipo dinamico e di tipo statico.

Hai probabilmente riscontrato la mancanza di suggerimenti di completamento automatico dopo un punto:

Opzioni di completamento per il codice dinamicamente tipizzato.

Questo è per via della tipizzazione dinamica. Godot non può sapere quale nodo o tipo stai passando alla funzione. Tuttavia, se dichiari il tipo esplicitamente, otterrai tutti i metodi, proprietà, costanti, ecc. dal valore:

Opzioni di completamento per il codice staticamente tipizzato.

Suggerimento

Se preferisci la tipizzazione statica, consigliamo di abilitare l'impostazione Editor di testo > Completamento > Aggiungi suggerimenti del tipo. Considera anche di abilitare alcuni avvertimenti che sono normalmente disabilitati.

Inoltre, il GDScript tipizzato migliora le prestazioni utilizzando opcode ottimizzati quando i tipi di operandi/argomenti sono noti in fase di compilazione. Sono previste ulteriori ottimizzazioni di GDScript in futuro, come la compilazione JIT/AOT.

In generale, la programmazione tipizzata offre un'esperienza più strutturata. Aiuta a prevenire gli errori e migliora l'aspetto auto-documentante degli script. Questo è particolarmente utile quando si lavora in team o su progetti a lungo termine: studi hanno dimostrato che gli sviluppatori trascorrono la maggior parte del tempo a leggere il codice di altri o script scritti in passato e dimenticati. Più il codice è chiaro e strutturato, più è veloce da comprendere e più velocemente si può procedere.

Come utilizzare la tipizzazione statica

Per definire il tipo di una variabile, un parametro o una costante, scrivi due punti dopo il nome, seguito dal suo tipo. Ad esempio, var health: int. Questo forza il tipo della variabile a rimanere sempre lo stesso:

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 proverà a dedurre il tipo se scrivi i due punti, ma ometti il tipo:

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

Nota

  1. Non c'è differenza tra = e := per le costanti.

  2. Non è necessario scrivere suggerimenti di tipo per le costanti, poiché Godot le imposta automaticamente in base al valore assegnato. Tuttavia, è comunque possibile farlo per rendere più chiaro l'intento del codice. È anche utile per gli array tipizzati (come const A: Array[int] = [1, 2, 3]), poiché normalmente gli array non sono tipizzati.

Cosa può essere un suggerimento del tipo

Ecco un elenco completo di ciò che si può utilizzare come suggerimento del tipo:

  1. Variant. Qualsiasi tipo. Nella maggior parte dei casi non è molto diverso da una dichiarazione non tipizzata, ma aumenta la leggibilità. Come tipo restituito, costringe la funzione a restituire esplicitamente un valore.

  2. (Solo tipo restituito) void. Indica che la funzione non restituisce alcun valore.

  3. Tipi integrati.

  4. Classi native (Object, Node, Area2D, Camera2D, ecc.).

  5. Classi globali.

  6. Classi interne.

  7. Enumerazioni denominate globali, native e personalizzate. Si noti che un tipo di enumerazione è solo un int, non vi è alcuna garanzia che il valore appartenga all'insieme dei valori nell'enumerazione.

  8. Costanti (comprese quelle locali) se contengono una classe o un'enumerazione precaricata.

Puoi usare qualsiasi classe, incluse le tue classi personalizzate, come tipo. Ci sono due modi per usarle negli script. Il primo metodo consiste nel precaricare lo script che vuoi usare come tipo in una costante:

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

Il secondo metodo consiste nell'usare la parola chiave class_name nella creazione dello script. Nell'esempio precedente, il file rifle.gd avrebbe questo aspetto:

class_name Rifle
extends Node2D

Se si utilizza class_name, Godot registra il tipo Rifle globalmente nell'editor, e sarà possibile utilizzarlo ovunque, senza doverlo precaricare in una costante:

var my_rifle: Rifle

Definisci il tipo restituito da una funzione con la freccia ->

Per definire il tipo restituito di una funzione, scrivi un trattino e una parentesi angolare destra -> dopo la sua dichiarazione, seguiti dal tipo restituito:

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

Il tipo void significa che la funzione non restituisce nulla. È possibile utilizzare qualsiasi tipo, come per le variabili:

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

È possibile anche utilizzare le proprie classi come tipi restituiti:

# 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

Covarianza e controvarianza

Quando si ereditano i metodi di una classe padre, si dovrebbe seguire il principio di sostituzione di Liskov.

Covarianza: quando si eredita un metodo, è possibile specificare un tipo restituito più specifico (sottotipo) rispetto al metodo padre.

Controvarianza: quando si eredita un metodo, è possibile specificare un tipo di parametro meno specifico (supertipo) rispetto al metodo padre.

Esempio:

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

Definire il tipo degli elementi in un Array

Per definire il tipo di un Array, racchiudi il nome del tipo tra [].

Il tipo di un array si applica alle variabili del ciclo for, così come ad alcuni operatori come [], []= (assegnamento) e +. I metodi degli array (come push_back) e altri operatori (come ==) sono comunque non tipizzati. Tipi integrati, classi native e personalizzate, ed enumerazioni si possono utilizzare come tipi degli elementi. Gli array tipizzati annidati (come Array[Array[int]]) non sono supportati.

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"

A partire da Godot 4.2, è anche possibile specificare un tipo per la variabile di ciclo in un ciclo for. Ad esempio, si può scrivere:

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

L'array rimarrà non tipizzato, ma la variabile name all'interno del ciclo for sarà sempre di tipo String.

Definire il tipo degli elementi in un Dictionary

Per definire il tipo delle chiavi e dei valori di un Dictionary, racchiudere il nome del tipo tra [] e separa il tipo delle chiavi e dei valori con una virgola.

Il tipo dei valori di un dizionario si applica alle variabili del ciclo for, così come ad alcuni operatori come [] e []= (assegnamento). I metodi dei dizionari che restituiscono valori e altri operatori (come ==) sono comunque non tipizzati. Tipi integrati, classi native e personalizzate, ed enumerazioni si possono utilizzare come tipi degli elementi. Le raccolte tipizzate annidate (come Dictionary[String, Dictionary[String, int]]) non sono supportate.

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"

Type casting

Il type casting è un concetto importante nei linguaggi tipizzati. Il casting è la conversione di un valore da un tipo a un altro.

Immagina un Enemy nel tuo gioco, che extends Area2D. Vuoi che entri in collisione con il Player, un CharacterBody2D con uno script denominato PlayerController associato. Utilizzi il segnale body_entered per rilevare la collisione. Con codice tipizzato, il corpo rilevato sarà un PhysicsBody2D generico, e non il tuo PlayerController sul callback _on_body_entered.

Puoi verificare se questo PhysicsBody2D è il tuo Player con la parola chiave as e usando nuovamente i due punti : per forzare la variabile a usare questo tipo. Ciò forza la variabile a rimanere di tipo PlayerController:

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

    player.damage()

Dato che abbiamo a che fare con un tipo personalizzato, se body non estende PlayerController, la variabile player verrà impostata a null. Possiamo sfruttare questo per verificare se il corpo è il player o meno. Otterremo anche il completamento automatico completo sulla variabile player grazie a questa conversione.

Nota

La parola chiave as converte silenziosamente la variabile a null in caso di incompatibilità di tipo in fase di esecuzione, senza generare errori/avvertimenti. Sebbene ciò sia comodo in alcuni casi, può anche causare bug. Usa la parola chiave as solo se è previsto questo comportamento. Un'alternativa più sicura è usare la parola chiave is:

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

var player: PlayerController = body
if not player:
    return

player.damage()

È anche possibile semplificare il codice utilizzando l'operatore is not:

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

Alternativamente, è possibile utilizzare l'istruzione assert():

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

var player: PlayerController = body
if not player:
    return

player.damage()

Nota

Se si tenta di convertire con un tipo integrato e l'operazione fallisce, Godot genererà un errore.

Righe sicure

È anche possibile usare il casting per garantire la sicurezza delle righe. Le righe sicure sono un mezzo che indica quando le righe di codice ambigue sono di tipo sicuro. Dato che è possibile combinare codice tipizzato e dinamico, a volte Godot non ha informazioni sufficienti per sapere se un'istruzione genererà o meno un errore in fase di esecuzione.

Questo accade quando si ottiene un nodo figlio. Prendiamo ad esempio un timer: con codice dinamico, si può ottenere il nodo con $Timer. GDScript supporta il duck-typing, quindi anche se il timer è di tipo Timer, è anche un Node e un Object, due classi che estende. Con GDScript dinamico, inoltre, il tipo del nodo non importa, purché abbia i metodi che c'è bisogno di chiamare.

È possibile utilizzare il casting per indicare a Godot il tipo che ci si aspetta quando si ottiene un nodo: ($Timer as Timer), ($Player as CharacterBody2D), ecc. Godot si assicurerà che il tipo va bene e, in tal caso, il numero di riga diventerà verde a sinistra dell'editor di script.

Confronto tra riga non sicura e sicura

Riga non sicura (riga 7) contro righe sicure (righe 6 e 8)

Nota

Le righe sicure non sempre significano codice migliore o più affidabile. Consulta la nota precedente sulla parola chiave as. Per esempio:

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

Sebbene la dichiarazione di node_2 sia segnata come riga non sicura, è più affidabile della dichiarazione di node_1. Infatti, se modifichi il tipo di nodo nella scena e per sbaglio lo dimentichi di modificare nello script, l'errore verrà rilevato immediatamente al caricamento della scena. A differenza di node_1, che verrà automaticamente convertito in null e l'errore verrà rilevato dopo.

Nota

È possibile disattivare le righe sicure o cambiarne il colore nelle impostazioni dell'editor.

Tipizzazione statica o dinamica: attieniti a un unico stile

GDScript tipizzato staticamente e GDScript tipizzato dinamicamente possono coesistere nello stesso progetto. Tuttavia, è consigliabile attenersi a uno dei due stili per garantire coerenza nel codice, e per altri colleghi. È più facile per tutti lavorare insieme se si seguono le stesse linee guida, e leggere e capire il codice degli altri diventa più veloce.

Il codice tipizzato richiede un po' più da scrivere, ma si ottengono i vantaggi di cui abbiamo parlato prima. Ecco un esempio dello stesso script vuoto, in stile dinamico:

extends Node


func _ready():
    pass


func _process(delta):
    pass

E con la tipizzazione statica:

extends Node


func _ready() -> void:
    pass


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

Come puoi vedere, puoi usare i tipi anche con i metodi virtuali del motore. Anche i callback dei segnali, come qualsiasi metodo, possono usare i tipi. Ecco un segnale body_entered in uno stile dinamico:

func _on_area_2d_body_entered(body):
    pass

E lo stesso callback, con suggerimenti di tipo:

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

Sistema di avvertimento

Nota

La documentazione dettagliata sul sistema di avvertimento in GDScript è stata spostata in Sistema di avvertimento in GDScript.

Godot emette avvertimenti mentre scrivi il tuo codice: il motore identifica sezioni del tuo codice che potrebbero causare problemi in fase di esecuzione, ma lascia a te decidere se mantenere il codice così com'è.

Esistono una serie di avvertimenti rivolti specificamente agli utenti di GDScript tipizzato. Normalmente, questi avvertimenti sono disabilitati; è possibile abilitarli nelle Impostazioni del progetto (Debug > GDScript, assicurandosi che Impostazioni avanzate sia abilitato).

È possibile abilitare l'avvertimento UNTYPED_DECLARATION se vuoi utilizzare sempre tipi statici. Inoltre, è possibile abilitare l'avvertimento INFERRED_DECLARATION se preferisci una sintassi più leggibile e affidabile, ma più verbosa.

Gli avvertimenti UNSAFE_* rendono le operazioni non sicure più evidenti rispetto alle righe non sicure. Attualmente, gli avvertimenti UNSAFE_* non coprono tutti i casi coperti dalle righe non sicure.

Operazioni comuni non sicure e le loro controparti sicure

Metodi nell'ambito globale

I seguenti metodi nell'ambito globale non sono tipizzati staticamente, ma hanno controparti tipizzate disponibili. Questi metodi restituiscono valori tipizzati staticamente:

Metodo

Equivalenti tipizzati staticamente

abs()

ceil()

clamp()

floor()

lerp()

round()

sign()

snapped()

Quando si usa la tipizzazione statica, si consiglia di usare i metodi tipizzati con ambito globale, ove possibile. Ciò garantisce righe di codice sicure e permette di godere delle istruzioni tipizzate per migliorare le prestazioni.

Avvertimenti UNSAFE_PROPERTY_ACCESS e UNSAFE_METHOD_ACCESS

In questo esempio, vogliamo impostare una proprietà e chiamare un metodo su un oggetto a cui è associato uno script con class_name MyScript e che extends Node2D. Se abbiamo un riferimento all'oggetto sotto forma di un Node2D (ad esempio, come ci è stato passato dal sistema di fisica), possiamo prima verificare se la proprietà e il metodo esistono e poi impostarli e chiamarli in tal caso:

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.

Tuttavia, questo codice genererà gli avvertimenti UNSAFE_PROPERTY_ACCESS e UNSAFE_METHOD_ACCESS poiché la proprietà e il metodo non sono presenti nel tipo referenziato, in questo caso Node2D. Per rendere queste operazioni sicure, è possibile prima verificare se l'oggetto è di tipo MyScript tramite la parola chiave is e poi dichiarare una variabile di tipo MyScript su cui è possibile impostarne le proprietà e chiamarne i metodi:

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

Alternativamente, è possibile dichiarare una variabile e usare l'operatore as per provare a convertire l'oggetto. Sarà quindi necessario verificare se la conversione è riuscita confermando che la variabile sia stata assegnata:

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

Avvertimento UNSAFE_CAST

In questo esempio, vorremmo che l'etichetta collegata a un oggetto che entra nella nostra area di collisione ne mostrasse il nome. Una volta che l'oggetto entra nell'area di collisione, il sistema di fisica invia un segnale con un oggetto Node2D e la soluzione più diretta (ma non staticamente tipizzata) per ottenere ciò che vogliamo si potrebbe ottenere in questo modo:

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

Questo frammento di codice genera un avvertimento UNSAFE_PROPERTY_ACCESS perché label non è definito in Node2D. Per risolvere questo problema, potremmo prima verificare se la proprietà label esiste e convertirla al tipo Label prima di impostare la sua proprietà text, in questo modo:

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

Tuttavia, questo genera un avvertimento UNSAFE_CAST perché body.label è di tipo Variant. Per ottenere in modo sicuro la proprietà nel tipo desiderato, è possibile utilizzare il metodo Object.get() che restituisce l'oggetto sotto forma di un valore Variant o restituisce null se la proprietà non esiste. È quindi possibile determinare se la proprietà contiene un oggetto del tipo corretto tramite la parola chiave is e infine dichiarare una variabile staticamente tipizzata con l'oggetto:

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

Casi in cui non è possibile specificare i tipi

Per concludere questa introduzione, accenniamo ai casi in cui non è possibile utilizzare i suggerimenti di tipi. Questo genererà un errore di sintassi.

  1. Non è possibile specificare il tipo di singoli elementi in un array o in un dizionario:

var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy]
var character: Dictionary = {
    name: String = "Richard",
    money: int = 1000,
    inventory: Inventory = $Inventory,
}
  1. I tipi annidati non sono attualmente supportati:

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

Riepilogo

GDScript tipizzato è uno strumento potente. Aiuta a scrivere codice più strutturato, evitare errori comuni e creare sistemi scalabili e affidabili. I tipi statici migliorano le prestazioni di GDScript e sono previste ulteriori ottimizzazioni per il futuro.