- Home /
Using reflection, sometimes a field is "null" and sometimes it is "Null"?
I've been writing a generic serialization system over the past few days and it's getting close to complete right now. I put in a null check to make sure no empty fields are passed in (and thus don't need to be serialized) and I noticed a result that I don't understand.
I have a sample script named TestScript
which looks like this:
public class TestScript : MonoBehaviour
{
public Texture2D texture;
public Material mat;
public GameObject gobj;
public OtherScript other;
}
When I send this to the serializer, I get all the fields off of TestScript using FieldInfo. I then check to see if these field info's are null. This is the comparer:
//field is a FieldInfo. We get the value from an instance that it exists on. In This case target.
object obj = field.GetValue(target);//Get the value of this field on our target script
Debug.Log(obj);
if(obj.Equals(null))
{
return;
}
When comparing the Texture2D, the Material, and the GameObject, I get a return type of "null". When I try to compare the class OtherScript, I get a return type of "Null". The "Null" return throws a NullReferenceException at this line and the others don't. Why is this and how can I check to make sure the field OtherScript is not null?
@BadAssGames Did you solve this thing? I'm getting the same "null" reference on Unity native components.
Answer by Bunny83 · Apr 03, 2015 at 03:06 AM
I'm pretty sure you just stumbled on that little "null trick" Unity implements for all objects derived from UnityEngine.Object.
As you might know objects derived from UnityEngine.Object usually have a native counterpart on the C++ side of the engine. Since C# uses a managed memory space and C++ not there are some problems with this object "duality". In C++ (and Unity) you can actually "destroy" / free an object manually. In C# that's not possible since the managed memory is released when all references to an object go out of extent.
Now we have the method Destroy which can destroy all objects derived from UnityEngine.Object. So how does this work in the managed environment? The answer is: it actually doesn'T work. It's not possible to destroy a managed object. When you call Destroy on a Component Unity just destroys the native part on the C++ side. The managed object is still there until all references to that object are gone.
Unity uses a little "trick" to make a reference to such a "dead object" appear null by simply overloading the Equals method and == operator. So when you compare the reference to null they will return true, even the reference isn't null. Though since the object is actually dead you can't use it anymore.
The same thing happens to MonoBehaviours objects that never had a native part. This happens when you create an instance of a MonoBehaviour with the "new" keyword. This will create a managed object, but since the component isn't attached to a GameObject it's in it's dead state from the very beginning.
So first rule No1: Never use the new keyword on MonoBehaviour derived classes.
btw:
This comparison makes no sense at all:
if(obj.Equals(null))
If obj is actually null this will throw a NullReferenceException since you can't call Equals on a null object. It's possible to call it on a "fake-null" object since you actually have an object.
Since the == operator is not a virtual method you will notice a difference when using a variable of a type derived from UnityEngine.Object and a variable of type System.Object:
SomeMonoBehaviour A = new SomeMonoBehaviour(); // create fake null object
Debug.Log("A == null: " + (A == null));
Debug.Log("A.Equals(null) : " + (A.Equals(null)));
object B = A; // assign the same reference to an System.Object variable
Debug.Log("B == null: " + (B == null));
Debug.Log("B.Equals(null) : " + (B.Equals(null)));
The result will be:
"A == null: true"
"A.Equals(null) : true"
"B == null: false"
"B.Equals(null) : true"
So knowing that little fact you can test a "seamingly" null reference for being fake null by casting it into "object" do a == check for null and an Equals check afterwards like this:
bool IsReferenceFakeNull(object aRef)
{
return aRef != null && aRef.Equals(null);
}
This method is safe for any reference as true null values won't trigger the Equals check which would cause a null-ref-exception otherwise.
Just for clarity, this (for me) was happening with Unity native components (Texture2D, $$anonymous$$aterial, GameObject, and probably any other Unity specific classes). They would return as "fake-nulls".
@BadAssGames: That's not possible unless you actually used Destroy on them or they got destroyed in another way.
Example:
var A = new Texture2D(10,10);
DestroyImmediate(A);
Debug.Log("A == null: " + (A == null));
Debug.Log("A.Equals(null) : " + (A.Equals(null)));
object B = A;
Debug.Log("B == null: " + (B == null));
Debug.Log("B.Equals(null) : " + (B.Equals(null)));
With the DestroyImmediate call the texture reference will become a fake-null object. When you comment it out all 4 null checks will return false as it's a valid object.
Yeah I'm not sure what's happening here. But it's definitely still returning as I described above and not in the manner that you said it should. Basically all I'm doing is this:
object instance = field.GetValue(target); //Gets an instance of Texture2D from the target script. There is no Texture2D to get, so the value it should get is null
if(instance == null)<--- Will never return true
{
Debug.Log("No Texture2D is stored there!");
}
I don't know why this is. I'm not calling Destroy or DestroyImmediate anywhere. I'm not creating a new $$anonymous$$onobehaviour (unless maybe GetValue is doing this for me?)
@BadAssGames: I just made a few tests and i think i know what happens in your case. If you have a unity-serialized class ($$anonymous$$onoBehaviour or custom serialized class as part of a $$anonymous$$onoBehaviour) that contains a reference to a Texture2D which hasn't been setup, Unity creates a "fake" object and assigns that one to the variable when the class gets deserialized. This fake object also pretends to be null and is responsible for those error messages:
"UnassignedReferenceException:The variable tex of SerializationTests has not been assigned.\nYou probably need to assign the tex variable of the SerializationTests script in the inspector."
Answer by DiegoSLTS · Apr 03, 2015 at 01:40 AM
What does "GetValue" do?
Your null check is a little weird, if obj is actually "null" then you can't all a method ("Equals") on the object. Also, if the object is null you are trying to call the ToString method on it that, again, can't be called since the object is null at that point.
To check if a variable is null you should use:
if (obj == null) {
Debug.Log("obj is null"); //you can't call ToString on a null pointer
return;
}
For the class OtherScript your GetValue function is returning a null pointer, that's why your code crashes. For the rest, did you ever had one of the values actually printing that Debug.Log message? If you had, then the method it's returning... something else, I'm not sure what could it be, but it's not a "null", is something else that has a ToString method that returns "null" when called, and a redefinition of Equals that returns true when null is passed.
I've edited my original post to be a little more clear about "field". the field
variable in this case is a Field Info
. I use field.GetValue to return an actual instance of on the target script (which in this case is an instance of the TestScript class).
Using obj == null does not work in this case. It will not return true on my script.
$$anonymous$$y console readout, when running "TestScript" through my serializer says the following about each variable:
null <--- Texture2D
null <---- $$anonymous$$aterial
null <--- GameObject
Null <---- OtherScript
NullReferenceException etc. etc.
ETA: So, after some further testing. It seems you're very much correct. When trying to check the Texture2D, $$anonymous$$aterial, and GameObject, only obj.Equals(null)
will work. However, when trying to test the Otherscript object, obj == null
will work. I don't understand why this is, however. Is there a catch-all solution I can use that will ensure I accurately check if a value is null?
Your answer
Follow this Question
Related Questions
Save EditorWindow to disk 1 Answer
Save multiple scriptableObjects in just one binary file 2 Answers
Unity 2D C# - Save And Load Data 1 Answer