Порядок инициализации полей, статики и всего остального в C#

d5fb81ec3f964247b421359f2a0e2580

Всем привет! Многие сталкиваются с трудностями на собеседовании на впоросе по типу «Расскажите о порядке иницализации в C#». Либо банально когда видят квиз, стараются вспомнить, а что там должно инициализроватсья? Сегодня многие вспомнят, кто-то узнает о порядке инициализации. Затронем не только классы, а также стрктуры, а точнее — ключевое слово default для них.

Сделаем следующие классы и посмотрим, что будет при создании объекта B:

class A
{
    public int a =  Foo("pole a");
    public static int a1 = Foo($"вывод константы {con}\npole static a1");
    public const int con = 2;

    public A()
    {
        Console.WriteLine("const A");
    }

    static A()
    {
        Console.WriteLine("static const A");
    }

    public static int Foo(string c)
    {
        Console.WriteLine(c);
        return 1;
    }
}

class B:A
{
    public int b = Foo("pole b");
    public static int b1 = Foo("pole const b1");
    public B()
    {
        Console.WriteLine("const B");
    }

    static B()
    {
        Console.WriteLine("static const B");
    }
}

Единственное что нужно запомнить — каждый шаг работает с фкнцией Foo (она служит оповещением о пройденном этапе). У нас есть константа (c неё и начнём). Она определяется на этапе КОМПИЛЯЦИИ. То есть её значение мы узнаем первыми.

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

Сначала в бой идут статические конструкторы. Знает уже каждый. Но что же с полями? А всё просто — запоминаем простыми словами: «В констуркторе сначала идет инициализация полей пользователем, а дальше сама работа констурктора». То есть в Low-level коде всё выглядит попроще. Берем класс A:

internal class A
{
  public int a;
  public static int a1;
  public const int con = 2;

  public A()
  {
    this.a = A.Foo("pole a");
    base..ctor();
    Console.WriteLine("const A");
  }

  static A()
  {
    DefaultInterpolatedStringHandler interpolatedStringHandler = new DefaultInterpolatedStringHandler(31, 1);
    interpolatedStringHandler.AppendLiteral("вывод константы ");
    interpolatedStringHandler.AppendFormatted(2);
    interpolatedStringHandler.AppendLiteral("\npole static a1");
    A.a1 = A.Foo(interpolatedStringHandler.ToStringAndClear());
    Console.WriteLine("static const A");
  }

  [NullableContext(1)]
  public static int Foo(string c)
  {
    Console.WriteLine(c);
    return 1;
  }
}

Наши поля, которым мы задали значения — пусты (кроме константы)! Они инициализированы по умолчанию. А присвоение им значений происхдит в соответсвующих конструкторах (обычное поле — к обычному конструктору, статик — к статику). Теперь выставляем по полочкам:
Идёт сначала константа, далее статик конструктор (в нём сначала инициализация поля, далее работа конструктора), а после — обычный конструктор (логика та же, что и в статике).

Но у нас 2 класса, создаём мы дочерний класс, что нам выведется в итоге?

Давайте посмотрим на класс B в Low-level code:

internal class B : A
{
  public int b;
  public static int b1;

  public B()
  {
    this.b = A.Foo("pole b");
    base..ctor();
    Console.WriteLine("const B");
  }

  static B()
  {
    B.b1 = A.Foo("pole const b1");
    Console.WriteLine("static const B");
  }
}

Всё знакомо, видим (и помним!), что статик конструкторы не наследуются. А теперь смотрим на обычный конструктор — видим, что также сначала идет инициализация обычного поля b, а потом — вызывается конструктор родителя (если много наследований, всё будет также, получается «конструктор в конструкторе» для каждого класса). В конструкторе родителя (мы его смотрели выше) будет:

  public A()
  {
    this.a = A.Foo("pole a");
    base..ctor();
    Console.WriteLine("const A");
  }

То есть Foo для обычного поля, потом работа конструктора. Значит, после поля b мы приступим не к конструктору B, а к полю a.

Итоговый вывод:

вывод константы 2
pole static a1
static const A
pole const b1
static const B
pole b
pole a
const A
const B

Получается: идет работа статик конструкторов классов по очереди от родительского класса (тк мы в классе B можем брать стат поля из класса A, всё логично), далее идёт инициализация поля b, потом работа конструктора A (поле a + сам конструктор) и только потом работа конструктора B.

Выглядит громоздко, но запоминаем 2 вещи — сначала идут все статик конструкторы, потом обычные констуркторы + что такое конструктор в Low-level C# (работа с полем + наш конструктор).

А теперь к структуре — всё то же самое. Единственное слово default многих вводит в ступор. Вспоминаем, оно присваивает переменной дефолт значение. Для int — 0, для типов допускащих налл — null. Но что же со структурами?

Проверяем:

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            A a = default;
            Console.WriteLine(A.a1);
            Console.WriteLine(a.a);
        }

        private A a1;
    }
        
}

struct A
{
    public int a =  Foo("pole a");
    public static int a1 = Foo($"вывод константы {con}\npole static a1");
    public const int con = 2;

    public A()
    {
        Console.WriteLine("const A");
    }

    static A()
    {
        Console.WriteLine("static const A");
    }

    public static int Foo(string c)
    {
        Console.WriteLine(c);
        return 1;
    }
}

Создаём по той же идее СТРУКТУРУ A и тестим, заметьте, поле без вызова конструктора и присвоение default — одно и то же.

Запускаем

вывод константы 2
pole static a1
static const A
1
0

Обычный конструктор — не запускается, как и статик (его работа начинается, только если будем взаимодействовать с статик полем, поэтому вывод у статик поля — 1). Конструктор не изменил значение НЕстатик поля (вывод 0 в конце) и сам не сработал. Дефолт ставит значения всех полей структуры по умолчанию, что может быть неожиданно (особенно если поле класса — структра, и мы забудем её явно активировать конструктор). На этом и закончим, запомните, что такое конструктор и будет вам счастье!

© Habrahabr.ru