Вообще говоря, я понимал, что статические объекты в классах создаются, когда на класс ссылаются впервые. Однако я испытываю поведение, которого не ожидал.
Рассмотрим следующий класс
public abstract class SmartColor<TColor> where TColor : SmartColor<TColor> {
private static readonly ConcurrentDictionary<string, TColor> _items = new();
protected SmartColor(string code, string name) {
Code = code;
Name = name;
//register all colors
foreach (var field in typeof(TColor).GetFields(BindingFlags.Public | BindingFlags.Static)) {
TColor? item = (TColor?)field.GetValue(null);
if (item is not null) {
Register(item);
}
}
}
public string Code { get; }
public string Name { get; }
public static TColor FromCode(string code) {
if (_items.TryGetValue(code, out var result)) {
return result;
}
return null;
}
private void Register(TColor item) {
_items.GetOrAdd(item.Code, item);
}
}
который, в свою очередь, унаследован от этого
public class WebColor : SmartColor<WebColor> {
public static readonly WebColor White = new WebColor("#ffffff", nameof(White));
public static readonly WebColor Black = new WebColor("#000000", nameof(Black));
protected WebColor(string code, string name) : base(code, name) {
}
}
При таком использовании он создает исключение NullReferenceException.
public static void Main()
{
WebColor color = WebColor.FromCode("#ffffff");
Console.WriteLine(color.Name);
}
Могу ли я понять, почему это происходит? Скрипка здесь
Помимо «умного» имени, которое было первым именем, найденным для создания примера кода, есть ли какой-либо шанс/альтернатива увидеть, как это работает, сохраняя структуру кода?
Учитывая, что WebColor.FromCode сам по себе не инициализирует WebColor, я так не думаю. Вы могли бы добавить метод FromCode к самому WebColor, который, возможно, мог бы просто делегировать методу в базовом классе... но подобные вещи действительно могут привести к трудно диагностируемым проблемам. У вас действительно много «видов» цвета? Я бы удалил дженерики и наследование и оставил бы WebColor.FromCode, XyzColor.FromCode и т. д., каждый из которых использует свой отдельный кеш. (Заметьте, это может быть ColorCache или что-то в этом роде, чтобы избежать слишком большого дублирования.)





Из документации по статическим конструкторам:
Статический конструктор используется для инициализации любых статических данных или для выполнить определенное действие, которое необходимо выполнить только один раз. Это вызывается автоматически перед созданием первого экземпляра или любого другого статические члены ссылаются.
Итак, давайте посмотрим на это подробно. Чтобы ваш код работал, конструкторы статических полей для класса WebColor должны быть вызваны до вызова WebColor.FromCode("#ffffff").
Был ли создан экземпляр WebColor? Нет.
Ссылались ли на какие-либо его статические члены? Нет. Метод FromCode() не является членом WebColor — это статический член SmartColor<TColor>, и хотя статические методы базовых классов можно вызывать через производный класс, на самом деле они не являются членами этого производного класса.
Следовательно, критерии для вызова статических инициализаторов WebColor не были выполнены, поэтому инициализация White не произошла и, следовательно, она не была добавлена к _items в SmartColor<T>.
Но обратите внимание, что сам _items был инициализирован из-за вызова WebColor.FromCode().
Вы можете исправить это немного хакерским способом, добавив статический конструктор в SmartColor<TColor> следующим образом:
static SmartColor()
{
Type type = typeof(WebColor);
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type.TypeHandle);
}
Выражает тот факт, что статические члены SmartColor<TColor> имеют временную зависимость от типа WebColor, и это заставит статические инициализаторы WebColor запускаться при запуске статических инициализаторов SmartColor<TColor>.
Однако, как отмечали другие, это несколько неуклюжий дизайн, поэтому, возможно, стоит использовать другой подход!
Приложение:
Вы можете избежать использования статического конструктора, используя такой класс:
public sealed class StaticClassInitialiser<T>
{
public StaticClassInitialiser()
{
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle);
}
}
А затем создайте статический экземпляр этого класса в классе SmartColor<TColor>:
static readonly StaticClassInitialiser<WebColor> _ = new();
Это может обмануть SonarQube, но вряд ли это улучшение — я включил его сюда только из любопытства.
Большое спасибо за ваш ответ. Я собираюсь принять это как наиболее полное, даже если другие указали на аналогичные решения и описали проблемы этой конструкции. Кроме того, инструменты статического анализа кода не рекомендуют использовать статический конструктор. Пример Sonarsource
Если вы проверите декомпиляцию (например, @Sharplab.io), вы увидите, что вызов WebColor.FromCode("#ffffff"); на самом деле:
IL_000b: ldstr "#ffffff"
IL_0010: call !0 class SmartColor`1<class WebColor>::FromCode(string)
IL_0015: stloc.0
то есть это вызов SmartColor<WebColor>.FromCode, который не требует инициализации WebColor.
Из документации:
Он вызывается автоматически перед созданием первого экземпляра или перед обращением к каким-либо статическим элементам. Статический конструктор будет вызываться не более одного раза.
FromCode не создает экземпляры WebColor и не использует из него статические реквизиты.
Один из способов исправить это — переместить логику инициализации в статический вектор SmartColor (также сделать метод Register статическим или просто встроить его):
static SmartColor()
{
foreach (var field in typeof(TColor).GetFields(BindingFlags.Public | BindingFlags.Static)) {
TColor? item = (TColor?)field.GetValue(null);
if (item is not null) {
Register(item);
}
}
}
Использование статических универсальных типов с некоторой логикой инициализации в векторах было приемом, который я часто использовал, когда мне нужно было «кэшировать» отражение до того, как стали доступны генераторы исходного кода, но обычно я делал это через отдельный тип без дополнительного наследования.
Большое спасибо за предложение Sharplab. Я не знал об этом.
Даже при этом вызов
WebColor.FromCodeбудет скомпилирован какColor.FromCode, поэтомуWebColorне будет инициализирован. Лично я бы избегал попыток быть здесь «умным».