Skip to content

Latest commit

 

History

History
1072 lines (842 loc) · 54.1 KB

t5_exception.md

File metadata and controls

1072 lines (842 loc) · 54.1 KB
Предыдущая лекция   Следующая лекция
Делегаты, события и лямбды Содержание Многопоточность. Потоки, асинхронные вычисления

Исключения

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

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

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

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

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

try
{
     
}
catch
{
     
}
finally
{
     
}

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

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

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

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

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. Так, перепишем пример следующим образом:

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:

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

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

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

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

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

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

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

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

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

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, который может иметь следующие формы:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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:

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:

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 для обработки исключения. Так, в данном случае на строке

numbers[7] = 9;

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

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

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

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:

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, соответственно для создания своих типов мы можем унаследовать данный класс.

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

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:

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

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

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, который представляет исключение, генерируемое в результате передачи аргументу метода некорректного значения:

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

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

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

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

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

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

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...

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

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

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

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

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

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

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:

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

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

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

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

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 символов, возникало исключение:

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:

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. Пример:

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, так и для извлечения значения базового типа:

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, чтобы проверить, содержит ли переменная значение, перед его отображением:

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

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

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) для той же цели):

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, как показано в примере ниже.

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. В противном случае оператор использует содержащиеся значения операндов для вычисления результата. Пример:

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.

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?, как показано в следующем примере:

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. Посмотрим на примере:

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

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

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

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

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

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

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 название компании, например, так:

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

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

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);
        }
    }
}

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

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):

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

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

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

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

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

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

Конспект лекции "Исключения"

Деление на 0

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.
Предыдущая лекция   Следующая лекция
Делегаты, события и лямбды Содержание Многопоточность. Потоки, асинхронные вычисления