Когда конструктор может генерировать исключение?

Когда конструктор может генерировать исключение? (Или в случае с Objective C: когда инициатор должен возвращать nil?)

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

Знайте свои исключения!
Знайте свои исключения!
В Java исключение - это событие, возникающее во время выполнения программы, которое нарушает нормальный ход выполнения инструкций программы. Когда...
Управление ответами api для исключений на Symfony с помощью KernelEvents
Управление ответами api для исключений на Symfony с помощью KernelEvents
Много раз при создании api нам нужно возвращать клиентам разные ответы в зависимости от возникшего исключения.
229
0
77 019
23
Перейти к ответу Данный вопрос помечен как решенный

Ответы 23

Из-за всех проблем, которые может вызвать частично созданный класс, я бы сказал никогда.

Если вам нужно что-то проверить во время строительства, сделайте конструктор закрытым и определите общедоступный статический фабричный метод. Метод может бросить, если что-то не так. Но если все проходит проверку, он вызывает конструктор, который гарантированно не сработает.

Я бы сказал, что все обстоит наоборот - если мы не хотим, чтобы частично созданные объекты, конструктор должен выбрасывает при возникновении проблемы - таким образом вызывающий будет знать, что что-то пошло не так.

Blair Conrad 17.09.2008 02:18

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

Ben Voigt 27.07.2016 19:31

@BenVoigt, это вряд ли оправдание.

user487772 01.08.2016 17:37

@TimBezhashvyly: Никогда - это слово, которое вы используете, только если утверждаете, что правило охватывает все сценарии. Это правило не действует. Бесполезно притворяться, что это так. «Бесполезно» - одна из утвержденных на сайте причин для фактического голосования против ... и вы расстраиваетесь из-за того, что люди решили не голосовать за, а это решение имеет почти бесконечное количество обоснований.

Ben Voigt 01.08.2016 17:47

@BenVoigt Вся суть этой методологии состоит в том, чтобы переместить весь потенциально исключительный код в фабричный метод и вызвать конструктор, когда Только просто выделит память для выполнения. Помимо исключений, исходящих извне приложения (память, ЦП, материнская плата, через ОС), у вас есть все необходимое. Вы обманываете себя, если думаете, что большинство инженеров проактивно обрабатывают исключения такого типа в каждом конструкторе.

cwharris 16.10.2017 20:24

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

cwharris 16.10.2017 20:27

@ChristopherHarris: Я полагаю, вы интерпретируете «все проверено» иначе, чем я интерпретировал ответ. Насколько я понимаю, это означает «проверки», а не «действия», что открывает перед вами проблему TOCTOU. Похоже, вы включаете потенциально изменяющие действия в категорию проверок. Но я полагаю, что «только выделение памяти для выполнения» тоже недостаточно, потому что они тоже могут дать сбой. Вам нужно, чтобы все было уже выделено, открыто, инициализировано ... и конструктор должен только перемещать дескрипторы к уже подготовленным ресурсам.

Ben Voigt 16.10.2017 21:16

@ChristopherHarris: Во всяком случае, фабричные функции, о которых вы говорите (они не методы), похоже, подпадают под понятие «конструктор или инициатор» в не зависящей от языка формулировке вопроса. Например, в COM только видимые конструкторы - это фабричные функции.

Ben Voigt 16.10.2017 21:23

@BenVoigt Достаточно честно - "все проверено" довольно неоднозначно. Я не понял своего предположения. Я согласен с тем, что простая проверка значений - наивный подход. TBH, вся эта тема сводится к DRY и удобству разработчика. Вам придется где-то обрабатывать исключения. Имеет ли смысл делать это централизованно, например, в конструкторе / статической фабричной функции? Может быть. Имеет ли смысл обрабатывать все типы исключений? Зависит от конструкции устойчивости. иногда лучше всего позволить чему-то взорваться и перезапустить.

cwharris 16.10.2017 21:52

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

Единственный раз, когда я действительно это сделал, - это когда где-то возникла проблема с безопасностью, которая означает, что объект не должен или не может быть создан.

