How to correctly compare unity objects and value types in generic class

I am completely confused by now. I want to create generic class with method that is able to correctly compare if provided entity is different than previous one. Like this pseudocode:

public void SetPropertyIfDifferent(T value)
{
    if (this.classProperty != value)
    {
        this.classProperty = value;
        // And I want to do something additional here [...]
    }
}

By different, I mean that when T is reference type it will compare reference, so:
Example Reference Types

  • somePocoObject1 == somePocoObject2 // Different

  • someCollider == null // Different

  • someTransform1 == someTransform2 // Different

  • someTransform1 == null // Different

  • someTransform1 == someTransform1 // The same

and when T is value type it will correctly compare too:
Example Value Types

  • Vector3(0,0,1) == Vector3(0,0,1) // The same

  • Vector3(1,1,1) == Vector3(0,0,1) // Different

  • 1.0 == 3.0 // Different

  • 87 = 87 // The same

The problem is, I am unable to make it work without boxing.
In generic class “==” operator cannot be used, so I would need to use Equals or
EqualityComparer, but it seems it does not work. I am aware of special overload for == in unity and still I am confused how things work. See following example:
Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ComparerTest : ComparerTestBase<GameObject>
{
    IEnumerator Start()
    {
        Debug.LogWarning(obj == null);
        Debug.LogWarning(Object.Equals(obj, null));
        Debug.LogWarning(EqualityComparer<GameObject>.Default.Equals(obj, null));
        this.Test(obj);
        yield return new WaitForSeconds(1f);
    }
}   
   
public abstract class ComparerTestBase<T> : MonoBehaviour
{
    public T obj = default(T);
   
    public void Test(T value)
    {
        Debug.Log(value == null);
        Debug.Log(Object.Equals(value, null));
        Debug.Log(EqualityComparer<T>.Default.Equals(value, default(T)));
    }
}

When the obj is not set (none), the output is:

True
False
False

False
False
False

Why only first comparison works?
Why it says it is not null while it is null (or empty or whatever)?
What should I do?

So the problem going on here is even when an inspector property is null, when unity deserializes, in their infinite wisdom they actually don’t set it null.

They set it to this dummy object that overrides the == operator and evaluates true when compared to null.

This is what unity uses to propogate the error messages you get if you were to access it which mention you may have forgotten to set the serialized property (rather than just throwing the generic nullreferenceexception).

…

I know this is both dumb AND not well documented.

…

So this is why 1 = true, you’ve done an == comparison that relies on this override because the compiler from context knows you’re using the UnityEngine.Object == override.

But all 5 other places… it doesn’t know this. It just uses the generic System.Object.Equals operation (or the EqualityComparer in the case you explicitly use this). These don’t do the special unity == operation. And therefore it doesn’t think it’s null because well… it’s not. There’s an actual object there that just pretends to be null. And you don’t know it even exists.

What I usually do to avoid this problem is use my own ‘IsNullOrDestroyed’ method:

public static bool IsNullOrDestroyed(object obj)
{
    if (object.ReferenceEquals(obj, null)) return true;
    return obj is UnityEngine.Object ? (obj as UnityEngine.Object) == null : false;
}

This of course creates boxing garbage for value types. But uhhh… don’t use it for value types.

Just have another method like “IsDefaultValue” that you call for your value type fields:

public static bool IsDefaultValue<T>(T value)
{
    return EqualityComparer.Default.Equals(value, default(T));
}

You may even get away with combining the 2… something like:

public static bool IsDefaultOrDestroyed<T>(T value)
{
    if(EqualityComparer.Default.Equals(value, default(T))) return true;
    return value is UnityEngine.Object ? (value as UnityEngine.Object) == null : false;
}
2 Likes

So what you basically say is, to avoid this problem I would need to separate logic, one for value types and other for reference types or use some monster method that checks all possible cases?

Still I guess it’s something about how code is compiled, but shouldn’t the 4th if return true? I mean i used == operator on value, this value is generic, but the caller knows what kind of type is that, so what happened there?
Why Equals or something else was used there instead of overloaded version?

Does it means that whenever i make generic type, the overloaded == is always ignored?

Here’s the most authoritative explanation for what the fake null object and == override are for and how it works:

Yeah, but it doesn’t explain the 4th if.

In my use case there is class Variable with property “value” of type T (with custom Value get/set), so I can make any variables like RigidbodyVariable, FloatVariable, Vector3Variable etc. The thing is they are observable, so something can react to changes and this is implemented in setter. When i do:

RigidbodyVariable.Value = someObject;

I wanted to trigger change event, but as I explained before in current state there is boxing for value types and another worry is that generic property in example from the first post didn’t use the overloaded unity == check, so it would mean it’s still broken anyways.