C exception свой класс ошибок

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

Я создал свой класс 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

В C++ различают ошибки времени компиляции и ошибки времени выполнения. Ошибки первого типа обнаруживает компилятор до запуска программы. К ним относятся, например, синтаксические ошибки в коде. Ошибки второго типа проявляются при запуске программы. Примеры ошибок времени выполнения: ввод некорректных данных, некорректная работа с памятью, недостаток места на диске и т. д. Часто такие ошибки могут привести к неопределённому поведению программы.

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

Коды возврата и исключения

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

int ReadAge() {
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        // Что вернуть в этом случае?
    }
    return age;
}

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

int main() {
    if (int age = ReadAge(); age == 0) {
        // Произошла ошибка
    } else {
        // Работаем с возрастом age
    }
}

Такая проверка неудобна. Более того, нет никакой гарантии, что в вызывающей функции программист вообще её напишет. Фактически мы тут выбрали некоторое значение функции (ноль), обозначающее ошибку. Это пример подхода к обработке ошибок через коды возврата. Другим примером такого подхода является хорошо знакомая нам функция main. Только она должна возвращать ноль при успешном завершении и что-либо ненулевое в случае ошибки.

Другим способом сообщить об обнаруженной ошибке являются исключения. С каждым сгенерированным исключением связан некоторый объект, который как-то описывает ошибку. Таким объектом может быть что угодно — даже целое число или строка. Но обычно для описания ошибки заводят специальный класс и генерируют объект этого класса:

#include <iostream>

struct WrongAgeException {
    int age;
};

int ReadAge() {
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        throw WrongAgeException(age);
    }
    return age;
}

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

Перехват исключения

Мы вызывали нашу функцию ReadAge из функции main. Обработать ошибку в месте вызова можно с помощью блока try/catch:

int main() {
    try {
        age = ReadAge();  // может сгенерировать исключение
        // Работаем с возрастом age
    } catch (const WrongAgeException& ex) {  // ловим объект исключения
        std::cerr << "Age is not correct: " << ex.age << "n";
        return 1;  // выходим из функции main с ненулевым кодом возврата
    }
    // ...
}

Мы знаем заранее, что функция ReadAge может сгенерировать исключение типа WrongAgeException. Поэтому мы оборачиваем вызов этой функции в блок try. Если происходит исключение, для него подбирается подходящий catch-обработчик. Таких обработчиков может быть несколько. Можно смотреть на них как на набор перегруженных функций от одного аргумента — объекта исключения. Выбирается первый подходящий по типу обработчик и выполняется его код. Если же ни один обработчик не подходит по типу, то исключение считается необработанным. В этом случае оно пробрасывается дальше по стеку — туда, откуда была вызвана текущая функция. А если обработчик не найдётся даже в функции main, то программа аварийно завершается.

Усложним немного наш пример, чтобы из функции ReadAge могли вылетать исключения разных типов. Сейчас мы проверяем только значение возраста, считая, что на вход поступило число. Но предположим, что поток ввода досрочно оборвался, или на входе была строка вместо числа. В таком случае конструкция std::cin >> age никак не изменит переменную age, а лишь возведёт специальный флаг ошибки в объекте std::cin. Наша переменная age останется непроинициализированной: в ней будет лежать неопределённый мусор. Можно было бы явно проверить этот флаг в объекте std::cin, но мы вместо этого включим режим генерации исключений при таких ошибках ввода:

int ReadAge() {
    std::cin.exceptions(std::istream::failbit);
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        throw WrongAgeException(age);
    }
    return age;
}

Теперь ошибка чтения в операторе >> у потока ввода будет приводить к исключению типа std::istream::failure. Функция ReadAge его не обрабатывает. Поэтому такое исключение покинет пределы этой функции. Поймаем его в функции main:

int main() {
    try {
        age = ReadAge();  // может сгенерировать исключения разных типов
        // Работаем с возрастом age
    } catch (const WrongAgeException& ex) {
        std::cerr << "Age is not correct: " << ex.age << "n";
        return 1;
    } catch (const std::istream::failure& ex) {
        std::cerr << "Failed to read age: " << ex.what() << "n";
        return 1;
    } catch (...) {
        std::cerr << "Some other exceptionn";
        return 1;
    }
    // ...
}

При обработке мы воспользовались функцией ex.what у исключения типа std::istream::failure. Такие функции есть у всех исключений стандартной библиотеки: они возвращают текстовое описание ошибки.

Обратите внимание на третий catch с многоточием. Такой блок, если он присутствует, будет перехватывать любые исключения, не перехваченные ранее.

Исключения стандартной библиотеки

Функции и классы стандартной библиотеки в некоторых ситуациях генерируют исключения особых типов. Все такие типы выстроены в иерархию наследования от базового класса std::exception. Иерархия классов позволяет писать обработчик catch сразу на группу ошибок, которые представлены базовым классом: std::logic_error, std::runtime_error и т. д.

Вот несколько примеров:

  1. Функция at у контейнеров std::array, std::vector и std::deque генерирует исключение std::out_of_range при некорректном индексе.

  2. Аналогично, функция at у std::map, std::unordered_map и у соответствующих мультиконтейнеров генерирует исключение std::out_of_range при отсутствующем ключе.

  3. Обращение к значению у пустого объекта std::optional приводит к исключению std::bad_optional_access.

  4. Потоки ввода-вывода могут генерировать исключение std::ios_base::failure.

Исключения в конструкторах

В главе 3.1 мы написали класс Time. Этот класс должен был соблюдать инвариант на значение часов, минут и секунд: они должны были быть корректными. Если на вход конструктору класса Time передавались некорректные значения, мы приводили их к корректным, используя деление с остатком.

Более правильным было бы сгенерировать в конструкторе исключение. Таким образом мы бы явно передали сообщение об ошибке во внешнюю функцию, которая пыталась создать объект.

class Time {
private:
    int hours, minutes, seconds;

public:
    // Заведём класс для исключения и поместим его внутрь класса Time как в пространство имён
    class IncorrectTimeException {
    };

    Time::Time(int h, int m, int s) {
        if (s < 0 || s > 59 || m < 0 || m > 59 || h < 0 || h > 23) {
            throw IncorrectTimeException();
        }
        hours = h;
        minutes = m;
        seconds = s;
    }

    // ...
};

Генерировать исключения в конструкторах — совершенно нормальная практика. Однако не следует допускать, чтобы исключения покидали пределы деструкторов. Чтобы понять причины, посмотрим подробнее, что происходит при генерации исключения.

Свёртка стека

Вспомним класс Logger из предыдущей главы. Посмотрим, как он ведёт себя при возникновении исключения. Воспользуемся в этом примере стандартным базовым классом std::exception, чтобы не писать свой класс исключения.

#include <exception>
#include <iostream>

void f() {
    std::cout << "Welcome to f()!n";
    Logger x;
    // ...
    throw std::exception();  // в какой-то момент происходит исключение
}

int main() {
    try {
        Logger y;
        f();
    } catch (const std::exception&) {
        std::cout << "Something happened...n";
        return 1;
    }
}

Мы увидим такой вывод:

Logger(): 1
Welcome to f()!
Logger(): 2
~Logger(): 2
~Logger(): 1
Something happened...

Сначала создаётся объект y в блоке try. Затем мы входим в функцию f. В ней создаётся объект x. После этого происходит исключение. Мы должны досрочно покинуть функцию. В этот момент начинается свёртка стека (stack unwinding): вызываются деструкторы для всех созданных объектов в самой функции и в блоке try, как если бы они вышли из своей области видимости. Поэтому перед обработчиком исключения мы видим вызов деструктора объекта x, а затем — объекта y.

Аналогично, свёртка стека происходит и при генерации исключения в конструкторе. Напишем класс с полем Logger и сгенерируем нарочно исключение в его конструкторе:

#include <exception>
#include <iostream>

class C {
private:
    Logger x;

public:
    C() {
        std::cout << "C()n";
        Logger y;
        // ...
        throw std::exception();
    }

    ~C() {
        std::cout << "~C()n";
    }
};

int main() {
    try {
        C c;
    } catch (const std::exception&) {
        std::cout << "Something happened...n";
    }
}

Вывод программы:

Logger(): 1  // конструктор поля x
C()
Logger(): 2  // конструктор локальной переменной y
~Logger(): 2  // свёртка стека: деструктор y
~Logger(): 1  // свёртка стека: деструктор поля x
Something happened...

Заметим, что деструктор самого класса C не вызывается, так как объект в конструкторе не был создан.

Механизм свёртки стека гарантирует, что деструкторы для всех созданных автоматических объектов или полей класса в любом случае будут вызваны. Однако он полагается на важное свойство: деструкторы самих классов не должны генерировать исключений. Если исключение в деструкторе произойдёт в момент свёртки стека при обработке другого исключения, то программа аварийно завершится.

Пример с динамической памятью

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

void f() {
    Logger* ptr = new Logger();  // конструируем объект класса Logger в динамической памяти
    // ...
    g();  // вызываем какую-то функцию
    // ...
    delete ptr;  // вызываем деструктор и очищаем динамическую память
}

На первый взгляд кажется, что в этом коде нет ничего опасного: delete вызывается в конце функции. Однако функция g может сгенерировать исключение. Мы не перехватываем его в нашей функции f. Механизм свёртки уберёт со стека лишь сам указатель ptr, который является автоматической переменной примитивного типа. Однако он ничего не сможет сделать с объектом в памяти, на которую ссылается этот указатель. В логе мы увидим только вызов конструктора класса Logger, но не увидим вызова деструктора. Нам придётся обработать исключение вручную:

