Search Unity

Unexpected Constructor behavior

Discussion in 'Scripting' started by CallMeSpam, Feb 10, 2018.

  1. CallMeSpam

    CallMeSpam

    Joined:
    Feb 10, 2018
    Posts:
    19
    I am new to Unity and while playing around with the Unity (2017.3) I noticed an anomaly and was wondering if someone could explain it to me. In the example below, attached to a simple cube in a new project, there are two classes: DataHolder and ADataHolder that are identical. However, DataHolder's constructor is called at declaration and ADataHolder's constructor is called in Awake(). I would expect more or less the same result from each.

    the console shows that DataHolder's constructor is called twice, then ADataHolder's constructor is called once, and when I stop the game, DataHolder's constructor is called a third time. Why is DataHolder being called multiple times?

    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class DemoProblem : MonoBehaviour
    7. {
    8.     DataHolder someDataHolder = new DataHolder();   // Initilaization called twice...
    9.     ADataHolder anotherDataHolder = null;
    10.    
    11.     void Awake ()
    12.     {
    13.         anotherDataHolder = new ADataHolder();  //Initialization called once as expected
    14.    }
    15. }
    16.  
    17. public class DataHolder
    18. {
    19.     private static int i = 0;   //count How many times initialization happens....
    20.     public DataHolder()
    21.     {
    22.         i++;
    23.         Debug.Log("DataHolder initialization called "+i+(i==1 ? " time. ": " times. "+"\n"));
    24.     }
    25. }
    26.  
    27. public class ADataHolder
    28. {
    29.     private static int i = 0;   //count How many times initialization happens....
    30.     public ADataHolder()
    31.     {
    32.         i++;
    33.         Debug.Log("ADataHolder initialization called " + i + (i == 1 ? " time." : " times. " + "\n"));
    34.     }
    35. }
    36.  

    Console output is:
    DataHolder initialization called 1 time.
    UnityEngine.Debug:Log(Object)
    DataHolder:.ctor() (at Assets/DemoProblem.cs:22)
    DemoProblem:.ctor() (at Assets/DemoProblem.cs:7)


    DataHolder initialization called 2 times.
    UnityEngine.Debug:Log(Object)
    DataHolder:.ctor() (at Assets/DemoProblem.cs:22)
    DemoProblem:.ctor() (at Assets/DemoProblem.cs:7)


    ADataHolder initialization called 1 time.
    UnityEngine.Debug:Log(Object)
    ADataHolder:.ctor() (at Assets/DemoProblem.cs:32)
    DemoProblem:Awake() (at Assets/DemoProblem.cs:12)


    DataHolder initialization called 3 times.
    UnityEngine.Debug:Log(Object)
    DataHolder:.ctor() (at Assets/DemoProblem.cs:22)
    DemoProblem:.ctor() (at Assets/DemoProblem.cs:7)
     
  2. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    I suspect it has to do with how Unity handles things in the editor. If you were to test this on an actual build, I think you'd see the results you'd expect, but because you're using PlayMode in the editor it's confusing the issue a bit because of the way the MonoBehaviour is being serialized and deserialized, even though the field itself is not specifically serialized.

    It may be best to think of each GameObject and each Component in a scene as having to be destroyed and recreated when Play is pressed, and then destroyed and recreated again when it's stopped- Awake is called only once because there's a specific point in the process when that call is made, but the inline initialization may need to be repeated because of the way the editor simulates building and unbuilding the game in real time. Does that make sense?

    This usually isn't really a concern- the editor isn't really focused around being super-efficient, as that's a concern better left for runtime features. If you want, you can focus on initializing everything in Awake and Start instead of inline though- just keep in mind that serializable fields (public, or with SerializeField attribute attached) are automatically initialized and treated like structs in the inspector.
     
    Last edited: Feb 10, 2018
    CallMeSpam and Suddoha like this.
  3. CallMeSpam

    CallMeSpam

    Joined:
    Feb 10, 2018
    Posts:
    19
    Thanks! Even if it works correctly in an actual build, unexpected constructors could make debugging a bit dicey. I guess I'll run constructors separately from declaration where possible.
     
  4. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Yes, it's a mix of usual C# stuff triggered by the serialization engine and the backups that are needed to restore states prior to the play-mode.

    Here's a rather simple example, attaching a component C, i.e instance of C must be created:
    1. Field initializers will be called as part of the construction process, this might trigger constructors if the expression requires construction of an object (note that constructed reference types may in turn call field initializers and constructors...)
    2. Field initializers of base classes will be called, same notes as above
    3. C's constructor will be executed (you usually don't implement anything in a MonoBehaviours or SO's constructor anyway, but it'll be called if available)
    4. C's base class constructors will be called and so on...
    5. Serialization engine creates instances of serialized fields that have not yet been initialized by field initializers
    • involves a constructor call for reference types if param-less (default or explicit) constructor is available, this may rely on reflection directly or a call to the activator class
    • may use creation of uninitialized instances if the a param-less constructor is not available
    That's already quite alot.

    Starting play mode, there are even more steps involved. Copies will be made in order to be able to restore the state prior to play-mode changes, the actual component will be instantiated and so on... This essentially involves more or less all the steps above, potentially multiple times, plus some additional constructions of serialized fields.

    On exit, everything will be restored which works similarly to attaching the component, except that the component's fields will now be injected using the copies that were made earlier...

    Something like that..
    Just try not to rely on it as this is neither officially documented nor guaranteed to stay the same.
    Personally, I try to avoid field-initializers and move all the logic to "Awake".
    However, you will never get around having constructors called for serialized fields, in some rare cases these might cause weird bugs indeed, luckily I've never encountered this problem so far.
     
    Last edited: Feb 11, 2018
    CallMeSpam likes this.
  5. CallMeSpam

    CallMeSpam

    Joined:
    Feb 10, 2018
    Posts:
    19
    Thanks for the painless lesson on the nuances of constructors and the editor. Just for fun, I did test it on a windows build and everything works as it should.