引用值类型的相等性
我做了一些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
是一个值类型,但在这里它应该传递对同一个对象的引用。
为什么此代码显示为
False
?
因为当调用object.ReferenceEquals
时, int a
和int 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
通过使用可validation的CIL(例如@ leppie的答案 )或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; } }
这不能直接在C#中完成。
但是,您可以在可validation的CIL中实现它:
.method public hidebysig static bool Test(!!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
笔记
这实际上并没有检查相同的引用,但更精细,因为它确保两者都是相同的“位置”(或从同一个变量引用)。 这是第3行返回false
即使o == p
返回true
。 然而,这种“位置”测试的有用性非常有限。
我知道,int是一个值类型,但在这里它应该传递对同一个对象的引用。
是的,传递给方法的引用是相同的,但它们在ReferenceEquals
方法中被加框(转换为对象/引用类型) 。
这就是为什么测试结果返回false的原因,因为你正在比较两个不同对象的引用, 因为装箱 。
请参阅: Object.ReferenceEquals方法
比较值类型时。 如果
objA
和objB
是值类型,则在将它们传递给ReferenceEquals
方法之前将它们装箱 。 这意味着如果objA
和objB
表示值类型的相同实例 ,则ReferenceEquals
方法仍然返回false
这里的混淆是因为与指针(如*)不同,C#中的“ref”不是类型的一部分,而是方法签名的一部分。 它适用于参数,表示“不得复制”。 它并不意味着“此参数具有引用类型”。
ref传递的参数不是代表新的存储位置,而是某个现有位置的别名。 如何创建别名在技术上是一个实现细节。 大多数情况下,别名是作为托管引用实现的,但并非总是如此。 例如,在一些异步相关的情况下,对数组元素的引用可以在内部表示为数组和索引的组合。
基本上对于所有目的,您的a和b仍被C#理解为int类型变量。 在任何采用类似a + b或SomeMethod(a,b)等int值的表达式中使用它们是合法且完全正常的,在这些情况下,使用存储在a和b中的实际int值。
实际上没有“引用”的概念作为您可以直接在C#中使用的实体。 与指针不同,必须假定托管引用的实际值能够随时或甚至异步地由GC更改,因此托管引用的有意义方案集将非常有限。