Книга «Программируем на C# 8.0. Разработка приложений» +10


image Привет, Хаброжители!

C# — универсальный язык, который может практически всё! Иэн Гриффитс рассказывает о его возможностях с точки зрения разработчика, перед которым стоит задача быстро и эффективно создавать приложения любой сложности.

Множество примеров кода научат работать с шаблонами, LINQ и асинхронными возможностями языка. Вы разберетесь с асинхронными потоками, ссылочными типами, допускающими значение NULL, сопоставлениями с образцом, реализациями по умолчанию для метода интерфейса, диапазонами и синтаксисом индексации и многим другим.

Исключения


Некоторые операции могут завершиться неудачей. Если ваша программа читает данные из файла, хранящегося на внешнем диске, кто-то может отключить диск. Приложение может попытаться создать массив и обнаружить, что в системе недостаточно свободной памяти. Ненадежное подключение к беспроводной сети может привести к сбою в работе сетевых запросов. Один из широко используемых способов обнаружения подобного рода ошибок в API — возвращать значение, указывающее, было ли выполненное действие успешным. Это требует бдительности от разработчиков относительно того, все ли ошибки они отслеживают, потому что программе нужно проверять возвращаемое значение каждой операции. Это, безусловно, жизнеспособная стратегия, но она может запутать код; логическая последовательность задач, выполняемых в нормальных условиях, может оказаться похороненной под всеми этими проверками на ошибки, что усложняет сопровождение кода. C# поддерживает другой популярный механизм обработки ошибок, который способен сгладить эту проблему, — исключения.

Когда API сообщает об ошибке с исключением, это нарушает нормальный ход выполнения, что приводит к переходу к ближайшему подходящему коду обработки ошибок. Механизм обеспечивает определенный уровень разделения между логикой обработки ошибок и кодом, который пытается выполнить поставленную задачу. Он может облегчить чтение и обслуживание кода, хотя, с другой стороны, способен затруднить просмотр всех возможных путей выполнения кода.

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

Большинство ошибок в .NET представлены в виде исключений. Однако некоторые API предлагают выбор между кодами возврата и исключениями. Например, тип int имеет метод Parse, принимающий строку и пытающийся интерпретировать ее содержимое как число. Если вы передадите ему какой-то нечисловой текст (например, «Hello»), он укажет на ошибку, вызвав исключение FormatException. Если вас это не устраивает, то вместо этого можно вызывать TryParse, который выполняет точно такую же работу, но если ввод не числовой, он возвращает false вместо вызова исключения. (Поскольку возвращаемое значение метода призвано сообщать об успехе или неудаче, метод предоставляет целочисленный результат через выходной параметр.) Числовой синтаксический анализ — не единственная операция, использующая эту схему, при которой используется пара методов (Parse и TryParse, в данном случае) и предоставляется выбор между исключениями и возвращаемыми значениями. Как вы видели в главе 5, аналогичный выбор предоставляют словари. Индексатор выдает исключение, если вы используете ключ, которого нет в словаре, но вы также можете искать значения с помощью TryGetValue, который возвращает false при сбое, подобно TryParse. Хотя эта схема и встречается в ряде мест, для большинства API исключения остаются единственным выбором.

Если вы разрабатываете API, который способен вызвать сбой, как он должен об этом сбое сообщать? Использовать ли в этом случае исключения, возвращаемое значение или и то и другое? Рекомендации Microsoft по разработке библиотеки классов содержат инструкции, которые кажутся однозначными:

Не возвращайте коды ошибок. Исключения являются основным средством сообщения об ошибках в библиотеках классов.

Рекомендации по разработке .NET Framework

Но как это согласуется с существованием int.TryParse? В руководстве есть раздел, посвященный вопросам производительности исключений, в котором говорится следующее:

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

Рекомендации по разработке .NET Framework

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

Исключения, как правило, могут создаваться и обрабатываться за доли миллисекунды, поэтому они не такие уж и медленные — во всяком случае, не такие, как чтение данных по сетевому соединению, — но и не слишком быстрые. Я выяснил, что на моем компьютере один поток может анализировать пятизначные числовые строки со скоростью примерно 65 миллионов строк в секунду в .NET Core 3.0 и он способен отклонять нечисловые строки с такой же скоростью при использовании TryParse. Метод Parse обрабатывает числовые строки так же быстро, но он примерно в 1000 раз медленнее отклоняет нечисловые строки, чем TryParse, благодаря затратам на исключения. Конечно, преобразование строк в целые числа — это довольно быстрая операция, поэтому это делает исключения неудачным выбором, но именно поэтому эта схема наиболее распространена для операций, быстрых по своей природе.

