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

3

8

I'm curious about how C# behaves when passing value/reference types into a method. I'd like to pass a boxed value type into the method "AddThree". The idea is to get in the caller function (Main) the result of the operations performed within "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;
}

I've tried this with pure reference types like string and I get the same result. If I "wrap" my int within a class and I use the "wrap" class in this example it works as I'm expecting, ie, I get myBox = 6. If I modify "AddThree" method signature to pass the parameter by ref, this also returns 6. But, I don't want to modify the signature or create a wrapper class, I want to just box the value.

Demurrer answered 21/11, 2015 at 14:25 Comment(1)
There's no way to do this without changing the signature since you need age2 to be a ref parameter.Hamel
O
3

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

Then that will be a problem. Your method passes a boxed int, then unboxes it and adds 3 to the local age2, which causes another boxing operation, and then throws away the value. De-facto, you're assinging age2 to two different objects on the heap, they do not point to the same object. Without modifying the method signature, this won't be possible.

If you look at the generated IL for AddThree, you'll see this clearly:

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    

You unbox the value, add 3 and then box the value again, but you never return it.

To visualize this case further, try returning the newly boxed value from the method (just for the sake of the test), and use object.ReferenceEquals to compare them both:

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;
}
Onward answered 21/11, 2015 at 14:31 Comment(0)
M
5

Boxed references meant to be immutable. For example, this will not compile (assuming Point is a value type):

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

As the others said, this line causes a pair of boxing and unboxing operation, which ends up in a new reference:

age2 = (int)age2 + 3;

So even though a boxed int is actually a reference the line above modifies the object reference as well, so the caller will still see the same content unless the object itself is passed by reference.

However, there are a few ways for dereferencing and changing a boxed value without changing the reference (none of them are recommended, though).

Solution 1:

The simplest way is via reflection. This seems a bit silly because the Int32.m_value field is the int value itself but this allows you to access the int directly.

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

Solution 2:

This is a much bigger hack and involves the use of the mainly undocumented TypedReference and the __makeref() operator but more or less this is what happens in the background in the first solution:

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();
    }
}

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

📝 Edit: For some reason the latest Roslyn compiler allows void* p = (void*)&myBoxedObject directly, which was illegal earlier so using a TypedReference is actually no longer needed for this approach.


Update: Solution 3:

Starting with .NET Core you can use a much simpler and performant trick: the Unsafe.As<T> method allows you to reinterpret any object as another reference type. All you need is just a class, whose first field has the same type as your boxed value. And actually you can perfectly use the StrongBox<T> type for this purpose as its single Value field is public and mutable:

private static void AddThree(object age2) =>
    Unsafe.As<StrongBox<int>>(age2).Value += 3;
Mephitic answered 6/4, 2019 at 18:34 Comment(0)
O
3

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

Then that will be a problem. Your method passes a boxed int, then unboxes it and adds 3 to the local age2, which causes another boxing operation, and then throws away the value. De-facto, you're assinging age2 to two different objects on the heap, they do not point to the same object. Without modifying the method signature, this won't be possible.

If you look at the generated IL for AddThree, you'll see this clearly:

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    

You unbox the value, add 3 and then box the value again, but you never return it.

To visualize this case further, try returning the newly boxed value from the method (just for the sake of the test), and use object.ReferenceEquals to compare them both:

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;
}
Onward answered 21/11, 2015 at 14:31 Comment(0)
D
1

Assigning a new value to a method parameter not passed by ref will not change the original reference.

In your case, age2 (the method parameter) is a copy of myBox. They both reference the same object (the boxed age), but assigning to age2 does not change myBox. It simply makes age2 reference another object.

Actually, it has nothing to do with boxing. It's just how parameters are passed to methods.

Dotard answered 21/11, 2015 at 14:32 Comment(3)
It is about boxing, if param would be int, ref would help here. But boxing creates temporary copy of original int, and modification of this copy will be lost anyway, with and without ref.Tryparsamide
@Tryparsamide The OP wanted myBox (the boxed one) to change, not age (the int) : // here myBox = 3 but I was expecting to be = 6. Passing the parameter as ref object would accomplish that.Dotard
you are right, I've missed part that OP wanted boxed value.Tryparsamide

© 2022 - 2024 — McMap. All rights reserved.