Как лучше всего обеспечить вызов статического конструктора базового класса?

документация по статическим конструкторам в C # говорит:

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

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

Теперь, я думаю, в большинстве случаев, когда вы имеете дело с производным классом, вы должны создать экземпляр, и он будет представлять собой экземпляр создаваемого базового класса, поэтому будет вызываться статический конструктор. Но если я имею дело только с статическими членами производного класса, что тогда?

Чтобы сделать это немного более конкретным, я подумал, что приведенный ниже код будет работать:

abstract class TypeBase
{
    static TypeBase()
    {
        Type<int>.Name = "int";
        Type<long>.Name = "long";
        Type<double>.Name = "double";
    }
}

class Type<T> : TypeBase
{
    public static string Name { get; internal set; }
}

class Program
{
    Console.WriteLine(Type<int>.Name);
}

Я предположил, что доступ к классу Type<T> автоматически вызовет статический конструктор для TypeBase; но, похоже, это не так. Type<int>.Name равно null, а приведенный выше код выводит пустую строку.

Помимо создания некоторого фиктивного члена (например, статического Initialize() метода, который ничего не делает), есть ли лучший способ гарантировать, что статический конструктор базового типа будет вызываться перед использованием любого из его производных типов?

Если нет, то ... фиктивный член это!


person Dan Tao    schedule 10.01.2011    source источник
comment
Если это не пример кода только ради вопроса, не могли бы вы просто сделать public static string Name { get { return typeof(T).Name; } }?   -  person Matt Greer    schedule 11.01.2011
comment
@Matt: Извините, я должен был прояснить это, вы правы: это пример кода просто для вопроса.   -  person Dan Tao    schedule 11.01.2011
comment
Используете абстрактный класс в качестве конкретного примера? ;-)   -  person Geeb    schedule 06.10.2017


Ответы (6)


Здесь правила очень сложны, и между CLR 2.0 и CLR 4.0 они фактически изменено тонкими и интересными способами, из-за чего IMO делает наиболее" умные "подходы хрупкими между версиями CLR. Initialize() метод также может не выполнять работу в CLR 4.0, если он не касается полей.

Я бы поискал альтернативный дизайн или, возможно, использовал бы обычную ленивую инициализацию в вашем типе (т.е.проверьте немного или ссылку (против null), чтобы увидеть, было ли это сделано).

person Marc Gravell    schedule 10.01.2011
comment
Мне удалось получить вызов фиктивного члена Initialize (), который не касался полей с помощью CLR 4.0. Согласно коду, который я опубликовал в своем ответе. - person Joshua Rodgers; 11.01.2011
comment
@Joshua: Пока у базового класса есть статический конструктор - а не только инициализаторы полей - тогда вызов Initialize гарантированно вызовет этот конструктор, потому что тип не будет отмечен beforefieldinit. Как сказано в спецификации, статический конструктор будет вызываться ... до создания первого экземпляра или ссылки на статические члены. И поскольку вопрос заключается в том, как заставить срабатывать статический конструктор, мы можем предположить, что он будет. - person LukeH; 11.01.2011
comment
Сказав это, полагаться на неясные уголки спецификации для гарантии функциональности - не лучшая идея, особенно когда кто-то, незнакомый с этим углом спецификации, приходит, чтобы обновить код через несколько лет. Гораздо лучше сделать что-нибудь явное и явно правильное, как вы предлагаете. - person LukeH; 11.01.2011
comment
Я пытался реализовать предложенную мной идею в этом ответе на основе предложения SLaks об использовании универсального статического типа вместо словаря. Я думал, что создание производного от неуниверсального типа и использование статического конструктора этого базового типа для инициализации словаря было бы элегантным решением; теперь, когда я понимаю, что для использования такого конструктора (например, фиктивного члена) потребуется взлом, я вижу, что вы правы (продолжение) ... - person Dan Tao; 11.01.2011
comment
... и вообще другой дизайн имел бы больше смысла. Думаю, я просто откажусь от идеи базового типа и просто воспользуюсь отдельным классом internal для выполнения нужной мне инициализации. - person Dan Tao; 11.01.2011
comment
Вторая ссылка мертва :( - person Erik Philips; 17.11.2016

Вы можете вызвать статический конструктор explicity, поэтому вам не нужно будет создавать какие-либо методы для инициализации:

System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof (TypeBase).TypeHandle);

Вы можете вызвать его в статическом конструкторе производного класса.

person petro.sidlovskyy    schedule 11.01.2011
comment
+1 Кажется, работает. Как и в случае любого решения для статического порядка инициализации, я сомневаюсь, является ли оно пуленепробиваемым, но, по крайней мере, оно не запускает статический ctor дважды, если вы вызываете его дважды в простом однопоточном приложении. - person Merlyn Morgan-Graham; 11.01.2011
comment
Любое представление о производительности этого метода? Использует ли он отражение и / или быстро, если статический конструктор уже был вызван? - person devios1; 22.02.2011
comment
Это способ сделать это - для репликации base () в статических конструкторах просто вызовите RuntimeHelpers. RunClassConstructor (typeof (TypeBase) .TypeHandle); в начале каждого конструктора. - person Tod Thomson; 30.01.2014

