max pleaner
max pleaner

Reputation: 26768

null GameObject becomes non-null when I convert to object

I have a particular function which can take an object as an argument. This is so it can be either a MonoBehaviour or a GameObject:

The wierd thing is, when I convert a null GameObject to an Object, it no longer appears as null:

public class EquipmentSlots : MonoBehaviour {

  // This is null: it hasn't been set in the inspector
  public GameObject offHandEquipLocation;

  void Start () {
    Validation.RequireField(
      "offHandEquipLocation",
      offHandEquipLocation
    ) 
  }

}

public class Validation : MonoBehaviour {
  public static void RequireField(string name, object field) {
    if (field == null) {
      Debug.Log($"{name} field is unset");
    }
  }
}

The Log call never gets run here, because the object is not null.

If I put Debug.Log(offHandEquipLocation == null) in the Start() function, it prints true. If I put Debug.Log(field == null) in the RequireField() method, it prints false.

Is there a way I can see if the object is null?

Upvotes: 2

Views: 474

Answers (3)

David
David

Reputation: 10708

It's because Unity's Object defines an == operator, which will return true if you compare a destroyed instance to null, even if the reference isn't actually null. By using offHandEquipLocation, you're calling this operator, while object field calls the .NET == operator.

As has been stated, you can call if directly against GameObjects, since Unity's Object defines an implicit conversion to bool, or you could cast to System.Object before performing a null check.

You should also be cautious about comparisons checking for referential null, and use ReferenceEquals if you really must know the difference between a nullref and a destroyed object.

Upvotes: 2

derHugo
derHugo

Reputation: 90649

A very first thought in general on this: Do not do it ^^

There is one issue with this: You change the stacktrace of the call. By passing the null check to a static class when you see the eror in the console you can not like before directly see where it was thrown/logged from, directly go to the according code line by double clicking it and can't (that easy) highlight the according object "context" in the hierarchy.

So I would always keep the checks as close to the actual usage as possible and rather do it like

if(!offHandEquipLocation) Debug.LogError($"{nameof(offHandEquipLocation)} is not referenced!", this);

The whole sense of Debug.Log is making your life easier when debugging ;) Using the above approach offers way more advantages than the fact that using your approach you don't have to type Debug.LogError($"{nameof(offHandEquipLocation)} is not referenced!", this); multiple times.


As others and I already mentioned the reason why it doesn't work as you expect is Unity's custom == null behavior for the type Object from which most built-in types in particular GameObject, ScriptableObject and Component inherit.

Even though an Object "might appear to be" or better said returns a value equal to the value of null Unity actually still stores some meta information in the reference in order to throw custom exceptions (MissingReferenceException, MissingComponentException, UnassignedReferenceException) which are more self explanatory then a simple NullReferenceException (as you can also see here). It therefore actually is not null in the underlying object type.

As soon as you convert it to object the custom == null behavior is gone but the underlying object still exists and therefoe you get field == nullfalse


However a solution might be creating a simple overload using the bool operator for Object. This replaces the null check and means something like This object is referenced, exists, and was not destroyed yet.. And keep using == null for anything else

public static void RequireField(string name, object field) 
{
    if (field == null) Debug.LogError($"{name} field is unset");
}

public static void RequireField(string name, Object field) 
{
    if (!field) Debug.LogError($"{name} field is unset");
}

Now you can use both. As a little note: I wouldn't use a verbal string for the first parameter but instead always pass it inusing nameof to make it more secure for renamings

var GameObject someObject;
var string someString;

Validation.validate(nameof(someObject), someObject);
Validation.validate(nameof(someString), someString);

A little sidenote regarding your argument for using this on e.g. string in general:

As soon as this is a public or [SerializedField] field in the Inspector for any Component (MonoBehaviour) or ScriptableObject it is always initialized by the Unity Inspector itself with a default value. Therefore a null check for any of e.g.

public int intValue;
[SerializeField] private string stringValue;
public Vector3 someVector;
public int[] intArray;
...

is redundant since none of them will ever be null. This counts for any serializable type as well so even if you have a custom class

[Serializable]
public class Example
{
    public string aField;
}

and then use a serialized field in your behaviour like e.g.

public List<Example> example;

also this one will never be null.


Btw as also mentioned already Validation should not inherit from MonoBehaviour but rather be

public static class Validation
{
    // only static members
    ...
}

Upvotes: 3

LeBoucher
LeBoucher

Reputation: 406

It is because GameObject's operator == is overridden. That is why gameObject == null is true, but after casted to object, it is not null anymore. See this and this

Upvotes: 1

Related Questions