void f() {
    Logger* ptr = new Logger();
    // ...
    try {
        g();
    } catch (...) {  // ловим любое исключение
        delete ptr;  // вручную удаляем объект
        throw;  // перекидываем объект исключения дальше
    }
    // ...
    delete ptr;

}

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

Согласитесь, этот код очень далёк от совершенства. При непосредственной работе с объектами в динамической памяти нам приходится оборачивать в try/catch любую конструкцию, из которой может вылететь исключение. Понятно, что такой код чреват ошибками. В главе 3.6 мы узнаем, как с точки зрения C++ следует работать с такими ресурсами, как память.

Гарантии безопасности исключений

Предположим, что мы пишем свой класс-контейнер, похожий на двусвязный список. Наш контейнер позволяет добавлять элементы в хранилище и отдельно хранит количество элементов в некотором поле elementsCount. Один из инвариантов этого класса такой: значение elementsCount равно реальному числу элементов в хранилище.

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

template <typename T>
class List {
private:
    struct Node {  // узел двусвязного списка
        T element;
        Node* prev = nullptr;  // предыдущий узел
        Node* next = nullptr;  // следующий узел
    };

    Node* first = nullptr;  // первый узел списка
    Node* last = nullptr;  // последний узел списка
    int elementsCount = 0;

public:
    // ...

    size_t Size() const {
        return elementsCount;
    }

    void PushBack(const T& elem) {
        ++elementsCount;

        // Конструируем в динамической памяти новой узел списка
        Node* node = new Node(elem, last, nullptr);

        // Связываем новый узел с остальными узлами
        if (last != nullptr) {
            last->next = node;
        } else {
            first = node;
        }
        last = node;
    }
};

Не будем здесь рассматривать другие функции класса — конструкторы, деструктор, оператор присваивания… Рассмотрим функцию PushBack. В ней могут произойти такие исключения:

  1. Выражение new может сгенерировать исключение std::bad_alloc из-за нехватки памяти.

  2. Конструктор копирования класса T может сгенерировать произвольное исключение. Этот конструктор вызывается при инициализации поля element создаваемого узла в конструкторе класса Node. В этом случае new ведёт себя как транзакция: выделенная перед этим динамическая память корректно вернётся системе.

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

#include <iostream>

class C;  // какой-то класс

int main() {
    List<C> data;
    C element;

    try {
        data.PushBack(element);
    } catch (...) {  // не получилось добавить элемент
        std::cout << data.Size() << "n";  // внезапно 1, а не 0
    }

    // работаем дальше с data
}

Наша функция PushBack сначала увеличивает счётчик элементов, а затем выполняет опасные операции. Если происходит исключение, то в классе List нарушается инвариант: значение счётчика elementsCount перестаёт соответствовать реальности. Можно сказать, что функция PushBack не даёт гарантий безопасности.

Всего выделяют четыре уровня гарантий безопасности исключений (exception safety guarantees):

  1. Гарантия отсутствия сбоев. Функции с такими гарантиями вообще не выбрасывают исключений. Примерами могут служить правильно написанные деструктор и конструктор перемещения, а также константные функции вида Size.

  2. Строгая гарантия безопасности. Исключение может возникнуть, но от этого объект нашего класса не поменяет состояние: количество элементов останется прежним, итераторы и ссылки не будут инвалидированы и т. д.

  3. Базовая гарантия безопасности. При исключении состояние объекта может поменяться, но оно останется внутренне согласованным, то есть, инварианты будут соблюдаться.

  4. Отсутсвие гарантий. Это довольно опасная категория: при возникновении исключений могут нарушаться инварианты.

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

Переместим в нашей функции PushBack изменение счётчика в конец:

    void PushBack(const T& elem) {
        Node* node = new Node(elem, last, nullptr);

        if (last != nullptr) {
            last->next = node;
        } else {
            first = node;
        }
        last = node;

        ++elementsCount;  // выполнится только если раньше не было исключений
    }

Теперь такая функция соответствует строгой гарантии безопасности.

В документации функций из классов стандартной библиотеки обычно указано, какой уровень гарантии они обеспечивают. Рассмотрим, например, гарантии безопасности класса std::vector.

  • Деструктор, функции empty, size, capacity, а также clear предоставляют гарантию отсутствия сбоев.

  • Функции push_back и resize предоставляют строгую гарантию.

  • Функция insert предоставляет лишь базовую гарантию. Можно было бы сделать так, чтобы она предоставляла строгую гарантию, но за это пришлось бы заплатить её эффективностью: при вставке в середину вектора пришлось бы делать реаллокацию.

Функции класса, которые гарантируют отсутсвие сбоев, следует помечать ключевым словом noexcept:

class C {
public:
    void f() noexcept {
        // ...
    }
};

С одной стороны, эта подсказка позволяет компилятору генерировать более эффективный код. С другой — эффективно обрабатывать объекты таких классов в стандартных контейнерах. Например, std::vector<C> при реаллокации будет использовать конструктор перемещения класса C, если он помечен как noexcept. В противном случае будет использован конструктор копирования, который может быть менее эффективен, но зато позволит обеспечить строгую гарантию безопасности при реаллокации.

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

Последнее обновление: 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

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

При разработке программного обеспечения в C# и не только, довольно часто встроенных типов исключений бывает не достаточно. Например, при взаимодействии с каким-либо API web-сервиса вам может понадобиться не только предоставить пользователю сообщение об ошибке, но и передать её код, в соответствии с документацией по API сервиса, какую-то служебную информацию и так далее. И здесь нам уже вряд ли хватить стандартных возможностей того же класса Exception — потребуется создать собственный класс исключений, который будет «заточен» на работу, например, с конкретными API. О том, как создавать собственные классы исключений мы сегодня и поговорим.

Пример создания собственного класса исключений

Как было сказано вые, стандартных классов исключений в C# может не всегда хватать. Например, мы хотим ограничить возраст пользователя и сделать так, чтобы зарегистрироваться могли только люди старше 18 лет. Используя класс Exception, мы можем написать следующий класс:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Person p = new Person { Name = "Tom", Age = 17 };
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Ошибка: {ex.Message}");
        }
        Console.Read();
    }
}
class Person
{
    private int age;
    public string Name { get; set; }
    public int Age
    {
        get { return age; }
        set
        {
            if (value < 18)
            {
                throw new Exception("Лицам до 18 регистрация запрещена");
            }
            else
            {
                age = value;
            }
        }
    }
}

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

Технически, в представленном коде нет ошибок и цель достигнута — как только возраст оказывается менее 18 лет, то сразу генерируется исключение. Однако, в таких случаях удобнее использовать собственный класс исключений. Ответ на вопрос «Почему?» окажется на поверхности, если попытаться ответить на такой вопрос: что будет с кодом нашей программы, если мы захотим ограничить возраст не 18-ю годами, а, скажем, 40? Как минимум, нам придётся менять сообщение об ошибке. А что, если исключение будет генерироватьс не в одном, а в 100 местах в программе? Собственный класс исключений позволяет нам в этом случае:

  1. сделать код программы более универсальным и повысить его поддержку;
  2. сделать код программы более удобным для других разработчиков, так как, согласитесь, что тип исключений, например, AgeException говорит нам об ошибке больше, чем просто Exception.

Таким образом, собственный класс исключений можно создать таким:

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

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

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

В свойстве Value мы можем хранить, например, значение возраста и, при необходимости, его получать.

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

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

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

    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Person p = new Person { Name = "Tom", Age = 17 };
            }
            catch (AgeException ex)
            {
                Console.WriteLine("Ошибка: " + ex.Message);
            }
            Console.Read();
        }
    }

    class Person
    {
        public string Name;
        private int age;
        public int Age
        {
            get { return age; }
            set
            {
                if (value < 18)
                    throw new AgeException(value, 18);
                else
                    age = value;
            }
        }
    }

    class AgeException : Exception
    {
        public int value;
        public AgeException(int value, int minValue) : base($"Лицам до {minValue} регистрация запрещена. Указан возраст {value}")
        {
            this.value = value;
        }
    }
}

В этом примере мы уже генерируем собственное исключение типа AgeException в конструктор которого передаем два числа — возраст, который мы пробовали указать в классе Person и минимально допустимый возраст, то есть 18 лет. Исходя из полученных значений мы создаем строку сообщения, которую и передаем в конструктор базового класса.

Итого

Создание собственных классов исключений в C# позволяет, с одной стороны, сделать код программы более читабельным и понятным, а, с другой стороны, используя все возможности наследования, повысить информативность генерируемых исключений. Классы исключений могут наследоваться не только от базового класса Exception, но и, в принципе, от любых известных классов исключений и переопределять их виртуальные методы и свойства.

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

ГЛАВА 13. Обработка исключительных ситуаций

Исключительная ситуация, или просто исключение,
происходит во время выполнения. Используя под­
систему обработки исключительных ситуаций в С#,
можно обрабатывать структурированным и контроли­
руемым образом ошибки, возникающие при выполнении
программы. Главное преимущество обработки исключи­
тельных ситуаций заключается в том, что она позволяет ав­
томатизировать получение большей части кода, который
раньше приходилось вводить в любую крупную програм­
му вручную для обработки ошибок. Так, если программа
написана на языке программирования без обработки ис­
ключительных ситуаций, то при неудачном выполнении
методов приходится возвращать коды ошибок, которые не­
обходимо проверять вручную при каждом вызове метода.
Это не только трудоемкий, но и чреватый ошибками про­
цесс. Обработка исключительных ситуаций рационализи­
рует весь процесс обработки ошибок, позволяя определить
в программе блок кода, называемый обработчиком исклю­
чений и выполняющийся автоматически, когда возникает
ошибка. Эго избавляет от необходимости проверять вруч­
ную, насколько удачно или неудачно завершилась конкрет­
ная операция либо вызов метода. Если возникнет ошибка,
она будет обработана соответствующим образом обработ­
чиком ошибок.

