How to determine whether two "ref" variables refer to the same variable, even if null?

How can I determine whether two ref variables refer to the same variable – even if both variables contain null?

Example:

public static void Main( string[] args )
{
    object a = null;
    object b = null;

    Console.WriteLine( AreSame( ref a, ref b ) ); // Should print False
    Console.WriteLine( AreSame( ref a, ref a ) ); // Should print True
}

static bool AreSame<T1, T2>( ref T1 a, ref T2 b )
{
    // ?????
}

Things I have tried that don't work:

  • return object.ReferenceEquals( a, b ); (Returns true in both test cases)
  • unsafe { return &a == &b; } (Cannot take the address of a managed object)

Solution 1:

There is a way without modifying the values, using unsafe code and the undocumented __makeref method:

public static void Main(string[] args)
{
    object a = null;
    object b = null;

    Console.WriteLine(AreSame(ref a, ref b));  // prints False
    Console.WriteLine(AreSame(ref a, ref a));  // prints True
}

static bool AreSame<T1, T2>(ref T1 a, ref T2 b)
{
    TypedReference trA = __makeref(a);
    TypedReference trB = __makeref(b);

    unsafe
    {
        return *(IntPtr*)(&trA) == *(IntPtr*)(&trB);
    }
}

Note: The expression *(IntPtr*)(&trA) relies on the fact that the first field of TypedReference is an IntPtr pointing to the variable we want to compare. Unfortunately (or fortunately?), there is no managed way to access that field -- not even with reflection, since TypedReference can't be boxed and, thus, can't be used with FieldInfo.GetValue.

Solution 2:

You can actually just use the Unsafe.AreSame method from the System.Runtime.CompilerServices.Unsafe package.

This will compare references directly and is the cleanest solution. The method is written in IL and simply compares the references, because, well... you can do that in IL :)

If you want to compare two references of different types, you can cast one of them using this overload of Unsafe.As:

static bool AreSame<T1, T2>(ref T1 a, ref T2 b) 
    => Unsafe.AreSame(ref Unsafe.As<T1, T2>(ref a), ref b);

Here's another suggestion if casting a reference feels clunky: use my InlineIL.Fody library which lets you inject arbitrary IL code directly into your C# code:

static bool AreSame<T1, T2>(ref T1 a, ref T2 b)
{
    IL.Emit.Ldarg(nameof(a));
    IL.Emit.Ldarg(nameof(b));
    IL.Emit.Ceq();
    return IL.Return<bool>();
}

I'm suggesting this since it's easier than emitting code at runtime with Reflection.Emit, because you can't create a generic DynamicMethod and you would need to generate a dynamic type. You could also write an IL project but it also feels overkill just for one method.

Also, you avoid taking a dependency on an external library, if that is important to you.


Note that I wouldn't completely trust the __makeref and Unsafe.AsPointer solutions because of the possibility of a race condition: if you're unfortunate enough to get these conditions together:

  • the two references are equal
  • the GC is triggered by another thread after the first side of the comparison is evaluated but before the other one is
  • your reference points somewhere to the managed heap
  • the referenced object is moved by the GC for heap compaction purposes

Well, then the pointer that has already been evaluated won't be updated by the GC prior to the comparison, so you'll get an incorrect result.

Is it likely to happen? Not really. But it could.

The Unsafe.AreSame method always operates in byref space, so the GC can track and update the references at any time.

Solution 3:

Maybe this could be done by changing the reference to a temporary variable and checking if the other one changes as well.
I made a quick test, and this seems to work:

static bool AreSame(ref object a, ref object b) {
    var old_a = a;
    a = new object();
    bool result = object.ReferenceEquals(a, b);
    a = old_a;
    return result;
}

static void Main(string[] args) {
    object a = null;
    object b = null;

    var areSame1 = AreSame(ref a, ref b); // returns false
    var areSame2 = AreSame(ref a, ref a); // returns true
}