Почему CLR разрешает мутировать типы неизменяемых значений в штучной упаковке? - PullRequest
12 голосов
/ 22 августа 2011

У меня есть ситуация, когда у меня есть простой, неизменный тип значения:

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 для типов значений - поскольку API-интерфейс System.Reflection работает только с object.Но Reflection разбивается на части при использовании Nullable<> типов значений (они упаковываются в нуль, если у них нет значения).Что за история здесь?

Ответы [ 3 ]

15 голосов
/ 22 августа 2011

Ящики не являются неизменными в отношении CLR. Действительно, в C ++ / CLI я считаю, что есть способ их поменять напрямую.

Тем не менее, в C # операция распаковки всегда занимает копию - это язык C # , который предотвращает изменение окна, а не CLR. Инструкция распаковки IL просто предоставляет типизированный указатель в поле. Из раздела 4.32 раздела III ECMA-335 (инструкция unbox):

Инструкция unbox преобразует obj (типа O), упакованное представление типа значения, в valueTypePtr (управляемый указатель управляемой изменчивости (§1.8.1.2.2), type &), в его распакованную форму. valuetype - это токен метаданных (typeref, typedef или typespec). Тип valuetype , содержащийся в obj , должен иметь значение присвоения для верификатора.

В отличие от box, который требуется для копирования типа значения для использования в объекте, unbox равен , а не , необходимому для копирования типа значения из объекта. Обычно он просто вычисляет адрес типа значения, который уже присутствует внутри упакованного объекта.

Компилятор C # всегда генерирует IL, в результате чего за unbox следует операция копирования, или unbox.any, что эквивалентно unbox, за которым следует ldobj. Сгенерированный IL, конечно, не является частью спецификации C #, но это (раздел 4.3 спецификации C # 4):

Операция распаковки для типа, отличного от nullable-value , состоит из первой проверки, что экземпляр объекта представляет собой упакованное значение заданного типа, отличного от null-value-type , а затем скопировать значение из экземпляра.

Распаковка в nullable-type дает нулевое значение nullable-type , если исходным операндом является null, или завернутый результат распаковки экземпляра объекта в базовый тип nullable-type в противном случае.

В этом случае вы используете отражение и поэтому обходите защиту, предлагаемую C #. (Это тоже очень странное использование отражения, я должен сказать ... вызывать конструктор "на" целевом экземпляре очень странно - я не думаю, что когда-либо видел это раньше.)

3 голосов
/ 22 августа 2011

Просто добавьте.

В IL вы можете изменить коробочное значение, если используете какой-то небезопасный (читай не проверяемый) код.

Эквивалент C # выглядит примерно так:*

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

object a = 1;
Foo(a);
// now a is 2
0 голосов
/ 27 февраля 2012

Экземпляры типа значения следует считать неизменяемыми только в следующих случаях:

  1. Не существует никаких средств создания экземпляра структуры, который каким-либо образом можно отличить от экземпляра по умолчанию.Например, структуру без полей можно разумно считать неизменной, поскольку нечего было бы изменять.
  2. Место хранения, в котором находится экземпляр, находится в частном владении чего-то, что никогда не изменит его.

Хотя первый сценарий мог бы быть свойством типа, а не экземпляра, понятие "изменчивости" довольно неактуально для типов без состояния.Это не означает, что такие типы бесполезны (*), а скорее что понятие изменчивости для них неактуально.В противном случае структурные типы, которые содержат любое состояние, являются изменяемыми, даже если они притворяются, что они являются другими.Обратите внимание, что по иронии судьбы, если не пытаться сделать структуру «неизменяемой», а просто раскрыть ее поля (и, возможно, использовать метод фабрики, а не конструктор, чтобы установить его значение), мутировать экземпляр структуры через его «конструктор»не работает.

(*) Тип структуры без полей может реализовывать интерфейс и удовлетворять ограничению new;невозможно использовать статические методы переданного универсального типа, но можно определить тривиальную структуру, которая реализует интерфейс и передает тип структуры в код, который может создать новый фиктивный экземпляр и использовать его методы).Можно, например, определить тип FormattableInteger<T> where T:IFormatableIntegerFormatter,new(), чей метод ToString() будет выполнять T newT = new T(); return newT.Format(value); Используя такой подход, если у одного есть массив из 20 000 FormattableInteger<HexIntegerFormatter>, метод по умолчанию для хранения целых чисел будет сохранен один раз какчасть типа, а не хранится 20 000 раз - по одному разу для каждого экземпляра.

...