Порядок строительства статического объекта

Вообще говоря, я понимал, что статические объекты в классах создаются, когда на класс ссылаются впервые. Однако я испытываю поведение, которого не ожидал.

Рассмотрим следующий класс

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 будет скомпилирован как Color.FromCode, поэтому WebColor не будет инициализирован. Лично я бы избегал попыток быть здесь «умным».

Jon Skeet 27.02.2024 12:11

Помимо «умного» имени, которое было первым именем, найденным для создания примера кода, есть ли какой-либо шанс/альтернатива увидеть, как это работает, сохраняя структуру кода?

user23137927 27.02.2024 12:28

Учитывая, что WebColor.FromCode сам по себе не инициализирует WebColor, я так не думаю. Вы могли бы добавить метод FromCode к самому WebColor, который, возможно, мог бы просто делегировать методу в базовом классе... но подобные вещи действительно могут привести к трудно диагностируемым проблемам. У вас действительно много «видов» цвета? Я бы удалил дженерики и наследование и оставил бы WebColor.FromCode, XyzColor.FromCode и т. д., каждый из которых использует свой отдельный кеш. (Заметьте, это может быть ColorCache или что-то в этом роде, чтобы избежать слишком большого дублирования.)

Jon Skeet 27.02.2024 12:33
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
3
83
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Из документации по статическим конструкторам:

Статический конструктор используется для инициализации любых статических данных или для выполнить определенное действие, которое необходимо выполнить только один раз. Это вызывается автоматически перед созданием первого экземпляра или любого другого статические члены ссылаются.

Итак, давайте посмотрим на это подробно. Чтобы ваш код работал, конструкторы статических полей для класса 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

user23137927 27.02.2024 13:17

Если вы проверите декомпиляцию (например, @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. Я не знал об этом.

user23137927 27.02.2024 13:12

Другие вопросы по теме