Как определить, ссылаются ли две переменные "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;
}