Работая над приложением C#, я только что заметил, что в нескольких местах статические инициализаторы зависят друг от друга следующим образом:
static private List<int> a = new List<int>() { 0 };
static private List<int> b = new List<int>() { a[0] };
Не делая ничего особенного, что работало. Это просто удача? Есть ли в C# правила для решения этой проблемы?
Редактировать: (re: Panos) В файле лексический порядок кажется королем? как насчет файлов?
При поиске я пробовал такую циклическую зависимость:
static private List<int> a = new List<int>() { b[0] };
static private List<int> b = new List<int>() { a[0] };
и программа работала иначе (тестовый костюм не прошел по всем направлениям, и я не стал смотреть дальше).
Теперь, для файлов одного и того же класса (частичный класс), вероятно, препроцессор должен определить, выйдет ли он из строя или нет.
Итак, если A ссылается на B.b, то получение A.a приведет к столкновению с B.b?





Похоже, это зависит от последовательности строк. Этот код работает:
static private List<int> a = new List<int>() { 1 };
static private List<int> b = new List<int>() { a[0] };
пока этот код не работает (выдает NullReferenceException)
static private List<int> a = new List<int>() { b[0] };
static private List<int> b = new List<int>() { 1 };
Итак, очевидно, что правил для циклической зависимости не существует. Однако странно, что компилятор не жалуется ...
РЕДАКТИРОВАТЬ - Что происходит «по файлам»? Если мы объявим эти два класса:
public class A {
public static List<int> a = new List<int>() { B.b[0] };
}
public class B {
public static List<int> b = new List<int>() { A.a[0] };
}
и попробуйте получить к ним доступ с помощью этого кода:
try { Console.WriteLine(B.b); } catch (Exception e) { Console.WriteLine(e.InnerException.Message.); }
try { Console.WriteLine(A.a); } catch (Exception e) { Console.WriteLine(e.InnerException.Message); }
try { Console.WriteLine(B.b); } catch (Exception e) { Console.WriteLine(e.InnerException.Message); }
мы получаем такой вывод:
The type initializer for 'A' threw an exception.
Object reference not set to an instance of an object.
The type initializer for 'A' threw an exception.
Таким образом, инициализация B вызывает исключение в статическом конструкторе A и оставляет поле a со значением по умолчанию (null). Поскольку a - это null, b также не может быть правильно инициализирован.
Если у нас нет циклических зависимостей, все работает нормально.
Обновлено: На всякий случай, если вы не читали комментарии, Джон Скит дает очень интересное чтение: Различия между статическими конструкторами и инициализаторами типов.
Это точно. Статический конструктор вызывается при первой ссылке на тип.
Будьте осторожны с различиями между инициализаторами статических переменных и статическими конструкторами. Существуют разные правила, когда инициализация типа происходит на основе наличия / отсутствия статического конструктора. См. pobox.com/~skeet/csharp/beforefieldinit.html
Да, тебе повезло. Кажется, что C# выполняет код в том порядке, в котором он отображается в классе.
static private List<int> a = new List<int>() { 0 };
static private List<int> b = new List<int>() { a[0] };
Будет работать, но ...
static private List<int> b = new List<int>() { a[0] };
static private List<int> a = new List<int>() { 0 };
Не удастся.
Я бы рекомендовал разместить все ваши зависимости в одном месте, статический конструктор как раз для этого.
static MyClass()
{
a = new List<int>() { 0 };
b = new List<int>() { a[0] };
}
Лично я бы избавился от статических инициализаторов, поскольку они не ясны, и добавил бы статический конструктор для инициализации этих переменных.
static private List<int> a;
static private List<int> b;
static SomeClass()
{
a = new List<int>() { 0 };
b = new List<int>() { a[0] };
}
Тогда вам не нужно гадать, что происходит, и вы ясно понимаете свои намерения.
Обратите внимание, что эти фрагменты кода не эквивалентны точно с точки зрения времени их выполнения: pobox.com/~skeet/csharp/beforefieldinit.html
См. Правила здесь раздел 10.4 спецификации C#:
when a class is initialized, all static fields in that class are first initialized to their default values, and then the static field initializers are executed in textual order. Likewise, when an instance of a class is created, all instance fields in that instance are first initialized to their default values, and then the instance field initializers are executed in textual order. It is possible for static fields with variable initializers to be observed in their default value state. However, this is strongly discouraged as a matter of style.
Другими словами, в вашем примере «b» инициализируется своим состоянием по умолчанию (null), и поэтому ссылка на него в инициализаторе «a» является допустимой, но приведет к исключению NullReferenceException.
Эти правила отличаются от правил Java (см. раздел 8.3.2.3 JLS для правил Java о прямых ссылках, которые являются более строгими).
Я не специалист по C# (просто знал, где искать), но обратите внимание на msdn.microsoft.com/en-us/library/aa645612(VS.71).aspx - «Можно построить циклические зависимости, которые позволяют наблюдать статические поля с инициализаторами переменных в их состоянии значения по умолчанию». Это помогает?
@ Кован, не совсем. Он не решает проблему перекрестных файлов.
Я думаю, что в файлах (т.е. в разных классах) это будет одинаково. Во время инициализации типа класса A классу B будет предложено инициализировать, и класс B найдет нулевую ссылку на класс A.