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

1- Что такое Exception?

Сначала давайте посмотрим на следующий иллюстративный пример:

В этом примере есть часть кода c ошибкой, которая получается из-за деления на 0. Деление на 0 вызывает исключение: DivideByZeroException
HelloException.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class HelloException
    {
        public static void Main(string[] args)
        {

            Console.WriteLine("Three");

            // Данное деление совершенно не имеет проблем.
            int value = 10 / 2;

            Console.WriteLine("Two");

            // Данное деление тоже
            value = 10 / 1;

            Console.WriteLine("One");

            int d = 0;

            // Данное деление имеет проблему, делит на 0.
            // Ошибка происходит здесь.
            value = 10 / d;

            // И строка кода ниже не будет выполнена.
            Console.WriteLine("Let's go!");

            Console.Read();
        }
    }
}
Результат запуска примера:
Вы можете увидеть уведомление ошибки на экране Console. Уведомление об ошибке очень четко, на какой строке кода она произошла.
Давайте посмотрим на поток программы на следующем рисунке:
  • Программа выполняется как обычно с этапа (1), (2) до (5).
  • На этапе (6) появляется проблема при делении на 0.
  • Программа выскочила из функции main, и строка кода (7) не будет выполнена.

Мы будем модифицировать код примера выше:

HelloCatchException.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class HelloCatchException
    {
        public static void Main(string[] args)
        {

            Console.WriteLine("Three");

            // Данное деление не имеет проблем.          
            int value = 10 / 2;


            Console.WriteLine("Two");

            // Данное деление не имеет проблем.          
            value = 10 / 1;


            Console.WriteLine("One");


            int d = 0;

            try
            {
                // Данное деление имеет проблему, делит на 0.
                // Ошибка происходит здесь.
                value = 10 / d;

                // Данный код не будет запущен.
                Console.WriteLine("Value =" + value);
            }
            catch (DivideByZeroException e)
            {
                // Код в catch будет выполнен.
                Console.WriteLine("Error: " + e.Message); 

                Console.WriteLine("Ignore...");

            }

            // Данный код выполняется.
            Console.WriteLine("Let's go!");


            Console.Read();
        }
    }

}
И результаты запуска примера:
Мы объясним поток программы следующими иллюстрационными изображениями:
  • Шаги (1) - (6) являются полностью нормальными.
  • Исключение происходит на этапе (7), проблема при делении на 0.
  • Сразу же после звпрыгивает для выполнения команды в блоке catch, шаг (8) пропущен.
  • Шаги (9), (10) выполнены.
  • Шаги(11), (12) выполнены.

2- Иерархия исключений

Это модель иерархической карты Exception в CSharp.
  • Самый высокий класс - Exception
  • Два прямых подкласса - это SystemError и ApplicationException.

 
Готовые Exception в  CSharp, обычно получены (derived) от SystemException. Между тем, Exception пользователей (программистов) должно наследоваться от ApplicationException или его подклассов.
Некоторые общие исключения (Exception), доступные в Csharp:

Kiểu ngoại lệ

Описание

Exception

Основной класс всех исключений.

SystemException

Основной класс всех исключений генерированных во время запуска программы.

IndexOutOfRangeException

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

NullReferenceException

Выбрасывается во время запуска при ссылке на объект null.

AccessViolationException 

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

InvalidOperationException

Выбрасывается методом при недействительном статусе.

ArgumentException

Основной класс всех исключений связанных с аргументом (Argument).

ArgumentNullException

Это подкласс ArgumentException, который выбрасывается методом, не позволяющим передачу аргументов null.

ArgumentOutOfRangeException

Это подкласс ArgumentException, который выбрасывается методом когда аргумент не соответствует рамке позволяющей передачу.

ExternalException

Основной класс для исключений или пришедших из внешней среды.

COMException

Класс расширенный из ExternalException, исключение упаковывает информацию COM.

SEHException

Класс расширенный из ExternalException, ловил все исключения из Win32.

3- Поймать исключения используя try-catch

