0
\$\begingroup\$

The following code throws a null reference exception when I stop the project because touchInterfaceController has already been destroyed.

private void OnDestroy()
{           
    touchInterfaceController.Pitch.OnInput -= OnTouchPitchInput;
    touchInterfaceController.Thrust.OnThrustInput -= OnTouchThrustInput;
}

Is it safe to simply check for null and in that case not bother to unsubscribe? i.e.

if (touchInterfaceController == null) return;

Both touchInterfaceController and the script which contains this code will be destroyed, and usually will be re-instantiated in the next scene.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Expanding on the answer I gave here, we can test this by using a script that holds references to both a publisher and subscriber "gently" and then checks whether they've been successfully claimed by the garbage collector after destruction.

using System.Collections;
using UnityEngine;

public class SubscriptionTest : MonoBehaviour
{

    public bool killPublisherFirst;

    System.WeakReference publisherRef;
    System.WeakReference subscriberRef;
    
    void Start()
    {
        var publisher = new GameObject("Publisher").AddComponent<Publisher>();
        var subscriber = new GameObject("Subscriber").AddComponent<Subscriber>();

        publisherRef = new (publisher, false);
        subscriberRef = new (subscriber, false);

        publisher.MyEvent += subscriber.DoThing;

        StartCoroutine(Monitor());
    }

    IEnumerator Monitor() {
        
        if (killPublisherFirst) {
            yield return KillAndReport(publisherRef);
            yield return KillAndReport(subscriberRef);
        } else {
            yield return KillAndReport(subscriberRef);
            yield return KillAndReport(publisherRef);
        }        
    }

    IEnumerator KillAndReport(System.WeakReference toKill) {
        yield return new WaitForSeconds(0.5f);

        {
            var victim = (Component)toKill.Target; 
            Debug.Log($"Killing {victim.name}");
            Destroy(victim.gameObject);
        }

        yield return new WaitForSeconds(0.5f);

        Debug.Log($"After destroy, publisher is alive: {publisherRef.IsAlive} / subscriber: {subscriberRef.IsAlive}");

        System.GC.Collect();

        Debug.Log($"After collect, publisher is alive: {publisherRef.IsAlive} / subscriber: {subscriberRef.IsAlive}");

        yield return new WaitForSeconds(0.5f);

        Debug.Log($"After delay, publisher is alive: {publisherRef.IsAlive} / subscriber: {subscriberRef.IsAlive}");
    }
}

public class Subscriber : MonoBehaviour {

    public void DoThing() {
        Debug.Log("Did the thing.");
    }
}

public class Publisher : MonoBehaviour {
    public event System.Action MyEvent;
}

The upshot, if we don't explicitly unsubscribe:

  • When destroying the object with the event (the publisher), that object can be claimed by the garbage collector immediately.

  • When destroying the object subscribing to the event (the subscriber), that object cannot be claimed until the publisher is also destroyed.

  • After both objects have been destroyed, the garbage collector can successfully claim them both, regardless of the order in which they were destroyed.

So:

  • It's good to unsubscribe if the subscriber is the first to be destroyed. (This keeps the subscriber's ghost from lingering in memory until the publisher is also destroyed - which might take until the scene is unloaded, or when the game is shut down, whichever happens first).

  • If the publisher was the first to die (== null), it's safe to skip unsubscribing. No lingering references remain to be cleaned up.

The same goes for UnityEvent, so they don't behave materially differently from standard C# events in this regard.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .