Читать в оригинале

<< ПредыдущаяОглавлениеСледующая >>


2.2.6. Проверяемые преобразования

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

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

Синтаксически проверяемый блок предваряется ключевым словом checked. В теле такого блока арифметические преобразования проверяются на допустимость, что требует дополнительных временных затрат. Если группа операторов в теле такого блока нам кажется безопасной, то их можно выделить в непроверяемый блок, используя ключевое слово unchecked. Заметим еще, что и в непроверяемом блоке при работе методов Convert все опасные преобразования проверяются и приводят к выбрасыванию исключений.

Приведем пример, демонстрирующий все описанные ситуации:

/// <summary>
/// Демонстрация проверяемых и непроверяемых преобразований.
/// Опасные проверяемые преобразования приводят к исключениям.
/// Опасные непроверяемые преобразования приводят к неверным
/// результатам, что совсем плохо.
/// </summary>
public void CheckUncheckTest() {
          x = -25 A 2; WhoIsWho("x", x);
          b = 255;
          WhoIsWho("b", b);
          // Проверяемые опасные преобразования.
          // Возникают исключения, перехватываемые catch-блоком.
          checked {
          try {
            b += 1;
          }
          catch (Exception e) {
            Console.WriteLine("Переполнение при вычислении b");
            Console.WriteLine(e);
          }
          try {
            b = (byte) x;
          }
          catch (Exception e) {
            Console.WriteLine("Переполнение при преобразовании к byte");
            Console.WriteLine(e);
          }
          // непроверяемые опасные преобразования
     unchecked {
            try {
                   b += 1;
                        WhoIsWho("b", b);
                        b = (byte) x;
                        WhoIsWho("b", b);
                        ux = (uint) x;
                        WhoIsWho("ux", x);
                        Console.WriteLine("Исключений нет, но результаты не верны!");
            }
            catch (Exception e{
                        Console.WriteLine(“Этот текст не должен появляться");
                        Console.WriteLine(e);
            }
            // автоматическая проверка преобразований в Convert
            // исключения возникают, несмотря на unchecked
            try {
                        b = Convert.ToByte(x);
            }
            catch (Exception e) {
                        Console.WriteLine("Переполнение при преобразовании к byte!");
                        Console.WriteLine(e);
            }
            try {
                        ux = Convert.ToUInt32(x);
            }
            catch (Exception e) {
                        Console.WriteLine("Потеря знака при преобразовании к uint!");
                        Console.WriteLine(e);
            }
          }
     }
}
 

Исключения и охраняемые блоки

 

В этом примере мы впервые встречаемся с охраняемыми try-блоками. Как показывает практика программирования, любая вызываемая программа не гарантирует, что в процессе ее работы не возникнут какие-либо неполадки, в результате которых она не сможет выполнить свою часть контракта. Исключения являются нормальным способом уведомления об ошибках в работе программы. Возникновение ошибки в работе программы должно приводить к выбрасыванию исключения соответствующего типа, следствием чего является прерывание нормального хода выполнения программы и передача управления обработчику исключения - стандартному или предусмотренному самой программой. Вызывающая программа должна анализировать результат, чтобы понять, была ли ошибка в работе вызванной функции и какова ее природа. При программировании в стиле C# ответственность за обнаружение ошибок лежит на вызванной программе. Она должна не только обнаружить ошибку, но и явно сообщить о ней, генерируя исключение соответствующего типа. Вызываемая программа должна попытаться исправить последствия ошибки в обработчике исключения.

В состав библиотеки NFCL входит класс Exception, свойства и методы которого позволяют работать с исключениями как с объектами, получать нужную информацию, дополнять объект собственной информацией. У класса Exception большое число потомков, каждый из которых описывает определенный тип исключения. При проектировании собственных классов можно параллельно проектировать и классы, задающие собственный тип исключений, который может выбрасываться в случае ошибок при работе методов класса. Создаваемый класс исключений должен быть потомком класса Exception.

Если в некотором модуле предполагается возможность появления исключений, то разумно предусмотреть и их обработку. В этом случае в модуле создается охраняемый try-блок, предваряемый ключевым словом try. Вслед за этим блоком следуют один или несколько блоков, обрабатывающих исключения, - catch-блоков. Каждый catch-блок имеет формальный параметр класса Exception или одного из его потомков. Если в try-блоке возникает исключение типа T, то catch-блоки начинают конкурировать в борьбе за перехват исключения. Первый по порядку catch-блок, тип формального аргумента которого согласован с типом T - совпадает с ним или является его потомком - захватывает исключение и начинает выполняться; поэтому порядок написания catch-блоков небезразличен. Вначале должны идти специализированные обработчики. Универсальным обработчиком является catch-блок с формальным параметром родового класса Exception, согласованным с исключением любого типа T. Универсальный обработчик, если он есть, стоит последним, поскольку захватывает исключение любого типа. Исключение может быть нужным образом обработано, после чего продолжится нормальный ход вычислений приложения.

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

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

 

Опасные вычисления в охраняемых проверяемых блоках

 

Такая ситуация возникает в первых двух try-блоках нашего примера. Эти блоки встроены в проверяемый checked-блок. В каждом из них используются опасные вычисления, приводящие к неверным результатам. Так, при присваивании невинного выражения b+1 из-за переполнения переменная b получает значение 0, а не 256. Поскольку вычисление находится в проверяемом блоке, то ошибка обнаруживается и результатом является вызов исключения. Далее, поскольку все это происходит в охраняемом блоке, то управление перехватывается и обрабатывается в соответствующем catch-блоке. Эту ситуацию следует отнести к нормальному, разумно построенному процессу вычислений.

 

Опасные вычисления в охраняемых непроверяемых блоках

 

Такую ситуацию демонстрирует третий try-блок нашего примера, встроенный в непроверяемый unchecked-блок. Здесь участвуют те же самые опасные вычисления, но теперь их корректность не проверяется, они не вызывают исключений, и как следствие, соответствующий catch-блок не вызывается. Результаты вычислений при этом неверны, но никаких уведомлений об этом нет. Это самая плохая ситуация, которая может случиться при работе наших программ.

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

Область действия проверки или ее отключения можно распространить и на отдельное выражение. В этом случае спецификаторы checked и unchecked предшествуют выражению, заключенному в круглые скобки. Такое выражение называется проверяемым (непроверяемым) выражением, а checked и unchecked рассматриваются как операции, допустимые в выражениях.

 

Опасные преобразования и методы класса Convert

 

Явно выполняемые преобразования по определению относятся к опасным. Явные преобразования можно выполнять по-разному. Синтаксически наиболее просто выполнить приведение типа (кастинг), явно указав тип приведения, как это сделано в только что рассмотренном примере. Но если это делается в непроверяемом блоке, последствия могут быть самыми печальными. Поэтому такой способ приведения типов следует применять с большой осторожностью. Надежнее выполнять преобразования типов более универсальным способом, используя стандартный встроенный класс Convert, специально спроектированный для этих целей.

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

На рис. 16 показаны результаты работы процедуры CheckUncheckTest. Их анализ способствует лучшему пониманию рассмотренных нами ситуаций.

Рисунок 16. Вывод на печать результатов теста CheckUncheckTest

 

 



<< ПредыдущаяОглавлениеСледующая >>