Подтвердить что ты не робот

Почему CLR разрешает мутировать типы неизменяемых значений в штучной упаковке?

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

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<> (они попадают в нуль, если у них нет значения). Какая история здесь?

4b9b3361

Ответ 1

Коробки не являются неизменными в отношении CLR. В самом деле, в С++/CLI я считаю, что есть способ их непосредственного изменения.

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

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

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

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

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

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

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

Ответ 2

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

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

Эквивалент С# - это что-то вроде:

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

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

Ответ 3

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

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

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

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