Структурные исключения — один из ключевых механизмов обработки ошибочных (в том числе и собственно исключительных) ситуаций. Ниже перечислены некоторые рекомендации по программированию, повышающие общее качество кода при работе с исключениями на C# и шире — платформе .NET.
Собственный класс. Выбрасывайте исключения на основе собственного класса, унаследованного от Exception
, а не напрямую — на основе Exception
, потому что это дает возможность определить свой собственный обработчик и отделить отслеживание и обработку исключений, выбрасываемых вашим кодом и кодом фреймворка .NET.
Отдельные поля. Создавайте отдельные поля в собственном классе для передачи существенной информации, вместо сериализации и десериализации данных в поле Message
. Несмотря на то, что идея упаковки в Message
сложных данных в виде строки типа JSON выглядит соблазнительно, это редко является удачной идеей, поскольку добавляет дополнительный расход ресурсов на кодирование, локализацию, декодирование.
Сообщения в лог. Записывайте в лог сообщение всякий раз, когда обработчик отлавливает Exception
, с помощью вызова Exception.ToString();
это упростит отслеживание при отладке исключительных ситуаций.
Точный класс. Используйте наименее общий класс для отлавливания исключений, иначе это может приводить к труднообнаруживаемым ошибкам. Рассмотрим следующий код:
public class SimpleClass
{
public static string DoSomething()
{
try
{
return SimpleLibrary.ReportStatus();
}
catch (Exception)
{
return "Failure 1";
}
}
}
public class SimpleLibrary
{
public static string ReportStatus()
{
return String.Format("Success {0}.", 0);
}
}
Если предположить, что код классов SimpleClass
и SimpleLibrary
находится в отдельных сборках, то в случае, когда обе сборки установлены правильно, код выполняется правильно, выводя сообщение «Success 0», а если сборка с классом SimpleLibrary
не будет установлена, тогда код выполняется неправильно, выводя сообщение «Failure 1», несмотря на то, что никакой ошибки при исполнении функции ReportStatus
не происходит. Проблема неочевидна из-за слишком обобщенной обработки ислючений. Код, форматирующий строку, выбрасывает исключения ArgumentNullException
и FormatException
, поэтому именно эти исключения и должны перехватываться в блоке catch, тогда станет очевидной причина ошибки — это исключение FileNotFoundException
из-за отсутствия или неправильной установки сборки, содержащей класс SimpleLibrary
.
Содержательная обработка. Всегда обрабатывайте исключения содержательно. Код вида
try
{
DoSomething();
}
catch (SomeException)
{
// TODO: ...
}
скрывает проблемы, не позволяя обнаружить их при отладке или выполнении.
Очистка в блоке finally
. Удаляйте временные объекты в блоках finally
. Рассмотрим операцию записи временного файла.
void WorkWithFile(string filename)
{
try
{
using (StreamWriter sw = new StreamWriter(filename))
{
// TODO: Do something with temporary file
}
File.Delete(filename);
}
catch (Exception)
{
File.Delete(filename);
throw;
}
}
Как видно, код удаляющий файл, дублируется. Чтобы избежать повтора, нужно удалять временный файл из блока finally
.
void WorkWithFile(string filename)
{
try
{
using (StreamWriter sw = new StreamWriter(filename))
{
// TODO: Do something with temporary file
}
}
finally
{
File.Delete(filename);
}
}
Оператор using
. Используйте оператор using
. Он гарантирует вызов метода Dispose
, даже если при вызове методов в объекте происходит исключение.
using (Font f = new Font("Times New Roman", 12.0f))
{
byte charset = f.GdiCharSet;
}
Использование оператора using
равноценно блоку try/finally
, но более компактно и лаконично.
Font f = new Font("Times New Roman", 12.0f);
try
{
byte charset = f.GdiCharSet;
}
finally
{
if (f != null)
((IDisposable)f).Dispose();
}
Результат функции. Не используйте исключения для возврата результата работы функции и не используйте специальные коды возврата для обработки ошибок. Каждому — свое. Результат функции нужно возращать, а ошибки, которые нельзя пропускать, — обрабатывать с помощью исключений.
Отсутствие ресурса. Возвращайте null
при отсутствии ресурса. Согласно соглашению, общепринятому для API .NET, функции не должны выкидывать исключения при отсутствии ресурса, они должны возвращать null
. Так GetManifestResourceStream
возращает null
, если ресурсы не были указаны при компиляции или не видны для вызывающего кода.
Исходное место. Сохраняйте информацию об исходном месте возникновения исключения. Например,
try
{
// Do something to throw exception
}
catch (Exception e)
{
// Do something to handle exception
throw e; // Wrong way!
throw; // Right way
}
В случае вызова throw e;
информация о месте генерации исключения будет подменена новой строкой, поскольку создание нового исключения очистит стэк вызовов. Вместо этого нужно сделать вызов throw;
который просто перевыбросит исходное исключение.
Добавление смысла. Меняйте исходное исключение, только чтобы добавить смысл, необходимый для пользователя. Например, в случае подключения к базе данных, пользователю может быть безразлична причина сбоя подключения (ошибка сокета), достаточно информации о самом сбое.
Сериализация. Делайте исключения, унаследованные от Exception
, сериализуемыми с помощью [Serializable]
. Это полезно, так как никогда заранее неизвестно, где будет получено исключение, например, в веб службе.
[Serializable]
public class SampleSerializableException : Exception
{
public SampleSerializableException()
{
}
public SampleSerializableException(string message)
: base(message)
{
}
public SampleSerializableException(string message, Exception innerException)
: base(message, innerException)
{
}
protected SampleSerializableException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
Стандартные конструкторы. Реализуйте стандартные конструкторы, иначе правильная обработка исключений может быть затруднена. Так для исключения NewException
эти конструкторы таковы:
public NewException();
public NewException(string);
public NewException(string, Exception);
protected or private NewException(SerializationInfo, StreamingContext);
По материалам MSDN, CodeProject, StackOverflow.
Автор: tomaitsu