装箱值类型,将其发送到方法并获取结果

本文关键字:方法 结果 获取 类型 | 更新日期: 2023-09-27 18:23:42

我很好奇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;
}

我在纯引用类型(如string)中尝试过这种方法,得到了相同的结果。如果我在一个类中"包装"我的int,并且在本例中使用"包装"类,它的工作原理正如我所期望的,即,我得到myBox=6。如果我修改"AddThree"方法签名以通过ref传递参数,这也会返回6。但是,我不想修改签名或创建包装类,我只想把值框起来

装箱值类型,将其发送到方法并获取结果

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

((Point)p).X += 3; // CS0445: Cannot modify the result of an unboxing conversion.

正如其他人所说,这一行导致了一对装箱和拆箱操作,最终出现在一个新的引用中:

age2 = (int)age2 + 3;

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

但是,有几种方法可以在不更改引用的情况下取消引用和更改装箱值(但不建议使用这些方法)。

解决方案1:

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

private static void AddThree(object age2)
{
    FieldInfo intValue = typeof(int).GetTypeInfo().GetDeclaredField("m_value");
    intValue.SetValue(age2, (int)age2 + 3);
}

解决方案2:

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

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

⚠️ 注意:请注意,此解决方案依赖于平台,不适用于Mono,因为其TypedReference实现不同。

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


更新:解决方案3:

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

private static void AddThree(object age2) =>
    Unsafe.As<StrongBox<int>>(age2).Value += 3;

我不想修改签名或创建包装类,我只想把值框起来。

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

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

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    

您取消对值的装箱,添加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;
}

ref未传递的方法参数指定新值不会更改原始引用。

在您的情况下,age2(方法参数)是myBox的副本。它们都引用同一个对象(带框的age),但分配给age2不会更改myBox。它只是使age2引用另一个对象。

事实上,这与拳击无关。这只是参数传递给方法的方式。