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

发布于 2024-12-01 08:18:28 字数 1542 浏览 0 评论 0原文

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

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

¢蛋碎的人ぎ生 2024-12-08 08:18:28

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

然而,在 C# 中,拆箱操作始终需要一个副本 - 是 C# 语言(而不是 CLR)阻止您改变盒子。 IL 拆箱指令仅提供进入框中的类型化指针。来自 ECMA-335 分区 III 的第 4.32 节(<代码>拆箱指令):

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

box 不同的是,box 需要复制值类型以便在对象中使用,而 unbox不需要进行复制对象的值类型。通常,它只是计算已存在于装箱对象内部的值类型的地址。

C# 编译器始终生成 IL,其结果是 unbox 后跟复制操作,或 unbox.any 相当于 unbox 后跟 <代码>ldobj。当然,生成的 IL 不是 C# 规范的一部分,但这是(C# 4 规范的第 4.3 节):

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

如果源操作数为 null,则拆箱为 nullable-type 会生成 nullable-type 的 null 值,或者包装结果否则,将对象实例拆箱为可空类型的基础类型。

在这种情况下,您使用反射,因此绕过了 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 unbox instruction converts obj (of type O), the boxed representation of a value type, to valueTypePtr (a controlled-mutability managed pointer (§1.8.1.2.2), type &), its unboxed form. valuetype is a metadata token (a typeref, typedef or typespec). The type of valuetype contained within obj must be verifier-assignable-to valuetype.

Unlike box, which is required to make a copy of a value type for use in the object, unbox is not required to copy the value type from the object. Typically it simply computes the address of the value type that is already present inside of the boxed object.

The C# compiler always generates IL which results in unbox being followed by a copying operation, or unbox.any which is equivalent to unbox followed by ldobj. The generated IL isn't part of the C# spec of course, but this is (section 4.3 of the C# 4 spec):

An unboxing operation to a non-nullable-value-type consists of first checking that the object instance is a boxed value of the given non-nullable-value-type, and then copying the value out of the instance.

Unboxing to a nullable-type produces the null value of the nullable-type if the source operand is null, or the wrapped result of unboxing the object instance to the underlying type of the nullable-type otherwise.

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.)

蓬勃野心 2024-12-08 08:18:28

只是补充一下。

在 IL 中,如果使用一些“不安全”(读取不可验证)代码,则可以更改装箱值。

C# 的等价形式类似于:

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

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

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:

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

object a = 1;
Foo(a);
// now a is 2
此刻的回忆 2024-12-08 08:18:28

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

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

尽管第一种情况是类型的属性而不是实例,但“可变性”的概念与无状态类型无关。这并不是说这些类型没有用(*),而是说可变性的概念与它们无关。否则,保存任何状态的结构类型都是可变的,即使它们假装是可变的。请注意,具有讽刺意味的是,如果没有尝试使结构体“不可变”,而只是公开其字段(并且可能使用工厂方法而不是构造函数来设置其值),则通过其“构造函数”改变结构体实例将不会不工作。

(*)没有字段的结构类型可以实现接口并满足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:

  1. There does not exist any means of creating an instance of a structure which is in any way distinguishable from a default instance. For example, a structure with no fields could be reasonably considered immutable, since there would be nothing to mutate.
  2. The storage location holding the instance is privately held by something that will never mutate it.

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 type FormattableInteger<T> where T:IFormatableIntegerFormatter,new() whose ToString() method would perform T newT = new T(); return newT.Format(value); Using such an approach, if one had an array of 20,000 FormattableInteger<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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文