Search Unity

HELP: component?.gameobject == null results in exception

Discussion in 'Scripting' started by wpatel, Aug 12, 2019.

  1. wpatel

    wpatel

    Joined:
    Sep 27, 2018
    Posts:
    9
    Hello all,

    First time I'm posting so I apologize if I haven't followed a convention.

    Issue:
    I've been working on a Unity project for over a year now and I'm no stranger to the following exception:
    MissingReferenceException: The object of type 'DropboxAPI_Node' has been destroyed but you are still trying to access it.
    To resolve this, I usually follow this pattern but now I'm getting an exception:
    Code (CSharp):
    1. // Assume IDialogs are always  Monobehaviors
    2. List<IDialog> behaviours = GetDialogs();
    3.  
    4. // This line usually always avoids a GameObject destroyed error.
    5. // But, for this one object, it is not.
    6. var first = behaviours.FirstOrDefault(x=> x !=null && x.gameObject !=null);
    Comparatively, if I do this, I get no exception.
    Code (CSharp):
    1.  // Assume IDialogs are always  Monobehaviors
    2. List<IDialog> behaviours = GetDialogs();
    3.  
    4. // Below throws exception at x.gameObject
    5. var first = behaviours.FirstOrDefault(x=> (x as MonoBehaviour) !=null && (x as MonoBehaviour).gameObject !=null);

    Additional Information:
    • I am using an interface and I have guaranteed that the IDialog in question is not overriding and/or hiding the default MonoBehaviour.gameObject implementation.
    • As a safety net, I created a GetDialog() method so the IDialog can access the gameObject as is provided by the Monobehaviour class but I still get the "GameObject already destroyed error":
      Example:
      Code (CSharp):
      1. public class MyDialog: MonoBehaviour, IDialog
      2.  
      3. {
      4. public GameObject GetGameObject()
      5. {
      6.  
      7. // Throws the error.
      8. if( this.gameObject == null) { return null; }
      9. return this.gameObject;
      10. }
      11.  
      12. }
    • The error is consistent - in other words, it's not a race condition regarding the timing of when the object died or was accessed.

    Solution:
    So my casting solution works but I don't necessarily like it.
    Rather, if I know why this is happening in the first place, I'd be a bit more assured I won't get a random exception in the future.

    [EDIT] - typo on gameObjects.
     
    Last edited: Aug 12, 2019
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,689
    Do you have a property called .gameobject (all lowercase) somewhere?

    If so I recommend naming it to something actually meaningful, because in your MyDialog.GetGameobject() method above you are accessing both this.gameObject and this.gameobject and those are different things, the latter not being part of Unity.
     
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    What does your IDialog interface look like?

    What does your implementations of IDialog (like MyDialog) look like, and what is the implementation of this 'gameobject' property you should have defined on IDialog.

    I'm also with Kurt-Dekker, having a similarly named property like that is bad. You could just have IDialog define a 'IDialog.gameObject' property instead, and when your MyDialog implements IDialog C# will just use the already existing 'gameObject' property of the MonoBehaviour for the interface.
     
  4. wpatel

    wpatel

    Joined:
    Sep 27, 2018
    Posts:
    9
    Hey,
    thanks for the response.

    I agree with you and actually did exactly what you recommended. The gameobject property is auto-implemented just by being a MonoBehaviour. No new keywords or overrides present.


    With the above being said, even if I did override the behavior of gameObject, casting it to MonoBehaviour really shouldn't affect the comparison operator seeing that I'm working with interfaces and not direct classes. (Unless I'm missing something about .net)

    Here's the code - still in a messy state but hopefully readable
    It won't compile on your side because it has a few dependencies:

    Code (CSharp):
    1.     public interface IDialog: IComponent
    2.     {
    3.         GameObject Shadow { get; }
    4.         GameObject MainPanel { get; }
    5.         Xlate_Text Text_Title { get; }
    6.         bool IsVisible { get; }
    7.         void Show(DialogMode mode);
    8.         void Hide();
    9.         void RegisterAsDialog();
    10.         void DeregisterAsDialog();
    11.         Guid ID { get; }
    12.         string AutomationNamespace { get; }
    13.         void FindRequiredUiElements();
    14.     }
    15.  
    16. // This is the base interface which should be
    17. // auto-implemented by every monobehavior
    18. // I create.
    19.     public interface IComponent
    20.     {
    21.         GameObject gameObject { get; }
    22.         Transform transform { get; }
    23.     }
    24.  
    25. // The Dialog class is rather big and won't compile
    26. // on your side.
    27.     public class Dialog_EditDropboxApi : MonoBehaviour, IDialog
    28.     {
    29.         public Guid ID { get; private set; }
    30.         public string MainEvent= "DropboxAPIKey";
    31.         public string AutomationNamespace => "Dialog.Dropbox";
    32.         public GameObject MainPanel { get; private set; }
    33.         public GameObject Shadow { get; private set; }
    34.         public Xlate_Text Text_Title { get; private set; }
    35.  
    36.         public bool IsVisible
    37.         {
    38.             get
    39.             {
    40.                 if (!HasAwoken) { Awake(); }
    41.                 if (MainPanel == null) { return false; }
    42.                 bool visible = MainPanel.activeSelf;
    43.                 Shadow.SetActive(visible);
    44.                 return visible;
    45.             }
    46.         }
    47.  
    48.         public bool HasAwoken { get; private set; } = false;
    49.  
    50.  
    51.         public InputField InputField_DropboxSetting { get { return inputField_DropboxSetting; } }
    52.         [SerializeField] private InputField inputField_DropboxSetting;
    53.         IUnityLogger Log = null;
    54.         public bool Autosave = true;
    55.         private DataBinder binder;
    56.  
    57.         private void Awake()
    58.         {
    59.             if (!Core.IsCoreInitialized)
    60.             {
    61.                 Debug.LogError("Core failed!");
    62.                 return;
    63.             }
    64.  
    65.             Log = LogHelper_Adaptor.GetLogger(this.GetType());
    66.             Initialize();
    67.             HasAwoken = true;
    68.         }
    69.  
    70.         private void Initialize()
    71.         {
    72.             Log.Debug("Initialize");
    73.             ID = Guid.NewGuid();
    74.             FindRequiredUiElements();
    75.             InitializeUiElements();
    76.             SubscribeToEvents();
    77.             RegisterAsDialog();
    78.         }
    79.  
    80.         public void FindRequiredUiElements()
    81.         {
    82.             Shadow = SceneObjects.Find(AutomationNamespace + ".Shadow", true)?.gameObject;
    83.             if (Shadow == null)
    84.             {
    85.                 Log.Error("Shadow Panel was not found by automation ID");
    86.             }
    87.  
    88.             MainPanel = SceneObjects.Find(AutomationNamespace + ".MainPanel", true)?.gameObject;
    89.             if (MainPanel == null)
    90.             {
    91.                 Log.Error("MainPanel was not found by automation ID");
    92.             }
    93.  
    94.             // This one isn't actually important. More for consistency.
    95.             Text_Title = SceneObjects.Find<Xlate_Text>(AutomationNamespace + ".Title", includeInactive: true);
    96.             if (Text_Title == null)
    97.             {
    98.                 Log.Error("Could not find Title through Automation Id.");
    99.             }
    100.  
    101.             // Databind.
    102.             binder = gameObject?.AddComponent<DataBinder>();
    103.         }
    104.  
    105.         private void SubscribeToEvents()
    106.         {
    107.  
    108.             // Event handler
    109.             if (inputField_DropboxSetting == null) { return; }
    110.             inputField_DropboxSetting.onEndEdit.RemoveAllListeners();
    111.             inputField_DropboxSetting.onEndEdit.AddListener(OnSettingEdited);
    112.         }
    113.  
    114.         private void InitializeUiElements()
    115.         {
    116.  
    117.             if (binder != null)
    118.             {
    119.                 binder.Set();
    120.             }
    121.  
    122.             if (InputField_DropboxSetting != null)
    123.             {
    124.                 inputField_DropboxSetting.inputType = InputField.InputType.Password;
    125.             }
    126.         }
    127.  
    128.         private void OnSettingEdited(string newValue)
    129.         {
    130.             //TODO
    131.         }
    132.  
    133.         public void Save()
    134.         {
    135.            // TODO;
    136.         }
    137.  
    138.         /// <summary>
    139.         /// Call this only if the ui is out of sync. The <see cref="binder"/> should be keeping values in sync.
    140.         /// </summary>
    141.         public void Sync()
    142.         {
    143.             //TODO:
    144.         }
    145.  
    146.         public void Show(DialogMode viewMode)
    147.         {
    148.             if (IsVisible)
    149.             {
    150.                 Log.Info("Dialog already visible!");
    151.                 return;
    152.             }
    153.  
    154.             MainPanel.SetActive(true);
    155.             Shadow.SetActive(true);
    156.         }
    157.  
    158.         public void Hide()
    159.         {
    160.             if (IsVisible)
    161.             {
    162.                 Log.Info("Dialog already visible!");
    163.                 return;
    164.             }
    165.  
    166.             MainPanel.SetActive(false);
    167.             Shadow.SetActive(false);
    168.         }
    169.  
    170.         private void UnsubscribeFromEvents()
    171.         {
    172.             inputField_DropboxSetting?.onEndEdit?.RemoveAllListeners();
    173.         }
    174.  
    175.         private void OnDestroy()
    176.         {
    177.             UnsubscribeFromEvents();
    178.             DeregisterAsDialog();
    179.         }
    180.  
    181.         #region IDialog
    182.  
    183.         public void RegisterAsDialog()
    184.         {
    185.             Core.DialogManager?.RegisterDialog(this);
    186.         }
    187.  
    188.         public void DeregisterAsDialog()
    189.         {
    190.             Core.DialogManager?.DeregisterDialog(this);
    191.         }
    192.         #endregion
    193.     }
     
  5. wpatel

    wpatel

    Joined:
    Sep 27, 2018
    Posts:
    9
    Sorry, gameObject vs. gameobject was a typo. They are the same.
     
  6. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    Destroyed GameObjects and Components aren't actually null. Unity just pretends that they are null through various methods (most notably by overriding the == operator) because they believe that's a convenient simplification. But if you look at the actual bit pattern stored in that variable, it's not null.

    Those overrides only apply when one of the operands is a Unity type. If you cast your variable to type "object", or use Object.ReferenceEquals, then you bypass Unity's overridden operator and test whether the variable is actually null. This does not tell you whether the GameObject or Component has been destroyed.

    If your code already relies on every implementation of IDialog being a MonoBehaviour, you might consider turning it into an abstract base class instead of an interface to make that requirement explicit. Then you wouldn't need to cast here, because your dialogs would already be a subtype of MonoBehaviour.
     
    Kiwasi and lordofduct like this.
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    Yeah, it's because x is of type IDialog and so the == overload isn't getting applied.

    As @Antistone said you can change to an abstract base class.

    Or, if you like interfaces (as do I), you can create an extension method for type 'IComponent' that checks this. Something like "IsAlilve" that does the checking for you:

    Code (csharp):
    1.  
    2. public static class IComponentExtensions
    3. {
    4.    
    5.     public static bool IsAlive(this IComponent c)
    6.     {
    7.         if(c == null) return false;
    8.         if(c is UnityEngine.Object) return (c as UnityEngine.Object) != null;
    9.         return true;
    10.     }
    11.    
    12. }
    13.  
    And replace your code with:
    Code (csharp):
    1. var first = behaviours.FirstOrDefault(x=> x.IsAlive()); //no need to check the gameObject, because if it was dead, so would the component
     
    wpatel and Kiwasi like this.
  8. wpatel

    wpatel

    Joined:
    Sep 27, 2018
    Posts:
    9
    @Antistone, @lordofduct

    Thanks for the responses :)
    I guess I'm surprised that it's using .net's object comparison instead of going to the youngest child comparison operator even though I'm using an interface.

    Last question is
     Monobehaviour.Equals() 
    the same as
    Monobehaviour == someObject 
    ?
     
  9. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    I haven't tested, but the Unity scripting docs include a listing for UnityEngine.Object.operator== and no listing for UnityEngine.Object.Equals, so I would imagine that myMonoBehaviour.Equals() is falling through to the System.object implementation.
     
  10. wpatel

    wpatel

    Joined:
    Sep 27, 2018
    Posts:
    9
    @Antistone

    Yeah, they weren't given identical answers in my testing so that was no use.
    I think I'll just remove the IComponent interface altogether.
    Defining anything in the interface to check for is destroyed ( such as the IsAlive() method @lordofduct mentioned) doesn't work because the Component itself may be destroyed in which case trying to access any of it's methods or properties still throws an exception.
    Casting seems to be the only answer.

    Thanks again guys!
     
  11. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    Note that the example I showed implemented it as an extension method. It won't throw an exception if it's null, that's not how extension methods work. And the method safely checks if it's null as it's first line of code.

    Really all an extension method is, is a static method that allows for some syntax sugar to make it look like a member method.

    So when you say:
    Code (csharp):
    1. x.IsAlive()
    The compiler see's it as:
    Code (csharp):
    1. IComponentExtensions.IsAlive(x)
    More about extension methods:
    https://docs.microsoft.com/en-us/do...g-guide/classes-and-structs/extension-methods
     
  12. wpatel

    wpatel

    Joined:
    Sep 27, 2018
    Posts:
    9
    Oh; that's magic. That's perfect. Thanks!