- PVSM.RU - https://www.pvsm.ru -
Одним из самых замечательных и притягательных свойств языка Common Lisp является, безусловно, его система обработки исключений.
Более того, по моему, лично, мнению, подобный подход к исключениям является единственно правильным для всех императивных языков, и вот по какой простой причине:
Механизм «исключений»(или, как они называются в мире CL — conditions) в Common Lisp отделен от механизма раскрутки стека, а это, соответственно, позволяет обрабатывать любые всплывающие в программе исключительные(да и не только исключительные) ситуации прямо в том месте, где они возникли, без потери контекста выполнения программы, что влечет за собой удобство разработки, отладки, да и вообще, удобство построения логики программы.
Наверное, следует сказать, что Common Lisp Condition System, несмотря на свою уникальность в среде высокоуровневых языков программирования, очень близка известным многим разработчикам низкоуровневым средствам современных операционных систем, а именно: синхронным сигналам UNIX и, гораздо ближе, механизму SEH(Structured Exception Handling) из Windows. Ведущие реализации CL основывают такие элементы управления потоком вычислений, как механизм обработки исключений и раскрутка стека, именно на них.
Несмотря на отсутствие похожего механизма во многих других(если не всех) императивных языках программирования, он поддается реализации в более-менее вменяемом виде на большинстве из них. В данной статье я опишу реализацию на C#, по ходу дела разбирая в деталях саму концепцию данного подхода к «исключениям».
Для полноценной реализации CLCS от языка программирования, а скорее даже — от его рантайма, требуется следующие несколько вещей:
Мы перенесем на C# следующие примитивы системы обработки исключений CL:
Подробнее про эти, и другие примитивы, связанные с обработкой исключений, можно прочитать в замечательной книжке Питера Сибеля «Practical Common Lisp», в главе 19:
lisper.ru/pcl/beyond-exception-handling-conditions-and-restarts [12]
Вся реализация у нас будет содержаться в статическом классе Conditions. Далее я буду описывать его методы.
Но сначала следует описать пару статических переменных.
В каждом потоке выполнения программы обработчики исключений и перезапуски при установке формируют стек. Вообще, формально говоря, стек формируют динамические окружения каждого треда, но так как динамические окружения в C#, строго говоря, отсутствуют, мы будем «руками» связывать с каждым тредом структуру данных «стек».
static ConditionalWeakTable<Thread, Stack<Tuple<Type, HandlerBindCallback>>> _handlerStacks;
static ConditionalWeakTable<Thread, Stack<Tuple<string, RestartBindCallback>>> _restartStacks;
static Conditions()
{
_handlerStacks = new ConditionalWeakTable<Thread, Stack<Tuple<Type, HandlerBindCallback>>>();
_restartStacks = new ConditionalWeakTable<Thread, Stack<Tuple<string, RestartBindCallback>>>();
}
Для словаря «тред -> стек» я здесь выбрал класс ConditionalWeakTable, добавленный в .NET 4.0, но можно использовать любую другую подобную структуру данных. ConditionalWeakTable хорош тем, что является хеш-табличкой со «слабыми указателями»(WeakPointer — отсюда и Weak в названии класса) на ключи, а это, соответственно, значит, что при удалении объекта треда(Thread) сборщиком мусора, у нас не возникнет утечки памяти.
public static T HandlerBind<T>(Type exceptionType, HandlerBindCallback handler, HandlerBody<T> body)
{
if (null == exceptionType)
throw new ArgumentNullException("exceptionType");
if (!exceptionType.IsSubclassOf(typeof(Exception)))
throw new InvalidOperationException("exceptionType is not a subtype of System.Exception");
if (null == handler)
throw new ArgumentNullException("handler");
if (null == body)
throw new ArgumentNullException("body");
Thread currentThread = Thread.CurrentThread;
var clusters = _handlerStacks.GetOrCreateValue(currentThread);
clusters.Push(Tuple.Create(exceptionType, handler));
try
{
return body();
}
finally
{
clusters.Pop();
}
}
Метод HandlerBind у нас принимает три параметра — тип исключения, с которым связывается обработчик(как видно из тела метода, он должен быть подклассом Exception), коллбек, определяющий код обработчика, и еще один делегат, определяющий код, исполняемый в теле оператора.
Типы делегатов handler и body такие:
public delegate void HandlerBindCallback(Exception exception);
public delegate T HandlerBody<T>();
Параметр exception, передаваемый обработчику в аргументы это собственно сам объект исключения.
Как видно, реализация HandlerBind проста — к стеку обработчиков, связанных с текущим тредом, мы добавляем новый, после — выполняем код тела оператора, и в итоге, в теле finally, убираем обработчик со стека. Таким образом, стек обработчиков исключений связывается со стеком выполнения текущего треда, и каждый установленный обработчик становится недействительным при выходе из соответствующего стекового кадра потока выполнения программы.
public static T HandlerCase<T>(Type exceptionType, HandlerCaseCallback<T> handler, HandlerBody<T> body)
{
if (null == exceptionType)
throw new ArgumentNullException("exceptionType");
if (!exceptionType.IsSubclassOf(typeof(Exception)))
throw new InvalidOperationException("exceptionType is not a subtype of System.Exception");
if (null == handler)
throw new ArgumentNullException("handler");
if (null == body)
throw new ArgumentNullException("body");
var unwindTag = new UnwindTag<T>();
HandlerBindCallback handlerCallback = (e) =>
{
unwindTag.Value = handler(e);
throw unwindTag;
};
try
{
return HandlerBind(exceptionType, handlerCallback, body);
}
catch (UnwindTag<T> e)
{
if (e == unwindTag)
{
return e.Value;
}
else
throw;
}
}
Реализация HandlerCase несколько сложнее. Отличие от HandlerBind, напомню, в том, что этот оператор раскручивает стек до точки, в которой установлен обработчик. Так как в C# запрещены явные escaping continuations(то есть, грубо говоря, мы не можем сделать goto или return из лямбды, передаваемой вниз по стеку, во внешний блок), то для раскрутки стека мы используем обычные try-catch, а блок обработчика идентифицируем объектом вспомогательного класса UnwindTag
class UnwindTag<T> : Exception
{
public T Value { get; set; }
}
HandlerCaseCallback отличается от HandlerBindCallback только тем, что возвращает какое-либо значение:
public delegate T HandlerCaseCallback<T>(Exception exception);
Функция Signal это самое сердце системы обработки исключений CL. В отличие от throw и сотоварищей, из других языков программирования, она не раскручивает стек вызовов, а всего лишь сигнализирует о произошедшем исключении, то есть просто вызывает подходящий обработчик.
public static void Signal<T>(T exception)
where T : Exception
{
if (null == exception)
throw new ArgumentNullException("exception");
Thread currentThread = Thread.CurrentThread;
var clusters = _handlerStacks.GetOrCreateValue(currentThread);
var i = clusters.GetEnumerator();
while (i.MoveNext())
{
var type = i.Current.Item1;
var handler = i.Current.Item2;
if (type.IsInstanceOfType(exception))
{
handler(exception);
break;
}
}
}
Как видно — всё очень просто. Из текущего стека обработчиков исключений мы берем первый, способный работать с классом исключений, экземпляром которого является объект, переданный нам в параметр exception.
public static void Error<T>(T exception)
where T : Exception
{
Signal(exception);
throw exception;
}
Error отличается от Signal только тем, что прерывает нормальный поток выполнения программы в случае отсутствия подходящего обработчика. Если бы мы писали полноценную реализацию Common Lisp под .NET, вместо «throw exception» было бы что-то вроде «InvokeDebuggerOrDie(exception);»
RestartBind и RestartCase очень похожи на HandlerBind и HandlerCase, с тем отличием, что работают со стеком перезапусков, и ставят в соответствие делегату-обработчику не тип исключения, а строку, имя перезапуска.
public delegate object RestartBindCallback(object param);
public delegate T RestartCaseCallback<T>(object param);
public static T RestartBind<T>(string name, RestartBindCallback restart, HandlerBody<T> body)
{
if (null == name)
throw new ArgumentNullException("name");
if (null == restart)
throw new ArgumentNullException("restart");
if (null == body)
throw new ArgumentNullException("body");
Thread currentThread = Thread.CurrentThread;
var clusters = _restartStacks.GetOrCreateValue(currentThread);
clusters.Push(Tuple.Create(name, restart));
try
{
return body();
}
finally
{
clusters.Pop();
}
}
public static T RestartCase<T>(string name, RestartCaseCallback<T> restart, HandlerBody<T> body)
{
if (null == name)
throw new ArgumentNullException("name");
if (null == restart)
throw new ArgumentNullException("restart");
if (null == body)
throw new ArgumentNullException("body");
var unwindTag = new UnwindTag<T>();
RestartBindCallback restartCallback = (param) =>
{
unwindTag.Value = restart(param);
throw unwindTag;
};
try
{
return RestartBind(name, restartCallback, body);
}
catch (UnwindTag<T> e)
{
if (e == unwindTag)
{
return e.Value;
}
else
throw;
}
}
FindRestart и InvokeRestart, в свою очередь, очень похожи на метод Signal — первая функция находит перезапуск в соответствующем стеке текущего треда по имени, а вторая не только находит его, но и сразу запускает.
public static RestartBindCallback FindRestart(string name, bool throwOnError)
{
if (null == name)
throw new ArgumentNullException("name");
Thread currentThread = Thread.CurrentThread;
var clusters = _restartStacks.GetOrCreateValue(currentThread);
var i = clusters.GetEnumerator();
while (i.MoveNext())
{
var restartName = i.Current.Item1;
var restart = i.Current.Item2;
if (name == restartName)
return restart;
}
if (throwOnError)
throw new RestartNotFoundException(name);
else
return null;
}
public static object InvokeRestart(string name, object param)
{
var restart = FindRestart(name, true);
return restart(param);
}
ComputeRestarts просто возвращает список всех установленных в данный момент перезапусков — это может быть полезно, например, обработчику исключений, чтобы он, при вызове, мог выбрать подходящий перезапуск для какой-то конкретной ситуации.
public static IEnumerable<Tuple<string, RestartBindCallback>> ComputeRestarts()
{
var restarts = new Dictionary<string, RestartBindCallback>();
Thread currentThread = Thread.CurrentThread;
var clusters = _restartStacks.GetOrCreateValue(currentThread);
return clusters.AsEnumerable();
}
Наша реализация UnwindProtect просто оборачивает блок try-finally.
public static T UnwindProtect<T>(HandlerBody<T> body, params Action[] actions)
{
if (null == body)
throw new ArgumentNullException("body");
if (null == actions)
actions = new Action[0];
try
{
return body();
}
finally
{
foreach (var a in actions)
a();
}
}
Напоследок — несколько примеров использования.
static int DivSignal(int x, int y)
{
if (0 == y)
{
Conditions.Signal(new DivideByZeroException());
return 0;
}
else
return x / y;
}
int r = Conditions.HandlerBind(
typeof(DivideByZeroException),
(e) =>
{
Console.WriteLine("Entering handler callback");
},
() =>
{
Console.WriteLine("Entering HandlerBind with DivSignal");
var rv = DivSignal(123, 0);
Console.WriteLine("Returning {0} from body", rv);
return rv;
});
Console.WriteLine("Return value: {0}n", r);
Здесь функция DivSignal, при делителе равном нулю, сигнализирует о возникшей ситуации, но тем не менее, сама «справляется» с ней(возвращает нуль). В данном случае ни обработчик, ни сама функция не прерывают нормальный ход программы.
Вывод на консоль получается такой:
Entering HandlerBind with DivSignal
Entering handler callback
Returning 0 from body
Return value: 0
static int DivError(int x, int y)
{
if (0 == y)
Conditions.Error(new DivideByZeroException());
return x / y;
}
int r = Conditions.HandlerCase(
typeof(DivideByZeroException),
(e) =>
{
Console.WriteLine("Entering handler callback");
Console.WriteLine("Returning 0 from handler");
return 0;
},
() =>
{
Console.WriteLine("Entering HandlerCase with DivError and UnwindProtect");
return Conditions.UnwindProtect(
() =>
{
Console.WriteLine("Entering UnwindProtect");
var rv = DivError(123, 0);
Console.WriteLine("This line should not be printed");
return rv;
},
() =>
{
Console.WriteLine("UnwindProtect exit point");
});
});
Console.WriteLine("Return value: {0}n", r);
В данном случае, функция DivError выбрасывает исключение, но обработчик перехватывает его, раскручивает стек, и возвращает свое значение(в данном случае — 0). По ходу раскрутки стека, поток вычисления проходит через UnwindProtect.
Данный пример, в отличие от остальных, можно было бы переписать с помощью обычных try, catch и finally.
Вывод на консоль:
Entering HandlerCase with DivError and UnwindProtect
Entering UnwindProtect
Entering handler callback
Returning 0 from handler
UnwindProtect exit point
Return value: 0
static int DivRestart(int x, int y)
{
return Conditions.RestartCase(
"ReturnValue",
(param) =>
{
Console.WriteLine("Entering restart ReturnValue");
Console.WriteLine("Returning {0} from restart", param);
return (int)param;
},
() =>
{
Console.WriteLine("Entering RestartCase");
return DivError(x, y);
});
}
DivRestart устанавливает перезапуск с именем «ReturnValue», который, при активации, просто возвращает значение, переданное ему через параметр(param). Тело RestartCase вызывает DivError описанную в предыдущем примере.
int r = Conditions.HandlerBind(
typeof(DivideByZeroException),
(e) =>
{
Console.WriteLine("Entering handler callback");
Console.WriteLine("Invoking restart ReturnValue with param = 0");
Conditions.InvokeRestart("ReturnValue", 0);
},
() =>
{
Console.WriteLine("Entering HandlerBind with DivRestart");
return DivRestart(123, 0);
});
Console.WriteLine("Return value: {0}", r);
Обработчик, установленный в HandlerBind, при вызове ищет перезапуск «ReturnValue» и передает ему в параметр число 0, после этого «ReturnValue» активируется, раскручивает стек до своего уровня, и возвращает это самое число из RestartCase, установленного в DivRestart, как видно выше.
Вывод:
Entering HandlerBind with DivRestart
Entering RestartCase
Entering handler callback
Invoking restart ReturnValue with param = 0
Entering restart ReturnValue
Returning 0 from restart
Return value: 0
Полный исходный код библиотеки и примеров доступен на github: github.com/Lovesan/ConditionSystem [13]
Автор: love5an
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/13745
Ссылки в тексте:
[1] continuations: http://ru.wikipedia.org/wiki/Continuations
[2] love5an.livejournal.com/371169.html: http://love5an.livejournal.com/371169.html
[3] handler-bind: http://l1sp.org/cl/handler-bind
[4] handler-case: http://l1sp.org/cl/handler-case
[5] signal: http://l1sp.org/cl/signal
[6] error: http://www.lispworks.com/documentation/HyperSpec/Body/f_error.htm
[7] restart-bind: http://l1sp.org/cl/restart-bind
[8] restart-case: http://l1sp.org/cl/restart-case
[9] find-restart: http://l1sp.org/cl/find-restart
[10] compute-restarts: http://l1sp.org/cl/compute-restarts
[11] unwind-protect: http://l1sp.org/cl/unwind-protect
[12] lisper.ru/pcl/beyond-exception-handling-conditions-and-restarts: http://lisper.ru/pcl/beyond-exception-handling-conditions-and-restarts
[13] github.com/Lovesan/ConditionSystem: http://github.com/Lovesan/ConditionSystem
Нажмите здесь для печати.