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

时间:2011-08-22 16:51:11

标签: 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; }
    }
}

当我打开这个值类型的实例时,我通常会发现当我打开一个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() } );
    }
}

示例输出:

  

方框内的内容: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)。这是什么故事?

3 个答案:

答案 0 :(得分:15)

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

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

  

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

     

box不同,unbox需要复制值类型才能在对象中使用,unbox 需要从对象复制值类型。通常,它只是计算已装箱对象内部已存在的值类型的地址。

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

  

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

     

如果源操作数为{{1}},则取消装箱到 nullable-type 会产生 nullable-type 的空值,或者取消装箱的结果为对象实例到 nullable-type 的基础类型。

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

答案 1 :(得分:3)

只是添加。

在IL中,如果使用某些“不安全”(读取无法验证的)代码,则可以改变一个盒装值。

C#等价物是这样的:

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

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

答案 2 :(得分:0)

仅在以下情况下才应将值类型实例视为不可变:

  1. 不存在任何创建结构实例的方法,该实例与默认实例有任何区别。例如,没有字段的结构可以合理地认为是不可变的,因为没有什么可以改变。
  2. 持有实例的存储位置是由永远不会改变它的东西私有。

虽然第一个场景是类型而不是实例的属性,但“可变性”的概念与无状态类型无关。这并不意味着这些类型是无用的(*),而是可变性的概念与它们无关。否则,保持任何状态的结构类型都是可变的,即使它们假装不是这样。请注意,具有讽刺意味的是,如果没有尝试使结构“不可变”但只是暴露其字段(并且可能使用工厂方法而不是构造函数来设置其值),则通过其“构造函数”改变结构实例不行。

(*)没有字段的结构类型可以实现接口并满足new约束;不可能使用传入泛型类型的静态方法,但可以定义一个实现接口的简单结构,并将结构类型传递给可以创建新虚拟实例并使用其方法的代码。例如,可以定义FormattableInteger<T> where T:IFormatableIntegerFormatter,new()方法将执行的ToString()类型T newT = new T(); return newT.Format(value);使用这种方法,如果有一个20,000 FormattableInteger<HexIntegerFormatter>的数组,则默认方法为存储整数将作为类型的一部分存储一次,而不是存储20,000次 - 每个实例一次。