为什么CLR允许改变盒装的不可变值类型?

Joh*_*lph 12 .net c# clr boxing system.reflection

我有一种情况,我有一个简单的,不可变的值类型:

public struct ImmutableStruct
{
    private readonly string _name;

    public ImmutableStruct( string name )
    {
        _name = name;
    }

    public string Name
    {
        get { return _name; }
    }
}
Run Code Online (Sandbox Code Playgroud)

当我打开这个值类型的实例时,我通常会期望当我执行unbox时,我装箱的内容会相同.令我惊讶的是,事实并非如此.使用Reflection有人可以通过重新初始化其中包含的数据来轻松修改我的盒子的内存:

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() } );
    }
}
Run Code Online (Sandbox Code Playgroud)

样本输出:

框中有什么:013b50a4-451e-4ae8-b0ba-73bdcb0dd612 :: ConsoleApplication1.ImmutableStruct框中的内容:176380e4-d8d8-4b8e-a85e-c29d7f09acd0 :: ConsoleApplication1.ImmutableStruct

(MSDN中实际上有一个小提示,表明这是预期的行为)

为什么CLR允许以这种微妙的方式改变盒装(不可变)值类型?我知道readonly并不能保证,而且我知道使用"传统"反射可以很容易地改变一个值实例.当复制对框的引用并且突变显示在意外的位置时,此行为将成为问题.

我所拥有的一件事是,它允许在值类型上使用Reflection - 因为System.Reflection API object仅适用于.但是当使用Nullable<>值类型时,Reflection会分崩离析(如果它们没有值,它们会被装箱为null).这是什么故事?

Jon*_*eet 15

就CLR而言,框不是不可变的.实际上,在C++/CLI中我相信有一种方法可以直接改变它们.

但是,在C#中,取消装箱操作总是需要一个副本 - 它是C#语言,它阻止你改变盒子,而不是CLR.IL unbox指令仅提供指向框中的类型指针.从ECMA-335第III部分第4.32节(unbox指令):

unbox指令将obj(类型为O)(值类型的盒装表示)转换为valueTypePtr(受控可变性管理指针(§1.8.1.2.2),类型&),其未装箱形式.valuetype是元数据标记(typeref,typedef或typespec).obj中包含的valuetype类型必须是verifier-assignable-to valuetype.

不像box,这需要使值类型在对象使用的一个副本,unbox要求的值类型从对象复制.通常,它只是计算已装箱对象内部已存在的值类型的地址.

C#编译器总是生成IL,导致unbox后面跟着复制操作,或者unbox.any等效于unbox后面跟着ldobj.生成的IL当然不是C#规范的一部分,但这是(C#4规范的第4.3节):

非可空值类型的取消装箱操作包括首先检查对象实例是否为给定的非可空值类型的盒装值,然后将该值复制出实例.

开箱到一个可空型产生的空值可空型如果源操作数是null,或取消装箱的对象实例的基础类型的包裹结果可空型否则.

在这种情况下,您使用反射,因此绕过C#提供的保护.(这也是对反射的一种特别奇怪的用法,我必须说......在一个目标实例上"调用构造函数"非常奇怪 - 我不认为我以前见过它.)