C exception в конструкторе

Обновлено: 28.04.2024

I'm having a debate with a co-worker about throwing exceptions from constructors, and thought I would like some feedback.

Is it OK to throw exceptions from constructors, from a design point of view?

Lets say I'm wrapping a POSIX mutex in a class, it would look something like this:

My question is, is this the standard way to do it? Because if the pthread mutex_init call fails the mutex object is unusable so throwing an exception ensures that the mutex won't be created.

Should I rather create a member function init for the Mutex class and call pthread mutex_init within which would return a bool based on pthread mutex_init 's return? This way I don't have to use exceptions for such a low level object.

Well it is ok to throw from ctors as much as it is from any other function, that being said you should throw with care from any function.

Something unrelated: why not removing your lock/unlock methods, and directly lock the mutex in the constructor and unlock in the destructor? That way simply declaring an auto variable in a scope automatically lock/unlock, no need to take care of exceptions, returns, etc. See std::lock_guard for a similar implementation.

If your construction fails and throws an exception, ~Mutex() will not be called and mutex_ will not be cleaned up. Don't throw exceptions in constructors.

11 Answers 11

Yes, throwing an exception from the failed constructor is the standard way of doing this. Read this FAQ about Handling a constructor that fails for more information. Having a init() method will also work, but everybody who creates the object of mutex has to remember that init() has to be called. I feel it goes against the RAII principle.

In most situations. Don;t forget things like std::fstream. On failure it still creates an object, but because we are always testing the state of the object normally it works well. So an object that has a natural state that is tested under normal usage may not need to throw.

Not really, in this specific case, note that his Mutex destructor will never be called, possibly leaking the pthread mutex. The solution to that is to use a smart pointer for the pthread mutex, better yet use boost mutexes or std::mutex, no reason to keep using old functional-style OS constructs when there are better alternatives.

@Martin York: I'm not sure std::fstream is a good example. Yes. It does rely on post-constructor error checking. But should it? It's an awful design that dates from a version of C++ where constructors were forbidden to throw exceptions.

If you do throw an exception from a constructor, keep in mind that you need to use the function try/catch syntax if you need to catch that exception in a constructor initializer list.

It should be noted that exceptions raised from the construction of a sub object can't be suppressed: gotw.ca/gotw/066.htm

Throwing an exception is the best way of dealing with constructor failure. You should particularly avoid half-constructing an object and then relying on users of your class to detect construction failure by testing flag variables of some sort.

On a related point, the fact that you have several different exception types for dealing with mutex errors worries me slightly. Inheritance is a great tool, but it can be over-used. In this case I would probably prefer a single MutexError exception, possibly containing an informative error message.

I'd second Neil's point about the exception heirarchy - a single MutexError is likely to be a better choice unless you specifically want to handle a lock error differently. If you have too many exception types, catching them all can become tiresome and error prone.

I agree that one type of mutex exception is enough. And this will also make error handling more intuitive.

the destructors are not called, so if a exception need to be thrown in a constructor, a lot of stuff(e.g. clean up?) to do.

You should be using a std::unique_ptr or similar. Destructor of members is called if an exception is thrown during construction, but plain pointers don't have any. Replace bar* b with std::unique_ptr b (you'll have to remove the delete b; and add the header), and run again.

This behavior is quite sensible. If the constructor has failed (was no successfully completed) why should the destructor be called? It has nothing to clean up and if did try to clean up objects which have not even been instantiated properly (think some pointers), it will cause a lot more problems, unnecessarily.

@zar Yes, the problem is not whether the destructor should be called or not. In this example, clean up should be done before throwing the exception. And I don't mean we cannot throw an exception in the constructor, I just mean the developer should known what he is dong. No good, no bad, but think before doing.

According to @Naveen's answer, it seems that the memory does freed. But valgrind --leak-check=full ./a.out complains block lost: ERROR SUMMARY: 2 errors from 2 contexts

It is OK to throw from your constructor, but you should make sure that your object is constructed after main has started and before it finishes:

