Browse Source

Глава 9 готова

Alexander Zhirov 2 years ago
parent
commit
950ad650d9

+ 495 - 0
09-обработка-ошибок/README.md

@@ -0,0 +1,495 @@
+# 9. Обработка ошибок
+
+- [9.1. Порождение и обработка исключительных ситуаций](#9-1-порождение-и-обработка-исключительных-ситуаций)
+- [9.2. Типы](#9-2-типы)
+- [9.3. Блоки finally](#9-3-блоки-finally)
+- [9.4. Функции, не порождающие исключения (nothrow), и особая природа класса Throwable](#9-4-функции-не-порождающие-исключения-nothrow-и-особая-природа-класса-throwable)
+- [9.5. Вторичные исключения](#9-5-вторичные-исключения)
+- [9.6. Раскрутка стека и код, защищенный от исключений](#9-6-раскрутка-стека-и-код-защищенный-от-исключений)
+- [9.7. Неперехваченные исключения](#9-7-неперехваченные-исключения)
+
+Обработка ошибок – это слабо формализованная область программного инжиниринга, связанная с обработкой ожидаемых и возникших ошибочных ситуаций, способных помешать нормальному функционированию системы. Обработка исключительных ситуаций (исключений) – подход к обработке ошибок, принятый во многих современных языках (включая 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]: Имеется в виду карточка из игры «Монополия». – *Прим. пер.*

BIN
09-обработка-ошибок/images/image-9-2.png