I have a situation where I have a simple, immutable value type:
public struct ImmutableStruct
{
private readonly string _name;
public ImmutableStruct( string name )
{
_name = name;
}
public string Name
{
get { return _name; }
}
}
When I box an instance of this value type, I would normally expect that whatever it is that I boxed would come out the same when I do an unbox. To my big suprise this is not the case. Using Reflection someone may easily modify my box's memory by reinitializing the data contained therein:
class Program
{
static void Main( string[] args )
{
object a = new ImmutableStruct( Guid.NewGuid().ToString() );
PrintBox( a );
MutateTheBox( a );
PrintBox( a );;
}
private static void PrintBox( object a )
{
Console.WriteLine( String.Format( "Whats in the box: {0} :: {1}", ((ImmutableStruct)a).Name, a.GetType() ) );
}
private static void MutateTheBox( object a )
{
var ctor = typeof( ImmutableStruct ).GetConstructors().Single();
ctor.Invoke( a, new object[] { Guid.NewGuid().ToString() } );
}
}
Sample output:
Whats in the box: 013b50a4-451e-4ae8-b0ba-73bdcb0dd612 :: ConsoleApplication1.ImmutableStruct Whats in the box: 176380e4-d8d8-4b8e-a85e-c29d7f09acd0 :: ConsoleApplication1.ImmutableStruct
(There's actually a small hint in the MSDN that indicates this is the intended behavior)
Why does the CLR allow mutating boxed (immutable) value types in this subtle way? I know that readonly is no guarantee, and I know that using "traditional" reflection a value instance can be easily mutated. This behavior becomes an issue, when the reference to the box is copied around and mutations show up in unexpected places.
One thing I have though about is that this enables using Reflection on value types at all - since the System.Reflection API works with object
only. But Reflection breaks apart when using Nullable<>
value types (they get boxed to null if they do not have a Value). Whats the story here?
The most basic types (numbers, strings, and null) are inherently immutable because there is nothing (field/property) to change about them. A 5 is a 5 is a 5. Any operation on the 5 only returns another immutable value.
Reference type whose object value cannot be modified once created. Immutable type variables can be used with same assumptions as primitive type variables that is, when immutable type variables are passed to method as arguments, the object value of that variable is guaranteed to be preserved.
Boxes aren't immutable as far as the CLR is concerned. Indeed, in C++/CLI I believe there's a way of mutating them directly.
However, in C# the unboxing operation always takes a copy - it's the C# language which prevents you from mutating the box, not the CLR. The IL unbox instruction merely provides a typed pointer into the box. From section 4.32 of partition III of ECMA-335 (the unbox
instruction):
The unbox instruction converts obj (of type O), the boxed representation of a value type, to valueTypePtr (a controlled-mutability managed pointer (§1.8.1.2.2), type &), its unboxed form. valuetype is a metadata token (a typeref, typedef or typespec). The type of valuetype contained within obj must be verifier-assignable-to valuetype.
Unlike
box
, which is required to make a copy of a value type for use in the object,unbox
is not required to copy the value type from the object. Typically it simply computes the address of the value type that is already present inside of the boxed object.
The C# compiler always generates IL which results in unbox
being followed by a copying operation, or unbox.any
which is equivalent to unbox
followed by ldobj
. The generated IL isn't part of the C# spec of course, but this is (section 4.3 of the C# 4 spec):
An unboxing operation to a non-nullable-value-type consists of first checking that the object instance is a boxed value of the given non-nullable-value-type, and then copying the value out of the instance.
Unboxing to a nullable-type produces the null value of the nullable-type if the source operand is
null
, or the wrapped result of unboxing the object instance to the underlying type of the nullable-type otherwise.
In this case, you're using reflection and therefore bypassing the protection offered by C#. (It's a particularly odd use of reflection too, I must say... calling a constructor "on" a target instance is very strange - I don't think I've ever seen that before.)
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