Kaynağa Gözat

Update "Static typing in GDScript" page

Danil Alexeev 1 yıl önce
ebeveyn
işleme
9eb7aa3f59

+ 2 - 0
tutorials/scripting/gdscript/gdscript_basics.rst

@@ -1788,6 +1788,8 @@ when the class is loaded, after the static variables have been initialized::
 
 A static constructor cannot take arguments and must not return any value.
 
+.. _doc_gdscript_basics_inner_classes:
+
 Inner classes
 ^^^^^^^^^^^^^
 

BIN
tutorials/scripting/gdscript/img/typed_gdscript_code_completion_dynamic.webp


BIN
tutorials/scripting/gdscript/img/typed_gdscript_code_completion_typed.webp


+ 262 - 214
tutorials/scripting/gdscript/static_typing.rst

@@ -5,12 +5,12 @@ Static typing in GDScript
 
 In this guide, you will learn:
 
--  **How to use types in GDScript**
--  That **static types can help you avoid bugs**
+- how to use static typing in GDScript;
+- that static types can help you avoid bugs;
+- that static typing improves your experience with the editor.
 
-Where and how you use this new language feature is entirely up to you:
-you can use it only in some sensitive GDScript files, use it everywhere,
-or write code like you always did!
+Where and how you use this language feature is entirely up to you: you can use it
+only in some sensitive GDScript files, use it everywhere, or don't use it at all.
 
 Static types can be used on variables, constants, functions, parameters,
 and return types.
@@ -18,56 +18,50 @@ and return types.
 A brief look at static typing
 -----------------------------
 
-With typed GDScript, Godot can detect even more errors as you write
-code! It gives you and your teammates more information as you're
-working, as the arguments' types show up when you call a method.
+With static typing, GDScript can detect more errors without even running the code.
+Also type hints give you and your teammates more information as you're working,
+as the arguments' types show up when you call a method. Static typing improves
+editor autocompletion and :ref:`documentation <doc_gdscript_documentation_comments>`
+of your scripts.
 
-Imagine you're programming an inventory system. You code an ``Item``
-node, then an ``Inventory``. To add items to the inventory, the people
-who work with your code should always pass an ``Item`` to the
-``Inventory.add`` method. With types, you can enforce this:
+Imagine you're programming an inventory system. You code an ``Item`` class,
+then an ``Inventory``. To add items to the inventory, the people who work with
+your code should always pass an ``Item`` to the ``Inventory.add()`` method.
+With types, you can enforce this::
 
-::
-
-    # In 'item.gd'.
-    class_name Item
-    # In 'inventory.gd'.
     class_name Inventory
 
 
     func add(reference: Item, amount: int = 1):
-        var item = find_item(reference)
+        var item := find_item(reference)
         if not item:
             item = _instance_item_from_db(reference)
-
         item.amount += amount
 
-Another significant advantage of typed GDScript is the new **warning
-system**. From version 3.1, 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. More on that in a moment.
+Static types also give you better code completion options. Below, you can see
+the difference between a dynamic and a static typed completion options.
 
-Static types also give you better code completion options. Below, you
-can see the difference between a dynamic and a static typed completion
-options for a class called ``PlayerController``.
-
-You've probably stored a node in a variable before, and typed a dot to
-be left with no autocomplete suggestions:
+You've probably encountered a lack of autocomplete suggestions after a dot:
 
 .. figure:: img/typed_gdscript_code_completion_dynamic.webp
-   :alt: code completion options for dynamic
+    :alt: Completion options for dynamic typed code.
 
-This is due to dynamic code. Godot cannot know what node or value type
-you're passing to the function. If you write the type explicitly
-however, you will get all public methods and variables from the node:
+This is due to dynamic code. Godot cannot know what value type you're passing
+to the function. If you write the type explicitly however, you will get all
+methods, properties, constants, etc. from the value:
 
 .. figure:: img/typed_gdscript_code_completion_typed.webp
