- [9.1. Порождение и обработка исключительных ситуаций](#9-1-порождение-и-обработка-исключительных-ситуаций)
- [9.2. Типы](#9-2-типы)
- [9.3. Блоки finally](#9-3-блоки-finally)
- [9.4. Функции, не порождающие исключения (nothrow), и особая природа класса Throwable](#9-4-функции-не-порождающие-исключения-nothrow-и-особая-природа-класса-throwable)
Обработка ошибок – это слабо формализованная область программного инжиниринга, связанная с обработкой ожидаемых и возникших ошибочных ситуаций, способных помешать нормальному функционированию системы. Обработка исключительных ситуаций (исключений) – подход к обработке ошибок, принятый во многих современных языках (включая D) и уже успевший породить великое множество руководств, методик и, конечно, споров.
Исключения – это средство языка, реализующее обработку ошибок благодаря специальным обходным путям передачи управления. Если функции не удается вернуть вызвавшему ее коду осмысленный результат, она может породить объект исключения, в котором закодирована причина ошибки. Порождение исключений (throwing) – это карточка «Бесплатно освободитесь из тюрьмы»[^1], освобождающая функцию от ее обычных обязанностей. Исключение пропускает всех инициаторов вызовов, не предусматривающих его обработку, и попадает в *место обработки*, где и принимаются чрезвычайные меры. В хорошо спроектированной программе гораздо меньше мест обработки, чем мест порождения исключений, что способствует централизованной обработке ошибок с многократным использованием кода. Все это было бы проблематично организовать на основе традиционных методик с вездесущими кодами ошибки.
[В начало ⮍](#9-обработка-ошибок)
## 9.1. Порождение и обработка исключительных ситуаций
D использует популярную модель исключений. Функция может инициировать исключения с помощью инструкции `throw` (см. раздел 3.11), порождающей объект особого типа. Чтобы завладеть этим объектом, код должен использовать инструкцию `try` (см. раздел 3.11), где этот объект указан в блоке `catch`. Перефразируя пословицу, лучше один пример кода, чем 1024 слова. Так что рассмотрим пример:
```d
import std.stdio;
void main()
{
try
{
auto x = fun();
}
catch (Exception e)
{
writeln(e);
}
}
int fun()
{
return gun() * 2;
}
int gun()
{
throw new Exception("Вернемся прямо в main");
}
```
Вместо того чтобы вернуть значение типа `int`, функция `gun` решает породить исключение, в данном случае это экземпляр класса `Exception`. В порожденном объекте можно передать любую информацию о том, что произошло. Управление передается исключительным путем (что освобождает от возвращения результата не только функцию, породившую исключение, но и всех инициаторов ее вызова) тому из инициаторов вызова, который готов обработать ошибку с помощью блока `catch`.
После выполнения инструкции `throw` функция `fun` полностью пропускается, поскольку она не готова обработать исключение. Вот в чем принципиальная разница между обработкой ошибок старой школы, когда ошибки вручную проводились через все уровни вызовов, и относительно новым подходом – обработкой исключений, когда управление искусно передается из места возникновения ошибки (`gun`) непосредственно туда, где есть все необходимое для ее обработки (блок `catch` в функции `main`). Такой подход обещает более простую, централизованную обработку ошибок, освобождая множество функций от обязанности проталкивать ошибки дальше по стеку; `fun` может оставаться в блаженном неведении о прямом сообщении между `gun` и `main`.
К сожалению, непосредственная передача потока управления из места порождения в место обработки – это также и слабое звено обработки исключений: на самом деле, то блаженное неведение – лишь несбыточная мечта. В действительности, функции, пересекаемые исключением, должны помнить о дополнительных неявных точках выхода и гарантировать поддержку инвариантов программы независимо от того, каким путем будет передано управление. D предоставляет надежные механизмы, обеспечивающие сохранение инвариантности при возникновении исключений. В свое время мы обсудим их в этой главе.
[В начало ⮍](#9-1-порождение-и-обработка-исключительных-ситуаций) [Наверх ⮍](#9-обработка-ошибок)
## 9.2. Типы
Базовая иерархия исключений в D проста (рис. 9.1). Инструкция `throw` порождает не просто какие-то значения, а только объекты-потомки класса `Throwable`. В подавляющем большинстве случаев код действительно порождает исключение как экземпляр потомка класса `Exception`, подкласса `Throwable`. Это обычные исключения, после которых возможно восстановление, и они распознаются языком именно так. Исключения-наследники `Throwable`, но не `Exception` (такие как `AssertError`; см. главу 10) относятся к фатальным ошибкам, после которых восстановление невозможно, и должны использоваться в коде крайне редко, практически никогда. (О том, что язык гарантирует в случае фатальных ошибок, а что нет, см. подробнее в разделе 9.4.)
![image-9-2](images/image-9-2.png)
***Рис. 9.1.*** *Обычные исключения – потомки класса `Exception`, поэтому их можно обработать с помощью блока `catch(Exception)`. Класс `Error` – прямой наследник класса `Throwable`. Обычный код должен перехватывать только исключения типа `Exception` и потомков `Exception`. Остальные исключения лишь позволяют аккуратно завершить работу программы в случае нахождения ошибки в ее логике*
Инструкция `try` может определять больше одного блока `catch`, например:
```d
try
{
...
}
catch (SomeException e)
{
...
}
catch (SomeOtherException e)
{
...
}
```
Исключения распространяются от места порождения до самого раннего места обработки, следуя правилу *первого совпадения*: сразу же после обнаружения блока `catch`, обрабатывающего исключение порожденного класса или его предка, этот блок `catch` активируется и порожденное исключение передается в него. Вот пример, порождающий и обрабатывающий исключения двух различных типов:
```d
import std.stdio;
class MyException : Exception
{
this(string s) { super(s); }
}
void fun(int x)
{
if (x == 1)
{
throw new MyException("");
}
else
{
throw new StdioException("");
}
}
void main()
{
foreach (i; 1 .. 3)
{
try
{
fun(i);
}
catch (StdioException e)
{
writeln("StdioException");
}
catch (Exception e)
{
writeln("Exception");
}
}
}
```
Эта программа выводит на экран:
```
Exception
StdioException
```
Первый вызов `fun` порождает объект исключения типа `MyException`. При его сопоставлении с первым `catch`-обработчиком совпадения нет, но зато оно обнаруживается при сопоставлении со вторым блоком `catch`, поскольку `MyException` является потомком `Exception`. А в случае исключения, порожденного второй инструкцией `throw`, совпадение обнаруживается при сопоставлении с первым же `catch`-обработчиком. До первого совпадения этот процесс может затронуть и несколько уровней функций, как показывает следующий более замысловатый пример:
```d
import std.stdio;
class MyException : Exception
{
this(string s) { super(s); }
}
void fun(int x)
{
if (x == 1)
{
throw new MyException("");
}
else if (x == 2)
{
throw new StdioException("");
}
else
{
throw new Exception("");
}
}
void funDriver(int x)
{
try
{
fun(x);
}
catch (MyException e)
{
writeln("MyException");
}
}
unittest
{
foreach (i; 1 .. 4)
{
try
{
funDriver(i);
}
catch (StdioException e)
{
writeln("StdioException");
}
catch (Exception e)
{
writeln("Просто Exception");
}
}
}
```
Эта программа выводит на экран:
```
MyException
StdioException
Просто Exception
```
поскольку обработчики в соответствии с концепцией пробуются по мере того, как поток управления всплывает вверх по стеку вызовов.
У правила первого совпадения есть очевидный недостаток. Если блок `catch` для исключения типа `E1` расположен до блока `catch` для исключения типа `E2` и при этом `E2` является подтипом `E1`, то обработчик для `E2` оказывается недоступным. В такой ситуации компилятор диагностирует ошибку. Например:
```d
import std.stdio;
void fun()
{
try
{
...
}
catch (Exception e)
{
...
}
catch (StdioException e)
{
... // Ошибка!
// Недоступный обработчик catch!
}
}
```
Ошибочность подобного кода очевидна, но всегда есть угроза динамического маскирования между разными функциями. Любая функция легко может сделать недееспособными `catch`-обработчики вызвавшей ее функции. Тем не менее в большинстве случаев это не ошибка, а лишь нормальное следствие динамики стека вызовов.
[В начало ⮍](#9-2-типы) [Наверх ⮍](#9-обработка-ошибок)
## 9.3. Блоки finally
Инструкция `try` может завершаться блоком `finally`, что фактически означает: «Непременно выполните этот код, даже если наступит конец света или начнется потоп». Было или нет порождено исключение, блок `finally` будет выполнен просто как часть инструкции `try`, и вам решать, закончится ли это сбоем программы, порождением исключения, возвратом с помощью инструкции `return` или досрочным выходом из включающего цикла с помощью инструкции `break`. Например:
```d
import std.stdio;
string fun(int x)
{
string result;
try
{
if (x == 1)
{
throw new Exception("некоторое исключение");
}
result = "исключение не было порождено";
return result;
}
catch (Exception e)
{
if (x == 2) throw e;
result = "исключение было порождено и обработано: " ~ e.toString;
return result;
}
finally
{
writeln("Выход из fun");
}
}
```
При нормальном ходе выполнения функция `fun` вернет некоторое значение, при исключительном – породит исключение, но в любом случае она всегда напечатает в стандартный поток вывода: `Выход из fun`.
[В начало ⮍](#9-3-блоки-finally) [Наверх ⮍](#9-обработка-ошибок)
## 9.4. Функции, не порождающие исключения (nothrow), и особая природа класса Throwable
С помощью ключевого слова `nothrow` можно объявить функцию, не порождающую исключения:
```d
nothrow int iDontThrow(int a, int b)
{
return a / b;
}
```
Функции, не порождающие исключения, уже упоминались в разделе 5.11.2. А вот и новый поворот сюжета: атрибут `nothrow` обещает, что функция не породит объект типа `Exception`. Ноу функции по-прежнему остается право порождать объекты более грозного класса `Throwable`. Собственно, восстановление после исключений типа `Throwable` считается невозможным, поэтому компилятору разрешается не «продумывать» ход событий при возникновении такого исключения, соответственно он оптимизирует код исходя из предположения, что никакого исключения нет. Для функций с атрибутом `nothrow` компилятор упрощает последовательности входа и выхода, не планируя экстренные мероприятия на случай, если будет порождено исключение.
Проясним и подчеркнем особый статус класса `Throwable`. Первое правило для исключений `Throwable`: исключения `Throwable` не обрабатывают. Решив обработать такие исключения с помощью блока `catch`, вы не можете рассчитывать на то, что деструкторы структур будут вызваны, а блоки `finally`– выполнены. Это означает, что состояние вашей системы неопределенно и может быть нарушен целый ряд высокоуровневых инвариантов, на которые вы полагаетесь при нормальном исполнении. Тем не менее D гарантирует безопасность базовых типов и целостность стандартной библиотеки. Вы не можете рассчитывать на высокоуровневую целостность состояния своего приложения, поскольку не сработало неизвестное количество кода, обеспечивающего эту целостность. Так что при обработке `Throwable` вам доступны только несколько простых операций. Все, что вы сможете сделать в большинстве случаев, – это вывести сообщение об ошибке в стандартный поток или в файл журнала, попытаться сохранить в отдельном файле то, что еще можно сохранить, и, стиснув зубы, достойно завершить выполнение программы насколько это возможно.
[В начало ⮍](#9-4-функции-не-порождающие-исключения-nothrow-и-особая-природа-класса-throwable) [Наверх ⮍](#9-обработка-ошибок)
## 9.5. Вторичные исключения
Иногда во время обработки исключения порождается еще одно. Например:
```d
import std.conv;
class MyException : Exception
{
this(string s) { super(s); }
}
void fun()
{
try
{
throw new Exception("порождено в fun");
}
finally
{
gun(100);
}
}
void gun(int x)
{
try
{
throw new MyException(text("порождено в gun #", x));
}
finally
{
if (x > 1)
{
gun(x - 1);
}
}
}
```
Что происходит, когда вызвана функция `fun`? Ситуация на грани непредсказуемости. Во-первых, `fun` пытается породить исключение, но благодаря упомянутой привилегии блока `finally` всегда выполняться «даже если наступит конец света или начнется потоп», `gun(100)` вызывается тогда же, когда из `fun` вылетает `Exception`. В свою очередь, вызов `gun(100)` создает исключение типа `MyException`с сообщением `"порождено в gun #100"`. Назовем второе исключение *вторичным*, чтобы отличать его от порожденного первым, которое мы назовем *первичным*. Затем уже функция `gun`с помощью блока `finally` порождает добавочные вторичные исключения – ровно 100 исключений. Такой код испугал бы и самого Макиавелли.
Ввиду необходимости обрабатывать вторичные исключения язык может выбрать один из следующих вариантов поведения:
- немедленно прервать выполнение;
- продолжить распространять первичное исключение, игнорируя все вторичные;
- заменить первичное исключение вторичным и продолжить распространять его;
- продолжить в той или иной форме распространять и первичное, и все вторичные исключения.
С точки зрения сохранения информации о происходящем последний подход видится наиболее обоснованным, но и самым сложным в реализации. Например, осмысленно обработать залп исключений гораздо труднее, чем одно исключение.
D выбрал подход простой и эффективный. Всякий объект типа `Throwable` содержит ссылку на следующий вторичный объект типа `Throwable`. Этот вторичный объект доступен через свойство `Throwable.next`. Если вторичных исключений (больше) нет, значением свойства `Throwable.next` будет `null`. По сути, создается односвязный список с полной информацией обо всех вторичных ошибках в порядке их возникновения. В голове списка находится первичное исключение. Вот ключевые моменты определения `Throwable`:
```d
class Throwable
{
this(string s);
override string toString();
@property Throwable next();
}
```
Разделение исключений на первичное и вторичные позволяет реализовать очень простую модель поведения. В любой момент, когда бы ни порождалось исключение, первичное исключение или уже порождено, или нет. Если нет, то порождаемое исключение становится первичным. Иначе порождаемое исключение добавляется в конец односвязного списка, головой которого является первичное исключение. Продолжив начатый выше пример, напечатаем всю цепочку исключений:
```d
unittest
{
try
{
fun();
}
catch (Exception e)
{
writeln("Первичное исключение: исключение типа ", typeid(e), " ", e);
Throwable secondary;
while ((secondary = secondary.next) !is null)
{
writeln("Вторичное исключение: исключение типа ", typeid(e), " ", e);
}
}
}
```
Этот код напечатает:
```
Первичное исключение: исключение типа Exception порожденов fun
Вторичное исключение: исключение типа MyException порожденов gun #100
Вторичное исключение: исключение типа MyException порожденов gun #99
...
Вторичное исключение: исключение типа MyException порожденов gun #1
```
Вторичные исключения появляются в этой последовательности, поскольку присоединение к списку исключений выполняется в момент порождения исключения. Каждый раз инструкция `throw` извлекает первичное исключение (если есть), регистрирует новое исключение и инициирует или продолжает процесс порождения исключений.
Благодаря вторичным исключениям код на D может порождать исключения внутри деструкторов и блоков инструкций `scope`. В месте обработки исключения доступна полная информация о том, что произошло.
[В начало ⮍](#9-5-вторичные-исключения) [Наверх ⮍](#9-обработка-ошибок)
## 9.6. Раскрутка стека и код, защищенный от исключений
Пока исключение в полете, управление переносится из изначального места возникновения исключения через всю иерархию вверх до обработчика, при сопоставлении с которым происходит совпадение. Все участвующие в цепочке вызовов функции пропускаются. Ну, или почти все. Должную очистку после вызова пропускаемых функций обеспечивает так называемая *раскрутка стека* (*stack unwinding*) – часть процесса распространения исключения. Язык гарантирует, что пока исключение в полете, выполняются следующие фрагменты кода:
- деструкторы расположенных в стеке объектов-структур всех пропущенных функций;
- блоки `finally` всех пропускаемых функций;
- инструкции `scope(exit)` и `scope(failure)`, действующие на момент порождения исключения.
Раскрутка стека – бесценная помощь в обеспечении корректности программы при возникновении исключений. Программы, использующие исключения, обычно предрасположены к утечке ресурсов. Многие ресурсы рассчитаны только на использование в режиме «получить/освободить», а при порождении исключений то и дело возникают малозаметные потоки управления, «забывающие» освободить ресурсы. Такие ресурсы лучше всего инкапсулировать в структуры, которые надлежащим образом освобождают управляемые ресурсы в своих деструкторах. Эта тема уже обсуждалась в разделе 7.1.3.6, пример такой инкапсуляции – стандартный тип `File` из модуля `std.stdio`. Структура `File` управляет системным дескриптором файла и гарантирует, что при уничтожении объекта типа `File` внутренний дескриптор будет корректно закрыт. Объект типа `File` можно копировать; счетчик ссылок отслеживает все активные копии; копия, уничтожаемая последней, закрывает файл в его низкоуровневом представлении. Этот популярный идиоматический подход к использованию деструкторов высоко ценят программисты на C++. (Данная идиома известна как RAII, см. раздел 6.16.) Другие языки и фреймворки также используют подсчет ссылок, вручную или автоматически.
Утечка ресурсов – лишь одно из проявлений более масштабной проблемы. Иногда шаблон «выполнить/отменить» связан с ресурсом, который невозможно «пощупать». Например, при записи текста HTML-файла многие теги (например, `<b>`) полагается закрывать парным тегом (`</b>`). Нелинейный поток управления, включающий порождение исключений, может привести к генерации некорректно сформированных HTML-документов. Например:
```d
void sendHTML(Connection conn)
{
conn.send("<html>");
... // Отправить полезную информацию в файл
conn.send("</html>");
}
```
Если код между двумя вызовами `conn.send` преждевременно прервет выполнение функции `sendHTML`, то закрывающий тег не будет отправлен и результатом станет некорректный поток HTML-данных. Такую же проблему могла бы вызвать инструкция `return`, расположенная в середине `sendHTML`, но `return` можно хотя бы увидеть невооруженным глазом, просто внимательно просмотрев тело функции. Исключение же, напротив, может быть порождено любой из функций, вызывающих `sendHTML` (напрямую или косвенно). Из-за этого оценка корректности `sendHTML` становится гораздо более сложным и трудоемким процессом. Более того, у рассматриваемого кода есть серьезные проблемы со связанностью, поскольку корректность `sendHTML` зависит от того, как поведет себя при порождении исключений потенциально огромное число других функций.
Одно из возможных решений – имитировать RAII (даже если никакие ресурсы не задействованы): определить структуру, которая отсылает закрывающий тег в своем деструкторе. В лучшем случае это паллиатив. На самом деле, нужно гарантировать выполнение определенного кода, а не захламлять программу типами и объектами.
Другое возможное решение – воспользоваться блоком `finally`:
```d
void sendHTML(Connection conn)
{
try
{
conn.send("<html>");
...
}
finally
{
conn.send("</html>");
}
}
```
У этого подхода другой недостаток – масштабируемость, вернее ее отсутствие. Слабая масштабируемость `finally` становится очевидной, как только появляются несколько вложенных пар `try`/`finally`. Например, добавим в пример еще и отправку корректно закрытого тега `<body>`. Для этого потребуются *два* вложенных блока `try`/`finally`:
```d
void sendHTML(Connection conn)
{
try
{
conn.send("<html>");
... // Отправить заголовок
try
{
conn.send("<body>");
... // Отправить содержимое
}
finally
{
conn.send("</body>");
}
}
finally
{
conn.send("</html>");
}
}
```
Тот же результат можно получить альтернативным способом –с единственным блоком `finally` и дополнительной переменной состояния, отслеживающей, насколько продвинулось выполнение функции:
```d
void sendHTML(Connection conn)
{
int step = 0;
try
{
conn.send("<html>");
... // Отправить заголовок
step = 1;
conn.send("<body>");
... // Отправить содержимое
step = 2;
}
finally
{
if (step > 1) conn.send("</body>");
if (step > 0) conn.send("</html>");
}
}
```
При таком подходе дела обстоят куда лучше, но теперь целый кусок кода посвящен только управлению состоянием, что затуманивает истинное предназначение функции.
Такие ситуации удобнее всего обрабатывать с помощью инструкций `scope`. Работающая функция в какой-то момент исполнения может включать инструкцию `scope`. Таким образом, любые фрагменты кода, представляющие собой логические пары, окажутся еще и объединенными физически.
```d
void sendHTML(Connection conn)
{
conn.send("<html>");
scope(exit) conn.send("</html>");
... // Отправить заголовок
conn.send("<body>");
scope(exit) conn.send("</body>");
... // Отправить содержимое
}
```
Новая организация кода обладает целым рядом привлекательных качеств. Во-первых, код теперь расположен линейно, без излишних вложенностей. Это позволяет легко разместить в коде сразу несколько пар типа «открыть/закрыть». Во-вторых, этот подход устраняет необходимость пристального рассмотрения кода функции `sendHTML` и вызываемых ею функций на предмет скрытых потоков управления, возникающих при возможном порождении исключений. В-третьих, взаимосвязанные понятия сгруппированы, что упрощает чтение и сопровождение кода. В-четвертых, код получается компактным, поскольку накладные расходы на запись инструкции `scope` малы.
[В начало ⮍](#9-6-раскрутка-стека-и-код-защищенный-от-исключений) [Наверх ⮍](#9-обработка-ошибок)
## 9.7. Неперехваченные исключения
Если найти обработчик для исключения не удалось, встроенный обработчик просто выводит сообщение об исключении в стандартный поток ошибок и завершает выполнение с ненулевым кодом выхода. Эта схема работает не только для исключений, распространяемых из `main`, но и для исключений, порождаемых блоками `static this`.
Как уже говорилось, обычно исключения типа `Throwable` не обрабатывают. В очень редких случаях вы, возможно, все же захотите обработать `Throwable` и принять какие-то экстренные меры, даже если наступит конец света или начнется потоп. Но при этом не рассчитывайте на осмысленное состояние системы в целом; логика вашей программы, скорее всего, будет нарушена, так что вы уже мало что сможете сделать.
[В начало ⮍](#9-7-неперехваченные-исключения) [Наверх ⮍](#9-обработка-ошибок)
[^1]: Имеется в виду карточка из игры «Монополия». –*Прим. пер.*