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.
Boxed references meant to be immutable. For example, this will not compile:
((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 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 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.
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;
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With