Search Unity

Why can I still call functions in destroyed MonoBehaviour objects?

Discussion in 'Scripting' started by MrDude, Mar 5, 2015.

  1. MrDude

    MrDude

    Joined:
    Sep 21, 2006
    Posts:
    2,569
    I know I am supposed to remove functions from delegates when I no longer need them. That I know, but that is not the question I am asking here... What I want to know is WHY are the functions still being called when the objects no longer exist?

    Check this out...
    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public class test : MonoBehaviour
    5. {
    6.     public void PrintSomething()
    7.     {
    8.         print ("adding 1");
    9.         GKSEvents.value++;
    10.     }
    11. }
    12.  
    13. public class GKSEvents : MonoBehaviour {
    14.  
    15.     static public int value = 0;
    16.     public System.Action UpdateValue;
    17.    
    18.     // Use this for initialization
    19.     void Start () {
    20.         for(int i = 0; i < 100; i++)
    21.         {
    22.             GameObject go = new GameObject();
    23.             test t = go.AddComponent<test>();
    24.             UpdateValue += t.PrintSomething;
    25.         }
    26.         Invoke("DestroyAll", 1);
    27.     }
    28.  
    29.     void DestroyAll()
    30.     {
    31.         test[] all = FindObjectsOfType<test>();
    32.         print ("Destroying " + all.Length);
    33.         foreach(test t in all)
    34.             Destroy(t.gameObject);
    35.     }
    36.    
    37.     void OnGUI () {
    38.         if (GUILayout.Button ("print"))
    39.         {
    40.             if (null != UpdateValue)
    41.                 UpdateValue();
    42.             print ("value is " + value);
    43.         }
    44.     }
    45. }
    46.  
    By my logic, when I destroyed the 100 objects, they no longer exist so they and everything they contained are not only now null, they no longer exist at all... so how come this script prints 100 and not 0? Does this mean that when the objects are destroyed I still hold a reference to them in memory until the garbage collector eventually picks it up and I am thus still able to call it until such time? Is there a way for me to trigger the GC from within Unity if so...

    ...why, though, that is the question... why can I still access these functions when the gameobject has been destroyed?

    Of course modifying the game objects to give them names and modifying the print string to:
    Code (csharp):
    1.         print ("adding 1 via " + name);
    ...that results in a runtime error because the object has been destroyed. So it knows it's destroyed... so what the hell?

    Thanks for ay insights
     
  2. Anozireth

    Anozireth

    Joined:
    Feb 7, 2013
    Posts:
    71
    Just because a GameObject is destroyed does not mean that any C# scripts associated with the GameObject (MonoBehaviours, etc) have been destroyed or garbage collected. As long as something has a reference to the C# object, it will be held in memory and will not be GC'ed. You will be leaking the C# objects until you clear all references to them. It's very important to un-set any delegates or events for this reason. The OnDestroy or OnDisable methods tend to be a good place to do this.

    You also noticed that you can't access this.name in the component that has been destroyed. Again, this is because the unmanaged GameObject and component have been destroyed, but the C# object has not. You can check if the unmanaged objects have been destroyed. Unity has overridden the == operator for anything that is a UnityEngine.Object. Thus, it is possible this == null to be true if it's code in a MonoBehaviour for which the GameObject has been destroyed. This is a quirk of Unity; in normal C#, this == null would always be false (unless the operator has been similarly overridden).
     
  3. MrDude

    MrDude

    Joined:
    Sep 21, 2006
    Posts:
    2,569
    Yeah, I am very familiar with that null thing. I like sending people code along the lines of
    Code (csharp):
    1.  if (null == myObject)
    2. {
    3.     myObject.PerformFunctionX();
    4.     Debug.Log("My object is of type: " + myObject.GetType());
    5. }
    6.  
    just to watch their reaction when it prints out: "My object is of type null" :p

    If you don't derive from anything when you create your custom class, your class will always return as null (I forget if you are supposed to derive from object or Object to fix this but whatever :p )

    In this instance,though, it is impossible to test if the unmanaged code is null because there is nothing left that references it so there is nothing that I can test against null. If you look at that code above you will see I create the objects in one function without keeping a reference to it, then I find it in another function and destroy it again without keeping a reference to it. So when I later want to use it, there is absolutely no way at all for me to get to them cause they don't exist. I can't get the parent GameObject or the child c# component cause it's gone...

    Are you saying that even though there is no more gameobject through which I can access the component, I can still access all the functions and classes within the destroyed class because it was in fact not destroyed with the parent??? I just tested your theory by adding a second function and having the function call the second function and it clearly works... The c# script is in fact still in memory, just nowhere where I can access it from anymore...

    Since I destroyed the GameObject and it didn't destroy all the components as I expected, I tried adding a Transform field into my script and adding a reference to that to see what happens to that value once the parent object is destroyed seeing as I now hold a reference to the transform inside my class... as expected, the transform is destroyed along with the GameObject and my variable points to null...

    THAT is the behaviour I expected! You destroy the gameobject and everything that pointed to it or any class it contained will now point to null... that is what I would expect... and yet... look at this:
    Code (csharp):
    1.             GameObject go = new GameObject();
    2.             test t = go.AddComponent<test>();
    3.             Destroy (go);
    4.             t.PrintSomething();
    5.  
    ...this also work just fine! t still exists even though the game object is destroyed!!! If you told me that t would be available until the next Update(), that I could understand but if you are going to tell me that t will now be available until the game ends then I say "What the hell????"

    I thought destroying a parent object destroys all child objects and components. So this brings me back to my question:
    The gameObject has a Transform and it has my c# component. An external script holds a reference to my c# script and my c# script holds a reference to the transform so when I destroy the parent object, it destroys the transform but not my script even though my script holds a reference to the transform...

    My variable now points to null because the object it pointed to was destroyed... so why doesn't my script get destroyed and force other scripts to now point to null also?

    That is my question. Unity is supposed to destroy all child objects when you destroy the parent. Why is it not destroying my components also, forcing anything that point to it to point to null?

    Look at this for example:
    Code (csharp):
    1.  
    2.     IEnumerator Delayed(GameObject go, test t)
    3.     {
    4.         yield return 5;
    5.         t.PrintSomethingElse();
    6.         print (go.name);
    7.     }
    8.    
    9.     void OnGUI () {
    10.         if (GUILayout.Button ("print"))
    11.         {
    12.             GameObject go = new GameObject("testing");
    13.             test t = go.AddComponent<test>();
    14.             Destroy (go);
    15.             t.PrintSomething();
    16.             Debug.Log (go.name);
    17.             StartCoroutine(Delayed(go, t));
    18.  
    19.             if (null != UpdateValue)
    20.                 UpdateValue();
    21.             print ("value is " + value);
    22.         }
    23.     }
    24.  
    In the same frame as you destroy the GameObject you can still access it because it hasn't been destroyed yet. Now inside the coroutine, though, there you wait 5 frames and what do you find? The Gameobject has been made null. It has been destroyed. I t cannot be used. But the C# script on it still remains in memory. It still works just fine. Why? Why does it not also get destroyed? I am holding a reference to both the game object and the script inside this same coroutine and yet the destroy destroyed the GameObject but not my script... Why not?

    Like I said, I have always been under the impression that destroying the parent destroys all children. I never knew that this was not the case...
     
  4. MrDude

    MrDude

    Joined:
    Sep 21, 2006
    Posts:
    2,569
    Actually consider this a a rhetorical question. Don't bother explaining. It's good that I am finally aware of this. Now that I know it works this way I'll just have to adapt and keep that in mind as I work.

    I am well aware of how Automatic Reference Counting works and how objects get a counter each time something else accesses it and once nothing accesses it any more it is destroyed. The concept makes sense to me and it is all well and good... but I honestly believed that when you call Destroy on a GameObject that everything inside of it is forced to null and everything that pointed to it now points to null no matter what...

    I was completely unaware that the components still honour the reference counters...
     
  5. MrDude

    MrDude

    Joined:
    Sep 21, 2006
    Posts:
    2,569
    Last example... just to prove you are right and to finally cement this new fact in my memory:
    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public class test : MonoBehaviour
    5. {
    6.     public void PrintSomething()
    7.     {
    8.         print ("hallo, world");
    9.     }
    10. }
    11.  
    12. public class GKSEvents : MonoBehaviour {
    13.  
    14.     test t;
    15.  
    16.     void Start () {
    17.         GameObject go = new GameObject("testing");
    18.         t = go.AddComponent<test>();
    19.         Destroy (go);
    20.     }
    21.  
    22.     void OnGUI () {
    23.         if (GUILayout.Button ("print"))
    24.         {
    25.             t.PrintSomething();
    26.         }
    27.     }
    28. }
    Before tonight I would have thought that t would be null...
     
  6. Anozireth

    Anozireth

    Joined:
    Feb 7, 2013
    Posts:
    71
    It sounds like you've got it. Your C# objects will never be GC'ed as long as something in your code references them, even if their unmanaged portion is destroyed. But just because the C# object still exists in this state, doesn't mean you should use it. When you have something in that state, it is probably a memory leak and you should have released the C# object by clearing out the field that was holding a reference to it.

    In the example from the OP, your test objects all still exist because the UpdateValue delegate still has a reference to them. You have to be careful to unsubscribe from delegates or it's really easy to leak things. From the original example, a more proper DestroyAll might look like this:

    Code (csharp):
    1.  
    2.  
    3.    void DestroyAll()
    4.    {
    5.         test[] all = FindObjectsOfType<test>();
    6.        print ("Destroying " + all.Length);
    7.        foreach(test t in all)
    8.        {
    9.            UpdateValue -= t.PrintSomething;
    10.            Destroy(t.gameObject);
    11.        }
    12.    }
    13.  
    14.  
     
  7. MrDude

    MrDude

    Joined:
    Sep 21, 2006
    Posts:
    2,569
    Yeah, definitely. Because of my belief that the scripts will be nulled I recently deleted all my -= statements from an update I was gonna do at some point, thinking that it is all rather redundant code anyways. Thank goodness I discovered my mistake tonight or else I would have actually gone out of my way to CAUSE a memory leak where I had originally prevented it...

    Not good... :O
     
  8. RandomiaGaming

    RandomiaGaming

    Joined:
    Apr 3, 2019
    Posts:
    12
    public class TestClass : MonoBehaviour
    {
    public static TestClass Instance;
    void Start()
    {
    Instance = this;
    Destroy(gameObject);
    }
    }

    Will TestClass.Instance be null after Start?
     
  9. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    Not immediately after Start, but I would expect it to be null before the next frame. When you call Destroy, the GameObject is only scheduled to be destroyed during the current frame (at the end I believe), not destroyed immediately.
     
    RandomiaGaming likes this.
  10. RandomiaGaming

    RandomiaGaming

    Joined:
    Apr 3, 2019
    Posts:
    12
    Thanks!