Почему C# генерирует исключение TypeInitializationException, когда конструктор базового класса пытается добавить в статический список, который должен был быть инициализирован статическим конструктором, но равен null?
Когда конструктор базового класса C# пытается добавить в статический список, который должен был быть инициализирован статическим конструктором, но равен null, возникает исключение TypeInitializationException, поскольку статический конструктор еще не выполнился, оставляя статическое поле в состоянии null по умолчанию, а когда конструктор пытается изменить этот список, он вызывает исключение, которое среда CLR оборачивает в TypeInitializationException.
Содержание
- Понимание TypeInitializationException
- Порядок инициализации статических полей
- Проблемная ситуация
- Почему статический конструктор не выполняется
- Решения и лучшие практики
- Реальные примеры
Понимание TypeInitializationException
TypeInitializationException - это исключение CLR, которое возникает при сбое инициализации типа. Согласно документации Microsoft, “Если в статическом конструкторе возникает исключение, это исключение оборачивается в исключение TypeInitializationException, и тип не может быть создан”.
Это исключение служит оболочкой для исключений, возникающих во время статической инициализации, что усложняет отладку, поскольку исходное исключение может быть потеряно или скрыто. CLR использует этот механизм для обеспечения того, чтобы после сбоя статического конструктора тип оставался в нерабочем состоянии для всего домена приложения.
Порядок инициализации статических полей
Инициализация статических полей следует определенной последовательности, что важно понимать:
-
Присвоение значений по умолчанию: Когда процесс загружается, все статические поля сначала устанавливаются в их значения по умолчанию (0 для целых чисел, false для булевых значений, null для ссылочных типов)
-
Статические инициализаторы: Статические инициализаторы полей (в форме
private static readonly List<string> _items = new List<string>();) выполняются в текстовом порядке в соответствии с их положением в исходном коде -
Статический конструктор: Статический конструктор (статический конструктор) выполняется после всех статических инициализаторов
Важно: Статические инициализаторы выполняются до вызова любых статических конструкторов, а порядок выполнения статических конструкторов не определен.
Проблемная ситуация
Конкретная проблема описывается следующим образом:
public class BaseClass
{
public static List<string> SharedItems { get; private set; }
static BaseClass()
{
// Здесь должен инициализироваться SharedItems
SharedItems = new List<string>();
}
public BaseClass()
{
// Этот конструктор выполняется ДО статического конструктора!
SharedItems.Add("item"); // Выбрасывает NullReferenceException
}
}
Проблема заключается в том, что при создании экземпляра BaseClass конструктор экземпляра выполняется до статического конструктора. В этот момент SharedItems все еще равен null (его значение по умолчанию), поэтому вызов SharedItems.Add() вызывает NullReferenceException, который оборачивается CLR в TypeInitializationException.
Почему статический конструктор не выполняется
Статический конструктор выполняется только тогда, когда:
- В статический член обращаются впервые, ИЛИ
- Экземпляр класса создается впервые
Однако существует проблема с таймингом. При создании экземпляра:
- Выделяется память для экземпляра
- Поля экземпляра инициализируются значениями по умолчанию
- Выполняются инициализаторы полей экземпляра в текстовом порядке
- Выполняется конструктор базового класса (если применимо)
- Выполняется конструктор экземпляра
Статический конструктор выполняется после шага 4, что означает, что конструктор базового класса (и любые конструкторы базовых классов) выполняются до статического конструктора текущего класса.
Это создает состояние гонки, когда конструктор предполагает, что статические поля уже инициализированы, но они еще не были.
Решения и лучшие практики
1. Используйте отложенную инициализацию
public class BaseClass
{
private static readonly Lazy<List<string>> _sharedItems =
new Lazy<List<string>>(() => new List<string>());
public static List<string> SharedItems => _sharedItems.Value;
public BaseClass()
{
// Теперь безопасно - создаст список, если он еще не создан
SharedItems.Add("item");
}
}
2. Инициализируйте статические поля напрямую
public class BaseClass
{
// Инициализируйте напрямую, а не в статическом конструкторе
public static List<string> SharedItems { get; } = new List<string>();
public BaseClass()
{
// Теперь безопасно использовать
SharedItems.Add("item");
}
}
3. Проверяйте на null перед использованием
public class BaseClass
{
public static List<string> SharedItems { get; private set; }
static BaseClass()
{
SharedItems = new List<string>();
}
public BaseClass()
{
// Защитное программирование
SharedItems?.Add("item");
}
}
4. Избегайте статических зависимостей в конструкторах
Наиболее чистое решение - избегать наличия в конструкторах зависимостей от статических полей. Вместо этого используйте поля экземпляра или внедрение зависимостей.
Реальные примеры
Пример 1: Классы конфигурации
public class ConfigManager
{
public static Dictionary<string, string> Settings { get; private set; }
static ConfigManager()
{
Settings = LoadSettings();
}
public ConfigManager()
{
// Проблема: настройки могут еще не быть загружены
var value = Settings["key"]; // Может вызвать TypeInitializationException
}
}
Пример 2: Реестр с паттерном Registry
public class ServiceRegistry
{
public static List<IService> Services { get; private set; }
static ServiceRegistry()
{
Services = new List<IService>();
}
public ServiceRegistry(IService service)
{
// Проблема: список Services равен null, когда выполняется конструктор
Services.Add(service); // Выбрасывает NullReferenceException
}
}
Ключевое понимание заключается в том, что статические конструкторы выполняются после первого вызова конструктора экземпляра, а не до. Это означает, что вы никогда не должны предполагать, что статические поля инициализированы, когда выполняется конструктор экземпляра. Всегда используйте отложенную инициализацию, прямую инициализацию полей или защитные проверки на null, чтобы избежать проблем с TypeInitializationException.