Понятия не имею, почему кто-то оценил это так низко ... Мне кажется, что это довольно хорошая вещь, которую следует учитывать при создании исключения в конструкторе. Я уверен, что существует несколько проектов с утечками памяти, потому что деструктор не был вызван после выброса исключения.

Scott Swezey 17.09.2008 04:36

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

Sebastian Redl 16.09.2013 17:14
Ответ принят как подходящий

Задача конструктора - привести объект в пригодное для использования состояние. По этому поводу существуют две основные точки зрения.

Одна группа выступает за двухэтапное строительство. Конструктор просто переводит объект в спящее состояние, в котором он отказывается выполнять какую-либо работу. Есть дополнительная функция, которая выполняет фактическую инициализацию.

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

Одноэтапные конструкторы должны выбрасывать, если им не удается полностью инициализировать объект. Если объект не может быть инициализирован, он не должен существовать, поэтому конструктор должен выбросить.

Классы с одноэтапными конструкторами нелегко использовать в модульном тесте путем создания подклассов.

EricSchaefer 17.09.2008 02:06

Двухэтапная конструкция предназначена для сред, в которых исключения не работают должным образом или не реализованы. MFC использует двухэтапную конструкцию, потому что на момент написания в Visual C++ не было работающих исключений C++. Windows CE не получала исключений C++ до версии 4.0 и MFC 8.0.

Mike Dimmick 17.09.2008 04:14

@EricSchaefer: Я считаю, что для модульного тестирования лучше имитировать зависимости, чем использовать подклассы.

sleske 14.09.2009 15:03

Если для правильного функционирования коллекцию объектов необходимо связать друг с другом, могут потребоваться двухэтапные конструкторы. Двухэтапный подход полезен для внедрения зависимостей.

Greg 11.05.2012 15:10

Некоторые языки требуют двухэтапного построения. В C++, например, как вызывающий объект освобождает память для объекта, конструктор которого вызвал исключение?

retrodrone 13.07.2012 17:20

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

Sebastian Redl 27.07.2012 16:00

@SebastianRedl: если вы выделяете память в C++, а затем генерируете исключение, язык НЕ ИСПОЛЬЗУЕТ эту память. Также не будет вызван деструктор объекта для освобождения памяти. Вы должны перехватить любые исключения в конструкторе, освободить всю память, а затем повторно выбросить. (или сделайте все, что вы бросаете, прежде чем выделять какую-либо память - и выделить только один фрагмент памяти).

Patrick 29.08.2012 21:02

@Patrick: Не уверен, что вы имеете в виду - если вы сделаете 'new Foo' и бросает конструктор Foo, язык БУДЕТ освобождать память. Если вы выделяете память в конструкторе и не предоставляете никаких средств для ее освобождения, кроме деструктора, то язык не вернет ее, если вы добавите ее позже в cnostructor. Но тогда в любом случае вам следовало сразу же обернуть каждое выделение в объект RAII.

Sebastian Redl 07.09.2012 15:38

Ах, я прочитал ваш комментарий, предполагающий, что динамически выделяемая память будет освобождена языком

Patrick 07.09.2012 16:49

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

magulla 29.12.2014 23:33

@magulla Вы хотите уточнить это утверждение?

Sebastian Redl 01.01.2015 16:28

@Sebastian Redl Я бы сказал, что иметь незавершенный объект нехорошо, по крайней мере, с эстетической точки зрения :). Конечно, это не могло быть золотым правилом для любого домена. Трудно представить, почему конструктор генерирует исключение. Если это так, это, вероятно, нарушает принципы SOLID. Также здесь хорошая статья futuretask.blogspot.com/2006/05/…

magulla 15.01.2015 23:23

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

Sebastian Redl 16.01.2015 16:13

Хорошая и обширная дискуссия, к которой я хотел бы присоединиться из-за одного конкретного случая: Object-Pool-Pattern с его методом getInstance () и private / protecetd ctor. Как мы можем добавить в эту конструкцию передовой опыт? Скажем, я использую один поэтапный ctor, который должен возвращать более или менее полностью инициализированный объект. Но есть проверки действительности, которые могут помешать созданию. Так бы бросил. Как должен реагировать метод getInstance ()? Просто перебрасывает, значит, клиента ловить надо будет как следует?

icbytes 11.12.2015 12:10

@icbytes Если я правильно понимаю OPP, по сути, это оптимизация производительности для строительства. Итак, getInstance берет на себя роль конструктора, и если он не может удовлетворить запрос, то да, он должен выбросить.

Sebastian Redl 11.12.2015 15:12

ОК, звучит логично. Значит, он может перебросить, просто передать те исключения, которые выбрасывает ctor? (например, в Java подпись метода будет просто соответствовать сигнатуре исключения ctor) ???

icbytes 11.12.2015 15:39

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

Ghilteras 10.11.2018 10:34

@Ghilteras Как конструкторы, которые нельзя высмеивать, предотвращают их тестирование? Это может немного усложнить тестирование другого кода в классе, но это совсем другая проблема.

Sebastian Redl 10.11.2018 11:18

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

Ghilteras 10.11.2018 20:19

Гарантируется ли, что недопустимая ссылка на объект (которая создавалась) не может быть получена каким-либо образом после выброса исключения?

M.kazem Akhgary 13.01.2019 18:50

@ M.kazemAkhgary Если конструктор сохраняет указатель this в некоторую глобальную переменную, а затем выбрасывает, указатель в этой глобальной переменной будет болтаться.

Sebastian Redl 13.01.2019 18:57

См. Разделы часто задаваемых вопросов по C++ 17,2 и 17,4.

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

Конструктор разумно генерировать исключение, если он правильно очищает себя. Если вы следуете парадигме RAII (получение ресурсов - это инициализация), тогда является довольно часто конструктор выполняет значимую работу; хорошо написанный конструктор, в свою очередь, очистит после себя, если он не может быть полностью инициализирован.

@cgreen, пожалуйста, еще раз проверьте даты в этих сообщениях. Эта запись в блоге от 3 декабря 2008 г. - вышеприведенная запись от 16 сентября 2008 г. - почти за три месяца до того, как появилось это сообщение в блоге.

Jeff Atwood 21.09.2011 12:42

@cgreeno: Как его можно скопировать, если этот пост был сделан за три месяца до этого сообщения в блоге?

user195488 22.09.2011 21:02

@JeffAtwood Я дважды проверял даты, прежде чем опубликовал комментарий. Даты публикаций в блогах иногда меняются, чтобы они казались более актуальными. Тем не менее, я удалю комментарий, чтобы избежать разногласий, поскольку я мог ошибаться и не располагаю доказательствами этого.

cgreeno 11.01.2012 16:23

Да, если конструктору не удается построить одну из своих внутренних частей, он может - по своему усмотрению - выдавать (и на определенном языке объявлять) явное исключение, должным образом отмеченное в документации конструктора.

Это не единственный вариант: он может завершить конструктор и построить объект, но с методом isCoherent (), возвращающим false, чтобы иметь возможность сигнализировать о некогерентном состоянии (что может быть предпочтительнее в определенных случаях, чтобы чтобы избежать резкого прерывания рабочего процесса выполнения из-за исключения)
Предупреждение: как сказал Эрик Шефер в своем комментарии, это может внести некоторую сложность в модульное тестирование (бросок может увеличить цикломатическая сложность функции из-за условия, которое его запускает)

Если это не удается из-за вызывающей стороны (например, нулевой аргумент, предоставленный вызывающей стороной, где вызываемый конструктор ожидает ненулевой аргумент), конструктор все равно выдаст непроверенное исключение времени выполнения.

Создание исключения во время построения - отличный способ сделать ваш код более сложным. Вещи, которые казались простыми, внезапно становятся трудными. Например, допустим, у вас есть стек. Как вы открываете стек и возвращаете верхнее значение? Что ж, если объекты в стеке могут вставлять свои конструкторы (создание временного объекта для возврата вызывающему), вы не можете гарантировать, что не потеряете данные (уменьшите указатель стека, создайте возвращаемое значение, используя конструктор копирования значения в стек, который кидает, и теперь есть стек, который только что потерял предмет)! Вот почему std :: stack :: pop не возвращает значение, и вам нужно вызвать std :: stack :: top.

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

Это может быть отличным аргументом в пользу того, что конструкторы копирования не могут генерировать вызовы, но не обычные конструкторы.

