Введение в С# для программистов (A programmer's Introduction to C#)

Автор - Eric Gunnerson
перевод - Трубецкой А.


На главную
Содержание
Глава 3
Глава 5

Глава 4: Обработка исключений

Чем неудобно возвращаемое значение?
Вероятно многим программистам приходилось писать приблизительно такой код:
bool success = CallFunction();
if (!success) {
// process the error
}
Такой код работает нормально, но возвращаемое значение каждый раз нужно проверять, чтобы обнаружить ошибку. Если же написать
CallFunction();
то любое сообщение об ошибке будет потеряно.

В .NET Runtime фундаментальным средством обработки ошибок являются исключения. Этот способ надежнее, чем возвращаемое значение, поскольку исключение невозможно проигнорировать.


Блоки try и catch
Для работы с исключениями код должен быть организован особым образом. Часть кода, в которой может быть сгенерировано исключение, должна быть помещена в блок try, а в блок catch следует поместить код, обрабатывающий исключения. Приведем пример:
using System;
class Test {
static int Zero = 0;
public static void Main() {
// watch for exceptions here
try {
int j = 22 / Zero;
}

// exceptions that occur in try are transferred here
catch (Exception e) {
Console.WriteLine("Exception " + e.Message);
}
Console.WriteLine("After catch");
}
}
Блок try содержит выражение, которое генерирует исключение. В данном случае оно генерирует исключение DivideByZeroException. Когда происходит деление, .NET Runtime останавливает выполнение кода и ищет блок try, окружающий код, в котором было сгенерировано исключение. Когда .NET Runtime находит блок try, то начинает искать связанные с ним блоки catch. Если блоки catch найдены, то .NET Runtime выбирает наиболее подходящий из них и выполняет код, заключенный в этом блоке. Код блока catch может обработать событие или повторно сгенерировать исключение.
В приведенном примере блок catch выводит сообщение, которое содержится в объекте типа Exception.


Иерархия исключений
Все исключения в C# являются производными от класса Exception, который представляет собой часть Common Language Runtime. Когда генерируется исключение, выбирается соответствующий блок catch путем сравнения типа сгенерированного исключения с типами исключений, которые перехватываются блоками catch. При этом отдается предпочтение блоку catch с наиболее точным соответствием типа, а не блоку, перехватывающему более общие исключения. Вернемся к примеру:
using System;
class Test {
static int Zero = 0;
public static void Main() {
try {
int j = 22 / Zero;
}
// catch a specific exception
catch (DivideByZeroException e) {
Console.WriteLine("DivideByZero {0}", e);
}
// catch any remaining exceptions
catch (Exception e) {
Console.WriteLine("Exception {0}", e);
}
}
}
Здесь блок catch, перехватывающий DivideByZeroException, более точно соответствует типу сгенерированного исключения, поэтому будет выполнен именно этот блок.

Этот пример немного сложнее:
using System;
class Test {
static int Zero = 0;
static void AFunction() {
int j = 22 / Zero;
// the following line is never executed.
Console.WriteLine("In AFunction()");
}
public static void Main() {
try {
AFunction();
}
catch (DivideByZeroException e) {
Console.WriteLine("DivideByZero {0}", e);
}
}
}
Что здесь происходит?
При делении на ноль генерируется исключение. .NET Runtime начинает искать блок try в функции AFunction(),
но не находит его там. Тогда .NET Runtime выходит за пределы функции AFunction() и начинает искать блок try в функции Main(). Найдя блок try, .NET Runtime ищет подходящий блок catch, который и выполняется.

Бывают ситуации, когда подходящий блок catch отсутствует:
using System;
class Test {
static int Zero = 0;
static void AFunction() {
try {
int j = 22 / Zero;
}
// this exception doesn't match
catch (ArgumentOutOfRangeException e) {
Console.WriteLine("OutOfRangeException: {0}", e);
}
Console.WriteLine("In AFunction()");
}
public static void Main() {
try {
AFunction();
}
// this exception doesn't match
catch (ArgumentException e) {
Console.WriteLine("ArgumentException {0}", e);
}
}
}
Ни блок catch из функции AFunction(), ни блок catch из функции Main() не соответствуют типу генерируемого исключения. В этом случае исключение перехватывается обработчиком "последней надежды" ("last chance" exception handler). Действия, совершаемые этим обработчиком, зависят от конфигурации .NET Runtime, но, как правило, он выводит диалоговое окно с сообщением о типе исключения и прерывает выполнение программы.


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

1. Предостережение вызывающей функции (Caller Beware)
Этот способ заключается в том, что исключение просто не перехватывается. Иногда это является правильным проектным решением, но в этом случае объект может остаться в некорректном состоянии. Если в дальнейшем попытаться использовать его, то могут возникнуть проблемы.
Кроме того, в этом случае вызывающая функция может получить недостаточную информацию.

2. Запутывание вызывающей функции (Caller Confuse)
Согласно этому способу следует перехватить исключение, произвести необходимые действия по восстановлению корректного состояния объекта, затем повторно сгенерировать исключение:
using System;
public class Summer {
int sum = 0;
int count = 0;
float average;

public void DoAverage() {
try {
average = sum / count;
}
catch (DivideByZeroException e) {
// do some cleanup here
throw e;
}
}
}
class Test {
public static void Main() {
Summer summer = new Summer();
try {
summer.DoAverage();
}
catch (Exception e) {
Console.WriteLine("Exception {0}", e);
}
}
}
Этот способ обычно предполагает минимальные затраты на обработку исключений для вызывающей функции, т.к. объект должен всегда сохранять корректное состояние после генерации исключения.
Способ называется запутыванием вызывающей функции, потому что, поскольку объект находится в корректном состоянии после генерации исключения, вызывающая функция часто имеет слишком мало информации, чтобы продолжать работу.
В приведенном примере вызывающая функция узнает только то, что где-то в вызванной функции произошло исключение DivideByZeroException, но не знает никаких подробностей исключения или способа его устранения.

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

