值类型的引用相等性

28

我进行了一些ref关键字的测试,有一件事我不太明白:

static void Test(ref int a, ref int b)
{
    Console.WriteLine(Int32.ReferenceEquals(a,b));
}

static void Main(string[] args)
{
    int a = 4;
    Test(ref a, ref a);
    Console.ReadLine();
}
为什么这段代码显示False?我知道int是一个值类型,但这里应该传递对同一对象的引用。

因为值类型的引用不相似。 - Amit Kumar Ghosh
1
ref修饰符不会导致相应的参数被装箱为引用类型。 - Lee
你是在尝试查看这两个参数是否引用了同一个变量吗? - BoltClock
@BoltClock:如果这是意图的话,可以通过一些“艰苦”的工作来完成;p - leppie
这并不会有用,因为基本值类型只能通过分配新值来更改(因此值相等性就是你所需要的)。这个简短的文档解释了值类型的工作原理。这个也很好。 - Trisped
4个回答

41
由于在调用object.ReferenceEquals时,int aint b被装箱了。每个整数都被装箱到一个object实例中。因此,您实际上是在比较两个装箱值之间的引用,这显然不相等。
如果您查看该方法生成的CIL,就可以轻松地看到这一点。
Test:
IL_0000:  nop
IL_0001:  ldarg.0     Load argument a
IL_0002:  ldind.i4
IL_0003:  box         System.Int32
IL_0008:  ldarg.1     Load argument b
IL_0009:  ldind.i4
IL_000A:  box         System.Int32
IL_000F:  call        System.Object.ReferenceEquals
IL_0014:  call        System.Console.WriteLine
IL_0019:  nop
IL_001A:  ret

检查存储位置的相等性可以通过使用可验证的CIL(例如在@leppie's answer中)或使用unsafe代码来实现:

unsafe static void Main(string[] args)
{
    int a = 4;
    int b = 5;
    Console.WriteLine(Test(ref a, ref a)); // True
    Console.WriteLine(Test(ref a, ref b)); // False;
}

unsafe static bool Test(ref int a, ref int b)
{
    fixed (int* refA = &a)
    fixed (int* refB = &b)
    {
        return refA == refB;
    }
}

1
如果你在方法内部更改它们中的一个,会发生什么?我认为这是不可能的。 - M.kazem Akhgary
2
@M.kazemAkhgary 是的,如果您直接操作 a,它的值将会改变。这种情况(OP所看到的行为)是因为在调用 ReferenceEquals 时,两个值都被装箱了(仅供方法调用)。 - Yuval Itzchakov
嗯,我在想 ldarg.0; ldarg.1; ceq; 是否可以在 C# 中被发出? - leppie
1
@YuvalItzchakov:你可以使用不安全的方式实现:即 static bool Foo(int* a, int* b) => a == b; 但是你能否在不使用不安全的情况下实现呢?请参见:http://tryroslyn.azurewebsites.net/#f:>ilr/K4Zwlgdg5gBAygTxAFwKYFsDcAoFBDZMAYxiIBs8QQYBhGAb2wEhgIQ8AzVGfQkgIwD2gsjAAqqFAApIyAFQw8AGhgxZC/gEoYAXgB8i3Tpj8cAXyAA= - leppie
1
@YuvalItzchakov:这就是为什么你需要==。你可以在IL中完成它,并且它是可验证的,但我看不到在C#中的方法。 - leppie
显示剩余2条评论

19

这不能直接在C#中完成。

但是你可以在可验证的CIL中实现它:

.method public hidebysig static bool Test<T>(!!T& a, !!T& b) cil managed
{
  .maxstack 8
  ldarg.0 
  ldarg.1 
  ceq 
  ret 
}

测试

int a = 4, b = 4, c = 5;
int* aa = &a; // unsafe needed for this
object o = a, p = o;
Console.WriteLine(Test(ref a, ref a)); // True
Console.WriteLine(Test(ref o, ref o)); // True
Console.WriteLine(Test(ref o, ref p)); // False
Console.WriteLine(Test(ref a, ref b)); // False
Console.WriteLine(Test(ref a, ref c)); // False
Console.WriteLine(Test(ref a, ref *aa)); // True
// all of the above works for fields, parameters and locals

注释

这并不实际检查相同的引用,但更加细粒度,因为它确保两者也是从相同的“位置”引用(或从同一变量引用)。即使 o == p 返回 true,第三行仍返回false。然而,“位置”测试的有用性非常有限。


注意:这不是答案,只是实现它的一种迂回方式。 - leppie
1
太棒了。我使用DynamicMethod创建了一个C#答案,链接在这里:https://dev59.com/o4Xca4cB1Zd3GeqPFTkO#43570334。非常感谢。 - Glenn Slayden

2
我知道,int是值类型,但在这里它应该传递对同一对象的引用。
是的,传递给方法的引用是相同的,但它们在ReferenceEquals方法中被装箱(转换为对象/引用类型)。这就是为什么你的测试结果返回false的原因,因为你正在比较两个不同对象的引用,由于装箱的原因。
参见:Object.ReferenceEquals方法
当比较值类型时,如果objA和objB都是值类型,则它们在传递给ReferenceEquals方法之前会被装箱。这意味着,如果objA和objB都表示值类型的同一实例,则ReferenceEquals方法仍然返回false。

1

这里的混淆是因为与指针(如 *)不同,“ref”在C#中不是类型的一部分,而是方法签名的一部分。它适用于参数,并表示“这个参数不能被复制”。它并不意味着“此参数具有引用类型”。

通过引用传递的参数,而不是表示新的存储位置,而是现有位置的别名。如何创建别名在技术上是一个实现细节。大多数情况下,别名被实现为托管引用,但并非总是如此。例如,在某些异步相关情况下,对数组元素的引用可以在内部表示为数组和索引的组合。

从所有目的来看,您的a和b仍然被C#视为int类型的变量。在任何接受int值的表达式中(如a + b或SomeMethod(a,b)),使用它们都是合法且完全正常的,并且在这些情况下,使用存储在a和b中的实际int值。

实际上,在C#中没有“引用”作为可以直接使用的实体的概念。 与指针不同,托管引用的实际值必须被假定能够在任何时刻甚至异步地由GC更改,因此托管引用的有意义的情况集将极其有限。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接