Порядок инициализации полей, статики и всего остального в C#
Всем привет! Многие сталкиваются с трудностями на собеседовании на впоросе по типу «Расскажите о порядке иницализации в 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 в конце) и сам не сработал. Дефолт ставит значения всех полей структуры по умолчанию, что может быть неожиданно (особенно если поле класса — структра, и мы забудем её явно активировать конструктор). На этом и закончим, запомните, что такое конструктор и будет вам счастье!