Reputation: 26768
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
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
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 == null
→ false
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