繁体   English   中英

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

[英]Why does the CLR allow mutating boxed immutable value types?

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

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)。 这是什么故事?

就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节):

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

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

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

只是补充一下。

在IL中,如果使用某些“不安全”(read unverifiable)代码,则可以改变一个盒装值。

C#等价物是这样的:

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM