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

问题描述 投票:6回答:3

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

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

c# pass-by-reference value-type boxing reference-type
3个回答
2
投票

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

那将是一个问题。你的方法传递一个盒装的int,然后将它取消装箱并向本地age2添加3,这会导致另一个装箱操作,然后丢弃该值。事实上,你正在将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;
}

2
投票

盒装参考意味着不可改变。例如,这将无法编译:

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

正如其他人所说的那样,这一行会导致一对装箱和拆箱操作,最终会出现一个新的参考:

age2 = (int)age2 + 3;

因此,即使boxed 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 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();
    }
}

1
投票

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.