Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add C# signal automatic disconnection info #8815

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 168 additions & 9 deletions tutorials/scripting/c_sharp/c_sharp_signals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------------

Expand All @@ -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
---------------------------

Expand Down Expand Up @@ -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));
}