SWB
SWB

Reputation: 1452

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:

Upvotes: 55

Views: 3116

Answers (4)

Lucas Trzesniewski
Lucas Trzesniewski

Reputation: 51430

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

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.

Upvotes: 32

Heinzi
Heinzi

Reputation: 172468

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.

Upvotes: 44

SWB
SWB

Reputation: 1452

Here's another solution that does not use the undocumented __makeref keyword. This makes use of the System.Runtime.CompilerServices.Unsafe NuGet package:

using System.Runtime.CompilerServices;

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
}

unsafe static bool AreSame<T1, T2>( ref T1 a, ref T2 b )
{
    var pA = Unsafe.AsPointer( ref a );
    var pB = Unsafe.AsPointer( ref b );

    return pA == pB;
}

Upvotes: 0

Paolo Tedesco
Paolo Tedesco

Reputation: 57282

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
}

Upvotes: 29

Related Questions