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

C# 装箱值类型以将其发送到方法并获取结果,c#,pass-by-reference,value-type,boxing,reference-type,C#,Pass By Reference,Value Type,Boxing,Reference Type,我对C#在将值/引用类型传递给方法时的行为感到好奇。我想将一个装箱的值类型传递给方法“AddThree”。其思想是在调用者函数(Main)中获取在“AddThree”中执行的操作的结果 我尝试过纯引用类型,比如string,我得到了相同的结果。如果我在一个类中“包装”我的int,并且在本例中使用“包装”类,它将按照我的预期工作,即,我得到myBox=6。如果我修改“AddThree”方法签名以通过ref传递参数,这也会返回6。但是,我不想修改签名或创建包装类,我只想将值装箱。 我不想修改签名或创

我对C#在将值/引用类型传递给方法时的行为感到好奇。我想将一个装箱的值类型传递给方法“AddThree”。其思想是在调用者函数(Main)中获取在“AddThree”中执行的操作的结果

我尝试过纯引用类型,比如string,我得到了相同的结果。如果我在一个类中“包装”我的int,并且在本例中使用“包装”类,它将按照我的预期工作,即,我得到myBox=6。如果我修改“AddThree”方法签名以通过ref传递参数,这也会返回6。但是,我不想修改签名或创建包装类,我只想将值装箱。

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

那就成问题了。您的方法传递一个装箱的
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
引用另一个对象


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

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

((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:

这是一个更大的黑客攻击,涉及到使用主要未记录的
类型引用
\u 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();
    }
}

如果不更改签名,则无法执行此操作,因为您需要
age2
作为
ref
参数。这是关于装箱的,如果参数为int,ref将在此处提供帮助。但是装箱会创建原始int的临时副本,并且不管有没有ref,对该副本的修改都将丢失。@ISanych OP想要改变
myBox
(装箱的那一个),而不是
age
(The
int
):
//这里myBox=3,但我希望是=6
。将参数作为
ref object
传递将实现这一点。您是对的,我遗漏了OP想要装箱值的部分。
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();
    }
}