Denise Skidmore 28.02.2018 19:00

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

На практике вам, возможно, придется быть очень осторожным. Помните, что в C++ деструктор не будет вызываться, поэтому, если вы выбрасываете после выделения ресурсов, вам нужно позаботиться о том, чтобы обработать это правильно!

Эта страница подробно обсуждает ситуацию в C++.

Если говорить строго с точки зрения Java, каждый раз, когда вы инициализируете конструктор с недопустимыми значениями, он должен вызывать исключение. Таким образом, он не будет построен в плохом состоянии.

Для меня это несколько философское дизайнерское решение.

Очень приятно иметь экземпляры, которые действительны, пока они существуют, начиная с момента создания. Для многих нетривиальных случаев это может потребовать выброса исключений из ctor, если невозможно выделить память / ресурсы.

Некоторые другие подходы - это метод init (), который имеет свои собственные проблемы. Один из них - обеспечение фактического вызова init ().

В одном из вариантов используется ленивый подход для автоматического вызова init () при первом вызове метода доступа / мутатора, но для этого требуется, чтобы любой потенциальный вызывающий объект беспокоился о том, что объект действителен. (В отличие от «он существует, следовательно, это действительная философия»).

Я видел различные предлагаемые шаблоны проектирования для решения этой проблемы. Например, возможность создать начальный объект через ctor, но при этом необходимо вызвать init (), чтобы получить доступ к содержащемуся, инициализированному объекту с акцесорами / мутаторами.

У каждого подхода есть свои плюсы и минусы; Я успешно все это использовал. Если вы не делаете готовые к использованию объекты с момента их создания, я рекомендую большую дозу утверждений или исключений, чтобы убедиться, что пользователи не взаимодействуют до init ().

Дополнение

Я писал с точки зрения программистов на C++. Я также предполагаю, что вы правильно используете идиому RAII для обработки ресурсов, высвобождаемых при возникновении исключений.

Обычный контракт в объектно-ориентированном стиле заключается в том, что методы объекта действительно работают.

Итак, как заключение, никогда не возвращать зомби-объект из конструктора / init.

Зомби не работает, и, возможно, в нем отсутствуют внутренние компоненты. Просто ожидание исключения с нулевым указателем.

Я впервые создал зомби в Objective C много лет назад.

Как и во всех эмпирических правилах, есть «исключения».

Вполне возможно, что специфический интерфейс может иметь контракт, в котором говорится, что существует метод "инициализации", позволяющий запускать исключение. Что объект, реализующий этот интерфейс, может некорректно реагировать на какие-либо вызовы, кроме установщиков свойств, до тех пор, пока не будет вызвана инициализация. Я использовал это для драйверов устройств в операционной системе OO во время процесса загрузки, и это было работоспособно.

В общем, вам не нужны зомби-объекты. В таких языках, как Smalltalk с становиться, все становится немного странно, но чрезмерное использование становиться - тоже плохой стиль. Become позволяет объекту превращаться в другой объект на месте, поэтому нет необходимости в оболочке-оболочке (Advanced C++) или шаблоне стратегии (GOF).

Эрик Липперт говорит есть 4 вида исключений.

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

Ваш конструктор никогда не должен сам по себе генерировать фатальное исключение, но код, который он выполняет, может вызвать фатальное исключение. Что-то вроде «нехватки памяти» нельзя контролировать, но если это происходит в конструкторе, это случается.

Тупоголовые исключения никогда не должны возникать ни в одном коде, поэтому они прямо сейчас.

Досадные исключения (например, Int32.Parse()) не должны создаваться конструкторами, потому что они не имеют неисключительных обстоятельств.

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

Ссылка для ссылки: https://blogs.msdn.microsoft.com/ericlippert/2008/09/10/vexing-exceptions/

Так где же в этой схеме находится Argument [Null] Exception? Это тупоголовое исключение, и его не следует бросать? Или это фатальное исключение, и его можно выбросить?

alastairs 06.07.2010 18:53