Особенно медленно исключения могут работать при отладке. Отчасти это связано с тем, что отладчику необходимо решить, куда ему влезать, но это особенно заметно при первом необработанном исключении, которое вызывает ваша программа. Может создаться впечатление, что исключения значительно дороже, чем они есть на самом деле. Числа в предыдущем абзаце основаны на наблюдаемом поведении во время выполнения без отладки. Тем не менее эти цифры несколько занижают затраты, поскольку обработка исключения приводит к тому, что CLR запускает кусочки кода и получает доступ к структурам данных, которые в противном случае не нужны, а это может привести к вытеснению полезных данных из кэша ЦП. Это может привести к тому, что код будет работать медленнее в течение короткого времени после обработки исключения, пока не относящийся к исключению код и данные не вернутся в кэш. Простота теста уменьшает этот эффект.

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

Источники исключений


API библиотеки классов — не единственный источник исключений. Они могут возникнуть в любом из следующих сценариев:

  • Проблему обнаруживает ваш собственный код.
  • Ваша программа использует API библиотеки классов, где возникает проблема.
  • Среда выполнения обнаруживает сбой операции (например, арифметическое переполнение в проверяемом контексте, попытку использовать нулевую ссылку или разместить объект, для которого недостаточно памяти).
  • Среда выполнения обнаруживает сбой вне вашего контроля, который влияет на ваш код (например, среда выполнения пытается выделить память для какой-то внутренней цели и обнаруживает, что свободной памяти недостаточно).

Хотя все они используют одни и те же механизмы обработки исключений, места возникновения исключений отличаются. Когда ваш собственный код вызовет исключение (позже я покажу вам, как это сделать), вы будете знать, какие условия привели к его возникновению, но что происходит, когда исключения вызывают другие сценарии? В следующих разделах я опишу, где ожидать каждый вид исключений.

Исключения от API

При вызове API есть несколько видов проблем, способных привести к исключениям. Возможно, вы предоставили аргументы, которые не имеют смысла, например пустую ссылку вместо рабочей или пустую строку вместо имени файла. Или аргументы могут выглядеть хорошо по отдельности, но не все вместе. Например, вы можете вызвать API, который копирует данные в массив, и попросить его скопировать больше данных, чем в него умещается. Их можно описать как ошибки в стиле «никогда не сработает», и обычно они являются результатом ошибок в коде. (Один разработчик, который раньше работал в команде компилятора C#, называет их тупоголовыми исключениями (boneheaded exceptions).)

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

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

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

Асинхронное программирование добавляет еще один вариант. В главах 16 и 17 я покажу различные асинхронные API, в которых работа может продолжаться после возвращения из метода, который ее запустил. Работа, которая выполняется асинхронно, также асинхронно сбоит, и в этом случае библиотеке может потребоваться дождаться следующего вызова вашего кода, прежде чем она сможет сообщить об ошибке.

Несмотря на различия, во всех этих случаях исключение будет исходить из некоторого API, который вызывается вашим кодом. (Даже в случае сбоя асинхронных операций исключения возникают либо при попытке получить результат операции, либо когда вы явно спрашиваете, была ли ошибка.) В листинге 8.1 показан код, в котором могут возникать исключения такого рода.

Листинг 8.1. Получение исключения из библиотечного вызова

static void Main(string[] args)
{
     using (var r = new StreamReader(@"C:\Temp\File.txt"))
     {
          while (!r.EndOfStream)
          {
             Console.WriteLine(r.ReadLine());
          }
     }
}

В этой программе нет ничего категорически неправильного, поэтому мы не получим никаких исключений относительно того, что аргументы изначально неверные. (В неофициальной терминологии он не допускает тупоголовых ошибок.) Если на диске C: вашего компьютера есть папка Temp, и если она содержит файл File.txt, и если пользователь, запускающий программу, имеет разрешение на чтение файла, и если на компьютере никто не получил монопольного доступа к файлу, и если нет проблем — таких, как повреждение диска, — которые могут сделать любую часть файла недоступной, и если нет новых проблем (таких, как воспламенение диска) во время работы программы, этот код будет работать замечательно: он покажет каждую строку текста в файле. Но тут очень много если.

Если такого файла нет, конструктор StreamReader не будет завершен. Вместо этого он выдаст исключение. Эта программа не пытается обработать его, поэтому приложение будет завершено. Если вы запустили программу за пределами отладчика Visual Studio, то увидите следующий вывод:

Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\Temp\File.txt'.
     at System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle)
     at System.IO.FileStream.CreateFileOpenHandle(FileMode mode, FileShare share, FileOptions options)
     at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess
access, FileShare share, Int32 bufferSize, FileOptions options)
     at System.IO.StreamReader.ValidateArgsAndOpenPath(String path,
Encoding encod ing, Int32 bufferSize)
     at System.IO.StreamReader..ctor(String path)
     at Exceptional.Program.Main(String[] args) in c:\Examples\Ch08\Example1\Progr am.cs:line 10

