Опасные вычисления в охраняемых проверяемых блоках
Такая ситуация возникает в первых двух try-блоках нашего примера. Эти блоки встроены в проверяемый checked-блок. В каждом из них используются опасные вычисления, приводящие к неверным результатам. Так, при присваивании невинного выражения b+1 из-за переполнения переменная b получает значение 0, а не 256. Поскольку вычисление находится в проверяемом блоке, то ошибка обнаруживается и результатом является вызов исключения. Далее, поскольку все это происходит в охраняемом блоке, то управление перехватывается и обрабатывается в соответствующем catch-блоке. Эту ситуацию следует отнести к нормальному, разумно построенному процессу вычислений.
Опасные вычисления в охраняемых непроверяемых блоках
Такую ситуацию демонстрирует третий try-блок нашего примера, встроенный в непроверяемый unchecked-блок. Здесь участвуют те же самые опасные вычисления, но теперь их корректность не проверяется, они не вызывают исключений, и как следствие, соответствующий catch-блок не вызывается. Результаты вычислений при этом неверны, но никаких уведомлений об этом нет. Это самая плохая ситуация, которая может случиться при работе наших программ.
Заметьте, проверку переполнения в арифметических вычислениях можно включить не только с помощью создания checked-блоков, но и задав свойство checked проекта (по умолчанию, оно выключено). Как правило, это свойство проекта всегда включается в процессе разработки и отладки. В законченной версии проекта свойство вновь отключается, поскольку полная проверка всех преобразований требует определенных накладных расходов, увеличивая время работы; а проверяемые блоки остаются лишь там, где такой контроль действительно необходим.
Область действия проверки или ее отключения можно распространить и на отдельное выражение. В этом случае спецификаторы checked и unchecked предшествуют выражению, заключенному в круглые скобки. Такое выражение называется проверяемым (непроверяемым) выражением, а checked и unchecked рассматриваются как операции, допустимые в выражениях.
Опасные преобразования и методы класса Convert
Явно выполняемые преобразования по определению относятся к опасным. Явные преобразования можно выполнять по-разному. Синтаксически наиболее просто выполнить приведение типа - кастинг, явно указав тип приведения, как это сделано в только что рассмотренном примере. Но если это делается в непроверяемом блоке, последствия могут быть самыми печальными. Поэтому такой способ приведения типов следует применять с большой осторожностью. Надежнее выполнять преобразования типов более универсальным способом, используя стандартный встроенный класс Convert, специально спроектированный для этих целей.
В нашем примере четвертый и пятый try-блоки встроены в непроверяемый unchecked-блок. Но опасные преобразования реализуются методами класса Convert, которые сами проводят проверку и при необходимости выбрасывают исключения, что и происходит в нашем случае.
На рис. 4.5 показаны результаты работы процедуры CheckUncheckTest. Их анализ способствует лучшему пониманию рассмотренных нами ситуаций.
Рис. 4.5.Вывод на печать результатов теста CheckUncheckTest
На этом, пожалуй, пора поставить точку в обсуждении системы типов языка C#. За получением тех или иных подробностей, как всегда, следует обращаться к справочной системе.
Лекция 5. Переменные и выражения
Объявление переменных
В лекции 4 рассматривались типы языка C#. Естественным продолжением этой темы является рассмотрение переменных языка. Переменные и типы - тесно связанные понятия. С объектной точки зрения переменная - это экземпляр типа. Скалярную переменную можно рассматривать как сущность, обладающую именем, значением и типом. Имя и тип задаются при объявлении переменной и остаются неизменными на все время ее жизни. Значение переменной может меняться в ходе вычислений, эта возможность вариации значений и дала имя понятию переменная (Variable) в математике и программировании. Получение начального значения переменной называется ее инициализацией. Важной новинкой языка C# является требование обязательной инициализации переменной до начала ее использования. Попытка использовать неинициализированную переменную приводит к ошибкам, обнаруживаемым еще на этапе компиляции. Инициализация переменных, как правило, выполняется в момент объявления, хотя и может быть отложена.
Тесная связь типов и классов в языке C# обсуждалась в предыдущей лекции. Не менее тесная связь существует между переменными и объектами. Так что, когда речь идет о переменной значимого типа, то во многих ситуациях она может играть роль объекта некоторого класса. В этой лекции обсуждение будет связано со скалярными переменными встроенных типов. Все переменные, прежде чем появиться в вычислениях, должны быть объявлены. Давайте рассмотрим, как это делается в C#.
Проект Variables
Как обычно, для рассмотрения примеров построен специальный проект. В данной лекции это консольный проект с именем Variables. Построенный по умолчанию класс Class1 содержит точку входа Main. Добавленный в проект класс Testing содержит набор скалярных переменных и методов, тестирующих разные аспекты работы со скалярными переменными в C#. В процедуре Main создается объект класса Testing и поочередно вызываются его методы, каждый из которых призван проиллюстрировать те или иные моменты работы.
Синтаксис объявления
Общий синтаксис объявления сущностей в C# похож на синтаксис объявления в C++, хотя и имеет ряд отличий. Вот какова общая структура объявления:
[<атрибуты>] [<модификаторы>] <тип> <объявители>;
Об атрибутах - этой новинке языка C# - уже шла речь, о них будем говорить и в последующих лекциях курса. Модификаторы будут появляться по мере необходимости. При объявлении переменных чаще всего задаются модификаторы доступа - public, private и другие. Если атрибуты и модификаторы могут и не указываться в объявлении, то задание типа необходимо всегда. Ограничимся пока рассмотрением уже изученных встроенных типов. Когда в роли типа выступают имена типов из таблицы 3.1, это означает, что объявляются простые скалярные переменные. Структурные типы - массивы, перечисления, структуры и другие пользовательские типы - будут изучаться в последующих лекциях.
При объявлении простых переменных указывается их тип и список объявителей, где объявитель - это имя или имя с инициализацией. Список объявителей позволяет в одном объявлении задать несколько переменных одного типа. Если объявитель задается именем переменной, то имеет место объявление с отложенной инициализацией. Хороший стиль программирования предполагает задание инициализации переменной в момент ее объявления. Инициализацию можно осуществлять двояко - обычным присваиванием или в объектной манере. Во втором случае для переменной используется конструкция new и вызывается конструктор по умолчанию. Процедура SimpleVars класса Testing иллюстрирует различные способы объявления переменных и простейшие вычисления над ними:
public void SimpleVars()
{
//Объявления локальных переменных
int x, s; //без инициализации
int y =0, u = 77; //обычный способ инициализации
//допустимая инициализация
float w1=0f, w2 = 5.5f, w3 =w1+ w2 + 125.25f;
//допустимая инициализация в объектном стиле
int z= new int();
//Недопустимая инициализация.
//Конструктор с параметрами не определен
//int v = new int(77);
x=u+y; //теперь x инициализирована
if(x> 5) s = 4;
for (x=1; x<5; x++)s=5;
//Инициализация в if и for не рассматривается,
//поэтому s считается неинициализированной переменной
//Ошибка компиляции:использование неинициализированной переменной
//Console.WriteLine("s= {0}",s);
} //SimpleVars
В первой строке объявляются переменные x и s с отложенной инициализацией. Заметьте (и это важно!), что всякая попытка использовать еще не инициализированную переменную в правых частях операторов присваивания, в вызовах функций, вообще в вычислениях приводит к ошибке уже на этапе компиляции.
Последующие объявления переменных эквивалентны по сути, но демонстрируют два стиля инициализации - обычный и объектный. Обычная форма инициализации предпочтительнее не только в силу своей естественности, но она и более эффективна, поскольку в этом случае инициализирующее выражение может быть достаточно сложным, с переменными и функциями. На практике объектный стиль для скалярных переменных используется редко. Вместе с тем полезно понимать, что объявление с инициализацией int y =0 можно рассматривать как создание нового объекта ( new ) и вызова для него конструктора по умолчанию. При инициализации в объектной форме может быть вызван только конструктор по умолчанию, другие конструкторы с параметрами для встроенных типов не определены. В примере закомментировано объявление переменной v с инициализацией в объектном стиле, приводящее к ошибке, где делается попытка дать переменной значение, передавая его конструктору в качестве параметра.
Откладывать инициализацию не стоит, как показывает пример с переменной s, объявленной с отложенной инициализацией. В вычислениях она дважды получает значение: один раз в операторе if, другой - в операторе цикла for. Тем не менее, при компиляции возникнет ошибка, утверждающая, что в процедуре WriteLine делается попытка использовать неинициализированную переменную s. Связано это с тем, что для операторов if и for на этапе компиляции не вычисляются условия, зависящие от переменных. Поэтому компилятор предполагает худшее - условия ложны, инициализация s в этих операторах не происходит. А за инициализацией наш компилятор следит строго, ты так и знай!