Теперь я понимаю, что не включил ссылку на исходную статью, blogs.msdn.com/b/ericlippert/archive/2008/09/10/… Такие вещи, как ArgumentException / ArgumentNullException, тупоголовы: просто ошибки в вызывающем коде. Он говорит: «Исправьте свой код, чтобы он никогда не запускал тупиковое исключение - исключение« индекс вне диапазона »никогда не должно происходить в производственном коде».

Jacob Krall 08.07.2010 00:56

@alastairs: вам обязательно стоит бросать ArgumentExceptions, потому что единственная альтернатива - притвориться, что аргументы действительны, когда это не так. (Это приводит к NullReferenceExceptions или, возможно, к гораздо худшим вещам.) Но, как говорит Джейкоб, вы должны никогда их поймать.

Joren 27.09.2010 22:25

Несомненно, создание исключений ArgumentException / ArgumentNullException / ArgumentoutOfRangeException для защиты аргументов в конструкторах или методах - это просто защитное программирование на основе здравого смысла для любого конструктора или метода, который является public или protected? В конце концов, вы не можете гарантировать, что весь вызывающий код будет передавать допустимые аргументы. Однако для private и internal, где у вас есть контроль, они могут не понадобиться, поскольку вы, программист НАХОДЯТСЯ, управляете вызывающим кодом.

Dib 10.12.2015 10:59

@Dib Да. Вы согласны с комментариями Джорена и моих по поводу тупоголовых исключений. Абсолютно бросьте их; никогда их не лови.

Jacob Krall 10.12.2015 18:43

@JacobKrall, могу я предложить добавить второе предложение этого комментария к «тупоголовой» части вашего ответа? Я читал ответ уже несколько раз - похоже, вы сосредоточились на перехвате исключений, в то время как OP просит их выбросить.

guntbert 16.10.2017 18:50

Согласитесь с @Dib. Мы должны выбросить все исключения о недопустимом состоянии в конструктор. Имо вопрос в том, должны ли мы их поймать? Нам не следует. Это, вероятно, приведет к дублированию выполнения кода: - сначала вы проверяете, что потенциально предотвращает создание недопустимого объекта - затем вы создаете объект и снова проверяете, чтобы проверить, следует ли вам бросать. Я считаю, что это правильный подход.

Tomasz Durka 21.07.2018 00:23

«результат неопрятных внешних реалий, посягающих на вашу красивую, четкую программную логику» ... Почему, спасибо!

Nir Lanka 22.11.2019 12:24

Я думаю, что этот ответ немного сбивает с толку, мы говорим о выбросе или перехвате исключений? «Тупоголовые исключения никогда не должны возникать ни в одном из ваших кодов, поэтому они прямо сейчас» Что это значит, разве мы не должны проверять в нашем коде грубые ошибки в случае неправильного поведения вызывающего кода и выдавать исключения там, где мы обнаруживаем недопустимое использование? Но вы, кажется, согласны с комментарием Дибса о таких исключениях? Я не слежу.

Alex 31.07.2020 18:35

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

Alex 05.08.2020 15:26

Я не могу обратиться к лучшим практикам в Objective-C, но в C++ конструктор может генерировать исключение. Тем более, что нет другого способа гарантировать, что исключительное условие, обнаруженное при построении, будет сообщено без обращения к методу isOK ().

Функция блока try была разработана специально для поддержки сбоев при поэлементной инициализации конструктора (хотя она также может использоваться для обычных функций). Это единственный способ изменить или дополнить генерируемую информацию об исключении. Но из-за своей первоначальной цели разработки (использование в конструкторах) он не позволяет исключить исключение с помощью пустого предложения catch ().

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

Например, если конструктор должен выделить 1024 КБ оперативной памяти, а он не может этого сделать, он должен выдать исключение, таким образом вызывающий конструктор знает, что объект не готов к использованию и есть ошибка. где-то, что нужно исправить.

Объекты, которые наполовину инициализированы и наполовину мертвы, просто вызывают проблемы и проблемы, так как вызывающий действительно не имеет возможности узнать об этом. Я бы предпочел, чтобы мой конструктор выдавал ошибку, когда что-то пойдет не так, вместо того, чтобы полагаться на программирование для запуска вызова функции isOK (), которая возвращает true или false.

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

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

