Свой класс ошибок c

Время на прочтение
5 мин

Количество просмотров 52K

Для кого написана статья

Данная статья предназначена прежде всего для новичков в мире .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 мы сгенерируем созданный нами 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-поле:

[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: Благодарю за комментарии и здоровую критику. Тем, кто прочитал статью до конца — прочитайте также комментарии, там есть полезная информация.

Создание классов исключений

Последнее обновление: 30.12.2021

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

Допустим, у нас в программе будет ограничение по возрасту:

try
{
    Person person = new Person { Name = "Tom", Age = 17 };
}
catch (Exception ex)
{
    Console.WriteLine($"Ошибка: {ex.Message}");
}

class Person
{
    private int age;
    public string Name { get; set; } = "";
    public int Age
    {
        get => age;
        set
        {
            if (value < 18)
                throw new Exception("Лицам до 18 регистрация запрещена");
            else
                age = value;
        }
    }
}

В классе Person при установке возраста происходит проверка, и если возраст меньше 18, то выбрасывается исключение. Класс Exception принимает
в конструкторе в качестве параметра строку, которое затем передается в его свойство Message.

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

class PersonException : Exception
{
    public PersonException(string message)
        : base(message) { }
}

По сути класс кроме пустого конструктора ничего не имеет, и то в конструкторе мы просто обращаемся к конструктору базового класса
Exception, передавая в него строку message. Но теперь мы можем изменить класс Person, чтобы он выбрасывал исключение именно этого типа и
соответственно в основной программе обрабатывать это исключение:

try
{
    Person person = new Person { Name = "Tom", Age = 17 };
}
catch (PersonException ex)
{
    Console.WriteLine($"Ошибка: {ex.Message}");
}

class Person
{
    private int age;
    public string Name { get; set; } = "";
    public int Age
    {
        get => age;
        set
        {
            if (value < 18)
                throw new PersonException("Лицам до 18 регистрация запрещена");
            else
                age = value;
        }
    }
}

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

class PersonException : ArgumentException
{
	public PersonException(string message)
		: base(message)
	{ }
}

Каждый тип исключений может определять какие-то свои свойства. Например, в данном случае мы можем определить в классе свойство для хранения устанавливаемого значения:

class PersonException : ArgumentException
{
	public int Value { get;}
	public PersonException(string message, int val)
		: base(message)
	{
		Value = val;
	}
}

В конструкторе класса мы устанавливаем это свойство и при обработке исключения мы его можем получить:

try
{
    Person person = new Person { Name = "Tom", Age = 17 };
}
catch (PersonException ex)
{
    Console.WriteLine($"Ошибка: {ex.Message}");
    Console.WriteLine($"Некорректное значение: {ex.Value}");
}

class Person
{
    private int age;
    public string Name { get; set; } = "";
    public int Age
    {
        get => age;
        set
        {
            if (value < 18)
                throw new PersonException("Лицам до 18 регистрация запрещена", value);
            else
                age = value;
        }
    }
}

И в данном случае мы получим следующий консольный вывод:

Ошибка: Лицам до 18 регистрация запрещена
Некорректное значение: 17

Я создал свой класс MyException

class MyException: public std::exception
{
private:
    std::string msg;
public:
    MyException(std::string msg): std::exception(msg)
    {
        
    }
};

Пример взят из интернета, но у меня ругается компилятор (gcc) на строчку std::exception(msg)

note: candidate: 'constexpr std::exception::exception(const std::exception&)'
no matching function for call to 'std::exception::exception(std::__cxx11::string&)'
     MyException(std::string msg): std::exception(msg){
no known conversion for argument 1 from 'std::__cxx11::string' {aka 'std::__cxx11::basic_string<char>'} to 'const std::exception&'

После нашел другой пример

class MyException: public std::exception
{
private:
    
public:
    MyException(std::string msg);
    ~MyException() = default;
    const char* what() const noexcept override;
};

Тут все работает, но у меня есть пару вопросов:

  1. Почему я не могу реализовать первый пример, хотя у другого человека получилось? (c++11)
  2. Почему в строчке const char(указатель) what() const noexcept override; используется последовательность const noexcept override;? Почему я не могу написать просто noexcept override или просто override. Насколько я знаю noexcept не позволяет выбросить исключение. И почему используется два раза const? Мы же уже указали, что функция будет константная

Harry's user avatar

Harry

215k15 золотых знаков117 серебряных знаков228 бронзовых знаков

задан 19 апр 2022 в 12:52

Barev's user avatar

1

Дело в том, что по стандарту exception не имеет конструктора иного, кроме как по умолчанию.
введите сюда описание изображения

Так что формально ваш код

MyException(std::string msg): std::exception(msg)

ошибочен. Другое дело, что некоторые компиляторы от стандарта отходят и допускают

exception::exception(const string&)

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

ответ дан 19 апр 2022 в 13:32

Harry's user avatar

HarryHarry

215k15 золотых знаков117 серебряных знаков228 бронзовых знаков

9

В соседнем ответе объяснили, в чем ошибка. А теперь как чинить.

Да, можно положить в свой класс std::string с текстом ошибки, и перегрузить what(), чтобы он его возвращал. Но есть нюанс: стандартные исключения можно копировать, гарантированно не получая исключений (например от нехватки памяти) — у них внутри какой-то аналог std::shared_ptr<std::string>.

А ваше исключение при копировании сможет выбросить исключение (если new кинет исключение) — маловероятно, но все равно не по фен-шую.

Чтобы этого не произошло, можно не хранить строку самому, а унаследоваться от std::runtime_error или std::logic_error, и хранить строку в нем. Или, если совсем не хочется наследоваться, хранить один из этих классов в своем.

ответ дан 19 апр 2022 в 17:41

HolyBlackCat's user avatar

HolyBlackCatHolyBlackCat

25.6k3 золотых знака26 серебряных знаков38 бронзовых знаков

3

  1. Наверное вы не внимательно смотрели, или там была опечатка, потому
    что конструктор std::exception не принимает в аргумент std::string,
    а принимает только С_строку(в некоторых реализациях). Поэтому нужно ему передать именно
    такую строку. Вот, например, так:

    MyException(std::string msg) 
        : std::exception(msg.c_str())...
    
  2. спецификатор const noexcept override может быть только для
    функции члена. Это значит, что функция член определена и для
    константных объектов (то есть она не изменяет состояние объекта),
    не генерирует исключение(помощь компилятору) и переопределен в
    производном классе(то есть это виртуальная функция_член).

А второй констант относится к возвращаемому типу(возвращает указатель на константную строку.

ответ дан 19 апр 2022 в 13:11

AR Hovsepyan's user avatar

AR HovsepyanAR Hovsepyan

15.8k3 золотых знака13 серебряных знаков30 бронзовых знаков

5

Цитата
Сообщение от oleggy
Посмотреть сообщение

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

Плохая идея.
Потому что не информативная.

В чем киллер-фича исключений?
Эксепшен не просто уведомляет клиентский код о том, что что-то пошло не так.
Эксепшен информирует: что конкретно не так.

Рассмотрим пример классический ситуации, где используют исключения.
Итак, я хочу загрузить файл с диска.
Но файла на месте не оказалось.
Мало ли — может быть его ничайно удалили с диска.
Функция загрузки бросает эксепшен.

Клиентский код отлавливает этот эксепшен,
и выводит в консоль информацию о происшествии.

C++
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <stdexcept>
#include <cassert>
#include <fstream>
#include <string>
#include <vector>
 
using str_t = ::std::string;
using vec_t = ::std::vector<str_t>;
 
vec_t load(const std::string& path)
{
    const auto flags = ::std::ios::in;
 
    ::std::ifstream in(path, flags);
    if(!in)
        throw ::std::runtime_error("error(2): can`t open file: '" + path + "'");
 
    str_t line;
    vec_t lines;
    while(::std::getline(in, line))
    {
        assert(in);
        if(!in)
            throw ::std::runtime_error("error(2): can`t load line from: '" + path + "'");
 
        lines.emplace_back(::std::move(line));
    }
    return lines;
}
 
int main()
{
    try
    {
        std::cout << "main: begin...n";
        const auto lines = load("no_exist.txt");
        std::cout << "main: success!!!n";
        return 0;
    }
    catch(const std::exception& e)
    {
        std::cerr << "main(exception): " << e.what() << 'n';
    }
    catch(...)
    {
        std::cerr << "main(exception): unknownn";
    }
    return 1;
}

Код

main: begin...
main(exception): error(2): can`t open file: 'no_exist.txt'

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

При желании, в текст эксепшена можно запихнуть значение errno,
или код ::GetLastError()

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

Но как вы сможете сделать такую диагностику со своими свитч-кейсами?
Максимум на что способны ваши свитч-кейсы: «у меня тут что-то не работает».
Вот прям как

на Кибер-форуме

в детском садике.

А ты сиди потом и думай: что именно, где и почему там не заработало.
Как хоть файл то назывался, который не смог загрузиться?

И вот тут возникает закономерный вопрос:
А зачем вообще нужны такие костылье эксепшены,
по которым нефига не понятно, что именно пошло не так?

Добавлено через 4 минуты

Цитата
Сообщение от Croessmah
Посмотреть сообщение

https://m.habr.com/ru/post/340604/

На хабре в последнее время столько глупостей постят))

Пользовательские классы-исключения

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

И так, как мы помним, механизм генерации исключений нужен для информирования системы, о том, что в программе произошла некая исключительная ситуация (ошибка) и дальнейшее выполнение программы, без её обработки, невозможно! А объекты-исключения, который как бы «выбрасываются в эфир» оператором throw должны содержать информацию о произошедшей ошибке. И когда Вы разрабатываете свой специфический класс, Вам нужно позаботиться о генерации исключений в нештатных ситуациях, связанных с неправильным использованием этого класса. И вот тут начинается самое интересное…

Как мы помним, объект, который как бы «выбрасывается в эфир» оператором throw должен относиться к классу, который является прямым или косвенным наследником класса «Exception» (из пространства имен «System»), ну или вообще являться объектом этого класса. В примере из предыдущей статьи, мы поступали так:

//Конструктор
public Person(string aName, int anAge)
{
    name = aName;

    //Если указан отрицательный возраст
    if (anAge < 0)
    {
        throw new Exception("Отрицательный возраст");
        //Дальше, конструктор выполняться не будет...
    }

    age = anAge;
}

Строка генерации исключения выделена. Так поступать можно, но не совсем правильно! Почему? Да потому, что при возможности, нужно максимально конкретизировать произошедшую ошибку (чтобы в месте её обработки было легче понять причину возникновения). Это делается как минимум двумя способами:

  • генерацией ошибки (объекта) определенного типа (класса), а не обобщенного, такого как «Exception», что использовался в примере выше;
  • передачей в создаваемый объект-исключение дополнительной информации.

Как это делается на практике? Ну в нашем примере, причиной ошибки служило некорректное значение аргумента, по-этому вместо объекта класса «Exception» мы могли бы использовать объект стандартного класса «ArgumentException» (думаю, название класса, говорит само за себя) из пространства имен «System«. Выглядеть это могло бы так:

//Конструктор
public Person(string aName, int anAge)
{
    name = aName;

    //Если указан отрицательный возраст
    if (anAge < 0)
    {
        throw new ArgumentException("Отрицательный возраст");
        //Дальше, конструктор выполняться не будет...
    }

    age = anAge;
}

Среди стандартных классов C#, есть ряд классов, которые предназначены для описания часто встречаемых в программах ошибок. Одним из таких классов является класс «ArgumentException».

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

//Класс, для описания пользовательского типа ошибок
class PersonException : Exception //Используем наследование
{
    //Принимает сообщение с описание ошибки, и код ошибки
    public PersonException(string aMessage, int aCode)
        : base(aMessage) //Вызываем конструктор базового класса
    {
        errorCode = aCode;
    }

    //Возвращает код ошибки
    public int ErrorCode { get { return errorCode; } }

    //Код ошибки
    private int errorCode;
}

Как не сложно заметить, в примере приведенном выше, мы создали класс наследник класса «Exception» и расширили его полем «errorCode», которое хранит код ошибки. Так же, добавили свойство, возвращающее этот код, и создали конструктор, который принимает описание ошибки и её код. Причем, в конструкторе, мы вызываем конструктор базового класса, которому передаем сообщение об ошибке. Сгенерить исключение такого типа в конструкторе класса «Person» мы могли бы так:

//Конструктор
public Person(string aName, int anAge)
{
    name = aName;

    //Если указан отрицательный возраст
    if (anAge < 0)
    {
        throw new PersonException("Отрицательный возраст", 20);
        //Дальше, конструктор выполняться не будет...
    }

    age = anAge;
}

А обрабатывать так:

try
{
    Person somePerson = new Person("Иван", -21);
}
catch (PersonException ex)
{
    //Обработка ошибок
    Console.WriteLine("Произошла ошибка: " + ex.Message + "; с кодом: " + ex.ErrorCode);
}

Как видите, в блоке catch мы явно указали что он обрабатывает исключения типа «PersonException» и исключения других типов в него не попадут. В следующей статье, я хочу подробнее коснуться этой темы.

Понравилась статья? Поделить с друзьями:
  • Свои собственные это ошибка
  • Свои преступления он исполнял одинаково речевая ошибка
  • Свои ошибки нужно смывать кровью
  • Свои ошибки не замечают
  • Свои ошибки мы другим не прощаем