为什么CLR允许改变盒装不可变值类型
本文关键字:盒装 不可变 类型 改变 CLR 许改变 为什么 | 更新日期: 2023-09-27 18:06:16
我有一个简单的,不可变的值类型:
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::ConsoleApplication1。盒子里有什么?176380 e4-d8d8-4b8e-a85e-c29d7f09acd0::ConsoleApplication1。ImmutableStruct
(实际上MSDN中有一个小提示,表明这是预期的行为)
为什么CLR允许以这种微妙的方式改变盒装(不可变)值类型?我知道只读不能保证,而且我知道使用"传统"反射的值实例很容易发生变异。当对框的引用被复制,并且在意想不到的地方出现突变时,这种行为就会成为一个问题。
我想到的一件事是,这允许在值类型上使用反射——因为系统。反射API仅适用于object
。但是,当使用Nullable<>
值类型时,反射会破裂(如果它们没有值,则会被框为null)。这是怎么回事?
方框就CLR而言不是不可变的。事实上,在c++/CLI中,我相信有一种方法可以直接改变它们。
然而,在c#中拆箱操作总是需要一个副本——是c# 语言阻止你改变盒子,而不是CLR。ilunbox指令仅仅提供了一个进入盒子的类型指针。ECMA-335分区第4.32节(unbox
指令):
unbox指令将值类型的盒装表示obj(类型为O)转换为其未盒装形式valuetypetr(受控可变性托管指针(§1.8.1.2.2),类型为&;)。Valuetype是一个元数据标记(typeef、typepedef或typepec)。obj中包含的valuetype的类型必须是verifier-assignable-to valuetype
不像
box
,它需要复制一个值类型用于对象,unbox
是不需要从对象复制值类型。通常,它只是计算已经存在于盒装对象内部的值类型的地址。
c#编译器总是生成IL,导致unbox
后面跟着复制操作,或者unbox.any
相当于unbox
后面跟着ldobj
。生成的IL当然不是c#规范的一部分,但这是(c# 4规范的第4.3节):
对非空值类型的拆箱操作包括:首先检查对象实例是给定非空值类型的装箱值,然后将该值从实例中复制出来。
如果源操作数为
null
,则解装箱为空类型产生空类型的空值,否则将对象实例解装箱为空类型的底层类型的包装结果。
在这种情况下,你使用反射,因此绕过了c#提供的保护。(我必须说,这也是反射的一个特别奇怪的用法……调用构造函数"on"目标实例是非常奇怪的——我想我以前从未见过。)
只是补充一下。
在IL中,如果您使用一些"不安全"(读取不可验证)的代码,则可以改变盒装值。
在c#中相当于:
unsafe void Foo(object o)
{
void* p = o;
((int*)p) = 2;
}
object a = 1;
Foo(a);
// now a is 2
只有在以下情况下,值类型的实例才应该被认为是不可变的:
- 不存在任何创建结构实例的方法,它在任何方面都与默认实例不同。例如,没有字段的结构可以被合理地认为是不可变的,因为没有什么可以改变的。
- 保存实例的存储位置由永远不会改变它的东西私有持有。
尽管第一种情况是类型的属性而不是实例,但是"可变性"的概念与无状态类型无关。这并不是说这些类型是无用的(*),而是说可变性的概念与它们无关。否则,持有任何状态的结构类型都是可变的,即使它们假装是可变的。请注意,具有讽刺意味的是,如果不尝试使结构"不可变",而只是简单地暴露其字段(并且可能使用工厂方法而不是构造函数来设置其值),则通过其"构造函数"改变结构实例将不起作用。
(*)一个没有字段的struct类型可以实现一个接口并满足new
约束;不可能使用传入的泛型类型的静态方法,但可以定义一个简单的结构来实现接口,并将结构的类型传递给代码,代码可以创建一个新的虚拟实例并使用它的方法)。例如,可以定义一个类型FormattableInteger<T> where T:IFormatableIntegerFormatter,new()
,其ToString()
方法将执行T newT = new T(); return newT.Format(value);
。使用这种方法,如果有一个包含20,000个FormattableInteger<HexIntegerFormatter>
的数组,那么存储整数的默认方法将作为类型的一部分存储一次,而不是存储20,000次——每个实例存储一次。