Це теж не повна відповідь, але у мене є кілька ідей.
Я вважаю, що я знайшов таке хороше пояснення, як ми знайдемо, не відповівши хтось із команди .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)
: той самий, що і в негенеріальній версії.