в общем ничего не даст, отделив инициализацию объекта от построения. RAII правильный, успешный вызов конструктора должен привести либо к полностью инициализированному живому объекту, либо к сбою, а сбои ВСЕ в любой точке любого пути кода всегда должны вызывать исключение. Вы ничего не получите, используя отдельный метод init (), кроме дополнительной сложности на каком-то уровне. Контракт ctor должен либо возвращать функциональный действительный объект, либо очищаться после себя и выбрасывать.

Учтите, что если вы реализуете отдельный метод инициализации, вам придется вызывать его Все еще. У него по-прежнему будет возможность генерировать исключения, их все равно нужно обрабатывать, и они практически всегда должны вызываться сразу после конструктора, за исключением того, что теперь у вас есть 4 возможных состояния объекта вместо 2 (IE, сконструирован, инициализирован, неинициализирован, и не удалось против просто действительного и несуществующего).

В любом случае, за 25 лет разработки в объектно-ориентированном стиле я встречал случаи, когда казалось, что отдельный метод инициализации «решит некоторую проблему» - это недостатки дизайна. Если вам не нужен объект СЕЙЧАС, вам не следует создавать его сейчас, а если он вам нужен сейчас, вам нужно его инициализировать. KISS всегда должен следовать принципу, наряду с простой концепцией, согласно которой поведение, состояние и API любого интерфейса должны отражать ЧТО делает объект, а не КАК он это делает, клиентский код не должен даже знать, что объект имеет какой-либо вид. внутреннего состояния, требующего инициализации, поэтому шаблон init after нарушает этот принцип.

Один контрпример к «конструировать только при необходимости». Если он нужен в теле цикла, объявление его там уничтожит его при выходе из области видимости, что, возможно, будет расточительно. Наличие отдельной конструкции (в более широком масштабе) и инициализации / повторной инициализации (внутри цикла) позволяет повторно использовать ресурсы для последующих итераций вместо полной очистки и необходимости многократно восстанавливать их.

Ben Voigt 27.07.2016 19:29

Если при инициализации необходимо выполнить значительную логику, и вы хотите иметь множество c'tors, которые могут повторно использовать эту логику, то наличие отдельной функции init () будет правильным с точки зрения СУХОЙ, которая является частью KISS. Инициализацию все еще можно выполнить «за кадром» во время строительства.

Joshua Richardson 13.01.2017 20:29

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

Если вы пишете элементы управления UI (ASPX, WinForms, WPF, ...), вам следует избегать создания исключений в конструкторе, потому что дизайнер (Visual Studio) не может их обработать при создании элементов управления. Знайте свой жизненный цикл элемента управления (события управления) и по возможности используйте отложенную инициализацию.

Обратите внимание, что если вы создадите исключение в инициализаторе, вы закончите утечку, если какой-либо код использует шаблон [[[MyObj alloc] init] autorelease], поскольку исключение пропустит автоматический выпуск.

См. Этот вопрос:

Как предотвратить утечку при возникновении исключения в init?

Кажется, это зависит от языка. Можете ли вы привести пример языка, в котором этот шаблон распространен (я думаю, код выглядит как TCL или Objective-C).

Ben Voigt 27.07.2016 19:33

Насколько я могу судить, никто не предлагает достаточно очевидного решения, в котором воплощены лучшие черты как одноэтапного, так и двухэтапного строительства.

Примечание: Этот ответ предполагает C#, но принципы могут применяться на большинстве языков.

Во-первых, преимущества обоих:

Одноэтапный

Одноэтапное построение приносит нам пользу, предотвращая существование объектов в недопустимом состоянии, тем самым предотвращая все виды ошибочного управления состоянием и все связанные с ним ошибки. Тем не менее, это заставляет некоторых из нас чувствовать себя странно, потому что мы не хотим, чтобы наши конструкторы генерировали исключения, а иногда это то, что нам нужно делать, когда аргументы инициализации недействительны.

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    public Person(string name, DateTime dateOfBirth)
    {
        if (string.IsNullOrWhitespace(name))
        {
            throw new ArgumentException(nameof(name));
        }

        if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(dateOfBirth));
        }

        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }
}

Двухэтапный метод проверки

