Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

'is null' returns false on null objects

Discussion in 'Scripting' started by Edy, Dec 26, 2020.

  1. Edy

    Edy

    Joined:
    Jun 3, 2010
    Posts:
    2,477
    I was converting all my
    obj == null
    comparisons to the C# 7 optimized form
    obj is null
    . This is quite faster than == null, as == is an operator that may be overriden and causes some overhead that can be seen in the profiler.

    However, I've verified that is null returns true on objects that are actually null. Just add this script to a new, empty GameObject:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class NullTest : MonoBehaviour
    4. {
    5.     void Start ()
    6.     {
    7.         MeshFilter nonExisting = GetComponent<MeshFilter>();
    8.  
    9.         Debug.Log(nonExisting);             // null
    10.         Debug.Log(nonExisting == null);     // true
    11.         Debug.Log(nonExisting is null);     // false  (!!!)
    12.         Debug.Log(nonExisting is object);   // true   (!!!)
    13.  
    14.         MeshFilter nullObject = null;
    15.         Debug.Log(nullObject);              // Null
    16.         Debug.Log(nullObject == null);      // true
    17.         Debug.Log(nullObject is null);      // true
    18.         Debug.Log(nullObject is object);    // false
    19.     }
    20. }
    How is this possible? This renders the
    is null
    form totally useless.

    The capitalization when debugging the object itself is slightly different (null vs. Null). So it seems that GetComponent is returning a non-null object, but with the
    ==
    operator overloaded so it returns true when compared with null. Is this really the intended behavior?

    object.ReferenceEquals
    provide the same result as the
    is
    keyword.
     
    Last edited: Dec 26, 2020
    Mehrdad995 likes this.
  2. JoNax97

    JoNax97

    Joined:
    Feb 4, 2016
    Posts:
    611
    Unfortunately, unity's objects depend of that custom null comparison. So, just like you can't trust the
    ?.
    operator, you can't trust
    is null
    .
     
    Mehrdad995 and Edy like this.
  3. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,835
    It's interesting that almost on a daily basis there's someone somewhere in the world discovering the custom == operator of Unity ^^. I think we had a gazillion threads and UA questions about that already.
     
    melipefello likes this.
  4. Edy

    Edy

    Joined:
    Jun 3, 2010
    Posts:
    2,477
    Silly me, how should I ever expected
    <null object> is null
    to return true?

    Not surprisingly, when something works the opposite of what it is obviously supposed to do, then everyone expects it to work the correct way until they find out that it doesn't.
     
    Last edited: Dec 26, 2020
    ChrisVMC likes this.
  5. Edy

    Edy

    Joined:
    Jun 3, 2010
    Posts:
    2,477
    It seems that the ? operator can actually be trusted. Casting the object to boolean returns False on null and True on non-null, in both Unity and standard objects.

    EDIT: Unless you refer to the null-coalescing operator (??), which performs the same comparison as the "is" keyword, so it doesn't work with Unity objects.
     
    Last edited: Dec 26, 2020
    Mehrdad995 and JoNax97 like this.
  6. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,979
    There was never any chance
    is null
    could have worked, no matter what Unity did. The Destroy command takes 1 reference to a gameObject and magically makes every other reference turn to null? That's impossible. If you have
    g=cat1
    , no one can change
    g1
    to null if all they have is another reference to
    cat
    . The best they can do to is zero-out every cat variable. That's basic reference-type stuff. Unity had to use a trick. But they didn't break how null-checks work -- the concept of the very useful Destroy command did that.
     
  7. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Unless you reference it via System.Object or an interface type. It'll then use System.Object's static equality operator and voila, there you go again.
     
  8. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    OK, so...I understand why calling Destroy on an object can't turn all references to that object into true nulls.

    But why does GetComponent (in the OP's test code) return a pretend-null object instead of a true null?
     
  9. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Debugging purposes in the editor.
    (See [1] @ https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/)
     
    Antistone and Bunny83 like this.
  10. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,835
    Watch out, he didn't mean the ternary operator
    ?:
    but the null conditional operators like
    ?.
    or
    ?[]
    .

    Also you can not cast objects to boolean. This only works when the type implements an implicit or explicit type conversion operator which UnityEngine.Object did.

    While I do agree that you can easily fall for this detail, on the other hand it's an intrinsic detail of how Unity uses the managed layer as scripting language and how Unity actually works under the hood. Unity is not a C# engine and at the interface between the engine core and the managed scripting layer there are several kinda unusual phenomenons.


    You already have the wrong initial assumption, If "is null" returns false the object is not null. This statement is always true. However "not null" UnityEngine.Objects can be "unusable" and they fake to be null to indicate that state. As it was explained in the blog I linked they consider this a bad design decision themselfs, but it's with Unity for over 10 years now. They have thought about changing it, however it would be a huge breaking change in almost every single project out there and would also affect a lot of assets in the store. I'm sure that are the main reasons why they haven't done this step yet.

    I'm sorry if it sounded condescending. I didn't mean to imply that this behaviour is obvious. As I said there are probably several dozens cases which do not behave as you might expect. Though understanding the relationship between the engine and the scripting layer usually helps to not fall for those issues and to better understand why it behaves as it does. Another huge part of Unity that doesn't behave as most would expect is serialization. So reading the script serialization docs is highly recommended if you're new to Unity or if you have never heard about it ^^.
     
    JoNax97 likes this.
  11. Edy

    Edy

    Joined:
    Jun 3, 2010
    Posts:
    2,477
    THIS is the actual issue. Maybe I'm missing something evident, but just returning a true null instead of a fake null in GetComponent would make the is keyword and all other null-related operators work as the C# language expects. At the same time the standard comparison operators (== null, != null) will keep working as always.

    I can't see how returning a true null in GetComponent could be a huge dealbreaker, again unless I'm missing something evident. Surely Unity didn't have this problem back in 2014, but now we have C# 6, 7, etc with new null-checking operators that don't work as evidently expected with the values returned by GetComponent.

    I've extended the test code, see below. As you can see, the unexpected problems arise only with the fake-null value returned by GetComponent and the new C# 6+ operators (marked with !!! below). All other cases work just fine. Anyone feel free to propose new test cases.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class NullTest : MonoBehaviour
    4. {
    5.     class TestClass
    6.     {
    7.         public bool member;
    8.     }
    9.  
    10.     void Start ()
    11.     {
    12.         MeshFilter nullObject = null;
    13.         MeshFilter nonExisting = GetComponent<MeshFilter>();
    14.         Transform existing = GetComponent<Transform>();
    15.         MeshFilter defaultObject = default(MeshFilter);
    16.         TestClass classInstance = new TestClass();
    17.         TestClass nullClassInstance = null;
    18.  
    19.         Debug.Log("---- Actual null");
    20.         Debug.Log(nullObject);                                      // Null
    21.         Debug.Log(nullObject == null);                              // True
    22.         Debug.Log(nullObject is null);                              // True
    23.         Debug.Log(nullObject is object);                            // False
    24.         Debug.Log(nullObject ? "Is Not null" : "Is Null");          // Is Null
    25.         Debug.Log(_ = nullObject as object ?? "Was null");          // Was null
    26.         Debug.Log(object.ReferenceEquals(nullObject, null));        // True
    27.  
    28.         Debug.Log("---- Non-existing");
    29.         Debug.Log(nonExisting);                                     // null
    30.         Debug.Log(nonExisting == null);                             // True
    31.         Debug.Log(nonExisting is null);                             // False  (!!!)
    32.         Debug.Log(nonExisting is object);                           // True   (!!!)
    33.         Debug.Log(nonExisting ? "Is Not null" : "Is Null");         // Is Null
    34.         Debug.Log(_ = nonExisting as object ?? "Was null");         // null   (!!!)
    35.         Debug.Log(object.ReferenceEquals(nonExisting, null));       // False  (!!!)
    36.  
    37.         Debug.Log("---- Existing");
    38.         Debug.Log(existing);                                        // GameObject (UnityEngine.Transform)
    39.         Debug.Log(existing == null);                                // False
    40.         Debug.Log(existing is null);                                // False
    41.         Debug.Log(existing is object);                              // True
    42.         Debug.Log(existing ? "Is Not null" : "Is Null");            // Is Not Null
    43.         Debug.Log(_ = existing as object ?? "Was null");            // GameObject (UnityEngine.Transform)
    44.         Debug.Log(object.ReferenceEquals(existing, null));          // False
    45.  
    46.         Debug.Log("---- Default");
    47.         Debug.Log(defaultObject);                                   // Null
    48.         Debug.Log(defaultObject == null);                           // True
    49.         Debug.Log(defaultObject is null);                           // True
    50.         Debug.Log(defaultObject is object);                         // False
    51.         Debug.Log(defaultObject ? "Is Not null" : "Is Null");       // Is Null
    52.         Debug.Log(_ = defaultObject as object ?? "Was null");       // Was null
    53.         Debug.Log(object.ReferenceEquals(defaultObject, null));     // True
    54.  
    55.         Debug.Log("---- Non-Unity.Object");
    56.         Debug.Log(classInstance);                                   // NullTest+TestClass
    57.         Debug.Log(classInstance == null);                           // False
    58.         Debug.Log(classInstance is null);                           // False
    59.         Debug.Log(classInstance is object);                         // True
    60.         // Debug.Log(classInstance ? "Is Not null" : "Is Null");    // Compile error
    61.         Debug.Log(_ = classInstance as object ?? "Was null");       // NullTest+TestClass
    62.         Debug.Log(object.ReferenceEquals(classInstance, null));     // False
    63.  
    64.         Debug.Log("---- Null Non-Unity.Object");
    65.         Debug.Log(nullClassInstance);                                   // Null
    66.         Debug.Log(nullClassInstance == null);                           // True
    67.         Debug.Log(nullClassInstance is null);                           // True
    68.         Debug.Log(nullClassInstance is object);                         // False
    69.         // Debug.Log(nullClassInstance ? "Is Not null" : "Is Null");    // Compile error
    70.         Debug.Log(_ = nullClassInstance as object ?? "Was null");       // Was null
    71.         Debug.Log(object.ReferenceEquals(nullClassInstance, null));     // True
    72.     }
    73. }
     
    Last edited: Jan 11, 2021
    tw00275 likes this.
  12. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,835
    My comment wasn't about GetComponent but UnityEngine.Objects in general. GetComponent does only return fake null object when testing in the editor, not in the actual build. When you build your game GetComponent will return null if the component doesn't exist. Of course that doesn't help much when testing in the editor.

    I agree that the explicit creation of a fake null object when the component does not exist was one of the worst decisions. Though most people who hit a missingReference exception do silently appreciate the indication which object caused it. This would not be possible when you hit a normal NullReferenceException.
     
  13. Edy

    Edy

    Joined:
    Jun 3, 2010
    Posts:
    2,477
    That's even worse, as it directly leads to inconsistent behaviors between the Editor and the build.
    It's surely my fault, but I can't see how this case (a fake null object returned by GetComponent) can be of any help. You call GetComponent<TypeOfComponent>(). TypeOfComponent doesn't exists. Returns a fake null. That fake null is tried to get accessed later and raises an exception in the line that tried to use the fake null object. So there, in that line, you can see the problem. Which additional information comes in the fake null object that is so useful for this? Am I missing something evident?
     
  14. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,240
    WOW, welcome back to your thread Edy... I dunno what the forum rules say about necro-ing your own post to reply three years later, but well, here you are!!

    I do know this however: almost everything about Unity3D example code, developer mindset, tutorials, and everything else relies critically on this quirky "special" null-boolean-true-whatever-you-wanna-call-it handling that Unity implemented long ago.

    Was it smart? It had a point at the time to allow people to write C# code more like Javascript or Python code.

    Is it going to change? Perhaps, but I wouldn't count on it anytime soon.

    My company employs about 60 engineers who use this stuff daily... and we use it successfully. I cannot even think of the last time anybody even skipped a beat working with it. We certainly never have any hard bugs about it.

    We recognize this quirk as an important part of how Unity works.

    It actually doesn't MATTER that it "isn't right." It simply IS.

    Therefore I urge you not to let it slow you down, not to waste your life agonizing about it.

    Understand how to use it, adapt your thinking to it, and soon you'll find that it is really quite easy.

    It might even help you to think "This isn't C# but rather UnityC#."

    Have fun making games! That's what it's all about after all.
     
    Last edited: Nov 28, 2023
    SisusCo likes this.
  15. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    2,274
    Holy necro!

    If you call Debug.Log(message) and then double-click the message in the console, you get nothing but the call stack.

    If you call Debug.Log(message, object) and then double-click the message in the console, the object which was given will be "pinged" in the hierarchy, as well as getting the call stack.

    If you cause a C# Null Reference Exception, the underlying C# error message results in a call of the first form. You have the call stack, and that's all.

    If you cause Unity's Unassigned Reference Exception, the message uses the second form, with the component which actually has the reference field that needs to be filled in. The hierarchy will ping the gameobject that has a "Missing" reference.
     
    Last edited: Nov 28, 2023
    SisusCo and Kurt-Dekker like this.
  16. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,199
    This is why we use
    TryGetComponent<T>(out T component)
    , which allows for cleaner code while providing a real null object, and not a fake null.

    Pretty much no reason to use
    GetComponent<T>
    these days.
     
    Edy likes this.
  17. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,835
    Well, the benefit is minimal. Though to quote the blog post which explains that:
     
    SisusCo likes this.
  18. Edy

    Edy

    Joined:
    Jun 3, 2010
    Posts:
    2,477
    Thing is that I had received a notification related to this thread, and I assumed that the latest reply was just added. Didn't check the date, so I just replied to it :oops:

    Didn't know about that! That's really useful info indeed.