Предыдущая лекция | &nbsp; | Следующая лекция
:----------------:|:----------:|:----------------:
[Делегаты, события и лямбды](./t5_delegate.md) | [Содержание](../readme.md#тема-5-продвинутый-c-функции-лямбды-исключения-работа-с-файлами-многопоточность-регулярные-выражения) | [Многопоточность. Потоки, асинхронные вычисления](./t5_thread_async.md)

<!-- https://metanit.com/sharp/tutorial/2.14.php -->

# Исключения

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

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

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

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

## Конструкция try..catch..finally

```cs
try
{
     
}
catch
{
     
}
finally
{
     
}
```

При использовании блока `try...catch...finally` вначале выполняются все инструкции в блоке **try**. Если в этом блоке не возникло исключений, то после его выполнения начинает выполняться блок **finally**. И затем конструкция `try..catch...finally` завершает свою работу.

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

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

Рассмотрим следующий пример:

```cs
int x = 5;
int y = x / 0;
Console.WriteLine($"Результат: {y}");
Console.WriteLine("Конец программы");
Console.Read();
```

В данном случае происходит деление числа на `0`, что приведет к генерации исключения. И при запуске приложения в консоли увидим сообщение об ошибке:

```
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
   at Program.<Main>$(String[] args) in /home/kei/RiderProjects/tryCatch/Program.cs:line 2

Process finished with exit code 134.
```

Здесь мы видим, что возникло исключение, которое представляет тип `System.DivideByZeroException`, то есть попытка деления на ноль. Ниже указано в каком файле и в какой строке файла произошло это исключение.

Чтобы избежать подобного аварийного завершения программы, следует использовать для обработки исключений конструкцию `try...catch...finally`. Так, перепишем пример следующим образом:

```cs
try
{
    int x = 5;
    int y = x / 0;
    Console.WriteLine($"Результат: {y}");
}
catch
{
    Console.WriteLine("Возникло исключение!");
}
finally
{
    Console.WriteLine("Блок finally");
}
Console.WriteLine("Конец программы");
Console.Read();
```

В данном случае у нас опять же возникнет исключение в блоке **try**, так как мы пытаемся разделить на ноль. И дойдя до строки `int y = x / 0;` выполнение программы остановится. CLR найдет блок **catch** и передаст управление этому блоку.

После блока **catch** будет выполняться блок **finally**.

```
Возникло исключение!
Блок finally
Конец программы
```

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

Следует отметить, что в этой конструкции обязателен блок **try**. При наличии блока **catch** мы можем опустить блок **finally**:

```cs
try
{
    int x = 5;
    int y = x / 0;
    Console.WriteLine($"Результат: {y}");
}
catch
{
    Console.WriteLine("Возникло исключение!");
}
```

И, наоборот, при наличии блока **finally** мы можем опустить блок **catch** и не обрабатывать исключение:

```cs
try
{
    int x = 5;
    int y = x / 0;
    Console.WriteLine($"Результат: {y}");
}
finally
{
    Console.WriteLine("Блок finally");
}
```

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

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

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

```cs
Console.WriteLine("Введите число");
int x = Int32.Parse(Console.ReadLine());

x *= x;
Console.WriteLine("Квадрат числа: " + x);
Console.Read();
```

Если пользователь введет не число, а строку, какие-то другие символы, то программа выпадет в ошибку. С одной стороны, здесь как раз та ситуация, когда можно применить блок `try...catch`, чтобы обработать возможную ошибку. Однако гораздо оптимальнее было бы проверить допустимость преобразования:

```cs
Console.WriteLine("Введите число");
int x;
string input = Console.ReadLine();

if (Int32.TryParse(input, out x))
{
    x *= x;
    Console.WriteLine("Квадрат числа: " + x);
}
else
{
    Console.WriteLine("Некорректный ввод");
}

Console.Read();
```

Метод `Int32.TryParse()` возвращает **true**, если преобразование можно осуществить, и **false** - если нельзя. При допустимости преобразования переменная `x` будет содержать введенное число. Так, не используя `try...catch` можно обработать возможную исключительную ситуацию.

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

## Блок catch и фильтры исключений

### Определение блока catch

За обработку исключения отвечает блок **catch**, который может иметь следующие формы:

```cs
catch
{
    // выполняемые инструкции
}
```

Обрабатывает любое исключение, которое возникло в блоке **try**. Выше уже был продемонстрирован пример подобного блока.

```cs
catch (тип_исключения)
{
    // выполняемые инструкции
}
```

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

Например, обработаем только исключения типа *DivideByZeroException*:

```cs
try
{
    int x = 5;
    int y = x / 0;
    Console.WriteLine($"Результат: {y}");
}
catch(DivideByZeroException)
{
    Console.WriteLine("Возникло исключение DivideByZeroException");
}
```

Однако если в блоке **try** возникнут исключения каких-то других типов, отличных от *DivideByZeroException*, то они не будут обработаны.

```cs
catch (тип_исключения имя_переменной)
{
    // выполняемые инструкции
}
```

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

```cs
try
{
    int x = 5;
    int y = x / 0;
    Console.WriteLine($"Результат: {y}");
}
catch(DivideByZeroException ex)
{
    Console.WriteLine($"Возникло исключение {ex.Message}");
}
```

Фактически этот случай аналогичен предыдущему за тем исключением, что здесь используется переменная. В данном случае в переменную *ex*, которая представляет тип *DivideByZeroException*, помещается информация о возникшем исключени. И с помощью свойства _Message_ мы можем получить сообщение об ошибке.

Если нам не нужна информация об исключении, то переменную можно не использовать как в предыдущем случае.

### Фильтры исключений

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

```cs
catch when(условие)
{
     
}
```

В этом случае обработка исключения в блоке **catch** производится только в том случае, если условие в выражении **when** истинно. Например:

```cs
int x = 1;
int y = 0;
 
try
{
    int result = x / y;
}
catch(DivideByZeroException) when (y==0 && x == 0)
{
    Console.WriteLine("y не должен быть равен 0");
}
catch(DivideByZeroException ex)
{
    Console.WriteLine(ex.Message);
}
```

В данном случае будет выброшено исключение, так как `y=0`. Здесь два блока **catch**, и оба они обрабатывают исключения типа *DivideByZeroException*, то есть по сути все исключения, генерируемые при делении на ноль. Но поскольку для первого блока указано условие `y == 0 && x == 0`, то оно не будет обрабатывать исключение - условие, указанное после оператора **when** возвращает *false*. Поэтому CLR будет дальше искать соответствующие блоки **catch** далее и для обработки исключения выберет второй блок **catch**. В итоге если мы уберем второй блок **catch**, то исключение вобще не будет обрабатываться.

## Типы исключений. Класс Exception

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

* **InnerException**: хранит информацию об исключении, которое послужило причиной текущего исключения

* **Message**: хранит сообщение об исключении, текст ошибки

* **Source**: хранит имя объекта или сборки, которое вызвало исключение

* **StackTrace**: возвращает строковое представление стека вызывов, которые привели к возникновению исключения

* **TargetSite**: возвращает метод, в котором и было вызвано исключение

Например, обработаем исключения типа **Exception**:

```cs
try
{
    int x = 5;
    int y = x / 0;
    Console.WriteLine($"Результат: {y}");
}
catch (Exception ex)
{
    Console.WriteLine($"Исключение: {ex.Message}");
    Console.WriteLine($"Метод: {ex.TargetSite}");
    Console.WriteLine($"Трассировка стека: {ex.StackTrace}");
}

Console.Read();
```

```sh
Исключение: Попытка деления на нуль.
Метод: Void Main(System.String[])
Трассировка стека:    в oap_labs.Program.Main(String[] args) в C:\Users\John\source\repos\oap_labs\oap_labs\Program.cs:строка 16
```

Так как тип **Exception** является базовым типом для всех исключений, то выражение `catch (Exception ex)` будет обрабатывать все исключения, которые могут возникнуть.

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

* **DivideByZeroException**: представляет исключение, которое генерируется при делении на ноль

* **ArgumentOutOfRangeException**: генерируется, если значение аргумента находится вне диапазона допустимых значений

* **ArgumentException**: генерируется, если в метод для параметра передается некорректное значение

* **IndexOutOfRangeException**: генерируется, если индекс элемента массива или коллекции находится вне диапазона допустимых значений

* **InvalidCastException**: генерируется при попытке произвести недопустимые преобразования типов

* **NullReferenceException**: генерируется при попытке обращения к объекту, который равен null (то есть по сути неопределен)

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

```cs
try
{
    int[] numbers = new int[4];
    numbers[7] = 9;     // IndexOutOfRangeException

    int x = 5;
    int y = x / 0;  // DivideByZeroException
    Console.WriteLine($"Результат: {y}");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Возникло исключение DivideByZeroException");
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine(ex.Message);
}
            
Console.Read();
```

В данном случае блоки **catch** обрабатывают исключения типов *IndexOutOfRangeException* и *DivideByZeroException*. Когда в блоке **try** возникнет исключение, то CLR будет искать нужный блок **catch** для обработки исключения. Так, в данном случае на строке

```cs
numbers[7] = 9;
```

происходит обращение к 7-му элементу массива. Однако поскольку в массиве только 4 элемента, то мы получим исключение типа *IndexOutOfRangeException*. CLR найдет блок **catch**, который обрабатывает данное исключение, и передаст ему управление.

Следует отметить, что в данном случае в блоке **try** есть ситуация для генерации второго исключения - деление на ноль. Однако поскольку после генерации *IndexOutOfRangeException* управление переходит в соответствующий блок **catch**, то деление на ноль `int y = x / 0` в принципе не будет выполняться, поэтому исключение типа *DivideByZeroException* никогда не будет сгенерировано.

Рассмотрим другую ситуацию:

```cs
try
{
    object obj = "you";
    int num = (int)obj;     // InvalidCastException
    Console.WriteLine($"Результат: {num}");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Возникло исключение DivideByZeroException");
}
catch (IndexOutOfRangeException)
{
    Console.WriteLine("Возникло исключение IndexOutOfRangeException");
}
            
Console.Read();
```

В данном случае в блоке **try** генерируется исключение типа *InvalidCastException*, однако соответствующего блока **catch** для обработки данного исключения нет. Поэтому программа аварийно завершит свое выполнение.

Мы также можем определить для *InvalidCastException* свой блок **catch**, однако суть в том, что теоретически в коде могут быть сгенерированы сами различные типы исключений. А определять для всех типов исключений блоки **catch**, если обработка исключений однотипна, не имеет смысла. И в этом случае мы можем определить блок **catch** для базового типа *Exception*:

```cs
try
{
    object obj = "you";
    int num = (int)obj;     // InvalidCastException
    Console.WriteLine($"Результат: {num}");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Возникло исключение DivideByZeroException");
}
catch (IndexOutOfRangeException)
{
    Console.WriteLine("Возникло исключение IndexOutOfRangeException");
}
catch (Exception ex)
{
    Console.WriteLine($"Исключение: {ex.Message}");
}  
Console.Read();
```

И в данном случае блок `catch (Exception ex){}` будет обрабатывать все исключения кроме **DivideByZeroException** и **IndexOutOfRangeException**. При этом блоки **catch** для более общих, более базовых исключений следует помещать в конце - после блоков **catch** для более конкретный, специализированных типов. Так как CLR выбирает для обработки исключения первый блок **catch**, который соответствует типу сгенерированного исключения. Поэтому в данном случае сначала обрабатывается исключение **DivideByZeroException** и **IndexOutOfRangeException**, и только потом *Exception* (так как **DivideByZeroException** и **IndexOutOfRangeException** наследуется от класса **Exception**).

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

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

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

```cs
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;
            }
        }
    }
}
```

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

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

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

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

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

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

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

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

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

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

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

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

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

## Поиск блока catch при обработке исключений

Если код, который вызывает исключение, не размещен в блоке **try** или помещен в конструкцию `try..catch`, которая не содержит соответствующего блока **catch** для обработки возникшего исключения, то система производит поиск соответствующего обработчика исключения в стеке вызовов.

Например, рассмотрим следующую программу:

```cs
try
{
    TestClass.Method1();
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Catch в Main : {ex.Message}");
}
finally
{
    Console.WriteLine("Блок finally в Main");
}
Console.WriteLine("Конец метода Main");
Console.Read();

class TestClass
{
    public static void Method1()
    {
        try
        {
            Method2();
        }
        catch (IndexOutOfRangeException ex)
        {
            Console.WriteLine($"Catch в Method1 : {ex.Message}");
        }
        finally
        {
            Console.WriteLine("Блок finally в Method1");
        }
        Console.WriteLine("Конец метода Method1");
    }
    static void Method2()
    {
        try
        {
            int x = 8;
            int y = x / 0;
        }
        finally
        {
            Console.WriteLine("Блок finally в Method2");
        }
        Console.WriteLine("Конец метода Method2");
    }
}
```

В данном случае стек вызовов выглядит следующим образом: метод *Method1* вызывает метод *Method2*. И в методе *Method2* генерируется исключение **DivideByZeroException**. Визуально стек вызовов можно представить следующим образом:

```sh
Блок finally в Method2
Блок finally в Method1
Catch в Main : Attempted to divide by zero.
Блок finally в Main
Конец метода Main
```

Внизу стека метод *Main*, с которого началось выполнение, и на самом верху метод *Method2*.

Что будет происходить в данном случае при генерации исключения?

Метод *Main* вызывает метод *Method1*, а тот вызывает метод *Method2*, в котором генерируется исключение *DivideByZeroException*.

Система видит, что код, который вызывал исключение, помещен в конструкцию `try...`

```cs
try
{
    int x = 8;
    int y = x / 0;
}
finally
{
    Console.WriteLine("Блок finally в Method2");
}
```

Система ищет в этой конструкции блок **catch**, который обрабатывает исключение *DivideByZeroException*. Однако такого блока **catch** нет.

Система опускается в стеке вызовов в метод *Method1*, который вызывал *Method2*. Здесь вызов *Method2* помещен в конструкцию `try..catch`


```cs
try
{
    Method2();
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine($"Catch в Method1 : {ex.Message}");
}
finally
{
    Console.WriteLine("Блок finally в Method1");
}
```

Система также ищет в этой конструкции блок **catch**, который обрабатывает исключение **DivideByZeroException**. Однако здесь также подобный блок **catch** отсутствует.

Система далее опускается в стеке вызовов в метод *Main*, который вызывал *Method1*. Здесь вызов *Method1* помещен в конструкцию `try..catch`

```cs
try
{
    TestClass.Method1();
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Catch в Main : {ex.Message}");
}
finally
{
    Console.WriteLine("Блок finally в Main");
}
```

Система снова ищет в этой конструкции блок **catch**, который обрабатывает исключение **DivideByZeroException**. И в данном случае ткой блок найден.

Система наконец нашла нужный блок **catch** в методе *Main*, для обработки исключения, которое возникло в методе *Method2* - то есть к начальному методу, где непосредственно возникло исключение. Но пока данный блок **catch** НЕ выполняется. Система поднимается обратно по стеку вызовов в самый верх в метод *Method2* и выполняет в нем блок **finally**:

```cs
finally
{
    Console.WriteLine("Блок finally в Method2");
}
```

Далее система возвращается по стеку вызовов вниз в метод *Method1* и выполняет в нем блок **finally**:

```cs
finally
{
    Console.WriteLine("Блок finally в Method1");
}
```

Затем система переходит по стеку вызовов вниз в метод *Main* и выполняет в нем найденный блок **catch** и последующий блок **finally**:

```cs
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Catch в Main : {ex.Message}");
}
finally
{
    Console.WriteLine("Блок finally в Main");
}
```

Далее выполняется код, который идет в методе *Main* после конструкции `try..catch`:

```cs
Console.WriteLine("Конец метода Main");
```

Стоит отметить, что код, который идет после конструкции `try...catch` в методах *Method1* и *Method2*, не выполняется, потому что обработчик исключения найден именно в методе *Main*.

Консольный вывод программы:

```
Блок finally в Method2
Блок finally в Method1
Catch в Main: Попытка деления на нуль.
Блок finally в Main
Конец метода Main
```

## Генерация исключения и оператор throw

Обычно система сама генерирует исключения при определенных ситуациях, например, при делении числа на ноль. Но язык **C#** также позволяет генерировать исключения вручную с помощью оператора **throw**. То есть с помощью этого оператора мы сами можем создать исключение и вызвать его в процессе выполнения.

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

```cs
try
{
    Console.Write("Введите строку: ");
    string message = Console.ReadLine();
    if (message.Length > 6)
    {
        throw new Exception(
            "Длина строки больше 6 символов");
    }
}
catch (Exception e)
{
    Console.WriteLine($"Ошибка: {e.Message}");
}
Console.Read();
```

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

Затем в блоке **catch** сгенерированное нами исключение будет обработано.

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

```cs
try
{
    try
    {
        Console.Write("Введите строку: ");
        string message = Console.ReadLine();
        if (message.Length > 6)
        {
            throw new Exception(
                "Длина строки больше 6 символов");
        }
    }
    catch
    {
        Console.WriteLine("Возникло исключение");
        throw;
    }
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}
```

В данном случае при вводе строки с длиной больше 6 символов возникнет исключение, которое будет обработано внутренним блоком **catch**. Однако поскольку в этом блоке используется оператор **throw**, то исключение будет передано дальше внешнему блоку **catch**.

## NULL

Обычные типы значений не могут иметь значение **NULL**. Но вы можете создать специальные типы, допускающие значения **NULL**, добавив символ `?` после имени типа. Например, тип `int?` является типом **int**, который может иметь значение **NULL**. Типы, допускающие значение **NULL**, особенно полезны при передаче данных в базы данных, где могут использоваться числовые значения **NULL**, и из таких баз данных.

**Тип значений, допускающий значение NULL** , или `T?`, представляет все значения своего базового типа значения `T`, а также дополнительное значение **NULL**. Например, можно присвоить переменной `bool?` любое из следующих трех значений: **true**, **false** или **null**.

Тип значения, допускающий значение **NULL**, следует использовать, когда нужно представить неопределенное значение его базового типа. Например, логическая переменная (или **bool**) может иметь только значения **true** или **false**. Однако в некоторых приложениях значение переменной может быть неопределенным или отсутствовать. Например, поле базы данных может содержать значение **true** или **false** либо вообще никакого значения, то есть **NULL**. В этом сценарии можно использовать тип `bool?`.

### Назначение и объявление

Так как тип значения можно неявно преобразовать в соответствующий тип значения, допускающий значение **NULL**, вы назначаете значение переменной такого типа значения так же, как для базового типа значения. Вы можете также присвоить значение **null**. Пример:

```cs
double? pi = 3.14;
char? letter = 'a';

int m2 = 10;
int? m = m2;

bool? flag = null;

// An array of a nullable value type:
int?[] arr = new int?[10];
```

Значение по умолчанию для типа значения, допускающего значение **NULL**, равно `null`.

### Проверка экземпляра типа значения, допускающего значение NULL

Начиная с версии C# 7.0 можно использовать оператор **is** с шаблоном типа как для проверки экземпляра типа, допускающего значение **NULL**, для **null**, так и для извлечения значения базового типа:

```cs
int? a = 42;
if (a is int valueOfA)
{
    Console.WriteLine($"a is {valueOfA}");
}
else
{
    Console.WriteLine("a does not have a value");
}
```

Вывод:

```
a is 42
```

Вы всегда можете использовать следующие свойства, чтобы проверить и получить значение переменной типа, допускающего значение **NULL**:

* **Nullable<T>.HasValue** указывает, имеет ли экземпляр типа, допускающего значение NULL, значение своего базового типа.
* **Nullable<T>.Value** возвращает значение базового типа, если *HasValue* имеет значение **true**. Если *HasValue* имеет значение **false**, свойство *Value* выдает исключение *InvalidOperationException*.

В следующем примере используется свойство *HasValue*, чтобы проверить, содержит ли переменная значение, перед его отображением:

```cs
int? b = 10;
if (b.HasValue)
{
    Console.WriteLine($"b is {b.Value}");
}
else
{
    Console.WriteLine("b does not have a value");
}
```

Можно также сравнить переменную типа значения, допускающего значение **NULL**, с **null** вместо использования свойства *HasValue*, как показано в следующем примере:

```cs
int? c = 7;
if (c != null)
{
    Console.WriteLine($"c is {c.Value}");
}
else
{
    Console.WriteLine("c does not have a value");
}
```

### Преобразование из типа значения, допускающего значение NULL, в базовый тип

Если необходимо присвоить значение типа, допускающего значение **NULL**, переменной типа значения, не допускающего значения **NULL**, может потребоваться указать значение, назначаемое вместо **null**. Для этого используйте оператор объединения со значением **NULL** `??` (можно также применить метод `Nullable<T>.GetValueOrDefault(T)` для той же цели):

```cs
int? a = 28;
int b = a ?? -1;
Console.WriteLine($"b is {b}");  // output: b is 28

int? c = null;
int d = c ?? -1;
Console.WriteLine($"d is {d}");  // output: d is -1
```

Вы можете также явно привести тип значения, допускающий значение **NULL**, к типу, не допускающему значение **NULL**, как показано в примере ниже.

```cs
int? n = null;

//int m1 = n;    // Doesn't compile
int n2 = (int)n; // Compiles, but throws an exception if n is null
```

Во время выполнения, если значение типа значения, допускающего значение **NULL**, равно **null**, явное приведение вызывает исключение *InvalidOperationException*.

### Операторы с нулификацией

Предопределенные унарные и бинарные операторы или любые перегруженные операторы, поддерживаемые типом значения `T`, также поддерживаются соответствующим типом значения, допускающим значение **NULL**, т. е. `T?`. Эти операторы, также называемые операторами с нулификацией , возвращают значение **null**, если один или оба операнда имеют значение **null**. В противном случае оператор использует содержащиеся значения операндов для вычисления результата. Пример:

```cs
int? a = 10;
int? b = null;
int? c = 10;

a++;        // a is 11
a = a * c;  // a is 110
a = a + b;  // a is null
```

Для операторов сравнения `<`, `>`, `<=` и `>=`, если один или оба операнда равны **null**, результат будет равен **false**. В противном случае сравниваются содержащиеся значения операндов. Тут важно не полагать, что если какая-то операция сравнения (например, `<=`) возвращает **false**, то противоположное сравнение (`>`) обязательно вернет **true**. В следующем примере показано, что 10
не больше и не равно значению null, не меньше чем null.

```cs
int? a = 10;
Console.WriteLine($"{a} >= null is {a >= null}");
Console.WriteLine($"{a} < null is {a < null}");
Console.WriteLine($"{a} == null is {a == null}");
// Output:
// 10 >= null is False
// 10 < null is False
// 10 == null is False

int? b = null;
int? c = null;
Console.WriteLine($"null >= null is {b >= c}");
Console.WriteLine($"null == null is {b == c}");
// Output:
// null >= null is False
// null == null is True
```

Для оператора равенства `==`, если оба операнда равны **null**, результат будет равен **true**. Если один из операндов равен **null**, результат будет равен **false**. В противном случае сравниваются содержащиеся значения операндов.

Для оператора неравенства `!=`, если оба операнда равны **null**, результат будет равен **false**. Если один из операндов равен **null**, результат будет равен **true**. В противном случае сравниваются содержащиеся значения операндов.

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

### Упаковка-преобразование и распаковка-преобразование

Экземпляр типа значения, допускающего значение **NULL**, `T?` упакован следующим образом:
* Если *HasValue* возвращает **false**, создается пустая ссылка.
* Если *HasValue* возвращает **true**, упаковывается соответствующее значение базового типа `T`, а не экземпляр `Nullable<T>`.

Можно распаковать упакованный тип значения `T` в соответствующий тип, допускающий значение **NULL**, `T?`, как показано в следующем примере:

```cs
int a = 41;
object aBoxed = a;
int? aNullable = (int?)aBoxed;
Console.WriteLine($"Value of aNullable: {aNullable}");

object aNullableBoxed = aNullable;
if (aNullableBoxed is int valueOfA)
{
    Console.WriteLine($"aNullableBoxed is boxed int: {valueOfA}");
}
// Output:
// Value of aNullable: 41
// aNullableBoxed is boxed int: 41
```

### Оператор ??

Оператор `??` называется оператором null-объединения. Он применяется для установки значений по умолчанию для типов, которые допускают значение **null**. Оператор `??` возвращает левый операнд, если этот операнд не равен **null**. Иначе возвращается правый операнд. При этом левый операнд должен принимать **null**. Посмотрим на примере:

```cs
object x = null;
object y = x ?? 100;  // равно 100, так как x равен null
 
object z = 200;
object t = z ?? 44; // равно 200, так как z не равен null
```

Но мы не можем написать следующим образом:

```cs
int x = 44;
int y = x ?? 100;
```

Здесь переменная x представляет значимый тип **int** и не может принимать значение **null**, поэтому в качестве левого операнда в операции `??` она использоваться не может.

### Оператор условного null

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

```cs
class User
{
    public Phone Phone { get; set; }
}
 
class Phone
{
    public Company Company { get; set; }
}
 
class Company
{
    public string Name { get; set; }
}
```

Объект *User* содержит ссылку на объект *Phone*, а объект *Phone* содержит ссылку на объект *Company*, поэтому теоретически мы можем получить из объекта *User* название компании, например, так:

```cs
User user = new User();
Console.WriteLine(user.Phone.Company.Name);
```

В данном случае свойство *Phone* не определено, будет по умолчанию иметь значение **null**. Поэтому мы столкнемся с исключением *NullReferenceException*. Чтобы избежать этой ошибки мы могли бы использовать условную конструкцию для проверки на **null**:

```cs
User user = new User();
 
if(user!=null)
{
    if(user.Phone!=null)
    {
        if (user.Phone.Company != null)
        {
            string companyName = user.Phone.Company.Name;
            Console.WriteLine(companyName);
        }
    }
}
```

Получается многоэтажная конструкция, но на самом деле ее можно сократить:

```cs
if(user!=null && user.Phone!=null && user.Phone.Company!=null)
{
    string companyName = user.Phone.Company.Name;
    Console.WriteLine(companyName);
}
```

Если *user* не равно **null**, то проверяется следующее выражение `user.Phone!=null` и так далее. Конструкция намного проще, но все равно получается довольно большой. И чтобы ее упростить, в C# можно использовать **оператор условного null** (Null-Conditional Operator):

```cs
string companyName = user?.Phone?.Company?.Name;
```

Выражение `?`. и представляет **оператор условного null**. Здесь последовательно проверяется равен ли объект *user* и вложенные объекты значению **null**. Если же на каком-то этапе один из объектов окажется равным **null**, то *companyName* будет иметь значение по умолчанию, то есть **null**.

И в этом случае мы можем пойти дальше и применить операцию `??` для установки значения по умолчанию, если название компании не установлено:

```cs
User user = new User();
string companyName = user?.Phone?.Company?.Name ?? "не установлено";
Console.WriteLine(companyName);
```

---

## Задание на дом:

Реализовать все примеры из лекции. Привести текст примера и текст результата, например:

># Конспект лекции "Исключения"
>
>## Деление на `0`
>
>```cs
>int x = 5;
>int y = x / 0;
>Console.WriteLine($"Результат: {y}");
>Console.WriteLine("Конец программы");
>Console.Read();
>```
>
>```
>Unhandled exception. System.DivideByZeroException: >Attempted to divide by zero.
>   at Program.<Main>$(String[] args) in /home/kei/>RiderProjects/tryCatch/Program.cs:line 2
>
>Process finished with exit code 134.
>```

Предыдущая лекция | &nbsp; | Следующая лекция
:----------------:|:----------:|:----------------:
[Делегаты, события и лямбды](./t5_delegate.md) | [Содержание](../readme.md#тема-5-продвинутый-c-функции-лямбды-исключения-работа-с-файлами-многопоточность-регулярные-выражения) | [Многопоточность. Потоки, асинхронные вычисления](./t5_thread_async.md)
