Как определить, ссылаются ли две переменные "ref" на одну и ту же переменную, даже если null?

Как я могу определить, ссылаются ли две переменные ref на одну и ту же переменную - даже если обе переменные содержат null?

Пример:

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 )
{
    // ?????
}

Вещи, которые я пробовал, которые не работают:

  • return object.ReferenceEquals( a, b ); (Возвращает true в обоих тестовых случаях)
  • unsafe { return &a == &b; } unsafe { return &a == &b; } (Невозможно принять адрес управляемого объекта)

Ответ 1

Фактически вы можете просто использовать метод Unsafe.AreSame из пакета System.Runtime.CompilerServices.Unsafe.

Это будет напрямую сравнивать ссылки и является самым чистым решением. Метод написан в IL и просто сравнивает ссылки, потому что, ну... вы можете сделать это в IL :)

Если вы хотите сравнить две ссылки разных типов, вы можете отбросить одну из них, используя эту перегрузку Unsafe.As:

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

Здесь другое предложение, если литье ссылки кажется неуклюжим: используйте мою библиотеку InlineIL.Fody, которая позволяет вставлять произвольный код IL прямо в ваш код С#:

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>();
}

Я предлагаю это, поскольку это проще, чем испускать код во время выполнения с Reflection.Emit, потому что вы не можете создать общий DynamicMethod и вам нужно будет создать динамический тип. Вы также можете написать проект IL, но он также чувствует избыток только для одного метода.

Кроме того, вы избегаете зависимости от внешней библиотеки, если это важно для вас.


Обратите внимание, что я не буду полностью доверять __makeref и Unsafe.AsPointer из-за возможности условия гонки: если вы достаточно __makeref Unsafe.AsPointer эти условия вместе:

  • две ссылки равны
  • GC запускается другим потоком после оценки первой стороны сравнения, но перед тем, как другой
  • ваши контрольные точки где-то в управляемой куче
  • ссылочный объект перемещается GC для целей уплотнения кучи

Хорошо, тогда указатель, который уже был оценен, не будет обновляться GC до сравнения, так что вы получите неверный результат.

Возможно ли это? На самом деле, нет. Но это может быть.

Метод Unsafe.AreSame всегда работает в пространстве byref, поэтому GC может отслеживать и обновлять ссылки в любое время.

Ответ 2

Существует способ без изменения значений, используя небезопасный код и недокументированный метод __makeref:

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);
    }
}

Примечание: выражение *(IntPtr*)(&trA) полагается на то, что первое поле TypedReference является IntPtr указывающим на переменную, которую мы хотим сравнить. К сожалению (или, к счастью?), Нет управляемого способа доступа к этому полю - даже с отражением, так как TypedReference не может быть помещен в коробку и, следовательно, не может использоваться с FieldInfo.GetValue.

Ответ 3

Возможно, это можно сделать, изменив ссылку на временную переменную и проверив, изменится и другая.
Я сделал быстрый тест, и это, похоже, работает:

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
}

Ответ 4

Здесь другое решение, которое не использует недокументированное ключевое слово __makeref. Это использует пакет System.Runtime.CompilerServices.Unsafe NuGet:

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;
}