Це теж не повна відповідь, але у мене є кілька ідей.
Я вважаю, що я знайшов таке хороше пояснення, як ми знайдемо, не відповівши хтось із команди .NET JIT.
ОНОВЛЕННЯ
Я заглянув трохи глибше, і я вважаю, що знайшов джерело проблеми. Схоже, це викликано поєднанням помилки в логіці ініціалізації типу JIT та зміною компілятора C #, який спирається на припущення, що JIT працює за призначенням. Я думаю, що помилка JIT існувала в .NET 4.0, але була виявлена зміною компілятора для .NET 4.5.
Я не думаю, що beforefieldinitце єдине питання тут. Я думаю, що це простіше, ніж це.
Тип System.Stringу mscorlib.dll від .NET 4.0 містить статичний конструктор:
.method private hidebysig specialname rtspecialname static
void .cctor() cil managed
{
// Code size 11 (0xb)
.maxstack 8
IL_0000: ldstr ""
IL_0005: stsfld string System.String::Empty
IL_000a: ret
} // end of method String::.cctor
У версії .NET 4.5 mscorlib.dll String.cctor(статичний конструктор) помітно відсутній:
..... Немає статичного конструктора :( .....
В обох версіях Stringтип прикрашений beforefieldinit:
.class public auto ansi serializable sealed beforefieldinit System.String
Я намагався створити тип, який би компілювався в IL аналогічно (так, щоб у нього були статичні поля, але не було статичного конструктора .cctor), але я не міг цього зробити. Усі ці типи мають .cctorметод ІЛ:
public class MyString1 {
public static MyString1 Empty = new MyString1();
}
public class MyString2 {
public static MyString2 Empty = new MyString2();
static MyString2() {}
}
public class MyString3 {
public static MyString3 Empty;
static MyString3() { Empty = new MyString3(); }
}
Думаю, що між .NET 4.0 і 4.5 змінилися дві речі:
По-перше: EE було змінено, щоб воно автоматично ініціалізувалося String.Emptyз некерованого коду. Ця зміна, ймовірно, була зроблена для .NET 4.0.
По-друге: Компілятор змінився так, що не випромінював статичний конструктор для рядка, знаючи, що String.Emptyце буде призначено з некерованої сторони. Здається, ця зміна була здійснена для .NET 4.5.
Здається, що EE не призначається String.Emptyдосить швидко за деякими шляхами оптимізації. Зміни, внесені до компілятора (або все, що змінилося, щоб String.cctorзникнути), очікували, що EE виконає це завдання до того, як виконуватиметься будь-який код користувача, але виявляється, що EE не робить це призначення раніше, ніж String.Emptyвикористовується в методах рефікованих загальних загальних класів.
Нарешті, я вважаю, що помилка свідчить про більш глибоку проблему в логіці ініціалізації типу JIT. Здається, зміна компілятора є особливим випадком System.String, але я сумніваюся, що JIT зробив тут особливий випадок System.String.
Оригінал
Перш за все, WOW Люди BCL стали дуже креативними з деякими оптимізаціями роботи. Багато з Stringметодів в даний час здійснюються з допомогою різьблення статичного кешованого StringBuilderоб'єкта.
Я деякий час дотримувався цього підходу, але StringBuilderне використовується на Trimшляху коду, тому вирішив, що статична проблема теми не може бути.
Я думаю, що я виявив дивне прояв тієї самої помилки.
Цей код не вдається з порушенням доступу:
class A<T>
{
static A() { }
public A(out string s) {
s = string.Empty;
}
}
class B
{
static void Main() {
string s;
new A<object>(out s);
//new A<int>(out s);
System.Console.WriteLine(s.Length);
}
}
Однак, якщо ви незважаєте //new A<int>(out s);на Mainце, код працює просто чудово. Насправді, якщо Aвона повторно змінена з будь-яким типом посилань, програма виходить з ладу, але якщо Aїї повторно застосовують з будь-яким типом значення, код не виходить з ладу. Крім того, якщо ви коментуєте Aстатичний конструктор, код ніколи не виходить з ладу. Після заглиблення в Trimі Format, зрозуміло, що проблема полягає в тому Length, що вводиться, і що в цих зразках вище Stringтип не був ініціалізований. Зокрема, всередині корпусу A'конструктора, string.Emptyне призначено правильно, хоча всередині тіла Main, string.Emptyпризначено правильно.
Мені дивовижно, що ініціалізація типу Stringякось залежить від того, буде чи ні Aповторно змінена з типом значення. Моя єдина теорія полягає в тому, що існує якийсь оптимізуючи шлях коду JIT для загальної ініціалізації типу, який ділиться між усіма типами, і що цей шлях робить припущення щодо типів посилань BCL ("спеціальні типи?") Та їх стану. Швидкий погляд , хоча інші класи BCL з public staticполів показує , що в основному всі з них реалізувати статичний конструктор (навіть з порожніми конструкторами і немає даних, як System.DBNullі System.Emptyтипів. BCL значення з public staticполями , здається, не реалізувати статичний конструктор ( System.IntPtr, наприклад) Це, мабуть, вказує на те, що JIT робить деякі припущення щодо ініціалізації базового типу BCL.
FYI Ось JITed код для двох версій:
A<object>.ctor(out string):
public A(out string s) {
00000000 push rbx
00000001 sub rsp,20h
00000005 mov rbx,rdx
00000008 lea rdx,[FFEE38D0h]
0000000f mov rcx,qword ptr [rcx]
00000012 call 000000005F7AB4A0
s = string.Empty;
00000017 mov rdx,qword ptr [FFEE38D0h]
0000001e mov rcx,rbx
00000021 call 000000005F661180
00000026 nop
00000027 add rsp,20h
0000002b pop rbx
0000002c ret
}
A<int32>.ctor(out string):
public A(out string s) {
00000000 sub rsp,28h
00000004 mov rax,rdx
s = string.Empty;
00000007 mov rdx,12353250h
00000011 mov rdx,qword ptr [rdx]
00000014 mov rcx,rax
00000017 call 000000005F691160
0000001c nop
0000001d add rsp,28h
00000021 ret
}
Решта коду ( Main) однакова між двома версіями.
EDIT
Крім того, IL з двох версій ідентичний за винятком виклику A.ctorв B.Main(), де IL для першої версії містить:
newobj instance void class A`1<object>::.ctor(string&)
проти
... A`1<int32>...
у другій.
Ще слід зазначити, що код JITed для A<int>.ctor(out string): той самий, що і в негенеріальній версії.