-   :alt: code completion options for typed
+    :alt: Completion options for static typed code.
+
+.. tip::
+
+    If you prefer static typing, we recommend enabling the
+    **Text Editor > Completion > Add Type Hints** editor setting. Also consider
+    enabling `some warnings <Warning system_>`_ that are disabled by default.
 
-In the future, typed GDScript will also increase code performance:
-Just-In-Time compilation and other compiler improvements are already
-on the roadmap!
+Also, typed GDScript improves performance by using optimized opcodes when operand/argument
+types are known at compile time. More GDScript optimizations are planned in the future,
+such as JIT/AOT compilation.
 
 Overall, typed programming gives you a more structured experience. It
 helps prevent errors and improves the self-documenting aspect of your
@@ -80,177 +74,131 @@ faster it is to understand, the faster you can move forward.
 How to use static typing
 ------------------------
 
-To define the type of a variable or a constant, write a colon after the
-variable's name, followed by its type. E.g. ``var health: int``. This
-forces the variable's type to always stay the same:
-
-::
+To define the type of a variable, parameter, or constant, write a colon after the name,
+followed by its type. E.g. ``var health: int``. This forces the variable's type
+to always stay the same::
 
     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 will try to infer types if you write a colon, but you omit the
-type:
+Godot will try to infer types if you write a colon, but you omit the type::
 
-::
-
-    var life_points := 4
     var damage := 10.5
-    var motion := Vector2()
-
-Currently you can use three types of… types:
-
-1. :ref:`Built-in <doc_gdscript_builtin_types>`
-2. Core classes and nodes (``Object``, ``Node``, ``Area2D``,
-   ``Camera2D``, etc.)
-3. Your own custom classes. Look at the new :ref:`class_name <doc_gdscript_basics_class_name>`
-   feature to register types in the editor.
+    const MOVE_SPEED := 50.0
+    func sum(a := 0.0, b := 0.0) -> float:
+        return a + b
 
 .. note::
 
-    You don't need to write type hints for constants, as Godot sets it automatically from the assigned value. But you can still do so to make the intent of your code clearer.
-
-Custom variable types
-~~~~~~~~~~~~~~~~~~~~~
-
-You can use any class, including your custom classes, as types. There
-are two ways to use them in scripts. The first method is to preload the
-script you want to use as a type in a constant:
-
-::
+    1. There is no difference between ``=`` and ``:=`` for constants.
+    2. You don't need to write type hints for constants, as Godot sets it automatically
+       from the assigned value. But you can still do so to make the intent of your code clearer.
+       Also, this is useful for typed arrays (like ``const A: Array[int] = [1, 2, 3]``),
+       since untyped arrays are used by default.
+
+What can be a type hint
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Here is a complete list of what can be used as a type hint:
+
+1. ``Variant``. Any type. In most cases this is not much different from an untyped
+   declaration, but increases readability. As a return type, forces the function
+   to explicitly return some value.
+2. *(Only return type)* ``void``. Indicates that the function does not return any value.
+3. :ref:`Built-in types <doc_gdscript_builtin_types>`.
+4. Native classes (``Object``, ``Node``, ``Area2D``, ``Camera2D``, etc.).
+5. :ref:`Global classes <doc_gdscript_basics_class_name>`.
+6. :ref:`Inner classes <doc_gdscript_basics_inner_classes>`.
+7. Global, native and custom named enums. Note that an enum type is just an ``int``,
+   there is no guarantee that the value belongs to the set of enum values.
+8. Constants (including local ones) if they contain a preloaded class or enum.
+
+You can use any class, including your custom classes, as types. There are two ways
+to use them in scripts. The first method is to preload the script you want to use
+as a type in a constant::
 
     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.
-For the example above, your rifle.gd would look like this:
-
-::
+For the example above, your ``rifle.gd`` would look like this::
 
     class_name Rifle
     extends Node2D
 
