Quellcode durchsuchen

Add C# signal automatic disconnection info

31 vor 1 Jahr
Ursprung
Commit
52414e8581
1 geänderte Dateien mit 168 neuen und 9 gelöschten Zeilen
  1. 168 9
      tutorials/scripting/c_sharp/c_sharp_signals.rst

+ 168 - 9
tutorials/scripting/c_sharp/c_sharp_signals.rst

@@ -15,6 +15,10 @@ In some cases it's necessary to use the older
 :ref:`Disconnect()<class_object_method_disconnect>` APIs.
 See :ref:`using_connect_and_disconnect` for more details.
 
+If you encounter a ``System.ObjectDisposedException`` while handling a signal,
+you might be missing a signal disconnection. See
+:ref:`disconnecting_automatically_when_the_receiver_is_freed` for more details.
+
 Signals as C# events
 --------------------
 
@@ -33,15 +37,6 @@ In addition, you can always access signal names associated with a node type thro
 
     await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
 
-.. warning::
-
-    While all engine signals connected as events are automatically disconnected when nodes are freed, custom
-    signals connected using ``+=`` aren't. This means you will need to manually disconnect (using ``-=``)
-    all the custom signals you connected as C# events (using ``+=``).
-
-    An alternative to manually disconnecting using ``-=`` is to
-    :ref:`use Connect <using_connect_and_disconnect>` rather than ``+=``.
-
 Custom signals as C# events
 ---------------------------
 
@@ -184,3 +179,167 @@ does nothing.
     {
         GD.Print("Greetings!");
     }
+
+.. _disconnecting_automatically_when_the_receiver_is_freed:
+
+Disconnecting automatically when the receiver is freed
+------------------------------------------------------
+
+Normally, when any ``GodotObject`` is freed (such as any ``Node``), Godot
+automatically disconnects all connections associated with that object. This
+happens for both signal emitters and signal receivers.
+
+For example, a node with this code will print "Hello!" when the button is
+pressed, then free itself. Freeing the node disconnects the signal, so pressing
+the button again doesn't do anything:
+
+.. code-block:: csharp
+
+    public override void _Ready()
+    {
+        Button myButton = GetNode<Button>("../MyButton");
+        myButton.Pressed += SayHello;
+    }
+
+    private void SayHello()
+    {
+        GD.Print("Hello!");
+        Free();
+    }
+
+When a signal receiver is freed while the signal emitter is still alive, in some
+cases automatic disconnection won't happen:
+
+- The signal is connected to a lambda expression that captures a variable.
+- The signal is a custom signal.
+
+The following sections explain these cases in more detail and include
+suggestions for how to disconnect manually.
+
+.. note::
+
+    Automatic disconnection is totally reliable if a signal emitter is freed
+    before any of its receivers are freed. With a project style that prefers
+    this pattern, the above limits may not be a concern.
+
+No automatic disconnection: a lambda expression that captures a variable
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you connect to a lambda expression that captures variables, Godot can't tell
+that the lambda is associated with the instance that created it. This causes
+this example to have potentially unexpected behavior:
+
+.. code-block:: csharp
+
+    Timer myTimer = GetNode<Timer>("../Timer");
+    int x = 0;
+    myTimer.Timeout += () =>
+    {
+        x++; // This lambda expression captures x.
+        GD.Print($"Tick {x} my name is {Name}");
+        if (x == 3)
+        {
+            GD.Print("Time's up!");
+            Free();
+        }
+    };
+
+.. code-block:: text
+
+    Tick 1, my name is ExampleNode
+    Tick 2, my name is ExampleNode
+    Tick 3, my name is ExampleNode
+    Time's up!
+    [...] System.ObjectDisposedException: Cannot access a disposed object.
+
+On tick 4, the lambda expression tries to access the ``Name`` property of the
+node, but the node has already been freed. This causes the exception.
+
+To disconnect, keep a reference to the delegate created by the lambda expression
+and pass that to ``-=``. For example, this node connects and disconnects using
+the ``_EnterTree`` and ``_ExitTree`` lifecycle methods:
+
+.. code-block:: csharp
+
+    [Export]
+    public Timer MyTimer { get; set; }
+
+    private Action _tick;
+
+    public override void _EnterTree()
+    {
+        int x = 0;
+        _tick = () =>
+        {
+            x++;
+            GD.Print($"Tick {x} my name is {Name}");
+            if (x == 3)
+            {
+                GD.Print("Time's up!");
+                Free();
+            }
+        };
+        MyTimer.Timeout += _tick;
+    }
+
+    public override void _ExitTree()
+    {
+        MyTimer.Timeout -= _tick;
+    }
+
+In this example, ``Free`` causes the node to leave the tree, which calls
+``_ExitTree``. ``_ExitTree`` disconnects the signal, so ``_tick`` is never
+called again.
+
+The lifecycle methods to use depend on what the node does. Another option is to
+connect to signals in ``_Ready`` and disconnect in ``Dispose``.
+
+.. note::
+
+    Godot uses `Delegate.Target <https://learn.microsoft.com/en-us/dotnet/api/system.delegate.target>`_
+    to determine what instance a delegate is associated with. When a lambda
+    expression doesn't capture a variable, the generated delegate's ``Target``
+    is the instance that created the delegate. When a variable is captured, the
+    ``Target`` instead points at a generated type that stores the captured
+    variable. This is what breaks the association. If you want to see if a
+    delegate will be automatically cleaned up, try checking its ``Target``.
+
+    ``Callable.From`` doesn't affect the ``Delegate.Target``, so connecting a
+    lambda that captures variables using ``Connect`` doesn't work any better
+    than ``+=``.
+
+No automatic disconnection: a custom signal
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Connecting to a custom signal using ``+=`` doesn't disconnect automatically when
+the receiving node is freed.
+
+To disconnect, use ``-=`` at an appropriate time. For example:
+
+.. code-block:: csharp
+
+    [Export]
+    public MyClass Target { get; set; }
+
+    public override void _EnterTree()
+    {
+        Target.MySignal += OnMySignal;
+    }
+
+    public override void _ExitTree()
+    {
+        Target.MySignal -= OnMySignal;
+    }
+
+Another solution is to use ``Connect``, which does disconnect automatically with
+custom signals:
+
+.. code-block:: csharp
+
+    [Export]
+    public MyClass Target { get; set; }
+
+    public override void _EnterTree()
+    {
+        Target.Connect(MyClass.SignalName.MySignal, Callable.From(OnMySignal));
+    }