The only time you would NOT throw exceptions from constructors is if your project has a rule against using exceptions (for instance, Google doesn't like exceptions). In that case, you wouldn't want to use exceptions in your constructor any more than anywhere else, and you'd have to have an init method of some sort instead.

Interesting discussion. My personal opinion is that you should use exceptions only when you actually design the program's error handling structure to take advantage of them. If you try to do error handling after writing the code, or try to shoehorn exceptions into programs that were not written for them, it's just going to lead to either try/catch EVERYWHERE (eliminating the advantages of exceptions) or to programs crashing out at the least little error. I deal with both every day and I don't like it.

Adding to all the answers here, I thought to mention, a very specific reason/scenario where you might want to prefer to throw the exception from the class's Init method and not from the Ctor (which off course is the preferred and more common approach).

I will mention in advance that this example (scenario) assumes that you don't use "smart pointers" (i.e.- std::unique_ptr ) for your class' s pointer(s) data members.

So to the point: In case, you wish that the Dtor of your class will "take action" when you invoke it after (for this case) you catch the exception that your Init() method threw - you MUST not throw the exception from the Ctor, cause a Dtor invocation for Ctor's are NOT invoked on "half-baked" objects.

See the below example to demonstrate my point:

I will mention again, that it is not the recommended approach, just wanted to share an additional point of view.

Also, as you might have seen from some of the print in the code - it is based on item 10 in the fantastic "More effective C++" by Scott Meyers (1st edition).

If your project generally relies on exceptions to distinguish bad data from good data, then throwing an exception from the constructor is better solution than not throwing. If exception is not thrown, then object is initialized in a zombie state. Such object needs to expose a flag which says whether the object is correct or not. Something like this:

Problem with this approach is on the caller side. Every user of the class would have to do an if before actually using the object. This is a call for bugs - there's nothing simpler than forgetting to test a condition before continuing.

In case of throwing an exception from the constructor, entity which constructs the object is supposed to take care of problems immediately. Object consumers down the stream are free to assume that object is 100% operational from the mere fact that they obtained it.

This discussion can continue in many directions.

For example, using exceptions as a matter of validation is a bad practice. One way to do it is a Try pattern in conjunction with factory class. If you're already using factories, then write two methods:

With this solution you can obtain the status flag in-place, as a return value of the factory method, without ever entering the constructor with bad data.

Second thing is if you are covering the code with automated tests. In that case every piece of code which uses object which does not throw exceptions would have to be covered with one additional test - whether it acts correctly when IsValid() method returns false. This explains quite well that initializing objects in zombie state is a bad idea.

Apart from the fact that you do not need to throw from the constructor in your specific case because pthread_mutex_lock actually returns an EINVAL if your mutex has not been initialized and you can throw after the call to lock as is done in std::mutex :

then in general throwing from constructors is ok for acquisition errors during construction, and in compliance with RAII ( Resource-acquisition-is-Initialization ) programming paradigm.

Focus on these statements:

  1. static std::mutex mutex
  2. std::lock_guard lock(mutex);
  3. std::ofstream file("example.txt");

The first statement is RAII and noexcept . In (2) it is clear that RAII is applied on lock_guard and it actually can throw , whereas in (3) ofstream seems not to be RAII , since the objects state has to be checked by calling is_open() that checks the failbit flag.

At first glance it seems that it is undecided on what it the standard way and in the first case std::mutex does not throw in initialization , *in contrast to OP implementation * . In the second case it will throw whatever is thrown from std::mutex::lock , and in the third there is no throw at all.

Notice the differences:

(1) Can be declared static, and will actually be declared as a member variable (2) Will never actually be expected to be declared as a member variable (3) Is expected to be declared as a member variable, and the underlying resource may not always be available.

All these forms are RAII; to resolve this, one must analyse RAII.

  • Resource : your object
  • Acquisition ( allocation ) : you object being created
  • Initialization : your object is in its invariant state

This does not require you to initialize and connect everything on construction. For example when you would create a network client object you would not actually connect it to the server upon creation, since it is a slow operation with failures. You would instead write a connect function to do just that. On the other hand you could create the buffers or just set its state.

Therefore, your issue boils down to defining your initial state. If in your case your initial state is mutex must be initialized then you should throw from the constructor. In contrast it is just fine not to initialize then ( as is done in std::mutex ), and define your invariant state as mutex is created . At any rate the invariant is not compromized necessarily by the state of its member object, since the mutex_ object mutates between locked and unlocked through the Mutex public methods Mutex::lock() and Mutex::unlock() .

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

Инициализирует новый экземпляр класса Exception.

Перегрузки

Инициализирует новый экземпляр класса Exception.

Инициализирует новый экземпляр класса Exception с сериализованными данными.

Exception()

Инициализирует новый экземпляр класса Exception.

Примеры

Комментарии

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

Свойство Значение
InnerException Пустая ссылка ( Nothing в Visual Basic).
Message Локализованное описание, предоставляемое системой.

Применяется к

Exception(String)

Параметры

Примеры

Комментарии

Этот конструктор инициализирует Message свойство нового экземпляра message с помощью параметра. message Если параметр имеет значение null , это то же самое, что и вызов конструктораException.

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

Применяется к

Exception(SerializationInfo, StreamingContext)

Инициализирует новый экземпляр класса Exception с сериализованными данными.

Параметры

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

Объект StreamingContext, содержащий контекстные сведения об источнике или назначении.

Исключения

info имеет значение null .

Имя класса — null , или HResult равно нулю (0).

Примеры

В следующем примере кода определяется производный сериализуемый Exception класс. Код вызывает ошибку деления на 0, а затем создает экземпляр производного исключения с помощью конструктора (SerializationInfo, StreamingContext). Код сериализует экземпляр в файл, десериализует файл в новое исключение, которое он создает, а затем перехватывает и отображает данные исключения.

Комментарии

Этот конструктор вызывается во время десериализации для восстановления объекта исключения, переданного в потоке. Дополнительные сведения см. в статье о сериализации XML и SOAP.

См. также раздел

Применяется к

Exception(String, Exception)

Параметры

Исключение, вызвавшее текущее исключение, или пустая ссылка ( Nothing в Visual Basic), если внутреннее исключение не задано.

Примеры

Комментарии

Исключение, созданное как прямой результат предыдущего исключения, должно содержать в свойстве InnerException ссылку на предыдущее исключение. Свойство InnerException возвращает то же значение, которое передается конструктору, или пустую ссылку ( Nothing в Visual Basic), если свойство InnerException не предоставляет конструктору значение внутреннего исключения.

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

Говорят, что конструктор конструктор и деструктор класса не должны вырабатывать исключения?
В тоже время, я видел огромное количество кода и даже примеров из книг, где в конструкторе выделяется память при помощи оператора `new`, но ведь даже он может вырабатывать исключение `bad_alloc` — выходит, что весь этот код потенциально опасный? Как с этим бороться?

Исключения, ровно как и оператор return прерывают поток выполнения команд функции, из системного стека выбираются объекты (такие как локальные переменные) и для них вызываются деструкторы. Однако, если при выполнении оператора return раскрутка стека прекратиться в точке где была вызвана завершенная функция, то при при выполнении throw объекты из стека будут уничтожаться до тех пор, пока управление не будет передано в блок try<> , содержащий обработчик, соответствующий типу выброшенного исключения. Читать подробнее про обработку исключений [1].

Исключения в деструкторе класса

В связи с этим, в большинстве случаев разрушение объектов созданных на стеке (без использования new/malloc ) произойдет корректно — вызовом деструктора. Однако исключения в конструкторе или деструкторе могут приводить к нежелательным последствиям.

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

destructor_exception_abort

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

Еще один из аспектов работы деструкторов и исключений иллюстрирует следующий фрагмент кода:

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

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

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

Исключения в конструкторе класса

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

Стандарт языка С++ гарантирует, что если исключение возникнет в конструкторе, то памяти из под членов-данных класса будет освобождена корректно вызовом деструктора — т.е. если вы используете идиому RAII [2], то проблем не будет. Часто для этого достаточно использовать std::vector/std::string вместо старых массивов и строк, и умные указатели вместо обычных [3]. Если же вы продолжите использовать сырые указатели и динамически выделять память — нужно будет очень тщательно следить за ней, например в следующем фрагменте кода нет утечки, т.к. исключение будет выработано только если память не будет выделена [4]:

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

Представляет ошибки, которые происходят во время выполнения приложения.

Примеры

Комментарии

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

Ошибки и исключения

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

Ошибки использования. Ошибка использования представляет собой ошибку в логике программы, которая может привести к исключению. Однако ошибка должна быть устранена не с помощью обработки исключений, а путем изменения кода сбоя. Например, переопределение Object.Equals(Object) метода в следующем примере предполагает, что obj аргумент всегда должен быть ненулевым.

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

Вместо обработки исключений для ошибок использования можно использовать Debug.Assert метод для выявления ошибок использования в отладочных сборках, а Trace.Assert также метода для выявления ошибок использования как в отладочных, так и в сборках выпуска. Дополнительные сведения см. в разделе Утверждения в управляемом коде.

Ошибки программы. Ошибка программы — это ошибка во время выполнения, которую не обязательно можно избежать путем написания кода без ошибок.

В некоторых случаях ошибка программы может отражать ожидаемое или обычное состояние ошибки. В этом случае может потребоваться избежать использования обработки исключений для устранения ошибки программы и вместо этого повторить операцию. Например, если пользователь должен ввести дату в определенном формате, можно проанализировать строку даты, вызвав DateTime.TryParseExact метод, который возвращает Boolean значение, указывающее, успешно ли выполнена операция синтаксического анализа, а не с помощью DateTime.ParseExact метода, что вызывает FormatException исключение, если строка даты не может быть преобразована в DateTime значение. Аналогичным образом, если пользователь пытается открыть файл, который не существует, можно сначала вызвать File.Exists метод, чтобы проверить, существует ли файл, и, если он этого не делает, предложите пользователю создать его.

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

Сбои системы. Сбой системы — это ошибка во время выполнения, которая не может обрабатываться программным способом. Например, любой OutOfMemoryException метод может вызвать исключение, если среде CLR не удается выделить дополнительную память. Обычно системные сбои не обрабатываются с помощью обработки исключений. Вместо этого вы можете использовать такое событие, как AppDomain.UnhandledException и вызвать Environment.FailFast метод для регистрации сведений об исключении и уведомить пользователя о сбое до завершения работы приложения.

Блоки try/catch

Среда CLR предоставляет модель обработки исключений, основанную на представлении исключений в виде объектов, а также разделение кода программы и кода обработки исключений на try блоки и catch блоки. Может быть один или несколько catch блоков, каждый из которых предназначен для обработки определенного типа исключения, или один блок, предназначенный для перехвата более конкретного исключения, чем другой блок.

Если приложение обрабатывает исключения, возникающие во время выполнения блока кода приложения, код должен быть помещен в try инструкцию и называется блоком try . Код приложения, обрабатывающий исключения, создаваемые блоком try , помещается в catch оператор и называется блоком catch . С блоком try связаны ноль или более catch блоков, каждый из которых catch включает фильтр типов, определяющий типы исключений, которые он обрабатывает.

При возникновении исключения в try блоке система выполняет поиск связанных catch блоков в том порядке, в который они отображаются в коде приложения, пока не будет обнаружен catch блок, обрабатывающий исключение. Блок catch обрабатывает исключение типа T , если фильтр типов блока catch указывает T или какой-либо тип, T производный от. Система перестает выполнять поиск после обнаружения первого catch блока, обрабатывающего исключение. По этой причине в коде приложения необходимо указать блок, обрабатывающий тип, catch перед блоком catch , обрабатывающим его базовые типы, как показано в примере ниже. Блок catch, обрабатывающий дескриптор System.Exception , задается последним.

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

Функции типа исключений

Типы исключений поддерживают следующие функции:

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

Свойства класса exception

Класс Exception содержит ряд свойств, которые помогают определить расположение кода, тип, файл справки и причину исключения: StackTrace, InnerExceptionMessage, HelpLinkSourceHResult, и . TargetSiteData

Если между двумя или более исключениями существует причинная связь, InnerException свойство сохраняет эти сведения. Внешнее исключение создается в ответ на это внутреннее исключение. Код, обрабатывающий внешнее исключение, может использовать сведения из предыдущего внутреннего исключения для более правильной обработки ошибки. Дополнительные сведения об исключении могут храниться в виде коллекции пар "ключ-значение" в свойстве Data .

Чтобы предоставить пользователю подробные сведения о причинах возникновения исключения, HelpLink свойство может содержать URL-адрес (или URN) в файле справки.

Класс Exception использует COR_E_EXCEPTION HRESULT, имеющий значение 0x80131500.

Список начальных значений свойств для экземпляра Exception класса см. в Exception конструкторах.

Вопросы производительности

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

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

Повторный вызов исключения

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

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

Затем вызывающий объект вызывается FindOccurrences дважды. Во втором вызове FindOccurrences вызывающий объект передает строку null поиска, которая вызывает String.IndexOf(String, Int32) исключение ArgumentNullException . Это исключение обрабатывается методом FindOccurrences и передается обратно вызывающей стороне. Так как оператор throw используется без выражения, выходные данные из примера показывают, что стек вызовов сохраняется.

В отличие от этого, если исключение создается повторно с помощью

оператор , полный стек вызовов не сохраняется, и пример создаст следующие выходные данные:

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

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

Выбор стандартных исключений

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

Исключение Условие
ArgumentException Недопустимый аргумент, который передается в метод.
ArgumentNullException Аргумент, передаваемый методу null .
ArgumentOutOfRangeException Аргумент находится за пределами диапазона допустимых значений.
DirectoryNotFoundException Недопустимая часть пути к каталогу.
DivideByZeroException Знаменатель в целочисленной или Decimal делении операции равен нулю.
DriveNotFoundException Диск недоступен или не существует.
FileNotFoundException Файл не существует.
FormatException Значение не имеет соответствующего формата для преобразования из строки с помощью такого метода Parse преобразования.
IndexOutOfRangeException Индекс находится за пределами массива или коллекции.
InvalidOperationException Вызов метода недопустим в текущем состоянии объекта.
KeyNotFoundException Не удается найти указанный ключ для доступа к элементу в коллекции.
NotImplementedException Метод или операция не реализованы.
NotSupportedException Метод или операция не поддерживаются.
ObjectDisposedException Операция выполняется для объекта, который был удален.
OverflowException Операция арифметического приведения, приведения или преобразования приводит к переполнению.
PathTooLongException Путь или имя файла превышает максимальную системную длину.
PlatformNotSupportedException Операция не поддерживается на текущей платформе.
RankException Массив с неправильным числом измерений передается в метод.
TimeoutException Истек интервал времени, выделенный для операции.
UriFormatException Используется недопустимый универсальный код ресурса (URI).

Реализация пользовательских исключений

Чтобы определить собственный класс исключений, выполните следующие действия.

Определение класса, наследуемого от Exception. При необходимости определите все уникальные члены, необходимые классу для предоставления дополнительных сведений об исключении. Например, ArgumentException класс содержит ParamName свойство, указывающее имя параметра, аргумент которого вызвал исключение, а RegexMatchTimeoutException свойство содержит MatchTimeout свойство, указывающее интервал времени ожидания.

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

Определите, является ли настраиваемый объект исключения сериализуемым. Сериализация позволяет сохранять сведения об исключении и разрешать доступ к сведениям об исключениях сервером и прокси-сервером клиента в контексте удаленного взаимодействия. Чтобы сделать объект исключения сериализуемым, пометьте его атрибутом SerializableAttribute .

Определите конструкторы класса исключений. Как правило, классы исключений имеют один или несколько из следующих конструкторов:

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

Exception(SerializationInfo, StreamingContext)— конструктор protected , который инициализирует новый объект исключения из сериализованных данных. Этот конструктор следует реализовать, если вы решили сделать объект исключения сериализуемым.

В следующем примере показано использование пользовательского класса исключений. Он определяет исключение, которое возникает NotPrimeException при попытке клиента получить последовательность простых чисел, указав начальное число, которое не является простым. Исключение определяет новое свойство, NonPrime которое возвращает неисчислимое число, вызвавшее исключение. Помимо реализации защищенного конструктора без параметров и конструктора с SerializationInfo параметрами StreamingContext для сериализации NotPrimeException класс определяет три дополнительных конструктора для поддержки NonPrime свойства. Каждый конструктор вызывает конструктор базового класса в дополнение к сохранению значения незначимого числа. Класс NotPrimeException также помечается атрибутом SerializableAttribute .

Класс, PrimeNumberGenerator показанный в следующем примере, использует Sieve of Eratosthenes для вычисления последовательности простых чисел от 2 до предела, указанного клиентом в вызове конструктора класса. Метод GetPrimesFrom возвращает все простые числа, которые больше или равны заданному нижнему пределу, но вызывает NotPrimeException исключение, если это нижнее ограничение не является простым числом.

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

Конструкторы

Инициализирует новый экземпляр класса Exception.

Инициализирует новый экземпляр класса Exception с сериализованными данными.

Свойства

Возвращает коллекцию пар «ключ-значение», предоставляющую дополнительные сведения об исключении.

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

Возвращает или задает HRESULT — кодированное числовое значение, присвоенное определенному исключению.

Возвращает экземпляр класса Exception, который вызвал текущее исключение.

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

Получает строковое представление непосредственных кадров в стеке вызова.

Возвращает метод, создавший текущее исключение.

Методы

Определяет, равен ли указанный объект текущему объекту.

При переопределении в производном классе возвращает исключение Exception, которое является первопричиной одного или нескольких последующих исключений.

Служит хэш-функцией по умолчанию.

При переопределении в производном классе задает объект SerializationInfo со сведениями об исключении.

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

Возвращает объект Type для текущего экземпляра.

Создает неполную копию текущего объекта Object.

Создает и возвращает строковое представление текущего исключения.

События

Возникает, когда исключение сериализовано для создания объекта состояния исключения, содержащего сериализованные данные об исключении.

Can you use a throw or try-catch within a constructor?
If so, what is the purpose of having a constructor that has an argument that can throw an exception?

This constructor is an example:

The line chatRoom = chatClient.JoinRoom(Configuration.RoomUrl); can throw exceptions.

It's generally a code smell if a constructor does any work. Like "joining rooms" etc. You should move this to a seperate function

From memory exceptions thrown in a constructor are bad bad bad. They can leave the AppDomain in an undefined state. I'll see if I can find a reference.

3 Answers 3

Throwing an exception when appropriate is part of a constructor's job.

Let's consider why we have constructors at all.

One part is convenience in having a method that sets various properties, and perhaps does some more advanced initialisation work (e.g. a FileStream will actually access the relevant file). But then if it was really that convenient all the time we wouldn't sometimes find member initialisers convenient.

The main reason for constructors is so that we can maintain object invariants.

An object invariant is something we can say about an object at the beginning and end of every method call. (If it was designed for concurrent use we would even have invariants that held during method calls).

One of the invariants of the Uri class is that if IsAbsoluteUri is true, then Host will be string that is a valid host, (if IsAbsoluteUri is false Host might be a valid host if it's scheme-relative, or accessing it might cause an InvalidOperationException ).

As such when I'm using an object of such a class and I've checked IsAbsoluteUri I know I can access Host without an exception. I also know that it will indeed be a hostname, rather than e.g. a short treatise on mediaeval and early-modern beliefs in the apotropaic qualities of bezoars.

Okay, so some code putting such a treatise in there isn't exactly likely, but code putting some sort of junk into an object certainly is.

Maintaining an invariant comes down to making sure that the combinations of values the object holds always make sense. That has to be done in any property-setter or method that mutates the object (or by making the object immutable, because you can never have an invalid change if you never have a change) and those that set values initially, which is to say in a constructor.

In a strongly typed language we get some of the checking from that type-safety (a number that must be between 0 and 15 will never be set to "Modern analysis has found that bezoars do indeed neutralise arsenic." because the compiler just won't let you do that.) but what about the rest?

So if you call new List(-2) or new List(null) you get an exception, rather than an invalid list.

An interesting thing to note about this case, is that this is a case where they could possibly have "fixed" things for the caller. In this case it would have been safe to consider negative numbers as the same as 0 and null enumerables as the same as an empty one. They decided to throw anyway. Why?

Well, we have three cases to consider when writing constructors (and indeed other methods).

  1. Caller gives us values we can't meaningfully use at all. (e.g. setting a value from an enum to an undefined value).

Definitely throw an exception.

  1. Caller gives us values we can massage into useful values. (e.g. limiting a number of results to a negative number, which we could consider as the same as zero).

This is the trickier case. We need to consider:

Is the meaning unambiguous? If there's more than one way to consider what it "really" means, then throw an exception.

Is the caller likely to have arrived at this result in a reasonable manner? If the value is just plain stupid, then the caller presumably made a mistake in passing it to the constructor (or method) and you aren't doing them any favours in hiding their mistakes. For one thing, they're quite likely making other mistakes in other calls but this is the case where it becomes obvious.

If in doubt, throw an exception. For one thing if you're in doubt about what you should do then it's likely the caller would be in doubt about what they should expect you to do. For another it's much better to come back later and turn code that throws into code that doesn't than turn code that doesn't throw into code that does, because the latter will be more likely to turn working uses into broken applications.

So far I've only looked at code that can be considered validation; we were asked to do something silly and we refused. Another case is when we were asked to do something reasonable (or silly, but we couldn't detect that) and we weren't able to do it. Consider:

There's nothing invalid in this call that should definitely fail. All validation checks should pass. It will hopefully open the file at D:\logFile.log in read mode and give us a FileStream object through which we can access it.

But what if there's no D:\logFile.log ? Or no D:\ (amounts to the same thing, but the internal code might fail in a different way) or we don't have permission to open it. Or it's locked by another process?

In all of these cases we're failing to do what is asked. It's no good our returning an object that represents attempts to read a file that are all going to fail! So again, we throw an exception here.

Okay. Now consider the case of StreamReader() that takes a path. It works a bit like (adjusting to cut out some indirection for the sake of example):

Here we've got both cases where a throw might happen. First we've got validation against bogus arguments. After that we've a call to the FileStream constructor that in turn might throw an exception.

In this case the exception is just allowed to pass through.

Now the cases we need to consider are a tiny bit more complicated.

With most of the validation cases considered at the beginning of this answer, it didn't really matter in what order we did things. With a method or property we have to make sure that we've either changed things to be in a valid state or thrown an exception and left things alone, otherwise we can still end up with the object in an invalid state even if the exception was thrown (for the most part it suffices to do all of your validation before you change anything). With constructors it doesn't much matter what order things are done in, since we're not going to return an object in that case, so if we throw at all, we haven't put any junk into the application.

With the call to new FileStream() above though, there could be side-effects. It's important that it is only attempted after any other case that would throw an exception is done.

For the most part again, this is easy to do in practice. It's natural to put all of your validation checks first, and that's all you need to do 99% of the time. An important case though is if you are obtaining an unmanaged resource in the course of a constructor. If you throw an exception in such a constructor then it will mean the object wasn't constructed. As such it won't be finalised or disposed, and as such the unmanaged resource won't be released.

A few guidelines on avoiding this:

Don't use unmanaged resources directly in the first place. If at all possible, work through managed classes that wrap them, so it's that object's problem.

If you do have to use an unmanaged resource, don't do anything else.

If you need to have a class with both an unmanaged resource and other state, then combine the two guidelines above; create a wrapper class that only deals with the unmanaged resource and use that within your class.

  1. Better yet, use SafeHandle to hold the pointer to the unmanaged resource if at all possible. This deals with a lot of the work for point 2 very well.

Now. What about catching an exception?

We can certainly do so. The question is, what do we do when we've caught something? Remember that we have to either create an object that matches what we were asked for, or throw an exception. Most of the time if one of the things we've attempted in the course of that fails, there isn't anything we can do to successfully construct the object. We're likely therefore to just let the exception pass through, or to catch an exception just to throw a different one more suitable from the perspective of someone calling the constructor.

But certainly if we can meaningfully continue on after a catch then that's allowed.

So in all, the answer to "Can you use a throw or try and catch within a constructor?" is, "Yes".

There is one fly in the ointment. As seen above the great thing about throwing within a constructor is that any new either gets a valid object or else an exception is thrown; there's no in between, you either have that object or you don't.

A static constructor though, is a constructor for the class as a whole. If an instance constructor fails, you don't get an object but if a static constructor fails you don't get a class!

You're pretty much doomed in any future attempt to make use of that class, or any derived from it, for the rest of the life of the application (strictly speaking, for the rest of the life of the app domain). For the most part this means throwing an exception in a static class is a very bad idea. If it's possible that something attempted and failed might be attempted and succeed another time, then it shouldn't be done in a static constructor.

About the only time when you want to throw in a static constructor is when you want the application to totally fail. For example, it's useful to throw in a web application that lacks a vital configuration setting; sure, it's annoying to have every single request fail with the same error message, but that means you're sure to fix that problem!

Читайте также: