Search Unity

"Usage of null propagation on Unity Objects is incorrect"

Discussion in 'Scripting' started by Ne0mega, May 9, 2022.

  1. Ne0mega

    Ne0mega

    Joined:
    Feb 18, 2018
    Posts:
    755
    Minor thing in visual studio (2019) I see:
    Code (csharp):
    1.  
    2.         mobile = GetComponent<Mobile>();
    3.         mobile?.Activate();
    4.  
    Visual Studio (2019) has a a few dots under the "mo" in "mobile?.Activate();" and tooltip says, "Usage of null propagation on Unity Objects is incorrect"


    However, it seems to understand and do the job, as I have attached no Mobile component, and got no null errors.
     
  2. karliss_coldwild

    karliss_coldwild

    Joined:
    Oct 1, 2020
    Posts:
    602
    It may work in some cases but there are common cases where it doesn't work. So unfortunately the most reliable approach is not using it for Unity objects. This is mostly due to differences between object lifetime in Unity engine and C#. Once an Unity object gets destroyed due to
    DestroyObject
    , scene switching or some other reasons c# property will still point to non null class instance (and potentially prevent fully garbage collecting it) thus
    .?Foo()
    will think that object is not null, but from Unity perspective object is destroyed from scene so it's not usable so trying to interact with it will throw an exception. From what I understand Unity has overridden how equality comparison operator works so that
    if (foo == null)
    behaves as if foo was null for objects that are destroyed in scene but not actually null. But this custom logic doesn't apply when using questionmark operator. You can still use questionmark operator for your own classes which don't inherit directly or indirectly from UnityEngine.Object (MonoBehavior, GameObject, ScriptableObject and few others). But it depends on you and your team to choose if benefits of questionmark operator outweighs the additional mental load of having to track which objects inherit from Unity Object and which ones do not. Some people may choose to completely ban it's usage in Unity projects as that's a simpler rule to follow than not allowing it's usage in specific situations.
     
    Gernata, gooby429, RoboDrone and 2 others like this.
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,912
    To summarise, as you can't override ?. or ?? or ??= operators, Unity can't do all the checks it normally does when you go (unityObject == null) etc. So it's advised not to do so with UnityEngine.Object's as good practice.
     
    Last edited: May 9, 2022
  4. karliss_coldwild

    karliss_coldwild

    Joined:
    Oct 1, 2020
    Posts:
    602
    Here are some code examples to illustrate the problematic cases.

    Code (CSharp):
    1. var foo = GetComponent<Foo>();
    2. foo.bar(); // let's assume this succeds
    3.  
    4. Destroy(foo); // component got destroyed
    5. // foo still points to non null C# object
    6.  
    7. // Not sure if Unity always destroys object immediately or not, so the demo might not work exactly out of the box.
    8. // In a more realistic situation where you stumble across this this it would be spread across multiple scripts and Update() calls and foo stored inside member field instea of local variable.
    9. if (foo == null) {
    10.     Debug.Log("equal null"); // Unity will execute this
    11. } else {
    12.     foo.bar(); // not this
    13. }
    14.  
    15. foo?.bar(); // Unity/Mono will try to call bar even though previous if said it's null resulting in exception. From C# perspective it's not null.
    16.  

    Code (CSharp):
    1. // A.cs
    2.  
    3. C foo;
    4. void Start() {
    5.     foo = GameObject.Find("foo").GetComponent<C>(); // potentially exception here but that's not the point of example
    6. }
    7.  
    8. void Update() {
    9.     if (foo == null) { // this will work fine
    10.         foo.bar();
    11.     }
    12.     foo?.bar(); // this will result in exception after DestroyC()
    13. }
    14.  
    15. // B.cs
    16.  
    17. C foo;
    18.  
    19. void SpawnC()
    20. {
    21.     foo = Instantiate(fooPrefab);
    22. }
    23.  
    24. void DestroyC()
    25. {
    26.     if (foo != null)
    27.     {
    28.         Destroy(foo.gameObject);
    29.     }
    30. }
     
    Gernata, V5Studio, K3CK and 1 other person like this.
  5. mafiasniper

    mafiasniper

    Joined:
    Jul 29, 2020
    Posts:
    3
    Since the question's already been answered. I'm just gonna share an example from my code when this won't work.

    Code (CSharp):
    1. private IEnumerator SendRocketsUp(List<RocketBH> rockets) {
    2.         yield return new WaitForSeconds(timeBeforeAscend);
    3.         rockets.ForEach(r => {
    4.             r?.GoUp();
    5.         });
    6.     }
    Rockets can automatically get destroyed before this "timeBeforeAscend" delay. If they do, "r?" is unable to check if they are destroyed or not and always calls the "GoUp()" function on RocketBG script which is already destroyed.
     
  6. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,513
    Further reading from the Unity blog: Custom == operator, should we keep it?
    They were leaning toward getting rid of it a decade ago.

    When you do this in Unity:
    if (myGameObject == null) {}

    Unity does something special with the == operator. Instead of what most people would expect, we have a special implementation of the == operator.

    This serves two purposes:

    1) When a MonoBehaviour has fields, in the editor only[1], we do not set those fields to "real null", but to a "fake null" object. Our custom == operator is able to check if something is one of these fake null objects, and behaves accordingly. While this is an exotic setup, it allows us to store information in the fake null object that gives you more contextual information when you invoke a method on it, or when you ask the object for a property. Without this trick, you would only get a NullReferenceException, a stack trace, but you would have no idea which GameObject had the MonoBehaviour that had the field that was null. With this trick, we can highlight the GameObject in the inspector, and can also give you more direction: "looks like you are accessing a non initialised field in this MonoBehaviour over here, use the inspector to make the field point to something".
    purpose two is a little bit more complicated.

    2) When you get a c# object of type "GameObject"[2], it contains almost nothing. this is because Unity is a C/C++ engine. All the actual information about this GameObject (its name, the list of components it has, its HideFlags, etc) lives in the c++ side. The only thing that the c# object has is a pointer to the native object. We call these c# objects "wrapper objects". The lifetime of these c++ objects like GameObject and everything else that derives from UnityEngine.Object is explicitly managed. These objects get destroyed when you load a new scene. Or when you call Object.Destroy(myObject); on them. Lifetime of c# objects gets managed the c# way, with a garbage collector. This means that it's possible to have a c# wrapper object that still exists, that wraps a c++ object that has already been destroyed. If you compare this object to null, our custom == operator will return "true" in this case, even though the actual c# variable is in reality not really null.
     
    AniolFolch and mafiasniper like this.