Tipizzazione statica in GDScript
In questa guida imparerai:
come utilizzare la tipizzazione statica in GDScript;
che la tipizzazione statica può aiutarti 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 può essere usata 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, ciò 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:
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:
Suggerimento
Se preferisci la tipizzazione statica, ti consigliamo di abilitare l'impostazione Editor di testo > Completamento > Aggiungi suggerimenti del tipo. Considera anche di abilitare alcuni avvertimenti che sono disabilitati come predefiniti.
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
Non c'è differenza tra
=e:=per le costanti.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. Inoltre, questo è 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:
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.(Solo tipo restituito)
void. Indica che la funzione non restituisce alcun valore.Classi native (
Object,Node,Area2D,Camera2D, ecc.).Enumerazioni denominate globali, native e personalizzate. Si noti che un tipo di enumerazione è semplicemente un
int, non vi è alcuna garanzia che il valore appartenga all'insieme dei valori nell'enumerazione.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 in una costante lo script che vuoi usare come tipo:
const Rifle = preload("res://player/weapons/rifle.gd")
var my_rifle: Rifle
Il secondo metodo consiste nell'utilizzare la parola chiave class_name nella creazione. 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
Puoi anche utilizzare le tue 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, racchiudere il nome del tipo tra [].
Il tipo di un array si applica alle variabili del ciclo for, così come ad alcuni operatori come [], []= 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. I tipi di array 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.
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 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 utilizzare 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. Utilizzare la parola chiave as solo se si desidera questo comportamento. Un'alternativa più sicura è utilizzare 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")
In alternativa, è 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
You can also use casting to ensure safe lines. Safe lines are a tool to tell you when ambiguous lines of code are type-safe. As you can mix and match typed and dynamic code, at times, Godot doesn't have enough information to know if an instruction will trigger an error or not at runtime.
This happens when you get a child node. Let's take a timer for example:
with dynamic code, you can get the node with $Timer. GDScript supports
duck-typing,
so even if your timer is of type Timer, it is also a Node and
an Object, two classes it extends. With dynamic GDScript, you also don't
care about the node's type as long as it has the methods you need to call.
You can use casting to tell Godot the type you expect when you get a node:
($Timer as Timer), ($Player as CharacterBody2D), etc.
Godot will ensure the type works and if so, the line number will turn
green at the left of the script editor.
Riga non sicura (riga 7) vs Righe sicure (righe 6 e 8)
Nota
Le righe sicure non sempre significano codice migliore o più affidabile. Consultare 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 contrassegnata come riga non sicura, è più affidabile della dichiarazione di node_1. Infatti, se si modifica il tipo di nodo nella scena e si dimentica accidentalmente di modificarlo 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 modificarne il colore nelle impostazioni dell'editor.
Tipizzazione statica o dinamica: attieniti a un unico stile
Typed GDScript and dynamic GDScript can coexist in the same project. But it's recommended to stick to either style for consistency in your codebase, and for your peers. It's easier for everyone to work together if you follow the same guidelines, and faster to read and understand other people's code.
Il codice tipizzato richiede un po' più da scrivere, ma si ottengono i vantaggi di cui abbiamo parlato sopra. 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 stile dinamico:
func _on_area_2d_body_entered(body):
pass
E lo stesso callback, con suggerimenti di tipo:
func _on_area_entered(area: CollisionObject2D) -> 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 avvisi rivolti specificamente agli utenti di GDScript tipizzato. Normalmente, questi avvisi sono disabilitati; è possibile abilitarli nelle Impostazioni del progetto (Debug > GDScript, assicurandosi che Impostazioni avanzate sia abilitato).
È possibile abilitare l'avvertimento UNTYPED_DECLARATION se si desidera utilizzare sempre tipi statici. Inoltre, è possibile abilitare l'avvertimento INFERRED_DECLARATION se si preferisce 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
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 utilizzando 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 un Label collegato 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ù semplice (ma non staticamente tipizzata) per ottenere ciò che desideriamo 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.
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, }
I tipi annidati non sono attualmente supportati:
var teams: Array[Array[Character]] = []
Sommario
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.