Предыдущая лекция |   | Следующая лекция :----------------:|:----------:|:----------------: [Делегаты, события и лямбды](./t5_delegate.md) | [Содержание](../readme.md#тема-5-продвинутый-c-функции-лямбды-исключения-работа-с-файлами-многопоточность-регулярные-выражения) | [Многопоточность. Потоки, асинхронные вычисления](./t5_thread_async.md) # Исключения В любой, особенно большой, программе могут возникать ошибки, приводящие к её неработоспособности или к тому, что программа делает не то, что должна. Причин возникновения ошибок много. Программист может сделать ошибку в употреблении самого языка программирования. Другими словами, выразиться так, как выражаться не положено. Например, начать имя переменной с цифры или забыть поставить двоеточие в заголовке сложной инструкции. Подобные ошибки называют **синтаксическими**, они нарушают синтаксис и пунктуацию языка. **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.
$(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.HasValue** указывает, имеет ли экземпляр типа, допускающего значение NULL, значение своего базового типа. * **Nullable.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.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` в соответствующий тип, допускающий значение **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.
$(String[] args) in /home/kei/>RiderProjects/tryCatch/Program.cs:line 2 > >Process finished with exit code 134. >``` Предыдущая лекция |   | Следующая лекция :----------------:|:----------:|:----------------: [Делегаты, события и лямбды](./t5_delegate.md) | [Содержание](../readme.md#тема-5-продвинутый-c-функции-лямбды-исключения-работа-с-файлами-многопоточность-регулярные-выражения) | [Многопоточность. Потоки, асинхронные вычисления](./t5_thread_async.md)