Страницы

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

вторник, 26 ноября 2019 г.

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


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

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


Ответы

Ответ 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.

Ответ 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 поля должны инициализироваться только в конструкторе, поэтому сгенерированный компилятором закрытый метод вполне мог бы эти поля инициализировать.

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

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