-If you use ``class_name``, Godot registers the Rifle type globally in
-the editor, and you can use it anywhere, without having to preload it
-into a constant:
-
-::
+If you use ``class_name``, Godot registers the ``Rifle`` type globally in the editor,
+and you can use it anywhere, without having to preload it into a constant::
 
     var my_rifle: Rifle
 
-Variable casting
-~~~~~~~~~~~~~~~~
-
-Type casting is a key concept in typed languages.
-Casting is the conversion of a value from one type to another.
-
-Imagine an Enemy in your game, that ``extends Area2D``. You want it to
-collide with the Player, a ``CharacterBody2D`` with a script called
-``PlayerController`` attached to it. You use the ``on_body_entered``
-signal to detect the collision. With typed code, the body you detect is
-going to be a generic ``PhysicsBody2D``, and not your
-``PlayerController`` on the ``_on_body_entered`` callback.
-
-You can check if this ``PhysicsBody2D`` is your Player with the ``as``
-casting keyword, and using the colon ``:`` again to force the variable
-to use this type. This forces the variable to stick to the
-``PlayerController`` type:
-
-::
-
-    func _on_body_entered(body: PhysicsBody2D) -> void:
-        var player := body as PlayerController
-        if not player:
-            return
-
-        player.damage()
-
-As we're dealing with a custom type, if the ``body`` doesn't extend
-``PlayerController``, the ``player``\ variable will be set to ``null``.
-We can use this to check if the body is the player or not. We will also
-get full autocompletion on the player variable thanks to that cast.
-
-.. note::
-
-    If you try to cast with a built-in type and it fails, Godot will throw an error.
-
-.. _doc_gdscript_static_typing_safe_lines:
+Specify the return type of a function with the arrow ``->``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Safe lines
-^^^^^^^^^^
+To define the return type of a function, write a dash and a right angle bracket ``->``
+after its declaration, followed by the return type::
 
-You can also use casting to ensure safe lines. Safe lines are a new
-tool in Godot 3.1 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.
+    func _process(delta: float) -> void:
+        pass
 
-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 <https://stackoverflow.com/a/4205163/8125343>`__,
-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.
+The type ``void`` means the function does not return anything. You can use any type,
+as with variables::
 
-.. figure:: img/typed_gdscript_safe_unsafe_line.webp
-   :alt: Unsafe vs Safe Line
+    func hit(damage: float) -> bool:
+        health_points -= damage
+        return health_points <= 0
 
-   Unsafe line (line 7) vs Safe Lines (line 6 and 8)
+You can also use your own classes as return types::
 
-.. note::
+    # 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)
 
-    You can turn off safe lines or change their color in the editor settings.
+        item.amount += amount
+        return item
 
-Define the return type of a function with the arrow ->
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Covariance and contravariance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-To define the return type of a function, write a dash and a right angle
-bracket ``->`` after its declaration, followed by the return type:
+When inheriting base class methods, you should follow the `Liskov substitution
+principle <https://en.wikipedia.org/wiki/Liskov_substitution_principle>`__.
 
-::
+**Covariance:** When you inherit a method, you can specify a return type that is
+more specific (**subtype**) than the parent method.
 
-    func _process(delta: float) -> void:
-        pass
+**Contravariance:** When you inherit a method, you can specify a parameter type
+that is less specific (**supertype**) than the parent method.
 
-The type ``void`` means the function does not return anything. You can
-use any type, as with variables:
+Example::
 
-::
+    class_name Parent
 
-    func hit(damage: float) -> bool:
-        health_points -= damage
-        return health_points <= 0
 
-You can also use your own nodes as return types:
+    func get_property(param: Label) -> Node:
+        # ...
 
 ::
 
-    # inventory.gd
+    class_name Child extends Parent
 
-    # 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
+    # `Control` is a supertype of `Label`.
+    # `Node2D` is a subtype of `Node`.
+    func get_property(param: Control) -> Node2D:
+        # ...
 
