为什么 CLR 允许改变装箱不可变值类型?
我遇到的情况是,我有一个简单的、不可变的值类型:
public struct ImmutableStruct
{
private readonly string _name;
public ImmutableStruct( string name )
{
_name = name;
}
public string Name
{
get { return _name; }
}
}
当我装箱该值类型的实例时,我通常期望无论我装箱的是什么,当我取消装箱时都会得到相同的结果。令我惊讶的是,事实并非如此。使用反射,某人可以通过重新初始化其中包含的数据来轻松修改我的盒子的内存:
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 允许以这种微妙的方式改变装箱(不可变)值类型?我知道只读不是保证,而且我知道使用“传统”反射可以很容易地改变值实例。当对框的引用被复制并且突变出现在意想不到的地方时,这种行为就会成为一个问题。
我想到的一件事是,这完全可以在值类型上使用 Reflection - 因为 System.Reflection API 仅适用于 object
。但是当使用 Nullable<> 值类型时,反射就会崩溃(如果它们没有值,它们就会被装箱为 null)。这里有什么故事?
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?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
就 CLR 而言,框并不是一成不变的。事实上,在 C++/CLI 中,我相信有一种方法可以直接改变它们。
然而,在 C# 中,拆箱操作始终需要一个副本 - 是 C# 语言(而不是 CLR)阻止您改变盒子。 IL 拆箱指令仅提供进入框中的类型化指针。来自 ECMA-335 分区 III 的第 4.32 节(<代码>拆箱指令):
C# 编译器始终生成 IL,其结果是
unbox
后跟复制操作,或unbox.any
相当于unbox
后跟 <代码>ldobj。当然,生成的 IL 不是 C# 规范的一部分,但这是(C# 4 规范的第 4.3 节):在这种情况下,您使用反射,因此绕过了 C# 提供的保护。 (我必须说,这也是反射的一种特别奇怪的使用......在目标实例“上”调用构造函数非常奇怪 - 我认为我以前从未见过这种情况。)
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 C# compiler always generates IL which results in
unbox
being followed by a copying operation, orunbox.any
which is equivalent tounbox
followed byldobj
. The generated IL isn't part of the C# spec of course, but this is (section 4.3 of the C# 4 spec):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.)
只是补充一下。
在 IL 中,如果使用一些“不安全”(读取不可验证)代码,则可以更改装箱值。
C# 的等价形式类似于:
Just to add.
In IL, you can mutate a boxed value if you use some 'unsafe' (read unverifiable) code.
The C# equivalent is something like:
仅在以下情况下才应将值类型实例视为不可变:
尽管第一种情况是类型的属性而不是实例,但“可变性”的概念与无状态类型无关。这并不是说这些类型没有用(*),而是说可变性的概念与它们无关。否则,保存任何状态的结构类型都是可变的,即使它们假装是可变的。请注意,具有讽刺意味的是,如果没有尝试使结构体“不可变”,而只是公开其字段(并且可能使用工厂方法而不是构造函数来设置其值),则通过其“构造函数”改变结构体实例将不会不工作。
(*)没有字段的结构类型可以实现接口并满足
new
约束;不可能使用传入泛型类型的静态方法,但可以定义一个实现接口的简单结构,并将该结构的类型传递给可以创建新的虚拟实例并使用其方法的代码)。例如,可以定义一个类型FormattableInteger。其中 T:IFormatableIntegerFormatter,new()
其ToString()
方法将执行T newT = new T(); return newT.Format(value);
使用这种方法,如果有一个 20,000 个FormattableInteger
的数组,则存储整数的默认方法将作为 的一部分存储一次类型,而不是存储 20,000 次——每个实例一次。Value-type instances should be considered immutable only in the following cases:
Although the first scenario would be a property of a type rather than an instance, the notion of "mutability" is rather irrelevant for stateless types. That's not to imply such types are useless(*), but rather that the notion of mutability is irrelevant for them. Otherwise, struct types which hold any state are mutable, even if they pretend to be otherwise. Note that, ironically, if one didn't try to make a struct "immutable" but simply exposed its fields (and possibly used a factory method rather than a constructor to set its value), mutating a struct instance via its "constructor" wouldn't work.
(*)A struct type with no fields may implement an interface and satisfy a
new
constraint; it's not possible to use static methods of a passed-in generic type, but one can define a trivial structure which implements an interface and pass the type of the structure to code which can create a new dummy instance and use its methods). One could, for example, define a typeFormattableInteger<T> where T:IFormatableIntegerFormatter,new()
whoseToString()
method would performT newT = new T(); return newT.Format(value);
Using such an approach, if one had an array of 20,000FormattableInteger<HexIntegerFormatter>
, the default method for storing the integers would be stored once as part of the type, rather than being stored 20,000 times--once for each instance.