Изменено поведение string.Empty(или System.String:: Empty) в .NET 4.5

Укороченная версия:

Код С#

typeof(string).GetField("Empty").SetValue(null, "Hello world!");
Console.WriteLine(string.Empty);

при компиляции и запуске выдает вывод "Hello world!" в .NET версии 4.0 и более ранних, но "" под .NET 4.5 и .NET 4.5.1.

Как можно игнорировать запись в поле или кто сбрасывает это поле?

Более длинная версия:

Я никогда не понимал, почему string.Empty поле (также известный как [mscorlib]System.String::Empty) не const (ака. literal), см " Почему не String.Empty постоянная? ". Это означает, что, например, в С# мы не можем использовать string.Empty в следующих ситуациях:

  • В операторе switch в форме case string.Empty:
  • В качестве значения по умолчанию для необязательного параметра, например void M(string x = string.Empty) { }
  • При применении атрибута, например [SomeAttribute(string.Empty)]
  • Другие ситуации, когда требуется постоянная времени компиляции

что имеет значение для известной "религиозной войны" по поводу того, использовать ли string.Empty или "", см. " В С# мне следует использовать string.Empty или String.Empty или" "для инициализации строки? ".

Пару лет назад я позабавился, установив Empty для какого-то другого экземпляра строки с помощью отражения, и увидел, как много частей BCL начали вести себя странно из-за этого. Это было довольно много. И изменение Empty ссылки, казалось, сохранялось в течение всего срока службы приложения. Сейчас, на днях, я попытался повторить этот маленький трюк, но потом использовал машину .NET 4.5, и я больше не мог этого делать.

(NB! Если на вашем компьютере установлен .NET 4.5, возможно, ваш PowerShell все еще использует более старую версию .NET(EDIT: верно только для Windows 7 или более ранней версии, где PowerShell не обновлялся после PowerShell 2.0), поэтому попробуйте вставить копию [String].GetField("Empty").SetValue($null, "Hello world!") PowerShell, чтобы увидеть некоторые эффекты изменения этой ссылки.)

Когда я попытался найти причину для этого, я наткнулся на интересную тему "В чем причина этой ошибки FatalExecutionEngineError в .NET 4.5 beta? ". В принятом ответе на этот вопрос отмечается, что в версии 4.0 System.String имел статический конструктор .cctor в котором было установлено поле Empty (в источнике С# это, конечно, просто инициализатор поля, конечно) в то время как в 4.5 нет статического конструктора. В обеих версиях само поле выглядит одинаково:

.field public static initonly string Empty

(как видно с IL DASM).

Похоже, что нет никаких других полей, кроме String::Empty. В качестве примера я экспериментировал с System.Diagnostics.Debugger::DefaultCategory. Этот случай кажется аналогичным: запечатанный класс, содержащий static readonly поле только для static initonly (static initonly) типа string. Но в этом случае работает нормально, чтобы изменить значение (ссылку) через отражение.

Вернуться к вопросу:

Как технически возможно, что Empty не меняется (в 4.5), когда я устанавливаю поле? Я проверил, что компилятор С# не "обманывает" чтение, он выводит IL как:

ldsfld     string [mscorlib]System.String::Empty

поэтому фактическое поле должно быть прочитано.


Редактировать после Баунти был поставлен на мой вопрос: Обратите внимание, что операция записи (которая нуждается отражение точно, так как поле readonly для initonly readonly (ака initonly в IL)) на самом деле работает, как ожидалось. Это операция чтения, которая является аномальной. Если вы читаете с отражением, как в typeof(string).GetField("Empty").GetValue(null), все в норме (т.е. видно изменение значения). Смотрите комментарии ниже.

Итак, лучший вопрос: почему эта новая версия фреймворка обманывает, когда читает это конкретное поле?

Ответ 1

Разница заключается в JIT для новой версии .NET, которая, по-видимому, оптимизирует ссылки на String.Empty, вставляя ссылку на конкретный экземпляр String вместо загрузки значения, хранящегося в поле Empty. Это оправдано под определением единственного ограничения в разделе ECMA-335 раздела я §8.6.1.2, которое может быть истолковано как означающее, что значение поля String.Empty не изменится после того, как String инициализируется.

Ответ 2

У меня нет ответа, может быть, какой-то намек.

Единственное отличие, которое я вижу между String::Empty и System.Diagnostics.Debugger::DefaultCategory, - это первое, отмеченное __DynamicallyInvokableAttribute.

Я не знаю значение этого недокументированного атрибута. Вопрос об этом атрибуте задан на SO: Что такое атрибут __DynamicallyInvokable для?

Я могу только предположить, что этот атрибут является уловкой во время выполнения, чтобы сделать некоторое кэширование?

Ответ 3

Потому что это возможно.

Значение этих системных полей initonly - это глобальные инварианты для среды выполнения .NET. Если эти инварианты нарушены, больше не существует никаких гарантий относительно поведения.

В С++ у нас, вероятно, будет правило, обозначающее это как вызывающее поведение undefined. В .NET это также поведение undefined, просто из-за отсутствия какого-либо правила, говорящего о том, что происходит, когда System.String.Empty.Length > 0. Вся спецификация всех слоев .NET и С# описывает поведение, когда System.String.Empty.Length == 0 и целая группа инвариантов также сохраняются.

Дополнительные сведения об оптимизации, которые зависят от времени выполнения и последствий, см. в ответах на