Некоторые разработчики предпочитают объектную инициализацию использованию конструкторов. Кто-то негодует из-за вынужденного "перекладывания" аргументов из одного конструктора в другой при наследовании. Кому-то конструкторы не нравятся просто как таковые. Но возможна ли жизнь без конструкторов?

 л
л

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

Спецификация. Обсуждение на GitHub.

Комментарии (12)


  1. RadmirT
    22.07.2022 13:43
    +4

    Большой список аргументов конструктора "звоночек" для проведения рефакторинга. К тому уже есть Паттерн позволяющий передавать большое количество аргументов: объект-параметр. В предлагаемом решении сложно определить какие параметры обязательно должны быть инициализированы, надо будет или компилировать каждый раз сборку или лезть в исходник и смотреть какие свойства помечены required


    1. sstcvetkov Автор
      22.07.2022 13:45
      -1

      Компилятор будет ругаться если не все проинициализировать.
      Кажется, что объект-параметр тоже будет не акруален, по крайней мере не везде.
      "звоночек" да, но когда цена рефакторина выше пользы, звоночек можно замьютать)


      1. RadmirT
        22.07.2022 13:52

        В том то и дело пока я не соберу код я не всегда смогу узнать какие свойства забыл инициализировать. А если все эти свойства "размазаны" по иерархии наследования то это может превратиться в нехилый квест. :) Я пользуюсь простым правилом как только количество аргументов становится больше 5 я или упаковываю их в объект параметр или делаю декомпозицию. Да и с большим количеством аргументов у меня получается не сильно много от силы 5-10% от всего кода, так что можно и по старинке через конструкторы :)


      1. SadOcean
        22.07.2022 23:16

        Ну то есть при прочих равных, как и с большим конструктором, мы просто будем иметь большой список инициализатора.
        Из плюсов - только не переопределять эти параметры у наследника
        Из минусов - контракт создания класса не ясен, тебе подсказывает, что класс простой, но на самом деле его просто так не проинициализировать.


    1. dopusteam
      22.07.2022 14:07
      +2

      Или строитель можно использовать, топовый паттерн)


    1. Deosis
      25.07.2022 07:47

      Проблема контруктора с большим количеством аргументов не решается объектом-пареметром. Просто она меняется на проблему инициализации объекта-параметра. И его придется инициализировать через конструктор с тем же количеством аргементов либо через свойства и ловить ошибки неполной инициализации в рантайме.


      1. RadmirT
        25.07.2022 09:26
        +1

        Тут была озвучена проблема передачи большойго количества аргументов по иерархии наследования. Данную проблему объект-параметр решит. Можно в конструкторе объектов параметра проводить нужную валидацию и не допускать неполную инициализацию. Ну я уже писал выше, что не нужно решать саму проблему, а нужно устранять причину. Большего количество аргументов а конструкторе повод провести рефакторинг или пересмотреть архитектуру.


  1. onyxmaster
    22.07.2022 15:29
    -1

    Ещё можно сделать source generator, который за вас будет писать конструкторы. Это не теория, я за пару дней справился (без поддержки наследования, но это у нас нужно редко).


  1. blowin
    22.07.2022 16:04
    +1

    Можно написать roslyn анализатор. Писал как-то такой себе
    github.com/blowin/Blowin.Required


  1. md_backend_binance
    22.07.2022 16:17

    очень жду, мне очень не хватало этой прелести. Из моих хотелок остались пропосы на:

    1) анонимные деконструкторы типов (в том числе и в ограничениях обобщенных методах)

    2) наследование энамок

    Надеюсь протолкну их как можно скорей , в отличии от required )))


  1. IL_Agent
    22.07.2022 20:11
    +5

    А если надо private поле инициализировать через конструктор? Лучше б primary constructor сделали. А вообще наследование - зло.


  1. Wolfdp
    22.07.2022 22:51

    Одна из альтернатив -- использовать Import\Export атрибуты. Правда это скорее всего только для случая "нужно проинициализировать 100500 полей, проект простой, а мне лень".