-Define the element type of an Array
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Specify the element type of an ``Array``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-To define the type of an Array, enclose the type name in ``[]``.
+To define the type of an ``Array``, enclose the type name in ``[]``.
 
-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.
-Primitive types, builtin classes, and custom classes may be used as types.
-Nested array types are not supported.
+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 are not supported.
 
 ::
 
@@ -268,31 +216,128 @@ Nested array types are not supported.
     scores[0] = "lots"
 
 Since Godot 4.2, you can also specify a type for the loop variable in a ``for`` loop.
-For instance, you can write:
-
-::
+For instance, you can write::
 
     var names = ["John", "Marta", "Samantha", "Jimmy"]
     for name: String in names:
         pass
 
 The array will remain untyped, but the ``name`` variable within the ``for`` loop
-will always be of String type.
+will always be of ``String`` type.
+
+Type casting
+~~~~~~~~~~~~
+
+Type casting is an important concept in typed languages.
+Casting is the conversion of a value from one type to another.
+
+Imagine an ``Enemy`` in your game, that ``extends Area2D``. You want it to collide
+with the ``Player``, a ``CharacterBody2D`` with a script called ``PlayerController``
+attached to it. You use the ``body_entered`` signal to detect the collision.
+With typed code, the body you detect is going to be a generic ``PhysicsBody2D``,
+and not your ``PlayerController`` on the ``_on_body_entered`` callback.
+
+You can check if this ``PhysicsBody2D`` is your ``Player`` with the ``as`` keyword,
+and using the colon ``:`` again to force the variable to use this type.
+This forces the variable to stick to the ``PlayerController`` type::
+
+    func _on_body_entered(body: PhysicsBody2D) -> void:
+        var player := body as PlayerController
+        if not player:
+            return
+
+        player.damage()
+
+As we're dealing with a custom type, if the ``body`` doesn't extend
+``PlayerController``, the ``player`` variable will be set to ``null``.
+We can use this to check if the body is the player or not. We will also
+get full autocompletion on the player variable thanks to that cast.
+
+.. note::
+
+    The ``as`` keyword silently casts the variable to ``null`` in case of a type
+    mismatch at runtime, without an error/warning. While this may be convenient
+    in some cases, it can also lead to bugs. Use the ``as`` keyword only if this
+    behavior is intended. A safer alternative is to use the ``is`` keyword::
+
+        if not (body is PlayerController):
+            push_error("Bug: body is not PlayerController.")
+
+        var player: PlayerController = body
+        if not player:
+            return
+
+        player.damage()
+
+    or ``assert()`` statement::
+
+        assert(body is PlayerController, "Bug: body is not PlayerController.")
+
+        var player: PlayerController = body
+        if not player:
+            return
+
+        player.damage()
+
+.. note::
+
+    If you try to cast with a built-in type and it fails, Godot will throw an error.
+
+.. _doc_gdscript_static_typing_safe_lines:
+
+Safe lines
+^^^^^^^^^^
+
+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 <https://stackoverflow.com/a/4205163/8125343>`__,
+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.
+
+.. figure:: img/typed_gdscript_safe_unsafe_line.webp
+   :alt: Unsafe vs Safe Line
+
+   Unsafe line (line 7) vs Safe Lines (line 6 and 8)
+
+.. note::
+
+    Safe lines do not always mean better or more reliable code. See the note above
+    about the ``as`` keyword. For example::
+
+        @onready var node_1 := $Node1 as Type1 # Safe line.
+        @onready var node_2: Type2 = $Node2 # Unsafe line.
+
+    Even though ``node_2`` declaration is marked as an unsafe line, it is more
+    reliable than ``node_1`` declaration. Because if you change the node type
+    in the scene and accidentally forget to change it in the script, the error
+    will be detected immediately when the scene is loaded. Unlike ``node_1``,
+    which will be silently cast to ``null`` and the error will be detected later.
+
+.. note::
+
+    You can turn off safe lines or change their color in the editor settings.
 
 Typed or dynamic: stick to one style
 ------------------------------------
 
 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.