3. Информирование вызывающей функции (Caller Inform)
При использовании этого способа пользователю сообщается дополнительная информация. Для перехваченного исключения создается оболочка, содержащая дополнительную информацию об исключении.
using System;
public class Summer {
int sum = 0;
int count = 0;
float average;

public void DoAverage() {
try {
average = sum / count;
}
catch (DivideByZeroException e) {
// wrap exception in another one,
// adding additional context.
throw (new DivideByZeroException(
"Count is zero in DoAverage()", e));
}
}
}
public class Test {
public static void Main() {
Summer summer = new Summer();
try {
summer.DoAverage();
}
catch (Exception e) {
Console.WriteLine("Exception: {0}", e);
}
}
}
Когда DivideByZeroException перехватывается в функции DoAverage(), для него создается новое исключение-оболочка, которое сообщает пользователю дополнительную информацию о причине исходного исключения. Обычно исключение-оболочка имеет тот же тип, что и перехваченное исключение, но это может изменяться в зависимости от сведений, предоставляемых вызывающей функции.

Исключение может быть описано следующим образом:

Исключение: System.DivideByZeroException: Count is zero in DoAverage() --->
System.DivideByZeroException
at Summer.DoAverage()
at Summer.DoAverage()
at Test.Main()

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


Классы исключений, определяемые пользователем
У предыдущего примера есть один недостаток. Он состоит в том, что, определив тип исключения, вызывающая функция не может сообщить, какое именно исключение было сгенерировано при вызове DoAverage(). Чтобы узнать, что исключение произошло из-за того, что count был равен нулю, в сообщении придется искать строку "count is zero".
Это может оказаться довольно неудобным, так как пользователь не может быть уверен, что текст останется точно таким же в более поздних версиях класса. Кроме того, автор класса не сможет изменить текст. Если текст нужно изменить, можно создать новый класс исключения.
using System;
public class CountIsZeroException: Exception {
public CountIsZeroException() {
}
public CountIsZeroException(string message)
: base(message) {
}
public CountIsZeroException(string message, Exception inner)
: base(message, inner) {
}
}
public class Summer {
int sum = 0;
int count = 0;
float average;
public void DoAverage() {
if (count == 0)
throw(new CountIsZeroException("Zero count in DoAverage"));
else
average = sum / count;
}
}
class Test {
public static void Main() {
Summer summer = new Summer();
try {
summer.DoAverage();
}
catch (CountIsZeroException e) {
Console.WriteLine("CountIsZeroException: {0}", e);
}
}
}
Теперь DoAverage() определяет, будет ли генерироваться исключение (т.е. равен ли count нулю). Если это так, то DoAverage() создает и бросает исключение CountIsZeroException.


Ключевое слово finally
Бывают ситуации, когда перед завершением функции обязательно должны быть выполнены определенные действия (например, закрытие файла). Если возникает исключение, эти действия могут остаться не выполненными.
using System;
using System.IO;

class Processor {
int count;
int sum;
public int average;
void CalculateAverage(int countAdd, int sumAdd) {
count += countAdd;
sum += sumAdd;
average = sum / count;
}
public void ProcessFile() {
FileStream f = new FileStream("data.txt", FileMode.Open);
try {
StreamReader t = new StreamReader(f);
string line;
while ((line = t.ReadLine()) != null) {
int count;
int sum;
count = Int32.FromString(line);
line = t.ReadLine();
sum = Int32.FromString(line);
CalculateAverage(count, sum);
}
f.Close();
}
// always executed before function exit, even if an
// exception was thrown in the try.
finally {
f.Close();
}
}
}
class Test {
public static void Main() {
Processor processor = new Processor();
try {
processor.ProcessFile();
}
catch (Exception e) {
Console.WriteLine("Exception: {0}", e);
}
}
}
В этом примере программа просматривает файл, считывает из него count и sum и использует их для вычисления среднего значения. Что происходит, однако, если первое считанное из файла значение count равно нулю?
В этом случае при делении в CalculateAverage() будет брошено исключение DivideByZeroException, которое прервет цикл чтения из файла. Если бы программист писал функцию, не задумываясь об исключениях, вызов функции file.Close() был бы пропущен, и файл остался бы открытым.
Код внутри блока finally гарантированно выполнится перед выходом из функции независимо от того, будет ли сгенерировано исключение. Если поместить вызов функции file.Close() в блок finally, файл будет закрыт в любом случае.


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

В C#, однако, объекты отслеживаются сборщиком мусора (garbage collector), а не компилятором, поэтому обработка исключений оказывается очень недорогой в реализации и почти не увеличивает время выполнения, если исключительные ситуации не возникают.


Рекомендации по проектированию
Исключения должны сообщать об исключительных условиях. Не следует использовать их, чтобы сообщить о событии, которое ожидалось (например, о достижении конца файла). При нормальной работе класса не должно возникать никаких исключений.

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

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

На главную
Глава 3
Глава 5

Rambler's Top100
Хостинг от uCoz