+2 votes

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?
in Engine by (41 points)

1 Answer

0 votes

C# will not detach event handlers before disposing object. There is a (long) explanation about why C# behaves in this way in this thread: https://stackoverflow.com/questions/17399991/should-i-always-disconnect-event-handlers-in-the-dispose-method

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.

by (70 points)
Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to webmaster@godotengine.org with your username.