Базовая структура нейтрального к исключениям кода

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

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

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

void ExceptionNeutralMethod()
{
//-
// Весь код, который потенциально может сгенерировать исключение,
// находится в первом разделе. Здесь не применяется никаких
// изменений в состоянии ни к каким объектам системы, включая данный.
//-
//-
// Все изменения фиксируются в этой точке с использованием
// операций, гарантирующих отсутствие генерации исключений.
}

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

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

1
2
3
4
5
using System.Collections;
class EmployeeDatabase {
private ArrayList activeEmployees;
private ArrayList terminatedEmployees;
}

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

Теперь посмотрим, что произойдет при увольнении сотрудника. Естественно, этот сотрудник должен быть перемещен из коллекции activeEmployees в коллекцию terminatedEmployees. Наивная реализация такой задачи могла бы выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Collections;
class Employee
{
}
class EmployeeDatabase {
public void TerminateEmployee( int index )  {
object employee = activeEmployees[index];
activeEmployees.RemoveAt ( index );
terminatedEmployees.Add( employee );
}
private ArrayList activeEmployees;
private ArrayList terminatedEmployees;
}

Приведенный код выглядит достаточно адекватным. Метод, выполняющий перемещение, предполагает, что вызывающий его код каким-то образом знает индекс текущего сотрудника в списке activeEmployees до вызова TerminateEmployee. Он копирует ссылку на указанного сотрудника, удаляет ее из activeEmployees и добавляет в коллекцию terminatedEmployees. Так что же плохого в этом методе?

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

Разумеется, если исключение ArgumentOutOf Range будет сгенерировано в первой строке, то выполнение никогда не дойдет до второй строки, но идея должна быть ясна. Кроме того, если памяти недостаточно, может случиться, что вызов Add завершиться неудачей с генерацией исключения. Опасность исходит от возможности генерации исключения после модификации состояния системы.

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

Начальная попытка может заключаться в использовании операторов try для предотвращения повреждения состояния системы.

Рассмотрим следующий пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System.Collections;
class Employee
{
}
class EmployeeDatabase {
public void TerminateEmployee ( int index )
{
object employee = null; try {
employee = activeEmployees[index];
}
catch {
// Индекс находится за пределами допустимого диапазона.
}
if ( employee != null )  {
activeEmployees.RemoveAt ( index );
try {
terminatedEmployees.Add( employee );
}
catch {
//He удалось выделить память.
activeEmployees.Add( employee );
}
}
}
private ArrayList activeEmployees;
private ArrayList terminatedEmployees;
}

Посмотрите, насколько быстро код стало трудно читать и понимать — и все “благодаря” операторам try. Объявление переменной employee должно быть вынесено за пределы оператора try, а переменная должна быть инициализирована значением null. После получения ссылку на экземпляр employee потребуется проверить на равенство null, чтобы убедиться в ее действительности. Действительную ссылку employee можно добавить в список terminatedEmployees.

Однако если по какой-то причине выполнить это не получится, employee понадобиться вернуть обратно в список activeEmployees. Также обратите внимание, что вызов RemoveAt не находится внутри блока try. Это нормально, поскольку если он даст сбой, состояние не будет модифицировано, и перехват не нужен.

С описанным подходом связано несколько проблем, которые нетрудно заметить. Во-первых, что произойдет, если не получится добавить employee обратно в коллекцию activeEmployees? Можно ли смириться с таким сбоем? Нет! Это неприемлемо, поскольку состояние системы уже изменилось.

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

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

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

Сообщество программистов на С++ признало такую технику, отчасти, благодаря блестящей работе, опубликованной Хербом Саттером (Herb Sutter) в его серии Exceptional С++ (Addison-Wesley Professional). Ничто не мешает применять эту технику и в мире С#.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System.Collections;
class Employee
{
}
class EmployeeDatabase {
public void TerminateEmployee ( int index )  {
// Клонировать важнейшие объекты.
ArrayList tempActiveEmployees =
(ArrayList) activeEmployees.Clone ();
ArrayList tempTerminatedEmployees =
(ArrayList) terminatedEmployees.Clone ();
// Выполнить действия над временными объектами,
object employee = tempActiveEmployees[index];
tempActiveEmployees.RemoveAt( index );
tempTerminatedEmployees.Add( employee );
// Зафиксировать изменения.
ArrayList tempSpace = null;
ListSwap( ref activeEmployees, ref tempActiveEmployees, ref tempSpace ) ;
ListSwap( ref terminatedEmployees, ref tempTerminatedEmployees, ref tempSpace ) ;
}
void ListSwap( ref ArrayList first, ref ArrayList second, ref ArrayList temp )  {
temp = first;
first = second;
second = temp;
temp = null;
}
private ArrayList activeEmployees;
private ArrayList terminatedEmployees;
}

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

Метод ListSwap позволяет заменить ссылки на объекты ArrayList в EmployeeDatabase ссылками на временные модифицированные копии.

Как такая техника может быть лучше, если она выглядит намного менее эффективной? Секретов два. Один, очевидный, состоит в том, что независимо от того, где бы в этом методе не генерировалось исключение, состояние EmployeeDatabase останется незатронутым. Но что, если исключение произойдет внутри ListSwap? Здесь кроется другой секрет: ListSwap никогда не генерирует исключений.

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

Случаи, когда какой-нибудь “умелец” выдернет вилку компьютера из розетки во время выполнения ListSwap, либо в этот момент произойдет землетрясение или налетит торнадо, не рассматриваются. Давайте посмотрим, почему ListSwap не генерирует исключений.

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

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

Но что случится, когда потребуется преобразование? Может ли оно привести к генерации исключения? В стандарте С# указано, что неявные операции преобразования никогда не генерируют исключений.

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

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

Простое присваивание одной ссылки на место в памяти другой — это все, что делает ListSwap.

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

Относительно ListSwap следует отметить еще один момент: временное место для хранения экземпляра ArrayList во время обмена выделяется вне метода ListSwap и передается ему как параметр ref. Это делается для того, чтобы избежать исключения StackOverf lowException внутри ListSwap. Существует минимальная вероятность, что при вызове ListSwap стек будет заполнен, и простое выделение очередного пространства в стеке завершится неудачей и вызовет исключение.

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

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

Иногда возникает необходимость в том, чтобы сделать операции обмена, подобные ListSwap, атомарными для многопоточной среды. Код ListSwap можно модифицировать для использования некоторого рода блокирующего объекта, такого как семафор или объект System.Threading.Monitor. Но при этом есть риск непреднамеренно сделать ListSwap способным генерировать исключения, что нарушит предъявляемые к нему требования.

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

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

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

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

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100