Обработка исключительных ситуаций важна еще и по­
тому, что в С# определены стандартные исключения для
типичных программных ошибок, например деление на
нуль или выход индекса за границы массива. Для реаги­
рования на подобные ошибки в программе должно быть
организовано отслеживание и обработка соответствующих
исключительных ситуаций. Ведь в конечном счете для успешного программирования
на C# необходимо научиться умело пользоваться подсистемой обработки исключи­
тельных ситуаций.

Класс System.Exception

В C# исключения представлены в виде классов. Все классы исключений должны
быть производными от встроенного в C# класса Exception, являющегося частью про­
странства имен System. Следовательно, все исключения являются подклассами класса
Exception.

К числу самых важных подклассов Exception относится класс SystemException.
Именно от этого класса являются производными все исключения, генерируемые испол­
няющей системой C# (т.е. системой CLR). Класс SystemException ничего не добавляет
к классу Exception, а просто определяет вершину иерархии стандартных исключений.

В среде .NET Framework определено несколько встроенных исключений, являю­
щихся производными от класса SystemException. Например, при попытке выпол­
нить деление на нуль генерируется исключение DivideByZeroException. Как будет
показано далее в этой главе, в C# можно создавать собственные классы исключений,
производные от класса Exception.

Основы обработки исключительных ситуаций

Обработка исключительных ситуаций в C# организуется с помощью четырех клю­
чевых слов: try, catch, throw и finally. Они образуют взаимосвязанную подсистему,
в которой применение одного из ключевых слов подразумевает применение другого.
На протяжении всей этой главы назначение и применение каждого из упомянутых
выше ключевых слов будет рассмотрено во всех подробностях. Но прежде необходимо
дать общее представление о роли каждого из них в обработке исключительных ситуа­
ций. Поэтому ниже кратко описан принцип их действия.

Операторы программы, которые требуется контролировать на появление исключе­
ний, заключаются в блок try. Если внутри блока try возникает исключительная ситуа­
ция, генерируется исключение. Это исключение может быть перехвачено и обработано
каким-нибудь рациональным способом в коде программы с помощью оператора, обо­
значаемого ключевым словом catch. Исключения, возникающие на уровне системы,
генерируются исполняющей системой автоматически. А для генерирования исключе­
ний вручную служит ключевое слово throw. Любой код, который должен быть непре­
менно выполнен после выхода из блока try, помещается в блок finally.

Применение пары ключевых слов try и catch

Основу обработки исключительных ситуаций в C# составляет пара ключевых слов
try и catch. Эти ключевые слова действуют совместно и не могут быть использованы
порознь. Ниже приведена общая форма определения блоков try/catch для обработ­
ки исключительных ситуаций:

try {
    // Блок кода, проверяемый на наличие ошибок.
}
catch (ExcepType1 exOb) {
    // Обработчик исключения типа ExcepTypel.
}
catch (ExcepType2 exOb) {
    // Обработчик исключения типа ExcepType2.
}

где ЕхсерТуре — это тип возникающей исключительной ситуации. Когда исключение
генерируется оператором try, оно перехватывается составляющим ему пару опера­
тором catch, который затем обрабатывает это исключение. В зависимости от типа
исключения выполняется и соответствующий оператор catch. Так, если типы гене­
рируемого исключения и того, что указывается в операторе catch, совпадают, то вы­
полняется именно этот оператор, а все остальные пропускаются. Когда исключение
перехватывается, переменная исключения exOb получает свое значение.

На самом деле указывать переменную ехОb необязательно. Так, ее необязательно
указывать, если обработчику исключений не требуется доступ к объекту исключения,
что бывает довольно часто. Для обработки исключения достаточно и его типа. Именно
поэтому во многих примерах программ, приведенных в этой главе, переменная ехОb
опускается.

Следует, однако, иметь в виду, что если исключение не генерируется, то блок опера­
тора try завершается как обычно, и все его операторы catch пропускаются. Выполне­
ние программы возобновляется с первого оператора, следующего после завершающе­
го оператора catch. Таким образом, оператор catch выполняется лишь в том случае,
если генерируется исключение.

Простой пример обработки исключительной ситуации

Рассмотрим простой пример, демонстрирующий отслеживание и перехватывание
исключения. Как вам должно быть уже известно, попытка индексировать массив за его
границами приводит к ошибке. Когда возникает подобная ошибка, система CLR гене­
рирует исключение IndexOutOfRangeException, которое определено как стандарт­
ное для среды .NET Framework. В приведенной ниже программе такое исключение
генерируется намеренно и затем перехватывается.

// Продемонстрировать обработку исключительной ситуации.
using System;

