拳击值类型将其发送到方法并获得结果

Rob*_*ber 8 c# boxing value-type reference-type pass-by-reference

我很好奇在将值/引用类型传递给方法时C#的行为方式.我想将盒装值类型传递给方法" AddThree ".我们的想法是在调用函数(Main)中输入" AddThree "中执行的操作的结果.

static void Main(string[] args)
{
    int age = 3;
    object myBox = age;
    AddThree(myBox);
    // here myBox = 3 but I was expecting to be  = 6
}

private static void AddThree(object age2)
{
    age2 = (int)age2 + 3;
}
Run Code Online (Sandbox Code Playgroud)

我已尝试使用像字符串这样的纯引用类型,我得到了相同的结果.如果我在一个类中"包装"我的int并且我在这个例子中使用"wrap"类它就像我期望的那样工作,即我得到myBox = 6.如果我修改"AddThree"方法签名来传递参数ref,这也返回6.但是,我不想修改签名或创建一个包装类,我只想将值包装起来.

Gyö*_*zeg 5

盒装引用意味着不可变。例如,这不会编译(假设Point是值类型):

\n
((Point)p).X += 3; // CS0445: Cannot modify the result of an unboxing conversion.\n
Run Code Online (Sandbox Code Playgroud)\n

正如其他人所说,这一行会导致一对装箱和拆箱操作,最终产生一个新的引用:

\n
age2 = (int)age2 + 3;\n
Run Code Online (Sandbox Code Playgroud)\n

因此,即使装箱 int 实际上是一个引用,上面的行也会修改对象引用,因此调用者仍然会看到相同的内容,除非对象本身是通过引用传递的。

\n

但是,有几种方法可以在不更改引用的情况下取消引用和更改装箱值(但不推荐使用任何一种方法)。

\n

解决方案一:

\n

最简单的方法是通过反射。这看起来有点愚蠢,因为该Int32.m_value字段本身就是 int 值,但这允许您直接访问 int。

\n
private static void AddThree(object age2)\n{\n    FieldInfo intValue = typeof(int).GetTypeInfo().GetDeclaredField("m_value");\n    intValue.SetValue(age2, (int)age2 + 3);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

解决方案2:

\n

这是一个更大的黑客攻击,涉及主要使用未记录的TypedReference操作__makeref()符,但或多或​​少这是第一个解决方案中后台发生的情况:

\n
private static unsafe void AddThree(object age2)\n{\n    // pinning is required to prevent GC relocating the object during the pointer operations\n    var objectPinned = GCHandle.Alloc(age2, GCHandleType.Pinned);\n    try\n    {\n        // The __makeref() operator returns a TypedReference.\n        // It is basically a pair of pointers for the reference value and type.\n        TypedReference objRef = __makeref(age2);\n\n        // Dereference it to access the boxed value like this: objRef.Value->object->boxed content\n        // For more details see the memory layout of objects: https://blogs.msdn.microsoft.com/seteplia/2017/05/26/managed-object-internals-part-1-layout/\n        int* rawContent = (int*)*(IntPtr*)*(IntPtr*)&objRef;\n\n        // rawContent now points to the type handle (just another pointer to the method table).\n        // The actual instance fields start after these 4 or 8 bytes depending on the pointer size:\n        int* boxedInt = rawContent + (IntPtr.Size == 4 ? 1 : 2);\n        *boxedInt += 3;\n    }\n    finally\n    {\n        objectPinned.Free();\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
\n

\xe2\x9a\xa0\xef\xb8\x8f注意:请注意,此解决方案依赖于平台,并且不适用于 Mono,因为其TypedReference实现不同。

\n
\n
\n

编辑:由于某种原因,最新的 Roslyn 编译器void* p = (void*)&myBoxedObject直接允许,这在之前是非法的TypedReference,因此这种方法实际上不再需要使用 a 。

\n
\n
\n

更新:解决方案3:

\n

从 .NET Core 开始,您可以使用更简单且更高效的技巧:该Unsafe.As<T>方法允许您将任何对象重新解释为另一种引用类型。您所需要的只是一个类,其第一个字段的类型与您的装箱值相同。实际上,您可以完美地使用该StrongBox<T>类型来实现此目的,因为它的单个Value字段是公共且可变的:

\n
((Point)p).X += 3; // CS0445: Cannot modify the result of an unboxing conversion.\n
Run Code Online (Sandbox Code Playgroud)\n


Yuv*_*kov 3

我不想修改签名或创建包装类,我只想将值装箱。

那么这将是一个问题。您的方法传递一个装箱的int,然后将其拆箱并将 3 添加到 local age2,这会导致另一个装箱操作,然后丢弃该值。事实上,您分配age2给堆上的两个不同对象,它们并不指向同一个对象。如果不修改方法签名,这是不可能的。

如果您查看生成的 IL AddThree,您会清楚地看到这一点:

AddThree:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  unbox.any   System.Int32 // unbox age2
IL_0007:  ldc.i4.3    // load 3
IL_0008:  add         // add the two together
IL_0009:  box         System.Int32 // box the result
IL_000E:  starg.s     00 
IL_0010:  ret    
Run Code Online (Sandbox Code Playgroud)

您取消对该值的装箱,添加 3,然后再次对该值进行装箱,但您永远不会返回它。

为了进一步可视化这种情况,尝试从方法返回新装箱的值(只是为了测试),并使用object.ReferenceEquals它们来比较它们:

static void Main(string[] args)
{
    int age = 3;
    object myBox = age;
    var otherBox = AddThree(myBox);
    Console.WriteLine(object.ReferenceEquals(otherBox, myBox)); // False
}

private static object AddThree(object age2)
{
    age2 = (int)age2 + 3;
    return age2;
}
Run Code Online (Sandbox Code Playgroud)