Изменено поведение 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
и целая группа инвариантов также сохраняются.
Дополнительные сведения об оптимизации, которые зависят от времени выполнения и последствий, см. в ответах на