Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Resolved is keyword acting weird.

Discussion in 'Scripting' started by elmerirusi, Jul 28, 2023.

  1. elmerirusi

    elmerirusi

    Joined:
    Sep 8, 2020
    Posts:
    7
    Is the "is" keyword acting weirdly here? Or is it working as expected?

    The expected result was for the last two logs to display "is null".
    Screenshot 2023-07-28 105856.png

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class DebugTest : MonoBehaviour
    4. {
    5.     [SerializeField] private GameObject m_Target = null;
    6.     [SerializeField] private GameObject m_Target2 = null;
    7.     private void Awake()
    8.     {
    9.         Debug.Log("_______________________________________________");
    10.         Debug.Log($"m_Target: {m_Target}");
    11.         Debug.Log(m_Target == null ? "m_Target is null" : "m_Target is not null");
    12.         Debug.Log(m_Target != null ? "m_Target is not null" : "m_Target is null");
    13.         Debug.Log(m_Target is null ? "m_Target is null" : "m_Target is not null");
    14.         Debug.Log(m_Target is not null ? "m_Target is not null" : "m_Target is null");
    15.         Debug.Log("_______________________________________________");
    16.         Debug.Log($"m_Target2: {m_Target2}");
    17.         Debug.Log(m_Target2 == null ? "m_Target2 is null" : "m_Target2 is not null");
    18.         Debug.Log(m_Target2 != null ? "m_Target2 is not null" : "m_Target2 is null");
    19.         Debug.Log(m_Target2 is null ? "m_Target2 is null" : "m_Target2 is not null");
    20.         Debug.Log(m_Target2 is not null ? "m_Target2 is not null" : "m_Target2 is null");
    21.         Debug.Log("_______________________________________________");
    22.     }
    23. }
    24.  
     
  2. RadRedPanda

    RadRedPanda

    Joined:
    May 9, 2018
    Posts:
    1,593
    I believe this has to do with what people call Unity's "fake null". You can look at this to see what that's all about.

    https://blog.unity.com/technology/custom-operator-should-we-keep-it

    Essentially, since you exposed
    m_Target2
    to the Inspector, instead of actually being null, it has been replaced with Unity's "fake null" value, which Unity has done some custom implementations for comparison operators, but I believe they did not do it for "is". I also think that what you have replicated would only happen in Editor mode, and would behave fine in a Built project.
     
  3. elmerirusi

    elmerirusi

    Joined:
    Sep 8, 2020
    Posts:
    7
    Thanks. I thought I was going insane.
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,843
    Namely because they can't. Nor can they override
    ?.
    ,
    ??
    and
    ??=
    null coalescing operators, which is why they also should not be used with Unity objects either.

    Ternary operators are fine though.
     
    Bunny83 likes this.
  5. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    4,019
    Wait ... as far as I understand it, this has nothing to do with Inspector fields. It is the same behaviour also for non-serialized private fields or local variables.

    Please correct me if I'm wrong. ;)

    In any case, within Unity whenever you want to check for "null" of an object that derives from UnityEngine.Object (eg MonoBehaviour, ScriptableObject, Texture2D, MeshRenderer, etc etc etc etc you get the point) you should always write it explicitly:
    Code (CSharp):
    1. if (someUnityEngineObject != null)
    2. {
    3.     // access someUnityEngineObject properties or call its methods here
    4. }
    Whereas this will fail:
    Code (CSharp):
    1. if (someUnityEngineObject)
    2. {
    3.     // this block is never run, the above condition will always evaluate to false!
    4. }
    Same goes for your "is" and "is null" checks as well as ? and ?? operators as mentioned before.
     
    Last edited: Jul 28, 2023
  6. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,843
    No it won't? It uses the exact same comparison method as the overloaded
    ==
    and
    !=
    operators. It's an implicit operator specifically set up by Unity.
     
  7. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    4,019
    Had to confirm this real quick in case I was remembering 10+ years old best practices that have long been overthrown ... but no, just checking for the object's "state" (what do you call that?) rather than explicitly checking against null will fail to run the second if statement even though the object isn't null:
    upload_2023-7-28_11-15-37.png

    I did however phrase it the other way around: I implied the condition will be true even if the object is null. I updated the code comment. ;)
     
  8. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,843
    That's kind of weird as the operator is just doing this:
    Code (CSharp):
    1. public static implicit operator bool(Object exists)
    2. {
    3.     return !CompareBaseObjects(exists, null);
    4. }
    So it's looks to be the same as comparing to null. The
    CompareBaseObjects
    is also used by the
    ==
    and
    !=
    operators.
     
    orionsyndrome likes this.
  9. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,058
    Yeah this fake null is quite annoying when you don't know about it.
    Rider didn't warn either. It does for the
    ?
    and
    ??
    operator but not for the
    is
    keyword.

    I do use the is keyword quite often because it is easy to read. Guess I'll have to check projects whether I am doing this with
    UnityEngine.Object
    types or not.

    Let's test that with an actual build. It should give the same results.

    Script:
    Code (CSharp):
    1. using System.Collections;
    2. using UnityEngine;
    3.  
    4. namespace Testing
    5. {
    6.     public class DestroyGameObject : MonoBehaviour
    7.     {
    8.         public IEnumerator Start()
    9.         {
    10.             var target = new GameObject();
    11.             yield return StartCoroutine(IsNormalNullCheck(target));
    12.        
    13.             target = new GameObject();
    14.             yield return StartCoroutine(IsNullCheck(target));
    15.  
    16.             yield return null;
    17.        
    18.             #if UNITY_EDITOR
    19.                 UnityEditor.EditorApplication.ExitPlaymode();
    20.             #else
    21.                 Application.Quit();
    22.             #endif
    23.         }
    24.  
    25.         private IEnumerator IsNormalNullCheck(GameObject target)
    26.         {
    27.             Debug.Log($"Before Destroy '== null': {target == null}");
    28.             Destroy(target);
    29.             Debug.Log($"After Destroy '== null': {target == null}");
    30.             yield return null;
    31.             Debug.Log($"After Update '== null': {target == null}");
    32.         }
    33.  
    34.         private IEnumerator IsNullCheck(GameObject target)
    35.         {
    36.             Debug.Log($"Before Destroy 'is null': {target is null}");
    37.             Destroy(target);
    38.             Debug.Log($"After Destroy 'is null': {target is null}");
    39.             yield return null;
    40.             Debug.Log($"After Update 'is null': {target is null}");
    41.         }
    42.     }
    43. }

    Editor:
    Before Destroy '== null': False
    After Destroy '== null': False
    After Update '== null': True

    Before Destroy 'is null': False
    After Destroy 'is null': False
    After Update 'is null': False

    Build IL2CPP:
    Before Destroy '== null': False
    After Destroy '== null': False
    After Update '== null': True

    Before Destroy 'is null': False
    After Destroy 'is null': False
    After Update 'is null': False

    So exact same behaviour as you'd expect. Doesn't matter whether it is a build or not.

    Conclusion: You should not use the
    is
    keyword nor
    ?
    and
    ??
    operators on
    UnityEngine.Object
    types.


    I've opened up an issue on Rider's support to add a warning to this type of lifetime check for Unity objects.
     
    Last edited: Jul 28, 2023
    Chubzdoomer and CodeSmile like this.
  10. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    4,019
    I tried the other combinations (Unity 2022.3.5f1) but no, none of the (object) and (!object) conditions evaluate to true:
    upload_2023-7-28_11-30-14.png

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class NullTest : MonoBehaviour
    4. {
    5.     [SerializeField] private GameObject m_ShouldNotBeNull = null;
    6.     [SerializeField] private GameObject m_ExpectedToBeNull = null;
    7.  
    8.     void OnEnable()
    9.     {
    10.         if (m_ShouldNotBeNull != null)
    11.             Debug.Log("(m_ShouldNotBeNull != null) is true");
    12.         if (m_ShouldNotBeNull)
    13.             Debug.LogWarning("(m_ShouldNotBeNull) is true");
    14.         if (!m_ShouldNotBeNull)
    15.             Debug.LogWarning("(!m_ShouldNotBeNull) is true");
    16.  
    17.         if (m_ExpectedToBeNull == null)
    18.             Debug.Log("(m_ExpectedToBeNull == null) is true");
    19.         if (m_ExpectedToBeNull)
    20.             Debug.LogWarning("(m_ExpectedToBeNull) is true");
    21.         if (!m_ExpectedToBeNull)
    22.             Debug.LogWarning("(!m_ExpectedToBeNull) is true");
    23.     }
    24. }
    25.  
     
  11. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,058
    Perhaps you've turned off warnings?

    It works as expected here.
    I'm receiving "The object was null" in the console.

    Code (CSharp):
    1. private IEnumerator NullCheck(GameObject target)
    2. {
    3.     if(target)
    4.         Debug.Log("Is not null");
    5.  
    6.     Destroy(target);
    7.     yield return null;
    8.  
    9.     if(target)
    10.         Debug.Log($"Is not null (should be null) '{target}'");
    11.     else
    12.         Debug.Log("The object was null");
    13. }
     
    Last edited: Jul 28, 2023
    orionsyndrome likes this.
  12. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    4,019
    :eek:

    Damn!!! :D

    Thanks for pointing this out. Yeah, I was in one of the Synty demo scenes with hundreds of "BoxColliders don't support negative size" warnings so I had them disabled. :rolleyes:

    Here's what I get now:
    upload_2023-7-28_11-55-32.png
     
  13. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,051
    It does! There's two places where "fake null" appears, as the linked article points out: When Unity Objects get destroyed and, only in the editor, for all Unity Object fields that are exposed in the editor but not assigned.

    The second is to help debugging. You might notice that sometimes you get a
    MissingReferenceException
    instead of a
    NullReferenceException
    . That's Unity-specific and basically an enhanced null reference exception that tells you which script and field the missing reference comes from. Unity does this by assigning null inspector references a special "fake null" object that carries the original script / field information (so that information is preserved, even if you pass the "null" reference around in code).
     
    orionsyndrome, Bunny83 and CodeSmile like this.
  14. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,524
    Your test case is NOT what the op had. Yes, fake null objects still exists at runtime, especially when you destroy objects (explicitly through Destroy or implicitly through a scene load). However the case in the OP was about serialized fields. A similar thing is true for GetComponent. If no component is found, GetComponent returns a fake null object but only inside the editor. At runtime it actually returns null.

    Just always keep in mind that any UnityEngine.Object is just a managed wrapper around a native C++ object. Managed objects can NEVER be destroyed. They are always garbage collected once all references are gone. Some people misinterpret the blog article since they talk about if the custom == operator should be removed or not. However if they remove it, it doesn't change a thing about
    is null
    ,
    ??
    and the like. The fake null objects are necessary regardless. Well, they could stop deliberately returning fake null objects from GetComponent as it's unnecessary. However since we can destroy objects, there has to be away to tell if the object is still there / alive or not. So if they would remove the custom null check, ibstead if this:
    Code (CSharp):
    1. if (obj != null)
    you would be forced to do something like this every time:

    Code (CSharp):
    1. if (obj != null && obj.IsAlive)
    "is null" and "??" would still detect that the wrapper is non null when you destroy an object, so they can not be used to tell if the object "is still there". You would need to do an explicit test for that.

    In the beginning when Unity started there was only the ?? operator that would fail on fake null objects. So at that time overloading the == operator was actually a quite clever "hack" to handle the manage - native interactions. However over the years C# got more and more special operators which do a plain null check and they can not be overloaded. So the hack can't be applied to them. Where the overloaded operator caused issues even back then was in the rare case when you cast your UnityEngine.Object type to System.Object or an interface type. In that case the overloaded == operator isn't used since it's not a virtual method. So when you do
    if (obj == xxx)
    the C# compiler uses the operator associated to the field / property type. It does not take the actual dynamic runtime type into account. Code is statically compiled and does not change at runtime.
     
    MaskedMouse likes this.
  15. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    BTW, comparing with "is" is a lot faster because it doesn't invoke the native object validity check. I used it a few times in hot code paths where "fake" nulls were not expected to happen during normal program operation and the goal was to simply check if a variable was explicitly assigned or not.
     
    Bunny83 likes this.
  16. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,524
    Right, this can be done. Though you can also do
    Code (CSharp):
    1. if ((object)obj == null)
    which also just uses the default == operator. Of course "is null" is more clean in that case.
     
  17. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    Here's a null coalescing utility I hastily did the other day, exactly because I suspected that
    ??
    gave me headaches with UnityEngine.Object. Turns out it was just fine, everything was fine, because it was only used on start initialization, but I'm kind of paranoid.

    Code (csharp):
    1. static public T NonNullOrDefault<T>(T obj, Func<T> dflt) where T : class {
    2. #if UNITY_EDITOR
    3.   if(dflt is null) throw new ArgumentNullException(nameof(dflt));
    4. #endif
    5.   return obj.IsNull()? obj = dflt() : obj;
    6. }
    7.  
    8. static public bool IsNull<T>(this T obj) where T : class {
    9.   if(obj is UnityEngine.Object unityObj)
    10.     return unityObj == null; // because fake null
    11.   return obj is null;
    12. }
    13.  
    14. // just for completion sake
    15. static public bool IsNotNull<T>(this T obj) where T : class {
    16.   if(obj is UnityEngine.Object unityObj)
    17.     return unityObj != null; // because fake null
    18.   return obj is not null;
    19. }
    The key difference from
    ??
    is that obj is evaluated twice, and IsNull (Unity-compatible imitation of
    is null
    ) has a slight overhead. (In other words, use this if you care about uniformity and outside of any hot paths.)

    Usage example
    Code (csharp):
    1. static Material _vcmat;
    2.  
    3. some function {
    4.   mr.sharedMaterial = Utils.NonNullOrDefault(_vcmat, () => newVertexColorMaterial());
    5. }
    Instead of
    Code (csharp):
    1. static Material _vcmat;
    2.  
    3. some function {
    4.   mr.sharedMaterial = _vcmat ??= newVertexColorMaterial();
    5. }