Мы напишем класс унаследованный от ApplicationException.
AgeException.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
   class AgeException : ApplicationException
   {

       public AgeException(String message)
           : base(message)
       {
       }

   }

   class TooYoungException : AgeException
   {


       public TooYoungException(String message)
           : base(message)
       {

       }

   }


   class TooOldException : AgeException
   {


       public TooOldException(String message)
           : base(message)
       {

       }

   }
}
И класс AgeUtils со статическим методом для проверки возраста.
AgeUtils.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class AgeUtils
    {
        // Данный метод выполняет роль проверки возраста.
        // Если возраст меньше 18 метод выбросит исключение TooYoungException
        // Если возраст старше 40 метод выбросит исключение TooOldException
        public static void checkAge(int age)
        {
            if (age < 18)
            {
                // Если возраст меньше 18, выбросится исключение
                // Данный метод завершается здесь.
                throw new TooYoungException("Age " + age + " too young");
            }
            else if (age > 40)
            {
                // Если возраст старше  40, выбросится исключение.
                // Данный метод завершается здесь.
                throw new TooOldException("Age " + age + " too old");
            }
            // Если возраст в рамке 18-40.
            // Данный код запускается.
            Console.WriteLine("Age " + age + " OK!");
        }
    }

}
TryCatchDemo1.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class TryCatchDemo1
    {
        public static void Main(string[] args)
        {
            // Начать рекрутирование ...
            Console.WriteLine("Start Recruiting ...");

            // Проверить ваш возраст.
            Console.WriteLine("Check your Age");
            int age = 50;

            try
            {

                AgeUtils.checkAge(age);

                Console.WriteLine("You pass!");

            }
            catch (TooYoungException e)
            {
                // Оповещение об исключениях "слишком молодой" ..
                Console.WriteLine("You are too young, not pass!");
                Console.WriteLine(e.Message);

            }
            catch (TooOldException e)
            {
                // Оповещение об исключениях "превышает возраст" ..
                Console.WriteLine("You are too old, not pass!");
                Console.WriteLine(e.Message);

            }

            Console.Read();

        }
    }

}
Запуск примера:
В следующем примере мы поймаем исключения через родительские исключения на уровне выше. На высшем уровне поймаются эти исключения и все унаследованные исключения.
TryCatchDemo2.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class TryCatchDemo2
    {
        public static void Main(string[] args)
        {
            // Начать рекрутирование ...
            Console.WriteLine("Start Recruiting ...");

            // Проверить ваш возраст.
            Console.WriteLine("Check your Age");
            int age = 15;

            try
            {
                // Здесь может быбросить (throw) исключение TooOldException,
                // или TooYoungException
                AgeUtils.checkAge(age);

                Console.WriteLine("You pass!");

            }
            // Если происходит исключение, вида AgeException
            // Данный блок catch будет запущен.
            catch (AgeException e)
            {

                Console.WriteLine("Your age invalid, you not pass");
                Console.WriteLine(e.Message);

            }

            Console.Read();
        }
    }

}
Запуск примера:

4- Блок try-catch-finally

Мы ознакомились тем, как поймать ошибку через блок try-catch. try-catch-finally используется для полной обработки исключения.
try {

  // Сделать что-то здесь
} catch (Exception1 e) {

  // Сделать что-то здесь
} catch (Exception2 e) {

  // Сделать что-то здесь
} finally {

  // Блок finally всегда выполняется
  // Сделать что-то здесь
}
TryCatchFinallyDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class TryCatchFinallyDemo
    {
        public static void Main(string[] args)
        {

            String text = "001234A2";

            int value = toInteger(text);

            Console.WriteLine("Value= " + value);

            Console.Read(); 

        }

        public static int toInteger(String text)
        {
            try
            {

                Console.WriteLine("Begin parse text: " + text);

                // Здесь может произойти исключение FormatException
                int value = int.Parse(text);

                return value;

            }
            catch (FormatException e)
            {
                // В случае 'text' не является числом.
                // Данный блок catch будет выполнен.
                Console.WriteLine("Number format exception: " + e.Message);
 
                return 0;

            }
            finally
            {

                Console.WriteLine("End parse text: " + text);

            }

        }
    }

}
Запуск примера:
Это поток программы. И finally блок всегда выполняется.

5- Обернуть Exception в другом Exception

Нам нужны некоторые классы, для участия в этом примере
  • Person: Имитирует человека подающего заявление на работу в компанию с информацией: 
    • Имя, возраст, пол.
  • GenderException: гендерное исключение.
  • ValidateException: исключение оценки кандидата.
  • ValidateUtils: Класс со статическим методом оценивает соответствие стандарту кандидатов.
    • Стандартный возраст от 18 до 40 лет
    • И мужчины.
