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;
age2
to be aref
parameter. – Hamel