2
\$\begingroup\$

If I have a GameObject with a component like some:

class SomeComponent : MonoBehaviour
{
    System.Action DoStuff;
}

class SomeOtherComponentInAnotherGameObject
{
    public GameObject ObjectWithThatComponent;
    void Start()
    {
        ObjectWithThatComponent.GetComponent<SomeComponent>().DoStuff += SomeFunc;

        Destroy(ObjectWithThatComponent);

        // is there still a reference to SomeComponent somewhere?
    }
}

I'm getting some odd behaviour/random crashing when deleting this stuff but I'm not sure if it's being caused by these delegates.

\$\endgroup\$
  • \$\begingroup\$ Why don't you use static delegate and events instead of action? \$\endgroup\$ – Hamza Hasan Oct 6 '16 at 12:15
  • \$\begingroup\$ Are you sure you're using System.Action right? I can't find any documentation on the += operator for that class. I know that C# events look very similar to what it looks like you're trying to do though? \$\endgroup\$ – Coburn Oct 6 '16 at 14:20
  • \$\begingroup\$ @HamzaHasan Does it make a difference? \$\endgroup\$ – meds Oct 6 '16 at 15:16
  • \$\begingroup\$ @Coburn Certainly compiles and runs as expected \$\endgroup\$ – meds Oct 6 '16 at 15:17
  • \$\begingroup\$ In that case, you don't have to create and destroy GameObject at runtime. Script just should be dropped on any gameObject in that scene \$\endgroup\$ – Hamza Hasan Oct 6 '16 at 15:31
1
\$\begingroup\$

"In Unity, does a reference delegate clear out when its parent GameObject is destroyed?"

Very little happens in managed land when a UnityEngine.Object is destroyed. The object will compare as equal to null from that point on, but it remains present until all references to it go out of scope, after which the garbage collector can pick it up. No MonoBehaviour methods like Update or OnCollisionEnter will get called on it, so its lingering presence usually isn't noticeable unless you're accessing its fields or calling functions on it manually.

Most of the work of removing the object happens under the hood in the unmanaged engine code where we can't see it. If we want something to happen to managed code we've written when something is destroyed, we need to specify that in an OnDestroy() method or similar.

"Is there still a reference to SomeComponent somewhere?"

We can put this to the test in the following way:

public class DestructionTest : MonoBehaviour {

    System.Action DoStuff;

    System.WeakReference watchedComponent;

    void Start () {
        var testObject = new GameObject();
        var component = testObject.AddComponent<DestructionTest>();
        component.DoStuff += SomeFunc;

        // Get a weak reference to the component so we can keep an eye on it,
        // without influencing the behaviour of the garbage collector.
        watchedComponent = new System.WeakReference(component, false);

        Destroy(testObject);
    }

    // Update is called once per frame
    void Update () {
        // Two seconds in, force a garbage collection.
        if(Time.time >= 2f && Time.time - Time.deltaTime < 2f)
        {
            System.GC.Collect();
            Debug.Log("-------Garbage Collected--------");
        }

        // Report whether the other component still exists.
        Debug.Log("Alive: " + watchedComponent.IsAlive);
    }

    void SomeFunc()
    {
    }
}

When you run this, you'll see it print "Alive: true" for the first 2 seconds.Then the garbage collector hits and the output changes to "Alive: false"

That means the garbage collector found no (strong) references to the other component and was able to clean it up, despite our mucking with its DoStuff action. So this itself does not create a dangling reference to it, at least so far as the garbage collector is concerned.

\$\endgroup\$
1
\$\begingroup\$

Spot on; delegates can cause odd behavior if the subscribing member is removed. While I am not sure if this is always the case, they are not necessarily automatically removed during garbage collection (that is, when you delete the subscribing game object). As such, we are often told to ensure the subscriber also unsubscribes from the delegate. I note that this instruction is not present in the Unity tutorial; but it is in every secondary tutorial I have ever completed in regards to delegates and delegates in Unity


In the below script, we pass a reference to the target DelegateComponent from which we wish to subscribed to DoStuff. In doing so, we add a reference to the DelegateComponent, so we can later unsubscribe from all subscribed DoStuff delegates when the OnDestroy() method is called; for instance, when you delete your game object.

using System.Collections.Generic;

class DelegateComponent : MonoBehaviour
{
    System.Action DoStuff;
}

class ObjectScript : MonoBehaviour
{
    // Store reference to all linked delegateComponents; if you only ever intend 
    // to subscribe to one delegateComponent, this does not need to be a list.
    private List<DelegateComponent>() subscribedDelegates;

    void Start()
    {
        // Only needed if we are instantiating a list.
        subscribedDelegates = new List<DelegateComponent>();
    }

    void OnDestroy()
    {
        for(int i = 0; i < subscribeDelegates.Count; i++)
        {
            subscribedDelegates[i].DoStuff -= SomeMethod;
        }
    }

    public void SomeMethod()
    {
        // Your subscribed logic, here.
    }

    public void Subscribe(DelegateComponent delegateComponent)
    {
        subscribedDelegates.Add(delegateComponent);
        delegateComponent.DoStuff += SomeMethod;
    }
}

If you only intend to subscribe to a single DoStuff, you should replace the List<DelegateComponent> with a single DelegateComponent reference. You can then omit the instantiation in the Start() method, and directly unsubscribe from this delegate in the OnDestroy() method. For completion sake, we should also use some additional logic in the Subscribe(DelegateComponent delegateComponent) method to account for the possibility that we were previously subscribing to a different DelegateComponent.

private DelegateComponent subscribedDelegate;

public void Subscribe(DelegateComponent delegateComponent)
{
    if(subscribedDelegate != null)
    {
        subscribedDelegate.DoStuff -= SomeMethod;
    }

    subscribedDelegate = delegateComponent;
    subscribedDelegate.DoStuff += SomeMethod;
}
\$\endgroup\$
  • 1
    \$\begingroup\$ In the example given in the question, it's not the subscriber that's destroyed, but the "subscribee" SomeComponent, or DelegateComponent in this example. So far as my tests have been able to determine, this doesn't lead to any hanging references. Do you know of any problems that arise in this scenario? \$\endgroup\$ – DMGregory May 3 '17 at 1:32
  • \$\begingroup\$ @DMGregory, I am unaware of the specific problem. That said, I always do as above, and never run into a problem; as I mention, I have always been taught to have an unsubscription to balance the subscription, often in the OnDestroy() method. \$\endgroup\$ – Gnemlock May 3 '17 at 1:54
  • \$\begingroup\$ In the specific case of the asker's question, your unsubscribe code will throw a null reference exception because you're trying to access a member of an object that's been destroyed already. So don't forget to check for / prune nulls first! \$\endgroup\$ – DMGregory Jun 26 at 10:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.