Двухэтапное построение приносит нам пользу, позволяя выполнять нашу проверку вне конструктора и, следовательно, предотвращает необходимость генерирования исключений внутри конструктора. Однако это оставляет нам «недействительные» экземпляры, что означает, что есть состояние, которое мы должны отслеживать и управлять для экземпляра, или мы выбрасываем его сразу после выделения кучи. Возникает вопрос: почему мы выполняем выделение кучи и, следовательно, сбор памяти для объекта, который в конечном итоге даже не используем?

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    public Person(string name, DateTime dateOfBirth)
    {
        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }

    public void Validate()
    {
        if (string.IsNullOrWhitespace(Name))
        {
            throw new ArgumentException(nameof(Name));
        }

        if (DateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
        }
    }
}

Одноэтапный через частный конструктор

Итак, как мы можем исключить исключения из наших конструкторов и предотвратить выделение кучи для объектов, которые будут немедленно отброшены? Это довольно просто: мы делаем конструктор закрытым и создаем экземпляры с помощью статического метода, предназначенного для создания экземпляра и, следовательно, выделения кучи, только для проверки после.

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    private Person(string name, DateTime dateOfBirth)
    {
        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }

    public static Person Create(
        string name,
        DateTime dateOfBirth)
    {
        if (string.IsNullOrWhitespace(Name))
        {
            throw new ArgumentException(nameof(name));
        }

        if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
        }

        return new Person(name, dateOfBirth);
    }
}

Асинхронный одноступенчатый через частный конструктор

Помимо вышеупомянутых преимуществ проверки и предотвращения распределения кучи, предыдущая методология дает нам еще одно изящное преимущество: поддержку асинхронности. Это удобно при работе с многоэтапной аутентификацией, например, когда вам нужно получить токен-носитель перед использованием вашего API. Таким образом, вы не получите недействительный «вышедший из системы» клиент API, а вместо этого можете просто воссоздать клиент API, если вы получаете ошибку авторизации при попытке выполнить запрос.

public class RestApiClient
{
    public RestApiClient(HttpClient httpClient)
    {
        this.httpClient = new httpClient;
    }

    public async Task<RestApiClient> Create(string username, string password)
    {
        if (username == null)
        {
            throw new ArgumentNullException(nameof(username));
        }

        if (password == null)
        {
            throw new ArgumentNullException(nameof(password));
        }

        var basicAuthBytes = Encoding.ASCII.GetBytes($"{username}:{password}");
        var basicAuthValue = Convert.ToBase64String(basicAuthBytes);

        var authenticationHttpClient = new HttpClient
        {
            BaseUri = new Uri("https://auth.example.io"),
            DefaultRequestHeaders = {
                Authentication = new AuthenticationHeaderValue("Basic", basicAuthValue)
            }
        };

        using (authenticationHttpClient)
        {
            var response = await httpClient.GetAsync("login");
            var content = response.Content.ReadAsStringAsync();
            var authToken = content;
            var restApiHttpClient = new HttpClient
            {
                BaseUri = new Uri("https://api.example.io"), // notice this differs from the auth uri
                DefaultRequestHeaders = {
                    Authentication = new AuthenticationHeaderValue("Bearer", authToken)
                }
            };

            return new RestApiClient(restApiHttpClient);
        }
    }
}

По моему опыту, недостатков у этого метода немного.

Как правило, использование этой методологии означает, что вы больше не можете использовать класс в качестве DTO, потому что десериализация объекта без общедоступного конструктора по умолчанию в лучшем случае сложна. Однако, если вы использовали объект в качестве DTO, вам не следует проверять сам объект, а скорее аннулировать значения объекта, когда вы пытаетесь их использовать, поскольку технически значения не являются «недействительными» в отношении в DTO.

Это также означает, что вы в конечном итоге создадите фабричные методы или классы, когда вам нужно разрешить контейнеру IOC создавать объект, поскольку в противном случае контейнер не будет знать, как создать экземпляр объекта. Однако во многих случаях фабричные методы сами становятся одним из методов Create.

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

Этот совет заменяет нечеткое субъективное решение (является ли это отличная идея) техническим, точным вопросом, основанным на проектных решениях (инвариантных и пост-условиях), которые вы уже должны были принять.

Конструкторы - это всего лишь частный, но не особый случай этого совета. Возникает вопрос: какие инварианты должны быть у класса? Сторонники отдельного метода инициализации, который должен вызываться после построения, предполагают, что класс имеет два или более рабочий режим, с режимом не готов после построения и по крайней мере одним режимом готовы, введенным после инициализации. Это дополнительная сложность, но она допустима, если в классе и так есть несколько режимов работы. Трудно понять, насколько оправдано это усложнение, если в противном случае у класса не было бы рабочих режимов.

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

Также обратите внимание, что избежать возможности создания исключений вашим конструктором проблематично, и во многих случаях невозможно во многих стандартных библиотеках. Это связано с тем, что разработчики этих библиотек считают, что выбрасывать исключения из конструкторов - это хорошая идея. В частности, любая операция, которая пытается получить не разделяемый или конечный ресурс (например, выделение памяти), может завершиться ошибкой, и этот сбой обычно указывается в объектно-ориентированных языках и библиотеках с помощью исключения.

Я не уверен, что любой ответ может быть полностью независимым от языка. Некоторые языки по-разному обрабатывают исключения и управление памятью.

Раньше я работал в соответствии со стандартами кодирования, требующими, чтобы исключения никогда не использовались, а только коды ошибок в инициализаторах, потому что разработчики были обижены языком, плохо обрабатывающим исключения. Языки без сборки мусора будут обрабатывать кучу и стек по-разному, что может иметь значение для объектов, отличных от RAII. Однако важно, чтобы команда решила быть последовательной, чтобы по умолчанию знать, нужно ли вызывать инициализаторы после конструкторов. Все методы (включая конструкторы) также должны быть хорошо документированы в отношении того, какие исключения они могут генерировать, чтобы вызывающие абоненты знали, как их обрабатывать.

Я вообще сторонник одноэтапной конструкции, так как легко забыть инициализировать объект, но есть много исключений из этого.

  • У вас не очень хорошая языковая поддержка исключений.
  • У вас есть веская причина по дизайну, чтобы по-прежнему использовать new и delete.
  • Инициализация требует интенсивного использования процессора и должна выполняться асинхронно с потоком, создавшим объект.
  • Вы создаете DLL, которая может генерировать исключения за пределами своего интерфейса для приложения, использующего другой язык. В этом случае проблема может заключаться не столько в том, чтобы не генерировать исключения, сколько в том, чтобы убедиться, что они перехватываются перед общедоступным интерфейсом. (Вы можете поймать исключения C++ в C#, но есть обручи, через которые нужно прыгнуть.)
  • Статические конструкторы (C#)

Вопрос OP имеет тег «языковой независимости» ... на этот вопрос нельзя безопасно ответить одинаково для всех языков / ситуаций.

В следующем примере C# иерархия классов вызывает конструктор класса B, пропуская немедленный вызов IDisposeable.Dispose класса A при выходе из using основного, пропуская явное удаление ресурсов класса A.

Если, например, класс A создал Socket при создании, подключенный к сетевому ресурсу, это, вероятно, все еще будет иметь место после блока using (относительно скрытая аномалия).

class A : IDisposable
{
    public A()
    {
        Console.WriteLine("Initialize A's resources.");
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose A's resources.");
    }
}

class B : A, IDisposable
{
    public B()
    {
        Console.WriteLine("Initialize B's resources.");
        throw new Exception("B construction failure: B can cleanup anything before throwing so this is not a worry.");
    }

    public new void Dispose()
    {
        Console.WriteLine("Dispose B's resources.");
        base.Dispose();
    }
}
class C : B, IDisposable
{
    public C()
    {
        Console.WriteLine("Initialize C's resources. Not called because B throws during construction. C's resources not a worry.");
    }

    public new void Dispose()
    {
        Console.WriteLine("Dispose C's resources.");
        base.Dispose();
    }
}


class Program
{
    static void Main(string[] args)
    {
        try
        {
            using (C c = new C())
            {
            }
        }
        catch
        {           
        }

        // Resource's allocated by c's "A" not explicitly disposed.
    }
}

Другие вопросы по теме