C# 为什么CLR允许变异装箱的不可变值类型?

C# 为什么CLR允许变异装箱的不可变值类型?,c#,.net,clr,boxing,system.reflection,C#,.net,Clr,Boxing,System.reflection,我的情况是,我有一个简单的、不可变的值类型: public struct ImmutableStruct { private readonly string _name; public ImmutableStruct( string name ) { _name = name; } public string Name { get { return _name; } } } 当我装箱这种值类型的实例时,

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

public struct ImmutableStruct
{
    private readonly string _name;

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

    public string Name
    {
        get { return _name; }
    }
}
当我装箱这种值类型的实例时,我通常希望装箱的内容在我进行unbox时都会显示出来。令我大吃一惊的是,事实并非如此。使用反射,某人可以通过重新初始化其中包含的数据来轻松修改我的盒子的内存:

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() } );
    }
}
样本输出:

盒子里有什么:013b50a4-451e-4ae8-b0ba-73bdcb0dd612:: 控制台应用程序1.ImmutableStruct框中的内容: 176380e4-d8d8-4b8e-a85e-c29d7f09acd0:: 控制台应用程序1.ImmutableStruct

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

为什么CLR允许以这种微妙的方式改变装箱(不可变)值类型?我知道只读并不能保证,而且我知道使用“传统”反射可以很容易地改变值实例。当对框的引用被复制,并且突变出现在意外的地方时,这种行为就会成为一个问题

我想到的一件事是,这允许对值类型使用反射,因为System.Reflection API只与
对象
一起工作。但是,当使用
可为null的
值类型时,反射会分离(如果它们没有值,则会被装箱为null)。这里的故事是什么?

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

然而,在C#中,取消装箱操作总是需要一个副本-是C#语言阻止了框的变异,而不是CLR。IL unbox指令仅向框中提供键入的指针。根据第三部分第4.32节(说明
unbox
):

unbox指令将值类型的装箱表示形式obj(O型)转换为valueTypePtr(受控可变管理指针(§1.8.1.2.2),类型为&),即其未装箱形式。valuetype是元数据标记(typeref、typedef或typespec)。obj中包含的valuetype类型必须是可分配给valuetype的验证器

与创建值类型副本以在对象中使用所需的
box
不同,从对象中复制值类型不需要
unbox
。通常,它只计算装箱对象中已经存在的值类型的地址

C#编译器总是生成IL,导致
unbox
后面跟着一个复制操作,或
unbox。任何
都相当于
unbox
后面跟着
ldobj
。生成的IL当然不是C#4规范的一部分,但这是(C#4规范的第4.3节):

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

如果源操作数为
null
,则取消绑定到可空类型将生成可空类型的null值,否则将对象实例取消绑定到可空类型的基础类型的包装结果

在本例中,您使用的是反射,因此绕过了C#提供的保护。(这也是反射的一种特别奇怪的用法,我必须说……在目标实例上“调用”构造函数是非常奇怪的——我想我以前从未见过这种情况。)

只是补充一下

在IL中,如果使用一些“不安全”(读取不可验证)代码,则可以对装箱值进行变异

C#等价物类似于:

unsafe void Foo(object o)
{
  void* p = o;
  ((int*)p) = 2;
}

object a = 1;
Foo(a);
// now a is 2

只有在以下情况下,才应将值类型实例视为不可变的:

  • 不存在任何创建结构实例的方法,该结构实例在任何方面都与默认实例不同。例如,一个没有字段的结构可以被合理地认为是不可变的,因为不会有任何变化。
  • 保存实例的存储位置是由永远不会对其进行变异的东西私有的。 尽管第一个场景是类型的属性而不是实例,但“可变性”的概念与无状态类型无关。这并不是说这些类型是无用的(*),而是说可变性的概念与它们无关。否则,持有任何状态的结构类型都是可变的,即使它们假装是可变的。请注意,具有讽刺意味的是,如果不尝试使结构“不可变”,而只是公开其字段(并且可能使用工厂方法而不是构造函数来设置其值),那么通过其“构造函数”对结构实例进行变异是行不通的


    (*)不带字段的结构类型可以实现接口并满足
    new
    约束;不可能使用传入泛型类型的静态方法,但是可以定义一个实现接口的普通结构,并将该结构的类型传递给可以创建新的伪实例并使用其方法的代码)。例如,可以定义一个类型
    FormattableInteger,其中T:iformatableingerformatter,new()
    ToString()
    方法将执行
    T newT=new T();返回新格式(值)
    使用这种方法,如果一个数组有20000个
    FormattableInteger
    ,存储整数的默认方法将作为类型的一部分存储一次,而不是存储20000次--每个实例一次。

    从未见过像这样调用
    ConstructorInfo
    。您正在调用ConstructorInfo(object,object[]),重新运行构造函数。这不会违反可读性。治疗方法是我的医生通常建议的:如果疼痛,就不要做。@Hans:当然,我知道所有这些(并且readonly不是clr强制执行的保证,尽管这是特定于实现的,并且符合CLI的实现不禁止强制执行readonly)