Страницы

Поиск по вопросам

среда, 5 декабря 2018 г.

Inline инициализация полей

Рихтер в свой книге пишет, что если инициализировать поля inline, то в каждом конструкторе генерируется одинаковый IL-код инициализации этих полей и поэтому он советует не делать все inline, а выносить все в стандартный конструктор, а его вызывать из других конструкторов.
Актуально ли это сейчас? Почему microsoft сделали это именно так?


Ответ

Для начала, техническая сторона вопроса.
если инициализировать поля inline, то в каждом конструкторе генерируется одинаковый IL-код инициализации этих полей
Да, это так (для неделегирующих конструкторов, как подсказывает @PetSerAl, то есть, конструкторов, не указывающих this(...) вместо base(...)). Современная версия C# компилирует вот такой класс
public class C { int X = 1; public C() { Console.WriteLine("C()"); } public C(int y) { Console.WriteLine("C(int)"); } }
в такой IL

.class public auto ansi beforefieldinit C extends [mscorlib]System.Object { // Fields .field private int32 X
// Methods .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x2050 // Code size 24 (0x18) .maxstack 8
IL_0000: ldarg.0 IL_0001: ldc.i4.1 IL_0002: stfld int32 C::X IL_0007: ldarg.0 IL_0008: call instance void [mscorlib]System.Object::.ctor() IL_000d: ldstr "C()" IL_0012: call void [mscorlib]System.Console::WriteLine(string) IL_0017: ret } // end of method C::.ctor
.method public hidebysig specialname rtspecialname instance void .ctor ( int32 y ) cil managed { // Method begins at RVA 0x2069 // Code size 24 (0x18) .maxstack 8
IL_0000: ldarg.0 IL_0001: ldc.i4.1 IL_0002: stfld int32 C::X IL_0007: ldarg.0 IL_0008: call instance void [mscorlib]System.Object::.ctor() IL_000d: ldstr "C(int)" IL_0012: call void [mscorlib]System.Console::WriteLine(string) IL_0017: ret } // end of method C::.ctor
} // end of class C
Мы видим последовательность команд
IL_0000: ldarg.0 IL_0001: ldc.i4.1 IL_0002: stfld int32 C::X
которая инициализирует поле X, в обоих конструкторах.
Почему бы нам не вынести это в отдельный приватный конструктор, и не вызывать его самостоятельно из каждого публичного конструктора? (Приватный метод не подходит, т. к. он не может инициализировать readonly-поля.) Технически это можно, но это не одно и то же.
Разница начинается там, где у нас есть базовый класс с нетривиальным конструктором. Дело в том, что инициализаторы производного класса выполняются до выполнения базового конструктора. А вот сам конструктор производного класса выполняется после выполнения базового конструктора
Рассмотрим такой код:
public class B { public B() { Console.WriteLine("B constructor"); } }
public class C : B { public static int Get1() { Console.WriteLine("Getting 1"); return 1; }
int X = Get1(); public C() { Console.WriteLine("C Constructor"); } }
Конструктор C с точки зрения IL-кода таков:
X = Get1(); B::ctor(); Console.WriteLine("C Constructor");
и выведет, соответственно,
Getting 1 B constructor C Constructor
Если вы поместите инициализацию X в конструктор C, или в другой, вспомогательный конструктор класса C, то он будет выполнен лишь после окончания конструктора класса B. То есть, смысл кода будет другим.
Хуже того, такое преобразование не всегда возможно! Например, рассмотрим класс System.Exception
[Serializable] public class CustomException : Exception { readonly int ErrorCode; public CustomException(string message) : base(message) { } protected CustomException(SerializationInfo info, StreamingContext context) : base(info, context) { } }
Вынести общую часть в «общий» конструктор невозможно, т. к. общий конструктор будет не в состоянии вызвать правильный базовый конструктор.

Лазейкой может быть объявление конструкторов так, чтобы все они за исключением одного вызывали другие конструкторы того же класса, при этом инициализацию полей следует оставить там, где она есть. Например, если добавить конструктор
public C(int x) : this() { Console.WriteLine("C(int) Constructor"); }
при вызове его получим
Getting 1 B constructor C Constructor C(int) Constructor
В этом случае инициализация полей присутствует только в коде последнего конструктора. Впрочем, у этого трюка те же недостатки: не всегда возможно из «универсального» конструктора вызвать нужный базовый конструктор!

С технической стороной дела мы вроде бы разобрались. Теперь о реальном применении.
Я бы лично не заморачивался, и писал не «как экономнее», а как понятнее. Выигрыш от объединения в общий метод трёх-четырёх инициализаторов на копейку, а код становится более сложным, и к тому же приходится переписывать его без понятной для читателя необходимости. К тому же, вы можете считать, что компилятор самостоятельно применил к вашему коду оптимизацию, известную как method inlining :)

Ещё один аргумент за inline-инициализацию полей: то, что inline-инициализация происходит до вызова конструктора родительского типа, уменьшает шансы на обращение к неинициализированному объекту. Пример (одолжен из соседнего вопроса):
class Parent { public Parent() { DoSomething(); } protected virtual void DoSomething() {} }
class Child1 : Parent { private string foo = "FOO"; protected override void DoSomething() => Console.WriteLine(foo.ToLower()); }
class Child2 : Parent { private string foo; public Child2() { foo = "FOO"; } protected override void DoSomething() => Console.WriteLine(foo.ToLower()); }

Почему именно инициализиаторы пробегают до вызова базового конструктора, расписано у Эрика Липперта: Why Do Initializers Run In The Opposite Order As Constructors? Part One, Part Two

Комментариев нет:

Отправить комментарий