Как отмечали другие, ваш анализ верен. Спецификация реализована здесь буквально; поскольку ни один член базового класса не был вызван и экземпляр не был создан, статический конструктор базового класса не вызывается. Я понимаю, как это может быть удивительно, но это строгая и правильная реализация спецификации.

У меня нет никаких советов, кроме «если тебе больно, когда ты это делаешь, не делай этого». Я просто хотел указать, что противоположный случай тоже может вас укусить:

class Program 
{
  static void Main(string[] args)
  {      
    D.M();
  }      

}
class B 
{ 
  static B() { Console.WriteLine("B"); }
  public static void M() {}
} 
class D: B 
{ 
  static D() { Console.WriteLine("D"); }
}

Это печатает «B», несмотря на то, что был вызван «член D». M является членом D исключительно по наследству; CLR не имеет возможности отличить, был ли B.M вызван «через D» или «через B».

person Eric Lippert    schedule 11.01.2011

Во время всего моего тестирования мне удавалось получить вызов только фиктивного члена на базе, чтобы база вызвала свой статический конструктор, как показано:

class Base
{
    static Base()
    {
        Console.WriteLine("Base static constructor called.");
    }

    internal static void Initialize() { }
}

class Derived : Base
{
    static Derived()
    {
        Initialize(); //Removing this will cause the Base static constructor not to be executed.
        Console.WriteLine("Derived static constructor called.");
    }

    public static void DoStaticStuff()
    {
        Console.WriteLine("Doing static stuff.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Derived.DoStaticStuff();
    }
}

Другой вариант заключался в включении статического члена, доступного только для чтения, в производный тип, который выполнял следующие действия:

private static readonly Base myBase = new Base();

Однако это похоже на взлом (хотя и фиктивный член тоже), чтобы вызвать базовый статический конструктор.

person Joshua Rodgers    schedule 10.01.2011
comment
Да, да ... но, может быть, это единственный способ? - person Dan Tao; 11.01.2011
comment
Да, похоже, это единственный способ. Помимо вашего решения и фиктивного статического поля, которое создает основу, я не могу найти других способов сделать это. - person Joshua Rodgers; 11.01.2011

Я почти всегда жалею, что полагаюсь на что-то подобное. В дальнейшем статические методы и классы могут вас ограничить. Если вы позже захотите закодировать какое-то особое поведение для своего класса Type, вы окажетесь в коробке.

Итак, вот небольшой вариант вашего подхода. Это немного больше кода, но он позволит вам позже определить настраиваемый тип, который позволит вам делать настраиваемые вещи.

    abstract class TypeBase
    {
        private static bool _initialized;

        protected static void Initialize()
        {
            if (!_initialized)
            {
                Type<int>.Instance = new Type<int> {Name = "int"};
                Type<long>.Instance = new Type<long> {Name = "long"};
                Type<double>.Instance = new Type<double> {Name = "double"};
                _initialized = true;
            }
        }
    }

    class Type<T> : TypeBase
    {
        private static Type<T> _instance;

        public static Type<T> Instance
        {
            get
            {
                Initialize();
                return _instance;
            }
            internal set { _instance = value; }
        }

        public string Name { get; internal set; }
    }

Позже, когда вы доберетесь до добавления виртуального метода в Type и захотите специальной реализации для Type, вы можете реализовать его следующим образом:

class TypeInt : Type<int>
{
    public override string Foo()
    {
        return "Int Fooooo";
    }
}

А затем подключите его, изменив

protected static void Initialize()
{
      if (!_initialized)
      {
          Type<int>.Instance = new TypeInt {Name = "int"};
          Type<long>.Instance = new Type<long> {Name = "long"};
          Type<double>.Instance = new Type<double> {Name = "double"};
          _initialized = true;
       }
}

Я бы посоветовал избегать статических конструкторов - это легко сделать. Также избегайте статических классов и, где возможно, статических членов. Я не говорю никогда, просто умеренно. Предпочитайте синглтон класса статическому.

person Neil    schedule 11.01.2011
comment
+1: Хотя это не решает конкретную проблему, я думаю, что это хороший совет. - person Merlyn Morgan-Graham; 11.01.2011
comment
... и предпочитаю контейнеры IoC синглетонам. :) - person TrueWill; 20.01.2011
comment
это хорошая идея, но реализация не является потокобезопасной (и это действительно должно быть для чего-то вроде этого) - person T McKeown; 09.10.2019

Просто идея, вы можете сделать что-то вроде этого:

    abstract class TypeBase
    {
        static TypeBase()
        {
            Type<int>.Name = "int";
            Type<long>.Name = "long";
            Type<double>.Name = "double";
        }
    }

    class Type<T> : TypeBase
    {
        static Type() 
        {
            new Type<object>();
        }

        public static string Name { get; internal set; }
    }

    class Program
    {
        Console.WriteLine(Type<int>.Name);
    }
person HABJAN    schedule 11.01.2011