C# Event Handlers triggering Unhandled Exception: System.ObjectDisposedException: Cannot access a disposed object.

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By 2ndGarrison

For C#, when using delegates and events, I am wiring up objects to connect to events, and then eventually those objects are destroyed and replaced by new objects. As the game moves forward. The events are working as intended. In Ready() of the object, I attach the event handler.

CustomEvent.WasCustomTriggered += MyCustomFunction

Then I call my event, and it triggers the response and it performs as expected.
However. Later on, when the first object is disposed with QueueFree() and a new object is instantiated, when the event is triggered again, I get the below error:

System.ObjectDisposedException: Cannot access a disposed object.

For some reason, it looks like the event wired up in the disposed object’s script is still trying to execute the trigger even though it’s disposed.
My work around for this was to detach the event before disposing the object.

CustomEvent.WasCustomTriggered -= MyCustomFunction

It looks like normally, C# doesn’t require you to detach event handlers before disposing. I didn’t know if this is expected behavior or not. So my question is twofold:

  1. Is this expected behavior?
  2. If so, is there some kind of OnDestroy lifecycle method that a node calls before going into the ether that I could put cleanup code like this in?
:bust_in_silhouette: Reply From: betauer

C# will not detach event handlers before disposing object. There is a (long) explanation about why C# behaves in this way in this thread: c# - Should I always disconnect event handlers in the Dispose method? - Stack Overflow

So, to avoid your error you have a bunch of options:

1-Short and sweet solution: detach yourself the delegate when the object will not need it anymore in _ExitTree or Disposing (both belongs to Godot.Object):

protected override void Dispose(bool disposing) {
    CustomEvent.WasCustomTriggered -= MyCustomFunction;
    base.Dispose(disposing);
}

2-Defensive approach: stop invoking the events using delegate multicast (with CustomEvent.WasCustomTriggered.Invoke(…) and loop over the delegates instead, checking if they are disposed if it’s possible, because it’s not straightforward, and Invoke one by one. If one invocation throws an exception, remove the delegate from the event to avoid increase the amount of zombie delegates in your game.

foreach (YourDelegateType @delegate in WasCustomTriggered.GetInvocationList()) {
    if (@delegate.Target is Godot.Object target && target.NativeInstance == IntPtr.Zero) {
        WasCustomTriggered -= @delegate;
    } else {
        try {
            @delegate.Invoke( /* here your parameters */ );
        } catch (System.ObjectDisposedException e) {
            WasCustomTriggered -= @delegate;
        }
    }
}

So, in a Nutshell, this method first check if the delegate belongs to a disposed object in Godot (NativeInstance == IntPtr.Zero). Then, if it’s not a Godot object or it’s a valid Godot object, calls to the Invoke safely, wrapping the call in a try / catch to remove the delegate from the event.

Hello, anyone using Godot 4.0 and reading this! This Q&A predates 4.0 and it’s about “ordinary” C# events, not signals. Godot 4.0 has events that let you connect to signals with better syntax, and Godot does automatically disconnect signal connections that were made using +=.

(As of writing, there is a bug in 4.0 where using += with a custom signal causes Godot to not automatically disconnect it: https://github.com/godotengine/godot/issues/70414. The automatic disconnection reportedly still works for built-in signals or if you use .Connect rather than +=.)

This Q&A has been misinterpreted once–just hoping to prevent it happening again.

31 | 2023-06-15 05:52