Предыдущая лекция |   | Следующая лекция :----------------:|:----------:|:----------------: [Делегаты, события и лямбды](./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**, так как мы пытаемся разделить на ноль. И дойдя до строки ```cs 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(); ``` ``` Исключение: Попытка деления на нуль. Метод: 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*. Визуально стек вызовов можно представить следующим образом: ``` Блок 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`: ``` Console.WriteLine("Конец метода Main"); ``` Стоит отметить, что код, который идет после конструкции `try...catch` в методах *Method1* и *Method2*, не выполняется, потому что обработчик исключения найден именно в методе *Main*. Консольный вывод программы: ``` Блок finally в Method2 Блок finally в Method1 Catch в Main: Попытка деления на нуль. Блок finally в Main Конец метода Main ``` ## Генерация исключения и оператор throw Обычно система сама генерирует исключения при определенных ситуациях, например, при делении числа на ноль. Но язык C# также позволяет генерировать исключения вручную с помощью оператора **throw**. То есть с помощью этого оператора мы сами можем создать исключение и вызвать его в процессе выполнения. Например, в нашей программе происходит ввод строки, и мы хотим, чтобы, если длина строки будет больше 6 символов, возникало исключение: ```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)