Person.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
  class Person
  {

      public static readonly string MALE = "male";
      public static readonly string FEMALE = "female";

      private string name;
      private string gender;
      private int age;

      public Person(string name, string gender, int age)
      {
          this.name = name;
          this.gender = gender;
          this.age = age;
      }

      public string GetName()
      {
          return name;
      }


      public string GetGender()
      {
          return gender;
      }

      public int GetAge()
      {
          return age;
      }

  }

}
GenderException.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
   class GenderException : ApplicationException
   {

       public GenderException(String message)
           : base(message)
       {


       }
   }

}
Класс ValidateException обёртывает другой Exception:
ValidateException.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class ValidateException : ApplicationException
    {

        // Упаковать (wrap) Exception в Exception.
        public ValidateException(Exception e) : base("Something invalid", e)
        {
            
        }
    }

}
ValidateUtils.java
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{

    class ValidateUtils
    {
        // Метод проверяет человека, участвующего в рекрутировании.
        public static void CheckPerson(Person person)
        {
            try
            {
                // Проверка возраста.
                // Действительным является в рамках 18-40
                // Данный метод может выбросить TooOldException,TooYoungException.    
                AgeUtils.checkAge(person.GetAge());

            }
            catch (Exception e)
            {
                // Если недействительно
                // Обернуть данное исключение с помощью ValidateException, и выбросить (throw).
                throw new ValidateException(e);

            }

            // Если это Женщина, то недействительно.
            if (person.GetGender() == Person.FEMALE)
            {

                GenderException e = new GenderException("Do not accept women");
                throw new ValidateException(e);

            }
        }
    }

}
WrapperExceptionDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class WrapperExceptionDemo
    {
        public static void Main(string[] args)
        {

            // Кандидат участвует в рекрутировании.
            Person person = new Person("Marry", Person.FEMALE, 20);

            try
            {
                // Исключение может произойти здесь.
                ValidateUtils.CheckPerson(person);

            }
            catch (ValidateException wrap)
            {

                // Получить настоящую причину.
                // Может быть TooYoungException, TooOldException, GenderException.
                Exception cause = wrap.GetBaseException();

                if (cause != null)
                {
                    Console.WriteLine("Message: " + wrap.Message);
                    Console.WriteLine("Base Exception Message: " + cause.Message);
                }
                else
                {
                    Console.WriteLine("Message: " + wrap.Message);

                }
            }

            Console.Read();
        }
    }
   
}
Запуск примера:

6- Распространенные исключения

Теперь вы увидите некоторые примеры с распространённым исключениями:

6.1- NullReferenceException

Это одно из наиболее распространенных исключений, которое обычно вызывает ошибку в программе. Исключение отбрасывается при вызове функции или доступа к полям нулевого ( null) объекта.
NullReferenceExceptionDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class NullReferenceExceptionDemo
    {
        // Например это метод, который может вернуть строку null.
        public static string GetString()
        {
            if (1 == 2)
            {
                return "1==2 !!";
            }
            return null;
        }

        public static void Main(string[] args)
        {
            // Это объект с ссылкой отличающейся от null.
            string text1 = "Hello exception";

            // Получить длину строки.
            int length = text1.Length;

            Console.WriteLine("Length text1 = " + length);

            // Это объект с ссылкой (reference) null.
            String text2 = GetString(); // text2 = null.

            // Получить длину строки.
            // NullReferenceException произойдет здесь.
            length = text2.Length; // ==> Runtime Error!

            Console.WriteLine("Finish!");


            Console.Read();
        }
    }

}
На самом деле, подобно обработке других исключений, вы можете использовать try-catch, чтобы поймать и обработать это исключение. Тем не менее, это механически, как правило, мы должны проверить, чтобы значение объекта не было null до его использования.

Вы можете исправить приведенный выше код как приведено ниже, избегая NullReferenceException:
// Это объект с ссылкой null.
String text2 = GetString(); // ==> return null

// Проверить, чтобы удостовериться что 'text2' отличается от null,
// Вместо использования try-catch.
if (text2 != null)
{
      length = text2.Length;
}

6.2- IndexOutOfRangeException

Это исключение выбрасывается, когда вы пытаетесь получить доступ к элементу, индекс которого недопустим в массиве. Например, массив содержит 10 элементов, но вы получаете доступ к элементу с индексом 20.
IndexOutOfRangeExceptionDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class IndexOutOfRangeExceptionDemo
    {
        public static void Main(string[] args)
        {

            String[] strs = new String[] { "One", "Two", "Three" };

            // Получить доступ в элемент индекса 0.
            String str1 = strs[0];

            Console.WriteLine("String at 0 = " + str1);

            // Получить доступ в элемент индекса 5
            // IndexOutOfRangeException происходит здесь.
            String str2 = strs[5];

            Console.WriteLine("String at 5 = " + str2);

            Console.Read();

        }
    }
}
Чтобы избежать IndexOutOfRangeException, вы должны проверить массив вместо использования try-catch.
if (strs.length > 5)
{
     String str2 = strs[5];
     Console.WriteLine("String at 5 = " + str2);
}
else
{
     Console.WriteLine("No elements with index 5");
}