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

307
21 ноября 2017, 21:10

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

Актуально ли это сейчас? Почему microsoft сделали это именно так?

Answer 1

Для начала, техническая сторона вопроса.

если инициализировать поля 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.

Answer 2

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

Актуально ли это сейчас?

Есть ряд советов, особенно в вопросах эффективности, на которые очень сложно ответить правильно, ибо "правильность" сильно зависит от вашего приложения. Например, человек, который работал на достаточно высоконагруженным приложением может дать такой совет "Никогда не используйте LINQ". Совет вполне разумен, если речь идет о критических участках высоконагруженного приложения, но очень плох в общем случае, ибо применим лишь для жалких полупроцента приложений.

Совет из серии "выделяйте конструкторы и не используйте field-like инициализаторы" звучит еще смешнее, поскольку применим еще к меньшему числу use case-ов.

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

Этот совет так же не актуален сегодня, как он был не актуален 10 лет назад. Точнее, он актуален десятку человек на планете, которые занимаются .net framework-ом, corefx-ом и, пожалуй, разработчикам реюзабельных компонентов для unity.

Идея совета вполне валидна: дублирование IL-кода в каждом конструкторе ведет к увеличению размеров сборки (что увеличит время ее загрузки) и к увеличению длительности JIT-компиляции.

Теоретически - проблема существует. Практически же, при принятии решения об использовании или не использовании field-like инициализации нужно отталкиваться от читабельности кода, а не от размера генерируемого IL-кода.

Почему microsoft сделали это именно так? Сходу очень сложно придумать альтернативные простые и работающие подходы. Можно было бы выкусить всю инициализацию и поместить ее в закрытый метод, пометить его MethodImpl(AggressiveInline) аттрибутом, и дернуть его из каждого конструктора (*).

Не исключаю, что авторы C#-а даже могли проверить это решение на практике и прийти к заключению, что разницы нет и оно того не стоит. Ну а если сейчас даже и будут подобные доказательства, то уже поздно и менять вряд ли кто-то что-то будет, ибо обратная совместимость. Да, компиляторостроители не горяд желанием ломать фундаментальные вещи по генерации кода, поскольку в мире есть много тулов, которые инспектируют IL и будут очень удивлены, увидев там вызов левого метода.

(*) Я тут не согласен с ув. @VladD по поводу того, что приватый метод здесь не подойдет. Я не уверен, что CLR на самом деле энфорсит правило, что readonly поля должны инициализироваться только в конструкторе, поэтому сгенерированный компилятором закрытый метод вполне мог бы эти поля инициализировать.

READ ALSO
Кастомный оператор сравнения ==

Кастомный оператор сравнения ==

Здравствуйте! В Unity3D реализована возможность опустить оператор сравнения когда объект проверяется на null, те

306
Как определить цифру в числе

Как определить цифру в числе

Как определить цифру из введенного числа? Например, из числа 1337 узнать вторую цифру

567
Case C# укоротить

Case C# укоротить

Добрый день!

368
C# Перетаскивание формы за любой элемент. ( FormBorderStyle: None )

C# Перетаскивание формы за любой элемент. ( FormBorderStyle: None )

Здравствуйте, довольно давно использую несколько методов, приведу код нижеВозможно есть более гуманное решение этой задачи, поделитесь...

424