Для кого написана статья
Данная статья предназначена прежде всего для новичков в мире .NET, но может быть полезна также и разработчикам с опытом, которые не до конца разобрались, как правильно строить свои user-defined exceptions с помощью C#.
Пример кода для данной статьи можно скачать здесь.
Создание простого исключения
Создавать собственные типы исключений в C# рекомендуется в тех случаях, когда нужно четко отделить возникшую в написанном программистом коде исключительную ситуацию, от исключения, возникающего в стандартных типах .NET Framework.
К примеру, есть метод, призванный изменять имя пользователя:
private static void EditUser(string oldUserName, string newUserName)
{
var userForEdit = GetUserByName(oldUserName);
if (userForEdit == null)
return;
else
userForEdit.Name = newUserName;
}
Данный метод будет решать возложенные на него задачи, но не будет делать одну важную вещь – он не сообщит о том, что пользователь с заданным именем отсутствует, и операция смены его имени не была выполнена.
Чтобы проинформировать о возникшей исключительной ситуации можно генерировать стандартное исключение, что не является рекомендуемой практикой:
private static void EditUser(string oldUserNane, string newUserName)
{
var userForEdit = GetUserByName(oldUserName);
if(userForEdit == null)
throw new Exception();
else
userForEdit.Name = newUserName;
}
Для того чтобы можно было легко определить, что исключение генерируется на уровне конкретного приложения, нужно создать свой – пользовательский Exception, и при получении null вместо нужного пользователя выбрасывать именно его.
Создать свой Exception не сложно – нужно определить public-класс, который будет наследоваться от System.Exception или System.ApplicationException. Хотя это и не является хорошей практикой, кода внутри созданного класса исключения можно не писать вообще:
public class UserNotFoundException : ApplicationException
{
}
От чего лучше наследоваться, от System.Exception или от System.ApplicationException?
Каждый из этих типов предназначен для конкретной цели. Тогда как System.Exception является общим классом для всех user-defined exceptions, то System.ApplicationException определяет исключения, возникающие на уровне конкретного приложения.
К примеру, тестовое приложения из данной статьи является отдельной программой, поэтому вполне допустимо наследовать определенный нами exception от System.ApplicationException.
К примеру, тестовое приложения из данной статьи является отдельной программой, поэтому вполне допустимо наследовать определенный нами exception от System.ApplicationException.
Теперь вместо Exception мы сгенерируем созданный нами UserNotFoundException:
private static void EditUser(string oldUserNane, string newUserName)
{
var userForEdit = GetUserByName(oldUserName);
if(userForEdit == null) throw new UserNotFoundException();
else
userForEdit.Name = newUserName;
}
В таком случае в качестве сообщения о возникшем исключении будет: «Error in the application.». Что не очень информативно.
Чтобы код класса пользовательского исключения соответствовал рекомендациям .NET, нужно придерживаться следующих правил:
- класс исключения должен наследоваться от Exception/ApplicationException;
- класс должен быть помечен атрибутом [System.Serializable];
- класс должен определять стандартный конструктор;
- класс должен определять конструктор, который устанавливает значение унаследованного свойства Message;
- класс должен определять конструктор для обработки “внутренних исключений”;
- класс должен определять конструктор для поддержки сериализации типа.
Немного о предназначении отдельных конструкторов: конструктор для обработки “внутренних исключений” нужен для того, чтобы передать в него exception, послуживший причиной возникновения данного исключения. Подробнее, зачем нужен конcтруктор для поддержки сериализации типа под спойлером «Добавление дополнительных полей, их сериализация и десериализация» ниже.
Дабы избавить программиста от необходимости писать одинаковый код в Visual Studio есть сниппет «Exception», который генерирует класс исключения, соответствующий всем рекомендациям, перечисленным выше.

Итак, после воплощения рекомендаций в жизнь, код нашего исключения должен выглядеть примерно так:
public class UserNotFoundException : ApplicationException
{
public UserNotFoundException() { }
public UserNotFoundException(string message) : base(message) { }
public UserNotFoundException(string message, Exception inner) : base(message, inner) { }
protected UserNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
Теперь при генерации исключения мы можем указать причину его возникновения более подробно:
throw new UserNotFoundException("User \"" + oldUserName + "\" not found in system");
Добавление дополнительных полей, их сериализация и десериализация
Допустим, мы хотим добавить в класс нашего исключения дополнительное поле, хранящее имя пользователя, которого хотелось найти, но в итоге он не был найден (из-за чего собственно и было сгенерировано исключение). Добавляем к классу исключения дополнительное string-поле:
Проблема в том, что данные из добавленного нами поля не будут сериализоваться и десериализоваться автоматически. Мы должны позаботиться о том, чтобы CLR сериализовала и десериализовала данные по нашему исключению корректно.
Для сериализации поля мы должны переопределить метод GetObjectData, описываемый интерфейсом ISerializable. Метод GetObjectData заполняет объект SerializationInfo данными для сериализации. Именно в SerializationInfo мы должны передать имя нашего поля и информацию, хранящуюся в нем:
Метод GetObjectData для базового класса нужно вызвать для того, чтобы добавить в SerializationInfo все поля нашего исключения по умолчанию (такие как Message, TargetSite, HelpLink и т.д.).
Десериализация проходит в схожем ключе. При десериализации будет вызван конструктор нашего исключения, принимающий SerializationInfo и StreamingContext. Наша задача – получить из SerializationInfo данные и записать их в созданное нами поле:
[Serializable]
public class UserNotFoundException : ApplicationException
{
private string _userNotFoundName;
public string UserNotFoundName
{
get
{
return _userNotFoundName;
}
set
{
_userNotFoundName = value;
}
}
public UserNotFoundException() { }
public UserNotFoundException(string message) : base(message) { }
public UserNotFoundException(string message, Exception inner) : base(message, inner) { }
protected UserNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
Проблема в том, что данные из добавленного нами поля не будут сериализоваться и десериализоваться автоматически. Мы должны позаботиться о том, чтобы CLR сериализовала и десериализовала данные по нашему исключению корректно.
Для сериализации поля мы должны переопределить метод GetObjectData, описываемый интерфейсом ISerializable. Метод GetObjectData заполняет объект SerializationInfo данными для сериализации. Именно в SerializationInfo мы должны передать имя нашего поля и информацию, хранящуюся в нем:
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("UserNotFoundName", this.UserNotFoundName);
}
Метод GetObjectData для базового класса нужно вызвать для того, чтобы добавить в SerializationInfo все поля нашего исключения по умолчанию (такие как Message, TargetSite, HelpLink и т.д.).
Десериализация проходит в схожем ключе. При десериализации будет вызван конструктор нашего исключения, принимающий SerializationInfo и StreamingContext. Наша задача – получить из SerializationInfo данные и записать их в созданное нами поле:
protected UserNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context)
{
if (info != null)
{
this._userNotFoundName = info.GetString("UserNotFoundName");
}
}
И последний штрих – добавление в XML-документацию (если вы, конечно, ее используете) нашего метода информации о том, что он может выбросить исключение определенного типа:
/// <exception cref="UserDefinedException.UserNotFoundException" />
Итак, наш user-defined exception готов к применению. Вы можете добавить к нему все что душе угодно: дополнительные поля, описывающие состояние исключения, содержащие дополнительную информацию и т.д.
P.S.: Добавил информацию о том, как сериализовать и десериализовать дополнительные поля класса исключения. Подробности под спойлером «Добавление дополнительных полей, их сериализация и десериализация».
P.P.S: Благодарю за комментарии и здоровую критику. Тем, кто прочитал статью до конца — прочитайте также комментарии, там есть полезная информация.