12

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

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 允许以这种微妙的方式改变装箱(不可变)值类型?我知道 readonly 并不能保证,而且我知道使用“传统”反射可以轻松地改变值实例。当对盒子的引用被复制并且突变出现在意想不到的地方时,这种行为就会成为一个问题。

我所知道的一件事是,这完全可以在值类型上使用反射 - 因为 System.Reflection APIobject仅适用于。但是反射在使用值类型时会分开Nullable<>(如果它们没有值,它们会被装箱为空)。这里有什么故事?

4

3 回答 3

15

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

但是,在 C# 中,拆箱操作总是需要一个副本 - 是 C#语言阻止您改变盒子,而不是 CLR。IL unbox 指令仅仅提供一个类型化的指针到盒子中。从ECMA-335分区 III 的第 4.32 节(unbox指令):

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

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

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

对non-nullable-value-type的拆箱操作包括首先检查对象实例是否是给定non-nullable-value-type的装箱值,然后将值复制出实例。

如果源操作数是,则取消装箱到可空类型产生可空类型的空值null否则将对象实例取消装箱到可空类型的基础类型的包装结果。

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

于 2011-08-22T16:56:25.063 回答
3

只是补充。

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

C# 等价物类似于:

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

object a = 1;
Foo(a);
// now a is 2
于 2011-08-22T17:09:08.680 回答
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 次- - 每个实例一次。

于 2012-02-26T20:45:18.003 回答