Он сообщает нам, какая ошибка произошла, и показывает полный стек вызовов программы в момент возникновения проблемы. В Windows также включится общесистемная обработка ошибок, поэтому в зависимости от конфигурации вашего компьютера вы увидите диалоговое окно отчетов об ошибках и даже сможете сообщить о сбое в Microsoft. Если вы запустите ту же программу в отладчике, она сообщит вам об исключении, а также выделит строку, в которой произошла ошибка, как показано на рис. 8.1.

image

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

Между прочим, вызов конструктора StreamReader — не единственная строка, которая может вызвать исключение в листинге 8.1. Код вызывает ReadLine несколько раз, и любой из этих вызовов может завершиться ошибкой. В целом любой доступ к элементу может привести к исключению, включая простое чтение свойства, хотя разработчики библиотеки классов обычно пытаются ограничить масштабы, в которых свойства генерируют исключения. Если вы сделаете ошибку типа «это никогда не сработает» (тупоголовую), свойство может вызвать исключение, но такого обычно не делается для ошибок типа «эта конкретная операция не сработала». Например, в документации сказано, что свойство EndOfStream из листинга 8.1 выдает исключение при попытке прочитать его после вызова Dispose объекта StreamReader, что является очевидной ошибкой кодирования. Но если имеются проблемы с чтением файла, StreamReader будет генерировать исключения только из методов или конструктора.

Сбои, обнаруженные во время выполнения

Еще один источник исключений — это когда CLR сама обнаруживает, что некая операция завершилась неудачно. Листинг 8.2 показывает метод, в котором это может произойти. Как и в листинге 8.1, в самом коде нет ничего плохого (кроме того, что он не особо полезен). Его вполне возможно использовать безо всяких проблем. Но если кто-то передаст 0 в качестве второго аргумента, код попытается выполнить недопустимую операцию.

Листинг 8.2. Потенциальная ошибка во время выполнения

static int Divide(int x, int y)
{
    return x / y;
}

CLR обнаружит попытку деления на ноль и выдаст исключение DivideByZeroException. Это будет иметь тот же эффект, что и исключение из вызова API: если программа не попытается обработать исключение, произойдет сбой или в дело вступит отладчик.

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

Последним из описанных ранее источником исключений тоже является обнаружение определенных сбоев средой выполнения, но они работают немного по-другому. Они не обязательно напрямую инициируются тем, что ваш код делал в потоке, в котором происходит исключение. Иногда их называют асинхронными исключениями, и теоретически они могут быть сгенерированы буквально в любой точке вашего кода, что затрудняет обеспечение их правильной обработки. Тем не менее они, как правило, вызываются только в самых катастрофических условиях, часто перед закрытием вашей программы, поэтому вам не удастся с пользой их обработать. Например, в .NET Core, StackOver flowException и OutOfMemoryException теоретически могут быть выброшены в любой момент (поскольку CLR может потребоваться память для собственных целей, даже если ваш код не делал ничего, что явно к этому привело).

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

Обработка исключений


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

Листинг 8.3. Обработка исключения

try
{
      using (StreamReader r = new StreamReader(@"C:\Temp\File.txt"))
      {
         while (!r.EndOfStream)
         {
             Console.WriteLine(r.ReadLine());
         }
      }
}
catch (FileNotFoundException)
{
       Console.WriteLine("Couldn't find the file");
}

Блок сразу за ключевым словом try обычно называется блоком try, и, если программа выдает исключение, находясь внутри такого блока, CLR ищет подходящие блоки catch. В листинге 8.3 есть только один блок catch, и из находящегося в скобках после ключевого слова catch видно, что этот конкретный блок предназначен для обработки исключений типа FileNotFoundException.

Ранее вы уже видели, что если нет файла C:\Temp\File.txt, то конструктор StreamReader выдает исключение FileNotFoundException. В листинге 8.1 это привело к аварийному завершению программы, но поскольку в листинге 8.3 имеется блок catch для этого исключения, CLR выполнит его. На этом этапе он будет считать, что исключение обработано, поэтому программа не завершает работу аварийно. Наш блок catch может делать все, что угодно, и в данном случае мой код просто отображает сообщение, указывающее, что файл отсутствует.

Обработчики исключений не обязательно должны располагаться в методе, в котором возникло исключение. CLR идет вверх по стеку, пока не найдет подходящий обработчик. Если бы вызывающий сбой вызов конструктора StreamReader был в каком-то другом методе, который был вызван из блока try в листинге 8.3, наш блок catch все равно сработал бы (если только этот метод не предоставил собственный обработчик для того же исключения).

Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок

Для Хаброжителей скидка 25% по купону — C#

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.




К сожалению, не доступен сервер mySQL