class ExcDemol {
    static void Main() {
        int[] nums = new int[4];

        try {
            Console.WriteLine("До генерирования исключения.");

            // Сгенерировать исключение в связи с выходом индекса за границы массива.
            for(int i=0; i < 10; i++) {
                nums[i] = i;
                Console.WriteLine("nums[(0)]: {1}", i, nums[i]);
            }
            Console.WriteLine("He подлежит выводу");
        }
        catch (IndexOutOfRangeException) {
            // Перехватить исключение.
            Console.WriteLine("Индекс вышел за границы массива!");
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

При выполнении этой программы получается следующий результат.

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Индекс вышел за границы массива!
После блока перехвата исключения.

В данном примере массив nums типа int состоит из четырех элементов. Но в цикле
for предпринимается попытка проиндексировать этот массив от 0 до 9, что и приво­
дит к появлению исключения IndexOutOfRangeException, когда происходит обра­
щение к элементу массива по индексу 4.

Несмотря на всю свою краткость, приведенный выше пример наглядно демон­
стрирует ряд основных моментов процесса обработки исключительных ситуаций.
Во-первых, код, который требуется контролировать на наличие ошибок, содержится в
блоке try. Во-вторых, когда возникает исключительная ситуация (в данном случае —
при попытке проиндексировать массив nums за его границами в цикле for), в блоке
try генерируется исключение, которое затем перехватывается в блоке catch. В этот
момент выполнение кода в блоке try завершается и управление передается блоку
catch. Это означает, что оператор catch не вызывается специально, а выполнение
кода переходит к нему автоматически. Следовательно, оператор, содержащий метод
WriteLine() и следующий непосредственно за циклом for, где происходит выход
индекса за границы массива, вообще не выполняется. А в задачу обработчика исклю­
чений входит исправление ошибки, приведшей к исключительной ситуации, чтобы
продолжить выполнение программы в нормальном режиме.

Обратите внимание на то, что в операторе catch указан только тип исключения
(в данном случае — IndexOutOfRangeException), а переменная исключения отсут­
ствует. Как упоминалось ранее, переменную исключения требуется указывать лишь
в том случае, если требуется доступ к объекту исключения. В ряде случаев значение
объекта исключения может быть использовано обработчиком исключений для по­
лучения дополнительной информации о самой ошибке, но зачастую для обработки
исключительной ситуации достаточно просто знать, что она произошла. Поэтому
переменная исключения нередко отсутствует в обработчиках исключений, как в рас­
сматриваемом здесь примере.

Как пояснялось ранее, если исключение не генерируется в блоке try, то блок catch
не выполняется, а управление программой передается оператору, следующему после
блока catch. Для того чтобы убедиться в этом, замените в предыдущем примере про­
граммы строку кода

for(int i=0; i < 10; i++) {

на строку

for(int i=0; i < nums.Length; i++) {

Теперь индексирование массива не выходит за его границы в цикле for. Следова­
тельно, никакого исключения не генерируется и блок catch не выполняется.

Второй пример обработки исключительной ситуации

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

В качестве еще одного примера рассмотрим следующую программу, где блок try
помещается в методе Main(). Из этого блока вызывается метод GenException(), в ко­
тором и генерируется исключение IndexOutOfRangeException. Это исключение не
перехватывается методом GenException(). Но поскольку метод GenException() вы­
зывается из блока try в методе Main(), то исключение перехватывается в блоке catch,
связанном непосредственно с этим блоком try.

/* Исключение может быть сгенерировано одним методом
и перехвачено другим. */
using System;

class ExcTest {
    // Сгенерировать исключение.
    public static void GenException() {
        int[] nums = new int[4];

        Console.WriteLine("До генерирования исключения.");

        // Сгенерировать исключение в связи с выходом индекса за границы
        массива.
        for(int i=0; i < 10; i++) {
            nums[i] = i;
            Console.WriteLine("nums [{0}] : {1}", i, nums[i]);
        }

        Console.WriteLine("He подлежит выводу");
    }
}

class ExcDemo2 {
    static void Main() {
        try {
            ExcTest.GenException();
        }
        catch (IndexOutOfRangeException) {
            // Перехватить исключение.
            Console.WriteLine("Индекс вышел за границы массива!");
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

Выполнение этой программы дает такой же результат, как и в предыдущем
примере.

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Индекс вышел за границы массива!
После блока перехвата исключения.

Как пояснялось выше, метод GenException() вызывается из блока try, и поэтому
генерируемое им исключение перехватывается не в нем, а в блоке catch внутри мето­
да Main(). А если бы исключение перехватывалось в методе GenException(), оно не
было бы вообще передано обратно методу Main().

Последствия неперехвата исключений

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

// Предоставить исполняющей системе C# возможность самой обрабатывать ошибки.
using System;

class NotHandled {
    static void Main() {
        int[] nums = new int[4];

        Console.WriteLine("До генерирования исключения.");

        // Сгенерировать исключение в связи с выходом индекса за границы массива.
        for(int i=0; i < 10; i++) {
            nums[i] = i;
            Console.WriteLine("nums[{0}]: {1}", i, nums[i]);
        }
    }
}

Когда возникает ошибка индексирования массива, выполнение программы преры­
вается и выдается следующее сообщение об ошибке.

Необработанное исключение: System.IndexOutOfRangeException:
        Индекс находился вне границ массива.
    в NotHandled.Main() в <имя_файла>:строка 16

Это сообщение уведомляет об обнаружении в методе NotHandled.Main() необра­
ботанного исключения типа System.IndexOutOfRangeException, которое связано
с выходом индекса за границы массива.

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

Как упоминалось ранее, тип генерируемого исключения должен соответствовать
типу, указанному в операторе catch. В противном случае исключение не будет пере­
хвачено. Например, в приведенной ниже программе предпринимается попытка пере­
хватить ошибку нарушения границ массива в блоке catch, реагирующем на исключе­
ние DivideByZeroException, связанное с делением на нуль и являющееся еще одним
стандартным исключением. Когда индексирование массива выходит за его границы,
генерируется исключение IndexOutOfRangeException, но оно не будет перехвачено
блоком catch, что приведет к аварийному завершению программы.

// Не сработает!
using System;

class ExcTypeMismatch {
    static void Main() {
        int[] nums = new int[4];
        try {
            Console.WriteLine("До генерирования исключения.");
            // Сгенерировать исключение в связи с выходом индекса за границы массива.
            for(int i=0; i < 10; i++) {
                nums[i] = i;
                Console.WriteLine("nums[{0}]: {1}", i, nums[i]);
            }
            Console.WriteLine("He подлежит выводу");
        }
        /* Если перехват рассчитан на исключение DivideByZeroException,
        то перехватить ошибку нарушения границ массива не удастся. */
        catch (DivideByZeroException) {
            // Перехватить исключение.
            Console.WriteLine("Индекс вышел за границы массива!");
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

Вот к какому результату приводит выполнение этой программы.

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Необработанное исключение: System.IndexOutOfRangeException:
        Индекс находился вне границ массива
    в ExcTypeMismatch.Main() в <имя_файла>:строка 18

Как следует из приведенного выше результата, в блоке catch, реагирующем
на исключение DivideByZeroException, не удалось перехватить исключение
IndexOutOfRangeException.

Обработка исключительных ситуаций — “изящный” способ устранения программных ошибок

Одно из главных преимуществ обработки исключительных ситуаций заключается
в том, что она позволяет вовремя отреагировать на ошибку в программе и затем про­
должить ее выполнение. В качестве примера рассмотрим еще одну программу, в кото­
рой элементы одного массива делятся на элементы другого. Если при этом происходит
деление на нуль, то генерируется исключение DivideByZeroException. Обработка
подобной исключительной ситуации заключается в том, что программа уведомляет
об ошибке и затем продолжает свое выполнение. Таким образом, попытка деления
на нуль не приведет к аварийному завершению программы из-за ошибки при ее вы­
полнении. Вместо этого ошибка обрабатывается «изящно», не прерывая выполнение
программы.

// Изящно обработать исключительную ситуацию и продолжить выполнение программы.
using System;

class ExcDemo3 {
    static void Main() {
        int[] numer = { 4, 8, 16, 32, 64, 128 };
        int[] denom = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i < numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch (DivideByZeroException) {
                // Перехватить исключение.
                Console.WriteLine("Делить на нуль нельзя!");
            }
        }
    }
}

Ниже приведен результат выполнения этой программы.

4/2 равно 2
Делить на нуль нельзя!
16/4 равно 4
32/4 равно 8
Делить на нуль нельзя!
128 / 8 равно 16

Из данного примера следует еще один важный вывод: как только исключение обра­
ботано, оно удаляется из системы. Поэтому в приведенной выше программе проверка
ошибок в блоке try начинается снова на каждом шаге цикла for, при условии, что все
предыдущие исключительные ситуации были обработаны. Это позволяет обрабаты­
вать в программе повторяющиеся ошибки.

Применение нескольких операторов catch

С одним оператором try можно связать несколько операторов catch. И на прак­
тике это делается довольно часто. Но все операторы catch должны перехватывать ис­
ключения разного типа. В качестве примера ниже приведена программа, в которой
перехватываются ошибки выхода за границы массива и деления на нуль.

// Использовать несколько операторов catch.
using System;

class ExcDemo4 {
    static void Main() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i < numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch (DivideByZeroException) {
                Console.WriteLine("Делить на нуль нельзя!");
            }
            catch (IndexOutOfRangeException) {
                Console.WriteLine("Подходящий элемент не найден.");
            }
        }
    }
}

Вот к какому результату приводит выполнение этой программы.

4/2 равно 2
Делить на нуль нельзя!
16/4 равно 4
32/4 равно 8
Делить на нуль нельзя!
128 / 8 равно 16
Подходящий элемент не найден.
Подходящий элемент не найден.

Как следует из приведенного выше результата, каждый оператор catch реагирует
только на свой тип исключения.

Вообще говоря, операторы catch выполняются по порядку их следования в про­
грамме. Но при этом выполняется только один блок catch, в котором тип исклю­
чения совпадает с типом генерируемого исключения. А все остальные блоки catch
пропускаются.

Перехват всех исключений

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

catch {
    // обработка исключений
}

С помощью такой формы создается «универсальный» обработчик всех исключе­
ний, перехватываемых в программе.

Ниже приведен пример такого «универсального» обработчика исключений. Об­
ратите внимание на то, что он перехватывает и обрабатывает оба исключения,
IndexOutOfRangeException и DivideByZeroException, генерируемых в программе.

// Использовать "универсальный" обработчик исключений.
using System;

class ExcDemo5 {
    static void Main() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = { 2, 0, 4, 4, 0, 8 );

        for(int i=0; i < numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch { // "Универсальный" перехват.
                Console.WriteLine("Возникла некоторая исключительная ситуация.");
            }
        }
    }
}

При выполнении этой программы получается следующий результат.

4/2 равно 2
Возникла некоторая исключительная ситуация.
16/4 равно 4
32/4 равно 8
Возникла некоторая исключительная ситуация.
128 / 8 равно 16
Возникла некоторая исключительная ситуация.
Возникла некоторая исключительная ситуация.

Применяя «универсальный» перехват, следует иметь в виду, что его блок должен
располагаться последним по порядку среди всех блоков catch.

ПРИМЕЧАНИЕ
В подавляющем большинстве случаев «универсальный» обработчик исключений (не при­
меняется. Как правило, исключения, которые могут быть сгенерированы в коде, обрабаты­
ваются по отдельности. Неправильное использование “универсального” обработчика может
привести к тому, что ошибки, перехватывавшиеся при тестировании программы, маскируют­
ся. Кроме того, организовать надлежащую обработку всех исключительных ситуаций в одном
обработчике не так-то просто. Иными словами, “универсальный» обработчик исключений
может оказаться пригодным лишь в особых случаях, например в инструментальном средстве
анализа кода во время выполнения.

Вложение блоков try

Один блок try может быть вложен в другой. Исключение, генерируемое во вну­
треннем блоке try и не перехваченное в соответствующем блоке catch, передается во
внешний блок try. В качестве примера ниже приведена программа, в которой исклю­
чение IndexOutOfRangeException перехватывается не во внутреннем, а во внешнем
блоке try.

// Использовать вложенный блок try.
using System;

class NestTrys {
    static void Main() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = ( 2, 0, 4, 4, 0, 8 );

        try { // внешний блок try
            for(int i=0; i < numer.Length; i++) {
                try { // вложенный блок try
                    Console.WriteLine(numer[i] + " / " +
                                    denom[i] + " равно " +
                                    numer[i]/denom[i]);
                }
                catch (DivideByZeroException) {
                    Console.WriteLine("Делить на нуль нельзя!");
                }
            }
        }
        catch (IndexOutOfRangeException) {
            Console.WriteLine("Подходящий элемент не найден.");
            Console.WriteLine("Неисправимая ошибка - программа прервана.");
        }
    }
}

Выполнение этой программы приводит к следующему результату.

4/2 равно 2
Делить на нуль нельзя!
16/4 равно 4
32/4 равно 8
Делить на нуль нельзя!
128 / 8 равно 16
Подходящий элемент не найден.
Неисправимая ошибка - программа прервана.

В данном примере исключение, обрабатываемое во внутреннем блоке try и связан­
ное с ошибкой из-за деления на нуль, не мешает дальнейшему выполнению програм­
мы. Но ошибка нарушения границ массива, обнаруживаемая во внешнем блоке try,
приводит к прерыванию программы.

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

Генерирование исключений вручную

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

где в качестве exceptOb должен быть обозначен объект класса исключений, произво­
дного от класса Exception.

Ниже приведен пример программы, в которой демонстрируется применение опе­
ратора throw для генерирования исключения DivideByZeroException.

// Сгенерировать исключение вручную.
using System;

class ThrowDemo {
    static void Main() {
        try {
            Console.WriteLine("До генерирования исключения.");
            throw new DivideByZeroException();
        }
        catch (DivideByZeroException) {
            Console.WriteLine("Исключение перехвачено.");
        }
        Console.WriteLine("После пары операторов try/catch.");
    }
}

Вот к какому результату приводит выполнение этой программы.

До генерирования исключения.
Исключение перехвачено.
После пары операторов try/catch.

Обратите внимание на то, что исключение DivideByZeroException было сге­
нерировано с использованием ключевого слова new в операторе throw. Не следует
забывать, что в данном случае генерируется конкретный объект, а следовательно, он
должен быть создан перед генерированием исключения. Это означает, что сгенериро­
вать исключение только по его типу нельзя. В данном примере для создания объекта
DivideByZeroException был автоматически вызван конструктор, используемый по
умолчанию, хотя для генерирования исключений доступны и другие конструкторы.

Повторное генерирование исключений

Исключение, перехваченное в одном блоке catch, может быть повторно сгенери­
ровано в другом блоке, чтобы быть перехваченным во внешнем блоке catch. Наиболее
вероятной причиной для повторного генерирования исключения служит предоставле­
ние доступа к исключению нескольким обработчикам. Допустим, что один обработчик
оперирует каким-нибудь одним аспектом исключения, а другой обработчик — другим
его аспектом. Для повторного генерирования исключения достаточно указать опера­
тор throw без сопутствующего выражения, как в приведенной ниже форме.

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

В приведенном ниже примере программы демонстрируется повтор­
ное генерирование исключения. В данном случае генерируется исключение
IndexOutOfRangeException.

// Сгенерировать исключение повторно.
using System;

class Rethrow {
    public static void GenException() {
        // Здесь массив numer длиннее массива denom.
        int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int[] denom = { 2, 0, 4, 4, 0, 8 };

        for(int i=0; i<numer.Length; i++) {
            try {
                Console.WriteLine(numer[i] + " / " +
                                denom[i] + " равно " +
                                numer[i]/denom[i]);
            }
            catch (DivideByZeroException) {
                Console.WriteLine("Делить на нуль нельзя!");
            }
            catch (IndexOutOfRangeException) {
                Console.WriteLine("Подходящий элемент не найден.");
                throw; // сгенерировать исключение повторно
            }
        }
    }
}

class RethrowDemo {
    static void Main() {
        try {
            Rethrow.GenException();
        }
        catch(IndexOutOfRangeException) {
            // перехватить исключение повторно
            Console.WriteLine("Неисправимая ошибка - программа прервана.");
        }
    }
}

В этом примере программы ошибки из-за деления на нуль обрабатываются локаль­
но в методе GenException(), но ошибка выхода за границы массива генерируется
повторно. В данном случае исключение IndexOutOfRangeException обрабатывается
в методе Main().

Использование блока finally

Иногда требуется определить кодовый блок, который будет выполняться после вы­
хода из блока try/catch. В частности, исключительная ситуация может возникнуть
в связи с ошибкой, приводящей к преждевременному возврату из текущего метода.
Но в этом методе мог быть открыт файл, который нужно закрыть, или же установлено
сетевое соединение, требующее разрывания. Подобные ситуации нередки в програм­
мировании, и поэтому для их разрешения в C# предусмотрен удобный способ: вос­
пользоваться блоком finally.

Для того чтобы указать кодовый блок, который должен выполняться после блока
try/catch, достаточно вставить блок finally в конце последовательности операторов
try/catch. Ниже приведена общая форма совместного использования блоков try/
catch и finally.

try {
    // Блок кода, предназначенный для обработки ошибок.
}
catch (ExcepType1 exOb) {
    // Обработчик исключения типа ExcepType1.
}
catch (ExcepType2 ехОb) {
    // Обработчик исключения типа ЕхсерТуре2.
}
finally {
    // Код завершения обработки исключений.
}

Блок finally будет выполняться всякий раз, когда происходит выход из блока try/
catch, независимо от причин, которые к этому привели. Это означает, что если блок
try завершается нормально или по причине исключения, то последним выполняется
код, определяемый в блоке finally. Блок finally выполняется и в том случае, если
любой код в блоке try или в связанных с ним блоках catch приводит к возврату из
метода.

Ниже приведен пример применения блока finally.

// Использовать блок finally.
using System;

class UseFinally {
    public static void GenException(int what) {
        int t;
        int[] nums = new int[2];

        Console.WriteLine("Получить " + what);
        try {
            switch(what) {
                case 0:
                    t = 10 / what; // сгенерировать ошибку из-за деления на нуль
                    break;
                case 1:
                    nums[4] = 4; // сгенерировать ошибку индексирования массива
                    break;
                case 2:
                    return; // возврат из блока try
            }
        }
        catch (DivideByZeroException) {
            Console.WriteLine("Делить на нуль нельзя!");
            return; // возврат из блока catch
        }
        catch (IndexOutOfRangeException) {
            Console.WriteLine("Совпадающий элемент не найден.");
        }
        finally {
            Console.WriteLine("После выхода из блока try.");
        }
    }
}

class FinallyDemo {
    static void Main() {
        for(int i=0; i < 3; i++) {
            UseFinally.GenException(i);
            Console.WriteLine();
        }
    }
}

Вот к какому результату приводит выполнение этой программы.

Получить 0
Делить на нуль нельзя
После выхода из блока try.

Получить 1
Совпадающий элемент не найден.
После выхода из блока try.

Получить 2
После выхода из блока try.

Как следует из приведенного выше результата, блок finally выполняется независи­
мо от причины выхода из блока try.

И еще одно замечание: с точки зрения синтаксиса блок finally следует после блока
try, и формально блоки catch для этого не требуются. Следовательно, блок finally
можно ввести непосредственно после блока try, опустив блоки catch. В этом случае
блок finally начнет выполняться сразу же после выхода из блока try, но исключения
обрабатываться не будут.

Подробное рассмотрение класса Exception

В приведенных выше примерах исключения только перехватывались, но никакой
существенной обработке они не подвергались. Как пояснялось выше, в операторе
catch допускается указывать тип и переменную исключения. Переменная получает
ссылку на объект исключения. Во всех исключениях поддерживаются члены, опреде­
ленные в классе Exception, поскольку все исключения являются производными от
этого класса. В этом разделе будет рассмотрен ряд наиболее полезных членов и кон­
структоров класса Exception и приведены конкретные примеры использования пере­
менной исключения.

В классе Exception определяется ряд свойств. К числу самых интересных отно­
сятся три свойства: Message, StackTrace и TargetSite. Все эти свойства доступны
только для чтения. Свойство Message содержит символьную строку, описывающую
характер ошибки; свойство StackTrace — строку с вызовами стека, приведшими к ис­
ключительной ситуации, а свойство TargetSite получает объект, обозначающий ме­
тод, сгенерировавший исключение.

Кроме того, в классе Exception определяется ряд методов. Чаще всего приходится
пользоваться методом ToString(), возвращающим символьную строку с описанием
исключения. Этот метод автоматически вызывается, например, при отображении ис­
ключения с помощью метода WriteLine().

Применение всех трех упомянутых выше свойств и метода из класса Exception
демонстрируется в приведенном ниже примере программы.

// Использовать члены класса Exception.
using System;
class ExcTest {
    public static void GenException() {
        int[] nums = new int[4];
        Console.WriteLine("До генерирования исключения.");
        // Сгенерировать исключение в связи с выходом за границы массива.
        for(int i=0; i < 10; i++) {
            nums[i] = i;
            Console.WriteLine("nums[{0}]: (1)", i, nums[i]);
        }
        Console.WriteLine("He подлежит выводу");
    }
}

class UseExcept {
    static void Main() {
        try {
            ExcTest.GenException();
        }
        catch (IndexOutOfRangeException exc) {
            Console.WriteLine("Стандартное сообщение таково: ");
            Console.WriteLine(exc); // вызвать метод ToString()
            Console.WriteLine("Свойство StackTrace: " + exc.StackTrace);
            Console.WriteLine("Свойство Message: " + exc.Message);
            Console.WriteLine("Свойство TargetSite: " + exc.TargetSite);
        }
        Console.WriteLine("После блока перехвата исключения.");
    }
}

При выполнении этой программы получается следующий результат.

До генерирования исключения.
nums[0]: 0
nums[1]: 1
nums[2]: 2
nums[3]: 3
Стандартное сообщение таково: System.IndexOutOfRangeException: Индекс находился
вне границ массива.
    в ExcTest.genException() в <имя_файла>:строка 15
    в UseExcept.Main() в <имя_файла>:строка 29
Свойство StackTrace:в ExcTest.genException() в <имя_файла>:строка 15
    в UseExcept.Main()в <имя_файла>:строка 29
Свойство Message: Индекс находился вне границ массива.
Свойство TargetSite: Void genException()
После блока перехвата исключения.

В классе Exception определяются четыре следующих конструктора.

public Exception()
public Exception(string сообщение)
public Exception(string сообщение, Exception внутреннее_исключение)
protected Exception(System.Runtime.Serialization.SerializationInfo информация,
    System.Runtime.Serialization.StreamingContext контекст)

Первый конструктор используется по умолчанию. Во втором конструкторе ука­
зывается строка сообщение, связанная со свойством Message, которое имеет отно­
шение к генерируемому исключению. В третьем конструкторе указывается так на­
зываемое внутреннее исключение. Этот конструктор используется в том случае, когда
одно исключение порождает другое, причем внутреннее_исключение обозначает
первое исключение, которое будет пустым, если внутреннее исключение отсутствует.
(Если внутреннее исключение присутствует, то оно может быть получено из свойства
InnerException, определяемого в классе Exception.) И последний конструктор об­
рабатывает исключения, происходящие дистанционно, и поэтому требует десериали­
зации.

Следует также заметить, что в четвертом конструкторе класса Exception типы
SerializationInfo и StreamingContext относятся к пространству имен System.
Runtime.Serialization.

Наиболее часто используемые исключения

В пространстве имен System определено несколько стандартных, встроенных ис­
ключений. Все эти исключения являются производными от класса SystemException,
поскольку они генерируются системой CLR при появлении ошибки во время выпол­
нения. В табл. 13.1 перечислены некоторые наиболее часто используемые стандартные
исключения.

Таблица 13.1. Наиболее часто используемые исключения, определенные в пространстве имен System

Исключение Значение
ArrayTypeMismatchException Тип сохраняемого значения несовместим с типом массива
DivideByZeroException Попытка деления на нуль
IndexOutOfRangeException Индекс оказался за границами массива
InvalidCastException Неверно выполнено динамическое приведение типов
OutOfMemoryException Недостаточно свободной памяти для дальнейшего выполнения программы. Это исключение может быть, например, сгенерировано, если для создания объекта с помощью оператора new не хватает памяти
OverflowException Произошло арифметическое переполнение
NullReferenceException Попытка использовать пустую ссылку, т.е. ссылку, которая не указывает ни на один из объектов

Большинство исключений, приведенных в табл. 13.1, не требует особых пояснений,
кроме исключения NullReferenceException. Это исключение генерируется при по­
пытке использовать пустую ссылку на несуществующий объект, например, при вы­
зове метода по пустой ссылке. Пустой называется такая ссылка, которая не указывает
ни на один из объектов. Для того чтобы создать такую ссылку, достаточно, например,
присвоить явным образом пустое значение переменной ссылочного типа, используя
ключевое слово null. Пустые ссылки могут также появляться и другими, менее оче­
видными путями. Ниже приведен пример программы, демонстрирующий обработку
исключения NullReferenceException.

// Продемонстрировать обработку исключения NullReferenceException.
using System;

class X {
    int x;

    public X(int a) {
        x = a;
    }

    public int Add(X o) {
        return x + o.x;
    }
}

// Продемонстрировать генерирование и обработку
// исключения NullReferenceException.
class NREDemo {
    static void Main() {
        X p = new X(10);
        X q = null; // присвоить явным образом пустое значение переменной q
        int val;
        try {
            val = p.Add(q); // эта операция приведет к исключительной ситуации
        } catch (NullReferenceException) {
            Console.WriteLine("Исключение NullReferenceException!");
            Console.WriteLine("Исправление ошибки...n");
            // А теперь исправить ошибку.
            q = new X(9);
            val = p.Add(q);
        }
        Console.WriteLine("Значение val равно {0}", val);
    }
}

Вот к какому результату приводит выполнение этой программы.

Исключение NullReferenceException!
Исправление ошибки...

Значение val равно 19

В приведенном выше примере программы создается класс X, в котором определя­
ются член х и метод Add(), складывающий значение члена х в вызывающем объекте
со значением члена х в объекте, передаваемом этому методу в качестве параметра. Оба
объекта класса X создаются в методе Main(). Первый из них (переменная р) инициа­
лизируется, а второй (переменная q) — нет. Вместо этого переменной q присваивается
пустое значение. Затем вызывается метод р.Add() с переменной q в качестве аргумен­
та. Но поскольку переменная q не ссылается ни на один из объектов, то при попытке
получить значение члена q.х генерируется исключение NullReferenceException.

Получение производных классов исключений

Несмотря на то что встроенные исключения охватывают наиболее распространен­
ные программные ошибки, обработка исключительных ситуаций в C# не ограничива­
ется только этими ошибками. В действительности одна из сильных сторон принятого
в C# подхода к обработке исключительных ситуаций состоит в том, что в этом языке
допускается использовать исключения, определяемые пользователем, т.е. тем, кто про­
граммирует на С#. В частности, такие специальные исключения можно использовать
для обработки ошибок в собственном коде, а создаются они очень просто. Для этого
достаточно определить класс, производный от класса Exception. В таких классах со­
всем не обязательно что-то реализовывать — одного только их существования в систе­
ме типов уже достаточно, чтобы использовать их в качестве исключений.

ПРИМЕЧАНИЕ
В прошлом специальные исключения создавались как производные от класса
Application.Exception, поскольку эта иерархия классов была первоначально зарезер­
вирована для исключений прикладного характера. Но теперь корпорация Microsoft не реко­
мендует этого делать, а вместо этого получать исключения, производные от класса Exception.
Именно по этой причине данный подход и рассматривается в настоящей книге.

Создаваемые пользователем классы будут автоматически получать свойства и мето­
ды, определенные в классе Exception и доступные для них. Разумеется, любой из этих
членов класса Exception можно переопределить в создаваемых классах исключений.
Когда создается собственный класс исключений, то, как правило, желательно, что­
бы в нем поддерживались все конструкторы, определенные в классе Exception. В про­
стых специальных классах исключений этого нетрудно добиться, поскольку для этого
достаточно передать подходящие аргументы соответствующему конструктору класса
Exception, используя ключевое слово base. Но формально нужно предоставить толь­
ко те конструкторы, которые фактически используются в программе.

Рассмотрим пример программы, в которой используется исключение специального
типа. Напомним, что в конце главы 10 был разработан класс RangeArray, поддержи­
вающий одномерные массивы, в которых начальный и конечный индексы определяют­
ся пользователем. Так, например, вполне допустимым считается массив, индексируе­
мый в пределах от -5 до 27. Если же индекс выходил за границы массива, то для обра­
ботки этой ошибки в классе RangeArray была определена специальная переменная.
Такая переменная устанавливалась и проверялась после каждой операции обращения
к массиву в коде, использовавшем класс RangeArray. Безусловно, такой подход к об­
работке ошибок «неуклюж» и чреват дополнительными ошибками. В приведенном
ниже улучшенном варианте класса RangeArray обработка ошибок нарушения границ
массива выполняется более изящным и надежным способом с помощью специально
генерируемого исключения.

// Использовать специальное исключение для обработки
// ошибок при обращении к массиву класса RangeArray.
using System;

// Создать исключение для класса RangeArray.
class RangeArrayException : Exception {
/* Реализовать все конструкторы класса Exception. Такие конструкторы просто
реализуют конструктор базового класса. А поскольку класс исключения
RangeArrayException ничего не добавляет к классу Exception, то никаких
дополнительных действий не требуется. */
public RangeArrayException() : base)) { }

public RangeArrayException(string str) : base(str) { }

public RangeArrayException(
string str, Exception inner) : base (str, inner) { }

protected RangeArrayException(
System.Runtime.Serialization.SerializationInfo si,
System.Runtime.Serialization.StreamingContext sc) :
base(si, sc) { }

// Переопределить метод ToString() для класса исключения RangeArrayException.
public override string ToString() {
    return Message;
}

}

// Улучшенный вариант класса RangeArray.
class RangeArray {
// Закрытые данные.
int[] a; // ссылка на базовый массив
int lowerBound; // наименьший индекс
int upperBound; // наибольший индекс

// Автоматически реализуемое и доступное только для чтения свойство Length.
public int Length { get; private set; }
// Построить массив по заданному размеру

public RangeArray(int low, int high) {
    high++;
    if(high <= low) {
        throw new RangeArrayException("Нижний индекс не меньше верхнего.");
    }
    а = new int[high - low];
    Length = high - low;
    lowerBound = low;
    upperBound = --high;
}

// Это индексатор для класса RangeArray.
public int this[int index] {
    // Это аксессор get.
    get {
        if(ok(index)) {
            return a[index - lowerBound];
        } else {
            throw new RangeArrayException("Ошибка нарушения границ.");
        }
    }
    // Это аксессор set.
    set {
        if(ok(index)) {
            a[index - lowerBound] = value;
        }
        else throw new RangeArrayException("Ошибка нарушения границ.");
    }
}

// Возвратить логическое значение true, если
// индекс находится в установленных границах.
private bool ok(int index) {
    if(index >= lowerBound S index <= upperBound) return true;
    return false;
}

}

// Продемонстрировать применение массива с произвольно
// задаваемыми пределами индексирования.
class RangeArrayDemo {
static void Main() {
try {
RangeArray ra = new RangeArray(-5, 5);
RangeArray ra2 = new RangeArray(1, 10);

        // Использовать объект ra в качестве массива.
        Console.WriteLine("Длина массива ra: " + ra.Length);
        for(int i = -5; i <= 5; i++)
            ra[i] = i;

        Console.Write("Содержимое массива ra: ");
        for(int i = -5; i <= 5; i++)
            Console.Write(ra[i] + " ");

        Console.WriteLine("n");

        // Использовать объект ra2 в качестве массива.
        Console.WriteLine("Длина массива ra2: " + ra2.Length);
        for(int i = 1; i <= 10; i++)
            ra2[i] = i;

        Console.Write("Длина массива ra2: ");
        for(int i = 1; i <= 10; i++)
            Console.Write(ra2[i] + " ");

        Console.WriteLine("n");
    } catch (RangeArrayException exc) {
        Console.WriteLine(exc);
    }

    // А теперь продемонстрировать обработку некоторых ошибок.
    Console.WriteLine("Сгенерировать ошибки нарушения границ.");

    // Использовать неверно заданный конструктор.
    try {
        RangeArray ra3 = new RangeArray(100, -10); // Ошибка!
    } catch (RangeArrayException exc) {
        Console.WriteLine(exc);
    }

    // Использовать неверно заданный индекс.
    try {
        RangeArray ra3 = new RangeArray(-2, 2);

        for(int i = -2; i <= 2; i++)
            ra3[i] = i;

        Console.Write("Содержимое массива ra3: ");
        for(int i = -2; i <= 10; i++) // сгенерировать ошибку нарушения границ
            Console.Write(ra3[i] + " ");
    } catch (RangeArrayException exc) {
        Console.WriteLine(exc);
    }
}

}

После выполнения этой программы получается следующий результат.

Длина массива ra: 11
Содержимое массива ra: -5 -4 -3 -2 -1 0 1 2 3 4 5

Длина массива ra2: 10
Содержимое массива ra2: 1 2 3 4 5 6 7 8 9 10

Сгенерировать ошибки нарушения границ.
Нижний индекс не меньше верхнего.
Содержимое массива ra3: -2 -1 0 1 2 Ошибка нарушения границ.

Когда возникает ошибка нарушения границ массива класса RangeArray, генери­
руется объект типа RangeArrayException. В классе RangeArray это может произой­
ти в трех следующих местах: в аксессоре get индексатора, в аксессоре set индексатора
и в конструкторе класса RangeArray. Для перехвата этих исключений подразумева­
ется, что объекты типа RangeArray должны быть сконструированы и доступны из
блока try, что и продемонстрировано в приведенной выше программе. Используя
специальное исключение для сообщения об ошибках, класс RangeArray теперь дей­
ствует как один из встроенных в C# типов данных, и поэтому он может быть полностью
интегрирован в механизм обработки ошибок, обнаруживаемых в программе.

Обратите внимание на то, что в теле конструкторов класса исключения
RangeArrayException отсутствуют какие-либо операторы, но вместо этого они про­
сто передают свои аргументы классу Exception, используя ключевое слово base. Как
пояснялось ранее, в тех случаях, когда производный класс исключений не дополняет
функции базового класса, весь процесс создания исключений можно поручить кон­
структорам класса Exception. Ведь производный класс исключений совсем не обяза­
тельно должен чем-то дополнять функции, наследуемые от класса Exception.

Прежде чем переходить к дальнейшему чтению, попробуйте немного поэксперимен­
тировать с приведенной выше программой. В частности, попробуйте закомментировать
переопределение метода ToString() и понаблюдайте за результатами. Кроме того, по­
пытайтесь создать исключение, используя конструктор, вызываемый по умолчанию, и
посмотрите, какое сообщение при этом сформируется стандартными средствами С#.

## Перехват исключений производных классов
При попытке перехватить типы исключений, относящихся как к базовым, так
и к производным классам, следует особенно внимательно соблюдать порядок следо­
вания операторов catch, поскольку перехват исключения базового класса будет со­
впадать с перехватом исключений любых его производных классов. Например, класс
Exception является базовым для всех исключений, и поэтому вместе с исключением
типа Exception могут быть перехвачены и все остальные исключения производных от
него классов. Конечно, для более четкого перехвата всех исключений можно воспользо­
ваться упоминавшейся ранее формой оператора catch без указания конкретного типа
исключения. Но вопрос перехвата исключений производных классов становится весьма
актуальным и в других ситуациях, особенно при создании собственных исключений.

Если требуется перехватывать исключения базового и производного классов, то
первым по порядку должен следовать оператор catch, перехватывающий исключение
производного класса. Это правило необходимо соблюдать потому, что при перехвате
исключения базового класса будут также перехвачены исключения всех производных
от него классов. Правда, это правило соблюдается автоматически: если первым рас­
положить в коде оператор catch, перехватывающий исключение базового класса, то
во время компиляции этого кода будет выдано сообщение об ошибке.

В приведенном ниже примере программы создаются два класса исключений:
ExceptA и ExceptB. Класс ExceptA является производным от класса Exception,
а класс ExceptB — производным от класса ExceptA. Затем в программе генерируются
исключения каждого типа. Ради краткости в классах специальных исключений предо­
ставляется только один конструктор, принимающий символьную строку, описываю­
щую исключение. Но при разработке программ коммерческого назначения в классах
специальных исключений обычно требуется предоставлять все четыре конструктора,
определяемых в классе Exception.

// Исключения производных классов должны появляться до
// исключений базового класса.
using System;

// Создать класс исключения.
class ExceptA : Exception {
public ExceptA(string str) : base(str) { }

public override string ToString() {
    return Message;
}

}

// Создать класс исключения, производный от класса ExceptA.
class ExceptB : ExceptA {
public ExceptB(string str) : base(str) { }

public override string ToString() {
    return Message;
}

}

class OrderMatters {
static void Main() {
for(int x = 0; x < 3; x++) {
try {
if(x==0) throw new ExceptA(«Перехват исключения типа ExceptA»);
else if(x==1) throw new ExceptB(«Перехват исключения типа
ExceptB»);
else throw new Exception));
}
catch (ExceptB exc) {
Console.WriteLine(exc);
}
catch (ExceptA exc) {
Console.WriteLine(exc);
}
catch (Exception exc) {
Console.WriteLine(exc);
}
}
}
}

Вот к какому результату приводит выполнение этой программы.

Перехват исключения типа ExceptA.
Перехват исключения типа ExceptB.
System.Exception: Выдано исключение типа «System.Exception».
в OrderMatters.Main() в <имя_файла>:строка 36

Обратите внимание на порядок следования операторов catch. Именно в таком по­
рядке они и должны выполняться. Класс ExceptB является производным от класса
ExceptA, поэтому исключение типа ExceptB должно перехватываться до исключения
типа ExceptA. Аналогично, исключение типа Exception (т.е. базового класса для всех
исключений) должно перехватываться последним. Для того чтобы убедиться в этом,
измените порядок следования операторов catch. В итоге это приведет к ошибке во
время компиляции.

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

## Применение ключевых слов checked и unchecked
В C# имеется специальное средство, связанное с генерированием исключений, воз­
никающих при переполнении в арифметических вычислениях. Как вам должно быть
уже известно, результаты некоторых видов арифметических вычислений могут превы­
шать диапазон представления чисел для типа данных, используемого в вычислении.
В этом случае происходит так называемое переполнение. Рассмотрим в качестве при­
мера следующий фрагмент кода.

byte a, b, result;
а = 127;
b = 127;
result = (byte)(а * b);

В этом коде произведение значений переменных а и b превышает диапазон пред­
ставления чисел для типа byte. Следовательно, результат вычисления данного вы­
ражения приводит к переполнению для типа данных, сохраняемого в переменной
result.

В C# допускается указывать, будет ли в коде сгенерировано исключение при пере­
полнении, с помощью ключевых слов checked и unchecked. Так, если требуется ука­
зать, что выражение будет проверяться на переполнение, следует использовать клю­
чевое слово checked, а если требуется проигнорировать переполнение — ключевое
слово unchecked. В последнем случае результат усекается, чтобы не выйти за пределы
диапазона представления чисел для целевого типа выражения.

У ключевого слова checked имеются две общие формы. В одной форме проверя­
ется конкретное выражение, и поэтому она называется операторной. А в другой форме
проверяется блок операторов, и поэтому она называется блочной. Ниже приведены обе
формы:

checked (выражение)

checked {
// проверяемые операторы
}

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

У ключевого слова unchecked также имеются две общие формы. В первой, опера­
торной форме переполнение игнорируется при вычислении конкретного выражения.
А во второй, блочной форме оно игнорируется при выполнении блока операторов:

unchecked (выражение)

unchecked {
// операторы, для которых переполнение игнорируется
}

где выражение обозначает конкретное выражение, при вычислении которого перепол­
нение игнорируется. Если же в непроверяемом выражении происходит переполнение,
то результат его вычисления усекается.

Ниже приведен пример программы, в котором демонстрируется применение клю­
чевых слов checked и unchecked.

// Продемонстрировать применение ключевых слов checked и unchecked.
using System;

class CheckedDemo {
static void Main() {
byte a, b;
byte result;
a = 127;
b = 127;
try {
result = unchecked((byte)(a * b));
Console.WriteLine(«Непроверенный на переполнение результат: » +
result);
result = checked((byte)(a * b)); // эта операция приводит к
// исключительной ситуации
Console.WriteLine(«Проверенный на переполнение результат: » +
result); //не подлежит выполнению
}
catch (OverflowException exc) {
Console.WriteLine(exc);
}
}
}

При выполнении этой программы получается следующий результат.

Непроверенный на переполнение результат: 1
System.OverflowException: Переполнение в результате
выполнения арифметической операции.
в CheckedDemo.Main() в <имя_файла>:строка 20

Как видите, результат вычисления непроверяемого выражения был усечен. А вы­
числение проверяемого выражения привело к исключительной ситуации.

В представленном выше примере программы было продемонстрировано приме­
нение ключевых слов checked и unchecked в одном выражении. А в следующем при­
мере программы показывается, каким образом проверяется и не проверяется на пере­
полнение целый блок операторов.

// Продемонстрировать применение ключевых слов checked
// и unchecked в блоке операторов.
using System;

class CheckedBlocks {
static void Main() {
byte a, b;
byte result;
a = 127;
b = 127;
try {
unchecked {
a = 127;
b = 127;
result = unchecked((byte)(a * b));
Console.WriteLine(«Непроверенный на переполнение результат: » +
result);

            а = 125;
            b = 5;
            result = unchecked((byte)(a * b));
            Console.WriteLine("Непроверенный на переполнение результат: " +
                            result);
        }

        checked {
            a = 2;
            b = 7;
            result = checked((byte)(a * b)); // верно
            Console.WriteLine("Проверенный на переполнение результат: " +
                            result);

            а = 127;
            b = 127;
            result = checked((byte)(a * b)); // эта операция приводит к
            // исключительной ситуации
            Console.WriteLine("Проверенный на переполнение результат: " +
                            result); // не подлежит выполнению
        }
    }
    catch (OverflowException exc) {
        Console.WriteLine(exc);
    }
}

}

Результат выполнения этой программы приведен ниже.

Непроверенный на переполнение результат: 1
Непроверенный на переполнение результат: 113
Проверенный на переполнение результат: 14
System.OverflowException: Переполнение в результате
выполнения арифметической операции.
в CheckedDemo.Main() в <имя_файма>:строка 41

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

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

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

В предыдущей статье я рассказывал о генерации исключений в программах, написанных на 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» и исключения других типов в него не попадут. В следующей статье, я хочу подробнее коснуться этой темы.

_Loader_

6 / 6 / 1

Регистрация: 18.04.2012

Сообщений: 92

1

13.11.2012, 10:47. Показов 9196. Ответов 12

Метки нет (Все метки)


Студворк — интернет-сервис помощи студентам

Всем Привет. Вот такое дело надо создать свой класс ошибок наследованный от Exception;
Вот что попробовал но у меня не переходит в catch если ввожу строку.
подскажите что не так?

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
using System;
 
public class MyException : Exception
{
       public MyException() : base ()
       {
           Console.WriteLine("Общая ошибка");
       }
 
}
 
public class TestMyException
 {
    public static void Main()
    {
        try
        {
            int cost = Convert.ToInt32(Console.ReadLine());
            
        }
        catch (MyException e)
        {
            MyException m = new MyException();
        }
 
        Console.ReadLine();
    }
 }



0



Tessen

713 / 680 / 126

Регистрация: 30.03.2012

Сообщений: 1,124

13.11.2012, 11:08

2

у вас catch срабатывае на MyException или любой другой, но наследованный от него
соответственно если вы в try напишете

C#
1
throw new MyException()

catch сработает
а ошибка Convert.ToInt32 использует свой Exception, а совсем не ваш (да и с чего бы вдруг ему вашим пользоваться?)



1



6 / 6 / 1

Регистрация: 18.04.2012

Сообщений: 92

13.11.2012, 13:43

 [ТС]

3

Tessen, спасибо а вот тогда как сделать что бы ошибка Convert.ToInt32 обрабатывалась у меня а не от стандартного?



0



kolorotur

Эксперт .NET

17223 / 12675 / 3323

Регистрация: 17.09.2011

Сообщений: 20,949

13.11.2012, 14:06

4

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

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
public class MyException : Exception
{
       public MyException() : this("Общая ошибка", null)
       {
       }
       public MyException(string message, InnerException exception) : base(message, exception)
       {      
       }
 
}
 
public class TestMyException
 {
    public static void Main()
    {
        try
        {
            int cost = MyConvert(Console.ReadLine());
            
        }
        catch (MyException e)
        {
            Console.WriteLine(e.Message);
        }
 
        Console.ReadLine();
    }
 }
 
static int MyConvert(string input)
{
   try { return Convert.ToInt32(input); }
   catch (FormatException ex) { throw new MyException("Бум!", ex); }
}



0



713 / 680 / 126

Регистрация: 30.03.2012

Сообщений: 1,124

13.11.2012, 14:07

5

перехватить стандартную ошибку и вместо ее обработки кинуь свой exception
но зачем?



0



Эксперт .NET

17223 / 12675 / 3323

Регистрация: 17.09.2011

Сообщений: 20,949

13.11.2012, 14:09

6

Лучший ответ Сообщение было отмечено как решение

Решение

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

но зачем?

Очень полезный прием, особенно если пишется своя библиотека, которая является оберткой над каким-нибудь сокетом или СОМ-интеропом.
Чтобы в основную среду не летели всякие непонятные COMException и SocketException, они перехватываются внутри классов-оберток и дальше выбрасывается что-то вроде MyLibraryException, поясняющая понятным языком, что пошло не так.

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



3



_Loader_

6 / 6 / 1

Регистрация: 18.04.2012

Сообщений: 92

13.11.2012, 14:39

 [ТС]

7

kolorotur, Спасибо большое, и очень интересное высказывание.

Добавлено через 6 минут
kolorotur, еще вопрос есть

C#
1
2
3
4
catch (FormatException ex) 
   { 
       throw new MyException ("Бум!", ex);
   }

вот тут требует делегат, интерфейс или структуру? как от этого избавиться?

Добавлено через 2 минуты
и еще пару ошибок

C#
1
 InnerException exception

вот тут и тут

C#
1
 this("Общая ошибка", null)

не могли бы объяснить почему?



0



kolorotur

Эксперт .NET

17223 / 12675 / 3323

Регистрация: 17.09.2011

Сообщений: 20,949

13.11.2012, 15:01

8

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

вот тут требует делегат и еще пару ошибок вот тут и тут

Ну это я копипастой увлекся немного

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
public class MyException : Exception
{
       public MyException() : this("Общая ошибка", null)
       {
       }
       public MyException(string message, Exception exception) : base(message, exception)
       {      
       }
 
}
 
public class TestMyException
 {
    public static void Main()
    {
        try
        {
            int cost = MyConvert(Console.ReadLine());
            
        }
        catch (MyException e)
        {
            Console.WriteLine(e.Message);
        }
 
        Console.ReadLine();
    }
    static int MyConvert(string input)
    {
      try { return Convert.ToInt32(input); }
      catch (FormatException ex) { throw new MyException("Бум!", ex); }
    }
 }

1. Вместо InnerException должна быть просто Exception (про что думал — то и написал).
2. Метод MyConvert за пределы класса съехал.



1



6 / 6 / 1

Регистрация: 18.04.2012

Сообщений: 92

13.11.2012, 16:15

 [ТС]

9

kolorotur, прошу прощения за свою спешку ответа. Сам «стормозил». Еще раз благодарю за помощь.

Добавлено через 1 час 11 минут
Я снова повис, ни как не получается еще и ограничить введенную cost, и что бы не нарушилась структура программы. Подскажите как это реализовать ?



0



kolorotur

Эксперт .NET

17223 / 12675 / 3323

Регистрация: 17.09.2011

Сообщений: 20,949

13.11.2012, 16:35

10

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

ни как не получается еще и ограничить введенную cost, и что бы не нарушилась структура программы.

Не совсем понял вопрос, но, возможно, вы имели в виду что-то вроде этого?

C#
1
2
3
4
5
6
7
8
9
    static int MyConvert(string input)
    {
      try
      {
         int value = Convert.ToInt32(input);
         if (value < 100 || value > 1000) throw new ArgumentOutOfRangeException("input", "Input must be between 100 and 1000");
      }
      catch (Exception ex) { throw new MyException("Бум!", ex); }
    }



1



_Loader_

6 / 6 / 1

Регистрация: 18.04.2012

Сообщений: 92

13.11.2012, 16:55

 [ТС]

11

kolorotur, да как раз это имел в виду, но вот исключение не перехватывается в мой Exception

Попробовал вот так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    static int MyConvert(string input)
    {
        try 
        {
            int value = Convert.ToInt32(input);
            if (value < 100 || value > 1000) return value;
        }
        catch (FormatException ex) 
        {
            throw new MyException("Стоимость введена не верно!", ex);  
        }
        catch (ArgumentOutOfRangeException ex)
        {
            throw new MyException("ВВедено число меньше 100 или больше 1000", ex);
        }
    }

Произошла ошибка:

TestMyException.MyConvert(string)»: не все ветви кода возвращают значение

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

C#
1
2
3
4
5
6
 try 
        {
            int value = Convert.ToInt32(input);
            if (value < 100 || value > 1000)  return value;
            return Convert.ToInt32(input); ;
        }

вот так не ругается но и не работает if



0



kolorotur

Эксперт .NET

17223 / 12675 / 3323

Регистрация: 17.09.2011

Сообщений: 20,949

13.11.2012, 17:20

12

Как-то так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    static int MyConvert(string input)
    {
        try 
        {
            int value = Convert.ToInt32(input);
            if (value < 100 || value > 1000) throw new ArgumentOutOfRangeException("input");
            return value;
        }
        catch (FormatException ex) 
        {
            throw new MyException("Стоимость введена не верно!", ex);  
        }
        catch (ArgumentOutOfRangeException ex)
        {
            throw new MyException("ВВедено число меньше 100 или больше 1000", ex);
        }
    }

Свое исключение, конечно, можно бросать прямо с 6-й строчки, но лучше все-таки вложенным исключением передавать дополнительные детали об ошибке.



1



6 / 6 / 1

Регистрация: 18.04.2012

Сообщений: 92

13.11.2012, 17:27

 [ТС]

13

Все теперь разобрался. Спасибо большое.



0



Возможно, вам также будет интересно:

  • C builder если ошибка то
  • C 3102 ошибка konica minolta
  • C 1611 ошибка киа сид
  • C 1155 ошибка мазда 3
  • C 0213 konica minolta ошибка

  • Понравилась статья? Поделить с друзьями:
    0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии