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

Boxing value type to send it to a method and get the result

我很好奇 C# 在将 value/reference 类型传递给方法时的行为方式。我想将盒装值类型传递给方法“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 这样的纯引用类型,我得到了相同的结果。如果我 "wrap" 我的 int 在 class 中并且我在这个例子中使用了 "wrap" class 它会像我期望的那样工作,即,我得到 myBox = 6。如果我修改 "AddThree" 方法签名以通过 ref 传递参数,这也是 returns 6。但是, 我不想修改签名或创建包装器 class,我只想将值装箱。

I don't want to modify the signature or create a wrapper class, I want to just box the value.

那就麻烦了。您的方法传递一个装箱的 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,然后再次将值装箱,但您从未 return 它。

为了进一步形象化这种情况,尝试 return 从方法中输入新装箱的值(只是为了测试),并使用 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 引用另一个对象。

其实和拳击没有关系。这就是将参数传递给方法的方式。

盒装引用意味着不可变。例如,这不会编译:

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

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

age2 = (int)age2 + 3;

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

但是,有几种方法可以在不更改引用的情况下取消引用和更改装箱值(不过,推荐其中 none 种方法)。

解决方案一:

最简单的方法是通过反射。这看起来有点傻,因为 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);
}

方案二:

这是一个更大的 hack,涉及使用主要未记录的 TypedReference__makeref() 运算符,但或多或​​少这就是第一个解决方案在后台发生的事情:

private static unsafe void AddThree(object age2)
{
    // pinning is required to prevent GC reallocating 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();
    }
}

⚠️ Caution: Please note that this solution is platform dependent and does not work on Mono because its TypedReference implementation is different.


更新:解决方案 3:

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

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