Некоторые разработчики предпочитают объектную инициализацию использованию конструкторов. Кто-то негодует из-за вынужденного "перекладывания" аргументов из одного конструктора в другой при наследовании. Кому-то конструкторы не нравятся просто как таковые. Но возможна ли жизнь без конструкторов?
Если на скриншоте слева копипаст вам еще не кажется адским, представьте что аргументов у базового конструктора больше и MyClassChild тоже есть наследники, которые вынуждено наследуют родительский копипаст.
Object initialization
Простое решение здесь это банально не копипастить, отказаться от конструкторов, сделать сервисы паблик и создавать экземпляры через объектную инициализацию(в фигурных скобочках). Очевидной проблемой такого подхода является потеря каких-либо гарантий того, что мы точно проинициализировали все, что нужно. NullReference будет поджидать вас на каждой инициализации, ведь полное покрытие тестами обычно кажется слишком большой ценой.
Также становится сомнительно использование init в купе с nullable reference types. Дело в том, что мы теряем гарантии того, что init св-во не null:
Record
Records, представленные в C# 9 частично спасают ситуацию, но перекладывать аргументы все еще надо, плюс, не всегда хочется терять мутабельность:
Object initialization + Required
Нам поможет, на текущий момент proposed, фича - Required Members. Члены, обязательные для инициализации помечаются новым ключевым словом required, что дает возможность компилятору проверять, факт инициализации:
error CS9506: Required member 'MyClassNew.Service1' must be set in the object initializer or attribute constructor.
Модификатор допустим для полей и св-в классов, структур и рекордов. "Под капотом", члены типа с required
а также и сам тип помечаются атрибутом [RequiredMember]
. Как не сложно догадаться, есть вагон и маленькая тележка кейсов, в которых новый модификатор конфликтует с уже существующими. Из того, что на поверхности - такое поле нельзя делать недоступным для присвоения извне().
При этом конструкторы, не гарантирующие инициализацию всех required
членов типа, принудительно("под капотом") помечаются как нежелательные для использования. В том числе и конструктор по умолчанию:
Заключение
Как и прочие языковые фичи, required
должен применяться там, где он упрощает, а не усложняет жизнь, это не "золотая пуля". Лично я немного устал от бесполезных реализаций конструкторов, очень жду момента, когда их можно будет выпилить.
Комментарии (12)
onyxmaster
22.07.2022 15:29-1Ещё можно сделать source generator, который за вас будет писать конструкторы. Это не теория, я за пару дней справился (без поддержки наследования, но это у нас нужно редко).
blowin
22.07.2022 16:04+1Можно написать roslyn анализатор. Писал как-то такой себе
github.com/blowin/Blowin.Required
md_backend_binance
22.07.2022 16:17очень жду, мне очень не хватало этой прелести. Из моих хотелок остались пропосы на:
1) анонимные деконструкторы типов (в том числе и в ограничениях обобщенных методах)
2) наследование энамок
Надеюсь протолкну их как можно скорей , в отличии от required )))
IL_Agent
22.07.2022 20:11+5А если надо private поле инициализировать через конструктор? Лучше б primary constructor сделали. А вообще наследование - зло.
Wolfdp
22.07.2022 22:51Одна из альтернатив -- использовать Import\Export атрибуты. Правда это скорее всего только для случая "нужно проинициализировать 100500 полей, проект простой, а мне лень".
RadmirT
Большой список аргументов конструктора "звоночек" для проведения рефакторинга. К тому уже есть Паттерн позволяющий передавать большое количество аргументов: объект-параметр. В предлагаемом решении сложно определить какие параметры обязательно должны быть инициализированы, надо будет или компилировать каждый раз сборку или лезть в исходник и смотреть какие свойства помечены required
sstcvetkov Автор
Компилятор будет ругаться если не все проинициализировать.
Кажется, что объект-параметр тоже будет не акруален, по крайней мере не везде.
"звоночек" да, но когда цена рефакторина выше пользы, звоночек можно замьютать)
RadmirT
В том то и дело пока я не соберу код я не всегда смогу узнать какие свойства забыл инициализировать. А если все эти свойства "размазаны" по иерархии наследования то это может превратиться в нехилый квест. :) Я пользуюсь простым правилом как только количество аргументов становится больше 5 я или упаковываю их в объект параметр или делаю декомпозицию. Да и с большим количеством аргументов у меня получается не сильно много от силы 5-10% от всего кода, так что можно и по старинке через конструкторы :)
SadOcean
Ну то есть при прочих равных, как и с большим конструктором, мы просто будем иметь большой список инициализатора.
Из плюсов - только не переопределять эти параметры у наследника
Из минусов - контракт создания класса не ясен, тебе подсказывает, что класс простой, но на самом деле его просто так не проинициализировать.
dopusteam
Или строитель можно использовать, топовый паттерн)
Deosis
Проблема контруктора с большим количеством аргументов не решается объектом-пареметром. Просто она меняется на проблему инициализации объекта-параметра. И его придется инициализировать через конструктор с тем же количеством аргементов либо через свойства и ловить ошибки неполной инициализации в рантайме.
RadmirT
Тут была озвучена проблема передачи большойго количества аргументов по иерархии наследования. Данную проблему объект-параметр решит. Можно в конструкторе объектов параметра проводить нужную валидацию и не допускать неполную инициализацию. Ну я уже писал выше, что не нужно решать саму проблему, а нужно устранять причину. Большего количество аргументов а конструкторе повод провести рефакторинг или пересмотреть архитектуру.