+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.
 
-Typed code takes a little more writing, but you get the benefits we
-discussed above. Here's an example of the same, empty script, in a
-dynamic style:
-
-::
+Typed code takes a little more writing, but you get the benefits we discussed
+above. Here's an example of the same, empty script, in a dynamic style::
 
     extends Node
 
@@ -304,9 +349,7 @@ dynamic style:
     func _process(delta):
         pass
 
-And with static typing:
-
-::
+And with static typing::
 
     extends Node
 
@@ -318,62 +361,67 @@ And with static typing:
     func _process(delta: float) -> void:
         pass
 
-As you can see, you can also use types with the engine's virtual
-methods. Signal callbacks, like any methods, can also use types. Here's
-a ``body_entered`` signal in a dynamic style:
-
-::
+As you can see, you can also use types with the engine's virtual methods.
+Signal callbacks, like any methods, can also use types. Here's a ``body_entered``
+signal in a dynamic style::
 
     func _on_area_2d_body_entered(body):
         pass
 
-And the same callback, with type hints:
-
-::
+And the same callback, with type hints::
 
     func _on_area_entered(area: CollisionObject2D) -> void:
         pass
 
-You're free to replace, e.g. the ``CollisionObject2D``, with your own type,
-to cast parameters automatically:
+Warning system
+--------------
 
-::
+.. note::
 
-    func _on_area_entered(bullet: Bullet) -> void:
-        if not bullet:
-            return
+    Detailed documentation about the GDScript warning system has been moved to
+    :ref:`doc_gdscript_warning_system`.
 
-        take_damage(bullet.damage)
+From version 3.1, 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.
 
-The ``bullet`` variable could hold any ``CollisionObject2D`` here, but
-we make sure it is our ``Bullet``, a node we created for our project. If
-it's anything else, like an ``Area2D``, or any node that doesn't extend
-``Bullet``, the ``bullet`` variable will be ``null``.
+We have a number of warnings aimed specifically at users of typed GDScript.
+By default, these warnings are disabled, you can enable them in Project Settings
+(**Debug > GDScript**, make sure **Advanced Settings** is enabled).
 
-Warning system
---------------
+You can enable the ``UNTYPED_DECLARATION`` warning if you want to always use
+static types. Additionally, you can enable the ``INFERRED_DECLARATION`` warning
+if you prefer a more readable and reliable, but more verbose syntax.
 
-.. note::
+``UNSAFE_*`` warnings make unsafe operations more noticeable, than unsafe lines.
+Currently, ``UNSAFE_*`` warnings do not cover all cases that unsafe lines cover.
 
-    Documentation about the GDScript warning system has been moved to
-    :ref:`doc_gdscript_warning_system`.
+Cases where you can't specify types
+-----------------------------------
 
-A case where you can't specify types
-------------------------------------
+To wrap up this introduction, let's mention cases where you can't use type hints.
+This will trigger a **syntax error**.
 
-To wrap up this introduction, let's mention a case where you can't
-use type hints. This will trigger a **syntax error**.
+1. You can't specify the type of individual elements in an array or a dictionary::
 
-You can't specify the type of individual members in an array:
+        var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy]
+        var character: Dictionary = {
+            name: String = "Richard",
+            money: int = 1000,
+            inventory: Inventory = $Inventory,
+        }
 
-::
+2. Nested types are not currently supported::
+
+        var teams: Array[Array[Character]] = []
+
+3. Typed dictionaries are not currently supported::
 
-    var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy]
+        var map: Dictionary[Vector2i, Item] = {}
 
 Summary
 -------
 
 Typed GDScript is a powerful tool. It helps you write more structured code,
-avoid common errors, and create scalable systems. In the future, static types
-will also bring you a nice performance boost thanks to upcoming compiler
-optimizations.
+avoid common errors, and create scalable and reliable systems. Static types
+improve GDScript performance and more optimizations are planned for the future.